All checks were successful
Source release / source-package (push) Successful in 51s
Extend the conservative AFP Set File Information implementation to accept the file modification timestamp bitmap for path-backed file requests. The WebSDK/NWAFP Set File Information payload carries the timestamp in the same bitmap-ordered parameter stream as file attributes and FinderInfo, so the parser now admits the documented modification timestamp field while continuing to reject every other Set File Information bitmap bit. Do not implement a new AFP-specific timestamp backend. After resolving the raw VOL:-style smoke path to the effective mars_nwe volume and Unix path, convert the AFP/NW DOS date+time fields to time_t and route the update through the existing nw_utime_node() helper. That keeps trustee Modify-right checks and the established utime(2) fallback behavior shared with classic NetWare/NCP timestamp updates. Keep the implementation deliberately file-only and path-backed. Directory timestamps, create/access/backup timestamp fields, Entry-ID-only Set File Information, resource-fork semantics, DOS attribute mapping, Delete, Rename, Create, and Remove stay TODO so later patches can wire them to the existing NetWare helpers with focused smoke coverage. Update afp_set_file_info_smoke with --mtime-epoch and --timestamp-only, verify the written AFP date/time via the follow-up Get File Information record, and extend afp_smoke_suite.sh to run the timestamp probe and record the backing Linux stat output. The suite helper is already copied as a build target, so the new test is propagated into the build tree by the normal tests build. 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
413 lines
15 KiB
C
413 lines
15 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 0x0001
|
|
#define AFP_MODIFY_DATE_MASK 0x0010
|
|
#define AFP_FINDER_INFO_MASK 0x0020
|
|
#define AFP_ATTR_INVISIBLE 0x0001
|
|
#define AFP_ATTR_SYSTEM 0x0004
|
|
#define AFP_ATTR_BACKUP 0x0040
|
|
#define AFP_ATTR_SETCLR 0x8000
|
|
#define AFP_ATTR_STORED_MASK (AFP_ATTR_INVISIBLE | AFP_ATTR_SYSTEM | AFP_ATTR_BACKUP)
|
|
#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] [--allow-invalid-namespace] [--allow-invalid-path] "
|
|
"[--volume N] [--entry-id ID] [--type FOUR] [--creator FOUR] "
|
|
"[--invisible|--clear-invisible|--system|--clear-system|--backup|--clear-backup] "
|
|
"[--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 --invisible --attributes-only SYS:PUBLIC/pmdflts.ini\n"
|
|
" %s -S MARS -U SUPERVISOR -P secret --backup --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",
|
|
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;
|
|
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], "--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], "--invisible")) {
|
|
request_mask |= AFP_ATTRIBUTES_MASK;
|
|
attr_request = AFP_ATTR_SETCLR | AFP_ATTR_INVISIBLE;
|
|
attr_verify_mask = 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_verify_mask = AFP_ATTR_INVISIBLE;
|
|
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], "--backup")) {
|
|
request_mask |= AFP_ATTRIBUTES_MASK;
|
|
attr_request = AFP_ATTR_SETCLR | AFP_ATTR_BACKUP;
|
|
attr_verify_mask = AFP_ATTR_BACKUP;
|
|
attr_expected = AFP_ATTR_BACKUP;
|
|
} else if (!strcmp(argv[i], "--clear-backup")) {
|
|
request_mask |= AFP_ATTRIBUTES_MASK;
|
|
attr_request = AFP_ATTR_BACKUP;
|
|
attr_verify_mask = AFP_ATTR_BACKUP;
|
|
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 (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;
|
|
}
|