From 054ea6c867092fb9eb294eeaa8da428cde8d8063 Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Sun, 31 May 2026 09:49:17 +0000 Subject: [PATCH] salvage: add yyjson metadata helpers --- CMakeLists.txt | 21 ++ include/nwsalvage.h | 25 +++ src/CMakeLists.txt | 4 + src/nwsalvage.c | 320 +++++++++++++++++++++++++++ tests/salvage/CMakeLists.txt | 5 + tests/salvage/salvage_config_smoke.c | 100 +++++++++ third_party/README.md | 17 ++ 7 files changed, 492 insertions(+) create mode 100644 third_party/README.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 81d4401..1487ab0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,6 +191,27 @@ else() message(STATUS "AFP metadata backend: disabled (requires xattr support)") endif() +set(MARS_NWE_YYJSON_SOURCE_DIR + "${CMAKE_SOURCE_DIR}/third_party/yyjson" + CACHE PATH "Path to the vendored yyjson source tree") + +set(MARS_NWE_HAVE_YYJSON 0) +if(EXISTS "${MARS_NWE_YYJSON_SOURCE_DIR}/CMakeLists.txt") + set(YYJSON_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(YYJSON_BUILD_FUZZER OFF CACHE BOOL "" FORCE) + set(YYJSON_BUILD_MISC OFF CACHE BOOL "" FORCE) + add_subdirectory("${MARS_NWE_YYJSON_SOURCE_DIR}" + "${CMAKE_BINARY_DIR}/third_party/yyjson" + EXCLUDE_FROM_ALL) + set(MARS_NWE_HAVE_YYJSON 1) +endif() + +if(MARS_NWE_HAVE_YYJSON) + message(STATUS "Salvage JSON backend: yyjson") +else() + message(STATUS "Salvage JSON backend: disabled (third_party/yyjson not found)") +endif() + # we want to use systemd, if possible set(SYSTEMD_SERVICES_INSTALL_DIR "" CACHE PATH "Directory for systemd service files") INCLUDE(${CMAKE_MODULE_PATH}/systemdservice.cmake) diff --git a/include/nwsalvage.h b/include/nwsalvage.h index 6c8911c..fd159a1 100644 --- a/include/nwsalvage.h +++ b/include/nwsalvage.h @@ -2,6 +2,7 @@ #define _NWSALVAGE_H_ #include +#include typedef int (*nwsalvage_ini_getter)(int entry, char *str, size_t strsize, void *data); @@ -21,6 +22,26 @@ struct nwsalvage_config { char metadata_repository[NWSALVAGE_REPOSITORY_NAME_MAX]; }; +struct nwsalvage_deleted_entry { + const char *volume_name; + const char *relative_path; + const char *recycle_path; + unsigned int attributes; + unsigned long mode; + unsigned long long size; + long mtime; +}; + +struct nwsalvage_metadata_entry { + char volume_name[NWSALVAGE_REPOSITORY_NAME_MAX]; + char relative_path[NWSALVAGE_PATH_MAX]; + char recycle_path[NWSALVAGE_PATH_MAX]; + unsigned int attributes; + unsigned long mode; + unsigned long long size; + long mtime; +}; + int nwsalvage_config_defaults(struct nwsalvage_config *config); int nwsalvage_config_set_repositories(struct nwsalvage_config *config, const char *recycle_repository, @@ -42,5 +63,9 @@ int nwsalvage_build_metadata_path(char *out, size_t out_len, const struct nwsalvage_config *config, const char *volume_root, const char *relative_path); +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); #endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d449d74..4487349 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -77,6 +77,10 @@ add_executable(ftrustee ftrustee.c tools.c nwfname.c unxfile.c nwvolume.c nwattr target_link_libraries(nwserv ${CRYPT_LIBRARIES} ) target_link_libraries(nwconn ${CRYPT_LIBRARIES} ${XATTR_LIBRARIES} ) +if(MARS_NWE_HAVE_YYJSON) + target_compile_definitions(nwconn PRIVATE MARS_NWE_HAVE_YYJSON) + target_link_libraries(nwconn yyjson) +endif() target_link_libraries(ncpserv ${CRYPT_LIBRARIES} ) target_link_libraries(nwclient ${CRYPT_LIBRARIES} ) target_link_libraries(nwbind ${CRYPT_LIBRARIES} ${GDBM_LIBRARIES} ) diff --git a/src/nwsalvage.c b/src/nwsalvage.c index 812d3ce..bdbf20e 100644 --- a/src/nwsalvage.c +++ b/src/nwsalvage.c @@ -3,8 +3,15 @@ #include #include +#ifdef MARS_NWE_HAVE_YYJSON +#include +#include +#endif #include +#include #include +#include +#include int nwsalvage_repository_name_valid(const char *name) { @@ -293,3 +300,316 @@ int nwsalvage_build_metadata_path(char *out, size_t out_len, return(build_path(out, out_len, volume_root, config->metadata_repository, relative_path, ".json")); } + +#ifdef MARS_NWE_HAVE_YYJSON +static int make_dir_if_missing(const char *path) +{ + struct stat st; + + if (!path || !*path) { + errno = EINVAL; + return(-1); + } + + if (!stat(path, &st)) { + if (S_ISDIR(st.st_mode)) + return(0); + errno = ENOTDIR; + return(-1); + } + + if (mkdir(path, 0777) == 0) + return(0); + if (errno == EEXIST) + return(0); + return(-1); +} + +static int make_parent_dirs(const char *path) +{ + char tmp[NWSALVAGE_PATH_MAX]; + char *p; + size_t len; + + if (!path || !*path) { + errno = EINVAL; + return(-1); + } + + len = strlen(path); + if (len >= sizeof(tmp)) { + errno = ENAMETOOLONG; + return(-1); + } + + memcpy(tmp, path, len + 1); + p = strrchr(tmp, '/'); + if (!p) { + errno = EINVAL; + return(-1); + } + if (p == tmp) + return(0); + *p = '\0'; + + for (p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + if (make_dir_if_missing(tmp) < 0) + return(-1); + *p = '/'; + } + } + + return(make_dir_if_missing(tmp)); +} +#endif + +#ifndef MARS_NWE_HAVE_YYJSON +int nwsalvage_write_metadata(const char *metadata_path, + const struct nwsalvage_deleted_entry *entry) +{ + (void)metadata_path; + (void)entry; + errno = ENOSYS; + return(-1); +} + +int nwsalvage_read_metadata(const char *metadata_path, + struct nwsalvage_metadata_entry *entry) +{ + (void)metadata_path; + if (entry) + memset(entry, 0, sizeof(*entry)); + errno = ENOSYS; + return(-1); +} +#else +static int json_add_string(yyjson_mut_doc *doc, + yyjson_mut_val *object, + const char *name, + const char *value) +{ + if (!doc || !object || !name || !value) { + errno = EINVAL; + return(-1); + } + + if (!yyjson_mut_obj_add_strcpy(doc, object, name, value)) { + errno = ENOMEM; + return(-1); + } + + return(0); +} + +static int json_add_uint64(yyjson_mut_doc *doc, + yyjson_mut_val *object, + const char *name, + unsigned long long value) +{ + if (!doc || !object || !name) { + errno = EINVAL; + return(-1); + } + + if (!yyjson_mut_obj_add_uint(doc, object, name, (uint64_t)value)) { + errno = ENOMEM; + return(-1); + } + + return(0); +} + +static int json_get_string_copy(yyjson_val *object, + const char *name, + char *out, + size_t out_len) +{ + yyjson_val *value_object; + const char *value; + size_t len; + + if (!object || !name || !out || !out_len) { + errno = EINVAL; + return(-1); + } + + value_object = yyjson_obj_get(object, name); + if (!yyjson_is_str(value_object)) { + errno = EINVAL; + return(-1); + } + + value = yyjson_get_str(value_object); + len = strlen(value); + if (len >= out_len) { + errno = ENAMETOOLONG; + return(-1); + } + + memcpy(out, value, len + 1); + return(0); +} + +static int json_get_uint64(yyjson_val *object, + const char *name, + unsigned long long *out) +{ + yyjson_val *value_object; + + if (!object || !name || !out) { + errno = EINVAL; + return(-1); + } + + value_object = yyjson_obj_get(object, name); + if (!yyjson_is_uint(value_object)) { + errno = EINVAL; + return(-1); + } + + *out = (unsigned long long)yyjson_get_uint(value_object); + return(0); +} + +int nwsalvage_write_metadata(const char *metadata_path, + const struct nwsalvage_deleted_entry *entry) +{ + yyjson_mut_doc *doc; + yyjson_mut_val *object; + yyjson_write_err err; + int failed = 0; + + if (!metadata_path || !*metadata_path || !entry || + !entry->volume_name || !entry->relative_path || !entry->recycle_path) { + errno = EINVAL; + return(-1); + } + + if (make_parent_dirs(metadata_path) < 0) + return(-1); + + doc = yyjson_mut_doc_new(NULL); + if (!doc) { + errno = ENOMEM; + return(-1); + } + + object = yyjson_mut_obj(doc); + if (!object) { + yyjson_mut_doc_free(doc); + errno = ENOMEM; + return(-1); + } + yyjson_mut_doc_set_root(doc, object); + + if (json_add_uint64(doc, object, "version", 1) < 0) failed = 1; + if (!failed && json_add_string(doc, object, "volume", entry->volume_name) < 0) + failed = 1; + if (!failed && json_add_string(doc, object, "path", entry->relative_path) < 0) + failed = 1; + if (!failed && json_add_string(doc, object, "recycle_path", entry->recycle_path) < 0) + failed = 1; + if (!failed && json_add_uint64(doc, object, "attributes", entry->attributes) < 0) + failed = 1; + if (!failed && json_add_uint64(doc, object, "mode", entry->mode) < 0) + failed = 1; + if (!failed && json_add_uint64(doc, object, "size", entry->size) < 0) + failed = 1; + if (!failed && entry->mtime < 0) { + errno = EINVAL; + failed = 1; + } + if (!failed && json_add_uint64(doc, object, "mtime", + (unsigned long long)entry->mtime) < 0) + failed = 1; + + if (!failed && !yyjson_mut_write_file(metadata_path, doc, + YYJSON_WRITE_PRETTY_TWO_SPACES | + YYJSON_WRITE_NEWLINE_AT_END, + NULL, &err)) { + (void)err; + errno = EIO; + failed = 1; + } + + yyjson_mut_doc_free(doc); + + if (failed) { + unlink(metadata_path); + return(-1); + } + + return(0); +} + +int nwsalvage_read_metadata(const char *metadata_path, + struct nwsalvage_metadata_entry *entry) +{ + yyjson_doc *doc; + yyjson_val *object; + yyjson_read_err err; + unsigned long long value; + int failed = 0; + + if (!metadata_path || !*metadata_path || !entry) { + errno = EINVAL; + return(-1); + } + + memset(entry, 0, sizeof(*entry)); + + doc = yyjson_read_file(metadata_path, YYJSON_READ_NOFLAG, NULL, &err); + if (!doc) { + (void)err; + errno = EINVAL; + return(-1); + } + + object = yyjson_doc_get_root(doc); + if (!yyjson_is_obj(object)) + failed = 1; + + if (!failed && + (json_get_uint64(object, "version", &value) < 0 || value != 1)) + failed = 1; + if (!failed && json_get_string_copy(object, "volume", + entry->volume_name, + sizeof(entry->volume_name)) < 0) + failed = 1; + if (!failed && json_get_string_copy(object, "path", + entry->relative_path, + sizeof(entry->relative_path)) < 0) + failed = 1; + if (!failed && json_get_string_copy(object, "recycle_path", + entry->recycle_path, + sizeof(entry->recycle_path)) < 0) + failed = 1; + if (!failed && json_get_uint64(object, "attributes", &value) < 0) + failed = 1; + else if (!failed) + entry->attributes = (unsigned int)value; + if (!failed && json_get_uint64(object, "mode", &value) < 0) + failed = 1; + else if (!failed) + entry->mode = (unsigned long)value; + if (!failed && json_get_uint64(object, "size", &value) < 0) + failed = 1; + else if (!failed) + entry->size = value; + if (!failed && json_get_uint64(object, "mtime", &value) < 0) + failed = 1; + else if (!failed) + entry->mtime = (long)value; + + yyjson_doc_free(doc); + + if (failed) { + errno = EINVAL; + return(-1); + } + + return(0); +} +#endif diff --git a/tests/salvage/CMakeLists.txt b/tests/salvage/CMakeLists.txt index 5fda9f9..312e4bc 100644 --- a/tests/salvage/CMakeLists.txt +++ b/tests/salvage/CMakeLists.txt @@ -36,6 +36,11 @@ target_include_directories(salvage_config_smoke PRIVATE ${CMAKE_SOURCE_DIR}/include ) +if(MARS_NWE_HAVE_YYJSON) + target_compile_definitions(salvage_config_smoke PRIVATE MARS_NWE_HAVE_YYJSON) + target_link_libraries(salvage_config_smoke yyjson) +endif() + add_custom_target(run_salvage_config_smoke ALL COMMAND $ DEPENDS salvage_config_smoke diff --git a/tests/salvage/salvage_config_smoke.c b/tests/salvage/salvage_config_smoke.c index d1870eb..3e0c2ef 100644 --- a/tests/salvage/salvage_config_smoke.c +++ b/tests/salvage/salvage_config_smoke.c @@ -1,8 +1,14 @@ /* Smoke test for the initial nwsalvage configuration helpers. */ #include "nwsalvage.h" +#include #include #include +#ifdef MARS_NWE_HAVE_YYJSON +#include +#include +#include +#endif struct fake_ini { @@ -28,6 +34,34 @@ static int fake_ini_getter(int entry, char *str, size_t strsize, void *data) return(1); } + +#ifdef MARS_NWE_HAVE_YYJSON +static int file_contains(const char *path, const char *needle) +{ + FILE *fp = fopen(path, "r"); + char buffer[4096]; + size_t n; + + if (!fp) + return(0); + n = fread(buffer, 1, sizeof(buffer) - 1, fp); + fclose(fp); + buffer[n] = '\0'; + return(strstr(buffer, needle) != NULL); +} + +static void remove_test_tree(const char *root) +{ + char cmd[512]; + + if (!root || !*root) + return; + snprintf(cmd, sizeof(cmd), "rm -rf '%s'", root); + system(cmd); +} + +#endif + static int expect_true(int condition, const char *message) { if (!condition) { @@ -41,6 +75,9 @@ int main(void) { struct nwsalvage_config config; char path[NWSALVAGE_PATH_MAX]; +#ifdef MARS_NWE_HAVE_YYJSON + char root[NWSALVAGE_PATH_MAX]; +#endif struct fake_ini ini; int failures = 0; @@ -148,6 +185,69 @@ int main(void) "SUPERVISOR/PUBLIC/PMDFLTS.INI") < 0, "path builder rejects too-small buffers"); + +#ifdef MARS_NWE_HAVE_YYJSON + snprintf(root, sizeof(root), "/tmp/mars_nwe_salvage_smoke_%ld", + (long)getpid()); + remove_test_tree(root); + failures += expect_true(mkdir(root, 0700) == 0, + "temporary salvage smoke root creates"); + failures += expect_true(nwsalvage_build_metadata_path( + path, sizeof(path), &config, root, + "SUPERVISOR/PUBLIC/PMD\"FLTS.INI") == 0, + "metadata path builds for escaped JSON test"); + if (!failures) { + struct nwsalvage_deleted_entry deleted_entry; + struct nwsalvage_metadata_entry metadata_entry; + + memset(&deleted_entry, 0, sizeof(deleted_entry)); + deleted_entry.volume_name = "SYS"; + deleted_entry.relative_path = "SUPERVISOR/PUBLIC/PMD\"FLTS.INI"; + deleted_entry.recycle_path = "SYS/.recycle/SUPERVISOR/PUBLIC/PMD\"FLTS.INI"; + deleted_entry.attributes = 32; + deleted_entry.mode = 0100644; + deleted_entry.size = 1234; + deleted_entry.mtime = 1710000000; + + failures += expect_true(nwsalvage_write_metadata(path, &deleted_entry) == 0, + "metadata writer creates parent dirs and JSON"); + failures += expect_true(file_contains(path, "\"version\": 1"), + "metadata JSON contains version"); + failures += expect_true(file_contains(path, "\"volume\": \"SYS\""), + "metadata JSON contains volume"); + failures += expect_true(file_contains(path, "PMD\\\"FLTS.INI"), + "metadata JSON escapes quotes"); + failures += expect_true(file_contains(path, "\"attributes\": 32"), + "metadata JSON contains attributes"); + failures += expect_true(nwsalvage_read_metadata(path, &metadata_entry) == 0, + "metadata reader parses JSON"); + failures += expect_true(!strcmp(metadata_entry.volume_name, "SYS"), + "metadata reader returns volume"); + failures += expect_true(!strcmp(metadata_entry.relative_path, + "SUPERVISOR/PUBLIC/PMD\"FLTS.INI"), + "metadata reader returns escaped relative path"); + failures += expect_true(metadata_entry.attributes == 32, + "metadata reader returns attributes"); + failures += expect_true(metadata_entry.size == 1234, + "metadata reader returns size"); + } + remove_test_tree(root); +#else + { + struct nwsalvage_deleted_entry deleted_entry; + + memset(&deleted_entry, 0, sizeof(deleted_entry)); + deleted_entry.volume_name = "SYS"; + deleted_entry.relative_path = "SUPERVISOR/PUBLIC/PMDFLTS.INI"; + deleted_entry.recycle_path = "SYS/.recycle/SUPERVISOR/PUBLIC/PMDFLTS.INI"; + failures += expect_true(nwsalvage_write_metadata( + "/tmp/mars_nwe_salvage_disabled.json", + &deleted_entry) < 0 && errno == ENOSYS, + "metadata writer reports ENOSYS without yyjson"); + } +#endif + + if (failures) return(1); diff --git a/third_party/README.md b/third_party/README.md new file mode 100644 index 0000000..204d6b0 --- /dev/null +++ b/third_party/README.md @@ -0,0 +1,17 @@ +# Third-party dependencies + +## yyjson + +The salvage metadata backend uses yyjson for JSON read/write support when the +vendored source tree is available at `third_party/yyjson`. + +Recommended setup: + +```sh +git submodule add -b master https://github.com/ibireme/yyjson.git third_party/yyjson +git submodule update --init --recursive third_party/yyjson +``` + +CMake auto-detects `third_party/yyjson/CMakeLists.txt`. When the submodule is +missing, mars_nwe still builds, but the salvage JSON entry read/write helpers +return `ENOSYS` and the yyjson-backed smoke assertions are skipped.