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

@@ -18,7 +18,12 @@
#define MARS_NWE_AFP_ENTRY_ID_XATTR "org.mars-nwe.afp.entry-id"
#define MARS_NWE_AFP_FINDER_INFO_XATTR "org.mars-nwe.afp.finder-info"
#define MARS_NWE_AFP_ATTRIBUTES_XATTR "org.mars-nwe.afp.attributes"
#define MARS_NWE_AFP_ENTRY_ID_VERSION 1
#define MARS_NWE_AFP_ATTRIBUTES_VERSION 1
#define NWATALK_AFP_ATTR_INVISIBLE 0x0001
#define NWATALK_AFP_ATTR_SETCLR 0x8000
typedef struct {
uint8 version;
@@ -26,6 +31,12 @@ typedef struct {
uint8 entry_id[4];
} MARS_NWE_AFP_ENTRY_ID_XATTR_DATA;
typedef struct {
uint8 version;
uint8 reserved;
uint8 attributes[2];
} MARS_NWE_AFP_ATTRIBUTES_XATTR_DATA;
#if XATTR_SUPPORT
static int nwatalk_get_mars_entry_id_xattr(const char *path, uint32 *entry_id)
@@ -157,6 +168,73 @@ int nwatalk_set_finder_info(const char *path, const uint8 *finder_info,
}
int nwatalk_get_afp_attributes(const char *path, uint16 *attributes)
{
#if XATTR_SUPPORT
MARS_NWE_AFP_ATTRIBUTES_XATTR_DATA d;
ssize_t len;
#endif
if (!attributes) return(-0x9c);
*attributes = 0;
if (!path || !*path) return(-0x9c);
#if XATTR_SUPPORT
memset(&d, 0, sizeof(d));
len = mars_nwe_getxattr(path, MARS_NWE_AFP_ATTRIBUTES_XATTR,
&d, sizeof(d));
if (len == sizeof(d) && d.version == MARS_NWE_AFP_ATTRIBUTES_VERSION) {
*attributes = GET_BE16(d.attributes) & NWATALK_AFP_ATTR_INVISIBLE;
return(0);
}
#else
(void)path;
#endif
return(-0x9c);
}
int nwatalk_set_afp_attributes(const char *path, uint16 attributes)
{
#if XATTR_SUPPORT
MARS_NWE_AFP_ATTRIBUTES_XATTR_DATA d;
uint16 stored = 0;
uint16 requested = attributes & ~NWATALK_AFP_ATTR_SETCLR;
uint16 current = 0;
if (!path || !*path) return(-0x9c);
if (requested & ~NWATALK_AFP_ATTR_INVISIBLE) {
XDPRINTF((3,0,"AFP attributes xattr write rejected for %s attrs=0x%04x", path, attributes));
return(-0x9c);
}
(void)nwatalk_get_afp_attributes(path, &current);
if (attributes & NWATALK_AFP_ATTR_SETCLR)
stored = current | requested;
else
stored = current & ~requested;
memset(&d, 0, sizeof(d));
d.version = MARS_NWE_AFP_ATTRIBUTES_VERSION;
U16_TO_BE16(stored, d.attributes);
if (mars_nwe_setxattr(path, MARS_NWE_AFP_ATTRIBUTES_XATTR,
&d, sizeof(d), 0)) {
int err = errno;
XDPRINTF((3,0,"AFP attributes xattr write failed for %s attrs=0x%04x errno=%d", path, stored, err));
return(-0x8c);
}
return(0);
#else
(void)path;
(void)attributes;
return(-0xbf);
#endif
}
int nwatalk_set_entry_id(const char *path, uint32 entry_id)
{
#if XATTR_SUPPORT

View File

@@ -932,11 +932,20 @@ static void afp_leaf_name_from_path(uint8 *dst, int dst_len,
afp_copy_fixed_name(dst, dst_len, path + start, end - start);
}
static uint16 afp_basic_attributes(const struct stat *stb)
static uint16 afp_basic_attributes(const char *unixname, const struct stat *stb)
{
uint16 attributes = 0;
if (S_ISDIR(stb->st_mode))
return(0x0400); /* AFP_SA_SUBDIR */
return(0x0000); /* AFP_SA_NORMAL */
attributes |= 0x0400; /* AFP_SA_SUBDIR */
if (!S_ISDIR(stb->st_mode)) {
uint16 stored_attrs = 0;
if (!nwatalk_get_afp_attributes(unixname, &stored_attrs))
attributes |= stored_attrs;
}
return(attributes);
}
static uint16 afp_basic_access_privileges(const struct stat *stb)
@@ -987,7 +996,7 @@ static int afp_fill_file_info_response(const char *unixname,
memset(response, 0, 120);
U32_TO_BE32(entry_id, response + 0);
U32_TO_BE32(parent_id, response + 4);
U16_TO_BE16(afp_basic_attributes(&stbuff), response + 8);
U16_TO_BE16(afp_basic_attributes(unixname, &stbuff), response + 8);
U32_TO_BE32(S_ISDIR(stbuff.st_mode) ? 0 : (uint32)stbuff.st_size, response + 10);
if (!S_ISDIR(stbuff.st_mode))
@@ -1084,7 +1093,10 @@ static int afp_get_file_information(uint8 *afp_req, int afp_len,
}
#define AFP_FILE_BITMAP_ATTRIBUTES 0x0001
#define AFP_FILE_BITMAP_FINDER_INFO 0x0020
#define AFP_ATTR_INVISIBLE 0x0001
#define AFP_ATTR_SETCLR 0x8000
static int afp_set_file_information(uint8 *afp_req, int afp_len,
const char *call_name)
@@ -1092,12 +1104,12 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len,
* Conservative AFP 2.0 Set File Information subset.
*
* Netatalk's FPSetFileParams tests use the FinderInfo bitmap as a small,
* metadata-only write probe. Mirror that safest slice for the NCP AFP
* metadata-only write probes. Mirror the safest slices for the NCP AFP
* extension: accept the same path-backed VOL:-style smoke requests as Get File
* Information, persist only the 32-byte FinderInfo block in mars_nwe's private
* xattr namespace, and reject all other bitmap bits until DOS attribute,
* timestamp, resource-fork, and entry-id lookup semantics are deliberately
* wired in.
* Information, persist only the file attribute word and/or 32-byte FinderInfo
* block in mars_nwe's private xattr namespace, and reject all other bitmap
* bits until DOS attribute, timestamp, resource-fork, and entry-id lookup
* semantics are deliberately wired in.
*/
{
uint8 volume_number;
@@ -1109,6 +1121,7 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len,
char unixname[PATH_MAX];
struct stat stbuff;
int result;
uint16 log_attrs = 0;
if (afp_len < 9) {
XDPRINTF((2,0, "%s rejected: short request len=%d",
@@ -1137,16 +1150,39 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len,
return(-0x9c); /* Invalid Path until persistent entry-id lookup exists */
}
if (request_mask != AFP_FILE_BITMAP_FINDER_INFO) {
if (request_mask & ~(AFP_FILE_BITMAP_ATTRIBUTES | AFP_FILE_BITMAP_FINDER_INFO)) {
XDPRINTF((2,0, "%s rejected: unsupported bitmap vol=%d entry=0x%08x mask=0x%04x path='%s'",
call_name, (int)volume_number, request_entry_id, request_mask,
visable_data(afp_req + 9, path_len)));
return(-0x9c); /* keep unsupported write semantics conservative */
}
if (!request_mask) {
XDPRINTF((2,0, "%s rejected: empty bitmap vol=%d entry=0x%08x path='%s'",
call_name, (int)volume_number, request_entry_id,
visable_data(afp_req + 9, path_len)));
return(-0x9c);
}
data_off = 9 + path_len;
if (data_off & 1) data_off++;
if (afp_len < data_off + NWATALK_FINDER_INFO_LEN) {
if ((request_mask & AFP_FILE_BITMAP_ATTRIBUTES) && afp_len < data_off + 2) {
XDPRINTF((2,0, "%s rejected: short attribute data len=%d data_off=%d",
call_name, afp_len, data_off));
return(-0x7e);
}
if (request_mask & AFP_FILE_BITMAP_ATTRIBUTES) {
uint16 requested_attrs = GET_BE16(afp_req + data_off);
uint16 requested_bits = requested_attrs & ~AFP_ATTR_SETCLR;
if (requested_bits & ~AFP_ATTR_INVISIBLE) {
XDPRINTF((2,0, "%s rejected: unsupported AFP attributes attrs=0x%04x path='%s'",
call_name, requested_attrs, visable_data(afp_req + 9, path_len)));
return(-0x9c);
}
data_off += 2;
}
if ((request_mask & AFP_FILE_BITMAP_FINDER_INFO) && data_off & 1) data_off++;
if ((request_mask & AFP_FILE_BITMAP_FINDER_INFO) &&
afp_len < data_off + NWATALK_FINDER_INFO_LEN) {
XDPRINTF((2,0, "%s rejected: short FinderInfo data len=%d data_off=%d",
call_name, afp_len, data_off));
return(-0x7e);
@@ -1174,22 +1210,30 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len,
return(-0x9c);
}
result = nwatalk_set_finder_info(unixname, afp_req + data_off,
NWATALK_FINDER_INFO_LEN);
if (result < 0)
return(result);
data_off = 9 + path_len;
if (data_off & 1) data_off++;
if (request_mask & AFP_FILE_BITMAP_ATTRIBUTES) {
uint16 requested_attrs = GET_BE16(afp_req + data_off);
log_attrs = requested_attrs;
result = nwatalk_set_afp_attributes(unixname, requested_attrs);
if (result < 0)
return(result);
data_off += 2;
}
if ((request_mask & AFP_FILE_BITMAP_FINDER_INFO) && data_off & 1) data_off++;
if (request_mask & AFP_FILE_BITMAP_FINDER_INFO) {
result = nwatalk_set_finder_info(unixname, afp_req + data_off,
NWATALK_FINDER_INFO_LEN);
if (result < 0)
return(result);
}
XDPRINTF((3,0, "%s: vol=%d request_vol=%d entry=0x%08x mask=0x%04x path='%s' finder_type='%c%c%c%c' finder_creator='%c%c%c%c'",
XDPRINTF((3,0, "%s: vol=%d request_vol=%d entry=0x%08x mask=0x%04x path='%s'%s%s attrs=0x%04x",
call_name, path_volume, (int)volume_number, request_entry_id,
request_mask, visable_data(afp_req + 9, path_len),
afp_req[data_off + 0] ? afp_req[data_off + 0] : ' ',
afp_req[data_off + 1] ? afp_req[data_off + 1] : ' ',
afp_req[data_off + 2] ? afp_req[data_off + 2] : ' ',
afp_req[data_off + 3] ? afp_req[data_off + 3] : ' ',
afp_req[data_off + 4] ? afp_req[data_off + 4] : ' ',
afp_req[data_off + 5] ? afp_req[data_off + 5] : ' ',
afp_req[data_off + 6] ? afp_req[data_off + 6] : ' ',
afp_req[data_off + 7] ? afp_req[data_off + 7] : ' '));
(request_mask & AFP_FILE_BITMAP_ATTRIBUTES) ? " attributes" : "",
(request_mask & AFP_FILE_BITMAP_FINDER_INFO) ? " finder_info" : "",
log_attrs));
return(0);
}