From 4637f3ee57deab26e23dbc49f69da7edd40dd7f8 Mon Sep 17 00:00:00 2001 From: OpenAI Date: Sat, 30 May 2026 09:55:44 +0000 Subject: [PATCH] nwatalk: cache AFP fallback entry ids in xattrs The AFP smoke endpoints can now read mars_nwe-owned entry ids from the versioned org.mars-nwe.afp.entry-id xattr, but a newly discovered file still had to fall back to the temporary stat-derived id on every request until a real CNID allocator exists. Preserve the existing WebSDK/NWAFP response semantics while making that fallback sticky: when Get Entry ID, Get File Information, or Scan File Information has no mars_nwe xattr and no Netatalk/libatalk AppleDouble/CNID id, derive the existing compatibility id and cache it through nwatalk_set_entry_id(). The first request still logs fallback so diagnostics remain honest about the id origin; subsequent requests should read the xattr directly and avoid re-entering the stat fallback path. Keep the write narrowly scoped to mars_nwe's private AFP metadata namespace. The payload is versioned, big-endian, and stored through the nwxattr helper, so Linux persists it as user.org.mars-nwe.afp.entry-id while source-level code continues to use the Netatalk-style org.mars-nwe.afp.entry-id name. This does not implement CNID allocation, parent-id lookup, entry-id-only resolution, FinderInfo mutation beyond the existing smoke path, or resource-fork semantics. Tests: - git diff --check - cmake --build build-xattr-off --target nwconn with ENABLE_NETATALK_LIBATALK=OFF - cmake --build build-xattr-on --target nwconn with ENABLE_NETATALK_LIBATALK=ON against Netatalk 4.4.3 headers plus local link stubs --- TODO.md | 26 ++++++++++-------- include/nwatalk.h | 1 + src/nwatalk.c | 30 +++++++++++++++++++++ src/nwconn.c | 61 ++++++++++++++++++++++++++----------------- tests/linux/README.md | 36 ++++++++++++++++++------- 5 files changed, 110 insertions(+), 44 deletions(-) diff --git a/TODO.md b/TODO.md index fc4de1b..adbed4f 100644 --- a/TODO.md +++ b/TODO.md @@ -251,11 +251,14 @@ Current status: `tests/linux/afp_set_file_info_smoke`; runtime coverage is green for `SYS:PUBLIC/pmdflts.ini` with Finder type `TEXT` and creator `MARS`. The helper writes 32 bytes of FinderInfo to `org.mars-nwe.afp.finder-info` and - verifies the result through AFP 2.0 Get File Information. A `fallback` marker - on the verification Get File Information diagnostic still describes the - stat-derived entry-id source, not the FinderInfo write result. All other Set - File Information bits remain rejected until their write semantics are - explicitly designed. + verifies the result through AFP 2.0 Get File Information. The first + stat-derived AFP entry id for a path is now cached in the versioned + `org.mars-nwe.afp.entry-id` xattr; a `fallback` marker on that first + verification Get File Information diagnostic describes the entry-id origin, + not the FinderInfo write result. Follow-up probes should read the cached + mars_nwe entry id and omit the fallback marker. All other Set File + Information bits remain rejected until their write semantics are explicitly + designed. - 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. @@ -282,14 +285,15 @@ Follow-up: - Keep returning invalid namespace for AFP calls that still lack a real per-volume Mac namespace/AFP metadata layer. Do not return success for additional AFP calls without data/resource fork and Finder Info semantics. -- Replace the temporary stat-derived AFP entry-id fallback with a persistent - CNID/directory-id mapping once the libatalk/CNID backend is integrated. +- Replace the compatibility stat-derived AFP entry-id generator with a real + CNID/directory-id allocator once the libatalk/CNID backend is integrated. - mars_nwe-owned AFP entry ids are probed first from the versioned `org.mars-nwe.afp.entry-id` xattr before consulting Netatalk/libatalk - AppleDouble/CNID metadata and, finally, the stat-derived fallback. - FinderInfo now has a deliberately narrow write path through AFP 2.0 Set File - Information; CNID allocation and broader AFP metadata writes still need a - deliberate write-safe design. + AppleDouble/CNID metadata. If neither source has an id, mars_nwe derives the + existing stat-compatible id and caches it in that xattr so subsequent probes + can use persistent mars_nwe metadata. FinderInfo now has a deliberately narrow + write path through AFP 2.0 Set File Information; CNID allocation and broader + AFP metadata writes still need a deliberate write-safe design. - Put additional future mars_nwe-owned AFP metadata under `org.mars-nwe.afp.*` (or a compact `org.mars-nwe.afp.metadata` record) and keep Netatalk-owned metadata under Netatalk's own `org.netatalk.*` keys. diff --git a/include/nwatalk.h b/include/nwatalk.h index a873d9d..12c5380 100644 --- a/include/nwatalk.h +++ b/include/nwatalk.h @@ -12,5 +12,6 @@ int nwatalk_set_finder_info(const char *path, const uint8 *finder_info, int finder_info_len); int nwatalk_get_resource_fork_size(const char *path, uint32 *resource_size); int nwatalk_get_entry_id(const char *path, uint32 *entry_id); +int nwatalk_set_entry_id(const char *path, uint32 entry_id); #endif diff --git a/src/nwatalk.c b/src/nwatalk.c index 221a4b3..b258ac3 100644 --- a/src/nwatalk.c +++ b/src/nwatalk.c @@ -156,6 +156,36 @@ int nwatalk_set_finder_info(const char *path, const uint8 *finder_info, #endif } + +int nwatalk_set_entry_id(const char *path, uint32 entry_id) +{ +#if XATTR_SUPPORT + MARS_NWE_AFP_ENTRY_ID_XATTR_DATA d; + + if (!path || !*path) return(-0x9c); + entry_id &= 0x7fffffffU; + if (!entry_id) return(-0x9c); + + memset(&d, 0, sizeof(d)); + d.version = MARS_NWE_AFP_ENTRY_ID_VERSION; + U32_TO_BE32(entry_id, d.entry_id); + + if (mars_nwe_setxattr(path, MARS_NWE_AFP_ENTRY_ID_XATTR, + &d, sizeof(d), 0)) { + int err = errno; + XDPRINTF((5,0,"AFP entry-id xattr write ignored for %s entry=0x%08x errno=%d", + path, entry_id, err)); + return(-0x8c); + } + + return(0); +#else + (void)path; + (void)entry_id; + return(-0xbf); +#endif +} + int nwatalk_get_resource_fork_size(const char *path, uint32 *resource_size) { #if NETATALK_SUPPORT diff --git a/src/nwconn.c b/src/nwconn.c index 53c6d74..0cdd63c 100644 --- a/src/nwconn.c +++ b/src/nwconn.c @@ -507,6 +507,34 @@ static uint32 afp_fallback_entry_id(int volume, const struct stat *stb) } + +static uint32 afp_get_or_create_entry_id(const char *unixname, int volume, + const struct stat *stb, + int *fallback_out) +/* + * Return a persistent mars_nwe AFP entry id when available. If neither the + * mars_nwe xattr nor Netatalk/libatalk metadata contains an id yet, derive the + * existing stat-backed compatibility id and cache that id in mars_nwe's private + * AFP xattr namespace. The first caller still records fallback diagnostics so + * the log remains honest about the id origin; follow-up requests should read the + * xattr directly and no longer need the temporary stat derivation path. + */ +{ + uint32 entry_id = 0; + int result; + + result = nwatalk_get_entry_id(unixname, &entry_id); + if (!result && entry_id) { + if (fallback_out) *fallback_out = 0; + return(entry_id); + } + + entry_id = afp_fallback_entry_id(volume, stb); + if (fallback_out) *fallback_out = 1; + (void)nwatalk_set_entry_id(unixname, entry_id); + return(entry_id); +} + static int afp_get_entry_id_from_name(uint8 *afp_req, int afp_len, uint8 *response) { @@ -561,15 +589,13 @@ static int afp_get_entry_id_from_name(uint8 *afp_req, int afp_len, return(-0x9c); /* Invalid Path */ } - result = nwatalk_get_entry_id(unixname, &entry_id); - if (result < 0 || !entry_id) - entry_id = afp_fallback_entry_id(volume, &stbuff); + entry_id = afp_get_or_create_entry_id(unixname, volume, &stbuff, &result); U32_TO_BE32(entry_id, response); XDPRINTF((3,0, "AFP Get Entry ID From Name: vol=%d entry=0x%08x path='%s' reply_entry=0x%08x%s", (int)volume_number, request_entry_id, visable_data(afp_req + 7, path_len), entry_id, - (result < 0) ? " fallback" : "")); + result ? " fallback" : "")); return(4); } @@ -612,9 +638,7 @@ static int afp_get_entry_id_from_netware_handle(uint8 *afp_req, int afp_len, volume = afp_volume_from_unixname((char *)unixname); if (volume < 0) volume = 0; - result = nwatalk_get_entry_id((char *)unixname, &entry_id); - if (result < 0 || !entry_id) - entry_id = afp_fallback_entry_id(volume, &stbuff); + entry_id = afp_get_or_create_entry_id((char *)unixname, volume, &stbuff, &result); response[0] = (uint8)volume; U32_TO_BE32(entry_id, response + 1); @@ -622,7 +646,7 @@ static int afp_get_entry_id_from_netware_handle(uint8 *afp_req, int afp_len, XDPRINTF((3,0, "AFP Get Entry ID From NetWare Handle: handle=%u volume=%d unix='%s' entry=0x%08x%s", fhandle, volume, unixname, entry_id, - (result < 0) ? " fallback" : "")); + result ? " fallback" : "")); return(6); } @@ -844,14 +868,12 @@ static int afp_get_entry_id_from_path_name(uint8 *afp_req, int afp_len, return(-0x9c); /* Invalid Path */ } - result = nwatalk_get_entry_id(unixname, &entry_id); - if (result < 0 || !entry_id) - entry_id = afp_fallback_entry_id(volume, &stbuff); + entry_id = afp_get_or_create_entry_id(unixname, volume, &stbuff, &result); U32_TO_BE32(entry_id, response); XDPRINTF((3,0, "AFP Get Entry ID From Path Name: dh=%d path='%s' entry=0x%08x%s", (int)dir_handle, visable_data(afp_req + 3, path_len), entry_id, - (result < 0) ? " fallback" : "")); + result ? " fallback" : "")); return(4); } @@ -956,16 +978,11 @@ static int afp_fill_file_info_response(const char *unixname, uint32 parent_id = 0; uint32 resource_size = 0; uint8 finder_info[NWATALK_FINDER_INFO_LEN]; - int result; if (stat(unixname, &stbuff)) return(-0x9c); /* Invalid Path */ - result = nwatalk_get_entry_id(unixname, &entry_id); - if (result < 0 || !entry_id) { - entry_id = afp_fallback_entry_id(volume, &stbuff); - if (fallback_out) *fallback_out = 1; - } else if (fallback_out) *fallback_out = 0; + entry_id = afp_get_or_create_entry_id(unixname, volume, &stbuff, fallback_out); memset(response, 0, 120); U32_TO_BE32(entry_id, response + 0); @@ -1280,7 +1297,6 @@ static int afp_scan_file_information(uint8 *afp_req, int afp_len, struct stat stbuff; int child_fallback = 0; uint32 child_entry_id = 0; - int child_result; if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue; @@ -1289,11 +1305,8 @@ static int afp_scan_file_information(uint8 *afp_req, int afp_len, if (stat(childname, &stbuff)) continue; - child_result = nwatalk_get_entry_id(childname, &child_entry_id); - if (child_result < 0 || !child_entry_id) { - child_entry_id = afp_fallback_entry_id(volume, &stbuff); - child_fallback = 1; - } + child_entry_id = afp_get_or_create_entry_id(childname, volume, &stbuff, + &child_fallback); if (last_seen_id && !seen) { if (child_entry_id == last_seen_id) diff --git a/tests/linux/README.md b/tests/linux/README.md index fdb8710..5d5fc85 100644 --- a/tests/linux/README.md +++ b/tests/linux/README.md @@ -7,11 +7,13 @@ The tests use the ncpfs/libncp client library. They are not built by default because they require the host ncpfs development headers/library and a running NetWare-compatible server. -The AFP endpoints are intentionally conservative. When persistent AFP entry -ids become available, mars_nwe-owned ids are read from the versioned -`org.mars-nwe.afp.entry-id` xattr before falling back to Netatalk/libatalk -AppleDouble/CNID metadata and then the temporary stat-derived fallback. The -first AFP write smoke path is deliberately limited to the FinderInfo bitmap of +The AFP endpoints are intentionally conservative. mars_nwe-owned ids are read +from the versioned `org.mars-nwe.afp.entry-id` xattr before falling back to +Netatalk/libatalk AppleDouble/CNID metadata. When neither source has an id yet, +the existing stat-derived compatibility id is cached in that xattr so subsequent +AFP probes can reuse the same mars_nwe-owned id instead of re-entering the +temporary fallback path. The first AFP write smoke path is deliberately limited +to the FinderInfo bitmap of AFP 2.0 Set File Information; CNID allocation, DOS attribute mapping, resource fork writes, and data-fork writes remain separate write-safety work. mars_nwe source uses Netatalk-style `org.mars-nwe..*` xattr names; AFP @@ -401,13 +403,29 @@ AFP 2.0 Set File Information: vol=0 request_vol=0 entry=0x00000000 mask=0x0020 p AFP 2.0 Get File Information: vol=0 entry=0x00000000 mask=0xffff path='SYS:PUBLIC/pmdflts.ini' reply_entry=0x23c8787d fallback ``` -The `fallback` marker on the verification Get File Information diagnostic still -refers to the entry-id source: the returned entry id is derived from the current -stat-backed fallback path because no persistent CNID or `org.mars-nwe.afp.entry-id` -metadata was present. It does not mean the FinderInfo write was ignored; the +The `fallback` marker on the first verification Get File Information diagnostic +still refers to the entry-id source: the returned entry id was derived from the +stat-backed compatibility path because no CNID or mars_nwe entry-id xattr existed +yet. The server now caches that derived id in `org.mars-nwe.afp.entry-id`, so a +second probe of the same file should reuse the xattr-backed id and normally omit +the `fallback` marker. It does not mean the FinderInfo write was ignored; the helper verifies the written FinderInfo through the follow-up Get File Information reply. +Linux xattr checks for the FinderInfo and cached Entry ID look like this: + +```sh +getfattr -n user.org.mars-nwe.afp.finder-info -e hex /var/mars_nwe/SYS/public/pmdflts.ini +getfattr -n user.org.mars-nwe.afp.entry-id -e hex /var/mars_nwe/SYS/public/pmdflts.ini +``` + +For the verified FinderInfo smoke run, the FinderInfo xattr starts with +`TEXTMARS`: + +```text +user.org.mars-nwe.afp.finder-info=0x544558544d415253000000000000000000000000000000000000000000000000 +``` + All other Set File Information bitmap bits are intentionally rejected for now. That keeps attribute, timestamp, DOS/NetWare mode-bit mapping, resource-fork, and Entry-ID-only write semantics out of this first metadata-only write smoke