nwconn: route AFP scan file information
All checks were successful
Source release / source-package (push) Successful in 49s

Add the older WebSDK/NWAFP AFP Scan File Information subfunction 0x0a to the AFP dispatcher and route it through the existing conservative directory-scan implementation used by AFP 2.0 Scan File Information 0x11.

The Micro Focus NCP documentation describes both scan variants as read-only directory/file information scans with a Mac base Entry ID, Mac last-seen ID, DesiredResponseCount, SearchBitMap, RequestBitMap, and path modifier.  The mars_nwe compatibility subset still requires a raw path-backed VOL:-style request because persistent CNID/base-ID lookup is not available yet, but accepting 0x0a lets older AFP callers probe the same read-only semantics instead of receiving Invalid Namespace for an otherwise implemented scan shape.

Teach the scan parser to accept the documented DesiredResponseCount word while keeping compatibility with the earlier compact smoke-test layout that omitted it.  For now DesiredResponseCount is logged and constrained by the smoke helper, while the server still returns one conservative file-info record plus the next-last-seen entry id per request.  This avoids pretending to implement full multi-response AFP directory scans before CNID-backed ordering, response-count batching, and AppleDouble metadata are available.

Extend afp_scan_info_smoke with --afp10/--afp20 selection and a documented --desired-count argument.  The helper now sends the WebSDK-style request layout by default, uses 0x11 unless --afp10 is requested, and keeps the existing next_last_seen output used for iterative Linux smoke tests.

Tests:

- git diff --check

- gcc -fsyntax-only tests/linux/afp_scan_info_smoke.c with local ncpfs header stubs

- cmake --build build-off --target nwconn with ENABLE_NETATALK_LIBATALK=OFF

- cmake --build build-on --target nwconn with ENABLE_NETATALK_LIBATALK=ON against Netatalk 4.4.3 headers and local link stubs

TODO:

- Implement persistent CNID/base-ID lookup so entry-id-only scans can work without a raw path.

- Replace the one-record smoke response with true DesiredResponseCount batching once stable CNID ordering and full AFP scan filtering are available.

- Fill richer AppleDouble/Finder/ProDOS/resource-fork metadata when the libatalk backend grows write-safe metadata support.
This commit is contained in:
Mario Fetka
2026-05-30 08:35:17 +00:00
parent 00060e0e93
commit 8e739a1ac2
4 changed files with 106 additions and 48 deletions

10
TODO.md
View File

@@ -207,10 +207,12 @@ Current status:
`--afp20` against the same paths, using the same path-backed read-only reply
for now. The current reply fills stat/libatalk-derived fields and leaves
persistent CNID Parent ID / fuller Mac namespace metadata as future work.
- `AFP 2.0 Scan File Information` is implemented for path-backed read-only
directory scans. Linux smoke coverage exists in
`tests/linux/afp_scan_info_smoke` and has been verified against `SYS:PUBLIC`
by walking multiple directory entries with the returned `next_last_seen` AFP
- `AFP Scan File Information` (`0x0a`) and `AFP 2.0 Scan File Information`
(`0x11`) are implemented for path-backed read-only directory scans. Linux
smoke coverage exists in `tests/linux/afp_scan_info_smoke`; the helper
defaults to the AFP 2.0 subfunction and uses `--afp10` for the older
endpoint. Runtime coverage has been verified against `SYS:PUBLIC` by
walking multiple directory entries with the returned `next_last_seen` AFP
Entry ID continuation value.
- `AFP Get Entry ID From NetWare Handle` is implemented for read-only data-fork
file handles that are opened in the same client connection. Linux smoke

View File

