diff --git a/TODO.md b/TODO.md index e937a43..be16c4a 100644 --- a/TODO.md +++ b/TODO.md @@ -112,6 +112,29 @@ Follow-up: a real requester, especially where MARS-NWE currently relies on the existing underlying share implementation. +### AFP Set File Information timestamp coverage + +Current status: + +- AFP Set File Information (`0x09`) and AFP 2.0 Set File Information (`0x10`) + now accept the file modification timestamp bitmap (`0x0010`) for path-backed + file requests. +- Timestamp writes are routed through the existing NetWare `nw_utime_node()` + helper so trustee Modify rights and the established Unix `utime(2)` fallback + behavior are reused instead of inventing AFP-specific timestamp handling. +- The Linux smoke helper verifies the AFP date/time fields via the follow-up + Get File Information response, and the smoke suite also records `stat -c %Y` + for the backing Unix file. + +Follow-up: + +- Add directory timestamp handling once the shared AFP path/object resolver grows + directory-specific Set File Information semantics. +- Keep create/access/backup timestamp writes disabled until their exact AFP to + NetWare/Unix metadata mapping is deliberately wired and tested. +- Keep Delete/Rename/Create/Remove for later patches that can reuse the existing + NetWare server helpers and trustee checks. + ### Extended volume information field mapping Current status: diff --git a/src/nwconn.c b/src/nwconn.c index f3fbc91..d647379 100644 --- a/src/nwconn.c +++ b/src/nwconn.c @@ -1095,6 +1095,7 @@ static int afp_get_file_information(uint8 *afp_req, int afp_len, #define AFP_FILE_BITMAP_ATTRIBUTES 0x0001 +#define AFP_FILE_BITMAP_MODIFY_DATE 0x0010 #define AFP_FILE_BITMAP_FINDER_INFO 0x0020 #define AFP_ATTR_INVISIBLE 0x0001 #define AFP_ATTR_SYSTEM 0x0004 @@ -1111,9 +1112,12 @@ 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 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 + * Information, persist only the file attribute word, the file modification timestamp, + * and/or 32-byte FinderInfo block. Attribute and FinderInfo data live in + * mars_nwe's private xattr namespace; 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 + * bitmap bits until DOS attribute, resource-fork, and entry-id lookup * semantics are deliberately wired in. */ { @@ -1127,6 +1131,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_mtime = (time_t)-1; if (afp_len < 9) { XDPRINTF((2,0, "%s rejected: short request len=%d", @@ -1155,7 +1160,8 @@ 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_FINDER_INFO)) { + if (request_mask & ~(AFP_FILE_BITMAP_ATTRIBUTES | AFP_FILE_BITMAP_MODIFY_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, visable_data(afp_req + 9, path_len))); @@ -1185,6 +1191,13 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, } data_off += 2; } + if ((request_mask & AFP_FILE_BITMAP_MODIFY_DATE) && afp_len < data_off + 4) { + XDPRINTF((2,0, "%s rejected: short modify timestamp data len=%d data_off=%d", + call_name, afp_len, data_off)); + return(-0x7e); + } + if (request_mask & AFP_FILE_BITMAP_MODIFY_DATE) + data_off += 4; 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) { @@ -1225,6 +1238,20 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, return(result); data_off += 2; } + if (request_mask & AFP_FILE_BITMAP_MODIFY_DATE) { + time_t new_mtime = nw_2_un_time(afp_req + data_off, afp_req + data_off + 2); + if (new_mtime == (time_t)-1) { + XDPRINTF((2,0, "%s rejected: invalid modify timestamp path='%s'", + call_name, visable_data(afp_req + 9, path_len))); + return(-0x8c); + } + result = nw_utime_node(path_volume, (uint8 *)unixname, &stbuff, new_mtime); + if (result < 0) + return(result); + log_mtime = new_mtime; + if (!stat(unixname, &stbuff)) {} + data_off += 4; + } 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, @@ -1233,12 +1260,13 @@ 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 attrs=0x%04x", + 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", 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_MODIFY_DATE) ? " modify_time" : "", (request_mask & AFP_FILE_BITMAP_FINDER_INFO) ? " finder_info" : "", - log_attrs)); + log_attrs, (long)log_mtime)); return(0); } diff --git a/tests/linux/README.md b/tests/linux/README.md index 10d9ca0..9f44ee7 100644 --- a/tests/linux/README.md +++ b/tests/linux/README.md @@ -489,13 +489,14 @@ NCP 0x2222/35/16 AFP 2.0 Set File Information ``` The helper exercises two deliberately narrow write-safe AFP metadata subsets: -the file FinderInfo bitmap (`0x0020`) and the file Attributes bitmap (`0x0001`) -restricted to metadata-only file flags: Finder Invisible, System, and Backup. -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 +the file FinderInfo bitmap (`0x0020`), the file Attributes bitmap (`0x0001`) +restricted to metadata-only file flags: Finder Invisible, System, and Backup, +and the file modification timestamp bitmap (`0x0010`). 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`, routes modification timestamp +writes through the existing NetWare timestamp helper, 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. @@ -614,15 +615,36 @@ AFP 2.0 Set File Information: vol=0 request_vol=0 entry=0x00000000 mask=0x0001 p AFP 2.0 Set File Information: vol=0 request_vol=0 entry=0x00000000 mask=0x0001 path='SYS:PUBLIC/pmdflts.ini' attributes attrs=0x0004 AFP 2.0 Set File Information: vol=0 request_vol=0 entry=0x00000000 mask=0x0001 path='SYS:PUBLIC/pmdflts.ini' attributes attrs=0x8040 ``` + +Modification timestamp writes are deliberately routed through the existing +NetWare `nw_utime_node()` path so trustee Modify rights and the established +`utime(2)` fallback behavior stay shared with classic NCP timestamp updates. +The first timestamp smoke uses a fixed Unix epoch that the helper converts into +the AFP/NW DOS date+time fields and verifies through the follow-up Get File +Information response: + +```sh +./tests/linux/afp_set_file_info_smoke \ + -S MARS -U SUPERVISOR -P secret \ + --timestamp-only --mtime-epoch 1700000000 \ + SYS:PUBLIC/pmdflts.ini + +stat -c 'mtime_epoch=%Y mtime=%y' /var/mars_nwe/SYS/public/pmdflts.ini +``` + +This currently remains file-only and path-backed, just like the FinderInfo and +metadata-attribute Set File Information probes; directory timestamps and +Entry-ID-only Set File Information are left for later resolver work. + The legacy `0x09` endpoint is deliberately routed through the same narrow -metadata-only implementation as AFP 2.0 `0x10`; it does not add create, rename, -delete, timestamp, or fork-write semantics. +implementation as AFP 2.0 `0x10`; it does not add create, rename, delete, +directory timestamp, or fork-write semantics. All other Set File Information bitmap bits and AFP attribute bits, including NoWrite, NoRename, NoDelete, NoCopy, and the computed data/resource-fork-open -flags, are intentionally rejected for now. That keeps timestamp, -DOS/NetWare mode-bit mapping, enforcement, resource-fork, and Entry-ID-only -write semantics out of this metadata-only smoke path. +flags, are intentionally rejected for now. That keeps create/access/backup +timestamps, DOS/NetWare mode-bit mapping, enforcement, resource-fork, and +Entry-ID-only write semantics out of this conservative smoke path. If the server was built without the optional Netatalk/libatalk backend, use `--allow-invalid-namespace` for the expected negative test. Use diff --git a/tests/linux/afp_set_file_info_smoke.c b/tests/linux/afp_set_file_info_smoke.c index 0d4f0b0..7bcaee0 100644 --- a/tests/linux/afp_set_file_info_smoke.c +++ b/tests/linux/afp_set_file_info_smoke.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -23,6 +24,7 @@ #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 @@ -39,7 +41,7 @@ static void usage(const char *prog) "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] " - "[--finder-info-only|--attributes-only] " + "[--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" @@ -49,8 +51,9 @@ 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 --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, prog, prog, prog, prog, prog); } static int parse_u32(const char *text, uint32_t *value) @@ -103,6 +106,27 @@ static int copy_fourcc(const char *text, uint8_t out[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]) @@ -144,8 +168,10 @@ int main(int argc, char **argv) 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 + 1 + 32]; + 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; @@ -153,6 +179,7 @@ int main(int argc, char **argv) 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); @@ -219,10 +246,22 @@ int main(int argc, char **argv) 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; + request_mask &= ~(AFP_FINDER_INFO_MASK | AFP_MODIFY_DATE_MASK); } else if (!strcmp(argv[i], "--finder-info-only")) { - request_mask &= ~AFP_ATTRIBUTES_MASK; + 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"); @@ -285,7 +324,17 @@ int main(int argc, char **argv) cpu_to_be16(attr_request, request + data_off); data_off += 2; } - if ((request_mask & AFP_FINDER_INFO_MASK) && (request_mask & AFP_ATTRIBUTES_MASK) && + 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) { @@ -334,6 +383,16 @@ int main(int argc, char **argv) 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, @@ -343,8 +402,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 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 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); diff --git a/tests/linux/afp_smoke_suite.sh b/tests/linux/afp_smoke_suite.sh index b2a9499..be8b751 100755 --- a/tests/linux/afp_smoke_suite.sh +++ b/tests/linux/afp_smoke_suite.sh @@ -17,6 +17,7 @@ LOG_FILE="/var/log/mars_nwe/nw.log" OUT_FILE="" FINDER_TYPE="TEXT" FINDER_CREATOR="MARS" +TIMESTAMP_EPOCH="1700000000" KEEP_GOING=1 CAPTURE_LOG=1 @@ -31,6 +32,7 @@ Options: --out FILE Write the complete report to FILE as well as stdout --type FOURCC FinderInfo type written by Set File Info (default: $FINDER_TYPE) --creator FOURCC FinderInfo creator written by Set File Info (default: $FINDER_CREATOR) + --mtime-epoch SECONDS AFP modify timestamp to write (default: $TIMESTAMP_EPOCH) --no-log Do not tail/grep the server log --stop-on-failure Stop after the first failing smoke command -h, --help Show this help @@ -61,6 +63,8 @@ while [ $# -gt 0 ]; do FINDER_TYPE=$2; shift 2 ;; --creator) FINDER_CREATOR=$2; shift 2 ;; + --mtime-epoch) + TIMESTAMP_EPOCH=$2; shift 2 ;; --no-log) CAPTURE_LOG=0; shift ;; --stop-on-failure) @@ -170,6 +174,7 @@ emit "unix_path=$UNIX_PATH" emit "log=$LOG_FILE" emit "finder_type=$FINDER_TYPE" emit "finder_creator=$FINDER_CREATOR" +emit "mtime_epoch=$TIMESTAMP_EPOCH" for helper in \ afp_entry_id_smoke \ @@ -265,6 +270,12 @@ run_cmd \ "$SCRIPT_DIR/afp_set_file_info_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" \ --afp09 --attributes-only --clear-invisible "$NETWARE_PATH" +run_cmd \ + "AFP Set File Information Modify Timestamp" \ + "./afp_set_file_info_smoke $COMMON_PRINT --timestamp-only --mtime-epoch '$TIMESTAMP_EPOCH' '$NETWARE_PATH'" \ + "$SCRIPT_DIR/afp_set_file_info_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" \ + --timestamp-only --mtime-epoch "$TIMESTAMP_EPOCH" "$NETWARE_PATH" + run_cmd \ "AFP Set File Information System" \ "./afp_set_file_info_smoke $COMMON_PRINT --attributes-only --system '$NETWARE_PATH'" \ @@ -289,6 +300,13 @@ run_cmd \ "$SCRIPT_DIR/afp_set_file_info_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" \ --attributes-only --clear-backup "$NETWARE_PATH" +if command -v stat >/dev/null 2>&1; then + run_cmd \ + "Linux stat: AFP Modify Timestamp" \ + "stat -c 'mtime_epoch=%Y mtime=%y' '$UNIX_PATH'" \ + stat -c 'mtime_epoch=%Y mtime=%y' "$UNIX_PATH" +fi + if command -v getfattr >/dev/null 2>&1; then run_cmd \ "Linux xattr: AFP FinderInfo" \