diff --git a/src/libstore-c/nix_api_store.cc b/src/libstore-c/nix_api_store.cc index b95e5b749..4f71d0a3c 100644 --- a/src/libstore-c/nix_api_store.cc +++ b/src/libstore-c/nix_api_store.cc @@ -1,3 +1,6 @@ +#include +#include + #include "nix_api_store.h" #include "nix_api_store_internal.h" #include "nix_api_util.h" @@ -8,6 +11,7 @@ #include "nix/store/store-open.hh" #include "nix/store/build-result.hh" #include "nix/store/local-fs-store.hh" +#include "nix/util/base-nix-32.hh" #include "nix/store/globals.hh" @@ -215,7 +219,65 @@ void nix_derivation_free(nix_derivation * drv) StorePath * nix_store_path_clone(const StorePath * p) { - return new StorePath{p->path}; + try { + return new StorePath{p->path}; + } catch (...) { + return nullptr; + } +} + +} // extern "C" + +template +static auto to_cpp_array(const uint8_t (&r)[S]) +{ + return reinterpret_cast &>(r); +} + +extern "C" { + +nix_err +nix_store_path_hash(nix_c_context * context, const StorePath * store_path, nix_store_path_hash_part * hash_part_out) +{ + try { + auto hashPart = store_path->path.hashPart(); + // Decode from Nix32 (base32) encoding to raw bytes + auto decoded = nix::BaseNix32::decode(hashPart); + + assert(decoded.size() == sizeof(hash_part_out->bytes)); + std::memcpy(hash_part_out->bytes, decoded.data(), sizeof(hash_part_out->bytes)); + return NIX_OK; + } + NIXC_CATCH_ERRS +} + +StorePath * nix_store_create_from_parts( + nix_c_context * context, const nix_store_path_hash_part * hash, const char * name, size_t name_len) +{ + if (context) + context->last_err_code = NIX_OK; + try { + // Encode the 20 raw bytes to Nix32 (base32) format + auto hashStr = nix::BaseNix32::encode(std::span{to_cpp_array(hash->bytes)}); + + // Construct the store path basename: - + std::string baseName; + baseName += hashStr; + baseName += "-"; + baseName += std::string_view{name, name_len}; + + return new StorePath{nix::StorePath(std::move(baseName))}; + } + NIXC_CATCH_ERRS_NULL +} + +nix_derivation * nix_derivation_clone(const nix_derivation * d) +{ + try { + return new nix_derivation{d->drv}; + } catch (...) { + return nullptr; + } } nix_derivation * nix_derivation_from_json(nix_c_context * context, Store * store, const char * json) @@ -228,6 +290,20 @@ nix_derivation * nix_derivation_from_json(nix_c_context * context, Store * store NIXC_CATCH_ERRS_NULL } +nix_err nix_derivation_to_json( + nix_c_context * context, const nix_derivation * drv, nix_get_string_callback callback, void * userdata) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto result = static_cast(drv->drv).dump(); + if (callback) { + callback(result.data(), result.size(), userdata); + } + } + NIXC_CATCH_ERRS +} + StorePath * nix_add_derivation(nix_c_context * context, Store * store, nix_derivation * derivation) { if (context) @@ -252,4 +328,14 @@ nix_err nix_store_copy_closure(nix_c_context * context, Store * srcStore, Store NIXC_CATCH_ERRS } +nix_derivation * nix_store_drv_from_store_path(nix_c_context * context, Store * store, const StorePath * path) +{ + if (context) + context->last_err_code = NIX_OK; + try { + return new nix_derivation{store->ptr->derivationFromPath(path->path)}; + } + NIXC_CATCH_ERRS_NULL +} + } // extern "C" diff --git a/src/libstore-c/nix_api_store.h b/src/libstore-c/nix_api_store.h index 9eaa61a92..761fdf3c8 100644 --- a/src/libstore-c/nix_api_store.h +++ b/src/libstore-c/nix_api_store.h @@ -106,7 +106,7 @@ nix_err nix_store_get_storedir(nix_c_context * context, Store * store, nix_get_string_callback callback, void * user_data); /** - * @brief Parse a Nix store path into a StorePath + * @brief Parse a Nix store path that includes the store dir into a StorePath * * @note Don't forget to free this path using nix_store_path_free()! * @param[out] context Optional, stores error information @@ -188,9 +188,16 @@ nix_store_get_version(nix_c_context * context, Store * store, nix_get_string_cal /** * @brief Create a `nix_derivation` from a JSON representation of that derivation. * + * @note Unlike `nix_derivation_to_json`, this needs a `Store`. This is because + * over time we expect the internal representation of derivations in Nix to + * differ from accepted derivation formats. The store argument is here to help + * any logic needed to convert from JSON to the internal representation, in + * excess of just parsing. + * * @param[out] context Optional, stores error information. * @param[in] store nix store reference. * @param[in] json JSON of the derivation as a string. + * @return A new derivation, or NULL on error. Free with `nix_derivation_free` when done using the `nix_derivation`. */ nix_derivation * nix_derivation_from_json(nix_c_context * context, Store * store, const char * json); @@ -242,6 +249,16 @@ nix_err nix_store_get_fs_closure( void * userdata, void (*callback)(nix_c_context * context, void * userdata, const StorePath * store_path)); +/** + * @brief Returns the derivation associated with the store path + * + * @param[out] context Optional, stores error information + * @param[in] store The nix store + * @param[in] path The nix store path + * @return A new derivation, or NULL on error. Free with `nix_derivation_free` when done using the `nix_derivation`. + */ +nix_derivation * nix_store_drv_from_store_path(nix_c_context * context, Store * store, const StorePath * path); + // cffi end #ifdef __cplusplus } diff --git a/src/libstore-c/nix_api_store/derivation.h b/src/libstore-c/nix_api_store/derivation.h index 9c42cfd60..239ffd52f 100644 --- a/src/libstore-c/nix_api_store/derivation.h +++ b/src/libstore-c/nix_api_store/derivation.h @@ -20,6 +20,14 @@ extern "C" { /** @brief Nix Derivation */ typedef struct nix_derivation nix_derivation; +/** + * @brief Copy a `nix_derivation` + * + * @param[in] d the derivation to copy + * @return a new `nix_derivation` + */ +nix_derivation * nix_derivation_clone(const nix_derivation * d); + /** * @brief Deallocate a `nix_derivation` * @@ -28,6 +36,17 @@ typedef struct nix_derivation nix_derivation; */ void nix_derivation_free(nix_derivation * drv); +/** + * @brief Gets the derivation as a JSON string + * + * @param[out] context Optional, stores error information + * @param[in] drv The derivation + * @param[in] callback Called with the JSON string + * @param[in] userdata Arbitrary data passed to the callback + */ +nix_err nix_derivation_to_json( + nix_c_context * context, const nix_derivation * drv, nix_get_string_callback callback, void * userdata); + // cffi end #ifdef __cplusplus } diff --git a/src/libstore-c/nix_api_store/store_path.h b/src/libstore-c/nix_api_store/store_path.h index 9f3717aea..1aa9bcac7 100644 --- a/src/libstore-c/nix_api_store/store_path.h +++ b/src/libstore-c/nix_api_store/store_path.h @@ -10,6 +10,9 @@ * @brief Store path operations */ +#include +#include + #include "nix_api_util.h" #ifdef __cplusplus @@ -44,6 +47,45 @@ void nix_store_path_free(StorePath * p); */ void nix_store_path_name(const StorePath * store_path, nix_get_string_callback callback, void * user_data); +/** + * @brief A store path hash + * + * Once decoded from "nix32" encoding, a store path hash is 20 raw bytes. + */ +typedef struct nix_store_path_hash_part +{ + uint8_t bytes[20]; +} nix_store_path_hash_part; + +/** + * @brief Get the path hash (e.g. "" in /nix/store/-) + * + * The hash is returned as raw bytes, decoded from "nix32" encoding. + * + * @param[out] context Optional, stores error information + * @param[in] store_path the path to get the hash from + * @param[out] hash_part_out the decoded hash as 20 raw bytes + * @return NIX_OK on success, error code on failure + */ +nix_err +nix_store_path_hash(nix_c_context * context, const StorePath * store_path, nix_store_path_hash_part * hash_part_out); + +/** + * @brief Create a StorePath from its constituent parts (hash and name) + * + * This function constructs a store path from a hash and name, without needing + * a Store reference or the store directory prefix. + * + * @note Don't forget to free this path using nix_store_path_free()! + * @param[out] context Optional, stores error information + * @param[in] hash The store path hash (20 raw bytes) + * @param[in] name The store path name (the part after the hash) + * @param[in] name_len Length of the name string + * @return owned store path, NULL on error + */ +StorePath * nix_store_create_from_parts( + nix_c_context * context, const nix_store_path_hash_part * hash, const char name[/*name_len*/], size_t name_len); + // cffi end #ifdef __cplusplus } diff --git a/src/libstore-tests/nix_api_store.cc b/src/libstore-tests/nix_api_store.cc index bf411053a..a7fa4d8d8 100644 --- a/src/libstore-tests/nix_api_store.cc +++ b/src/libstore-tests/nix_api_store.cc @@ -1,5 +1,7 @@ #include +#include + #include "nix_api_util.h" #include "nix_api_store.h" @@ -92,6 +94,70 @@ TEST_F(nix_api_store_test, DoesNotCrashWhenContextIsNull) nix_store_path_free(path); } +// Verify it's 20 bytes +static_assert(sizeof(nix_store_path_hash_part::bytes) == 20); +static_assert(sizeof(nix_store_path_hash_part::bytes) == sizeof(nix_store_path_hash_part)); + +TEST_F(nix_api_store_test, nix_store_path_hash) +{ + StorePath * path = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str()); + ASSERT_NE(path, nullptr); + + nix_store_path_hash_part hash; + auto ret = nix_store_path_hash(ctx, path, &hash); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + + // The hash should be non-zero + bool allZero = true; + for (size_t i = 0; i < sizeof(hash.bytes); i++) { + if (hash.bytes[i] != 0) { + allZero = false; + break; + } + } + ASSERT_FALSE(allZero); + + nix_store_path_free(path); +} + +TEST_F(nix_api_store_test, nix_store_create_from_parts_roundtrip) +{ + // Parse a path + StorePath * original = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str()); + EXPECT_NE(original, nullptr); + + // Get its hash + nix_store_path_hash_part hash; + auto ret = nix_store_path_hash(ctx, original, &hash); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + + // Get its name + std::string name; + nix_store_path_name(original, OBSERVE_STRING(name)); + + // Reconstruct from parts + StorePath * reconstructed = nix_store_create_from_parts(ctx, &hash, name.c_str(), name.size()); + assert_ctx_ok(); + ASSERT_NE(reconstructed, nullptr); + + // Should be equal + EXPECT_EQ(original->path, reconstructed->path); + + nix_store_path_free(original); + nix_store_path_free(reconstructed); +} + +TEST_F(nix_api_store_test, nix_store_create_from_parts_invalid_name) +{ + nix_store_path_hash_part hash = {}; + // Invalid name with spaces + StorePath * path = nix_store_create_from_parts(ctx, &hash, "invalid name", 12); + ASSERT_EQ(path, nullptr); + ASSERT_EQ(nix_err_code(ctx), NIX_ERR_NIX_ERROR); +} + TEST_F(nix_api_store_test, get_version) { std::string str; @@ -795,4 +861,97 @@ TEST_F(NixApiStoreTestWithRealisedPath, nix_store_get_fs_closure_error_propagati ASSERT_EQ(call_count, 1); // Should have been called exactly once, then aborted } +/** + * @brief Helper function to load JSON from a test data file + * + * @param filename Relative path from _NIX_TEST_UNIT_DATA + * @return JSON string contents of the file + */ +static std::string load_json_from_test_data(const char * filename) +{ + std::filesystem::path unitTestData{getenv("_NIX_TEST_UNIT_DATA")}; + std::ifstream t{unitTestData / filename}; + std::stringstream buffer; + buffer << t.rdbuf(); + return buffer.str(); +} + +TEST_F(nix_api_store_test, nix_derivation_to_json_roundtrip) +{ + // Load JSON from test data + auto originalJson = load_json_from_test_data("derivation/invariants/filled-in-deferred-empty-env-var-pre.json"); + + // Parse to derivation + auto * drv = nix_derivation_from_json(ctx, store, originalJson.c_str()); + assert_ctx_ok(); + ASSERT_NE(drv, nullptr); + + // Convert back to JSON + std::string convertedJson; + auto ret = nix_derivation_to_json(ctx, drv, OBSERVE_STRING(convertedJson)); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + ASSERT_FALSE(convertedJson.empty()); + + // Parse both JSON strings to compare (ignoring whitespace differences) + auto originalParsed = nlohmann::json::parse(originalJson); + auto convertedParsed = nlohmann::json::parse(convertedJson); + + // Remove parts that will be different due to filling-in. + originalParsed.at("outputs").erase("out"); + originalParsed.at("env").erase("out"); + convertedParsed.at("outputs").erase("out"); + convertedParsed.at("env").erase("out"); + + // They should be equivalent + ASSERT_EQ(originalParsed, convertedParsed); + + nix_derivation_free(drv); +} + +TEST_F(nix_api_store_test, nix_derivation_store_round_trip) +{ + // Load a derivation from JSON + auto json = load_json_from_test_data("derivation/invariants/filled-in-deferred-empty-env-var-pre.json"); + auto * drv = nix_derivation_from_json(ctx, store, json.c_str()); + assert_ctx_ok(); + ASSERT_NE(drv, nullptr); + + // Add to store + auto * drvPath = nix_add_derivation(ctx, store, drv); + assert_ctx_ok(); + ASSERT_NE(drvPath, nullptr); + + // Retrieve from store + auto * drv2 = nix_store_drv_from_store_path(ctx, store, drvPath); + assert_ctx_ok(); + ASSERT_NE(drv2, nullptr); + + // The round trip should make the same derivation + ASSERT_EQ(drv->drv, drv2->drv); + + nix_store_path_free(drvPath); + nix_derivation_free(drv); + nix_derivation_free(drv2); +} + +TEST_F(nix_api_store_test, nix_derivation_clone) +{ + // Load a derivation from JSON + auto json = load_json_from_test_data("derivation/invariants/filled-in-deferred-empty-env-var-pre.json"); + auto * drv = nix_derivation_from_json(ctx, store, json.c_str()); + assert_ctx_ok(); + ASSERT_NE(drv, nullptr); + + // Clone the derivation + auto * drv2 = nix_derivation_clone(drv); + ASSERT_NE(drv2, nullptr); + + // The clone should be equal + ASSERT_EQ(drv->drv, drv2->drv); + + nix_derivation_free(drv); + nix_derivation_free(drv2); +} + } // namespace nixC diff --git a/src/libutil-c/nix_api_util.cc b/src/libutil-c/nix_api_util.cc index a2f7710bc..f28a9168e 100644 --- a/src/libutil-c/nix_api_util.cc +++ b/src/libutil-c/nix_api_util.cc @@ -13,7 +13,11 @@ extern "C" { nix_c_context * nix_c_context_create() { - return new nix_c_context(); + try { + return new nix_c_context(); + } catch (...) { + return nullptr; + } } void nix_c_context_free(nix_c_context * context)