From 59bfdd65b28505b701292d206fe4d42dc6e14d0b Mon Sep 17 00:00:00 2001 From: ai Date: Mon, 1 Jun 2026 09:44:56 +0000 Subject: [PATCH] afp: persist ProDOS info metadata --- AI.md | 15 ++++++- include/nwatalk.h | 5 +++ include/nwsalvage.h | 5 +++ src/nwatalk.c | 54 +++++++++++++++++++++++++ src/nwconn.c | 42 ++++++++++++++++--- src/nwsalvage.c | 62 ++++++++++++++++++++++++++++- tests/afp/AFP_DELETED_FILE_INFO.md | 2 +- tests/afp/README.md | 2 +- tests/afp/TODO.md | 2 +- tests/afp/afp_deleted_info_smoke.c | 46 ++++++++++++++++++++- tests/afp/afp_set_file_info_smoke.c | 50 ++++++++++++++++++++++- tests/afp/afp_smoke_suite.sh | 20 +++++++--- 12 files changed, 287 insertions(+), 18 deletions(-) diff --git a/AI.md b/AI.md index 24a0bbe..4562f99 100644 --- a/AI.md +++ b/AI.md @@ -109,7 +109,7 @@ use, the current project status that the user pasted into the chat. record. Do not expose or normally open `.recycle` or `.salvage` through AFP code; those remain hidden backend repositories. - The first implementation returns FinderInfo from the salvage JSON snapshot, - ProDOSInfo as zeroes, resource fork size from the JSON snapshot, and the + ProDOSInfo from the nwatalk xattr-backed JSON snapshot, resource fork size from the JSON snapshot, and the deleted original name. - The AFP smoke suite has a dedicated `afp_deleted_info_smoke` helper. It pre-cleans salvage entries in the tested directory through NCP purge, creates @@ -199,3 +199,16 @@ Normal NCP reads of `.recycle` or `.salvage` are expected to fail with invalid path. Verify payload data through the visible live file after NCP write or recover, using `ncp_read_smoke`. Treat the final summary (`failures=0`, `ncp_warnings=0`) as the important signal. + + +### AFP ProDOSInfo storage + +ProDOSInfo is AFP/NCP per-entry metadata. Store it in the existing nwatalk +AFP metadata layer, not in nwarchive/nwxattr directly and not in a parallel DB. +The xattr key is `user.org.mars-nwe.afp.prodos-info` via the mars_nwe xattr +wrapper name `org.mars-nwe.afp.prodos-info`; it is a raw 6-byte value, analogous +to FinderInfo's 32-byte `org.mars-nwe.afp.finder-info`. + +Salvage captures this as `prodos_info_hex` (12 hex characters) beside +`finder_info_hex`. AFP 35/19 Get Macintosh Info On Deleted File returns +FinderInfo[32] followed by ProDOSInfo[6] from the Salvage snapshot. diff --git a/include/nwatalk.h b/include/nwatalk.h index df18ebc..74252b8 100644 --- a/include/nwatalk.h +++ b/include/nwatalk.h @@ -4,12 +4,17 @@ #include "net.h" #define NWATALK_FINDER_INFO_LEN 32 +#define NWATALK_PRODOS_INFO_LEN 6 int nwatalk_backend_available(void); int nwatalk_get_finder_info(const char *path, uint8 *finder_info, int finder_info_len); int nwatalk_set_finder_info(const char *path, const uint8 *finder_info, int finder_info_len); +int nwatalk_get_prodos_info(const char *path, uint8 *prodos_info, + int prodos_info_len); +int nwatalk_set_prodos_info(const char *path, const uint8 *prodos_info, + int prodos_info_len); int nwatalk_get_afp_attributes(const char *path, uint16 *attributes); int nwatalk_set_afp_attributes(const char *path, uint16 attributes); int nwatalk_get_resource_fork_size(const char *path, uint32 *resource_size); diff --git a/include/nwsalvage.h b/include/nwsalvage.h index 088e9c3..b96c2b4 100644 --- a/include/nwsalvage.h +++ b/include/nwsalvage.h @@ -31,6 +31,7 @@ typedef int (*nwsalvage_ini_getter)(int entry, char *str, #define NWSALVAGE_NAME_MAX 256 #define NWSALVAGE_USER_NAME_MAX 128 #define NWSALVAGE_FINDER_INFO_HEX_LEN 64 +#define NWSALVAGE_PRODOS_INFO_HEX_LEN 12 #define NWSALVAGE_AFP_ENTRY_ID_MAX 32 #define NWSALVAGE_TRUSTEE_MAX 100 #define NWSALVAGE_PATTERN_MAX 32 @@ -77,6 +78,7 @@ struct nwsalvage_deleted_entry { long ctime; const char *finder_info_hex; + const char *prodos_info_hex; const char *afp_entry_id; unsigned int afp_attributes; unsigned long long resource_fork_size; @@ -120,6 +122,7 @@ struct nwsalvage_metadata_entry { long ctime; char finder_info_hex[NWSALVAGE_FINDER_INFO_HEX_LEN + 1]; + char prodos_info_hex[NWSALVAGE_PRODOS_INFO_HEX_LEN + 1]; char afp_entry_id[NWSALVAGE_AFP_ENTRY_ID_MAX]; unsigned int afp_attributes; unsigned long long resource_fork_size; @@ -193,6 +196,8 @@ int nwsalvage_read_metadata(const char *metadata_path, struct nwsalvage_metadata_entry *entry); int nwsalvage_metadata_finder_info(const struct nwsalvage_metadata_entry *entry, unsigned char *out, size_t out_len); +int nwsalvage_metadata_prodos_info(const struct nwsalvage_metadata_entry *entry, + unsigned char *out, size_t out_len); /* * Scan one salvageable entry in a directory. scan_sequence must be diff --git a/src/nwatalk.c b/src/nwatalk.c index fbd25ab..0750cd4 100644 --- a/src/nwatalk.c +++ b/src/nwatalk.c @@ -13,6 +13,7 @@ #define MARS_NWE_AFP_ENTRY_ID_XATTR "org.mars-nwe.afp.entry-id" #define MARS_NWE_AFP_FINDER_INFO_XATTR "org.mars-nwe.afp.finder-info" +#define MARS_NWE_AFP_PRODOS_INFO_XATTR "org.mars-nwe.afp.prodos-info" #define MARS_NWE_AFP_ATTRIBUTES_XATTR "org.mars-nwe.afp.attributes" #define MARS_NWE_AFP_ENTRY_ID_VERSION 1 #define MARS_NWE_AFP_ATTRIBUTES_VERSION 1 @@ -128,6 +129,59 @@ int nwatalk_set_finder_info(const char *path, const uint8 *finder_info, #endif } + +int nwatalk_get_prodos_info(const char *path, uint8 *prodos_info, + int prodos_info_len) +{ +#if XATTR_SUPPORT + ssize_t len; +#endif + + if (!prodos_info || prodos_info_len < NWATALK_PRODOS_INFO_LEN) { + return(-0x9c); + } + + memset(prodos_info, 0, prodos_info_len); + +#if XATTR_SUPPORT + if (path && *path) { + len = mars_nwe_getxattr(path, MARS_NWE_AFP_PRODOS_INFO_XATTR, + prodos_info, NWATALK_PRODOS_INFO_LEN); + if (len == NWATALK_PRODOS_INFO_LEN) + return(0); + memset(prodos_info, 0, prodos_info_len); + } +#else + (void)path; +#endif + + /* Missing ProDOSInfo is a valid AFP state; absent metadata reads as zeroes. */ + return(0); +} + +int nwatalk_set_prodos_info(const char *path, const uint8 *prodos_info, + int prodos_info_len) +{ +#if XATTR_SUPPORT + if (!path || !*path || !prodos_info || prodos_info_len < NWATALK_PRODOS_INFO_LEN) + return(-0x9c); + + if (mars_nwe_setxattr(path, MARS_NWE_AFP_PRODOS_INFO_XATTR, + prodos_info, NWATALK_PRODOS_INFO_LEN, 0)) { + int err = errno; + XDPRINTF((3,0,"AFP ProDOSInfo xattr write failed for %s errno=%d", path, err)); + return(-0x8c); + } + + return(0); +#else + (void)path; + (void)prodos_info; + (void)prodos_info_len; + return(-0xbf); +#endif +} + int nwatalk_get_afp_attributes(const char *path, uint16 *attributes) { #if XATTR_SUPPORT diff --git a/src/nwconn.c b/src/nwconn.c index f802957..3679796 100644 --- a/src/nwconn.c +++ b/src/nwconn.c @@ -1924,6 +1924,16 @@ static uint16 afp_count_offspring(const char *unixname, const struct stat *stb) return((uint16)count); } +static int afp_info_bytes_nonzero(const uint8 *data, size_t len) +{ + size_t i; + + if (!data) return(0); + for (i = 0; i < len; i++) + if (data[i]) return(1); + return(0); +} + static int afp_fill_file_info_response(const char *unixname, const uint8 *display_path, int display_path_len, @@ -1938,6 +1948,7 @@ static int afp_fill_file_info_response(const char *unixname, uint32 parent_id = 0; uint32 resource_size = 0; uint8 finder_info[NWATALK_FINDER_INFO_LEN]; + uint8 prodos_info[NWATALK_PRODOS_INFO_LEN]; if (stat(unixname, &stbuff)) return(-0x9c); /* Invalid Path */ @@ -1996,7 +2007,11 @@ static int afp_fill_file_info_response(const char *unixname, U32_TO_BE32(get_file_owner(&stbuff), response + 96); afp_leaf_name_from_path(response + 100, 12, display_path, display_path_len); U16_TO_BE16(afp_access_privileges(volume, unixname, &stbuff), response + 112); - /* ProDOS info at offset 114 stays zero until a real Mac namespace maps it. */ + if (include_prodos_info) { + memset(prodos_info, 0, sizeof(prodos_info)); + (void)nwatalk_get_prodos_info(unixname, prodos_info, sizeof(prodos_info)); + memcpy(response + 114, prodos_info, sizeof(prodos_info)); + } if (entry_id_out) *entry_id_out = entry_id; return(include_prodos_info ? 120 : 114); @@ -2010,6 +2025,7 @@ static int afp_get_macintosh_info_on_deleted_file(uint8 *afp_req, int afp_len, uint32 dos_directory_number; struct nwsalvage_scan_result scan; uint8 finder_info[NWATALK_FINDER_INFO_LEN]; + uint8 prodos_info[NWATALK_PRODOS_INFO_LEN]; const char *name; int name_len; int result; @@ -2048,7 +2064,10 @@ static int afp_get_macintosh_info_on_deleted_file(uint8 *afp_req, int afp_len, if (nwsalvage_metadata_finder_info(&scan.metadata, finder_info, sizeof(finder_info)) == 0) memcpy(response + 0, finder_info, sizeof(finder_info)); - /* ProDOSInfo at +32 stays zero until mars_nwe stores ProDOS metadata. */ + memset(prodos_info, 0, sizeof(prodos_info)); + if (nwsalvage_metadata_prodos_info(&scan.metadata, prodos_info, + sizeof(prodos_info)) == 0) + memcpy(response + 32, prodos_info, sizeof(prodos_info)); U32_TO_BE32((uint32)scan.metadata.resource_fork_size, response + 38); name = scan.metadata.original_name[0] ? scan.metadata.original_name : ""; @@ -2057,9 +2076,11 @@ static int afp_get_macintosh_info_on_deleted_file(uint8 *afp_req, int afp_len, memcpy(response + 43, name, name_len); XDPRINTF((3, 0, - "INFO AFP 35/19 DONE fn=0x23 sub=0x13 vol=0x%02x dosdir=0x%08x name=\"%s\" resource=0x%08lx", + "INFO AFP 35/19 DONE fn=0x23 sub=0x13 vol=0x%02x dosdir=0x%08x name=\"%s\" resource=0x%08lx prodos=%02x%02x%02x%02x%02x%02x", (unsigned int)volume_number, (unsigned int)dos_directory_number, - name, (unsigned long)scan.metadata.resource_fork_size)); + name, (unsigned long)scan.metadata.resource_fork_size, + prodos_info[0], prodos_info[1], prodos_info[2], + prodos_info[3], prodos_info[4], prodos_info[5])); return(43 + name_len); } @@ -2241,6 +2262,7 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, uint8 *modify_data = NULL; uint8 *backup_data = NULL; uint8 *finder_data = NULL; + uint8 *prodos_data = NULL; if (afp_len < 9) { XDPRINTF((2,0, "%s rejected: short request len=%d", @@ -2282,6 +2304,7 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, modify_data = afp_req + 14; backup_data = afp_req + 18; finder_data = afp_req + 22; + prodos_data = is_afp20 ? afp_req + 54 : NULL; } path_data = afp_req + path_off; @@ -2338,6 +2361,8 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, needs_afp_metadata_modify = 1; if (request_mask & AFP_FILE_BITMAP_FINDER_INFO) needs_afp_metadata_modify = 1; + if (is_afp20 && afp_info_bytes_nonzero(prodos_data, NWATALK_PRODOS_INFO_LEN)) + needs_afp_metadata_modify = 1; if (!resolved_by_entry_id) { path_volume = afp_resolve_path_volume(path_data, path_len, @@ -2436,8 +2461,14 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, if (result < 0) return(result); } + if (is_afp20 && afp_info_bytes_nonzero(prodos_data, NWATALK_PRODOS_INFO_LEN)) { + result = nwatalk_set_prodos_info(unixname, prodos_data, + NWATALK_PRODOS_INFO_LEN); + if (result < 0) + return(result); + } - XDPRINTF((3,0, "%s: vol=%d request_vol=%d entry=0x%08x mask=0x%04x path='%s'%s%s%s%s%s%s attrs=0x%04x atime=%ld mtime=%ld", + XDPRINTF((3,0, "%s: vol=%d request_vol=%d entry=0x%08x mask=0x%04x path='%s'%s%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(path_data, path_len), (request_mask & AFP_FILE_BITMAP_ATTRIBUTES) ? " attributes" : "", @@ -2446,6 +2477,7 @@ static int afp_set_file_information(uint8 *afp_req, int afp_len, (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" : "", + (is_afp20 && afp_info_bytes_nonzero(prodos_data, NWATALK_PRODOS_INFO_LEN)) ? " prodos_info" : "", log_attrs, (long)log_atime, (long)log_mtime)); return(0); } diff --git a/src/nwsalvage.c b/src/nwsalvage.c index 4bc222d..d56575f 100644 --- a/src/nwsalvage.c +++ b/src/nwsalvage.c @@ -711,6 +711,13 @@ static const char *metadata_finder_info_hex( entry->finder_info_hex : NULL); } +static const char *metadata_prodos_info_hex( + const struct nwsalvage_deleted_entry *entry) +{ + return(entry->prodos_info_hex && *entry->prodos_info_hex ? + entry->prodos_info_hex : NULL); +} + static int validate_hex_string(const char *value, size_t expected_len) { size_t i; @@ -958,6 +965,7 @@ int nwsalvage_write_metadata(const char *metadata_path, yyjson_mut_val *object; yyjson_write_err err; const char *finder_info_hex; + const char *prodos_info_hex; int failed = 0; if (!metadata_path || !*metadata_path || !entry || @@ -972,6 +980,10 @@ int nwsalvage_write_metadata(const char *metadata_path, if (finder_info_hex && validate_hex_string(finder_info_hex, NWSALVAGE_FINDER_INFO_HEX_LEN) < 0) return(-1); + prodos_info_hex = metadata_prodos_info_hex(entry); + if (prodos_info_hex && + validate_hex_string(prodos_info_hex, NWSALVAGE_PRODOS_INFO_HEX_LEN) < 0) + return(-1); if (!metadata_time_valid(entry->deleted_at) || !metadata_time_valid(entry->atime) || !metadata_time_valid(entry->mtime) || @@ -1036,6 +1048,9 @@ int nwsalvage_write_metadata(const char *metadata_path, if (!failed && finder_info_hex && json_add_string(doc, object, "finder_info_hex", finder_info_hex) < 0) failed = 1; + if (!failed && prodos_info_hex && + json_add_string(doc, object, "prodos_info_hex", prodos_info_hex) < 0) + failed = 1; if (!failed && json_add_string(doc, object, "afp_entry_id", entry->afp_entry_id ? entry->afp_entry_id : "") < 0) failed = 1; @@ -1198,6 +1213,18 @@ int nwsalvage_read_metadata(const char *metadata_path, failed = 1; } } + { + yyjson_val *prodos = yyjson_obj_get(object, "prodos_info_hex"); + if (prodos) { + if (!yyjson_is_str(prodos) || + strmaxcpy((uint8 *)entry->prodos_info_hex, + yyjson_get_str(prodos), + sizeof(entry->prodos_info_hex) - 1) < 0 || + validate_hex_string(entry->prodos_info_hex, + NWSALVAGE_PRODOS_INFO_HEX_LEN) < 0) + failed = 1; + } + } if (!failed && json_get_string_copy(object, "afp_entry_id", entry->afp_entry_id, sizeof(entry->afp_entry_id)) < 0) @@ -1740,10 +1767,13 @@ static void nwsalvage_fill_afp_metadata(const char *unixname, struct nwsalvage_deleted_entry *entry, char *finder_info_hex, size_t finder_info_hex_len, + char *prodos_info_hex, + size_t prodos_info_hex_len, char *afp_entry_id, size_t afp_entry_id_len) { uint8 finder_info[NWATALK_FINDER_INFO_LEN]; + uint8 prodos_info[NWATALK_PRODOS_INFO_LEN]; uint16 afp_attributes = 0; uint32 entry_id = 0; uint32 resource_size = 0; @@ -1755,6 +1785,13 @@ static void nwsalvage_fill_afp_metadata(const char *unixname, entry->finder_info_hex = finder_info_hex; } + memset(prodos_info, 0, sizeof(prodos_info)); + if (nwatalk_get_prodos_info(unixname, prodos_info, sizeof(prodos_info)) == 0) { + nwsalvage_format_hex(prodos_info_hex, prodos_info_hex_len, + prodos_info, sizeof(prodos_info)); + entry->prodos_info_hex = prodos_info_hex; + } + if (nwatalk_get_entry_id(unixname, &entry_id) == 0) slprintf(afp_entry_id, (int)afp_entry_id_len - 1, "0x%08x", entry_id); else @@ -2143,6 +2180,19 @@ int nwsalvage_metadata_finder_info(const struct nwsalvage_metadata_entry *entry, return(nwsalvage_hex_to_bytes(entry->finder_info_hex, out, out_len)); } +int nwsalvage_metadata_prodos_info(const struct nwsalvage_metadata_entry *entry, + unsigned char *out, size_t out_len) +{ + if (!entry || !out) { + errno = EINVAL; + return(-1); + } + memset(out, 0, out_len); + if (!entry->prodos_info_hex[0]) + return(0); + return(nwsalvage_hex_to_bytes(entry->prodos_info_hex, out, out_len)); +} + static int nwsalvage_finder_info_is_zero(const uint8 *finder_info, size_t len) { size_t i; @@ -2198,6 +2248,13 @@ static int nwsalvage_restore_metadata(int volume, const char *unixname, (void)nwatalk_set_finder_info(unixname, finder_info, sizeof(finder_info)); } + if (entry->prodos_info_hex[0]) { + uint8 prodos_info[NWATALK_PRODOS_INFO_LEN]; + if (nwsalvage_hex_to_bytes(entry->prodos_info_hex, prodos_info, + sizeof(prodos_info)) == 0) + (void)nwatalk_set_prodos_info(unixname, prodos_info, sizeof(prodos_info)); + } + if (entry->afp_attributes) (void)nwatalk_set_afp_attributes(unixname, (uint16)entry->afp_attributes); @@ -2412,6 +2469,7 @@ int nwsalvage_capture_node_delete(int volume, const char *unixname, char volume_name[NWSALVAGE_REPOSITORY_NAME_MAX]; char deleted_by[NWSALVAGE_USER_NAME_MAX]; char finder_info_hex[NWSALVAGE_FINDER_INFO_HEX_LEN + 1]; + char prodos_info_hex[NWSALVAGE_PRODOS_INFO_HEX_LEN + 1]; char afp_entry_id[NWSALVAGE_AFP_ENTRY_ID_MAX]; if (!unixname || !*unixname || !stb) { @@ -2470,6 +2528,7 @@ int nwsalvage_capture_node_delete(int volume, const char *unixname, memset(&entry, 0, sizeof(entry)); finder_info_hex[0] = '\0'; + prodos_info_hex[0] = '\0'; afp_entry_id[0] = '\0'; entry.source = "mars_nwe"; @@ -2490,7 +2549,8 @@ int nwsalvage_capture_node_delete(int volume, const char *unixname, entry.ctime = (long)stb->st_ctime; nwsalvage_fill_afp_metadata(unixname, &entry, finder_info_hex, - sizeof(finder_info_hex), afp_entry_id, + sizeof(finder_info_hex), prodos_info_hex, + sizeof(prodos_info_hex), afp_entry_id, sizeof(afp_entry_id)); nwsalvage_fill_netware_xattrs(unixname, &entry); nwsalvage_fill_trustees(volume, unixname, stb, &entry); diff --git a/tests/afp/AFP_DELETED_FILE_INFO.md b/tests/afp/AFP_DELETED_FILE_INFO.md index fba4c37..394da83 100644 --- a/tests/afp/AFP_DELETED_FILE_INFO.md +++ b/tests/afp/AFP_DELETED_FILE_INFO.md @@ -37,7 +37,7 @@ AFP `0x13` behaves like this: 1. Validate the request volume and DOS directory entry. 2. Look up the deleted entry through the mars_nwe salvage/deleted-entry backend. 3. Return FinderInfo from the AFP metadata store when available, otherwise zeroes. -4. Return ProDOS information as zeroes unless mars_nwe gains a real ProDOS store. +4. Return ProDOS information from the Salvage JSON snapshot (`prodos_info_hex`), which is captured from the nwatalk ProDOSInfo xattr backend. 5. Return resource fork size as zero while resource forks remain unsupported. 6. Return the deleted file name from the salvage entry, not from a live path scan. diff --git a/tests/afp/README.md b/tests/afp/README.md index a46f3aa..18d6c9b 100644 --- a/tests/afp/README.md +++ b/tests/afp/README.md @@ -1397,7 +1397,7 @@ NCP 0x2222/35/19 AFP Get Macintosh Info On Deleted File The request carries `VolumeNumber` and `DOSDirectoryNumber`. The server maps that deleted directory number through the shared mars_nwe salvage/deleted-entry -backend and returns the FinderInfo snapshot, zero ProDOS info, the stored +backend and returns the FinderInfo snapshot, ProDOSInfo snapshot, the stored resource fork size, and the deleted original filename. It does not open or expose `.recycle` or `.salvage` as AFP-visible paths. diff --git a/tests/afp/TODO.md b/tests/afp/TODO.md index eb1d886..27f687d 100644 --- a/tests/afp/TODO.md +++ b/tests/afp/TODO.md @@ -19,7 +19,7 @@ Current AFP mapping: 1. Resolve the deleted DOS directory entry through the mars_nwe salvage backend. 2. Return FinderInfo from the salvage JSON snapshot. -3. Return ProDOS information as zeroes unless mars_nwe gains a real ProDOS store. +3. Return ProDOS information from the nwatalk xattr-backed Salvage JSON snapshot (`prodos_info_hex`). 4. Return resource fork size from the salvage JSON snapshot. 5. Return the deleted filename from the salvage/deleted-entry record. diff --git a/tests/afp/afp_deleted_info_smoke.c b/tests/afp/afp_deleted_info_smoke.c index d115b0c..d95541c 100644 --- a/tests/afp/afp_deleted_info_smoke.c +++ b/tests/afp/afp_deleted_info_smoke.c @@ -26,7 +26,7 @@ static void usage(const char *prog) { fprintf(stderr, - "Usage: %s [--expect-name NAME] [--expect-type FOURCC] [--expect-creator FOURCC] [--purge-after] [--purge-all] [ncpfs options] DIRECTORY\n" + "Usage: %s [--expect-name NAME] [--expect-type FOURCC] [--expect-creator FOURCC] [--expect-prodos-hex 12HEX] [--purge-after] [--purge-all] [ncpfs options] DIRECTORY\n" "\n" "ncpfs options are parsed by ncp_initialize(), for example:\n" " -S SERVER -U USER -P PASSWORD -n\n" @@ -53,6 +53,30 @@ static uint32_t be32_to_cpu(const uint8_t p[4]) p[3]; } +static int hex_nibble(int c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static int parse_hex_bytes(const char *text, uint8_t *out, size_t out_len) +{ + size_t i; + + if (!text || strlen(text) != out_len * 2) + return -1; + for (i = 0; i < out_len; i++) { + int hi = hex_nibble((unsigned char)text[i * 2]); + int lo = hex_nibble((unsigned char)text[i * 2 + 1]); + if (hi < 0 || lo < 0) + return -1; + out[i] = (uint8_t)((hi << 4) | lo); + } + return 0; +} + static int find_deleted(NWCONN_HANDLE conn, const char *dir, const char *expect_name, struct ncp_deleted_file *info, @@ -117,6 +141,8 @@ int main(int argc, char **argv) const char *expect_name = NULL; const char *expect_type = NULL; const char *expect_creator = NULL; + uint8_t expect_prodos[6]; + int have_expect_prodos = 0; int purge_after = 0; int do_purge_all = 0; struct ncp_deleted_file info; @@ -128,6 +154,8 @@ int main(int argc, char **argv) NWCCODE err; int i; + memset(expect_prodos, 0, sizeof(expect_prodos)); + if (NWCallsInit(NULL, NULL)) { fprintf(stderr, "NWCallsInit failed\n"); return 2; @@ -156,6 +184,12 @@ int main(int argc, char **argv) ncp_close(conn); return 2; } expect_creator = argv[i]; + } else if (!strcmp(argv[i], "--expect-prodos-hex")) { + if (++i >= argc || parse_hex_bytes(argv[i], expect_prodos, sizeof(expect_prodos))) { + fprintf(stderr, "--expect-prodos-hex must be 12 hex characters\n"); + ncp_close(conn); return 2; + } + have_expect_prodos = 1; } else if (!strcmp(argv[i], "--purge-after")) { purge_after = 1; } else if (!strcmp(argv[i], "--purge-all")) { @@ -228,6 +262,9 @@ int main(int argc, char **argv) (int)info.seq, (unsigned int)info.vol, (unsigned int)info.base, (unsigned int)resource_size, name_len, reply_buf + 43); printf("AFP deleted finder type=%.4s creator=%.4s\n", reply_buf, reply_buf + 4); + printf("AFP deleted prodos=%02x%02x%02x%02x%02x%02x\n", + reply_buf[32], reply_buf[33], reply_buf[34], + reply_buf[35], reply_buf[36], reply_buf[37]); if (expect_name && (name_len != (int)strlen(expect_name) || memcmp(reply_buf + 43, expect_name, name_len))) { @@ -248,6 +285,13 @@ int main(int argc, char **argv) ncp_close(conn); return 1; } + if (have_expect_prodos && memcmp(reply_buf + 32, expect_prodos, sizeof(expect_prodos))) { + fprintf(stderr, "AFP deleted info ProDOSInfo mismatch: got=%02x%02x%02x%02x%02x%02x\n", + reply_buf[32], reply_buf[33], reply_buf[34], + reply_buf[35], reply_buf[36], reply_buf[37]); + ncp_close(conn); + return 1; + } if (purge_after) { err = ncp_ns_purge_file(conn, &info); diff --git a/tests/afp/afp_set_file_info_smoke.c b/tests/afp/afp_set_file_info_smoke.c index 802488b..f38c0cb 100644 --- a/tests/afp/afp_set_file_info_smoke.c +++ b/tests/afp/afp_set_file_info_smoke.c @@ -42,7 +42,7 @@ 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] [--entry-id-only] [--type FOUR] [--creator FOUR] " + "[--volume N] [--entry-id ID] [--entry-id-only] [--type FOUR] [--creator FOUR] [--prodos-hex 12HEX] " "[--hidden|--clear-hidden|--invisible|--clear-invisible|--system|--clear-system|--archive|--clear-archive] " "[--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" @@ -113,6 +113,30 @@ static int copy_fourcc(const char *text, uint8_t out[4]) return 0; } +static int hex_nibble(int c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static int parse_hex_bytes(const char *text, uint8_t *out, size_t out_len) +{ + size_t i; + + if (!text || strlen(text) != out_len * 2) + return -1; + for (i = 0; i < out_len; i++) { + int hi = hex_nibble((unsigned char)text[i * 2]); + int lo = hex_nibble((unsigned char)text[i * 2 + 1]); + if (hi < 0 || lo < 0) + return -1; + out[i] = (uint8_t)((hi << 4) | lo); + } + return 0; +} + static int epoch_to_nw_time(uint32_t epoch, uint8_t out[4]) { time_t t = (time_t)epoch; @@ -236,6 +260,8 @@ int main(int argc, char **argv) uint32_t entry_id = 0; int entry_id_only = 0; uint8_t finder_info[32]; + uint8_t prodos_info[6]; + int have_prodos_info = 0; uint8_t set_subfunction = AFP20_SET_FILE_INFORMATION; uint16_t request_mask = AFP_FINDER_INFO_MASK; uint16_t attr_request = 0; @@ -257,6 +283,7 @@ int main(int argc, char **argv) NWCCODE err; memset(finder_info, 0, sizeof(finder_info)); + memset(prodos_info, 0, sizeof(prodos_info)); memset(access_time, 0, sizeof(access_time)); memset(create_time, 0, sizeof(create_time)); memset(modify_time, 0, sizeof(modify_time)); @@ -401,6 +428,13 @@ int main(int argc, char **argv) ncp_close(conn); return 2; } + } else if (!strcmp(argv[i], "--prodos-hex")) { + if (++i >= argc || parse_hex_bytes(argv[i], prodos_info, sizeof(prodos_info))) { + fprintf(stderr, "invalid --prodos-hex value, expected 12 hex characters\n"); + ncp_close(conn); + return 2; + } + have_prodos_info = 1; } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { usage(argv[0]); ncp_close(conn); @@ -497,6 +531,8 @@ int main(int argc, char **argv) if (have_backup_time) memcpy(request + 17, backup_time, sizeof(backup_time)); memcpy(request + 21, finder_info, sizeof(finder_info)); + if (set_subfunction == AFP20_SET_FILE_INFORMATION && have_prodos_info) + memcpy(request + 53, prodos_info, sizeof(prodos_info)); request[path_len_off] = (uint8_t)path_len; memcpy(request + path_off, path, path_len); data_off = path_off + path_len; @@ -642,12 +678,22 @@ 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 access=0x%04x modify=0x%04x%04x backup=0x%04x%04x finder_type=%.4s finder_creator=%.4s entry_id=0x%08x verified%s\n", + if (have_prodos_info && memcmp(verify_buf + 114, prodos_info, sizeof(prodos_info))) { + fprintf(stderr, + "AFP Set File Information ProDOSInfo verify mismatch: path=%s got=%02x%02x%02x%02x%02x%02x\n", + path, verify_buf[114], verify_buf[115], verify_buf[116], + verify_buf[117], verify_buf[118], verify_buf[119]); + ncp_close(conn); + return 1; + } + + 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%s verified%s\n", set_subfunction, path, request_mask, be16_to_cpu(verify_buf + 8), 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), + have_prodos_info ? " prodos_info" : "", entry_id_only ? " entry-id-only" : ""); ncp_close(conn); diff --git a/tests/afp/afp_smoke_suite.sh b/tests/afp/afp_smoke_suite.sh index 85159a6..f990ee6 100755 --- a/tests/afp/afp_smoke_suite.sh +++ b/tests/afp/afp_smoke_suite.sh @@ -17,6 +17,7 @@ LOG_FILE="/var/log/mars_nwe/nw.log" OUT_FILE="" FINDER_TYPE="TEXT" FINDER_CREATOR="MARS" +PRODOS_INFO_HEX="010203040506" TIMESTAMP_EPOCH="1700000000" CREATE_DIR_NAME="" CREATE_FILE_NAME="" @@ -42,6 +43,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) + --prodos-hex 12HEX ProDOSInfo written by AFP 2.0 Set File Info (default: $PRODOS_INFO_HEX) --mtime-epoch SECONDS AFP modify/backup timestamp to write (default: $TIMESTAMP_EPOCH) --create-dir-name NAME Temporary directory name for AFP Create Directory (default: generated aHHMMSS) --create-file-name NAME Temporary file name for AFP Create File (default: generated fHHMMR) @@ -83,6 +85,8 @@ while [ $# -gt 0 ]; do FINDER_TYPE=$2; shift 2 ;; --creator) FINDER_CREATOR=$2; shift 2 ;; + --prodos-hex) + PRODOS_INFO_HEX=$2; shift 2 ;; --mtime-epoch) TIMESTAMP_EPOCH=$2; shift 2 ;; --create-dir-name) @@ -370,6 +374,7 @@ emit "log=$LOG_FILE" emit "log_window_seconds=$LOG_WINDOW_SECONDS" emit "finder_type=$FINDER_TYPE" emit "finder_creator=$FINDER_CREATOR" +emit "prodos_info_hex=$PRODOS_INFO_HEX" emit "mtime_epoch=$TIMESTAMP_EPOCH" emit "create_dir_path=$CREATE_DIR_PATH" emit "create_dir20_path=$CREATE_DIR20_PATH" @@ -724,18 +729,18 @@ run_cmd \ "./afp_create_file_smoke $COMMON_PRINT '$CREATE_FILE_PATH'" \ "$SCRIPT_DIR/afp_create_file_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$CREATE_FILE_PATH" run_cmd \ - "AFP Deleted Info set FinderInfo" \ - "./afp_set_file_info_smoke $COMMON_PRINT --finder-info-only --type '$FINDER_TYPE' --creator '$FINDER_CREATOR' '$CREATE_FILE_PATH'" \ + "AFP Deleted Info set FinderInfo and ProDOSInfo" \ + "./afp_set_file_info_smoke $COMMON_PRINT --finder-info-only --type '$FINDER_TYPE' --creator '$FINDER_CREATOR' --prodos-hex '$PRODOS_INFO_HEX' '$CREATE_FILE_PATH'" \ "$SCRIPT_DIR/afp_set_file_info_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" \ - --finder-info-only --type "$FINDER_TYPE" --creator "$FINDER_CREATOR" "$CREATE_FILE_PATH" + --finder-info-only --type "$FINDER_TYPE" --creator "$FINDER_CREATOR" --prodos-hex "$PRODOS_INFO_HEX" "$CREATE_FILE_PATH" run_cmd \ "AFP Deleted Info delete file" \ "./afp_delete_smoke $COMMON_PRINT '$CREATE_FILE_PATH'" \ "$SCRIPT_DIR/afp_delete_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$CREATE_FILE_PATH" run_cmd \ "AFP Get Macintosh Info On Deleted File" \ - "./afp_deleted_info_smoke --expect-type '$FINDER_TYPE' --expect-creator '$FINDER_CREATOR' --purge-after $COMMON_PRINT '$DIR_PATH'" \ - "$SCRIPT_DIR/afp_deleted_info_smoke" --expect-type "$FINDER_TYPE" --expect-creator "$FINDER_CREATOR" --purge-after \ + "./afp_deleted_info_smoke --expect-type '$FINDER_TYPE' --expect-creator '$FINDER_CREATOR' --expect-prodos-hex '$PRODOS_INFO_HEX' --purge-after $COMMON_PRINT '$DIR_PATH'" \ + "$SCRIPT_DIR/afp_deleted_info_smoke" --expect-type "$FINDER_TYPE" --expect-creator "$FINDER_CREATOR" --expect-prodos-hex "$PRODOS_INFO_HEX" --purge-after \ -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$DIR_PATH" run_cmd \ @@ -878,6 +883,11 @@ if command -v getfattr >/dev/null 2>&1; then "getfattr -n user.org.mars-nwe.afp.finder-info -e hex '$UNIX_PATH'" \ getfattr -n user.org.mars-nwe.afp.finder-info -e hex "$UNIX_PATH" + run_optional_cmd \ + "Linux xattr: AFP ProDOSInfo" \ + "getfattr -n user.org.mars-nwe.afp.prodos-info -e hex '$UNIX_PATH'" \ + getfattr -n user.org.mars-nwe.afp.prodos-info -e hex "$UNIX_PATH" || true + run_optional_cmd \ "Linux xattr: AFP Attributes (optional AFP-only bits)" \ "getfattr -n user.org.mars-nwe.afp.attributes -e hex '$UNIX_PATH'" \