nwconn: implement AFP open file fork smoke path
All checks were successful
Source release / source-package (push) Successful in 46s

Implement the WebSDK/nwafp.h NCP 0x2222/35 AFP subfunction 0x08, Open File Fork, for the same conservative path-backed subset that the current AFP smoke endpoints use.

The request is decoded as volume number, AFP Entry ID, fork selector, access mode, path length, and AFP path.  Until persistent CNID/base-ID lookup exists, empty path / Entry-ID-only opens continue to fail with Invalid Path rather than pretending that the temporary stat-derived AFP Entry IDs are a durable namespace.  The handler also keeps resource forks and write access negative for now, because those require AppleDouble/resource-fork and write-safe Finder metadata semantics that are not implemented yet.

For the supported data-fork read-only case, delegate to the existing NetWare open-file path via nw_creat_open_file().  The reply returns the normal six-byte NetWare file handle shape used by the AFP handle APIs followed by the current data-fork length.  That lets follow-up smoke tests verify handle interoperability with AFP Get Entry ID From NetWare Handle and with the ordinary NetWare close path while keeping write/resource semantics conservative.

Add afp_open_file_fork_smoke so Linux ncpfs/libncp tests can exercise the endpoint through the same requester path as the other AFP probes.  The helper closes the returned handle in the same connection and documents the expected data-fork-only coverage in tests/linux/README.md and TODO.md.

Tests: git diff --check

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

Tests: cmake --build build-on --target nwconn with ENABLE_NETATALK_LIBATALK=ON requested; local environment lacks libatalk headers/library so CMake reports the metadata backend disabled, but the target still builds and the new handler has no direct libatalk symbol references

Tests: gcc -fsyntax-only tests/linux/afp_open_file_fork_smoke.c with local ncpfs header stubs

TODO: add persistent CNID/base-ID lookup, AppleDouble/resource-fork open support, and write-safe AFP fork semantics before accepting resource-fork or write-mode opens.
This commit is contained in:
Mario Fetka
2026-05-30 08:02:06 +00:00
parent f6bda83f67
commit db9283a410
5 changed files with 401 additions and 1 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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})

View File

@@ -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

View File

@@ -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 <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ncp/nwcalls.h>
#include <ncp/ncplib.h>
#include <ncp/kernel/ncp.h>
#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;
}