NCP 87/17 Recover Salvageable File
All checks were successful
Source release / source-package (push) Successful in 58s

This commit is contained in:
Mario Fetka
2026-05-31 19:39:29 +02:00
parent b39976c239
commit 23be038087
8 changed files with 680 additions and 0 deletions

View File

@@ -108,6 +108,8 @@ typedef struct {
extern int handle_func_0x57(uint8 *p, uint8 *responsedata, int task);
extern int handle_func_0x57_salvage_scan(uint8 *p, int request_len,
uint8 *responsedata, int task);
extern int handle_func_0x57_salvage_recover(uint8 *p, int request_len,
uint8 *responsedata, int task);
extern int handle_func_0x56(uint8 *p, uint8 *responsedata, int task);
extern int fill_namespace_buffer(int volume, uint8 *rdata);

View File

@@ -200,6 +200,8 @@ int nwsalvage_read_metadata(const char *metadata_path,
int nwsalvage_scan_directory(int volume, const char *unix_directory,
unsigned long scan_sequence,
struct nwsalvage_scan_result *result);
int nwsalvage_recover_scan_sequence(int volume, unsigned long scan_sequence,
unsigned long directory_base, int task);
/*
* 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

View File

@@ -2839,6 +2839,59 @@ int handle_func_0x57_salvage_scan(uint8 *q, int request_len,
return(result);
}
int handle_func_0x57_salvage_recover(uint8 *q, int request_len,
uint8 *responsedata, int task)
{
int namespace;
uint32 scan_sequence;
uint32 volume;
uint32 directory_base;
int result;
(void)namespace;
(void)responsedata;
/*
* ncpfs ncp_ns_salvage_file() packs 87/17 as:
* byte subfunction 0x11
* byte namespace
* byte reserved
* dword scan sequence / deleted file id
* dword volume id
* dword directory/base id
* pstr new filename
* mars_nwe recovers to the original_path from the trusted JSON sidecar; the
* client supplied new filename is intentionally not used for the first pass.
*/
if (!q || request_len < 16 || q[0] != 0x11)
return(-0xfb);
namespace = (int)q[1];
scan_sequence = GET_32(q + 3);
volume = GET_32(q + 7);
directory_base = GET_32(q + 11);
if (volume >= (uint32)used_nw_volumes)
return(-0x98);
result = nwsalvage_recover_scan_sequence((int)volume, scan_sequence,
directory_base, task);
if (result < 0) {
if (errno == EEXIST)
return(-0x92);
return(-0x98);
}
if (result == 0)
return(-0xff);
XDPRINTF((3, 0,
"NCP 87/17 salvage recover ns=%d seq=0x%lx vol=%lu base=0x%lx result=%d",
namespace, (unsigned long)scan_sequence, (unsigned long)volume,
(unsigned long)directory_base, result));
return(0);
}
int handle_func_0x57(uint8 *p, uint8 *responsedata, int task)
{
int result = -0xfb; /* unknown request */

View File

@@ -5088,6 +5088,11 @@ static int handle_ncp_serv(void)
break;
case 0x11: /* 87/17 Recover Salvageable File */
result = handle_func_0x57_salvage_recover(requestdata, requestlen,
responsedata,
ncprequest->task);
break;
case 0x12: /* 87/18 Purge Salvageable File */
result = -0xfb;
break;

View File

@@ -6,6 +6,7 @@
#include "nwarchive.h"
#include "nwatalk.h"
#include "nwattrib.h"
#include "nwfile.h"
#include "trustee.h"
#include "tools.h"
#include "connect.h"
@@ -1994,6 +1995,427 @@ int nwsalvage_scan_directory(int volume, const char *unix_directory,
return(found);
}
static int nwsalvage_original_path_to_unix(int volume,
const char *original_path,
char *unixname,
size_t unixname_len)
{
char volume_root[NWSALVAGE_PATH_MAX];
char volume_name[NWSALVAGE_REPOSITORY_NAME_MAX];
const char *colon;
const char *rel;
size_t volume_name_len;
if (!original_path || !*original_path || !unixname || !unixname_len) {
errno = EINVAL;
return(-1);
}
if (nw_get_volume_name(volume, (uint8 *)volume_name,
sizeof(volume_name)) < 1 ||
nwsalvage_copy_volume_root(volume, volume_root, sizeof(volume_root)) < 0)
return(-1);
colon = strchr(original_path, ':');
if (!colon) {
errno = EINVAL;
return(-1);
}
volume_name_len = strlen(volume_name);
if ((size_t)(colon - original_path) != volume_name_len ||
strncasecmp(original_path, volume_name, volume_name_len) != 0) {
errno = EINVAL;
return(-1);
}
rel = colon + 1;
while (*rel == '/' || *rel == '\\')
rel++;
if (!*rel || !nwsalvage_relative_path_valid(rel)) {
errno = EINVAL;
return(-1);
}
return(nwsalvage_path_join(unixname, unixname_len, volume_root, rel));
}
static int nwsalvage_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 nwsalvage_hex_to_bytes(const char *hex, uint8 *out, size_t out_len)
{
size_t i;
if (!hex || !out || strlen(hex) != out_len * 2) {
errno = EINVAL;
return(-1);
}
for (i = 0; i < out_len; i++) {
int hi = nwsalvage_hex_nibble((unsigned char)hex[i * 2]);
int lo = nwsalvage_hex_nibble((unsigned char)hex[i * 2 + 1]);
if (hi < 0 || lo < 0) {
errno = EINVAL;
return(-1);
}
out[i] = (uint8)((hi << 4) | lo);
}
return(0);
}
static int nwsalvage_finder_info_is_zero(const uint8 *finder_info, size_t len)
{
size_t i;
for (i = 0; i < len; i++)
if (finder_info[i])
return(0);
return(1);
}
static int nwsalvage_restore_metadata(int volume, const char *unixname,
const struct nwsalvage_metadata_entry *entry)
{
struct stat stb;
struct utimbuf times;
int i;
if (!unixname || !entry) {
errno = EINVAL;
return(-1);
}
if (stat(unixname, &stb) < 0)
return(-1);
(void)set_nw_attrib_dword(volume, (char *)unixname, &stb,
(uint32)entry->attributes);
(void)mars_nwe_set_archive_info((char *)unixname,
!!(entry->netware_archive_flags & MARS_NWE_ARCHIVE_HAS_DATE),
(uint16)entry->netware_archive_date,
!!(entry->netware_archive_flags & MARS_NWE_ARCHIVE_HAS_TIME),
(uint16)entry->netware_archive_time,
!!(entry->netware_archive_flags & MARS_NWE_ARCHIVE_HAS_ARCHIVER),
(uint32)entry->netware_archiver_id);
(void)mars_nwe_set_file_info((char *)unixname,
!!(entry->netware_fileinfo_flags & MARS_NWE_FILEINFO_HAS_CREATE_DATE),
(uint16)entry->netware_create_date,
!!(entry->netware_fileinfo_flags & MARS_NWE_FILEINFO_HAS_CREATE_TIME),
(uint16)entry->netware_create_time,
!!(entry->netware_fileinfo_flags & MARS_NWE_FILEINFO_HAS_CREATOR),
(uint32)entry->netware_creator_id);
(void)mars_nwe_set_file_modifier_info((char *)unixname,
!!(entry->netware_fileinfo_flags & MARS_NWE_FILEINFO_HAS_MODIFIER),
(uint32)entry->netware_modifier_id);
if (entry->finder_info_hex[0]) {
uint8 finder_info[NWATALK_FINDER_INFO_LEN];
if (nwsalvage_hex_to_bytes(entry->finder_info_hex, finder_info,
sizeof(finder_info)) == 0 &&
!nwsalvage_finder_info_is_zero(finder_info, sizeof(finder_info)))
(void)nwatalk_set_finder_info(unixname, finder_info, sizeof(finder_info));
}
if (entry->afp_attributes)
(void)nwatalk_set_afp_attributes(unixname, (uint16)entry->afp_attributes);
if (entry->afp_entry_id[0]) {
char *end = NULL;
unsigned long entry_id = strtoul(entry->afp_entry_id, &end, 0);
if (end && *end == '\0')
(void)nwatalk_set_entry_id(unixname, (uint32)entry_id);
}
(void)tru_set_inherited_mask(volume, (uint8 *)unixname, &stb,
(int)entry->inherited_rights_mask);
if (entry->trustee_count) {
NW_OIC nwoic[NWSALVAGE_TRUSTEE_MAX];
int count = (int)entry->trustee_count;
if (count > NWSALVAGE_TRUSTEE_MAX)
count = NWSALVAGE_TRUSTEE_MAX;
memset(nwoic, 0, sizeof(nwoic));
for (i = 0; i < count; i++) {
nwoic[i].id = (uint32)entry->trustees[i].object_id;
nwoic[i].trustee = (int)entry->trustees[i].rights;
}
(void)tru_add_trustee_set(volume, (uint8 *)unixname, &stb, count, nwoic);
}
if (entry->mode) {
if (seteuid(0)) {}
(void)chmod(unixname, (mode_t)(entry->mode & 07777));
(void)reseteuid();
}
times.actime = (time_t)entry->atime;
times.modtime = (time_t)entry->mtime;
if (times.actime || times.modtime)
(void)utime(unixname, &times);
return(0);
}
static int nwsalvage_copy_payload_with_novell_io(int volume,
const char *source_unixname,
const char *dest_unixname,
const struct stat *source_stb,
int task)
{
struct stat src_open_stb;
struct stat dst_open_stb;
int source_handle = -1;
int dest_handle = -1;
unsigned long long remaining;
uint32 offset = 0;
int result = 0;
if (!source_unixname || !dest_unixname || !source_stb) {
errno = EINVAL;
return(-1);
}
source_handle = file_creat_open(volume, (uint8 *)source_unixname,
&src_open_stb, 0, 1, 8, task);
if (source_handle < 0)
return(source_handle);
dest_handle = file_creat_open(volume, (uint8 *)dest_unixname,
&dst_open_stb, FILE_ATTR_NORMAL, 3, 2 | 8,
task);
if (dest_handle < 0) {
(void)nw_close_file(source_handle, 0, task);
return(dest_handle);
}
remaining = (unsigned long long)source_stb->st_size;
while (remaining) {
uint32 chunk;
int copied;
if (offset == 0xffffffffUL) {
result = -0xfe;
break;
}
chunk = (remaining > 0x7fffffffUL) ? 0x7fffffffUL : (uint32)remaining;
copied = nw_server_copy(source_handle, offset, dest_handle, offset, chunk);
if (copied < 0) {
result = copied;
break;
}
if ((uint32)copied != chunk) {
result = -0xff;
break;
}
remaining -= chunk;
offset += chunk;
}
if (!result)
result = nw_commit_file(dest_handle);
(void)nw_close_file(dest_handle, 0, task);
(void)nw_close_file(source_handle, 0, task);
return(result);
}
static int nwsalvage_metadata_volume_matches(const struct nwsalvage_metadata_entry *entry,
const char *volume_name)
{
const char *colon;
if (!entry || !volume_name)
return(0);
colon = strchr(entry->original_path, ':');
if (!colon)
return(0);
return((size_t)(colon - entry->original_path) == strlen(volume_name) &&
strncasecmp(entry->original_path, volume_name,
(size_t)(colon - entry->original_path)) == 0);
}
static int nwsalvage_find_by_base_in_tree(const char *metadata_dir,
const char *volume_root,
const char *volume_name,
unsigned long directory_base,
unsigned long scan_sequence,
unsigned long *match_index,
struct nwsalvage_scan_result *result)
{
DIR *dir;
struct dirent *de;
int found = 0;
dir = opendir(metadata_dir);
if (!dir)
return(errno == ENOENT ? 0 : -1);
while (!found && (de = readdir(dir)) != NULL) {
char child[NWSALVAGE_PATH_MAX];
struct stat st;
size_t len;
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)
continue;
if (S_ISDIR(st.st_mode)) {
found = nwsalvage_find_by_base_in_tree(child, volume_root, volume_name,
directory_base, scan_sequence,
match_index, result);
if (found < 0)
break;
continue;
}
if (!S_ISREG(st.st_mode))
continue;
len = strlen(de->d_name);
if (len < 6 || strcmp(de->d_name + len - 5, ".json") != 0)
continue;
memset(&result->metadata, 0, sizeof(result->metadata));
if (nwsalvage_read_metadata(child, &result->metadata) < 0)
continue;
if (!nwsalvage_metadata_volume_matches(&result->metadata, volume_name))
continue;
if (result->metadata.original_parent_entry_id != directory_base)
continue;
if (*match_index < scan_sequence) {
(*match_index)++;
continue;
}
if (*match_index != scan_sequence)
continue;
if (nwsalvage_path_join(result->recycle_path, sizeof(result->recycle_path),
volume_root, result->metadata.recycle_relative_path) < 0)
continue;
result->scan_sequence = *match_index;
result->scan_volume = (unsigned long)atoi(volume_name); /* overwritten below */
result->scan_directory_base = directory_base;
strmaxcpy(result->metadata_path, child, sizeof(result->metadata_path) - 1);
found = 1;
}
closedir(dir);
return(found);
}
static int nwsalvage_find_by_base(int volume, unsigned long scan_sequence,
unsigned long directory_base,
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];
unsigned long match_index = 0;
int found;
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);
memset(result, 0, sizeof(*result));
found = nwsalvage_find_by_base_in_tree(metadata_root, volume_root, volume_name,
directory_base, scan_sequence,
&match_index, result);
if (found > 0)
result->scan_volume = (unsigned long)volume;
return(found);
}
int nwsalvage_recover_scan_sequence(int volume, unsigned long scan_sequence,
unsigned long directory_base, int task)
{
struct nwsalvage_scan_result scan;
char dest_unixname[NWSALVAGE_PATH_MAX];
struct stat source_stb;
int result;
memset(&scan, 0, sizeof(scan));
result = nwsalvage_find_by_base(volume, scan_sequence, directory_base, &scan);
if (result <= 0)
return(result);
if (nwsalvage_original_path_to_unix(volume, scan.metadata.original_path,
dest_unixname, sizeof(dest_unixname)) < 0)
return(-1);
if (access(dest_unixname, F_OK) == 0) {
errno = EEXIST;
return(-1);
}
if (errno != ENOENT)
return(-1);
if (stat(scan.recycle_path, &source_stb) < 0)
return(-1);
if (!S_ISREG(source_stb.st_mode)) {
errno = EINVAL;
return(-1);
}
if (make_parent_dirs(dest_unixname) < 0)
return(-1);
result = nwsalvage_copy_payload_with_novell_io(volume, scan.recycle_path,
dest_unixname, &source_stb,
task);
if (result < 0) {
unlink(dest_unixname);
errno = EIO;
return(-1);
}
if (nwsalvage_restore_metadata(volume, dest_unixname, &scan.metadata) < 0) {
/* The payload is already safely restored through mars_nwe I/O. Keep it. */
}
if (unlink(scan.recycle_path) < 0)
return(-1);
if (unlink(scan.metadata_path) < 0)
return(-1);
return(1);
}
static void nwsalvage_fill_trustees(int volume, const char *unixname,
const struct stat *stb,
struct nwsalvage_deleted_entry *entry)

