nwconn: persist AFP invisible file attributes
All checks were successful
Source release / source-package (push) Successful in 47s

Extend the conservative NCP 0x2222/35/16 AFP 2.0 Set File Information smoke path beyond FinderInfo-only writes by accepting the file Attributes bitmap for one deliberately narrow bit: Finder Invisible.

WebSDK and Netatalk FPSetFileParams semantics carry file attributes as bitmap bit 0, with ATTRBIT_SETCLR selecting set-vs-clear behavior. Mirror that model only for ATTRBIT_INVISIBLE and reject all other AFP attribute bits so DOS/NetWare mode bits, timestamp writes, resource forks, and broader file protection semantics are not implied accidentally.

Persist the mars_nwe-owned AFP attribute word in org.mars-nwe.afp.attributes via the local xattr abstraction. On Linux this maps to user.org.mars-nwe.afp.attributes, matching the org.mars-nwe.* source-level namespace while remaining portable on Linux xattr backends. Get File Information and Scan File Information now merge that stored Invisible bit into the existing 120-byte AFP file-info record.

Update the Linux Set File Information smoke helper with --invisible, --clear-invisible, --attributes-only, and --finder-info-only so FinderInfo and the narrow AFP attribute path can be tested independently or together.

Tests: git diff --check

Tests: gcc -Iinclude -I/mnt/data/stubs -fsyntax-only tests/linux/afp_set_file_info_smoke.c

TODO: keep all other AFP Set File Information bits rejected until their write-safe mapping to NetWare/DOS attributes, timestamps, CNID, and resource-fork metadata is designed.
This commit is contained in:
OpenAI
2026-05-30 10:21:00 +00:00
committed by Mario Fetka
parent 97bce6edf5
commit c51fde95fe
6 changed files with 245 additions and 53 deletions

View File

