Files
mars-nwe/tests/linux/afp_set_file_info_smoke.c
OpenAI 55fdf64c8e
All checks were successful
Source release / source-package (push) Successful in 46s
nwconn: align AFP attribute bits with WebSDK
The WebSDK/NCP AFP File Information records use a distinct SetInfo request bitmap and attribute word.  The previous smoke-oriented implementation reused the low response-bit positions for Set Attributes, Modify Date/Time, FinderInfo, Hidden/Invisible, System, and Archive.  That made the current tests pass, but it was not faithful to the documented header semantics and risked keeping AFP metadata parallel to existing NetWare attributes.

Switch Set File Information to the documented request bitmap values: 0x0100 for Attributes, 0x1000 for Modify Date/Time, and 0x4000 for FinderInfo.  Switch the AFP attribute word to the documented NetWare-style bits: Hidden 0x0200, System 0x0400, Subdirectory 0x1000, and Archive 0x2000.

Map Hidden, System, and Archive through the existing NetWare attribute store via FILE_ATTR_H, FILE_ATTR_S, and FILE_ATTR_A.  This keeps AFP Set/Get/Scan aligned with mars_nwe's existing attribute helper instead of maintaining duplicate AFP-only xattr state.  FinderInfo remains AFP metadata and still uses the Modify-rights gate added earlier.

Update the Linux smoke helper and suite to use --hidden / --clear-hidden while keeping --invisible / --clear-invisible as compatibility aliases.  Document the corrected WebSDK bit values and the convergence rule that NetWare attributes must use mars_nwe NetWare helpers.

Tests: git diff --check; bash -n tests/linux/afp_smoke_suite.sh; gcc -Iinclude -I/mnt/data/stubs -fsyntax-only tests/linux/afp_set_file_info_smoke.c
2026-05-30 17:48:51 +02:00

437 lines
16 KiB
C

