From 92b0c4a34a2ec83f301ce10124f6baae54581939 Mon Sep 17 00:00:00 2001 From: ai Date: Mon, 1 Jun 2026 08:57:51 +0000 Subject: [PATCH] afp: add deleted file Macintosh info endpoint --- AI.md | 22 ++- TODO.md | 12 +- include/nwsalvage.h | 5 + src/nwconn.c | 67 +++++++ src/nwsalvage.c | 122 ++++++++++++ tests/afp/AFP_DELETED_FILE_INFO.md | 4 +- tests/afp/AFP_ENDPOINT_INVENTORY.md | 17 +- tests/afp/AFP_FINAL_AUDIT.md | 8 +- tests/afp/AFP_WEBSK_AUDIT_FINDINGS.md | 12 +- tests/afp/CMakeLists.txt | 4 + tests/afp/README.md | 19 ++ tests/afp/TODO.md | 22 +-- tests/afp/afp_deleted_info_smoke.c | 266 ++++++++++++++++++++++++++ tests/afp/afp_endpoint_inventory.py | 2 +- tests/afp/afp_smoke_suite.sh | 28 +++ 15 files changed, 560 insertions(+), 50 deletions(-) create mode 100644 tests/afp/afp_deleted_info_smoke.c diff --git a/AI.md b/AI.md index dd45d18..24a0bbe 100644 --- a/AI.md +++ b/AI.md @@ -99,17 +99,25 @@ use, the current project status that the user pasted into the chat. than creating unrelated top-level scripts, unless a helper binary is needed and then started by the suite. -## AFP 0x13 next task notes +## AFP 0x13 deleted-file info notes -- After the NCP salvage endpoint work is complete, the next intended task is - AFP `0x13 Get Macintosh Info On Deleted Files`. -- Implement AFP `0x13` as an adapter over the shared mars_nwe salvage/deleted - entry record. Do not scan `.recycle` or `.salvage` directly from AFP code. +- AFP `0x13 Get Macintosh Info On Deleted File` is NCP `0x2222 / 35 / 19` + (wire subfunction byte `0x13`). The Micro Focus / Novell WebSDK request is + `VolumeNumber` plus `DOSDirectoryNumber`; the reply is FinderInfo[32], + ProDOSInfo[6], ResourceForkSize, FileNameLen, FileName. +- Implement it only as an adapter over the shared mars_nwe salvage/deleted-entry + 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 + 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 + a temporary AFP file, writes FinderInfo, deletes it, verifies AFP `0x13`, and + purges the tested deleted entry afterwards. - Reuse existing AFP/nwatalk metadata mechanisms for FinderInfo, AFP attributes, entry ids, resource fork state, and related restore/lookup behavior. Do not add a parallel AFP metadata database. -- Check `tests/afp/` first for the endpoint inventory, WebSDK notes, and current - smoke coverage before writing code. ## Logging rules diff --git a/TODO.md b/TODO.md index aa201b0..4382389 100644 --- a/TODO.md +++ b/TODO.md @@ -246,15 +246,15 @@ Current status: versioned scan/recover/purge and stale-sidecar cleanup. It should be the backing data source for AFP deleted-file compatibility work. -Next task: +Current AFP focus: -- Implement AFP `0x13 Get Macintosh Info On Deleted Files` as an adapter over - the mars_nwe salvage/deleted-entry record. -- AFP `0x13` must not scan `.recycle` or `.salvage` directly; those remain - hidden backend repositories. +- AFP `0x13 Get Macintosh Info On Deleted File` is now implemented as a + salvage/deleted-entry adapter and covered by the AFP smoke suite. +- Keep future AFP deleted-file work on the shared salvage backend; do not expose + `.recycle` or `.salvage` through normal AFP/NCP path opens. - Keep AFP metadata restore/lookup paths tied to the existing mars_nwe AFP and nwatalk mechanisms, not a new side database. -- Keep the detailed AFP TODO, inventory, and audit notes in `tests/afp/`. +- Keep the detailed AFP inventory and audit notes in `tests/afp/`. ## Deferred / optional protocol work diff --git a/include/nwsalvage.h b/include/nwsalvage.h index 791198c..088e9c3 100644 --- a/include/nwsalvage.h +++ b/include/nwsalvage.h @@ -191,6 +191,8 @@ int nwsalvage_write_metadata(const char *metadata_path, const struct nwsalvage_deleted_entry *entry); 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); /* * Scan one salvageable entry in a directory. scan_sequence must be @@ -201,6 +203,9 @@ int nwsalvage_scan_directory(int volume, const char *relative_dir, unsigned long directory_base, unsigned long scan_sequence, struct nwsalvage_scan_result *result); +int nwsalvage_find_by_deleted_directory_number(int volume, + unsigned long dos_directory_number, + struct nwsalvage_scan_result *result); int nwsalvage_recover_scan_result(int volume, const struct nwsalvage_scan_result *scan, const char *dest_unixname, int task); diff --git a/src/nwconn.c b/src/nwconn.c index 07f7024..f802957 100644 --- a/src/nwconn.c +++ b/src/nwconn.c @@ -53,6 +53,7 @@ #include "nwattrib.h" #include "nwarchive.h" #include "namedos.h" +#include "nwsalvage.h" int act_pid = 0; @@ -2001,6 +2002,67 @@ static int afp_fill_file_info_response(const char *unixname, return(include_prodos_info ? 120 : 114); } + +static int afp_get_macintosh_info_on_deleted_file(uint8 *afp_req, int afp_len, + uint8 *response) +{ + uint8 volume_number; + uint32 dos_directory_number; + struct nwsalvage_scan_result scan; + uint8 finder_info[NWATALK_FINDER_INFO_LEN]; + const char *name; + int name_len; + int result; + + if (afp_len < 6) { + XDPRINTF((2,0, "AFP Get Macintosh Info On Deleted File rejected: short request len=%d", + afp_len)); + return(-0x7e); /* NCP Boundary Check Failed */ + } + + volume_number = afp_req[1]; + dos_directory_number = GET_BE32(afp_req + 2); + + if (!nwatalk_backend_available()) { + XDPRINTF((3,0, "AFP Get Macintosh Info On Deleted File rejected: AFP xattr metadata backend unavailable")); + return(-0xbf); /* invalid namespace */ + } + + memset(&scan, 0, sizeof(scan)); + result = nwsalvage_find_by_deleted_directory_number((int)volume_number, + (unsigned long)dos_directory_number, + &scan); + if (result < 0) { + XDPRINTF((2,0, "AFP Get Macintosh Info On Deleted File scan failed: vol=%d dosdir=0x%08x errno=%d", + (int)volume_number, (unsigned int)dos_directory_number, errno)); + return(-0x9c); /* Invalid Path */ + } + if (result == 0) { + XDPRINTF((2,0, "AFP Get Macintosh Info On Deleted File not found: vol=%d dosdir=0x%08x", + (int)volume_number, (unsigned int)dos_directory_number)); + return(-0x9c); /* Invalid Path */ + } + + memset(response, 0, 51 + NWSALVAGE_NAME_MAX); + memset(finder_info, 0, sizeof(finder_info)); + 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. */ + U32_TO_BE32((uint32)scan.metadata.resource_fork_size, response + 38); + + name = scan.metadata.original_name[0] ? scan.metadata.original_name : ""; + name_len = min((int)strlen(name), NWSALVAGE_NAME_MAX - 1); + response[42] = (uint8)name_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", + (unsigned int)volume_number, (unsigned int)dos_directory_number, + name, (unsigned long)scan.metadata.resource_fork_size)); + return(43 + name_len); +} + static int afp_get_file_information(uint8 *afp_req, int afp_len, uint8 *response, const char *call_name) { @@ -4629,6 +4691,11 @@ static int handle_ncp_serv(void) afp_len, responsedata); if (result > -1) data_len = result; else completition = (uint8)-result; + } else if (ufunc == 0x13) { + int result = afp_get_macintosh_info_on_deleted_file(afp_req, + afp_len, responsedata); + if (result > -1) data_len = result; + else completition = (uint8)-result; } else if (ufunc == 0x05 || ufunc == 0x0f) { int result = afp_get_file_information(afp_req, afp_len, responsedata, diff --git a/src/nwsalvage.c b/src/nwsalvage.c index d40fcc6..4bc222d 100644 --- a/src/nwsalvage.c +++ b/src/nwsalvage.c @@ -1915,6 +1915,115 @@ static int nwsalvage_scan_metadata_dir(const char *metadata_dir, return(found); } + +static int nwsalvage_scan_metadata_tree_by_directory_number( + const char *metadata_dir, const struct nwsalvage_config *config, + const char *volume_root, const char *volume_name, + unsigned long dos_directory_number, + struct nwsalvage_scan_result *result) +{ + DIR *dir; + struct dirent *de; + unsigned long match_index; + int found; + + if (!metadata_dir || !config || !volume_root || !volume_name || !result) { + errno = EINVAL; + return(-1); + } + + match_index = 0; + found = nwsalvage_scan_metadata_dir(metadata_dir, config, volume_root, + volume_name, dos_directory_number, + 0, &match_index, result); + if (found != 0) + return(found); + + dir = opendir(metadata_dir); + if (!dir) + return(errno == ENOENT ? 0 : -1); + + while ((de = readdir(dir)) != NULL) { + char child[NWSALVAGE_PATH_MAX]; + struct stat st; + + if (de->d_name[0] == '.') + continue; + if (nwsalvage_path_join(child, sizeof(child), metadata_dir, de->d_name) < 0) + continue; + if (stat(child, &st) < 0 || !S_ISDIR(st.st_mode)) + continue; + + found = nwsalvage_scan_metadata_tree_by_directory_number( + child, config, volume_root, volume_name, dos_directory_number, result); + if (found != 0) + break; + } + + closedir(dir); + return(found); +} + +int nwsalvage_find_by_deleted_directory_number(int volume, + unsigned long dos_directory_number, + struct nwsalvage_scan_result *result) +{ + struct nwsalvage_config config; + char volume_root[NWSALVAGE_PATH_MAX]; + char metadata_root[NWSALVAGE_PATH_MAX]; + char volume_name[NWSALVAGE_REPOSITORY_NAME_MAX]; + DIR *root; + struct dirent *de; + int found = 0; + + if (!result) { + errno = EINVAL; + return(-1); + } + + if (nwsalvage_config_load_from_ini(&config, nwsalvage_ini_get, NULL) < 0) + return(-1); + if (!config.enabled) + return(0); + + if (nwsalvage_copy_volume_root(volume, volume_root, sizeof(volume_root)) < 0) + return(-1); + if (nw_get_volume_name(volume, (uint8 *)volume_name, + sizeof(volume_name)) < 1) + return(-1); + if (nwsalvage_path_join(metadata_root, sizeof(metadata_root), volume_root, + config.metadata_repository) < 0) + return(-1); + + root = opendir(metadata_root); + if (!root) + return(errno == ENOENT ? 0 : -1); + + while ((de = readdir(root)) != NULL) { + char user_dir[NWSALVAGE_PATH_MAX]; + struct stat st; + + if (de->d_name[0] == '.') + continue; + if (nwsalvage_path_join(user_dir, sizeof(user_dir), + metadata_root, de->d_name) < 0) + continue; + if (stat(user_dir, &st) < 0 || !S_ISDIR(st.st_mode)) + continue; + + found = nwsalvage_scan_metadata_tree_by_directory_number( + user_dir, &config, volume_root, volume_name, + dos_directory_number, result); + if (found != 0) + break; + } + + closedir(root); + if (found > 0) + result->scan_volume = (unsigned long)volume; + return(found); +} + int nwsalvage_scan_directory(int volume, const char *relative_dir, unsigned long directory_base, unsigned long scan_sequence, @@ -2021,6 +2130,19 @@ static int nwsalvage_hex_to_bytes(const char *hex, uint8 *out, size_t out_len) return(0); } +int nwsalvage_metadata_finder_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->finder_info_hex[0]) + return(0); + return(nwsalvage_hex_to_bytes(entry->finder_info_hex, out, out_len)); +} + static int nwsalvage_finder_info_is_zero(const uint8 *finder_info, size_t len) { size_t i; diff --git a/tests/afp/AFP_DELETED_FILE_INFO.md b/tests/afp/AFP_DELETED_FILE_INFO.md index 7f6e730..fba4c37 100644 --- a/tests/afp/AFP_DELETED_FILE_INFO.md +++ b/tests/afp/AFP_DELETED_FILE_INFO.md @@ -26,13 +26,13 @@ must exist and be verified. The relevant non-AFP NCP family is: - `NCP 0x2222 / 87 / 18` - Purge Salvageable File - optional legacy `NCP 0x2222 / 22 / 27` - Scan Salvageable File (old) -Once that backend is present, AFP `0x13` should only translate the WebSDK/NWAFP +AFP `0x13` translates the WebSDK/NWAFP wire request to the mars_nwe salvage entry and then append AFP-specific deleted Macintosh metadata. ## Intended future mapping -When the salvage backend exists, AFP `0x13` should behave like this: +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. diff --git a/tests/afp/AFP_ENDPOINT_INVENTORY.md b/tests/afp/AFP_ENDPOINT_INVENTORY.md index d837f0f..2f5295b 100644 --- a/tests/afp/AFP_ENDPOINT_INVENTORY.md +++ b/tests/afp/AFP_ENDPOINT_INVENTORY.md @@ -30,7 +30,7 @@ tests/afp/afp_endpoint_inventory.py | `0x10` | AFP 2.0 Set File Information | implemented | same backend discipline as `0x09` | | `0x11` | AFP 2.0 Scan File Information | implemented for path and directory entry-id starts | same backend discipline as `0x0a` | | `0x12` | AFP Get DOS Name From Entry ID | implemented | mars_nwe path/namespace and AFP entry-id reverse lookup | -| `0x13` | AFP Get Macintosh Info On Deleted Files | unsupported / backend-dependent | requires mars_nwe salvage/deleted-entry backend first | +| `0x13` | AFP Get Macintosh Info On Deleted Files | implemented / salvage-backed | covered by afp_deleted_info_smoke | ## Backend rules for the final audit @@ -51,18 +51,15 @@ must verify that each endpoint continues to use the mars_nwe core backend: - `user.org.mars-nwe.afp.attributes` only for future AFP-only bits - Resource forks remain unsupported and should return the documented completion code instead of inventing a parallel storage backend. -- Deleted-file Macintosh metadata (`0x13`) must wait for the mars_nwe salvage - backend and must not perform an AFP-local deleted-file scan. +- Deleted-file Macintosh metadata (`0x13`) is implemented on the mars_nwe + salvage backend and must not perform an AFP-local deleted-file scan. -## Known final-audit items +## Resolved final-audit items -One unsupported item should stay visible until the WebSDK / Novell header -comparison is completed: +Resolved WebSDK compatibility items: -1. `0x13` is intentionally unsupported because the salvage/deleted-entry backend - is not part of the current AFP slice. - -Resolved WebSDK compatibility item: +1. `0x13` is implemented as a salvage/deleted-entry backend adapter and covered + by `afp_deleted_info_smoke`. - `0x0a` / `0x11` entry-id-only scan requests are supported when the base entry ID resolves to a directory through mars_nwe namespace/basehandle logic. diff --git a/tests/afp/AFP_FINAL_AUDIT.md b/tests/afp/AFP_FINAL_AUDIT.md index edbb01c..2be4f31 100644 --- a/tests/afp/AFP_FINAL_AUDIT.md +++ b/tests/afp/AFP_FINAL_AUDIT.md @@ -79,7 +79,7 @@ mars_nwe helper family used by the endpoint, not just the AFP handler. | `0x10` | AFP 2.0 Set File Information | inline AFP 2.0 set file information case | TODO | TODO | TODO | TODO | AFP 2.0 variant | | `0x11` | AFP 2.0 Scan File Information | `afp_scan_file_information` | TODO | TODO | TODO | TODO | entry-id-only directory scan supported | | `0x12` | AFP Get DOS Name From Entry ID | `afp_get_dos_name_from_entry_id` | TODO | TODO | TODO | TODO | | -| `0x13` | AFP Get Macintosh Info On Deleted Files | unsupported | TODO | TODO | TODO | TODO | requires salvage/deleted-entry backend | +| `0x13` | AFP Get Macintosh Info On Deleted Files | implemented | server | helper | suite | docs | salvage-backed | ## Backend discipline checklist @@ -106,15 +106,15 @@ are not valid scan bases. ### Deleted-file Macintosh metadata -AFP `0x13` is intentionally unsupported for now. It should remain unsupported -until the normal NetWare salvage family is present and verified: +AFP `0x13` is now implemented as a thin adapter over the normal NetWare +salvage family, which is present and verified: - `NCP 0x2222 / 87 / 16` - Scan Salvageable Files - `NCP 0x2222 / 87 / 17` - Recover Salvageable File - `NCP 0x2222 / 87 / 18` - Purge Salvageable File - optional legacy `NCP 0x2222 / 22 / 27` - Scan Salvageable File (old) -Do not implement AFP `0x13` as an AFP-local deleted-file scan. +Do not extend AFP `0x13` as an AFP-local deleted-file scan; keep it on the shared salvage backend. ## Completion criteria diff --git a/tests/afp/AFP_WEBSK_AUDIT_FINDINGS.md b/tests/afp/AFP_WEBSK_AUDIT_FINDINGS.md index bae9a26..4176d96 100644 --- a/tests/afp/AFP_WEBSK_AUDIT_FINDINGS.md +++ b/tests/afp/AFP_WEBSK_AUDIT_FINDINGS.md @@ -66,7 +66,7 @@ map: | `0x10` | `0002R004.htm` | AFP 2.0 Set File Information | implemented through shared semantics | | `0x11` | `0002R003.htm` | AFP 2.0 Scan File Information | implemented for path and directory entry-id starts | | `0x12` | `0002R009.htm` | AFP Get DOS Name From Entry ID | implemented | -| `0x13` | `0002R014.htm` | AFP Get Macintosh Info On Deleted Files | unsupported / salvage-backend dependent | +| `0x13` | `0002R014.htm` | AFP Get Macintosh Info On Deleted Files | implemented / salvage-backend adapter | No extra AFP subfunction beyond `0x13` appeared in the WebSDK AFP reference TOC. @@ -115,7 +115,7 @@ The implementation remains intentionally narrow: 2. The existing mars_nwe directory scan path is reused after resolution. 3. File AFP xattr entry IDs are not valid scan bases. -### AFP 0x13 should remain unsupported for this slice +### AFP 0x13 is salvage-backed in this slice The WebSDK `0x13` page makes this call dependent on deleted-file state: the request is keyed by volume and DOS directory entry, and the reply describes a @@ -127,15 +127,15 @@ contains the normal salvage family: - Purge Salvageable File - Scan Salvageable File (old) -Therefore `0x13` should stay unsupported until mars_nwe has a verified -salvage/deleted-entry backend. It must not be implemented as an AFP-local scan -of live paths or filesystem trash. +Therefore `0x13` is implemented only after the verified mars_nwe +salvage/deleted-entry backend. It must remain a backend adapter, not an +AFP-local scan of live paths or filesystem trash. ## Result The WebSDK pass does not require renaming the AFP endpoint map again. The remaining concrete work before closing the current AFP slice is: -1. Keep `0x13` documented unsupported until salvage exists. +1. Keep `0x13` documented as salvage-backed; do not add a second AFP deleted-file store. 2. Re-run `tests/afp/afp_endpoint_inventory.py` and the AFP smoke suite. 3. Fill the final audit table with these WebSDK source paths and results. diff --git a/tests/afp/CMakeLists.txt b/tests/afp/CMakeLists.txt index 7af5cf4..4df6fc3 100644 --- a/tests/afp/CMakeLists.txt +++ b/tests/afp/CMakeLists.txt @@ -79,6 +79,10 @@ add_executable(afp_delete_smoke afp_delete_smoke.c) target_include_directories(afp_delete_smoke PRIVATE ${NCPFS_INCLUDE_DIR}) target_link_libraries(afp_delete_smoke ${NCPFS_LIBRARY}) +add_executable(afp_deleted_info_smoke afp_deleted_info_smoke.c) +target_include_directories(afp_deleted_info_smoke PRIVATE ${NCPFS_INCLUDE_DIR}) +target_link_libraries(afp_deleted_info_smoke ${NCPFS_LIBRARY}) + add_executable(afp_rename_smoke afp_rename_smoke.c) target_include_directories(afp_rename_smoke PRIVATE ${NCPFS_INCLUDE_DIR}) target_link_libraries(afp_rename_smoke ${NCPFS_LIBRARY}) diff --git a/tests/afp/README.md b/tests/afp/README.md index 600f472..a46f3aa 100644 --- a/tests/afp/README.md +++ b/tests/afp/README.md @@ -1386,3 +1386,22 @@ AFP 2.0 variants, bitmaps, record lengths, and unsupported completion codes for the complete smoke-covered endpoint set. It should also audit every AFP handler implementation and confirm that NetWare semantics still go through existing mars_nwe functions/wrappers rather than AFP-local shortcuts. + +## AFP Get Macintosh Info On Deleted File smoke test + +`afp_deleted_info_smoke` covers the WebSDK / Novell AFP deleted-file call: + +```text +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 +resource fork size, and the deleted original filename. It does not open or +expose `.recycle` or `.salvage` as AFP-visible paths. + +The full AFP smoke suite pre-cleans deleted entries in the tested directory, +creates a temporary AFP file, writes FinderInfo through AFP Set File +Information, deletes the file through AFP Delete, verifies `0x13`, and purges +the tested salvage entry afterwards. diff --git a/tests/afp/TODO.md b/tests/afp/TODO.md index 6e478ba..eb1d886 100644 --- a/tests/afp/TODO.md +++ b/tests/afp/TODO.md @@ -6,27 +6,21 @@ neighboring AFP documentation files, not in the root `TODO.md`. ## Remaining AFP work -### `0x13 AFP Get Macintosh Info On Deleted Files` +### `0x13 AFP Get Macintosh Info On Deleted File` Current status: -- Unsupported in the current AFP compatibility slice. -- Documented as salvage/deleted-entry-backend dependent. -- Must not be implemented as an AFP-local deleted-file scan. +- Implemented as a conservative adapter over the mars_nwe salvage/deleted-entry + backend. +- Covered by `afp_deleted_info_smoke` and the full `afp_smoke_suite.sh`. +- Does not scan or expose `.recycle` / `.salvage` as AFP-visible paths. -Required backend first: - -- `NCP 0x2222 / 87 / 16` - Scan Salvageable Files -- `NCP 0x2222 / 87 / 17` - Recover Salvageable File -- `NCP 0x2222 / 87 / 18` - Purge Salvageable File -- Optional legacy `NCP 0x2222 / 22 / 27` - Scan Salvageable File (old) - -Future AFP mapping: +Current AFP mapping: 1. Resolve the deleted DOS directory entry through the mars_nwe salvage backend. -2. Return FinderInfo from AFP metadata when available, otherwise zeroes. +2. Return FinderInfo from the salvage JSON snapshot. 3. Return ProDOS information as zeroes unless mars_nwe gains a real ProDOS store. -4. Return resource fork size as zero while resource forks remain unsupported. +4. Return resource fork size from the salvage JSON snapshot. 5. Return the deleted filename from the salvage/deleted-entry record. References: diff --git a/tests/afp/afp_deleted_info_smoke.c b/tests/afp/afp_deleted_info_smoke.c new file mode 100644 index 0000000..d115b0c --- /dev/null +++ b/tests/afp/afp_deleted_info_smoke.c @@ -0,0 +1,266 @@ +/* + * Linux smoke test for AFP 0x13 Get Macintosh Info On Deleted File. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifndef NCPC_SUBFUNCTION +#define NCPC_SUBFUNCTION 0x10000 +#endif +#ifndef NCPC_SFN +#define NCPC_SFN(FN, SFN) ((FN) | ((SFN) << 8) | NCPC_SUBFUNCTION) +#endif + +#define AFP_GET_MAC_INFO_DELETED 0x13 +#define NWE_NO_MORE_FILES 0x89ff +#define AFP_DELETED_REPLY_MAX 512 + +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" + "\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 --expect-name TEST SYS:PUBLIC\n" + " %s --purge-all -S MARS -U SUPERVISOR -P secret SYS:PUBLIC\n", + prog, prog, prog); +} + +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 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 int find_deleted(NWCONN_HANDLE conn, const char *dir, + const char *expect_name, + struct ncp_deleted_file *info, + char *name, size_t name_len) +{ + long err; + + memset(info, 0, sizeof(*info)); + info->seq = -1; + while ((err = ncp_ns_scan_salvageable_file(conn, NW_NS_DOS, + NCP_DIRSTYLE_NOHANDLE, 0, 0, (const unsigned char *)dir, + NCP_PATH_STD, info, name, (int)name_len)) == 0) { + printf("NCP salvage scan candidate seq=%d vol=%u base=%u name=%s\n", + (int)info->seq, (unsigned int)info->vol, + (unsigned int)info->base, name); + if (!expect_name || !strcmp(name, expect_name)) + return 0; + } + + fprintf(stderr, "No matching deleted AFP entry found: dir=%s expect=%s last_error=0x%04x\n", + dir, expect_name ? expect_name : "", (unsigned int)err); + return 1; +} + +static int purge_all(NWCONN_HANDLE conn, const char *dir) +{ + struct ncp_deleted_file info; + char name[512]; + long err; + int count = 0; + + memset(&info, 0, sizeof(info)); + info.seq = -1; + while ((err = ncp_ns_scan_salvageable_file(conn, NW_NS_DOS, + NCP_DIRSTYLE_NOHANDLE, 0, 0, (const unsigned char *)dir, + NCP_PATH_STD, &info, name, sizeof(name))) == 0) { + printf("NCP salvage purge candidate seq=%d vol=%u base=%u name=%s\n", + (int)info.seq, (unsigned int)info.vol, + (unsigned int)info.base, name); + err = ncp_ns_purge_file(conn, &info); + if (err) { + fprintf(stderr, "NCP purge failed: dir=%s name=%s err=0x%04x\n", + dir, name, (unsigned int)err); + return 1; + } + count++; + memset(&info, 0, sizeof(info)); + info.seq = -1; + } + + printf("NCP purge all done dir=%s purged=%d last_error=0x%04x\n", + dir, count, (unsigned int)err); + return 0; +} + +int main(int argc, char **argv) +{ + NWCONN_HANDLE conn; + NW_FRAGMENT reply; + long init_err = 0; + const char *dir = NULL; + const char *expect_name = NULL; + const char *expect_type = NULL; + const char *expect_creator = NULL; + int purge_after = 0; + int do_purge_all = 0; + struct ncp_deleted_file info; + char name[512]; + uint8_t request[5]; + uint8_t reply_buf[AFP_DELETED_REPLY_MAX]; + uint32_t resource_size; + int name_len; + NWCCODE err; + int i; + + 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], "--expect-name")) { + if (++i >= argc) { usage(argv[0]); ncp_close(conn); return 2; } + expect_name = argv[i]; + } else if (!strcmp(argv[i], "--expect-type")) { + if (++i >= argc || strlen(argv[i]) != 4) { + fprintf(stderr, "--expect-type must be four characters\n"); + ncp_close(conn); return 2; + } + expect_type = argv[i]; + } else if (!strcmp(argv[i], "--expect-creator")) { + if (++i >= argc || strlen(argv[i]) != 4) { + fprintf(stderr, "--expect-creator must be four characters\n"); + ncp_close(conn); return 2; + } + expect_creator = argv[i]; + } else if (!strcmp(argv[i], "--purge-after")) { + purge_after = 1; + } else if (!strcmp(argv[i], "--purge-all")) { + do_purge_all = 1; + } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { + usage(argv[0]); + ncp_close(conn); + return 0; + } else if (!dir) { + dir = argv[i]; + } else { + fprintf(stderr, "unexpected argument: %s\n", argv[i]); + usage(argv[0]); + ncp_close(conn); + return 2; + } + } + + if (!dir) { + usage(argv[0]); + ncp_close(conn); + return 2; + } + + if (do_purge_all) { + int rc = purge_all(conn, dir); + ncp_close(conn); + return rc; + } + + if (find_deleted(conn, dir, expect_name, &info, name, sizeof(name))) { + ncp_close(conn); + return 1; + } + + request[0] = (uint8_t)info.vol; + cpu_to_be32(info.base, request + 1); + + memset(reply_buf, 0, sizeof(reply_buf)); + reply.fragAddr.rw = reply_buf; + reply.fragSize = sizeof(reply_buf); + + err = NWRequestSimple(conn, NCPC_SFN(0x23, AFP_GET_MAC_INFO_DELETED), + request, sizeof(request), &reply); + if (err) { + fprintf(stderr, + "AFP Get Macintosh Info On Deleted File failed: completion=0x%02x (%u) vol=%u dosdir=0x%08x\n", + (unsigned int)err & 0xff, (unsigned int)err, + (unsigned int)info.vol, (unsigned int)info.base); + ncp_close(conn); + return 1; + } + + if (reply.fragSize < 43) { + fprintf(stderr, "short AFP deleted-info reply: %zu bytes\n", reply.fragSize); + ncp_close(conn); + return 1; + } + + resource_size = be32_to_cpu(reply_buf + 38); + name_len = reply_buf[42]; + if (name_len < 0 || 43 + name_len > (int)reply.fragSize) { + fprintf(stderr, "bad AFP deleted-info name length: %d reply=%zu\n", + name_len, reply.fragSize); + ncp_close(conn); + return 1; + } + + printf("AFP deleted info seq=%d vol=%u dosdir=%u resource=%u name=%.*s\n", + (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); + + if (expect_name && (name_len != (int)strlen(expect_name) || + memcmp(reply_buf + 43, expect_name, name_len))) { + fprintf(stderr, "AFP deleted info name mismatch: got=%.*s expected=%s\n", + name_len, reply_buf + 43, expect_name); + ncp_close(conn); + return 1; + } + if (expect_type && memcmp(reply_buf, expect_type, 4)) { + fprintf(stderr, "AFP deleted info Finder type mismatch: got=%.4s expected=%s\n", + reply_buf, expect_type); + ncp_close(conn); + return 1; + } + if (expect_creator && memcmp(reply_buf + 4, expect_creator, 4)) { + fprintf(stderr, "AFP deleted info Finder creator mismatch: got=%.4s expected=%s\n", + reply_buf + 4, expect_creator); + ncp_close(conn); + return 1; + } + + if (purge_after) { + err = ncp_ns_purge_file(conn, &info); + if (err) { + fprintf(stderr, "NCP purge after AFP deleted-info failed: err=0x%04x\n", + (unsigned int)err); + ncp_close(conn); + return 1; + } + printf("NCP purge after AFP deleted-info ok seq=%d name=%s\n", + (int)info.seq, name); + } + + ncp_close(conn); + return 0; +} diff --git a/tests/afp/afp_endpoint_inventory.py b/tests/afp/afp_endpoint_inventory.py index 73d86b9..7651ac7 100755 --- a/tests/afp/afp_endpoint_inventory.py +++ b/tests/afp/afp_endpoint_inventory.py @@ -70,7 +70,7 @@ INLINE_ENDPOINTS: Dict[int, Tuple[str, str, Tuple[str, ...]]] = { 0x0D: ("inline AFP 2.0 create directory case", "implemented", ("mars_nwe path/namespace", "mars_nwe object lifecycle")), 0x0E: ("inline AFP 2.0 create file case", "implemented", ("mars_nwe path/namespace", "mars_nwe object lifecycle", "AFP-only xattrs via nwatalk")), 0x10: ("inline AFP 2.0 set file information case", "implemented", ("mars_nwe path/namespace", "mars_nwe attributes/archive/fileinfo", "mars_nwe trustee/rights", "AFP-only xattrs via nwatalk")), - 0x13: ("manual review", "unsupported / final-audit item", ("needs manual review",)), + 0x13: ("inline AFP deleted-info case", "implemented", ("mars_nwe salvage/deleted-entry backend", "AFP FinderInfo snapshot")), } diff --git a/tests/afp/afp_smoke_suite.sh b/tests/afp/afp_smoke_suite.sh index c86775e..85159a6 100755 --- a/tests/afp/afp_smoke_suite.sh +++ b/tests/afp/afp_smoke_suite.sh @@ -710,6 +710,34 @@ run_cmd \ "$SCRIPT_DIR/afp_set_file_info_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" \ --afp09 --finder-info-only --type "$FINDER_TYPE" --creator "$FINDER_CREATOR" "$NETWARE_PATH" +run_optional_cmd \ + "Prepare AFP Deleted Info purge" \ + "./afp_deleted_info_smoke --purge-all $COMMON_PRINT '$DIR_PATH'" \ + "$SCRIPT_DIR/afp_deleted_info_smoke" --purge-all \ + -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$DIR_PATH" || true +run_optional_cmd \ + "Prepare AFP Deleted Info file cleanup" \ + "./afp_delete_smoke $COMMON_PRINT '$CREATE_FILE_PATH'" \ + "$SCRIPT_DIR/afp_delete_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$CREATE_FILE_PATH" || true +run_cmd \ + "AFP Deleted Info create file" \ + "./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'" \ + "$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" +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 \ + -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$DIR_PATH" + run_cmd \ "AFP Set File Information Hidden" \ "./afp_set_file_info_smoke $COMMON_PRINT --attributes-only --hidden '$NETWARE_PATH'" \