@@ -383,7 +383,7 @@ If the server was built without the optional Netatalk/libatalk backend, use
when a scan continuation is expected to reach the end of the directory.
## AFP Set File Information FinderInfo smoke test
## AFP Set File Information metadata smoke test
`afp_set_file_info_smoke` sends the WebSDK-documented NetWare AFP 2.0 request:
@@ -391,11 +391,13 @@ when a scan continuation is expected to reach the end of the directory.
NCP 0x2222/35/16 AFP 2.0 Set File Information
```
The helper exercises only the first write-safe AFP subset: the file FinderInfo
bitmap (`0x0020`). It sends a path-backed raw `VOL:`-style request, writes the
32-byte FinderInfo block to mars_nwe's private `org.mars-nwe.afp.finder-info`
metadata key, and immediately verifies the update through AFP 2.0 Get File
Information. On Linux the source-level `org.mars-nwe.afp.*` name is stored via the
The helper exercises two deliberately narrow write-safe AFP metadata subsets:
the file FinderInfo bitmap (`0x0020`) and the file Attributes bitmap (`0x0001`)
restricted to the Finder Invisible bit. It sends path-backed raw `VOL:`-style
requests, writes the 32-byte FinderInfo block to mars_nwe's private
`org.mars-nwe.afp.finder-info` metadata key, writes the narrow AFP attribute word
to `org.mars-nwe.afp.attributes`, and immediately verifies the updates through
AFP 2.0 Get File Information. On Linux the source-level `org.mars-nwe.afp.*` name is stored via the
portable `user.` xattr namespace by mars_nwe's local xattr wrapper, the same
pattern Netatalk uses for its `org.netatalk.*` metadata names.
@@ -445,10 +447,29 @@ For the verified FinderInfo smoke run, the FinderInfo xattr starts with
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
path.
Finder Invisible can be tested without mutating DOS/NetWare mode bits:
```sh
./tests/linux/afp_set_file_info_smoke \
-S MARS -U SUPERVISOR -P secret \
--attributes-only --invisible \
SYS:PUBLIC/pmdflts.ini
getfattr -n user.org.mars-nwe.afp.attributes -e hex /var/mars_nwe/SYS/public/pmdflts.ini
```
The xattr payload is versioned. For an invisible file the expected Linux form
is:
```text
user.org.mars-nwe.afp.attributes=0x01000001
```
Use `--clear-invisible --attributes-only` to clear that bit; the same xattr then
stores `0x01000000`. All other Set File Information bitmap bits and all other
AFP attribute bits are intentionally rejected for now. That keeps timestamp,
DOS/NetWare mode-bit mapping, resource-fork, and Entry-ID-only write semantics
out of this metadata-only smoke path.
If the server was built without the optional Netatalk/libatalk backend, use
`--allow-invalid-namespace` for the expected negative test. Use

View File

@@ -1,5 +1,5 @@
/*
* Linux smoke test for NetWare AFP 2.0 Set File Information FinderInfo writes.
* Linux smoke test for NetWare AFP 2.0 Set File Information metadata writes.
*/
#include <errno.h>
@@ -21,7 +21,10 @@
#define AFP20_GET_FILE_INFORMATION 0x0f
#define AFP20_SET_FILE_INFORMATION 0x10
#define AFP_ATTRIBUTES_MASK 0x0001
#define AFP_FINDER_INFO_MASK 0x0020
#define AFP_ATTR_INVISIBLE 0x0001
#define AFP_ATTR_SETCLR 0x8000
#define AFP_REPLY_LEN 120
#define NWE_INVALID_NAMESPACE 0xbf
#define NWE_INVALID_PATH 0x9c
@@ -31,6 +34,7 @@ static void usage(const char *prog)
fprintf(stderr,
"Usage: %s [--allow-invalid-namespace] [--allow-invalid-path] "
"[--volume N] [--entry-id ID] [--type FOUR] [--creator FOUR] "
"[--invisible|--clear-invisible] [--finder-info-only|--attributes-only] "
"[ncpfs options] PATH\n"
"\n"
"ncpfs options are parsed by ncp_initialize(), for example:\n"
@@ -38,8 +42,9 @@ static void usage(const char *prog)
"\n"
"Examples:\n"
" %s -S MARS -U SUPERVISOR -P secret --type TEXT --creator MARS SYS:PUBLIC/pmdflts.ini\n"
" %s -S MARS -U SUPERVISOR -P secret --invisible --attributes-only SYS:PUBLIC/pmdflts.ini\n"
" %s --allow-invalid-namespace -S MARS SYS:PUBLIC/pmdflts.ini\n",
prog, prog, prog);
prog, prog, prog, prog);
}
static int parse_u32(const char *text, uint32_t *value)
@@ -55,6 +60,11 @@ static int parse_u32(const char *text, uint32_t *value)
return 0;
}
static uint16_t be16_to_cpu(const uint8_t p[2])
{
return ((uint16_t)p[0] << 8) | p[1];
}
static uint32_t be32_to_cpu(const uint8_t p[4])
{
return ((uint32_t)p[0] << 24) |
@@ -123,8 +133,11 @@ int main(int argc, char **argv)
uint32_t volume_number = 0;
uint32_t entry_id = 0;
uint8_t finder_info[32];
uint16_t request_mask = AFP_FINDER_INFO_MASK;
uint16_t attr_request = 0;
uint16_t attr_expected = 0;
uint8_t verify_buf[AFP_REPLY_LEN];
uint8_t request[1 + 4 + 2 + 1 + 255 + 1 + 32];
uint8_t request[1 + 4 + 2 + 1 + 255 + 1 + 2 + 1 + 32];
size_t path_len;
size_t afp_data_off;
size_t data_off;
@@ -164,6 +177,18 @@ int main(int argc, char **argv)
ncp_close(conn);
return 2;
}
} else if (!strcmp(argv[i], "--invisible")) {
request_mask |= AFP_ATTRIBUTES_MASK;
attr_request = AFP_ATTR_SETCLR | AFP_ATTR_INVISIBLE;
attr_expected = AFP_ATTR_INVISIBLE;
} else if (!strcmp(argv[i], "--clear-invisible")) {
request_mask |= AFP_ATTRIBUTES_MASK;
attr_request = AFP_ATTR_INVISIBLE;
attr_expected = 0;
} else if (!strcmp(argv[i], "--attributes-only")) {
request_mask &= ~AFP_FINDER_INFO_MASK;
} else if (!strcmp(argv[i], "--finder-info-only")) {
request_mask &= ~AFP_ATTRIBUTES_MASK;
} else if (!strcmp(argv[i], "--type")) {
if (++i >= argc || copy_fourcc(argv[i], finder_info + 0)) {
fprintf(stderr, "invalid --type value, expected four characters\n");
@@ -207,19 +232,28 @@ int main(int argc, char **argv)
memset(request, 0, sizeof(request));
request[0] = (uint8_t)volume_number;
cpu_to_be32(entry_id, request + 1);
cpu_to_be16(AFP_FINDER_INFO_MASK, request + 5);
cpu_to_be16(request_mask, request + 5);
request[7] = (uint8_t)path_len;
memcpy(request + 8, path, path_len);
afp_data_off = 1 + 8 + path_len;
if (afp_data_off & 1)
afp_data_off++;
data_off = afp_data_off - 1;
memcpy(request + data_off, finder_info, sizeof(finder_info));
if (request_mask & AFP_ATTRIBUTES_MASK) {
cpu_to_be16(attr_request, request + data_off);
data_off += 2;
}
if ((request_mask & AFP_FINDER_INFO_MASK) && (data_off & 1))
data_off++;
if (request_mask & AFP_FINDER_INFO_MASK) {
memcpy(request + data_off, finder_info, sizeof(finder_info));
data_off += sizeof(finder_info);
}
err = NWRequestSimple(conn,
NCPC_SFN(0x23, AFP20_SET_FILE_INFORMATION),
request,
data_off + sizeof(finder_info),
data_off,
NULL);
if (err == NWE_INVALID_NAMESPACE && allow_invalid_namespace) {
printf("AFP Set File Information returned invalid namespace as expected for path=%s\n", path);
@@ -248,7 +282,8 @@ int main(int argc, char **argv)
return 1;
}
if (memcmp(verify_buf + 32, finder_info, sizeof(finder_info))) {
if ((request_mask & AFP_FINDER_INFO_MASK) &&
memcmp(verify_buf + 32, finder_info, sizeof(finder_info))) {
fprintf(stderr,
"AFP Set File Information verify mismatch: path=%s got_type=%.4s got_creator=%.4s\n",
path, verify_buf + 32, verify_buf + 36);
@@ -256,9 +291,18 @@ int main(int argc, char **argv)
return 1;
}
printf("AFP Set File Info path=%s bitmap=0x%04x finder_type=%.4s finder_creator=%.4s entry_id=0x%08x verified\n",
path, AFP_FINDER_INFO_MASK, finder_info + 0, finder_info + 4,
be32_to_cpu(verify_buf + 0));
if ((request_mask & AFP_ATTRIBUTES_MASK) &&
((be16_to_cpu(verify_buf + 8) & AFP_ATTR_INVISIBLE) != attr_expected)) {
fprintf(stderr,
"AFP Set File Information attr verify mismatch: path=%s attrs=0x%04x expected_invisible=0x%04x\n",
path, be16_to_cpu(verify_buf + 8), attr_expected);
ncp_close(conn);
return 1;
}
printf("AFP Set File Info path=%s bitmap=0x%04x attrs=0x%04x finder_type=%.4s finder_creator=%.4s entry_id=0x%08x verified\n",
path, request_mask, be16_to_cpu(verify_buf + 8),
finder_info + 0, finder_info + 4, be32_to_cpu(verify_buf + 0));
ncp_close(conn);
return 0;