From 7241a28393f6fe24db1b7099f7757d129f32fb54 Mon Sep 17 00:00:00 2001 From: OpenAI Date: Sat, 30 May 2026 14:03:10 +0000 Subject: [PATCH] nwconn: implement AFP DOS name reverse lookup Implement the WebSDK/NWAFP Get DOS Name From Entry ID subfunction (NCP 0x2222/35/18) as a conservative, read-only reverse lookup over mars_nwe's existing volume and AFP metadata infrastructure. The documented request carries a volume number and 32-bit Macintosh directory entry ID, and the reply returns a length-prefixed DOS path string. mars_nwe's current AFP entry IDs are not the namespace base handles maintained by namspace.c; they are mars_nwe/libatalk AFP metadata IDs cached through nwatalk. Reuse the existing volume table as the search root and nwatalk_get_entry_id() as the identity probe instead of inventing a parallel namespace handle mapping. The reverse lookup deliberately does not create fallback IDs while walking the volume. It only matches entries that already have mars_nwe or Netatalk AFP metadata, which is the normal smoke-test sequence after Get Entry ID, Get File Information, or Scan File Information has cached the target ID. This keeps the lookup read-only and avoids populating entry-id xattrs across an entire volume as a side effect. Add a Linux afp_dos_name_smoke helper and wire it into the AFP smoke suite. The helper can resolve the supplied VOL:PATH to an entry ID first, then sends the 0x12 request and verifies the returned path without the volume prefix. The suite continues to exercise the existing path-backed AFP compatibility flow before future create/rename/remove work. Tests:\n- git diff --check\n- bash -n tests/linux/afp_smoke_suite.sh\n- gcc -Iinclude -I/mnt/data/stubs -fsyntax-only tests/linux/afp_dos_name_smoke.c\n\nTODO:\n- Replace the volume walk with a real CNID/base-ID index when persistent AFP identity storage grows one.\n- Return true DOS 8.3 aliases once the AFP reverse lookup is wired to the namespace alias helpers rather than preserving the cached path component spelling. --- TODO.md | 6 + src/nwconn.c | 159 ++++++++++++++++++- tests/linux/CMakeLists.txt | 4 + tests/linux/README.md | 36 +++++ tests/linux/afp_dos_name_smoke.c | 262 +++++++++++++++++++++++++++++++ tests/linux/afp_smoke_suite.sh | 6 + 6 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 tests/linux/afp_dos_name_smoke.c diff --git a/TODO.md b/TODO.md index a2d7bc3..46df14d 100644 --- a/TODO.md +++ b/TODO.md @@ -328,6 +328,12 @@ Current status: Netatalk's `org.netatalk.*` EA abstraction. - NetWare AFP calls are NCP entry points for Mac namespace semantics on a NetWare volume, not transport-level AFP proxy calls to `afpd`. +- AFP Get DOS Name From Entry ID (0x12) is implemented as a conservative + read-only reverse lookup over the existing mars_nwe volume table and the + `nwatalk_get_entry_id()` metadata probe. It returns the DOS/NetWare path + relative to the requested volume for entries that already have a cached + mars_nwe/Netatalk AFP ID, and deliberately does not create fallback IDs while + scanning a volume. Follow-up: diff --git a/src/nwconn.c b/src/nwconn.c index 3a81844..3691d62 100644 --- a/src/nwconn.c +++ b/src/nwconn.c @@ -433,6 +433,7 @@ static const char *afp_call_name(int ufunc) case 0x0f: return("AFP 2.0 Get File Information"); case 0x10: return("AFP 2.0 Set File Information"); case 0x11: return("AFP 2.0 Scan File Information"); + case 0x12: return("AFP Get DOS Name From Entry ID"); default: return("unknown AFP call"); } } @@ -880,6 +881,156 @@ static int afp_get_entry_id_from_path_name(uint8 *afp_req, int afp_len, } +typedef struct { + uint32 target_entry_id; + char path[256]; +} AFP_DOS_NAME_SEARCH; + +static int afp_path_join(char *dst, int dst_len, const char *base, + const char *name) +{ + int used; + + if (!dst || dst_len < 1 || !base || !name) return(-1); + if (!*base) + used = slprintf(dst, dst_len, "%s", name); + else + used = slprintf(dst, dst_len, "%s/%s", base, name); + if (used < 0 || used >= dst_len) return(-1); + return(0); +} + +static int afp_find_dos_name_from_entry_id_rec(int volume, + const char *unix_dir, + const char *rel_dir, + AFP_DOS_NAME_SEARCH *search) +/* + * Reverse-map the mars_nwe AFP entry-id xattr cache back to a DOS/NetWare path. + * + * NetWare's documented AFP 0x12 call is an entry-id-only lookup. mars_nwe's + * current AFP ids are not the namespace base handles used by namspace.c; they + * are mars_nwe/libatalk AFP metadata ids. Reuse the existing volume table for + * the search root and the nwatalk entry-id helper for the per-entry identity, + * but do not create fallback ids while scanning. The first conservative smoke + * target is therefore an entry that was already cached by Get Entry ID/Get File + * Information/Scan. + */ +{ + DIR *dir; + struct dirent *de; + char unix_child[PATH_MAX]; + char rel_child[256]; + + (void)volume; + dir = opendir(unix_dir); + if (!dir) return(-0x89); /* No Search Privilege */ + + while ((de = readdir(dir)) != NULL) { + struct stat stb; + uint32 entry_id = 0; + + if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) + continue; + if (afp_path_join(unix_child, sizeof(unix_child), unix_dir, de->d_name)) + continue; + if (afp_path_join(rel_child, sizeof(rel_child), rel_dir, de->d_name)) + continue; + if (lstat(unix_child, &stb)) + continue; + + if (!nwatalk_get_entry_id(unix_child, &entry_id) + && entry_id == search->target_entry_id) { + strmaxcpy((uint8 *)search->path, rel_child, + sizeof(search->path)-1); + closedir(dir); + return(0); + } + + if (S_ISDIR(stb.st_mode)) { + int result = afp_find_dos_name_from_entry_id_rec(volume, unix_child, + rel_child, search); + if (!result || result != -0xff) { + closedir(dir); + return(result); + } + } + } + closedir(dir); + return(-0xff); /* Failure, no files found */ +} + +static int afp_find_dos_name_from_entry_id(int volume, uint32 entry_id, + char *path, int path_len) +{ + AFP_DOS_NAME_SEARCH search; + char root[PATH_MAX]; + int len; + + if (volume < 0 || volume >= used_nw_volumes || !nw_volumes[volume].unixname) + return(-0x98); /* Volume does not exist */ + if (entry_id == 1) { + if (path && path_len > 0) *path = '\0'; + return(0); + } + + len = nw_volumes[volume].unixnamlen; + if (len < 1 || len >= (int)sizeof(root)) return(-0x98); + memcpy(root, nw_volumes[volume].unixname, len); + root[len] = '\0'; + while (len > 1 && root[len-1] == '/') + root[--len] = '\0'; + + memset(&search, 0, sizeof(search)); + search.target_entry_id = entry_id; + if (!afp_find_dos_name_from_entry_id_rec(volume, root, "", &search)) { + strmaxcpy((uint8 *)path, search.path, path_len-1); + return(0); + } + return(-0xff); +} + +static int afp_get_dos_name_from_entry_id(uint8 *afp_req, int afp_len, + uint8 *response) +{ + uint8 volume_number; + uint32 entry_id; + char path[256]; + int result; + int len; + + if (afp_len < 6) { + XDPRINTF((2,0, "AFP Get DOS Name From Entry ID rejected: short request len=%d", + afp_len)); + return(-0x7e); /* NCP Boundary Check Failed */ + } + + volume_number = afp_req[1]; + entry_id = GET_BE32(afp_req + 2); + + if (!nwatalk_backend_available()) { + XDPRINTF((3,0, "AFP Get DOS Name From Entry ID rejected: libatalk backend unavailable")); + return(-0xbf); /* Invalid Namespace */ + } + + result = afp_find_dos_name_from_entry_id((int)volume_number, entry_id, + path, sizeof(path)); + if (result) { + XDPRINTF((2,0, "AFP Get DOS Name From Entry ID lookup failed: vol=%d entry=0x%08x result=-0x%x", + (int)volume_number, entry_id, -result)); + return(result); + } + + len = strlen(path); + if (len > 255) return(-0x96); /* Server out of memory / path too long */ + response[0] = (uint8)len; + memcpy(response + 1, path, len); + + XDPRINTF((3,0, "AFP Get DOS Name From Entry ID: vol=%d entry=0x%08x path='%s'", + (int)volume_number, entry_id, path)); + return(1 + len); +} + + static void afp_copy_fixed_name(uint8 *dst, int dst_len, const uint8 *src, int src_len) { @@ -3402,7 +3553,8 @@ static int handle_ncp_serv(void) * Information (0x05), Get Entry ID From * NetWare Handle (0x06), Rename (0x07), Open File Fork * (0x08), Alloc Temporary Dir Handle (0x0b), Get Entry ID - * From Path Name (0x0c), the AFP Set File Information + * From Path Name (0x0c), Get DOS Name From Entry ID + * (0x12), the AFP Set File Information * write call (0x09), the AFP 2.0 create calls * (0x0d/0x0e), Get/Set File Information (0x0f/0x10), and * Scan File Information (0x11). @@ -3461,6 +3613,11 @@ static int handle_ncp_serv(void) afp_len, responsedata); if (result > -1) data_len = result; else completition = (uint8)-result; + } else if (ufunc == 0x12) { + int result = afp_get_dos_name_from_entry_id(afp_req, + afp_len, responsedata); + if (result > -1) data_len = result; + else completition = (uint8)-result; } else if (ufunc == 0x05 || ufunc == 0x0f) { int result = afp_get_file_information(afp_req, afp_len, responsedata, diff --git a/tests/linux/CMakeLists.txt b/tests/linux/CMakeLists.txt index 825bb53..1897261 100644 --- a/tests/linux/CMakeLists.txt +++ b/tests/linux/CMakeLists.txt @@ -49,6 +49,10 @@ add_executable(afp_file_info_smoke afp_file_info_smoke.c) target_include_directories(afp_file_info_smoke PRIVATE ${NCPFS_INCLUDE_DIR}) target_link_libraries(afp_file_info_smoke ${NCPFS_LIBRARY}) +add_executable(afp_dos_name_smoke afp_dos_name_smoke.c) +target_include_directories(afp_dos_name_smoke PRIVATE ${NCPFS_INCLUDE_DIR}) +target_link_libraries(afp_dos_name_smoke ${NCPFS_LIBRARY}) + add_executable(afp_scan_info_smoke afp_scan_info_smoke.c) target_include_directories(afp_scan_info_smoke PRIVATE ${NCPFS_INCLUDE_DIR}) target_link_libraries(afp_scan_info_smoke ${NCPFS_LIBRARY}) diff --git a/tests/linux/README.md b/tests/linux/README.md index 2673c0a..c831f31 100644 --- a/tests/linux/README.md +++ b/tests/linux/README.md @@ -678,3 +678,39 @@ Entry-ID-only write semantics out of this conservative smoke path. If the server was built without the optional Netatalk/libatalk backend, use `--allow-invalid-namespace` for the expected negative test. Use `--allow-invalid-path` for path-resolution negative tests. + +## AFP Get DOS Name From Entry ID smoke test + +`afp_dos_name_smoke` exercises the WebSDK-documented NetWare AFP reverse +lookup: + +```text +NCP 0x2222/35/18 AFP Get DOS Name From Entry ID +``` + +The request carries the AFP volume number and a 32-bit Macintosh directory +entry ID. The reply is a one-byte DOS path length followed by the DOS path +string for the matching entry. The smoke helper first resolves the supplied +`VOL:PATH` through AFP Get Entry ID From Path Name when `--entry-id` is not +provided, then calls AFP Get DOS Name From Entry ID and verifies that the +returned path matches the original path without the volume prefix. + +Example: + +```sh +./afp_dos_name_smoke -S MARS -U SUPERVISOR -P secret SYS:PUBLIC/pmdflts.ini +``` + +Expected output shape: + +```text +AFP Get DOS Name From Entry ID volume=0 entry_id=0x399193ed path=PUBLIC/pmdflts.ini verified +``` + +The server implementation deliberately reuses the existing mars_nwe volume table +and the `nwatalk_get_entry_id()` metadata probe. It does not create fallback +entry IDs while walking the volume; the target entry must already have a cached +mars_nwe/Netatalk AFP ID, which is the normal state after the Entry ID, Get File +Information, or Scan File Information smoke probes. This keeps the reverse +lookup read-only and avoids populating entry-id xattrs across a whole volume as a +side effect of one DOS-name lookup. diff --git a/tests/linux/afp_dos_name_smoke.c b/tests/linux/afp_dos_name_smoke.c new file mode 100644 index 0000000..5adaa28 --- /dev/null +++ b/tests/linux/afp_dos_name_smoke.c @@ -0,0 +1,262 @@ +/* + * Linux smoke test for NetWare AFP Get DOS Name From Entry ID. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifndef NCPC_SUBFUNCTION +#define NCPC_SUBFUNCTION 0x10000 +#endif +#ifndef NCPC_SFN +#define NCPC_SFN(FN, SFN) ((FN) | ((SFN) << 8) | NCPC_SUBFUNCTION) +#endif + +#define AFP_GET_ENTRY_ID_FROM_PATH_NAME 0x0c +#define AFP_GET_DOS_NAME_FROM_ENTRY_ID 0x12 +#define NWE_INVALID_NAMESPACE 0xbf +#define NWE_INVALID_PATH 0x9c + +static void usage(const char *prog) +{ + fprintf(stderr, + "Usage: %s [--volume N] [--entry-id ID] [--expect PATH] " + "[--allow-invalid-namespace] [--allow-invalid-path] [ncpfs options] PATH\n" + "\n" + "When --entry-id is omitted, the helper first resolves PATH with AFP " + "Get Entry ID From Path Name, then calls AFP Get DOS Name From Entry ID.\n" + "\n" + "Examples:\n" + " %s -S MARS -U SUPERVISOR -P secret SYS:PUBLIC/pmdflts.ini\n" + " %s --entry-id 0x12345678 --expect PUBLIC/pmdflts.ini " + "-S MARS -U SUPERVISOR -P secret SYS:PUBLIC/pmdflts.ini\n", + prog, prog, prog); +} + +static int parse_u32(const char *text, uint32_t *value) +{ + char *end = NULL; + unsigned long v; + + errno = 0; + v = strtoul(text, &end, 0); + if (errno || !end || *end || v > 0xffffffffUL) + return -1; + *value = (uint32_t)v; + return 0; +} + +static uint32_t be32_to_cpu(const uint8_t p[4]) +{ + return ((uint32_t)p[0] << 24) | + ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | + p[3]; +} + +static void cpu_to_be32(uint32_t v, uint8_t p[4]) +{ + p[0] = (uint8_t)(v >> 24); + p[1] = (uint8_t)(v >> 16); + p[2] = (uint8_t)(v >> 8); + p[3] = (uint8_t)v; +} + +static const char *path_without_volume(const char *path) +{ + const char *colon = strchr(path, ':'); + if (colon && colon[1]) + return colon + 1; + return path; +} + +static NWCCODE get_entry_id_for_path(NWCONN_HANDLE conn, const char *path, + uint32_t *entry_id) +{ + NW_FRAGMENT reply; + uint8_t request[2 + 255]; + uint8_t reply_buf[4]; + size_t path_len = strlen(path); + NWCCODE err; + + if (path_len > 255) + return NWE_INVALID_PATH; + + request[0] = 0; /* NetWare directory handle 0, raw VOL:PATH */ + request[1] = (uint8_t)path_len; + memcpy(request + 2, 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, AFP_GET_ENTRY_ID_FROM_PATH_NAME), + request, + 2 + path_len, + &reply); + if (err) + return err; + if (reply.fragSize < 4) + return NWE_INVALID_PATH; + + *entry_id = be32_to_cpu(reply_buf); + return 0; +} + +int main(int argc, char **argv) +{ + NWCONN_HANDLE conn; + NW_FRAGMENT reply; + long init_err = 0; + const char *path = NULL; + const char *expect = NULL; + int allow_invalid_namespace = 0; + int allow_invalid_path = 0; + uint32_t volume_number = 0; + uint32_t entry_id = 0; + int have_entry_id = 0; + int i; + uint8_t request[1 + 4]; + uint8_t reply_buf[256]; + char dos_path[256]; + NWCCODE err; + + if (NWCallsInit(NULL, NULL)) { + fprintf(stderr, "NWCallsInit failed\n"); + return 2; + } + + conn = ncp_initialize(&argc, argv, 1, &init_err); + if (!conn) { + fprintf(stderr, "ncp_initialize/login failed: %ld\n", init_err); + usage(argv[0]); + return 2; + } + + for (i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--allow-invalid-namespace")) { + allow_invalid_namespace = 1; + } else if (!strcmp(argv[i], "--allow-invalid-path")) { + allow_invalid_path = 1; + } else if (!strcmp(argv[i], "--volume")) { + if (++i >= argc || parse_u32(argv[i], &volume_number) || volume_number > 255) { + fprintf(stderr, "invalid --volume value\n"); + ncp_close(conn); + return 2; + } + } else if (!strcmp(argv[i], "--entry-id")) { + if (++i >= argc || parse_u32(argv[i], &entry_id)) { + fprintf(stderr, "invalid --entry-id value\n"); + ncp_close(conn); + return 2; + } + have_entry_id = 1; + } else if (!strcmp(argv[i], "--expect")) { + if (++i >= argc) { + fprintf(stderr, "missing --expect value\n"); + ncp_close(conn); + return 2; + } + expect = argv[i]; + } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { + usage(argv[0]); + ncp_close(conn); + return 0; + } else if (!path) { + path = argv[i]; + } else { + fprintf(stderr, "unexpected argument: %s\n", argv[i]); + usage(argv[0]); + ncp_close(conn); + return 2; + } + } + + if (!path && !have_entry_id) { + fprintf(stderr, "missing PATH or --entry-id\n"); + usage(argv[0]); + ncp_close(conn); + return 2; + } + + if (!have_entry_id) { + err = get_entry_id_for_path(conn, path, &entry_id); + if (err) { + fprintf(stderr, + "AFP Get Entry ID before DOS-name lookup failed: completion=0x%02x (%u) path=%s\n", + (unsigned int)err & 0xff, (unsigned int)err, path); + ncp_close(conn); + return 1; + } + } + + request[0] = (uint8_t)volume_number; + cpu_to_be32(entry_id, request + 1); + + memset(reply_buf, 0, sizeof(reply_buf)); + reply.fragAddr.rw = reply_buf; + reply.fragSize = sizeof(reply_buf); + + err = NWRequestSimple(conn, + NCPC_SFN(0x23, AFP_GET_DOS_NAME_FROM_ENTRY_ID), + request, + sizeof(request), + &reply); + + if (((unsigned int)err & 0xff) == NWE_INVALID_NAMESPACE && allow_invalid_namespace) { + printf("AFP Get DOS Name From Entry ID returned invalid namespace as expected: volume=%u entry_id=0x%08x\n", + (unsigned int)volume_number, (unsigned int)entry_id); + ncp_close(conn); + return 0; + } + + if (((unsigned int)err & 0xff) == NWE_INVALID_PATH && allow_invalid_path) { + printf("AFP Get DOS Name From Entry ID returned invalid path as expected: volume=%u entry_id=0x%08x\n", + (unsigned int)volume_number, (unsigned int)entry_id); + ncp_close(conn); + return 0; + } + + if (err) { + fprintf(stderr, + "AFP Get DOS Name From Entry ID failed: completion=0x%02x (%u) volume=%u entry_id=0x%08x\n", + (unsigned int)err & 0xff, (unsigned int)err, + (unsigned int)volume_number, (unsigned int)entry_id); + ncp_close(conn); + return 1; + } + + if (reply.fragSize < 1 || reply_buf[0] + 1U > reply.fragSize) { + fprintf(stderr, "short AFP DOS-name reply: %zu bytes\n", reply.fragSize); + ncp_close(conn); + return 1; + } + + memcpy(dos_path, reply_buf + 1, reply_buf[0]); + dos_path[reply_buf[0]] = '\0'; + + if (!expect && path) + expect = path_without_volume(path); + if (expect && strcmp(expect, dos_path)) { + fprintf(stderr, + "AFP Get DOS Name From Entry ID verify mismatch: volume=%u entry_id=0x%08x got=%s expected=%s\n", + (unsigned int)volume_number, (unsigned int)entry_id, + dos_path, expect); + ncp_close(conn); + return 1; + } + + printf("AFP Get DOS Name From Entry ID volume=%u entry_id=0x%08x path=%s verified\n", + (unsigned int)volume_number, (unsigned int)entry_id, dos_path); + + ncp_close(conn); + return 0; +} diff --git a/tests/linux/afp_smoke_suite.sh b/tests/linux/afp_smoke_suite.sh index 5c81816..6820918 100755 --- a/tests/linux/afp_smoke_suite.sh +++ b/tests/linux/afp_smoke_suite.sh @@ -179,6 +179,7 @@ emit "mtime_epoch=$TIMESTAMP_EPOCH" for helper in \ afp_entry_id_smoke \ afp_file_info_smoke \ + afp_dos_name_smoke \ afp_scan_info_smoke \ afp_temp_dir_handle_smoke \ afp_open_file_fork_smoke \ @@ -219,6 +220,11 @@ run_cmd \ "./afp_file_info_smoke $COMMON_PRINT '$NETWARE_PATH'" \ "$SCRIPT_DIR/afp_file_info_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$NETWARE_PATH" +run_cmd \ + "AFP Get DOS Name From Entry ID" \ + "./afp_dos_name_smoke $COMMON_PRINT '$NETWARE_PATH'" \ + "$SCRIPT_DIR/afp_dos_name_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$NETWARE_PATH" + run_cmd \ "AFP 2.0 Scan File Information" \ "./afp_scan_info_smoke $COMMON_PRINT '$DIR_PATH'" \