View File

@@ -66,6 +66,10 @@ if(SALVAGE_NCPFS_INCLUDE_DIR AND SALVAGE_NCPFS_LIBRARY)
add_executable(ncp_salvage_scan_smoke ncp_salvage_scan_smoke.c)
target_include_directories(ncp_salvage_scan_smoke PRIVATE ${SALVAGE_NCPFS_INCLUDE_DIR})
target_link_libraries(ncp_salvage_scan_smoke ${SALVAGE_NCPFS_LIBRARY})
add_executable(ncp_salvage_recover_smoke ncp_salvage_recover_smoke.c)
target_include_directories(ncp_salvage_recover_smoke PRIVATE ${SALVAGE_NCPFS_INCLUDE_DIR})
target_link_libraries(ncp_salvage_recover_smoke ${SALVAGE_NCPFS_LIBRARY})
else()
message(STATUS
"Skipping salvage NCP smoke helpers: ncpfs/libncp headers or library not found"

View File

@@ -0,0 +1,132 @@
/*
* Linux smoke helper for NCP 87/17 Recover Salvageable File.
*
* The helper scans a directory with the official ncplib salvage scan API, then
* recovers the Nth matching deleted original name using ncp_ns_salvage_file().
* It intentionally selects by the returned scan tuple (seq/vol/base), not by
* backend .recycle version names such as "Copy #1 of ...".
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <ncp/nwcalls.h>
#include <ncp/ncplib.h>
#ifndef NCP_PATH_STD
#define NCP_PATH_STD 0
#endif
/* Older distro headers may miss the prototype although patched libncp has it. */
long ncp_ns_salvage_file(NWCONN_HANDLE conn, u_int8_t src_ns,
const struct ncp_deleted_file *finfo, const char *newfname);
static void usage(const char *prog)
{
fprintf(stderr,
"Usage: %s [ncpfs options] DIRECTORY ORIGINAL_NAME [MATCH_INDEX]\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 SLVGCHK.TXT 0\n",
prog, prog);
}
int main(int argc, char **argv)
{
NWCONN_HANDLE conn;
long init_err = 0;
const char *path = NULL;
const char *wanted_name = NULL;
int wanted_index = 0;
int matched_index = 0;
struct ncp_deleted_file info;
struct ncp_deleted_file selected;
char name[512];
char selected_name[512];
long err;
int selected_valid = 0;
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], "-h") || !strcmp(argv[i], "--help")) {
usage(argv[0]);
ncp_close(conn);
return 0;
} else if (!path) {
path = argv[i];
} else if (!wanted_name) {
wanted_name = argv[i];
} else {
wanted_index = atoi(argv[i]);
}
}
if (!path || !wanted_name) {
usage(argv[0]);
ncp_close(conn);
return 2;
}
memset(&info, 0, sizeof(info));
memset(&selected, 0, sizeof(selected));
selected_name[0] = '\0';
info.seq = -1;
while ((err = ncp_ns_scan_salvageable_file(conn, NW_NS_DOS,
NCP_DIRSTYLE_NOHANDLE, 0, 0, (const unsigned char *)path,
NCP_PATH_STD, &info, name, sizeof(name))) == 0) {
printf("NCP salvage recover candidate seq=%d vol=%u base=%u name=%s\n",
(int)info.seq, (unsigned int)info.vol,
(unsigned int)info.base, name);
if (!strcmp(name, wanted_name)) {
if (matched_index == wanted_index) {
selected = info;
snprintf(selected_name, sizeof(selected_name), "%s", name);
selected_valid = 1;
break;
}
matched_index++;
}
}
if (!selected_valid) {
fprintf(stderr,
"no matching salvage entry: path=%s name=%s index=%d last_error=0x%04x\n",
path, wanted_name, wanted_index, (unsigned int)err);
ncp_close(conn);
return 1;
}
err = ncp_ns_salvage_file(conn, NW_NS_DOS, &selected, selected_name);
if (err) {
fprintf(stderr,
"NCP salvage recover failed: seq=%d vol=%u base=%u name=%s error=0x%04x\n",
(int)selected.seq, (unsigned int)selected.vol,
(unsigned int)selected.base, selected_name, (unsigned int)err);
ncp_close(conn);
return 1;
}
printf("NCP salvage recover ok seq=%d vol=%u base=%u name=%s\n",
(int)selected.seq, (unsigned int)selected.vol,
(unsigned int)selected.base, selected_name);
ncp_close(conn);
return 0;
}

