From 31a9234c8b6441aef8ccf05b63d01d18a8c5d4e4 Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Sat, 30 May 2026 17:25:15 +0000 Subject: [PATCH] nwconn: route AFP access timestamps through atime --- TODO.md | 9 ++-- src/nwconn.c | 67 +++++++++++++++++++++++---- tests/linux/README.md | 18 +++++++ tests/linux/afp_set_file_info_smoke.c | 58 ++++++++++++++++++----- tests/linux/afp_smoke_suite.sh | 6 +++ 5 files changed, 134 insertions(+), 24 deletions(-) diff --git a/TODO.md b/TODO.md index 361cb29..5ac92f6 100644 --- a/TODO.md +++ b/TODO.md @@ -414,9 +414,9 @@ Refactor/wrapper follow-up: ExecuteOnly, and Shareable still need either a real mapping through existing attribute helpers or an explicit unsupported result. - Extend Set File Information timestamps only through existing mars_nwe helpers: - Modify is routed through `nw_utime_node()`, while Create, Access, and Backup - Date/Time must be checked against existing NetWare/archive metadata support - before being enabled. + Modify is routed through `nw_utime_node()`, Access through the existing + `st_atime`/`utime()` path, and Create/Backup through the existing + `nwarchive.c` metadata helpers. - Normalize Get/Scan File Information so each field comes from the existing mars_nwe source of truth where possible: NetWare attributes, effective rights, file size, timestamps, owner information, archive date/time, FinderInfo, @@ -525,4 +525,5 @@ run confirmed the backing mars_nwe metadata xattr: intended split: Archive as a file attribute stays on the NetWare attribute path, while Backup Date/Time is stored by `nwarchive.c`. -- AFP Create Date/Time now uses `mars_nwe_set_file_info()` / `mars_nwe_get_file_info()` and the existing `user.org.mars-nwe.netware.fileinfo` xattr; Access remains to be wired through the existing atime path. +- AFP Create Date/Time now uses `mars_nwe_set_file_info()` / `mars_nwe_get_file_info()` and the existing `user.org.mars-nwe.netware.fileinfo` xattr. +- AFP Access Date/Time now uses the existing `st_atime`/`utime()` path and preserves `st_mtime`; no AFP-specific xattr is added. diff --git a/src/nwconn.c b/src/nwconn.c index 2a47247..66ebc76 100644 --- a/src/nwconn.c +++ b/src/nwconn.c @@ -35,6 +35,7 @@ #include #include #include +#include #if !CALL_NWCONN_OVER_SOCKET #include #include @@ -1114,6 +1115,7 @@ static void afp_leaf_name_from_path(uint8 *dst, int dst_len, } #define AFP_FILE_BITMAP_ATTRIBUTES 0x0100 +#define AFP_FILE_BITMAP_ACCESS_DATE 0x0400 #define AFP_FILE_BITMAP_CREATE_DATE 0x0800 #define AFP_FILE_BITMAP_MODIFY_DATE 0x1000 #define AFP_FILE_BITMAP_BACKUP_DATE 0x2000 @@ -1358,6 +1360,28 @@ static int afp_check_metadata_modify_rights(int volume, char *unixname, return(0); } + +static int afp_set_access_time(int volume, char *unixname, struct stat *stb, + time_t new_atime) +{ + struct utimbuf ut; + + if (tru_eff_rights_exists(volume, (uint8 *)unixname, stb, TRUSTEE_M)) + return(-0x8c); + + ut.actime = new_atime; + ut.modtime = stb->st_mtime; + if (!utime(unixname, &ut)) + return(0); + if (seteuid(0)) {} + if (!utime(unixname, &ut)) { + (void)reseteuid(); + return(0); + } + (void)reseteuid(); + return(-0x8c); +} + static int afp_set_file_information(uint8 *afp_req, int afp_len, const char *call_name) /* @@ -1366,12 +1390,13 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, * Netatalk's FPSetFileParams tests use the FinderInfo bitmap as a small, * 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 file attribute word, the file creation timestamp, - * the file modification timestamp, and/or 32-byte FinderInfo block. Attributes are routed through the NetWare attribute path and FinderInfo data lives in + * Information, persist only the file attribute word, the file access timestamp, + * the file creation timestamp, the file modification timestamp, and/or + * 32-byte FinderInfo block. Attributes are routed through the NetWare attribute path and FinderInfo data lives in * mars_nwe's private AFP xattr namespace; Create Date/Time reuses the existing - * nwarchive fileinfo helper, while the modification timestamp is routed - * through the existing NetWare timestamp helper so trustee Modify rights and - * utime handling stay shared with the classic NCP paths. Reject all other + * nwarchive fileinfo helper, while access and modification timestamps are + * routed through the existing POSIX atime/mtime path with trustee Modify + * rights enforced before utime(). Reject all other * bitmap bits until DOS attribute, resource-fork, and entry-id lookup * semantics are deliberately wired in. */ @@ -1386,6 +1411,7 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, struct stat stbuff; int result; uint16 log_attrs = 0; + time_t log_atime = (time_t)-1; time_t log_mtime = (time_t)-1; int needs_afp_metadata_modify = 0; @@ -1416,8 +1442,9 @@ 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_ATTRIBUTES | AFP_FILE_BITMAP_CREATE_DATE | - AFP_FILE_BITMAP_MODIFY_DATE | AFP_FILE_BITMAP_BACKUP_DATE | + if (request_mask & ~(AFP_FILE_BITMAP_ATTRIBUTES | AFP_FILE_BITMAP_ACCESS_DATE | + AFP_FILE_BITMAP_CREATE_DATE | AFP_FILE_BITMAP_MODIFY_DATE | + AFP_FILE_BITMAP_BACKUP_DATE | 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, @@ -1450,6 +1477,13 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, * the NetWare Modify-rights check. FinderInfo remains AFP metadata. */ data_off += 2; } + if ((request_mask & AFP_FILE_BITMAP_ACCESS_DATE) && afp_len < data_off + 4) { + XDPRINTF((2,0, "%s rejected: short access timestamp data len=%d data_off=%d", + call_name, afp_len, data_off)); + return(-0x7e); + } + if (request_mask & AFP_FILE_BITMAP_ACCESS_DATE) + data_off += 4; if ((request_mask & AFP_FILE_BITMAP_CREATE_DATE) && afp_len < data_off + 4) { XDPRINTF((2,0, "%s rejected: short create timestamp data len=%d data_off=%d", call_name, afp_len, data_off)); @@ -1529,6 +1563,20 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, } data_off += 2; } + if (request_mask & AFP_FILE_BITMAP_ACCESS_DATE) { + time_t new_atime = nw_2_un_time(afp_req + data_off, afp_req + data_off + 2); + if (new_atime == (time_t)-1) { + XDPRINTF((2,0, "%s rejected: invalid access timestamp path='%s'", + call_name, visable_data(afp_req + 9, path_len))); + return(-0x8c); + } + result = afp_set_access_time(path_volume, unixname, &stbuff, new_atime); + if (result < 0) + return(result); + log_atime = new_atime; + if (!stat(unixname, &stbuff)) {} + data_off += 4; + } if (request_mask & AFP_FILE_BITMAP_CREATE_DATE) { uint16 create_date = GET_BE16(afp_req + data_off); uint16 create_time = GET_BE16(afp_req + data_off + 2); @@ -1575,15 +1623,16 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, return(result); } - XDPRINTF((3,0, "%s: vol=%d request_vol=%d entry=0x%08x mask=0x%04x path='%s'%s%s%s attrs=0x%04x mtime=%ld", + XDPRINTF((3,0, "%s: vol=%d request_vol=%d entry=0x%08x mask=0x%04x path='%s'%s%s%s%s%s attrs=0x%04x atime=%ld mtime=%ld", call_name, path_volume, (int)volume_number, request_entry_id, request_mask, visable_data(afp_req + 9, path_len), (request_mask & AFP_FILE_BITMAP_ATTRIBUTES) ? " attributes" : "", + (request_mask & AFP_FILE_BITMAP_ACCESS_DATE) ? " access_time" : "", (request_mask & AFP_FILE_BITMAP_CREATE_DATE) ? " create_time" : "", (request_mask & AFP_FILE_BITMAP_MODIFY_DATE) ? " modify_time" : "", (request_mask & AFP_FILE_BITMAP_BACKUP_DATE) ? " backup_time" : "", (request_mask & AFP_FILE_BITMAP_FINDER_INFO) ? " finder_info" : "", - log_attrs, (long)log_mtime)); + log_attrs, (long)log_atime, (long)log_mtime)); return(0); } diff --git a/tests/linux/README.md b/tests/linux/README.md index c4ae52e..835d4e5 100644 --- a/tests/linux/README.md +++ b/tests/linux/README.md @@ -840,6 +840,24 @@ The report showed `bitmap=0x2000`, `backup=0x576eb9aa`, and the backing xattr `1700000000`. +### AFP Access Date/Time smoke + +`afp_set_file_info_smoke` supports the WebSDK Access Date/Time request bitmap +`0x0400` via: + +```sh +./afp_set_file_info_smoke \ + -S MARS -U SUPERVISOR -P secret \ + --access-time-only --access-time-epoch 1700000000 \ + SYS:PUBLIC/pmdflts.ini +``` + +The server stores Access Date/Time through the existing POSIX `st_atime` path, +preserving `st_mtime` with `utime()` and enforcing trustee Modify rights before +changing the timestamp. The AFP file-information record exposes the Access +Date at offset 22; no AFP-specific xattr is added for this NetWare-semantic +timestamp. + ### AFP Create Date/Time smoke `afp_set_file_info_smoke` supports the WebSDK Create Date/Time request bitmap diff --git a/tests/linux/afp_set_file_info_smoke.c b/tests/linux/afp_set_file_info_smoke.c index 8b47fe5..488d4ea 100644 --- a/tests/linux/afp_set_file_info_smoke.c +++ b/tests/linux/afp_set_file_info_smoke.c @@ -24,6 +24,7 @@ #define AFP_SET_FILE_INFORMATION 0x09 #define AFP20_SET_FILE_INFORMATION 0x10 #define AFP_ATTRIBUTES_MASK 0x0100 +#define AFP_ACCESS_DATE_MASK 0x0400 #define AFP_CREATE_DATE_MASK 0x0800 #define AFP_MODIFY_DATE_MASK 0x1000 #define AFP_BACKUP_DATE_MASK 0x2000 @@ -43,7 +44,7 @@ static void usage(const char *prog) "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] " - "[--create-time-epoch SECONDS] [--mtime-epoch SECONDS] [--backup-time-epoch SECONDS] [--finder-info-only|--attributes-only|--create-time-only|--timestamp-only|--backup-time-only] " + "[--access-time-epoch SECONDS] [--create-time-epoch SECONDS] [--mtime-epoch SECONDS] [--backup-time-epoch SECONDS] [--finder-info-only|--attributes-only|--access-time-only|--create-time-only|--timestamp-only|--backup-time-only] " "[ncpfs options] PATH\n" "\n" "ncpfs options are parsed by ncp_initialize(), for example:\n" @@ -53,12 +54,13 @@ static void usage(const char *prog) " %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 --access-time-epoch 1700000000 --access-time-only SYS:PUBLIC/pmdflts.ini\n" " %s -S MARS -U SUPERVISOR -P secret --create-time-epoch 1700000000 --create-time-only SYS:PUBLIC/pmdflts.ini\n" " %s -S MARS -U SUPERVISOR -P secret --mtime-epoch 1700000000 --timestamp-only SYS:PUBLIC/pmdflts.ini\n" " %s -S MARS -U SUPERVISOR -P secret --backup-time-epoch 1700000000 --backup-time-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, prog, prog); + prog, prog, prog, prog, prog, prog, prog, prog, prog, prog); } static int parse_u32(const char *text, uint32_t *value) @@ -174,6 +176,8 @@ int main(int argc, char **argv) uint16_t attr_request = 0; uint16_t attr_verify_mask = 0; uint16_t attr_expected = 0; + uint8_t access_time[4]; + int have_access_time = 0; uint8_t create_time[4]; int have_create_time = 0; uint8_t modify_time[4]; @@ -181,7 +185,7 @@ int main(int argc, char **argv) uint8_t backup_time[4]; int have_backup_time = 0; uint8_t verify_buf[AFP_REPLY_LEN]; - uint8_t request[1 + 4 + 2 + 1 + 255 + 1 + 2 + 4 + 4 + 4 + 1 + 32]; + uint8_t request[1 + 4 + 2 + 1 + 255 + 1 + 2 + 4 + 4 + 4 + 4 + 1 + 32]; size_t path_len; size_t afp_data_off; size_t data_off; @@ -189,6 +193,7 @@ int main(int argc, char **argv) NWCCODE err; memset(finder_info, 0, sizeof(finder_info)); + memset(access_time, 0, sizeof(access_time)); memset(create_time, 0, sizeof(create_time)); memset(modify_time, 0, sizeof(modify_time)); memset(backup_time, 0, sizeof(backup_time)); @@ -266,6 +271,15 @@ int main(int argc, char **argv) attr_request = AFP_ATTR_ARCHIVE; attr_verify_mask = AFP_ATTR_ARCHIVE; attr_expected = 0; + } else if (!strcmp(argv[i], "--access-time-epoch")) { + uint32_t epoch; + if (++i >= argc || parse_u32(argv[i], &epoch) || epoch_to_nw_time(epoch, access_time)) { + fprintf(stderr, "invalid --access-time-epoch value\n"); + ncp_close(conn); + return 2; + } + request_mask |= AFP_ACCESS_DATE_MASK; + have_access_time = 1; } else if (!strcmp(argv[i], "--create-time-epoch")) { uint32_t epoch; if (++i >= argc || parse_u32(argv[i], &epoch) || epoch_to_nw_time(epoch, create_time)) { @@ -294,17 +308,20 @@ int main(int argc, char **argv) request_mask |= AFP_BACKUP_DATE_MASK; have_backup_time = 1; } else if (!strcmp(argv[i], "--attributes-only")) { - request_mask &= ~(AFP_CREATE_DATE_MASK | AFP_FINDER_INFO_MASK | AFP_MODIFY_DATE_MASK | AFP_BACKUP_DATE_MASK); + request_mask &= ~(AFP_ACCESS_DATE_MASK | AFP_CREATE_DATE_MASK | AFP_FINDER_INFO_MASK | AFP_MODIFY_DATE_MASK | AFP_BACKUP_DATE_MASK); } else if (!strcmp(argv[i], "--finder-info-only")) { - request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_CREATE_DATE_MASK | AFP_MODIFY_DATE_MASK | AFP_BACKUP_DATE_MASK); + request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_ACCESS_DATE_MASK | AFP_CREATE_DATE_MASK | AFP_MODIFY_DATE_MASK | AFP_BACKUP_DATE_MASK); } else if (!strcmp(argv[i], "--timestamp-only")) { - request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_CREATE_DATE_MASK | AFP_FINDER_INFO_MASK | AFP_BACKUP_DATE_MASK); + request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_ACCESS_DATE_MASK | AFP_CREATE_DATE_MASK | AFP_FINDER_INFO_MASK | AFP_BACKUP_DATE_MASK); request_mask |= AFP_MODIFY_DATE_MASK; + } else if (!strcmp(argv[i], "--access-time-only")) { + request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_CREATE_DATE_MASK | AFP_FINDER_INFO_MASK | AFP_MODIFY_DATE_MASK | AFP_BACKUP_DATE_MASK); + request_mask |= AFP_ACCESS_DATE_MASK; } else if (!strcmp(argv[i], "--create-time-only")) { - request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_FINDER_INFO_MASK | AFP_MODIFY_DATE_MASK | AFP_BACKUP_DATE_MASK); + request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_ACCESS_DATE_MASK | AFP_FINDER_INFO_MASK | AFP_MODIFY_DATE_MASK | AFP_BACKUP_DATE_MASK); request_mask |= AFP_CREATE_DATE_MASK; } else if (!strcmp(argv[i], "--backup-time-only")) { - request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_CREATE_DATE_MASK | AFP_FINDER_INFO_MASK | AFP_MODIFY_DATE_MASK); + request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_ACCESS_DATE_MASK | AFP_CREATE_DATE_MASK | AFP_FINDER_INFO_MASK | AFP_MODIFY_DATE_MASK); request_mask |= AFP_BACKUP_DATE_MASK; } else if (!strcmp(argv[i], "--type")) { if (++i >= argc || copy_fourcc(argv[i], finder_info + 0)) { @@ -368,6 +385,15 @@ int main(int argc, char **argv) cpu_to_be16(attr_request, request + data_off); data_off += 2; } + if (request_mask & AFP_ACCESS_DATE_MASK) { + if (!have_access_time) { + fprintf(stderr, "--access-time-only requires --access-time-epoch\n"); + ncp_close(conn); + return 2; + } + memcpy(request + data_off, access_time, sizeof(access_time)); + data_off += sizeof(access_time); + } if (request_mask & AFP_CREATE_DATE_MASK) { if (!have_create_time) { fprintf(stderr, "--create-time-only requires --create-time-epoch\n"); @@ -396,7 +422,7 @@ int main(int argc, char **argv) data_off += sizeof(backup_time); } if ((request_mask & AFP_FINDER_INFO_MASK) && - (request_mask & (AFP_ATTRIBUTES_MASK | AFP_CREATE_DATE_MASK | AFP_MODIFY_DATE_MASK | AFP_BACKUP_DATE_MASK)) && + (request_mask & (AFP_ATTRIBUTES_MASK | AFP_ACCESS_DATE_MASK | AFP_CREATE_DATE_MASK | AFP_MODIFY_DATE_MASK | AFP_BACKUP_DATE_MASK)) && ((data_off + 1) & 1)) data_off++; if (request_mask & AFP_FINDER_INFO_MASK) { @@ -459,6 +485,16 @@ int main(int argc, char **argv) return 1; } + if ((request_mask & AFP_ACCESS_DATE_MASK) && + memcmp(verify_buf + 22, access_time, 2)) { + fprintf(stderr, + "AFP Set File Information access timestamp verify mismatch: path=%s got=0x%04x expected_date=0x%04x expected_time=0x%04x\n", + path, be16_to_cpu(verify_buf + 22), + be16_to_cpu(access_time + 0), be16_to_cpu(access_time + 2)); + ncp_close(conn); + return 1; + } + if ((request_mask & AFP_CREATE_DATE_MASK) && memcmp(verify_buf + 20, create_time, 2)) { fprintf(stderr, @@ -498,9 +534,9 @@ int main(int argc, char **argv) return 1; } - printf("AFP Set File Info subfunction=0x%02x path=%s bitmap=0x%04x attrs=0x%04x create=0x%04x modify=0x%04x%04x backup=0x%04x%04x finder_type=%.4s finder_creator=%.4s entry_id=0x%08x verified\n", + printf("AFP Set File Info subfunction=0x%02x path=%s bitmap=0x%04x attrs=0x%04x create=0x%04x access=0x%04x modify=0x%04x%04x backup=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 + 20), + be16_to_cpu(verify_buf + 20), be16_to_cpu(verify_buf + 22), be16_to_cpu(verify_buf + 24), be16_to_cpu(verify_buf + 26), be16_to_cpu(verify_buf + 28), be16_to_cpu(verify_buf + 30), finder_info + 0, finder_info + 4, be32_to_cpu(verify_buf + 0)); diff --git a/tests/linux/afp_smoke_suite.sh b/tests/linux/afp_smoke_suite.sh index 2c22bfe..42971ae 100755 --- a/tests/linux/afp_smoke_suite.sh +++ b/tests/linux/afp_smoke_suite.sh @@ -362,6 +362,12 @@ run_cmd \ "$SCRIPT_DIR/afp_set_file_info_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" \ --afp09 --attributes-only --clear-hidden "$NETWARE_PATH" +run_cmd \ + "AFP Set File Information Access Timestamp" \ + "./afp_set_file_info_smoke $COMMON_PRINT --access-time-only --access-time-epoch '$TIMESTAMP_EPOCH' '$NETWARE_PATH'" \ + "$SCRIPT_DIR/afp_set_file_info_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" \ + --access-time-only --access-time-epoch "$TIMESTAMP_EPOCH" "$NETWARE_PATH" + run_cmd \ "AFP Set File Information Create Timestamp" \ "./afp_set_file_info_smoke $COMMON_PRINT --create-time-only --create-time-epoch '$TIMESTAMP_EPOCH' '$NETWARE_PATH'" \