diff --git a/TODO.md b/TODO.md index 2c7f443..40c19c2 100644 --- a/TODO.md +++ b/TODO.md @@ -499,3 +499,20 @@ set/clear as `0x0400`/`0x0000`, and `Archive` set/clear as FinderInfo. Because all tested attribute bits are now stored through the NetWare attribute path, `user.org.mars-nwe.afp.attributes` may be absent in the final xattr dump; this is expected and no longer counts as a smoke failure. + +### AFP Backup Date/Time convergence + +AFP Set File Information now treats the WebSDK `0x2000` request bitmap as the +Backup Date/Time field, distinct from the `0x2000` Archive attribute bit inside +the Attributes word. The timestamp payload is routed through the existing +`mars_nwe_set_archive_info()` helper in `nwarchive.c`, so the data is stored in +`org.mars-nwe.netware.archive` together with the NetWare archive date/time +metadata rather than in an AFP-specific xattr. AFP Get/Scan File Information +merge the stored archive date/time back into the 120-byte AFP information record. + +This keeps the split explicit: Archive as an attribute is `FILE_ATTR_A` in the +NetWare attribute store; Backup Date/Time is archive metadata in +`nwarchive.c`; FinderInfo remains AFP metadata in `org.mars-nwe.afp.finder-info`. +The Linux smoke suite covers `--backup-time-only --backup-time-epoch` and dumps +`user.org.mars-nwe.netware.archive` so runtime reports show the underlying +mars_nwe storage path. diff --git a/src/nwconn.c b/src/nwconn.c index c73534b..f5c9b6b 100644 --- a/src/nwconn.c +++ b/src/nwconn.c @@ -50,6 +50,7 @@ #include "trustee.h" #include "nwatalk.h" #include "nwattrib.h" +#include "nwarchive.h" #include "namedos.h" int act_pid = 0; @@ -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_MODIFY_DATE 0x1000 +#define AFP_FILE_BITMAP_BACKUP_DATE 0x2000 #define AFP_FILE_BITMAP_FINDER_INFO 0x4000 #define AFP_ATTR_HIDDEN 0x0200 #define AFP_ATTR_SYSTEM 0x0400 @@ -1205,8 +1207,18 @@ static int afp_fill_file_info_response(const char *unixname, un_date_2_nw(stbuff.st_atime, response + 22, 1); un_date_2_nw(stbuff.st_mtime, response + 24, 1); un_time_2_nw(stbuff.st_mtime, response + 26, 1); - U16_TO_BE16(0, response + 28); /* Backup Date */ - U16_TO_BE16(0, response + 30); /* Backup Time */ + { + uint16 archive_date = 0; + uint16 archive_time = 0; + uint8 archive_flags = 0; + + mars_nwe_get_archive_info((char *)unixname, &archive_date, &archive_time, + NULL, &archive_flags); + U16_TO_BE16((archive_flags & MARS_NWE_ARCHIVE_HAS_DATE) ? archive_date : 0, + response + 28); + U16_TO_BE16((archive_flags & MARS_NWE_ARCHIVE_HAS_TIME) ? archive_time : 0, + response + 30); + } memset(finder_info, 0, sizeof(finder_info)); (void)nwatalk_get_finder_info(unixname, finder_info, sizeof(finder_info)); @@ -1388,7 +1400,7 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, } if (request_mask & ~(AFP_FILE_BITMAP_ATTRIBUTES | AFP_FILE_BITMAP_MODIFY_DATE | - AFP_FILE_BITMAP_FINDER_INFO)) { + 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, visable_data(afp_req + 9, path_len))); @@ -1427,6 +1439,15 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, } if (request_mask & AFP_FILE_BITMAP_MODIFY_DATE) data_off += 4; + if ((request_mask & AFP_FILE_BITMAP_BACKUP_DATE) && afp_len < data_off + 4) { + XDPRINTF((2,0, "%s rejected: short backup timestamp data len=%d data_off=%d", + call_name, afp_len, data_off)); + return(-0x7e); + } + if (request_mask & AFP_FILE_BITMAP_BACKUP_DATE) { + needs_afp_metadata_modify = 1; + data_off += 4; + } if ((request_mask & AFP_FILE_BITMAP_FINDER_INFO) && data_off & 1) data_off++; if (request_mask & AFP_FILE_BITMAP_FINDER_INFO) needs_afp_metadata_modify = 1; @@ -1495,6 +1516,18 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, if (!stat(unixname, &stbuff)) {} data_off += 4; } + if (request_mask & AFP_FILE_BITMAP_BACKUP_DATE) { + uint16 archive_date = GET_BE16(afp_req + data_off); + uint16 archive_time = GET_BE16(afp_req + data_off + 2); + + result = mars_nwe_set_archive_info(unixname, + 1, archive_date, + 1, archive_time, + 0, 0); + if (result < 0) + return(result); + 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, @@ -1508,6 +1541,7 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, 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_BACKUP_DATE) ? " backup_time" : "", (request_mask & AFP_FILE_BITMAP_FINDER_INFO) ? " finder_info" : "", log_attrs, (long)log_mtime)); return(0); diff --git a/tests/linux/README.md b/tests/linux/README.md index 3013dd6..3557b79 100644 --- a/tests/linux/README.md +++ b/tests/linux/README.md @@ -809,3 +809,27 @@ Attributes, `0x1000` Modify Date/Time, and `0x4000` FinderInfo. Since these attributes now use the existing NetWare attribute path, the final `user.org.mars-nwe.afp.attributes` dump is optional and may report `ENODATA`; that is expected when no AFP-only attribute bits remain set. + +### AFP Backup Date/Time smoke + +`afp_set_file_info_smoke` supports the WebSDK Backup Date/Time request bitmap +`0x2000` via: + +```sh +./afp_set_file_info_smoke \ + -S MARS -U SUPERVISOR -P secret \ + --backup-time-only --backup-time-epoch 1700000000 \ + SYS:PUBLIC/pmdflts.ini +``` + +This is intentionally separate from the Archive attribute bit in the Attributes +word. The server stores Backup Date/Time through `nwarchive.c` and the Linux +suite dumps the corresponding xattr as: + +```sh +getfattr -n user.org.mars-nwe.netware.archive -e hex /var/mars_nwe/SYS/public/pmdflts.ini +``` + +The expected AFP reply shows the same Backup Date/Time at offsets 28/30 of the +120-byte file information record, while Archive/Hidden/System attributes remain +mapped through the normal NetWare attribute store. diff --git a/tests/linux/afp_set_file_info_smoke.c b/tests/linux/afp_set_file_info_smoke.c index 09cb05e..98d379f 100644 --- a/tests/linux/afp_set_file_info_smoke.c +++ b/tests/linux/afp_set_file_info_smoke.c @@ -25,6 +25,7 @@ #define AFP20_SET_FILE_INFORMATION 0x10 #define AFP_ATTRIBUTES_MASK 0x0100 #define AFP_MODIFY_DATE_MASK 0x1000 +#define AFP_BACKUP_DATE_MASK 0x2000 #define AFP_FINDER_INFO_MASK 0x4000 #define AFP_ATTR_HIDDEN 0x0200 #define AFP_ATTR_SYSTEM 0x0400 @@ -41,7 +42,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] " - "[--mtime-epoch SECONDS] [--finder-info-only|--attributes-only|--timestamp-only] " + "[--mtime-epoch SECONDS] [--backup-time-epoch SECONDS] [--finder-info-only|--attributes-only|--timestamp-only|--backup-time-only] " "[ncpfs options] PATH\n" "\n" "ncpfs options are parsed by ncp_initialize(), for example:\n" @@ -52,9 +53,10 @@ static void usage(const char *prog) " %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 -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); } static int parse_u32(const char *text, uint32_t *value) @@ -172,8 +174,10 @@ int main(int argc, char **argv) uint16_t attr_expected = 0; uint8_t modify_time[4]; int have_modify_time = 0; + 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 + 1 + 32]; + uint8_t request[1 + 4 + 2 + 1 + 255 + 1 + 2 + 4 + 4 + 1 + 32]; size_t path_len; size_t afp_data_off; size_t data_off; @@ -182,6 +186,7 @@ int main(int argc, char **argv) memset(finder_info, 0, sizeof(finder_info)); memset(modify_time, 0, sizeof(modify_time)); + memset(backup_time, 0, sizeof(backup_time)); memcpy(finder_info + 0, "TEXT", 4); memcpy(finder_info + 4, "MARS", 4); @@ -265,13 +270,25 @@ int main(int argc, char **argv) } request_mask |= AFP_MODIFY_DATE_MASK; have_modify_time = 1; + } else if (!strcmp(argv[i], "--backup-time-epoch")) { + uint32_t epoch; + if (++i >= argc || parse_u32(argv[i], &epoch) || epoch_to_nw_time(epoch, backup_time)) { + fprintf(stderr, "invalid --backup-time-epoch value\n"); + ncp_close(conn); + return 2; + } + request_mask |= AFP_BACKUP_DATE_MASK; + have_backup_time = 1; } else if (!strcmp(argv[i], "--attributes-only")) { - request_mask &= ~(AFP_FINDER_INFO_MASK | AFP_MODIFY_DATE_MASK); + request_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_MODIFY_DATE_MASK); + request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_MODIFY_DATE_MASK | AFP_BACKUP_DATE_MASK); } else if (!strcmp(argv[i], "--timestamp-only")) { - request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_FINDER_INFO_MASK); + request_mask &= ~(AFP_ATTRIBUTES_MASK | AFP_FINDER_INFO_MASK | AFP_BACKUP_DATE_MASK); request_mask |= AFP_MODIFY_DATE_MASK; + } else if (!strcmp(argv[i], "--backup-time-only")) { + request_mask &= ~(AFP_ATTRIBUTES_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)) { fprintf(stderr, "invalid --type value, expected four characters\n"); @@ -343,8 +360,17 @@ int main(int argc, char **argv) memcpy(request + data_off, modify_time, sizeof(modify_time)); data_off += sizeof(modify_time); } + if (request_mask & AFP_BACKUP_DATE_MASK) { + if (!have_backup_time) { + fprintf(stderr, "--backup-time-only requires --backup-time-epoch\n"); + ncp_close(conn); + return 2; + } + memcpy(request + data_off, backup_time, sizeof(backup_time)); + data_off += sizeof(backup_time); + } if ((request_mask & AFP_FINDER_INFO_MASK) && - (request_mask & (AFP_ATTRIBUTES_MASK | AFP_MODIFY_DATE_MASK)) && + (request_mask & (AFP_ATTRIBUTES_MASK | AFP_MODIFY_DATE_MASK | AFP_BACKUP_DATE_MASK)) && ((data_off + 1) & 1)) data_off++; if (request_mask & AFP_FINDER_INFO_MASK) { @@ -417,6 +443,16 @@ int main(int argc, char **argv) return 1; } + if ((request_mask & AFP_BACKUP_DATE_MASK) && + memcmp(verify_buf + 28, backup_time, sizeof(backup_time))) { + fprintf(stderr, + "AFP Set File Information backup timestamp verify mismatch: path=%s got=0x%04x%04x expected=0x%04x%04x\n", + path, be16_to_cpu(verify_buf + 28), be16_to_cpu(verify_buf + 30), + be16_to_cpu(backup_time + 0), be16_to_cpu(backup_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, @@ -426,9 +462,10 @@ int main(int argc, char **argv) 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", + printf("AFP Set File Info subfunction=0x%02x path=%s bitmap=0x%04x attrs=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 + 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)); ncp_close(conn); diff --git a/tests/linux/afp_smoke_suite.sh b/tests/linux/afp_smoke_suite.sh index 097e9c1..b794f7f 100755 --- a/tests/linux/afp_smoke_suite.sh +++ b/tests/linux/afp_smoke_suite.sh @@ -37,7 +37,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) + --mtime-epoch SECONDS AFP modify/backup timestamp to write (default: $TIMESTAMP_EPOCH) --readonly-user USER Optional user expected to lack Modify rights --readonly-password PASS Password for --readonly-user --readonly-no-password Use ncpfs -n for --readonly-user @@ -368,6 +368,12 @@ run_cmd \ "$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 Backup Timestamp" \ + "./afp_set_file_info_smoke $COMMON_PRINT --backup-time-only --backup-time-epoch '$TIMESTAMP_EPOCH' '$NETWARE_PATH'" \ + "$SCRIPT_DIR/afp_set_file_info_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" \ + --backup-time-only --backup-time-epoch "$TIMESTAMP_EPOCH" "$NETWARE_PATH" + run_cmd \ "AFP Set File Information System" \ "./afp_set_file_info_smoke $COMMON_PRINT --attributes-only --system '$NETWARE_PATH'" \ @@ -448,6 +454,11 @@ if command -v getfattr >/dev/null 2>&1; then getfattr -n user.org.mars-nwe.afp.attributes -e hex "$UNIX_PATH" || \ emit "AFP-only attributes xattr is absent; this is expected when the tested Hidden/System/Archive bits are stored through the NetWare attribute path." + run_cmd \ + "Linux xattr: NetWare Archive metadata" \ + "getfattr -n user.org.mars-nwe.netware.archive -e hex '$UNIX_PATH'" \ + getfattr -n user.org.mars-nwe.netware.archive -e hex "$UNIX_PATH" + run_cmd \ "Linux xattr: AFP Entry ID" \ "getfattr -n user.org.mars-nwe.afp.entry-id -e hex '$UNIX_PATH'" \