View File

@@ -265,6 +265,65 @@ cat_metadata() {
grep -q '"trustees"' "$path" || fail_check "metadata missing trustees"
}
run_ncp_salvage_recover() {
local status before_scan_tmp after_scan_tmp
before_scan_tmp=$(mktemp "${TMPDIR:-/tmp}/mars-salvage-recover-before.XXXXXX") || {
fail_check "could not allocate recover pre-scan temp file"
return 1
}
after_scan_tmp=$(mktemp "${TMPDIR:-/tmp}/mars-salvage-recover-after.XXXXXX") || {
rm -f "$before_scan_tmp"
fail_check "could not allocate recover post-scan temp file"
return 1
}
section "NCP salvage recover 87/17"
emit "recover_path=$SCAN_PATH"
emit "recover_name=$BASENAME"
emit "\$ ./ncp_salvage_recover_smoke -S '$SERVER' -U '$USER_NAME' -P ****** '$SCAN_PATH' '$BASENAME' 0"
"$SCRIPT_DIR/ncp_salvage_recover_smoke" -S "$SERVER" -U "$USER_NAME" \
-P "$PASSWORD" "$SCAN_PATH" "$BASENAME" 0 2>&1 | tee "$before_scan_tmp" | tee -a "$REPORT_TMP"
status=${PIPESTATUS[0]}
emit "[exit=$status]"
if [ "$status" -ne 0 ]; then
fail_check "NCP salvage recover failed for $SCAN_PATH/$BASENAME"
rm -f "$before_scan_tmp" "$after_scan_tmp"
return 1
fi
check_file "recover smoke: restored live payload" "$UNIX_PATH"
if [ -e "$FIRST_RECYCLE" ]; then
fail_check "recover smoke: first recycle payload still exists: $FIRST_RECYCLE"
else
emit "recover smoke: first recycle payload consumed: $FIRST_RECYCLE"
fi
if [ -e "$FIRST_META" ]; then
fail_check "recover smoke: first metadata sidecar still exists: $FIRST_META"
else
emit "recover smoke: first metadata sidecar consumed: $FIRST_META"
fi
section "NCP salvage scan after recover"
"$SCRIPT_DIR/ncp_salvage_scan_smoke" -S "$SERVER" -U "$USER_NAME" \
-P "$PASSWORD" "$SCAN_PATH" 2>&1 | tee "$after_scan_tmp" | tee -a "$REPORT_TMP"
status=${PIPESTATUS[0]}
emit "[exit=$status]"
if [ "$status" -ne 0 ]; then
fail_check "NCP salvage scan after recover failed for $SCAN_PATH"
else
local basename_count
basename_count=$(grep -F "name=$BASENAME" "$after_scan_tmp" | wc -l)
if [ "$basename_count" -lt 1 ]; then
fail_check "NCP salvage scan after recover expected remaining history entry named $BASENAME"
else
emit "NCP salvage scan after recover remaining $BASENAME entries=$basename_count"
fi
fi
rm -f "$before_scan_tmp" "$after_scan_tmp"
}
run_ncp_salvage_scan() {
local status scan_tmp
scan_tmp=$(mktemp "${TMPDIR:-/tmp}/mars-salvage-scan.XXXXXX") || {
@@ -337,6 +396,7 @@ if [ -f "$SECOND_META" ]; then
fi
run_ncp_salvage_scan
run_ncp_salvage_recover
finish_report
[ "$FAILURES" -eq 0 ]