@@ -423,6 +423,7 @@ static const char *afp_call_name(int ufunc)
case 0x06: return("AFP Get Entry ID From NetWare Handle");
case 0x07: return("AFP Rename");
case 0x08: return("AFP Open File Fork");
case 0x0a: return("AFP Scan File Information");
case 0x0b: return("AFP Alloc Temporary Dir Handle");
case 0x0c: return("AFP Get Entry ID From Path Name");
case 0x0d: return("AFP 2.0 Create Directory");
@@ -1082,9 +1083,11 @@ static int afp_scan_file_information(uint8 *afp_req, int afp_len,
uint8 volume_number;
uint32 request_entry_id;
uint32 last_seen_id;
uint16 desired_count;
uint16 search_mask;
uint16 request_mask;
int path_len;
int path_off;
int volume;
char unixname[PATH_MAX];
char childname[PATH_MAX];
@@ -1106,12 +1109,31 @@ static int afp_scan_file_information(uint8 *afp_req, int afp_len,
volume_number = afp_req[1];
request_entry_id = GET_BE32(afp_req + 2);
last_seen_id = GET_BE32(afp_req + 6);
search_mask = GET_BE16(afp_req + 10);
request_mask = GET_BE16(afp_req + 12);
path_len = (int)afp_req[14];
if (path_len < 0 || afp_len < 15 + path_len) {
XDPRINTF((2,0, "%s rejected: boundary check len=%d path_len=%d",
call_name, afp_len, path_len));
/*
* WebSDK documents AFP Scan File Information (0x0a) and AFP 2.0 Scan
* File Information (0x11) with a DesiredResponseCount word between
* MacLastSeenID and SearchBitMap. Earlier smoke coverage used a compact
* internal layout without that word. Accept both so existing probes keep
* working, while new tests exercise the documented header layout.
*/
if (afp_len >= 17 && afp_len >= 17 + (int)afp_req[16]) {
desired_count = GET_BE16(afp_req + 10);
search_mask = GET_BE16(afp_req + 12);
request_mask = GET_BE16(afp_req + 14);
path_len = (int)afp_req[16];
path_off = 17;
} else {
desired_count = 1;
search_mask = GET_BE16(afp_req + 10);
request_mask = GET_BE16(afp_req + 12);
path_len = (int)afp_req[14];
path_off = 15;
}
if (path_len < 0 || afp_len < path_off + path_len) {
XDPRINTF((2,0, "%s rejected: boundary check len=%d path_len=%d path_off=%d",
call_name, afp_len, path_len, path_off));
return(-0x7e);
}
@@ -1128,11 +1150,11 @@ static int afp_scan_file_information(uint8 *afp_req, int afp_len,
}
volume = conn_get_kpl_unxname(unixname, sizeof(unixname), 0,
afp_req + 15, path_len);
afp_req + path_off, path_len);
if (volume < 0) {
XDPRINTF((2,0, "%s path resolve failed: vol=%d entry=0x%08x last=0x%08x path='%s' result=-0x%x",
call_name, (int)volume_number, request_entry_id, last_seen_id,
visable_data(afp_req + 15, path_len), -volume));
visable_data(afp_req + path_off, path_len), -volume));
return(volume);
}
@@ -1140,7 +1162,7 @@ static int afp_scan_file_information(uint8 *afp_req, int afp_len,
if (!dir) {
XDPRINTF((2,0, "%s opendir failed: vol=%d entry=0x%08x last=0x%08x path='%s' unix='%s' errno=%d",
call_name, (int)volume_number, request_entry_id, last_seen_id,
visable_data(afp_req + 15, path_len), unixname, errno));
visable_data(afp_req + path_off, path_len), unixname, errno));
return(-0x9c); /* Invalid Path */
}
@@ -1170,7 +1192,7 @@ static int afp_scan_file_information(uint8 *afp_req, int afp_len,
}
display_path_len = snprintf((char *)display_path, sizeof(display_path),
"%s%c%s", visable_data(afp_req + 15, path_len),
"%s%c%s", visable_data(afp_req + path_off, path_len),
':', de->d_name);
if (display_path_len < 0 || display_path_len >= (int)sizeof(display_path)) {
display_path_len = strlen(de->d_name);
@@ -1186,18 +1208,20 @@ static int afp_scan_file_information(uint8 *afp_req, int afp_len,
continue;
U32_TO_BE32(entry_id ? entry_id : child_entry_id, response);
closedir(dir);
XDPRINTF((3,0, "%s: vol=%d entry=0x%08x last=0x%08x mask=0x%04x req=0x%04x path='%s' reply_entry=0x%08x%s",
XDPRINTF((3,0, "%s: vol=%d entry=0x%08x last=0x%08x desired=%u mask=0x%04x req=0x%04x path='%s' reply_entry=0x%08x%s",
call_name, (int)volume_number, request_entry_id, last_seen_id,
search_mask, request_mask, visable_data(afp_req + 15, path_len),
(unsigned)desired_count, search_mask, request_mask,
visable_data(afp_req + path_off, path_len),
entry_id ? entry_id : child_entry_id,
(fallback || child_fallback) ? " fallback" : ""));
return(4 + 120);
}
closedir(dir);
XDPRINTF((3,0, "%s completed: vol=%d entry=0x%08x last=0x%08x path='%s' no more entries",
XDPRINTF((3,0, "%s completed: vol=%d entry=0x%08x last=0x%08x desired=%u path='%s' no more entries",
call_name, (int)volume_number, request_entry_id, last_seen_id,
visable_data(afp_req + 15, path_len)));
(unsigned)desired_count,
visable_data(afp_req + path_off, path_len)));
return(-0xff); /* No files found / scan complete */
}
@@ -3158,7 +3182,10 @@ static int handle_ncp_serv(void)
* maps an already-open mars_nwe file handle back to its
* Unix path and returns the corresponding AFP ID. Open
* File Fork opens the same path-backed subset as a read-only
* data fork and returns a normal NetWare file handle. Alloc
* data fork and returns a normal NetWare file handle. The
* older AFP Scan File Information (0x0a) now shares the
* same conservative directory-scan helper as the AFP 2.0
* scan call. Alloc
* Temporary Dir Handle uses the same path-backed subset and
* returns a connection-local NetWare directory handle plus
* effective rights. Then expose
@@ -3202,7 +3229,7 @@ static int handle_ncp_serv(void)
afp_call_name(ufunc));
if (result > -1) data_len = result;
else completition = (uint8)-result;
} else if (ufunc == 0x11) {
} else if (ufunc == 0x0a || ufunc == 0x11) {
int result = afp_scan_file_information(afp_req,
afp_len, responsedata,
afp_call_name(ufunc));

View File

@@ -300,30 +300,38 @@ If the server was built without the optional Netatalk/libatalk backend, use
## AFP Scan File Information smoke test
`afp_scan_info_smoke` sends the WebSDK-documented NetWare AFP scan request:
`afp_scan_info_smoke` sends the WebSDK-documented NetWare AFP scan requests:
```text
NCP 0x2222/35/10 AFP Scan File Information
NCP 0x2222/35/17 AFP 2.0 Scan File Information
```
It scans one directory entry per request using the same read-only AFP file
information record as `afp_file_info_smoke`. The test sends raw `SYS:`-style
path requests with directory handle 0 and uses the returned `next_last_seen` AFP
Entry ID as the continuation token for the next call.
The helper defaults to the AFP 2.0 subfunction (`0x11`) and uses `--afp10`
to exercise the older `0x0a` endpoint. Both variants include the documented
DesiredResponseCount word; mars_nwe currently returns one path-backed read-only
entry per request, using the same AFP file information record as
`afp_file_info_smoke`. The test sends raw `SYS:`-style path requests with
directory handle 0 and uses the returned `next_last_seen` AFP Entry ID as the
continuation token for the next call.
Useful smoke sequence for a standard MARS-NWE `SYS:PUBLIC` directory:
```sh
./tests/linux/afp_scan_info_smoke -S MARS -U SUPERVISOR -P secret SYS:PUBLIC
./tests/linux/afp_scan_info_smoke --afp10 -S MARS -U SUPERVISOR -P secret SYS:PUBLIC
./tests/linux/afp_scan_info_smoke -S MARS -U SUPERVISOR -P secret --last-seen 0x23c8787d SYS:PUBLIC
./tests/linux/afp_scan_info_smoke --allow-empty -S MARS -U SUPERVISOR -P secret --last-seen 0x260437f6 SYS:PUBLIC
```
The concrete Entry IDs vary by filesystem metadata and are currently marked as
`fallback` in server diagnostics when they are derived from `stat(2)` rather
than persistent CNID/AppleDouble metadata. The verified smoke path walks
multiple entries in `SYS:PUBLIC` by feeding each returned `next_last_seen` value
into the next request.
than persistent CNID/AppleDouble metadata. The verified AFP 2.0 smoke path
walks multiple entries in `SYS:PUBLIC` by feeding each returned
`next_last_seen` value into the next request. The `0x0a` path intentionally
shares that conservative scan implementation so older AFP callers can probe the
same read-only directory listing semantics before fuller multi-response and
CNID-backed scans are implemented.
If the server was built without the optional Netatalk/libatalk backend, use
`--allow-invalid-namespace` for the expected negative test. Use

View File

@@ -1,5 +1,5 @@
/*
* Linux smoke test for NetWare AFP 2.0 Scan File Information.
* Linux smoke test for NetWare AFP Scan File Information.
*/
#include <errno.h>
@@ -19,6 +19,7 @@
#define NCPC_SFN(FN, SFN) ((FN) | ((SFN) << 8) | NCPC_SUBFUNCTION)
#endif
#define AFP_SCAN_FILE_INFORMATION 0x0a
#define AFP20_SCAN_FILE_INFORMATION 0x11
#define NWE_INVALID_NAMESPACE 0xbf
#define NWE_INVALID_PATH 0x9c
@@ -30,17 +31,19 @@
static void usage(const char *prog)
{
fprintf(stderr,
"Usage: %s [--allow-invalid-namespace] [--allow-invalid-path] [--allow-empty] "
"[--volume N] [--entry-id ID] [--last-seen ID] [--search-mask MASK] "
"[--request-mask MASK] [ncpfs options] PATH\n"
"Usage: %s [--afp10|--afp20] [--allow-invalid-namespace] "
"[--allow-invalid-path] [--allow-empty] [--volume N] "
"[--entry-id ID] [--last-seen ID] [--desired-count N] "
"[--search-mask MASK] [--request-mask MASK] [ncpfs options] PATH\n"
"\n"
"ncpfs options are parsed by ncp_initialize(), for example:\n"
" -S SERVER -U USER -P PASSWORD -n\n"
"\n"
"Examples:\n"
" %s -S MARS -U SUPERVISOR -P secret SYS:PUBLIC\n"
" %s --afp10 -S MARS -U SUPERVISOR -P secret SYS:PUBLIC\n"
" %s --allow-empty -S MARS -U SUPERVISOR -P secret SYS:EMPTYDIR\n",
prog, prog, prog);
prog, prog, prog, prog);
}
static int parse_u32(const char *text, uint32_t *value)
@@ -107,11 +110,13 @@ int main(int argc, char **argv)
uint32_t volume_number = 0;
uint32_t entry_id = 0;
uint32_t last_seen_id = 0;
uint32_t desired_count = 1;
uint32_t search_mask = AFP_SEARCH_ALL;
uint32_t request_mask = AFP_GET_ALL;
uint32_t afp_subfunction = AFP20_SCAN_FILE_INFORMATION;
int i;
size_t path_len;
uint8_t request[1 + 4 + 4 + 2 + 2 + 1 + 255];
uint8_t request[1 + 4 + 4 + 2 + 2 + 2 + 1 + 255];
uint8_t reply_buf[AFP_SCAN_REPLY_LEN];
char long_name[33];
char short_name[13];
@@ -130,7 +135,11 @@ int main(int argc, char **argv)
}
for (i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--allow-invalid-namespace")) {
if (!strcmp(argv[i], "--afp10")) {
afp_subfunction = AFP_SCAN_FILE_INFORMATION;
} else if (!strcmp(argv[i], "--afp20")) {
afp_subfunction = AFP20_SCAN_FILE_INFORMATION;
} else if (!strcmp(argv[i], "--allow-invalid-namespace")) {
allow_invalid_namespace = 1;
} else if (!strcmp(argv[i], "--allow-invalid-path")) {
allow_invalid_path = 1;
@@ -154,6 +163,12 @@ int main(int argc, char **argv)
ncp_close(conn);
return 2;
}
} else if (!strcmp(argv[i], "--desired-count")) {
if (++i >= argc || parse_u32(argv[i], &desired_count) || desired_count > 4) {
fprintf(stderr, "invalid --desired-count value\n");
ncp_close(conn);
return 2;
}
} else if (!strcmp(argv[i], "--search-mask")) {
if (++i >= argc || parse_u32(argv[i], &search_mask) || search_mask > 0xffff) {
fprintf(stderr, "invalid --search-mask value\n");
@@ -198,40 +213,44 @@ int main(int argc, char **argv)
request[0] = (uint8_t)volume_number;
cpu_to_be32(entry_id, request + 1);
cpu_to_be32(last_seen_id, request + 5);
cpu_to_be16((uint16_t)search_mask, request + 9);
cpu_to_be16((uint16_t)request_mask, request + 11);
request[13] = (uint8_t)path_len;
memcpy(request + 14, path, path_len);
cpu_to_be16((uint16_t)desired_count, request + 9);
cpu_to_be16((uint16_t)search_mask, request + 11);
cpu_to_be16((uint16_t)request_mask, request + 13);
request[15] = (uint8_t)path_len;
memcpy(request + 16, path, path_len);
memset(reply_buf, 0, sizeof(reply_buf));
reply.fragAddr.rw = reply_buf;
reply.fragSize = sizeof(reply_buf);
err = NWRequestSimple(conn,
NCPC_SFN(0x23, AFP20_SCAN_FILE_INFORMATION),
NCPC_SFN(0x23, (int)afp_subfunction),
request,
14 + path_len,
16 + path_len,
&reply);
if (err == NWE_INVALID_NAMESPACE && allow_invalid_namespace) {
printf("AFP Scan File Information returned invalid namespace as expected for path=%s\n", path);
printf("AFP Scan File Information subfunction=0x%02x returned invalid namespace as expected for path=%s\n",
(unsigned int)afp_subfunction, path);
ncp_close(conn);
return 0;
}
if (err == NWE_INVALID_PATH && allow_invalid_path) {
printf("AFP Scan File Information returned invalid path as expected for path=%s\n", path);
printf("AFP Scan File Information subfunction=0x%02x returned invalid path as expected for path=%s\n",
(unsigned int)afp_subfunction, path);
ncp_close(conn);
return 0;
}
if (err == NWE_NO_FILES_FOUND && allow_empty) {
printf("AFP Scan File Information returned no files as expected for path=%s last_seen=0x%08x\n",
path, last_seen_id);
printf("AFP Scan File Information subfunction=0x%02x returned no files as expected for path=%s last_seen=0x%08x\n",
(unsigned int)afp_subfunction, path, last_seen_id);
ncp_close(conn);
return 0;
}
if (err) {
fprintf(stderr,
"AFP Scan File Information failed: completion=0x%02x (%u) path=%s last_seen=0x%08x\n",
(unsigned int)err & 0xff, (unsigned int)err, path, last_seen_id);
"AFP Scan File Information subfunction=0x%02x failed: completion=0x%02x (%u) path=%s last_seen=0x%08x\n",
(unsigned int)afp_subfunction, (unsigned int)err & 0xff,
(unsigned int)err, path, last_seen_id);
ncp_close(conn);
return 1;
}
@@ -245,11 +264,13 @@ int main(int argc, char **argv)
copy_fixed_string(long_name, sizeof(long_name), reply_buf + 4 + 64, 32);
copy_fixed_string(short_name, sizeof(short_name), reply_buf + 4 + 100, 12);
printf("AFP Scan File Info path=%s last_seen=0x%08x next_last_seen=0x%08x "
printf("AFP Scan File Info subfunction=0x%02x path=%s last_seen=0x%08x desired=%u next_last_seen=0x%08x "
"entry_id=0x%08x parent_id=0x%08x attrs=0x%04x data_len=%u "
"resource_len=%u offspring=%u long_name=%s short_name=%s rights=0x%04x\n",
(unsigned int)afp_subfunction,
path,
last_seen_id,
(unsigned int)desired_count,
be32_to_cpu(reply_buf + 0),
be32_to_cpu(reply_buf + 4),
be32_to_cpu(reply_buf + 8),