diff --git a/TODO.md b/TODO.md index 80ac8d4..a66a339 100644 --- a/TODO.md +++ b/TODO.md @@ -217,6 +217,12 @@ Current status: coverage uses `tests/linux/afp_entry_id_smoke --from-handle` and has been verified against `SYS:PUBLIC/pmdflts.ini` and `SYS:PUBLIC/ohlogscr.bat`, returning volume 0, `fork=0`, and stat-derived fallback Entry IDs for now. +- `AFP Alloc Temporary Directory Handle` is implemented for the same + path-backed smoke subset. Linux smoke coverage exists in + `tests/linux/afp_temp_dir_handle_smoke`; it returns a temporary NetWare + directory handle plus effective-rights mask and immediately deallocates the + handle in the same connection. Entry-ID-only allocation remains TODO until + persistent CNID/base-ID lookup exists. - The AFP dispatcher now decodes the WebSDK/NWAFP subfunction number in diagnostics so real client probes can be mapped to the corresponding AFP call before implementation work starts. diff --git a/src/nwconn.c b/src/nwconn.c index 2ac6e05..66be929 100644 --- a/src/nwconn.c +++ b/src/nwconn.c @@ -611,6 +611,68 @@ static int afp_get_entry_id_from_netware_handle(uint8 *afp_req, int afp_len, return(6); } +static int afp_alloc_temporary_dir_handle(uint8 *afp_req, int afp_len, + uint8 *response) +/* + * WebSDK / nwafp.h call 0x0b allocates a temporary NetWare directory handle + * from AFP namespace input: volume number, AFP Entry ID, and optional AFP-style + * path. Until a persistent CNID/base-ID map exists, accept the same raw + * SYS:-style path-backed subset used by the other implemented AFP probes and + * delegate the final handle allocation to the existing NetWare directory-handle + * table. + */ +{ + uint8 volume_number; + uint32 request_entry_id; + int path_len; + int eff_rights = 0; + int dirhandle; + + if (afp_len < 7) { + XDPRINTF((2,0, "AFP Alloc Temporary Dir Handle rejected: short request len=%d", + afp_len)); + return(-0x7e); /* NCP Boundary Check Failed */ + } + + volume_number = afp_req[1]; + request_entry_id = GET_BE32(afp_req + 2); + path_len = (int)afp_req[6]; + if (path_len < 0 || afp_len < 7 + path_len) { + XDPRINTF((2,0, "AFP Alloc Temporary Dir Handle rejected: boundary check len=%d path_len=%d", + afp_len, path_len)); + return(-0x7e); + } + + if (!nwatalk_backend_available()) { + XDPRINTF((3,0, "AFP Alloc Temporary Dir Handle rejected: libatalk backend unavailable")); + return(-0xbf); /* invalid namespace */ + } + + if (!path_len) { + XDPRINTF((2,0, "AFP Alloc Temporary Dir Handle rejected: entry-id-only lookup unsupported vol=%d entry=0x%08x", + (int)volume_number, request_entry_id)); + return(-0x9c); /* Invalid Path until persistent entry-id lookup exists */ + } + + dirhandle = nw_alloc_dir_handle(0, afp_req + 7, path_len, 0, 1, + (int)(ncprequest->task), &eff_rights); + if (dirhandle < 0) { + XDPRINTF((2,0, "AFP Alloc Temporary Dir Handle path resolve failed: vol=%d entry=0x%08x path='%s' result=-0x%x", + (int)volume_number, request_entry_id, + visable_data(afp_req + 7, path_len), -dirhandle)); + return(dirhandle); + } + + response[0] = (uint8)dirhandle; + response[1] = (uint8)eff_rights; + + XDPRINTF((3,0, "AFP Alloc Temporary Dir Handle: vol=%d entry=0x%08x path='%s' dir_handle=%d rights=0x%02x", + (int)volume_number, request_entry_id, + visable_data(afp_req + 7, path_len), dirhandle, eff_rights)); + return(2); +} + + static int afp_get_entry_id_from_path_name(uint8 *afp_req, int afp_len, uint8 *response) { @@ -2969,7 +3031,10 @@ static int handle_ncp_serv(void) * lookup exists, support the same path-backed SYS:-style * smoke-test subset. Get Entry ID From NetWare Handle * maps an already-open mars_nwe file handle back to its - * Unix path and returns the corresponding AFP ID. Then expose + * Unix path and returns the corresponding AFP ID. Alloc + * Temporary Dir Handle uses the same path-backed subset and + * returns a connection-local NetWare directory handle plus + * effective rights. Then expose * the read-only AFP Get File Information query for the same * SYS:-style path inputs. AFP 2.0 Get File Information * uses the same request/reply layout for this read-only @@ -2989,6 +3054,11 @@ static int handle_ncp_serv(void) afp_len, responsedata); if (result > -1) data_len = result; else completition = (uint8)-result; + } else if (ufunc == 0x0b) { + int result = afp_alloc_temporary_dir_handle(afp_req, + afp_len, responsedata); + if (result > -1) data_len = result; + else completition = (uint8)-result; } else if (ufunc == 0x0c) { int result = afp_get_entry_id_from_path_name(afp_req, afp_len, responsedata); diff --git a/tests/linux/CMakeLists.txt b/tests/linux/CMakeLists.txt index a64edbc..f434786 100644 --- a/tests/linux/CMakeLists.txt +++ b/tests/linux/CMakeLists.txt @@ -29,3 +29,7 @@ target_link_libraries(afp_file_info_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}) + +add_executable(afp_temp_dir_handle_smoke afp_temp_dir_handle_smoke.c) +target_include_directories(afp_temp_dir_handle_smoke PRIVATE ${NCPFS_INCLUDE_DIR}) +target_link_libraries(afp_temp_dir_handle_smoke ${NCPFS_LIBRARY}) diff --git a/tests/linux/README.md b/tests/linux/README.md index d9276eb..0fadb6d 100644 --- a/tests/linux/README.md +++ b/tests/linux/README.md @@ -14,6 +14,7 @@ cmake -DMARS_NWE_BUILD_LINUX_TESTS=ON ... cmake --build . --target afp_entry_id_smoke cmake --build . --target afp_file_info_smoke cmake --build . --target afp_scan_info_smoke +cmake --build . --target afp_temp_dir_handle_smoke ``` ## AFP Entry ID smoke test @@ -136,6 +137,47 @@ CNID/AppleDouble/libatalk-backed identity, parent Entry ID derivation, and AFP resource-fork handle semantics remain future Mac-namespace work; the current smoke coverage only verifies the conservative read-only data-fork mapping. +## AFP Alloc Temporary Directory Handle smoke test + +`afp_temp_dir_handle_smoke` sends the WebSDK-documented NetWare AFP request: + +```text +NCP 0x2222/35/11 AFP Alloc Temporary Directory Handle +``` + +The request layout is the AFP volume number, base AFP Entry ID, path length, +and AFP-style path. The current mars_nwe implementation supports the same +conservative path-backed subset as the Entry ID and File Information probes: +pass a raw `SYS:`-style path and keep volume/base Entry ID at zero. Pure +Entry-ID-relative allocation is still rejected with Invalid Path until +persistent CNID/base-ID lookup exists. + +Useful smoke cases for a standard MARS-NWE `SYS` volume are: + +```sh +./tests/linux/afp_temp_dir_handle_smoke -S MARS -U SUPERVISOR -P secret SYS: +./tests/linux/afp_temp_dir_handle_smoke -S MARS -U SUPERVISOR -P secret SYS:PUBLIC +./tests/linux/afp_temp_dir_handle_smoke -S MARS -U SUPERVISOR -P secret SYS:SYSTEM +./tests/linux/afp_temp_dir_handle_smoke -S MARS -U SUPERVISOR -P secret SYS:BURST +``` + +A successful reply prints the allocated temporary NetWare directory handle and +the effective-rights mask returned by the server. The smoke helper immediately +deallocates the handle with the normal NetWare Deallocate Directory Handle call +before closing the connection, so the handle value is only useful inside that +client connection and must not be copied into later tests or server logs. + +Example output and server diagnostic shape: + +```text +AFP Alloc Temporary Dir Handle path=SYS:PUBLIC dir_handle=2 rights=0xff +AFP Alloc Temporary Dir Handle: vol=0 entry=0x00000000 path='SYS:PUBLIC' dir_handle=2 rights=0xff +``` + +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 File Information smoke test `afp_file_info_smoke` sends the WebSDK-documented NetWare AFP file diff --git a/tests/linux/afp_temp_dir_handle_smoke.c b/tests/linux/afp_temp_dir_handle_smoke.c new file mode 100644 index 0000000..ab6451b --- /dev/null +++ b/tests/linux/afp_temp_dir_handle_smoke.c @@ -0,0 +1,208 @@ +/* + * Linux smoke test for NetWare AFP Alloc Temporary Directory Handle. + * + * This uses ncpfs/libncp so the request travels through the same NCP path as a + * normal Linux requester. The server-side AFP endpoint returns a temporary + * NetWare directory handle and an effective-rights mask for a path-backed AFP + * request. + */ + +#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_ALLOC_TEMPORARY_DIR_HANDLE 0x0b +#define NWE_INVALID_NAMESPACE 0xbf +#define NWE_INVALID_PATH 0x9c +#define AFP_TEMP_DH_NONE 0xff + +static void usage(const char *prog) +{ + fprintf(stderr, + "Usage: %s [--allow-invalid-namespace] [--allow-invalid-path] " + "[--volume N] [--entry-id ID] [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 --allow-invalid-namespace -S MARS -U SUPERVISOR -P secret SYS:PUBLIC\n" + " %s --allow-invalid-path -S MARS -U SUPERVISOR -P secret SYS:NO_SUCH_PATH\n", + prog, 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 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 void deallocate_dir_handle(NWCONN_HANDLE conn, unsigned int dir_handle) +{ + uint8_t rq[1]; + + if (dir_handle == AFP_TEMP_DH_NONE) + return; + + rq[0] = (uint8_t)dir_handle; + (void)NWRequestSimple(conn, NCPC_SFN(0x16, 0x14), rq, sizeof(rq), NULL); +} + +int main(int argc, char **argv) +{ + NWCONN_HANDLE conn; + NW_FRAGMENT reply; + long init_err = 0; + const char *path = NULL; + int allow_invalid_namespace = 0; + int allow_invalid_path = 0; + uint32_t volume_number = 0; + uint32_t base_entry_id = 0; + unsigned int allocated_dir_handle = AFP_TEMP_DH_NONE; + int i; + size_t path_len; + uint8_t request[1 + 4 + 1 + 255]; + uint8_t reply_buf[2]; + 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], &base_entry_id)) { + fprintf(stderr, "invalid --entry-id value\n"); + ncp_close(conn); + return 2; + } + } 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) { + fprintf(stderr, "missing PATH\n"); + usage(argv[0]); + ncp_close(conn); + return 2; + } + + path_len = strlen(path); + if (path_len > 255) { + fprintf(stderr, + "PATH is too long for AFP Alloc Temporary Directory Handle: %zu\n", + path_len); + ncp_close(conn); + return 2; + } + + request[0] = (uint8_t)volume_number; + cpu_to_be32(base_entry_id, request + 1); + request[5] = (uint8_t)path_len; + memcpy(request + 6, 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_ALLOC_TEMPORARY_DIR_HANDLE), + request, + 6 + path_len, + &reply); + + if (((unsigned int)err & 0xff) == NWE_INVALID_NAMESPACE && + allow_invalid_namespace) { + printf("AFP Alloc Temporary Dir Handle returned invalid namespace " + "as expected: path=%s\n", path); + ncp_close(conn); + return 0; + } + + if (((unsigned int)err & 0xff) == NWE_INVALID_PATH && allow_invalid_path) { + printf("AFP Alloc Temporary Dir Handle returned invalid path " + "as expected: path=%s\n", path); + ncp_close(conn); + return 0; + } + + if (err) { + fprintf(stderr, + "AFP Alloc Temporary Dir Handle failed: completion=0x%02x (%u) path=%s\n", + (unsigned int)err & 0xff, (unsigned int)err, path); + ncp_close(conn); + return 1; + } + + if (reply.fragSize < 2) { + fprintf(stderr, "short AFP reply: %zu bytes\n", reply.fragSize); + ncp_close(conn); + return 1; + } + + allocated_dir_handle = reply_buf[0]; + printf("AFP Alloc Temporary Dir Handle path=%s dir_handle=%u rights=0x%02x\n", + path, allocated_dir_handle, (unsigned int)reply_buf[1]); + + deallocate_dir_handle(conn, allocated_dir_handle); + ncp_close(conn); + return 0; +}