diff --git a/TODO.md b/TODO.md index 5ca30ce..8238a96 100644 --- a/TODO.md +++ b/TODO.md @@ -217,6 +217,13 @@ 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 Open File Fork` is implemented for the same path-backed smoke subset. + It opens only the data fork read-only and returns a normal six-byte NetWare + file handle plus the current data-fork length; the Linux smoke helper + `tests/linux/afp_open_file_fork_smoke` closes the returned handle in the same + connection. Resource-fork opens, write access, and Entry-ID-only lookup stay + TODO until AppleDouble/resource-fork and persistent CNID/base-ID semantics are + available. - `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`; runtime smoke coverage is green for diff --git a/src/nwconn.c b/src/nwconn.c index 9fcac10..051b269 100644 --- a/src/nwconn.c +++ b/src/nwconn.c @@ -611,6 +611,94 @@ static int afp_get_entry_id_from_netware_handle(uint8 *afp_req, int afp_len, return(6); } + +static int afp_open_file_fork(uint8 *afp_req, int afp_len, + uint8 *response) +/* + * WebSDK / nwafp.h call 0x08 opens a NetWare file handle for an AFP file + * fork. The full AFP semantics include data/resource fork selection and + * CNID-relative path lookup. Keep the first implementation deliberately + * conservative: accept the same raw SYS:-style, path-backed subset as the + * other AFP smoke probes, open only the data fork read-only, and return the + * normal six-byte NetWare handle shape used by the AFP handle APIs. + */ +{ + uint8 volume_number; + uint32 request_entry_id; + uint8 fork_indicator; + uint8 access_mode; + int path_len; + NW_FILE_INFO fileinfo; + int fhandle; + uint32 fork_len = 0; + + if (afp_len < 9) { + XDPRINTF((2,0, "AFP Open File Fork 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); + fork_indicator = afp_req[6]; + access_mode = afp_req[7]; + path_len = (int)afp_req[8]; + if (path_len < 0 || afp_len < 9 + path_len) { + XDPRINTF((2,0, "AFP Open File Fork rejected: boundary check len=%d path_len=%d", + afp_len, path_len)); + return(-0x7e); + } + + if (!nwatalk_backend_available()) { + XDPRINTF((3,0, "AFP Open File Fork rejected: libatalk backend unavailable")); + return(-0xbf); /* invalid namespace */ + } + + if (fork_indicator != 0) { + XDPRINTF((2,0, "AFP Open File Fork rejected: resource fork unsupported vol=%d entry=0x%08x fork=%d access=0x%02x", + (int)volume_number, request_entry_id, (int)fork_indicator, + (int)access_mode)); + return(-0x9c); /* Invalid Path until AppleDouble/resource forks exist */ + } + + if (access_mode & 0x02) { + XDPRINTF((2,0, "AFP Open File Fork rejected: write access unsupported vol=%d entry=0x%08x fork=%d access=0x%02x", + (int)volume_number, request_entry_id, (int)fork_indicator, + (int)access_mode)); + return(-0x84); /* No create/write rights for the AFP write path yet */ + } + + if (!path_len) { + XDPRINTF((2,0, "AFP Open File Fork rejected: entry-id-only lookup unsupported vol=%d entry=0x%08x fork=%d access=0x%02x", + (int)volume_number, request_entry_id, (int)fork_indicator, + (int)access_mode)); + return(-0x9c); /* Invalid Path until persistent entry-id lookup exists */ + } + + memset(&fileinfo, 0, sizeof(fileinfo)); + fhandle = nw_creat_open_file(0, afp_req + 9, path_len, &fileinfo, + 0, 0x1, 0, (int)(ncprequest->task)); + if (fhandle < 0) { + XDPRINTF((2,0, "AFP Open File Fork path open failed: vol=%d entry=0x%08x fork=%d access=0x%02x path='%s' result=-0x%x", + (int)volume_number, request_entry_id, (int)fork_indicator, + (int)access_mode, visable_data(afp_req + 9, path_len), + -fhandle)); + return(fhandle); + } + + fork_len = GET_BE32(fileinfo.size); + response[0] = 0; + response[1] = 0; + U32_TO_32(fhandle, response + 2); + U32_TO_BE32(fork_len, response + 6); + + XDPRINTF((3,0, "AFP Open File Fork: vol=%d entry=0x%08x fork=%d access=0x%02x path='%s' handle=%d fork_len=%u", + (int)volume_number, request_entry_id, (int)fork_indicator, + (int)access_mode, visable_data(afp_req + 9, path_len), + fhandle, fork_len)); + return(10); +} + static int afp_alloc_temporary_dir_handle(uint8 *afp_req, int afp_len, uint8 *response) /* @@ -3031,7 +3119,9 @@ 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. Alloc + * 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 * Temporary Dir Handle uses the same path-backed subset and * returns a connection-local NetWare directory handle plus * effective rights. Then expose @@ -3054,6 +3144,11 @@ static int handle_ncp_serv(void) afp_len, responsedata); if (result > -1) data_len = result; else completition = (uint8)-result; + } else if (ufunc == 0x08) { + int result = afp_open_file_fork(afp_req, + 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); diff --git a/tests/linux/CMakeLists.txt b/tests/linux/CMakeLists.txt index f434786..bc26e23 100644 --- a/tests/linux/CMakeLists.txt +++ b/tests/linux/CMakeLists.txt @@ -30,6 +30,11 @@ 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_open_file_fork_smoke afp_open_file_fork_smoke.c) +target_include_directories(afp_open_file_fork_smoke PRIVATE ${NCPFS_INCLUDE_DIR}) +target_link_libraries(afp_open_file_fork_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 edd15a4..1a3a47f 100644 --- a/tests/linux/README.md +++ b/tests/linux/README.md @@ -189,6 +189,49 @@ 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 Open File Fork smoke test + +`afp_open_file_fork_smoke` sends the WebSDK-documented NetWare AFP open fork +request: + +```text +NCP 0x2222/35/08 AFP Open File Fork +``` + +The first mars_nwe implementation is deliberately conservative. It supports +raw `SYS:`-style path requests with volume/base Entry ID zero, opens only the +AFP data fork, and only for read access. On success the server returns the +normal six-byte NetWare file handle shape used by AFP handle APIs plus the +current data-fork length. The smoke helper immediately closes the returned +NetWare file handle in the same connection. + +Useful smoke cases for a standard MARS-NWE `SYS` volume are: + +```sh +./tests/linux/afp_open_file_fork_smoke -S MARS -U SUPERVISOR -P secret SYS:PUBLIC/pmdflts.ini +./tests/linux/afp_open_file_fork_smoke -S MARS -U SUPERVISOR -P secret SYS:PUBLIC/ohlogscr.bat +``` + +A successful reply prints the returned NetWare handle, the requested fork, the +read access mode, and the data-fork length: + +```text +AFP Open File Fork path=SYS:PUBLIC/pmdflts.ini handle=1 fork=0 access=0x01 fork_len=1234 +AFP Open File Fork: vol=0 entry=0x00000000 fork=0 access=0x01 path='SYS:PUBLIC/pmdflts.ini' handle=1 fork_len=1234 +``` + +The exact handle number is connection-local and must not be reused across +processes. The exact `fork_len` depends on the backing file. Resource fork +opens (`--fork 1`), write access (`--access 2`), and Entry-ID-only open remain +negative/TODO coverage until persistent CNID/base-ID lookup and AppleDouble +resource-fork semantics are available. + +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, resource-fork, or Entry-ID-only +negative tests. + ## AFP File Information smoke test `afp_file_info_smoke` sends the WebSDK-documented NetWare AFP file diff --git a/tests/linux/afp_open_file_fork_smoke.c b/tests/linux/afp_open_file_fork_smoke.c new file mode 100644 index 0000000..0b8582a --- /dev/null +++ b/tests/linux/afp_open_file_fork_smoke.c @@ -0,0 +1,250 @@ +/* + * Linux smoke test for NetWare AFP Open File Fork. + * + * This uses ncpfs/libncp so the request travels through the same NCP path as a + * normal Linux requester. The first mars_nwe implementation deliberately + * exercises the conservative path-backed data-fork subset and returns a normal + * NetWare file handle, which this helper closes before exiting. + */ + +#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_OPEN_FILE_FORK 0x08 +#define NWE_INVALID_NAMESPACE 0xbf +#define NWE_INVALID_PATH 0x9c +#define AFP_DATA_FORK 0x00 +#define AFP_ACCESS_READ 0x01 + +static void usage(const char *prog) +{ + fprintf(stderr, + "Usage: %s [--allow-invalid-namespace] [--allow-invalid-path] " + "[--volume N] [--entry-id ID] [--fork N] [--access N] " + "[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/pmdflts.ini\n" + " %s --allow-invalid-namespace -S MARS -U SUPERVISOR -P secret SYS:PUBLIC/pmdflts.ini\n" + " %s --allow-invalid-path -S MARS -U SUPERVISOR -P secret SYS:NO_SUCH_FILE\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 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) | + (uint32_t)p[3]; +} + +static uint32_t le32_to_cpu(const uint8_t p[4]) +{ + return ((uint32_t)p[0]) | + ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | + ((uint32_t)p[3] << 24); +} + +static void cpu_to_le32(uint32_t v, uint8_t p[4]) +{ + p[0] = (uint8_t)v; + p[1] = (uint8_t)(v >> 8); + p[2] = (uint8_t)(v >> 16); + p[3] = (uint8_t)(v >> 24); +} + +static void close_file_handle(NWCONN_HANDLE conn, uint32_t fhandle) +{ + uint8_t rq[7]; + + memset(rq, 0, sizeof(rq)); + cpu_to_le32(fhandle, rq + 3); + (void)NWRequestSimple(conn, 0x42, 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; + uint32_t fork_indicator = AFP_DATA_FORK; + uint32_t access_mode = AFP_ACCESS_READ; + int i; + size_t path_len; + uint8_t request[1 + 4 + 1 + 1 + 1 + 255]; + uint8_t reply_buf[10]; + uint32_t fhandle; + uint32_t fork_len; + 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], "--fork")) { + if (++i >= argc || parse_u32(argv[i], &fork_indicator) || + fork_indicator > 255) { + fprintf(stderr, "invalid --fork value\n"); + ncp_close(conn); + return 2; + } + } else if (!strcmp(argv[i], "--access")) { + if (++i >= argc || parse_u32(argv[i], &access_mode) || + access_mode > 255) { + fprintf(stderr, "invalid --access 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 Open File Fork: %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)fork_indicator; + request[6] = (uint8_t)access_mode; + request[7] = (uint8_t)path_len; + memcpy(request + 8, 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_OPEN_FILE_FORK), + request, + 8 + path_len, + &reply); + + if (((unsigned int)err & 0xff) == NWE_INVALID_NAMESPACE && + allow_invalid_namespace) { + printf("AFP Open File Fork 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 Open File Fork returned invalid path as expected: path=%s\n", path); + ncp_close(conn); + return 0; + } + + if (err) { + fprintf(stderr, + "AFP Open File Fork failed: completion=0x%02x (%u) path=%s\n", + (unsigned int)err & 0xff, (unsigned int)err, path); + ncp_close(conn); + return 1; + } + + if (reply.fragSize < 10) { + fprintf(stderr, "short AFP reply: %zu bytes\n", reply.fragSize); + ncp_close(conn); + return 1; + } + + fhandle = le32_to_cpu(reply_buf + 2); + fork_len = be32_to_cpu(reply_buf + 6); + printf("AFP Open File Fork path=%s handle=%u fork=%u access=0x%02x fork_len=%u\n", + path, fhandle, (unsigned int)fork_indicator, + (unsigned int)access_mode, fork_len); + + close_file_handle(conn, fhandle); + ncp_close(conn); + return 0; +}