nwatalk: cache AFP fallback entry ids in xattrs
All checks were successful
Source release / source-package (push) Successful in 48s

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
This commit is contained in:
OpenAI
2026-05-30 09:55:44 +00:00
committed by Mario Fetka
parent 8c99eaa68b
commit 4637f3ee57
5 changed files with 110 additions and 44 deletions

26
TODO.md
View File

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

View File

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

View File

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

View File

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

View File

@@ -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.<domain>.*` 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