diff --git a/include/nwsalvage.h b/include/nwsalvage.h index e78783e..590cd8a 100644 --- a/include/nwsalvage.h +++ b/include/nwsalvage.h @@ -11,6 +11,10 @@ typedef int (*nwsalvage_ini_getter)(int entry, char *str, #define NWSALVAGE_ENABLE_INI_SECTION 51 #define NWSALVAGE_REPOSITORY_INI_SECTION 52 #define NWSALVAGE_FLAGS_INI_SECTION 53 +#define NWSALVAGE_SIZE_LIMIT_INI_SECTION 55 +#define NWSALVAGE_EXCLUDE_INI_SECTION 56 +#define NWSALVAGE_EXCLUDE_DIR_INI_SECTION 57 +#define NWSALVAGE_NOVERSIONS_INI_SECTION 58 #define NWSALVAGE_DEFAULT_ENABLED 1 #define NWSALVAGE_DEFAULT_RECYCLE_NAME ".recycle" @@ -29,12 +33,22 @@ typedef int (*nwsalvage_ini_getter)(int entry, char *str, #define NWSALVAGE_FINDER_INFO_HEX_LEN 64 #define NWSALVAGE_AFP_ENTRY_ID_MAX 32 #define NWSALVAGE_TRUSTEE_MAX 100 +#define NWSALVAGE_PATTERN_MAX 32 +#define NWSALVAGE_PATTERN_LEN_MAX 128 struct nwsalvage_config { int enabled; char recycle_repository[NWSALVAGE_REPOSITORY_NAME_MAX]; char metadata_repository[NWSALVAGE_REPOSITORY_NAME_MAX]; unsigned int flags; + unsigned long long min_size; + unsigned long long max_size; + unsigned int exclude_count; + char exclude_patterns[NWSALVAGE_PATTERN_MAX][NWSALVAGE_PATTERN_LEN_MAX]; + unsigned int exclude_dir_count; + char exclude_dir_patterns[NWSALVAGE_PATTERN_MAX][NWSALVAGE_PATTERN_LEN_MAX]; + unsigned int noversions_count; + char noversions_patterns[NWSALVAGE_PATTERN_MAX][NWSALVAGE_PATTERN_LEN_MAX]; }; struct nwsalvage_trustee_entry { @@ -134,6 +148,14 @@ int nwsalvage_config_parse_repositories(struct nwsalvage_config *config, const char *line); int nwsalvage_config_parse_flags(struct nwsalvage_config *config, const char *line); +int nwsalvage_config_parse_size_limits(struct nwsalvage_config *config, + const char *line); +int nwsalvage_config_parse_exclude(struct nwsalvage_config *config, + const char *line); +int nwsalvage_config_parse_exclude_dir(struct nwsalvage_config *config, + const char *line); +int nwsalvage_config_parse_noversions(struct nwsalvage_config *config, + const char *line); int nwsalvage_config_load_from_ini(struct nwsalvage_config *config, nwsalvage_ini_getter getter, void *data); diff --git a/opt/nw.ini.hook.cmake b/opt/nw.ini.hook.cmake index d946e89..3b16922 100644 --- a/opt/nw.ini.hook.cmake +++ b/opt/nw.ini.hook.cmake @@ -999,13 +999,17 @@ # 54 = recycle directory modes, reserved for Samba-compatible directory_mode # and subdir_mode handling # -# 55 = recycle size limits, reserved for Samba-compatible minsize/maxsize +# 55 = recycle size limits: minsize maxsize. Values accept bytes or +# case-insensitive k/kb, m/mb, g/gb suffixes. 0 means unlimited. # -# 56 = recycle exclude patterns, reserved for Samba-compatible exclude +# 56 = recycle exclude patterns. Matching files are deleted normally and +# are not moved to recycle/salvage. Use '-' for no patterns. # -# 57 = recycle exclude directories, reserved for Samba-compatible exclude_dir +# 57 = recycle exclude directories. Files below matching directories are +# deleted normally. Use '-' for no patterns. # -# 58 = recycle no-version patterns, reserved for Samba-compatible noversions +# 58 = recycle no-version patterns. Matching files are still recycled, but +# older recycled copies are replaced instead of using Copy #x names. # # 59 = reserved for future salvage cleanup/history policy # @@ -1023,6 +1027,10 @@ 51 1 52 .recycle .salvage 53 kv +55 0 0 +56 - +57 - +58 - # <<< SMARTHOOK SECTION 51-59 ACTIVE END # <<< SMARTHOOK SECTION 51-59 END # ========================================================================= diff --git a/src/nwsalvage.c b/src/nwsalvage.c index 2804a7d..b0ed112 100644 --- a/src/nwsalvage.c +++ b/src/nwsalvage.c @@ -12,15 +12,22 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include #include +#include + +#ifndef FNM_CASEFOLD +#define FNM_CASEFOLD 0 +#endif int nwsalvage_repository_name_valid(const char *name) { @@ -232,6 +239,168 @@ int nwsalvage_config_parse_flags(struct nwsalvage_config *config, } + +static int nwsalvage_parse_size_value(const char *token, + unsigned long long *out) +{ + char *end; + unsigned long long value; + unsigned long long multiplier = 1; + + if (!token || !*token || !out) { + errno = EINVAL; + return(-1); + } + + errno = 0; + value = strtoull(token, &end, 10); + if (end == token || errno == ERANGE) + return(-1); + + if (*end) { + char unit[8]; + size_t len = strlen(end); + size_t i; + + if (len >= sizeof(unit)) { + errno = EINVAL; + return(-1); + } + for (i = 0; i <= len; i++) + unit[i] = (char)tolower((unsigned char)end[i]); + + if (!strcmp(unit, "b") || !strcmp(unit, "byte") || !strcmp(unit, "bytes")) + multiplier = 1; + else if (!strcmp(unit, "k") || !strcmp(unit, "kb")) + multiplier = 1024ULL; + else if (!strcmp(unit, "m") || !strcmp(unit, "mb")) + multiplier = 1024ULL * 1024ULL; + else if (!strcmp(unit, "g") || !strcmp(unit, "gb")) + multiplier = 1024ULL * 1024ULL * 1024ULL; + else { + errno = EINVAL; + return(-1); + } + } + + if (value && multiplier > ULLONG_MAX / value) { + errno = ERANGE; + return(-1); + } + *out = value * multiplier; + return(0); +} + +int nwsalvage_config_parse_size_limits(struct nwsalvage_config *config, + const char *line) +{ + const char *p = line; + char min_token[64]; + char max_token[64]; + unsigned long long min_size; + unsigned long long max_size; + + if (!config || !line) { + errno = EINVAL; + return(-1); + } + + if (parse_token(&p, min_token, sizeof(min_token)) < 0 || + parse_token(&p, max_token, sizeof(max_token)) < 0) + return(-1); + while (*p && isspace((unsigned char)*p)) p++; + if (*p) { + errno = EINVAL; + return(-1); + } + + if (nwsalvage_parse_size_value(min_token, &min_size) < 0 || + nwsalvage_parse_size_value(max_token, &max_size) < 0) + return(-1); + if (max_size && min_size > max_size) { + errno = EINVAL; + return(-1); + } + + config->min_size = min_size; + config->max_size = max_size; + return(0); +} + +static int nwsalvage_config_parse_patterns(char patterns[][NWSALVAGE_PATTERN_LEN_MAX], + unsigned int *count, + const char *line) +{ + const char *p = line; + char token[NWSALVAGE_PATTERN_LEN_MAX]; + unsigned int used = 0; + + if (!patterns || !count || !line) { + errno = EINVAL; + return(-1); + } + + while (*p && isspace((unsigned char)*p)) p++; + if (!*p || *p == '-') { + if (*p == '-') { + p++; + while (*p && isspace((unsigned char)*p)) p++; + if (*p) { + errno = EINVAL; + return(-1); + } + } + *count = 0; + return(0); + } + + while (*p) { + if (used >= NWSALVAGE_PATTERN_MAX) { + errno = E2BIG; + return(-1); + } + if (parse_token(&p, token, sizeof(token)) < 0) + return(-1); + strcpy(patterns[used++], token); + while (*p && isspace((unsigned char)*p)) p++; + } + *count = used; + return(0); +} + +int nwsalvage_config_parse_exclude(struct nwsalvage_config *config, + const char *line) +{ + if (!config) { + errno = EINVAL; + return(-1); + } + return(nwsalvage_config_parse_patterns(config->exclude_patterns, + &config->exclude_count, line)); +} + +int nwsalvage_config_parse_exclude_dir(struct nwsalvage_config *config, + const char *line) +{ + if (!config) { + errno = EINVAL; + return(-1); + } + return(nwsalvage_config_parse_patterns(config->exclude_dir_patterns, + &config->exclude_dir_count, line)); +} + +int nwsalvage_config_parse_noversions(struct nwsalvage_config *config, + const char *line) +{ + if (!config) { + errno = EINVAL; + return(-1); + } + return(nwsalvage_config_parse_patterns(config->noversions_patterns, + &config->noversions_count, line)); +} + int nwsalvage_config_load_from_ini(struct nwsalvage_config *config, nwsalvage_ini_getter getter, void *data) @@ -264,6 +433,30 @@ int nwsalvage_config_load_from_ini(struct nwsalvage_config *config, return(-1); } + if (getter(NWSALVAGE_SIZE_LIMIT_INI_SECTION, + line, sizeof(line), data)) { + if (nwsalvage_config_parse_size_limits(config, line) < 0) + return(-1); + } + + if (getter(NWSALVAGE_EXCLUDE_INI_SECTION, + line, sizeof(line), data)) { + if (nwsalvage_config_parse_exclude(config, line) < 0) + return(-1); + } + + if (getter(NWSALVAGE_EXCLUDE_DIR_INI_SECTION, + line, sizeof(line), data)) { + if (nwsalvage_config_parse_exclude_dir(config, line) < 0) + return(-1); + } + + if (getter(NWSALVAGE_NOVERSIONS_INI_SECTION, + line, sizeof(line), data)) { + if (nwsalvage_config_parse_noversions(config, line) < 0) + return(-1); + } + return(0); } @@ -513,8 +706,7 @@ static const char *metadata_finder_info_hex( const struct nwsalvage_deleted_entry *entry) { return(entry->finder_info_hex && *entry->finder_info_hex ? - entry->finder_info_hex : - "0000000000000000000000000000000000000000000000000000000000000000"); + entry->finder_info_hex : NULL); } static int validate_hex_string(const char *value, size_t expected_len) @@ -775,7 +967,8 @@ int nwsalvage_write_metadata(const char *metadata_path, } finder_info_hex = metadata_finder_info_hex(entry); - if (validate_hex_string(finder_info_hex, NWSALVAGE_FINDER_INFO_HEX_LEN) < 0) + if (finder_info_hex && + validate_hex_string(finder_info_hex, NWSALVAGE_FINDER_INFO_HEX_LEN) < 0) return(-1); if (!metadata_time_valid(entry->deleted_at) || !metadata_time_valid(entry->atime) || @@ -838,7 +1031,8 @@ int nwsalvage_write_metadata(const char *metadata_path, if (!failed && json_add_long(doc, object, "ctime", entry->ctime) < 0) failed = 1; - if (!failed && json_add_string(doc, object, "finder_info_hex", finder_info_hex) < 0) + if (!failed && finder_info_hex && + json_add_string(doc, object, "finder_info_hex", finder_info_hex) < 0) failed = 1; if (!failed && json_add_string(doc, object, "afp_entry_id", entry->afp_entry_id ? entry->afp_entry_id : "") < 0) @@ -990,13 +1184,18 @@ int nwsalvage_read_metadata(const char *metadata_path, if (!failed && json_get_long(object, "ctime", &entry->ctime) < 0) failed = 1; - if (!failed && json_get_string_copy(object, "finder_info_hex", - entry->finder_info_hex, - sizeof(entry->finder_info_hex)) < 0) - failed = 1; - if (!failed && validate_hex_string(entry->finder_info_hex, - NWSALVAGE_FINDER_INFO_HEX_LEN) < 0) - failed = 1; + { + yyjson_val *finder = yyjson_obj_get(object, "finder_info_hex"); + if (finder) { + if (!yyjson_is_str(finder) || + strmaxcpy((uint8 *)entry->finder_info_hex, + yyjson_get_str(finder), + sizeof(entry->finder_info_hex) - 1) < 0 || + validate_hex_string(entry->finder_info_hex, + NWSALVAGE_FINDER_INFO_HEX_LEN) < 0) + failed = 1; + } + } if (!failed && json_get_string_copy(object, "afp_entry_id", entry->afp_entry_id, sizeof(entry->afp_entry_id)) < 0) @@ -1065,9 +1264,14 @@ int nwsalvage_read_metadata(const char *metadata_path, return(0); } +static int nwsalvage_touch_recycled(const char *path, + const struct nwsalvage_config *config, + const struct nwsalvage_deleted_entry *entry); + static int nwsalvage_move_deleted_file(const char *live_path, const char *recycle_path, const char *metadata_path, + const struct nwsalvage_config *config, const struct nwsalvage_deleted_entry *entry) { int saved_errno; @@ -1075,7 +1279,7 @@ static int nwsalvage_move_deleted_file(const char *live_path, if (!live_path || !*live_path || !recycle_path || !*recycle_path || !metadata_path || !*metadata_path || - !entry) { + !config || !entry) { errno = EINVAL; return(-1); } @@ -1106,6 +1310,13 @@ static int nwsalvage_move_deleted_file(const char *live_path, if (rename(live_path, recycle_path) < 0) return(-1); + if (nwsalvage_touch_recycled(recycle_path, config, entry) < 0) { + saved_errno = errno; + (void)rename(recycle_path, live_path); + errno = saved_errno; + return(-1); + } + if (nwsalvage_write_metadata(metadata_path, entry) == 0) return(0); @@ -1211,6 +1422,269 @@ static const char *nwsalvage_basename(const char *path) return(p ? p + 1 : path); } +static int nwsalvage_dirname_copy(const char *path, char *out, size_t out_len) +{ + const char *slash; + size_t len; + + if (!path || !out || !out_len) { + errno = EINVAL; + return(-1); + } + + slash = strrchr(path, '/'); + if (!slash) { + out[0] = '\0'; + return(0); + } + len = (size_t)(slash - path); + if (len >= out_len) { + errno = ENAMETOOLONG; + return(-1); + } + memcpy(out, path, len); + out[len] = '\0'; + return(0); +} + +static int nwsalvage_build_salvage_relative(char *out, size_t out_len, + const struct nwsalvage_config *config, + const char *deleted_by, + const char *relative_path) +{ + const char *payload_path; + + if (!out || !out_len || !config || !deleted_by || !*deleted_by || + !relative_path || !*relative_path) { + errno = EINVAL; + return(-1); + } + + payload_path = (config->flags & NWSALVAGE_FLAG_KEEP_TREE) ? + relative_path : nwsalvage_basename(relative_path); + + if (slprintf(out, (int)out_len - 1, "%s/%s", deleted_by, payload_path) < 0) { + errno = ENAMETOOLONG; + return(-1); + } + return(0); +} + +static int nwsalvage_build_copy_relative(char *out, size_t out_len, + const char *base_relative, + unsigned int copy_number) +{ + char dir[NWSALVAGE_PATH_MAX]; + const char *name; + + if (!out || !out_len || !base_relative || !*base_relative || !copy_number) { + errno = EINVAL; + return(-1); + } + + name = nwsalvage_basename(base_relative); + if (nwsalvage_dirname_copy(base_relative, dir, sizeof(dir)) < 0) + return(-1); + + if (dir[0]) { + if (slprintf(out, (int)out_len - 1, "%s/Copy #%u of %s", + dir, copy_number, name) < 0) { + errno = ENAMETOOLONG; + return(-1); + } + } else { + if (slprintf(out, (int)out_len - 1, "Copy #%u of %s", + copy_number, name) < 0) { + errno = ENAMETOOLONG; + return(-1); + } + } + return(0); +} + +static int nwsalvage_path_exists(const char *path) +{ + if (access(path, F_OK) == 0) + return(1); + if (errno == ENOENT) + return(0); + return(-1); +} + +static int nwsalvage_match_patterns(const char patterns[][NWSALVAGE_PATTERN_LEN_MAX], + unsigned int count, + const char *relative_path) +{ + const char *base; + unsigned int i; + + if (!relative_path) + return(0); + base = nwsalvage_basename(relative_path); + for (i = 0; i < count; i++) { + const char *pattern = patterns[i]; + if (!pattern[0]) + continue; + if (fnmatch(pattern, relative_path, FNM_CASEFOLD) == 0 || + fnmatch(pattern, base, FNM_CASEFOLD) == 0) + return(1); + } + return(0); +} + +static int nwsalvage_match_exclude_dir(const struct nwsalvage_config *config, + const char *relative_path) +{ + char dir[NWSALVAGE_PATH_MAX]; + unsigned int i; + + if (!config || !relative_path) + return(0); + if (nwsalvage_dirname_copy(relative_path, dir, sizeof(dir)) < 0) + return(0); + if (!dir[0]) + return(0); + + for (i = 0; i < config->exclude_dir_count; i++) { + const char *pattern = config->exclude_dir_patterns[i]; + const char *p = pattern; + if (!*p) + continue; + while (*p == '/') + p++; + if (fnmatch(p, dir, FNM_CASEFOLD) == 0 || + !strcasecmp(p, dir) || + (strlen(dir) > strlen(p) && + !strncasecmp(dir, p, strlen(p)) && dir[strlen(p)] == '/')) + return(1); + } + return(0); +} + +static int nwsalvage_should_skip(const struct nwsalvage_config *config, + const char *relative_path, + const struct stat *stb) +{ + unsigned long long size; + + if (!config || !relative_path || !stb) + return(0); + + size = (unsigned long long)stb->st_size; + if (config->min_size && size < config->min_size) + return(1); + if (config->max_size && size > config->max_size) + return(1); + if (nwsalvage_match_patterns(config->exclude_patterns, + config->exclude_count, relative_path)) + return(1); + if (nwsalvage_match_exclude_dir(config, relative_path)) + return(1); + return(0); +} + +static int nwsalvage_select_paths(const struct nwsalvage_config *config, + const char *volume_root, + const char *base_relative, + char *selected_relative, + size_t selected_relative_len, + char *recycle_path, + size_t recycle_path_len, + char *metadata_path, + size_t metadata_path_len, + char *recycle_relative_path, + size_t recycle_relative_path_len, + char *metadata_relative_path, + size_t metadata_relative_path_len) +{ + unsigned int copy; + int recycle_exists; + int metadata_exists; + int use_versions; + + if (!config || !volume_root || !base_relative || !selected_relative || + !recycle_path || !metadata_path || !recycle_relative_path || + !metadata_relative_path) { + errno = EINVAL; + return(-1); + } + + use_versions = (config->flags & NWSALVAGE_FLAG_VERSIONS) && + !nwsalvage_match_patterns(config->noversions_patterns, + config->noversions_count, base_relative); + + for (copy = 0; copy < 1000000; copy++) { + if (!copy) { + if (strlen(base_relative) >= selected_relative_len) { + errno = ENAMETOOLONG; + return(-1); + } + strcpy(selected_relative, base_relative); + } else { + if (!use_versions) + break; + if (nwsalvage_build_copy_relative(selected_relative, + selected_relative_len, + base_relative, copy) < 0) + return(-1); + } + + if (nwsalvage_build_recycle_path(recycle_path, recycle_path_len, + config, volume_root, + selected_relative) < 0 || + nwsalvage_build_metadata_path(metadata_path, metadata_path_len, + config, volume_root, + selected_relative) < 0 || + nwsalvage_build_recycle_relative_path(recycle_relative_path, + recycle_relative_path_len, + config, selected_relative) < 0 || + nwsalvage_build_metadata_relative_path(metadata_relative_path, + metadata_relative_path_len, + config, selected_relative) < 0) + return(-1); + + recycle_exists = nwsalvage_path_exists(recycle_path); + if (recycle_exists < 0) + return(-1); + metadata_exists = nwsalvage_path_exists(metadata_path); + if (metadata_exists < 0) + return(-1); + + if (!recycle_exists && !metadata_exists) + return(0); + + if (!use_versions) { + if (recycle_exists && unlink(recycle_path) < 0) + return(-1); + if (metadata_exists && unlink(metadata_path) < 0) + return(-1); + return(0); + } + } + + errno = EEXIST; + return(-1); +} + +static int nwsalvage_touch_recycled(const char *path, + const struct nwsalvage_config *config, + const struct nwsalvage_deleted_entry *entry) +{ + struct utimbuf times; + time_t now; + + if (!path || !config || !entry) + return(0); + if (!(config->flags & (NWSALVAGE_FLAG_TOUCH | NWSALVAGE_FLAG_TOUCH_MTIME))) + return(0); + + now = time(NULL); + times.actime = now; + times.modtime = (config->flags & NWSALVAGE_FLAG_TOUCH_MTIME) ? + now : (time_t)entry->mtime; + return(utime(path, ×)); +} + static void nwsalvage_format_deleted_by(char *out, size_t out_len) { if (!out || !out_len) @@ -1273,11 +1747,11 @@ static void nwsalvage_fill_afp_metadata(const char *unixname, 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_finder_info(unixname, finder_info, sizeof(finder_info)) == 0) { + 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); @@ -1360,7 +1834,8 @@ int nwsalvage_capture_node_delete(int volume, const char *unixname, 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 base_salvage_relative_path[NWSALVAGE_PATH_MAX]; + char selected_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]; @@ -1393,27 +1868,24 @@ int nwsalvage_capture_node_delete(int volume, const char *unixname, 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_should_skip(&config, relative_path, stb)) + 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) + nwsalvage_format_deleted_by(deleted_by, sizeof(deleted_by)); + if (nwsalvage_build_salvage_relative(base_salvage_relative_path, + sizeof(base_salvage_relative_path), + &config, deleted_by, relative_path) < 0) + return(-1); + + if (nwsalvage_select_paths(&config, volume_root, base_salvage_relative_path, + selected_salvage_relative_path, + sizeof(selected_salvage_relative_path), + recycle_path, sizeof(recycle_path), + metadata_path, sizeof(metadata_path), + recycle_relative_path, + sizeof(recycle_relative_path), + metadata_relative_path, + sizeof(metadata_relative_path)) < 0) return(-1); if (nw_get_volume_name(volume, (uint8 *)volume_name, @@ -1427,8 +1899,7 @@ int nwsalvage_capture_node_delete(int volume, const char *unixname, } memset(&entry, 0, sizeof(entry)); - memset(finder_info_hex, '0', NWSALVAGE_FINDER_INFO_HEX_LEN); - finder_info_hex[NWSALVAGE_FINDER_INFO_HEX_LEN] = '\0'; + finder_info_hex[0] = '\0'; afp_entry_id[0] = '\0'; entry.source = "mars_nwe"; @@ -1455,7 +1926,7 @@ int nwsalvage_capture_node_delete(int volume, const char *unixname, nwsalvage_fill_trustees(volume, unixname, stb, &entry); if (nwsalvage_move_deleted_file(unixname, recycle_path, - metadata_path, &entry) < 0) + metadata_path, &config, &entry) < 0) return(-1); return(0); diff --git a/tests/salvage/CMakeLists.txt b/tests/salvage/CMakeLists.txt index 289a23b..6c3117c 100644 --- a/tests/salvage/CMakeLists.txt +++ b/tests/salvage/CMakeLists.txt @@ -46,6 +46,26 @@ add_custom_target(salvage_ncp_delete_smoke_suite ALL DEPENDS ${SALVAGE_NCP_DELETE_SMOKE_SCRIPT} ) + +set(SALVAGE_NCP_HISTORY_SMOKE_SCRIPT + ${CMAKE_CURRENT_BINARY_DIR}/salvage_ncp_history_smoke.sh +) + +add_custom_command( + OUTPUT ${SALVAGE_NCP_HISTORY_SMOKE_SCRIPT} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/salvage_ncp_history_smoke.sh + ${SALVAGE_NCP_HISTORY_SMOKE_SCRIPT} + COMMAND ${CMAKE_COMMAND} -E env chmod +x ${SALVAGE_NCP_HISTORY_SMOKE_SCRIPT} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/salvage_ncp_history_smoke.sh + COMMENT "Copying salvage NCP history smoke helper" + VERBATIM +) + +add_custom_target(salvage_ncp_history_smoke_suite ALL + DEPENDS ${SALVAGE_NCP_HISTORY_SMOKE_SCRIPT} +) + find_path(SALVAGE_NCPFS_INCLUDE_DIR NAMES ncp/nwcalls.h ncp/ncplib.h ) diff --git a/tests/salvage/README.md b/tests/salvage/README.md index 32983fd..3bd7306 100644 --- a/tests/salvage/README.md +++ b/tests/salvage/README.md @@ -21,11 +21,18 @@ Default repository names come from the `nwserv.conf`/`nw.ini` template options: 51 1 52 .recycle .salvage 53 kv +55 0 0 +56 - +57 - +58 - ``` Section `53` is a compact behaviour-flag string inspired by Samba `vfs_recycle` options. Flags can be combined in one token: `k` = keeptree, -`v` = versions, `t` = touch, and `m` = touch_mtime. +`v` = versions, `t` = touch, and `m` = touch_mtime. Section `55` +contains min/max file sizes and accepts raw bytes or case-insensitive `kb`, +`mb`, and `gb` suffixes. Sections `56`, `57`, and `58` mirror Samba's +`exclude`, `exclude_dir`, and `noversions` pattern lists. Expected layout for a deleted `SYS:PUBLIC/PMDFLTS.INI` owned by `SUPERVISOR`. The sidecar JSON must carry the server-only metadata needed for exact recover, @@ -85,3 +92,22 @@ Example: 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`. + +## NCP history/version smoke + +`salvage_ncp_history_smoke.sh` deletes the same NetWare path twice through +NCP and verifies Samba-compatible version naming. With the `v` flag enabled, +the first recycled payload keeps its original name and the second is written as +`Copy #1 of NAME` in the same recycle directory. The `.salvage` sidecar uses +the same selected payload name with a `.json` suffix. + +Example: + +```sh +./tests/salvage/salvage_ncp_history_smoke.sh \ + -S MARS -U SUPERVISOR -P secret \ + --path SYS:PUBLIC/SLVGHIST.TXT \ + --unix-path /var/mars_nwe/SYS/public/SLVGHIST.TXT \ + --volume-root /var/mars_nwe/SYS \ + --out /tmp/mars-salvage-history-report.txt +``` diff --git a/tests/salvage/salvage_ncp_history_smoke.sh b/tests/salvage/salvage_ncp_history_smoke.sh new file mode 100755 index 0000000..7b7e302 --- /dev/null +++ b/tests/salvage/salvage_ncp_history_smoke.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +# Verify Samba-compatible salvage versioning through the normal NCP delete path. + +set -u + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +SERVER="MARS" +USER_NAME="SUPERVISOR" +PASSWORD="" +NETWARE_PATH="SYS:PUBLIC/SLVGHIST.TXT" +UNIX_PATH="/var/mars_nwe/SYS/public/SLVGHIST.TXT" +VOLUME_ROOT="" +RECYCLE_REPOSITORY=".recycle" +SALVAGE_REPOSITORY=".salvage" +DELETED_BY="" +OUT_FILE="" +FAILURES=0 + +usage() { + cat <&2; usage >&2; exit 2 ;; + esac +done + +[ -n "$PASSWORD" ] || { echo "Missing password. Use -P PASSWORD." >&2; exit 2; } +[ -n "$DELETED_BY" ] || DELETED_BY=$USER_NAME +case "$UNIX_PATH" in /*) ;; *) echo "--unix-path must be absolute" >&2; exit 2 ;; esac +case "$NETWARE_PATH" in *:*) ;; *) echo "--path must include a volume prefix" >&2; exit 2 ;; esac + +REPORT_TMP=$(mktemp "${TMPDIR:-/tmp}/mars-salvage-history.XXXXXX") +trap 'rm -f "$REPORT_TMP"' EXIT INT TERM + +emit() { printf '%s\n' "$*" | tee -a "$REPORT_TMP"; } +section() { emit ""; emit "## $*"; } +fail_check() { emit "$*"; FAILURES=$((FAILURES + 1)); } +finish_report() { + section "Summary" + emit "failures=$FAILURES" + if [ -n "$OUT_FILE" ]; then + cp "$REPORT_TMP" "$OUT_FILE" + emit "report=$OUT_FILE" + fi +} + +count_path_components() { + local path=$1 old_ifs=$IFS count=0 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 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 root=$2 + case "$path" in "$root"/*) printf '%s\n' "${path#$root/}" ;; *) return 1 ;; esac +} + +run_delete() { + local label=$1 status + section "$label" + emit "\$ ./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" 2>&1 | tee -a "$REPORT_TMP" + status=${PIPESTATUS[0]} + emit "[exit=$status]" + if [ "$status" -ne 0 ]; then + emit "warning: NCP helper exited $status; validating salvage artifacts anyway" + fi +} + +check_file() { + local label=$1 path=$2 + section "$label" + emit "path=$path" + [ -f "$path" ] || { fail_check "missing file: $path"; return 1; } + return 0 +} + +cat_metadata() { + local path=$1 + section "metadata JSON: $path" + emit "\$ cat '$path'" + cat "$path" 2>&1 | tee -a "$REPORT_TMP" + local status=${PIPESTATUS[0]} + emit "[exit=$status]" + [ "$status" -eq 0 ] || fail_check "could not cat metadata: $path" +} + +section "mars_nwe salvage NCP history 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" + +if [ -z "$VOLUME_ROOT" ]; then + VOLUME_ROOT=$(compute_unix_volume_root) || { fail_check "could not compute volume root"; VOLUME_ROOT=""; } +fi +RELATIVE_PATH="" +if [ -n "$VOLUME_ROOT" ]; then + RELATIVE_PATH=$(relative_to_volume_root "$UNIX_PATH" "$VOLUME_ROOT") || fail_check "could not derive relative path" +fi + +BASENAME=$(basename -- "$RELATIVE_PATH") +DIRNAME=$(dirname -- "$RELATIVE_PATH") +[ "$DIRNAME" = "." ] && DIRNAME="" +COPY_NAME="Copy #1 of $BASENAME" +if [ -n "$DIRNAME" ]; then + FIRST_REL="$DELETED_BY/$DIRNAME/$BASENAME" + SECOND_REL="$DELETED_BY/$DIRNAME/$COPY_NAME" +else + FIRST_REL="$DELETED_BY/$BASENAME" + SECOND_REL="$DELETED_BY/$COPY_NAME" +fi + +FIRST_RECYCLE="$VOLUME_ROOT/$RECYCLE_REPOSITORY/$FIRST_REL" +SECOND_RECYCLE="$VOLUME_ROOT/$RECYCLE_REPOSITORY/$SECOND_REL" +FIRST_META="$VOLUME_ROOT/$SALVAGE_REPOSITORY/$FIRST_REL.json" +SECOND_META="$VOLUME_ROOT/$SALVAGE_REPOSITORY/$SECOND_REL.json" + +emit "volume_root=$VOLUME_ROOT" +emit "relative_path=$RELATIVE_PATH" +emit "first_recycle=$FIRST_RECYCLE" +emit "second_recycle=$SECOND_RECYCLE" +emit "first_metadata=$FIRST_META" +emit "second_metadata=$SECOND_META" + +section "pre-clean test history artifacts" +rm -f -- "$FIRST_META" "$SECOND_META" "$FIRST_RECYCLE" "$SECOND_RECYCLE" 2>&1 | tee -a "$REPORT_TMP" + +run_delete "NCP create/delete #1" +run_delete "NCP create/delete #2" + +check_file "first recycle payload" "$FIRST_RECYCLE" +check_file "second recycle payload" "$SECOND_RECYCLE" +check_file "first salvage metadata" "$FIRST_META" && cat_metadata "$FIRST_META" +check_file "second salvage metadata" "$SECOND_META" && cat_metadata "$SECOND_META" + +if [ -f "$SECOND_META" ]; then + grep -q "Copy #1 of $BASENAME" "$SECOND_META" || fail_check "second metadata missing Copy #1 path" +fi + +finish_report +[ "$FAILURES" -eq 0 ]