diff --git a/doc/manual/rl-next/c-api-path-info.md b/doc/manual/rl-next/c-api-path-info.md new file mode 100644 index 000000000000..e0df0fd1d1ff --- /dev/null +++ b/doc/manual/rl-next/c-api-path-info.md @@ -0,0 +1,15 @@ +--- +synopsis: "C API: Add store path metadata accessors" +prs: [15675] +--- + +The C API now includes functions for querying store path metadata: + +- `nix_store_query_path_info()` - Query metadata for a store path +- `nix_path_info_get_nar_hash()` - Get the NAR hash +- `nix_path_info_get_nar_size()` - Get the NAR size +- `nix_path_info_get_references()` - Iterate over references +- `nix_path_info_get_deriver()` - Get the deriver +- `nix_path_info_get_sigs()` - Iterate over signatures +- `nix_path_info_get_ca()` - Get the content address +- `nix_path_info_free()` - Free store path metadata diff --git a/src/libstore-c/meson.build b/src/libstore-c/meson.build index 600de4d2ea61..e4e5b8ec96e5 100644 --- a/src/libstore-c/meson.build +++ b/src/libstore-c/meson.build @@ -36,6 +36,7 @@ include_dirs = [ include_directories('.') ] headers = files( 'nix_api_store.h', 'nix_api_store/derivation.h', + 'nix_api_store/path_info.h', 'nix_api_store/store_path.h', ) diff --git a/src/libstore-c/nix_api_store.cc b/src/libstore-c/nix_api_store.cc index c0b29625440d..0d5870c729b3 100644 --- a/src/libstore-c/nix_api_store.cc +++ b/src/libstore-c/nix_api_store.cc @@ -16,6 +16,7 @@ #include "nix/util/base-nix-32.hh" #include "nix/store/globals.hh" +#include "nix/store/content-address.hh" extern "C" { @@ -381,4 +382,112 @@ nix_err nix_store_copy_path( NIXC_CATCH_ERRS } +nix_path_info * nix_store_query_path_info(nix_c_context * context, Store * store, const StorePath * path) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto info = store->ptr->queryPathInfo(path->path); + return new nix_path_info{info}; + } + NIXC_CATCH_ERRS_NULL +} + +void nix_path_info_free(nix_path_info * path_info) +{ + delete path_info; +} + +nix_err nix_path_info_get_nar_hash( + nix_c_context * context, const nix_path_info * path_info, nix_get_string_callback callback, void * user_data) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto res = path_info->info->narHash.to_string(nix::HashFormat::Nix32, true); + return call_nix_get_string_callback(res, callback, user_data); + } + NIXC_CATCH_ERRS +} + +uint64_t nix_path_info_get_nar_size(nix_c_context * context, const nix_path_info * path_info) +{ + if (context) + context->last_err_code = NIX_OK; + try { + return path_info->info->narSize; + } + NIXC_CATCH_ERRS_RES(0); +} + +nix_err nix_path_info_get_references( + nix_c_context * context, + const nix_path_info * path_info, + void * user_data, + void (*callback)(nix_c_context * context, void * user_data, const StorePath * store_path)) +{ + if (context) + context->last_err_code = NIX_OK; + try { + if (callback) { + for (const auto & ref : path_info->info->references) { + const StorePath tmp{ref}; + callback(context, user_data, &tmp); + if (context && context->last_err_code != NIX_OK) + return context->last_err_code; + } + } + } + NIXC_CATCH_ERRS +} + +StorePath * nix_path_info_get_deriver(nix_c_context * context, const nix_path_info * path_info) +{ + if (context) + context->last_err_code = NIX_OK; + try { + if (path_info->info->deriver) + return new StorePath{*path_info->info->deriver}; + return nullptr; + } + NIXC_CATCH_ERRS_NULL +} + +nix_err nix_path_info_get_sigs( + nix_c_context * context, + const nix_path_info * path_info, + void * user_data, + void (*callback)(nix_c_context * context, void * user_data, const char * sig, unsigned int sig_len)) +{ + if (context) + context->last_err_code = NIX_OK; + try { + if (callback) { + for (const auto & sig : path_info->info->sigs) { + auto s = sig.to_string(); + callback(context, user_data, s.data(), s.size()); + if (context && context->last_err_code != NIX_OK) + return context->last_err_code; + } + } + } + NIXC_CATCH_ERRS +} + +nix_err nix_path_info_get_ca( + nix_c_context * context, const nix_path_info * path_info, nix_get_string_callback callback, void * user_data) +{ + if (context) + context->last_err_code = NIX_OK; + try { + if (!path_info->info->ca) + return nix_set_err_msg(context, NIX_ERR_KEY, "Store path is not content-addressed"); + if (callback) { + auto res = renderContentAddress(*path_info->info->ca); + return call_nix_get_string_callback(res, callback, user_data); + } + } + NIXC_CATCH_ERRS +} + } // extern "C" diff --git a/src/libstore-c/nix_api_store.h b/src/libstore-c/nix_api_store.h index e60a7d6c0f7c..e1f45fae7389 100644 --- a/src/libstore-c/nix_api_store.h +++ b/src/libstore-c/nix_api_store.h @@ -14,6 +14,7 @@ #include "nix_api_util.h" #include "nix_api_store/store_path.h" #include "nix_api_store/derivation.h" +#include "nix_api_store/path_info.h" #include #ifdef __cplusplus @@ -283,6 +284,19 @@ StorePath * nix_store_query_path_from_hash_part(nix_c_context * context, Store * nix_err nix_store_copy_path( nix_c_context * context, Store * srcStore, Store * dstStore, const StorePath * path, bool repair, bool checkSigs); +/** + * @brief Query metadata about a store path + * + * The path must be valid in the store; otherwise an error is returned. + * + * @note Don't forget to free this with nix_path_info_free()! + * @param[out] context Optional, stores error information + * @param[in] store Nix store reference + * @param[in] path The store path to query + * @return owned nix_path_info, NULL on error + */ +nix_path_info * nix_store_query_path_info(nix_c_context * context, Store * store, const StorePath * path); + // cffi end #ifdef __cplusplus } diff --git a/src/libstore-c/nix_api_store/path_info.h b/src/libstore-c/nix_api_store/path_info.h new file mode 100644 index 000000000000..e94e88efc4d5 --- /dev/null +++ b/src/libstore-c/nix_api_store/path_info.h @@ -0,0 +1,141 @@ +#ifndef NIX_API_STORE_PATH_INFO_H +#define NIX_API_STORE_PATH_INFO_H +/** + * @defgroup libstore_pathinfo PathInfo + * @ingroup libstore + * @brief Store path metadata + * @{ + */ +/** @file + * @brief Path info operations for querying store object metadata + */ + +#include + +#include "nix_api_util.h" +#include "nix_api_store/store_path.h" + +#ifdef __cplusplus +extern "C" { +#endif +// cffi start + +/** @brief Opaque handle to store path metadata */ +typedef struct nix_path_info nix_path_info; + +/** + * @brief Deallocate a nix_path_info + * + * Does not fail. + * @param[in] path_info the nix_path_info to free + */ +void nix_path_info_free(nix_path_info * path_info); + +/** + * @brief Get the NAR hash of a store path + * + * Returns the hash as a string with algorithm prefix in Nix base-32 encoding, + * e.g. "sha256:1b8m03r63zqhnjf7l5nh...". This is the format used in narinfo files. + * + * @param[out] context Optional, stores error information + * @param[in] path_info the nix_path_info to read from + * @param[in] callback called with the hash string + * @param[in] user_data arbitrary data, passed to the callback when it's called + * @return NIX_OK on success, error code on failure + */ +nix_err nix_path_info_get_nar_hash( + nix_c_context * context, const nix_path_info * path_info, nix_get_string_callback callback, void * user_data); + +/** + * @brief Get the NAR size of a store path + * + * @param[out] context Optional, stores error information + * @param[in] path_info the nix_path_info to read from + * @return NAR size in bytes, 0 on error. Note that a NAR always has a root object, + * so an actual NAR stream is never empty. + */ +uint64_t nix_path_info_get_nar_size(nix_c_context * context, const nix_path_info * path_info); + +/** + * @brief Iterate over the references of a store path + * + * Calls the callback once for each reference. The StorePath passed to the + * callback is borrowed and only valid for the duration of the callback. + * Iteration stops if the callback returns with `context` in an error state. + * + * @param[out] context Optional, stores error information + * @param[in] path_info the nix_path_info to read from + * @param[in] user_data arbitrary data, passed to the callback + * @param[in] callback called for each referenced store path + * @return NIX_OK on success, error code on failure + */ +nix_err nix_path_info_get_references( + nix_c_context * context, + const nix_path_info * path_info, + void * user_data, + void (*callback)(nix_c_context * context, void * user_data, const StorePath * store_path)); + +/** + * @brief Get the deriver of a store path + * + * @note Don't forget to free the result with nix_store_path_free()! + * @param[out] context Optional, stores error information + * @param[in] path_info the nix_path_info to read from + * @return owned StorePath of the deriver, or NULL if no deriver is known + */ +StorePath * nix_path_info_get_deriver(nix_c_context * context, const nix_path_info * path_info); + +/** + * @brief Iterate over the signatures of a store path + * + * Calls the callback once for each signature string (format: "keyName:base64sig"). + * The `sig` data is borrowed and the callback must not assume that the buffer + * persists after it returns. + * + * Iteration stops if the callback returns with `context` in an error state. + * + * @param[out] context Optional, stores error information + * @param[in] path_info the nix_path_info to read from + * @param[in] user_data arbitrary data, passed to the callback + * @param[in] callback called for each signature string + * @return NIX_OK on success, error code on failure + */ +nix_err nix_path_info_get_sigs( + nix_c_context * context, + const nix_path_info * path_info, + void * user_data, + void (*callback)(nix_c_context * context, void * user_data, const char * sig, unsigned int sig_len)); + +/** + * @brief Get the content address of a store path, if it has one + * + * If so, "returns" the hash as a string with method and algorithm prefix in Nix base-32 encoding, + * e.g. `"fixed:r:sha256:1i89icvvs2f3cym00414i3bbl1qidhg0b5yrmdlx9cjkj5is6ljg"`. + * + * If the store object referenced by `path_info` is not content-addressed, + * the return code is `NIX_ERR_KEY`, and the callback is not called. + * + * `NIX_ERR_KEY` is only returned when `path_info` is not content-addressed. + * + * Input-addressed store paths have a content hash (see `nix_path_info_get_nar_hash`, + * but no content *address*, so that results in NIX_ERR_KEY, distinguishable from + * other, perhaps more unexpected errors. + * + * @param[out] context Optional, stores error information + * @param[in] path_info the nix_path_info to read from + * @param[in] callback called with the content address string (only called when present) + * @param[in] user_data arbitrary data, passed to the callback when it's called + * @return NIX_OK on success, NIX_ERR_KEY if the path is not content-addressed, + * another error code on failure + */ +nix_err nix_path_info_get_ca( + nix_c_context * context, const nix_path_info * path_info, nix_get_string_callback callback, void * user_data); + +// cffi end +#ifdef __cplusplus +} +#endif +/** + * @} + */ +#endif // NIX_API_STORE_PATH_INFO_H diff --git a/src/libstore-c/nix_api_store_internal.h b/src/libstore-c/nix_api_store_internal.h index 712d96488a57..a8f78d801288 100644 --- a/src/libstore-c/nix_api_store_internal.h +++ b/src/libstore-c/nix_api_store_internal.h @@ -2,6 +2,7 @@ #define NIX_API_STORE_INTERNAL_H #include "nix/store/store-api.hh" #include "nix/store/derivations.hh" +#include "nix/store/path-info.hh" extern "C" { @@ -20,6 +21,11 @@ struct nix_derivation nix::Derivation drv; }; +struct nix_path_info +{ + nix::ref info; +}; + } // extern "C" #endif diff --git a/src/libstore-tests/nix_api_store.cc b/src/libstore-tests/nix_api_store.cc index 0162684daf4b..de21fc00c5e9 100644 --- a/src/libstore-tests/nix_api_store.cc +++ b/src/libstore-tests/nix_api_store.cc @@ -977,4 +977,252 @@ TEST_F(nix_api_store_test, nix_derivation_clone) nix_derivation_free(drv2); } +TEST_F(NixApiStoreTestWithRealisedPath, nix_store_query_path_info) +{ + auto expected = store->ptr->queryPathInfo(outPath->path); + nix_path_info * info = nix_store_query_path_info(ctx, store, outPath); + assert_ctx_ok(); + ASSERT_NE(info, nullptr); + + std::string narHash; + auto ret = nix_path_info_get_nar_hash(ctx, info, OBSERVE_STRING(narHash)); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + ASSERT_EQ(narHash, expected->narHash.to_string(nix::HashFormat::Nix32, true)); + + auto narSize = nix_path_info_get_nar_size(ctx, info); + assert_ctx_ok(); + ASSERT_EQ(narSize, expected->narSize); + + nix::StorePathSet refs; + + struct ReferenceCallbackData + { + nix::StorePathSet * refs; + }; + + ReferenceCallbackData refData{&refs}; + ret = nix_path_info_get_references( + ctx, info, &refData, [](nix_c_context *, void * user_data, const StorePath * refPath) { + static_cast(user_data)->refs->insert(refPath->path); + }); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + ASSERT_EQ(refs, expected->references); + + nix::Strings sigs; + + struct SignatureCallbackData + { + nix::Strings * sigs; + }; + + SignatureCallbackData sigData{&sigs}; + ret = nix_path_info_get_sigs( + ctx, info, &sigData, [](nix_c_context *, void * user_data, const char * sig, unsigned int sig_len) { + static_cast(user_data)->sigs->emplace_back(sig, sig_len); + }); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + ASSERT_EQ(sigs, nix::Signature::toStrings(expected->sigs)); + + ASSERT_TRUE(expected->ca); + std::string ca; + ret = nix_path_info_get_ca(ctx, info, OBSERVE_STRING(ca)); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + ASSERT_EQ(ca, nix::renderContentAddress(*expected->ca)); + + nix_path_info_free(info); +} + +TEST_F(NixApiStoreTestWithRealisedPath, nix_path_info_deriver) +{ + nix_path_info * info = nix_store_query_path_info(ctx, store, outPath); + assert_ctx_ok(); + ASSERT_NE(info, nullptr); + + StorePath * deriver = nix_path_info_get_deriver(ctx, info); + assert_ctx_ok(); + ASSERT_NE(deriver, nullptr); + ASSERT_EQ(deriver->path, drvPath->path); + + nix_store_path_free(deriver); + nix_path_info_free(info); +} + +TEST_F(nix_api_store_test, nix_store_query_path_info_invalid_path) +{ + StorePath * path = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str()); + ASSERT_NE(path, nullptr); + + nix_path_info * info = nix_store_query_path_info(ctx, store, path); + ASSERT_EQ(info, nullptr); + ASSERT_NE(nix_err_code(ctx), NIX_OK); + + nix_store_path_free(path); +} + +TEST_F(NixApiStoreTestWithRealisedPath, nix_path_info_get_references_early_exit) +{ + auto cppInfo = store->ptr->queryPathInfo(outPath->path); + auto mutInfo = std::make_shared(*cppInfo); + mutInfo->references.insert(outPath->path); + mutInfo->references.insert(drvPath->path); + auto * info = new nix_path_info{nix::ref(mutInfo)}; + + struct CallbackData + { + int callCount = 0; + }; + + CallbackData data; + auto ret = nix_path_info_get_references( + ctx, info, &data, [](nix_c_context * context, void * user_data, const StorePath *) { + static_cast(user_data)->callCount++; + nix_set_err_msg(context, NIX_ERR_UNKNOWN, "Test error from reference callback"); + }); + + ASSERT_EQ(data.callCount, 1); + ASSERT_EQ(ret, NIX_ERR_UNKNOWN); + ASSERT_EQ(nix_err_code(ctx), NIX_ERR_UNKNOWN); + ASSERT_STREQ(nix_err_msg(nullptr, ctx, nullptr), "Test error from reference callback"); + + nix_path_info_free(info); +} + +TEST_F(NixApiStoreTestWithRealisedPath, nix_path_info_get_sigs_early_exit) +{ + auto cppInfo = store->ptr->queryPathInfo(outPath->path); + auto mutInfo = std::make_shared(*cppInfo); + mutInfo->sigs.insert(nix::Signature::parse("key1:c2ln")); + mutInfo->sigs.insert(nix::Signature::parse("key2:c2ln")); + auto * info = new nix_path_info{nix::ref(mutInfo)}; + + struct CallbackData + { + int callCount = 0; + }; + + CallbackData data; + auto ret = nix_path_info_get_sigs( + ctx, info, &data, [](nix_c_context * context, void * user_data, const char *, unsigned int) { + static_cast(user_data)->callCount++; + nix_set_err_msg(context, NIX_ERR_UNKNOWN, "Test error from signature callback"); + }); + + ASSERT_EQ(data.callCount, 1); + ASSERT_EQ(ret, NIX_ERR_UNKNOWN); + ASSERT_EQ(nix_err_code(ctx), NIX_ERR_UNKNOWN); + ASSERT_STREQ(nix_err_msg(nullptr, ctx, nullptr), "Test error from signature callback"); + + nix_path_info_free(info); +} + +TEST_F(NixApiStoreTestWithRealisedPath, nix_path_info_iterates_all_references_and_sigs) +{ + auto cppInfo = store->ptr->queryPathInfo(outPath->path); + auto mutInfo = std::make_shared(*cppInfo); + mutInfo->references.insert(outPath->path); + mutInfo->references.insert(drvPath->path); + mutInfo->sigs.insert(nix::Signature::parse("key1:c2ln")); + mutInfo->sigs.insert(nix::Signature::parse("key2:c2ln")); + auto * info = new nix_path_info{nix::ref(mutInfo)}; + + nix::StorePathSet refs; + + struct ReferenceCallbackData + { + nix::StorePathSet * refs; + }; + + ReferenceCallbackData refData{&refs}; + auto ret = nix_path_info_get_references( + ctx, info, &refData, [](nix_c_context *, void * user_data, const StorePath * refPath) { + static_cast(user_data)->refs->insert(refPath->path); + }); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + ASSERT_EQ(refs, mutInfo->references); + + nix::Strings sigs; + + struct SignatureCallbackData + { + nix::Strings * sigs; + }; + + SignatureCallbackData sigData{&sigs}; + ret = nix_path_info_get_sigs( + ctx, info, &sigData, [](nix_c_context *, void * user_data, const char * sig, unsigned int sig_len) { + static_cast(user_data)->sigs->emplace_back(sig, sig_len); + }); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + ASSERT_EQ(sigs, nix::Signature::toStrings(mutInfo->sigs)); + + nix_path_info_free(info); +} + +TEST_F(NixApiStoreTestWithRealisedPath, nix_path_info_deriver_absent) +{ + nix_path_info * info = nix_store_query_path_info(ctx, store, drvPath); + assert_ctx_ok(); + ASSERT_NE(info, nullptr); + + StorePath * deriver = nix_path_info_get_deriver(ctx, info); + assert_ctx_ok(); + ASSERT_EQ(deriver, nullptr); + + nix_path_info_free(info); +} + +TEST_F(NixApiStoreTestWithRealisedPath, nix_path_info_get_ca_absent) +{ + auto cppInfo = store->ptr->queryPathInfo(outPath->path); + auto mutInfo = std::make_shared(*cppInfo); + mutInfo->ca = std::nullopt; + auto * info = new nix_path_info{nix::ref(mutInfo)}; + + bool callbackCalled = false; + auto ret = nix_path_info_get_ca( + ctx, info, [](const char *, unsigned int, void * ud) { *static_cast(ud) = true; }, &callbackCalled); + ASSERT_EQ(ret, NIX_ERR_KEY); + ASSERT_EQ(nix_err_code(ctx), NIX_ERR_KEY); + ASSERT_FALSE(callbackCalled); + + nix_path_info_free(info); +} + +TEST_F(NixApiStoreTestWithRealisedPath, nix_path_info_null_callbacks) +{ + nix_path_info * info = nix_store_query_path_info(ctx, store, outPath); + assert_ctx_ok(); + ASSERT_NE(info, nullptr); + + auto ret = nix_path_info_get_references(ctx, info, nullptr, nullptr); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + + ret = nix_path_info_get_sigs(ctx, info, nullptr, nullptr); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + + // outPath is content-addressed, so it succeeds, but lackign a callback, it + // ignores the value of it. + ret = nix_path_info_get_ca(ctx, info, nullptr, nullptr); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + + std::string ca; + ret = nix_path_info_get_ca(ctx, info, OBSERVE_STRING(ca)); + // The fixture provides a straightforward NAR-hashed output. + // Other CA paths may use a different method and prefix. + // Since the derivation has no impurities and a constant output, we can + // simply check the whole thing in one go: + ASSERT_EQ(ca, "fixed:r:sha256:1i89icvvs2f3cym00414i3bbl1qidhg0b5yrmdlx9cjkj5is6ljg"); + + nix_path_info_free(info); +} + } // namespace nixC diff --git a/src/libutil-c/nix_api_util.h b/src/libutil-c/nix_api_util.h index b48d9166d4be..30862cb97f09 100644 --- a/src/libutil-c/nix_api_util.h +++ b/src/libutil-c/nix_api_util.h @@ -323,7 +323,8 @@ nix_err nix_err_code(const nix_c_context * read_context); /** * @brief Set an error message on a nix context. * - * This should be used when you want to throw an error from a PrimOp callback. + * Use this to report an error from a callback that receives a context, + * such as a PrimOp or iterator callback. * * All other use is internal to the API. *