/*
* Linux smoke test for NetWare AFP Set File Information metadata writes.
*/
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.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 AFP20_GET_FILE_INFORMATION 0x0f
#define AFP_SET_FILE_INFORMATION 0x09
#define AFP20_SET_FILE_INFORMATION 0x10
#define AFP_ATTRIBUTES_MASK 0x0100
#define AFP_MODIFY_DATE_MASK 0x1000
#define AFP_FINDER_INFO_MASK 0x4000
#define AFP_ATTR_HIDDEN 0x0200
#define AFP_ATTR_SYSTEM 0x0400
#define AFP_ATTR_ARCHIVE 0x2000
#define AFP_ATTR_SETCLR 0x8000
#define AFP_ATTR_STORED_MASK (AFP_ATTR_HIDDEN | AFP_ATTR_SYSTEM | AFP_ATTR_ARCHIVE)
#define AFP_REPLY_LEN 120
#define NWE_INVALID_NAMESPACE 0xbf
#define NWE_INVALID_PATH 0x9c
static void usage(const char *prog)
{
fprintf(stderr,
"Usage: %s [--afp09|--afp20] [--expect-completion CODE] [--allow-invalid-namespace] [--allow-invalid-path] "
"[--volume N] [--entry-id ID] [--type FOUR] [--creator FOUR] "
"[--hidden|--clear-hidden|--invisible|--clear-invisible|--system|--clear-system|--archive|--clear-archive] "
"[--mtime-epoch SECONDS] [--finder-info-only|--attributes-only|--timestamp-only] "
"[ncpfs options] PATH\n"
"\n"
"ncpfs options are parsed by ncp_initialize(), for example:\n"
" -S SERVER -U USER -P PASSWORD -n\n"
"\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 --hidden --attributes-only SYS:PUBLIC/pmdflts.ini\n"
" %s -S MARS -U SUPERVISOR -P secret --archive --attributes-only SYS:PUBLIC/pmdflts.ini\n"
" %s -S MARS -U SUPERVISOR -P secret --mtime-epoch 1700000000 --timestamp-only SYS:PUBLIC/pmdflts.ini\n"
" %s --allow-invalid-namespace -S MARS SYS:PUBLIC/pmdflts.ini\n"
" %s --expect-completion 0x8c -S MARS -U NOPASSUSER -n --finder-info-only SYS:PUBLIC/pmdflts.ini\n",
prog, prog, prog, prog, 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 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) |
((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) |
p[3];
}
static void cpu_to_be16(uint16_t v, uint8_t p[2])
{
p[0] = (uint8_t)(v >> 8);
p[1] = (uint8_t)v;
}
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 int copy_fourcc(const char *text, uint8_t out[4])
{
size_t len = strlen(text);
if (len != 4)
return -1;
memcpy(out, text, 4);
return 0;
}
static int epoch_to_nw_time(uint32_t epoch, uint8_t out[4])
{
time_t t = (time_t)epoch;
struct tm *tmv = localtime(&t);
uint16_t date;
uint16_t timev;
if (!tmv || tmv->tm_year + 1900 < 1980 || tmv->tm_year + 1900 > 2107)
return -1;
date = (uint16_t)(((tmv->tm_year + 1900 - 1980) << 9) |
((tmv->tm_mon + 1) << 5) |
tmv->tm_mday);
timev = (uint16_t)((tmv->tm_hour << 11) |
(tmv->tm_min << 5) |
(tmv->tm_sec / 2));
cpu_to_be16(date, out + 0);
cpu_to_be16(timev, out + 2);
return 0;
}
static NWCCODE afp_get_file_info(NWCONN_HANDLE conn, const char *path,
uint32_t volume_number, uint32_t entry_id,
uint8_t reply_buf[AFP_REPLY_LEN])
{
NW_FRAGMENT reply;
uint8_t request[1 + 4 + 2 + 1 + 255];
size_t path_len = strlen(path);
memset(request, 0, sizeof(request));
request[0] = (uint8_t)volume_number;
cpu_to_be32(entry_id, request + 1);
cpu_to_be16(0xffff, request + 5);
request[7] = (uint8_t)path_len;
memcpy(request + 8, path, path_len);
memset(reply_buf, 0, AFP_REPLY_LEN);
reply.fragAddr.rw = reply_buf;
reply.fragSize = AFP_REPLY_LEN;
return NWRequestSimple(conn,
NCPC_SFN(0x23, AFP20_GET_FILE_INFORMATION),
request,
8 + path_len,
&reply);
}
int main(int argc, char **argv)
{
NWCONN_HANDLE conn;
long init_err = 0;
const char *path = NULL;
int allow_invalid_namespace = 0;
int allow_invalid_path = 0;
int expect_completion = -1;
uint32_t volume_number = 0;
uint32_t entry_id = 0;
uint8_t finder_info[32];
uint8_t set_subfunction = AFP20_SET_FILE_INFORMATION;
uint16_t request_mask = AFP_FINDER_INFO_MASK;
uint16_t attr_request = 0;
uint16_t attr_verify_mask = 0;
uint16_t attr_expected = 0;
uint8_t modify_time[4];
int have_modify_time = 0;
uint8_t verify_buf[AFP_REPLY_LEN];
uint8_t request[1 + 4 + 2 + 1 + 255 + 1 + 2 + 4 + 1 + 32];
size_t path_len;
size_t afp_data_off;
size_t data_off;
int i;
NWCCODE err;
memset(finder_info, 0, sizeof(finder_info));
memset(modify_time, 0, sizeof(modify_time));
memcpy(finder_info + 0, "TEXT", 4);
memcpy(finder_info + 4, "MARS", 4);
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], "--afp09")) {
set_subfunction = AFP_SET_FILE_INFORMATION;
} else if (!strcmp(argv[i], "--afp20")) {
set_subfunction = AFP20_SET_FILE_INFORMATION;
} else if (!strcmp(argv[i], "--expect-completion")) {
uint32_t completion;
if (++i >= argc || parse_u32(argv[i], &completion) || completion > 255) {
fprintf(stderr, "invalid --expect-completion value\n");
ncp_close(conn);
return 2;
}
expect_completion = (int)completion;
} else 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;
}
} else if (!strcmp(argv[i], "--hidden") || !strcmp(argv[i], "--invisible")) {
request_mask |= AFP_ATTRIBUTES_MASK;
attr_request = AFP_ATTR_SETCLR | AFP_ATTR_HIDDEN;
attr_verify_mask = AFP_ATTR_HIDDEN;
attr_expected = AFP_ATTR_HIDDEN;
} else if (!strcmp(argv[i], "--clear-hidden") || !strcmp(argv[i], "--clear-invisible")) {
request_mask |= AFP_ATTRIBUTES_MASK;
attr_request = AFP_ATTR_HIDDEN;
attr_verify_mask = AFP_ATTR_HIDDEN;
attr_expected = 0;
} else if (!strcmp(argv[i], "--system")) {
request_mask |= AFP_ATTRIBUTES_MASK;
attr_request = AFP_ATTR_SETCLR | AFP_ATTR_SYSTEM;
attr_verify_mask = AFP_ATTR_SYSTEM;
attr_expected = AFP_ATTR_SYSTEM;
} else if (!strcmp(argv[i], "--clear-system")) {
request_mask |= AFP_ATTRIBUTES_MASK;
attr_request = AFP_ATTR_SYSTEM;
attr_verify_mask = AFP_ATTR_SYSTEM;
attr_expected = 0;
} else if (!strcmp(argv[i], "--archive") || !strcmp(argv[i], "--backup")) {
request_mask |= AFP_ATTRIBUTES_MASK;
attr_request = AFP_ATTR_SETCLR | AFP_ATTR_ARCHIVE;
attr_verify_mask = AFP_ATTR_ARCHIVE;
attr_expected = AFP_ATTR_ARCHIVE;
} else if (!strcmp(argv[i], "--clear-archive") || !strcmp(argv[i], "--clear-backup")) {
request_mask |= AFP_ATTRIBUTES_MASK;
attr_request = AFP_ATTR_ARCHIVE;
attr_verify_mask = AFP_ATTR_ARCHIVE;
attr_expected = 0;
} else if (!strcmp(argv[i], "--mtime-epoch")) {
uint32_t epoch;
if (++i >= argc || parse_u32(argv[i], &epoch) || epoch_to_nw_time(epoch, modify_time)) {
fprintf(stderr, "invalid --mtime-epoch value\n");
ncp_close(conn);
return 2;
}
request_mask |= AFP_MODIFY_DATE_MASK;
have_modify_time = 1;
} else if (!strcmp(argv[i], "--attributes-only")) {
request_mask &= ~(AFP_FINDER_INFO_MASK | AFP_MODIFY_DATE_MASK);
} else if (!strcmp(argv[i], "--finder-info-only")) {
request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_MODIFY_DATE_MASK);
} else if (!strcmp(argv[i], "--timestamp-only")) {
request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_FINDER_INFO_MASK);
request_mask |= AFP_MODIFY_DATE_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");
ncp_close(conn);
return 2;
}
} else if (!strcmp(argv[i], "--creator")) {
if (++i >= argc || copy_fourcc(argv[i], finder_info + 4)) {
fprintf(stderr, "invalid --creator value, expected four characters\n");
ncp_close(conn);
return 2;
}
} 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) {
fprintf(stderr, "missing PATH\n");
usage(argv[0]);
ncp_close(conn);
return 2;
}
path_len = strlen(path);
if (path_len > 255) {
fprintf(stderr, "PATH is too long for AFP Set File Information: %zu\n", path_len);
ncp_close(conn);
return 2;
}
memset(request, 0, sizeof(request));
request[0] = (uint8_t)volume_number;
cpu_to_be32(entry_id, request + 1);
cpu_to_be16(request_mask, request + 5);
request[7] = (uint8_t)path_len;
memcpy(request + 8, path, path_len);
/*
* The server-side AFP request buffer includes the NCP AFP subfunction byte
* at offset 0. This smoke helper passes only the subfunction payload to
* NWRequestSimple(), so every server offset after the leading function byte
* maps to request_offset - 1 here. Align once in AFP/server coordinates,
* then subtract one; do not align the payload offset again or FinderInfo-only
* requests with an even path length get a leading zero byte persisted.
*/
afp_data_off = 1 + 8 + path_len;
if (afp_data_off & 1)
afp_data_off++;
data_off = afp_data_off - 1;
if (request_mask & AFP_ATTRIBUTES_MASK) {
cpu_to_be16(attr_request, request + data_off);
data_off += 2;
}
if (request_mask & AFP_MODIFY_DATE_MASK) {
if (!have_modify_time) {
fprintf(stderr, "--timestamp-only requires --mtime-epoch\n");
ncp_close(conn);
return 2;
}
memcpy(request + data_off, modify_time, sizeof(modify_time));
data_off += sizeof(modify_time);
}
if ((request_mask & AFP_FINDER_INFO_MASK) &&
(request_mask & (AFP_ATTRIBUTES_MASK | AFP_MODIFY_DATE_MASK)) &&
((data_off + 1) & 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, set_subfunction),
request,
data_off,
NULL);
if (expect_completion >= 0) {
if ((((unsigned int)err) & 0xff) == (unsigned int)expect_completion) {
printf("AFP Set File Information returned expected completion 0x%02x: subfunction=0x%02x path=%s bitmap=0x%04x\n",
expect_completion, set_subfunction, path, request_mask);
ncp_close(conn);
return 0;
}
fprintf(stderr,
"AFP Set File Information expected completion 0x%02x but got 0x%02x (%u): subfunction=0x%02x path=%s bitmap=0x%04x\n",
expect_completion, (unsigned int)err & 0xff, (unsigned int)err,
set_subfunction, path, request_mask);
ncp_close(conn);
return 1;
}
if (err == NWE_INVALID_NAMESPACE && allow_invalid_namespace) {
printf("AFP Set File Information returned invalid namespace as expected for path=%s\n", path);
ncp_close(conn);
return 0;
}
if (err == NWE_INVALID_PATH && allow_invalid_path) {
printf("AFP Set File Information returned invalid path as expected for path=%s\n", path);
ncp_close(conn);
return 0;
}
if (err) {
fprintf(stderr,
"AFP Set File Information failed: completion=0x%02x (%u) path=%s\n",
(unsigned int)err & 0xff, (unsigned int)err, path);
ncp_close(conn);
return 1;
}
err = afp_get_file_info(conn, path, volume_number, entry_id, verify_buf);
if (err) {
fprintf(stderr,
"AFP Get File Information verify failed: completion=0x%02x (%u) path=%s\n",
(unsigned int)err & 0xff, (unsigned int)err, path);
ncp_close(conn);
return 1;
}
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);
ncp_close(conn);
return 1;
}
if ((request_mask & AFP_MODIFY_DATE_MASK) &&
memcmp(verify_buf + 24, modify_time, sizeof(modify_time))) {
fprintf(stderr,
"AFP Set File Information timestamp verify mismatch: path=%s got=0x%04x%04x expected=0x%04x%04x\n",
path, be16_to_cpu(verify_buf + 24), be16_to_cpu(verify_buf + 26),
be16_to_cpu(modify_time + 0), be16_to_cpu(modify_time + 2));
ncp_close(conn);
return 1;
}
if ((request_mask & AFP_ATTRIBUTES_MASK) &&
((be16_to_cpu(verify_buf + 8) & attr_verify_mask) != attr_expected)) {
fprintf(stderr,
"AFP Set File Information attr verify mismatch: path=%s attrs=0x%04x verify_mask=0x%04x expected=0x%04x\n",
path, be16_to_cpu(verify_buf + 8), attr_verify_mask, attr_expected);
ncp_close(conn);
return 1;
}
printf("AFP Set File Info subfunction=0x%02x path=%s bitmap=0x%04x attrs=0x%04x modify=0x%04x%04x finder_type=%.4s finder_creator=%.4s entry_id=0x%08x verified\n",
set_subfunction, path, request_mask, be16_to_cpu(verify_buf + 8),
be16_to_cpu(verify_buf + 24), be16_to_cpu(verify_buf + 26),
finder_info + 0, finder_info + 4, be32_to_cpu(verify_buf + 0));
ncp_close(conn);
return 0;
}