diff --git a/include/nwsalvage.h b/include/nwsalvage.h index c7ffac3..063f222 100644 --- a/include/nwsalvage.h +++ b/include/nwsalvage.h @@ -3,6 +3,7 @@ #include #include +#include typedef int (*nwsalvage_ini_getter)(int entry, char *str, size_t strsize, void *data); @@ -145,5 +146,14 @@ 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); +/* + * Capture a server-side delete before nw_unlink_node() would remove it. + * Returns 0 when the file was moved to the recycle repository and metadata + * was written. Returns 1 when salvage is disabled or the node is not + * salvageable and the caller should continue with the normal unlink path. + * Returns -1 on a real salvage failure; the caller should keep the live file. + */ +int nwsalvage_capture_node_delete(int volume, const char *unixname, + const struct stat *stb); #endif diff --git a/src/connect.c b/src/connect.c index 70bc1fc..5eb1468 100644 --- a/src/connect.c +++ b/src/connect.c @@ -24,6 +24,7 @@ #include "net.h" #include "unxfile.h" +#include "nwsalvage.h" #include #include @@ -1976,6 +1977,7 @@ static int nw_rmdir(uint8 *unname) int nw_unlink_node(int volume, uint8 *unname, struct stat *stb) { int result=-1; + int salvaged=0; uint32 attrib=get_nw_attrib_dword(volume, unname, stb); /* first we look for attributes */ if (attrib & (FILE_ATTR_R|FILE_ATTR_DELETE_INH)) @@ -2000,13 +2002,20 @@ int nw_unlink_node(int volume, uint8 *unname, struct stat *stb) if (!(entry8_flags&0x10) && -1 == share_file(stb->st_dev, stb->st_ino, 0x10f, 2)) return(-0x8a); /* NO Delete Privileges, file is open */ - if (0 != (result=unlink(unname))){ - if (seteuid(0)) {} - result=unlink(unname) ? -0x8a : 0; - (void)reseteuid(); + result = nwsalvage_capture_node_delete(volume, (const char *)unname, stb); + if (!result) { + salvaged = 1; + } else if (result > 0) { + if (0 != (result=unlink(unname))){ + if (seteuid(0)) {} + result=unlink(unname) ? -0x8a : 0; + (void)reseteuid(); + } + } else if (result < 0) { + result = -0x8a; } } - if (!result) { + if (!result && !salvaged) { free_nw_ext_inode(volume, unname, stb->st_dev, stb->st_ino); } return(result); diff --git a/src/nwsalvage.c b/src/nwsalvage.c index 89de18f..dfe3c00 100644 --- a/src/nwsalvage.c +++ b/src/nwsalvage.c @@ -1,6 +1,15 @@ /* nwsalvage.c - NetWare salvage/recycle backend helpers */ #include "nwsalvage.h" +#include "net.h" +#include "nwvolume.h" +#include "nwarchive.h" +#include "nwatalk.h" +#include "nwattrib.h" +#include "trustee.h" +#include "tools.h" +#include "connect.h" + #include #include #include @@ -12,6 +21,7 @@ #include #include #include +#include #include int nwsalvage_repository_name_valid(const char *name) @@ -999,3 +1009,408 @@ int nwsalvage_read_metadata(const char *metadata_path, return(0); } #endif + +#ifdef MARS_NWE_HAVE_YYJSON +static int nwsalvage_move_deleted_file(const char *live_path, + const char *recycle_path, + const char *metadata_path, + const struct nwsalvage_deleted_entry *entry) +{ + int saved_errno; + + if (!live_path || !*live_path || + !recycle_path || !*recycle_path || + !metadata_path || !*metadata_path || + !entry) { + errno = EINVAL; + return(-1); + } + + if (!strcmp(live_path, recycle_path) || + !strcmp(live_path, metadata_path) || + !strcmp(recycle_path, metadata_path)) { + errno = EINVAL; + return(-1); + } + + if (access(recycle_path, F_OK) == 0) { + errno = EEXIST; + return(-1); + } + if (errno != ENOENT) + return(-1); + if (access(metadata_path, F_OK) == 0) { + errno = EEXIST; + return(-1); + } + if (errno != ENOENT) + return(-1); + + if (make_parent_dirs(recycle_path) < 0 || make_parent_dirs(metadata_path) < 0) + return(-1); + + if (rename(live_path, recycle_path) < 0) + return(-1); + + if (nwsalvage_write_metadata(metadata_path, entry) == 0) + return(0); + + saved_errno = errno; + unlink(metadata_path); + if (rename(recycle_path, live_path) < 0) { + errno = saved_errno; + return(-1); + } + + errno = saved_errno; + return(-1); +} +#endif + +static int nwsalvage_ini_get(int entry, char *str, size_t strsize, void *data) +{ + (void)data; + return(get_ini_entry(NULL, entry, (uint8 *)str, (int)strsize)); +} + +static int nwsalvage_copy_volume_root(int volume, char *root, size_t root_len) +{ + size_t len; + + if (volume < 0 || volume >= used_nw_volumes || + !nw_volumes[volume].unixname || !nw_volumes[volume].unixnamlen || + !root || !root_len) { + errno = EINVAL; + return(-1); + } + + len = (size_t)nw_volumes[volume].unixnamlen; + if (len >= root_len) { + errno = ENAMETOOLONG; + return(-1); + } + memcpy(root, nw_volumes[volume].unixname, len); + root[len] = '\0'; + while (len > 1 && root[len - 1] == '/') { + root[--len] = '\0'; + } + return(0); +} + +static int nwsalvage_relative_from_unix(int volume, const char *unixname, + char *relative, size_t relative_len) +{ + char root[NWSALVAGE_PATH_MAX]; + size_t root_len; + const char *p; + size_t len; + + if (!unixname || !*unixname || !relative || !relative_len) { + errno = EINVAL; + return(-1); + } + + if (nwsalvage_copy_volume_root(volume, root, sizeof(root)) < 0) + return(-1); + + root_len = strlen(root); + if (strncmp(unixname, root, root_len)) { + errno = EINVAL; + return(-1); + } + + p = unixname + root_len; + if (*p == '/') + p++; + if (!*p) { + errno = EINVAL; + return(-1); + } + + len = strlen(p); + if (len >= relative_len) { + errno = ENAMETOOLONG; + return(-1); + } + memcpy(relative, p, len + 1); + return(0); +} + +static int nwsalvage_relative_is_repo_path(const char *relative, + const char *repository) +{ + size_t len; + + if (!relative || !repository) + return(0); + len = strlen(repository); + return(!strcmp(relative, repository) || + (!strncmp(relative, repository, len) && relative[len] == '/')); +} + +static const char *nwsalvage_basename(const char *path) +{ + const char *p; + + if (!path) + return(""); + p = strrchr(path, '/'); + return(p ? p + 1 : path); +} + +static void nwsalvage_format_deleted_by(char *out, size_t out_len) +{ + if (!out || !out_len) + return; + + if (act_obj_id == 1) + strmaxcpy((uint8 *)out, "SUPERVISOR", (int)out_len - 1); + else + slprintf(out, (int)out_len - 1, "0x%08x", act_obj_id); +} + +static void nwsalvage_format_hex(char *out, size_t out_len, + const uint8 *data, size_t data_len) +{ + static const char hex[] = "0123456789abcdef"; + size_t i; + + if (!out || out_len < data_len * 2 + 1) + return; + for (i = 0; i < data_len; i++) { + out[i * 2] = hex[(data[i] >> 4) & 0xf]; + out[i * 2 + 1] = hex[data[i] & 0xf]; + } + out[data_len * 2] = '\0'; +} + +static unsigned long nwsalvage_parent_entry_id(const char *unixname) +{ + char parent[NWSALVAGE_PATH_MAX]; + char *slash; + uint32 entry_id = 0; + size_t len; + + if (!unixname || !*unixname) + return(0); + len = strlen(unixname); + if (len >= sizeof(parent)) + return(0); + memcpy(parent, unixname, len + 1); + slash = strrchr(parent, '/'); + if (!slash || slash == parent) + return(0); + *slash = '\0'; + + if (nwatalk_get_entry_id(parent, &entry_id) == 0) + return((unsigned long)entry_id); + return(0); +} + +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 *afp_entry_id, + size_t afp_entry_id_len) +{ + uint8 finder_info[NWATALK_FINDER_INFO_LEN]; + uint16 afp_attributes = 0; + uint32 entry_id = 0; + uint32 resource_size = 0; + + memset(finder_info, 0, sizeof(finder_info)); + if (nwatalk_get_finder_info(unixname, finder_info, sizeof(finder_info)) != 0) + memset(finder_info, 0, sizeof(finder_info)); + nwsalvage_format_hex(finder_info_hex, finder_info_hex_len, + finder_info, sizeof(finder_info)); + entry->finder_info_hex = finder_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 + afp_entry_id[0] = '\0'; + entry->afp_entry_id = afp_entry_id; + + if (nwatalk_get_afp_attributes(unixname, &afp_attributes) == 0) + entry->afp_attributes = afp_attributes; + if (nwatalk_get_resource_fork_size(unixname, &resource_size) == 0) + entry->resource_fork_size = resource_size; +} + +static void nwsalvage_fill_netware_xattrs(const char *unixname, + struct nwsalvage_deleted_entry *entry) +{ + uint16 archive_date = 0; + uint16 archive_time = 0; + uint32 archiver_id = 0; + uint8 archive_flags = 0; + uint16 create_date = 0; + uint16 create_time = 0; + uint32 creator_id = 0; + uint32 modifier_id = 0; + uint8 fileinfo_flags = 0; + uint8 modifier_flags = 0; + + mars_nwe_get_archive_info((char *)unixname, &archive_date, &archive_time, + &archiver_id, &archive_flags); + mars_nwe_get_file_info((char *)unixname, &create_date, &create_time, + &creator_id, &fileinfo_flags); + mars_nwe_get_file_modifier_info((char *)unixname, &modifier_id, + &modifier_flags); + + entry->netware_archive_flags = archive_flags; + entry->netware_archive_date = archive_date; + entry->netware_archive_time = archive_time; + entry->netware_archiver_id = archiver_id; + + entry->netware_fileinfo_flags = fileinfo_flags | modifier_flags; + entry->netware_create_date = create_date; + entry->netware_create_time = create_time; + entry->netware_creator_id = creator_id; + entry->netware_modifier_id = modifier_id; +} + +static void nwsalvage_fill_trustees(int volume, const char *unixname, + const struct stat *stb, + struct nwsalvage_deleted_entry *entry) +{ + uint32 ids[NWSALVAGE_TRUSTEE_MAX]; + int trustees[NWSALVAGE_TRUSTEE_MAX]; + int count; + int i; + + entry->inherited_rights_mask = + (unsigned int)tru_get_inherited_mask(volume, (uint8 *)unixname, + (struct stat *)stb); + + count = tru_get_trustee_set(volume, (uint8 *)unixname, (struct stat *)stb, + 0, NWSALVAGE_TRUSTEE_MAX, ids, trustees); + if (count <= 0) + return; + if (count > NWSALVAGE_TRUSTEE_MAX) + count = NWSALVAGE_TRUSTEE_MAX; + + entry->trustee_count = (unsigned int)count; + for (i = 0; i < count; i++) { + entry->trustees[i].object_id = ids[i]; + entry->trustees[i].rights = (unsigned int)trustees[i]; + } +} + +int nwsalvage_capture_node_delete(int volume, const char *unixname, + const struct stat *stb) +{ +#ifndef MARS_NWE_HAVE_YYJSON + (void)volume; + (void)unixname; + (void)stb; + return(1); +#else + struct nwsalvage_config config; + struct nwsalvage_deleted_entry entry; + char volume_root[NWSALVAGE_PATH_MAX]; + char relative_path[NWSALVAGE_PATH_MAX]; + char recycle_path[NWSALVAGE_PATH_MAX]; + char metadata_path[NWSALVAGE_PATH_MAX]; + char salvage_relative_path[NWSALVAGE_PATH_MAX]; + char recycle_relative_path[NWSALVAGE_PATH_MAX]; + char metadata_relative_path[NWSALVAGE_PATH_MAX]; + char original_path[NWSALVAGE_PATH_MAX]; + 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 afp_entry_id[NWSALVAGE_AFP_ENTRY_ID_MAX]; + + if (!unixname || !*unixname || !stb) { + errno = EINVAL; + return(-1); + } + + if (!S_ISREG(stb->st_mode)) + return(1); + + if (nwsalvage_config_load_from_ini(&config, nwsalvage_ini_get, NULL) < 0) + return(-1); + if (!config.enabled) + return(1); + + if (nwsalvage_copy_volume_root(volume, volume_root, sizeof(volume_root)) < 0 || + nwsalvage_relative_from_unix(volume, unixname, + relative_path, sizeof(relative_path)) < 0) + return(-1); + + if (nwsalvage_relative_is_repo_path(relative_path, + config.recycle_repository) || + nwsalvage_relative_is_repo_path(relative_path, + config.metadata_repository)) + return(1); + + nwsalvage_format_deleted_by(deleted_by, sizeof(deleted_by)); + if (slprintf(salvage_relative_path, sizeof(salvage_relative_path) - 1, + "%s/%s", deleted_by, relative_path) < 0) { + errno = ENAMETOOLONG; + return(-1); + } + + if (nwsalvage_build_recycle_path(recycle_path, sizeof(recycle_path), + &config, volume_root, + salvage_relative_path) < 0 || + nwsalvage_build_metadata_path(metadata_path, sizeof(metadata_path), + &config, volume_root, + salvage_relative_path) < 0 || + nwsalvage_build_recycle_relative_path(recycle_relative_path, + sizeof(recycle_relative_path), + &config, + salvage_relative_path) < 0 || + nwsalvage_build_metadata_relative_path(metadata_relative_path, + sizeof(metadata_relative_path), + &config, + salvage_relative_path) < 0) + return(-1); + + if (nw_get_volume_name(volume, (uint8 *)volume_name, + sizeof(volume_name)) < 1) + return(-1); + + if (slprintf(original_path, sizeof(original_path) - 1, "%s:%s", + volume_name, relative_path) < 0) { + errno = ENAMETOOLONG; + return(-1); + } + + memset(&entry, 0, sizeof(entry)); + memset(finder_info_hex, '0', NWSALVAGE_FINDER_INFO_HEX_LEN); + finder_info_hex[NWSALVAGE_FINDER_INFO_HEX_LEN] = '\0'; + afp_entry_id[0] = '\0'; + + entry.source = "mars_nwe"; + entry.volume_name = volume_name; + entry.deleted_by = deleted_by; + entry.deleted_at = (long)time(NULL); + entry.original_path = original_path; + entry.original_parent_entry_id = nwsalvage_parent_entry_id(unixname); + entry.original_name = nwsalvage_basename(relative_path); + entry.recycle_relative_path = recycle_relative_path; + entry.salvage_relative_path = metadata_relative_path; + entry.attributes = get_nw_attrib_dword(volume, (char *)unixname, + (struct stat *)stb); + entry.mode = (unsigned long)stb->st_mode; + entry.size = (unsigned long long)stb->st_size; + entry.atime = (long)stb->st_atime; + entry.mtime = (long)stb->st_mtime; + entry.ctime = (long)stb->st_ctime; + + nwsalvage_fill_afp_metadata(unixname, &entry, finder_info_hex, + sizeof(finder_info_hex), afp_entry_id, + sizeof(afp_entry_id)); + nwsalvage_fill_netware_xattrs(unixname, &entry); + nwsalvage_fill_trustees(volume, unixname, stb, &entry); + + if (nwsalvage_move_deleted_file(unixname, recycle_path, + metadata_path, &entry) < 0) + return(-1); + + return(0); +#endif +} diff --git a/tests/salvage/CMakeLists.txt b/tests/salvage/CMakeLists.txt index 312e4bc..8328d71 100644 --- a/tests/salvage/CMakeLists.txt +++ b/tests/salvage/CMakeLists.txt @@ -27,11 +27,56 @@ set_property(DIRECTORY APPEND PROPERTY ADDITIONAL_CLEAN_FILES ${SALVAGE_LAYOUT_SMOKE_SCRIPT} ) +set(SALVAGE_NCP_DELETE_SMOKE_SCRIPT + ${CMAKE_CURRENT_BINARY_DIR}/salvage_ncp_delete_smoke.sh +) + +add_custom_command( + OUTPUT ${SALVAGE_NCP_DELETE_SMOKE_SCRIPT} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/salvage_ncp_delete_smoke.sh + ${SALVAGE_NCP_DELETE_SMOKE_SCRIPT} + COMMAND ${CMAKE_COMMAND} -E env chmod +x ${SALVAGE_NCP_DELETE_SMOKE_SCRIPT} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/salvage_ncp_delete_smoke.sh + COMMENT "Copying salvage NCP delete smoke helper" + VERBATIM +) + +add_custom_target(salvage_ncp_delete_smoke_suite ALL + DEPENDS ${SALVAGE_NCP_DELETE_SMOKE_SCRIPT} +) + +find_path(SALVAGE_NCPFS_INCLUDE_DIR + NAMES ncp/nwcalls.h ncp/ncplib.h +) + +find_library(SALVAGE_NCPFS_LIBRARY + NAMES ncp +) + +if(SALVAGE_NCPFS_INCLUDE_DIR AND SALVAGE_NCPFS_LIBRARY) + add_executable(ncp_delete_smoke ncp_delete_smoke.c) + target_include_directories(ncp_delete_smoke PRIVATE ${SALVAGE_NCPFS_INCLUDE_DIR}) + target_link_libraries(ncp_delete_smoke ${SALVAGE_NCPFS_LIBRARY}) +else() + message(STATUS + "Skipping salvage NCP delete smoke helper: ncpfs/libncp headers or library not found" + ) +endif() + add_executable(salvage_config_smoke salvage_config_smoke.c ${CMAKE_SOURCE_DIR}/src/nwsalvage.c ) +# nwsalvage.c now also contains the server-side delete hook. The config smoke +# only exercises the standalone helper surface, so let the linker discard the +# unreferenced server hook sections instead of requiring all nwconn objects. +set_target_properties(salvage_config_smoke PROPERTIES + COMPILE_FLAGS "-ffunction-sections -fdata-sections" + LINK_FLAGS "-Wl,--gc-sections" +) + target_include_directories(salvage_config_smoke PRIVATE ${CMAKE_SOURCE_DIR}/include ) diff --git a/tests/salvage/README.md b/tests/salvage/README.md index 474bb14..ab10879 100644 --- a/tests/salvage/README.md +++ b/tests/salvage/README.md @@ -48,3 +48,35 @@ volume/dev/inode, rather than in the file payload itself. Salvage metadata therefore needs to preserve the inherited rights mask and the explicit trustee object/right pairs so recovery can recreate equivalent trustee records for the restored object. + +## NCP delete capture smoke + +`salvage_ncp_delete_smoke.sh` is the integration check for the first real +server-side salvage hook. It uses `ncp_delete_smoke`, a small libncp client, to +create and delete a file through the classic NetWare NCP file functions: + +- `0x43` create file +- `0x42` close file +- `0x44` delete file + +The script does not call local `rm`/`unlink` for the tested live path. Local +filesystem access is used only after the NCP delete to inspect the expected +recycle payload and JSON sidecar: + +```text +SYS/.recycle/SUPERVISOR/PUBLIC/SLVGCHK.TXT +SYS/.salvage/SUPERVISOR/PUBLIC/SLVGCHK.TXT.json +``` + +Example: + +```sh +./tests/salvage/salvage_ncp_delete_smoke.sh \ + -S MARS -U SUPERVISOR -P secret \ + --path SYS:PUBLIC/SLVGCHK.TXT \ + --unix-path /var/mars_nwe/SYS/public/SLVGCHK.TXT +``` + +This check lives under `tests/salvage` rather than `tests/afp` because later +salvage scan/recover/purge and versioning tests should share the same fixture. +AFP can use the same backend later as an adapter for `0x13`. diff --git a/tests/salvage/ncp_delete_smoke.c b/tests/salvage/ncp_delete_smoke.c new file mode 100644 index 0000000..0e4349d --- /dev/null +++ b/tests/salvage/ncp_delete_smoke.c @@ -0,0 +1,245 @@ +/* + * Linux smoke helper for classic NetWare file create/delete. + * + * The helper intentionally uses normal NCP file create/close/delete requests, + * not a local Unix unlink. It is used by the salvage smoke script to verify + * that mars_nwe captures files deleted through the server path. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#define NCP_CREATE_FILE_OVERWRITE 0x43 +#define NCP_CLOSE_FILE 0x42 +#define NCP_DELETE_FILE 0x44 +#define NWE_INVALID_PATH 0x9c +#ifndef NWE_INVALID_NCP_PACKET_LENGTH +#define NWE_INVALID_NCP_PACKET_LENGTH 0x7e +#endif + +static void usage(const char *prog) +{ + fprintf(stderr, + "Usage: %s [--expect-delete CODE] [--create-only] [--delete-only] " + "[ncpfs options] PATH\n" + "\n" + "ncpfs options are parsed by ncp_initialize(), for example:\n" + " -S SERVER -U USER -P PASSWORD -n\n" + "\n" + "Example:\n" + " %s -S MARS -U SUPERVISOR -P secret SYS:PUBLIC/SALVAGE.TXT\n", + prog, prog); +} + +static int parse_u32(const char *text, uint32_t *value) +{ + char *end = NULL; + unsigned long v; + + errno = 0; + v = strtoul(text, &end, 0); + if (errno || !end || *end || v > 0xffffffffUL) + return -1; + *value = (uint32_t)v; + return 0; +} + +static uint32_t le32_to_cpu(const uint8_t p[4]) +{ + return ((uint32_t)p[0]) | + ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | + ((uint32_t)p[3] << 24); +} + +static void cpu_to_le32(uint32_t v, uint8_t p[4]) +{ + p[0] = (uint8_t)v; + p[1] = (uint8_t)(v >> 8); + p[2] = (uint8_t)(v >> 16); + p[3] = (uint8_t)(v >> 24); +} + +static NWCCODE ncp_create_file(NWCONN_HANDLE conn, const char *path, + uint32_t *file_handle) +{ + size_t path_len = strlen(path); + uint8_t request[1 + 1 + 1 + 255]; + uint8_t reply_buf[2 + 4 + 2 + 64]; + NW_FRAGMENT reply; + NWCCODE err; + + if (path_len > 255) + return NWE_INVALID_PATH; + + request[0] = 0; /* directory handle 0, raw VOL:path */ + request[1] = 0; /* create attributes */ + request[2] = (uint8_t)path_len; + memcpy(request + 3, path, path_len); + + memset(reply_buf, 0, sizeof(reply_buf)); + reply.fragAddr.rw = reply_buf; + reply.fragSize = sizeof(reply_buf); + + err = NWRequestSimple(conn, NCP_CREATE_FILE_OVERWRITE, + request, 3 + path_len, &reply); + if (err) + return err; + if (reply.fragSize < 6) + return NWE_INVALID_NCP_PACKET_LENGTH; + + *file_handle = le32_to_cpu(reply_buf + 2); + return 0; +} + +static NWCCODE ncp_close_file(NWCONN_HANDLE conn, uint32_t file_handle) +{ + uint8_t request[1 + 2 + 4]; + NW_FRAGMENT reply; + NWCCODE err; + + memset(request, 0, sizeof(request)); + request[0] = 0; /* reserved */ + request[1] = 0; + request[2] = 0; /* extended file handle */ + cpu_to_le32(file_handle, request + 3); + + reply.fragAddr.rw = NULL; + reply.fragSize = 0; + + err = NWRequestSimple(conn, NCP_CLOSE_FILE, + request, sizeof(request), &reply); + return err; +} + +static NWCCODE ncp_delete_file(NWCONN_HANDLE conn, const char *path) +{ + size_t path_len = strlen(path); + uint8_t request[1 + 1 + 1 + 255]; + NW_FRAGMENT reply; + + if (path_len > 255) + return NWE_INVALID_PATH; + + request[0] = 0; /* directory handle 0, raw VOL:path */ + request[1] = 0; /* search attributes */ + request[2] = (uint8_t)path_len; + memcpy(request + 3, path, path_len); + + reply.fragAddr.rw = NULL; + reply.fragSize = 0; + + return NWRequestSimple(conn, NCP_DELETE_FILE, + request, 3 + path_len, &reply); +} + +int main(int argc, char **argv) +{ + NWCONN_HANDLE conn; + long init_err = 0; + const char *path = NULL; + uint32_t expect_delete = 0xffffffffU; + uint32_t file_handle = 0; + int create = 1; + int delete = 1; + int i; + NWCCODE err; + + 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-delete")) { + if (++i >= argc || parse_u32(argv[i], &expect_delete) || + expect_delete > 255) { + fprintf(stderr, "invalid --expect-delete value\n"); + ncp_close(conn); + return 2; + } + } else if (!strcmp(argv[i], "--create-only")) { + create = 1; + delete = 0; + } else if (!strcmp(argv[i], "--delete-only")) { + create = 0; + delete = 1; + } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { + usage(argv[0]); + ncp_close(conn); + return 0; + } else if (!path) { + path = argv[i]; + } else { + fprintf(stderr, "unexpected argument: %s\n", argv[i]); + usage(argv[0]); + ncp_close(conn); + return 2; + } + } + + if (!path) { + usage(argv[0]); + ncp_close(conn); + return 2; + } + + if (create) { + err = ncp_create_file(conn, path, &file_handle); + if (err) { + fprintf(stderr, "NCP create failed: path=%s error=0x%04x\n", + path, (unsigned int)err); + ncp_close(conn); + return 1; + } + err = ncp_close_file(conn, file_handle); + if (err) { + fprintf(stderr, + "NCP close failed: path=%s handle=0x%08x error=0x%04x\n", + path, (unsigned int)file_handle, (unsigned int)err); + ncp_close(conn); + return 1; + } + printf("NCP create path=%s handle=0x%08x verified\n", + path, (unsigned int)file_handle); + } + + if (delete) { + err = ncp_delete_file(conn, path); + if (expect_delete != 0xffffffffU) { + if (err != (NWCCODE)expect_delete) { + fprintf(stderr, + "NCP delete completion mismatch: path=%s got=0x%04x expected=0x%02x\n", + path, (unsigned int)err, (unsigned int)expect_delete); + ncp_close(conn); + return 1; + } + printf("NCP delete returned expected completion 0x%02x: path=%s\n", + (unsigned int)expect_delete, path); + } else if (err) { + fprintf(stderr, "NCP delete failed: path=%s error=0x%04x\n", + path, (unsigned int)err); + ncp_close(conn); + return 1; + } else { + printf("NCP delete path=%s verified\n", path); + } + } + + ncp_close(conn); + return 0; +} diff --git a/tests/salvage/salvage_ncp_delete_smoke.sh b/tests/salvage/salvage_ncp_delete_smoke.sh new file mode 100755 index 0000000..a83b29c --- /dev/null +++ b/tests/salvage/salvage_ncp_delete_smoke.sh @@ -0,0 +1,256 @@ +#!/usr/bin/env bash +# Verify that a file deleted through mars_nwe's normal NCP delete path is +# captured into .recycle with a .salvage JSON sidecar. + +set -u + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +SERVER="MARS" +USER_NAME="SUPERVISOR" +PASSWORD="" +NETWARE_PATH="SYS:PUBLIC/SLVGCHK.TXT" +UNIX_PATH="/var/mars_nwe/SYS/public/SLVGCHK.TXT" +RECYCLE_REPOSITORY=".recycle" +SALVAGE_REPOSITORY=".salvage" +DELETED_BY="" +OUT_FILE="" +KEEP_GOING=1 + +usage() { + cat <&2 + usage >&2 + exit 2 ;; + esac +done + +if [ -z "$PASSWORD" ]; then + echo "Missing password. Use -P PASSWORD." >&2 + usage >&2 + exit 2 +fi +if [ -z "$DELETED_BY" ]; then + DELETED_BY=$USER_NAME +fi +case "$NETWARE_PATH" in + *:*) ;; + *) echo "--path must include a NetWare volume prefix, e.g. SYS:PUBLIC/FILE" >&2; exit 2 ;; +esac +case "$UNIX_PATH" in + /*) ;; + *) echo "--unix-path must be absolute" >&2; exit 2 ;; +esac + +REPORT_TMP=$(mktemp "${TMPDIR:-/tmp}/mars-salvage-ncp.XXXXXX") +FAILURES=0 + +cleanup() { + rm -f "$REPORT_TMP" +} +trap cleanup EXIT INT TERM + +emit() { + printf '%s\n' "$*" | tee -a "$REPORT_TMP" +} + +section() { + emit "" + emit "## $*" +} + +finish_report() { + section "Summary" + emit "failures=$FAILURES" + if [ -n "$OUT_FILE" ]; then + cp "$REPORT_TMP" "$OUT_FILE" + emit "report=$OUT_FILE" + fi +} + +fail_check() { + emit "$*" + FAILURES=$((FAILURES + 1)) + if [ "$KEEP_GOING" -eq 0 ]; then + finish_report + exit 1 + fi +} + +run_cmd() { + local label=$1 + local printable=$2 + shift 2 + + section "$label" + emit "\$ $printable" + "$@" 2>&1 | tee -a "$REPORT_TMP" + local status=${PIPESTATUS[0]} + emit "[exit=$status]" + if [ "$status" -ne 0 ]; then + fail_check "$label failed" + fi +} + +count_path_components() { + local path=$1 + local old_ifs=$IFS + local count=0 + local part + + path=${path#*:} + path=${path#/} + path=${path#\\} + path=${path//\\//} + IFS=/ + for part in $path; do + [ -n "$part" ] && count=$((count + 1)) + done + IFS=$old_ifs + printf '%s\n' "$count" +} + +compute_unix_volume_root() { + local root + local components + + root=$(dirname -- "$UNIX_PATH") + components=$(count_path_components "$NETWARE_PATH") || return 1 + while [ "$components" -gt 1 ]; do + root=$(dirname -- "$root") + components=$((components - 1)) + done + printf '%s\n' "$root" +} + +relative_to_volume_root() { + local path=$1 + local root=$2 + + case "$path" in + "$root"/*) printf '%s\n' "${path#$root/}" ;; + *) return 1 ;; + esac +} + +check_file() { + local label=$1 + local path=$2 + + section "$label" + emit "path=$path" + if [ ! -f "$path" ]; then + fail_check "missing file: $path" + return 1 + fi + return 0 +} + +check_metadata() { + local path=$1 + + section "salvage metadata JSON" + emit "\$ cat '$path'" + cat "$path" 2>&1 | tee -a "$REPORT_TMP" + local status=${PIPESTATUS[0]} + emit "[exit=$status]" + if [ "$status" -ne 0 ]; then + fail_check "could not cat metadata: $path" + return 1 + fi + + grep -q '"source"[[:space:]]*:[[:space:]]*"mars_nwe"' "$path" || fail_check "metadata missing source=mars_nwe" + grep -q '"original_path"' "$path" || fail_check "metadata missing original_path" + grep -q '"recycle_relative_path"' "$path" || fail_check "metadata missing recycle_relative_path" + grep -q '"salvage_relative_path"' "$path" || fail_check "metadata missing salvage_relative_path" + grep -q '"trustees"' "$path" || fail_check "metadata missing trustees" +} + +section "mars_nwe salvage NCP delete smoke" +emit "date=$(date -Is)" +emit "server=$SERVER" +emit "user=$USER_NAME" +emit "path=$NETWARE_PATH" +emit "unix_path=$UNIX_PATH" +emit "deleted_by=$DELETED_BY" +emit "recycle_repository=$RECYCLE_REPOSITORY" +emit "salvage_repository=$SALVAGE_REPOSITORY" + +run_cmd \ + "NCP create/delete" \ + "./ncp_delete_smoke -S '$SERVER' -U '$USER_NAME' -P ****** '$NETWARE_PATH'" \ + "$SCRIPT_DIR/ncp_delete_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$NETWARE_PATH" + +VOLUME_ROOT=$(compute_unix_volume_root) || { + fail_check "could not compute Unix volume root from $NETWARE_PATH and $UNIX_PATH" + VOLUME_ROOT="" +} +RELATIVE_PATH="" +if [ -n "$VOLUME_ROOT" ]; then + RELATIVE_PATH=$(relative_to_volume_root "$UNIX_PATH" "$VOLUME_ROOT") || { + fail_check "could not derive relative path: unix_path=$UNIX_PATH volume_root=$VOLUME_ROOT" + RELATIVE_PATH="" + } +fi + +if [ -n "$RELATIVE_PATH" ]; then + RECYCLE_PATH="$VOLUME_ROOT/$RECYCLE_REPOSITORY/$DELETED_BY/$RELATIVE_PATH" + METADATA_PATH="$VOLUME_ROOT/$SALVAGE_REPOSITORY/$DELETED_BY/$RELATIVE_PATH.json" + + emit "volume_root=$VOLUME_ROOT" + emit "relative_path=$RELATIVE_PATH" + emit "recycle_path=$RECYCLE_PATH" + emit "metadata_path=$METADATA_PATH" + + check_file "recycle payload" "$RECYCLE_PATH" + check_file "salvage metadata sidecar" "$METADATA_PATH" + if [ -f "$METADATA_PATH" ]; then + check_metadata "$METADATA_PATH" + fi +fi + +finish_report +[ "$FAILURES" -eq 0 ]