diff --git a/TODO.md b/TODO.md index 5967beb..fa88067 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/src/nwconn.c b/src/nwconn.c index 4dcb680..ce2b496 100644 --- a/src/nwconn.c +++ b/src/nwconn.c @@ -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)); diff --git a/tests/linux/README.md b/tests/linux/README.md index a1d2854..d735bf2 100644 --- a/tests/linux/README.md +++ b/tests/linux/README.md @@ -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 diff --git a/tests/linux/afp_scan_info_smoke.c b/tests/linux/afp_scan_info_smoke.c index 37bcb9c..9dd01cc 100644 --- a/tests/linux/afp_scan_info_smoke.c +++ b/tests/linux/afp_scan_info_smoke.c @@ -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 @@ -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),