nwconn: implement AFP DOS name reverse lookup
All checks were successful
Source release / source-package (push) Successful in 50s

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.
This commit is contained in:
OpenAI
2026-05-30 14:03:10 +00:00
committed by Mario Fetka
parent 2482c2bd99
commit 7241a28393
6 changed files with 472 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,262 @@
/*
* Linux smoke test for NetWare AFP Get DOS Name From Entry ID.
*/
#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_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;
}

View File

@@ -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'" \