salvage: add yyjson metadata helpers
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define _NWSALVAGE_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
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
|
||||
|
||||
@@ -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} )
|
||||
|
||||
320
src/nwsalvage.c
320
src/nwsalvage.c
@@ -3,8 +3,15 @@
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#ifdef MARS_NWE_HAVE_YYJSON
|
||||
#include <stdint.h>
|
||||
#include <yyjson.h>
|
||||
#endif
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
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
|
||||
|
||||
@@ -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 $<TARGET_FILE:salvage_config_smoke>
|
||||
DEPENDS salvage_config_smoke
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
/* Smoke test for the initial nwsalvage configuration helpers. */
|
||||
#include "nwsalvage.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#ifdef MARS_NWE_HAVE_YYJSON
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#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);
|
||||
|
||||
|
||||
17
third_party/README.md
vendored
Normal file
17
third_party/README.md
vendored
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user