salvage: hook delete path through nwsalvage
All checks were successful
Source release / source-package (push) Successful in 55s

This commit is contained in:
ChatGPT
2026-05-31 11:35:17 +00:00
committed by Mario Fetka
parent 5fc5a5218f
commit 3fb45fd624
7 changed files with 1017 additions and 5 deletions

View File

@@ -3,6 +3,7 @@
#include <stddef.h>
#include <sys/types.h>
#include <sys/stat.h>
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

View File

@@ -24,6 +24,7 @@
#include "net.h"
#include "unxfile.h"
#include "nwsalvage.h"
#include <dirent.h>
#include <utime.h>
@@ -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);

View File

@@ -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 <ctype.h>
#include <errno.h>
#include <limits.h>
@@ -12,6 +21,7 @@
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
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
}

View File

@@ -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
)

View File

@@ -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`.

View File

@@ -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 <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ncp/nwcalls.h>
#include <ncp/ncplib.h>
#include <ncp/kernel/ncp.h>
#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;
}

View File

@@ -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 <<USAGE
Usage: $0 -S SERVER -U USER -P PASSWORD [options]
Options:
--path NETWARE_PATH NetWare file path to create/delete (default: $NETWARE_PATH)
--unix-path UNIX_PATH Unix path corresponding to --path (default: $UNIX_PATH)
--deleted-by NAME Expected directory below .recycle/.salvage (default: USER)
--recycle-name NAME Recycle repository name (default: $RECYCLE_REPOSITORY)
--salvage-name NAME Salvage metadata repository name (default: $SALVAGE_REPOSITORY)
--out FILE Write the complete report to FILE as well as stdout
--stop-on-failure Stop after the first failing check
-h, --help Show this help
The script uses tests/salvage/ncp_delete_smoke to create and delete the file
through classic NCP requests. It never removes the live file through local Unix
unlink/rm; local filesystem access is only used after the NCP delete to inspect
.recycle and .salvage.
USAGE
}
while [ $# -gt 0 ]; do
case "$1" in
-S|--server)
SERVER=$2; shift 2 ;;
-U|--user)
USER_NAME=$2; shift 2 ;;
-P|--password)
PASSWORD=$2; shift 2 ;;
--path)
NETWARE_PATH=$2; shift 2 ;;
--unix-path)
UNIX_PATH=$2; shift 2 ;;
--deleted-by)
DELETED_BY=$2; shift 2 ;;
--recycle-name)
RECYCLE_REPOSITORY=$2; shift 2 ;;
--salvage-name)
SALVAGE_REPOSITORY=$2; shift 2 ;;
--out)
OUT_FILE=$2; shift 2 ;;
--stop-on-failure)
KEEP_GOING=0; shift ;;
-h|--help)
usage; exit 0 ;;
*)
echo "Unknown argument: $1" >&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 ]