Skip to content

Commit a568edd

Browse files
Ericson2314RossComputerGuyroberth
committed
libstore-c: Add new derivation and store path functions
Add several new functions to the C API: StorePath operations: - nix_store_path_hash: Extract the hash part from a store path - nix_store_create_from_parts: Construct a store path from hash and name Derivation operations: - nix_derivation_clone: Clone a derivation - nix_derivation_to_json: Serialize a derivation to JSON Store operations: - nix_store_drv_from_store_path: Load a derivation from a store path Test the new functions, and improve documentation of some existing functions to better distinguish them, also. Co-authored-by: Tristan Ross <[email protected]> Co-authored-by: Robert Hensing <[email protected]>
1 parent 7f3f8f1 commit a568edd

File tree

5 files changed

+305
-1
lines changed

5 files changed

+305
-1
lines changed

src/libstore-c/nix_api_store.cc

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
#include "nix/store/local-fs-store.hh"
1111

1212
#include "nix/store/globals.hh"
13+
#include "nix/util/base-nix-32.hh"
14+
15+
#include <cstring>
16+
#include <span>
1317

1418
extern "C" {
1519

@@ -218,6 +222,47 @@ StorePath * nix_store_path_clone(const StorePath * p)
218222
return new StorePath{p->path};
219223
}
220224

225+
nix_err
226+
nix_store_path_hash(nix_c_context * context, const StorePath * store_path, nix_store_path_hash_part * hash_part_out)
227+
{
228+
try {
229+
auto hashPart = store_path->path.hashPart();
230+
// Decode from Nix32 (base32) encoding to raw bytes
231+
auto decoded = nix::BaseNix32::decode(hashPart);
232+
233+
assert(decoded.size() == 20);
234+
std::memcpy(hash_part_out->bytes, decoded.data(), 20);
235+
return NIX_OK;
236+
}
237+
NIXC_CATCH_ERRS
238+
}
239+
240+
StorePath * nix_store_create_from_parts(
241+
nix_c_context * context, const nix_store_path_hash_part * hash, const char * name, size_t name_len)
242+
{
243+
if (context)
244+
context->last_err_code = NIX_OK;
245+
try {
246+
// Encode the 20 raw bytes to Nix32 (base32) format
247+
auto hashStr =
248+
nix::BaseNix32::encode(std::span<const std::byte>(reinterpret_cast<const std::byte *>(hash->bytes), 20));
249+
250+
// Construct the store path basename: <hash>-<name>
251+
std::string baseName;
252+
baseName += hashStr;
253+
baseName += "-";
254+
baseName += std::string_view{name, name_len};
255+
256+
return new StorePath{nix::StorePath(std::move(baseName))};
257+
}
258+
NIXC_CATCH_ERRS_NULL
259+
}
260+
261+
nix_derivation * nix_derivation_clone(const nix_derivation * d)
262+
{
263+
return new nix_derivation{d->drv};
264+
}
265+
221266
nix_derivation * nix_derivation_from_json(nix_c_context * context, Store * store, const char * json)
222267
{
223268
if (context)
@@ -228,6 +273,20 @@ nix_derivation * nix_derivation_from_json(nix_c_context * context, Store * store
228273
NIXC_CATCH_ERRS_NULL
229274
}
230275

276+
nix_err nix_derivation_to_json(
277+
nix_c_context * context, const nix_derivation * drv, nix_get_string_callback callback, void * userdata)
278+
{
279+
if (context)
280+
context->last_err_code = NIX_OK;
281+
try {
282+
auto result = static_cast<nlohmann::json>(drv->drv).dump();
283+
if (callback) {
284+
callback(result.data(), result.size(), userdata);
285+
}
286+
}
287+
NIXC_CATCH_ERRS
288+
}
289+
231290
StorePath * nix_add_derivation(nix_c_context * context, Store * store, nix_derivation * derivation)
232291
{
233292
if (context)
@@ -252,4 +311,14 @@ nix_err nix_store_copy_closure(nix_c_context * context, Store * srcStore, Store
252311
NIXC_CATCH_ERRS
253312
}
254313

314+
nix_derivation * nix_store_drv_from_store_path(nix_c_context * context, Store * store, const StorePath * path)
315+
{
316+
if (context)
317+
context->last_err_code = NIX_OK;
318+
try {
319+
return new nix_derivation{store->ptr->derivationFromPath(path->path)};
320+
}
321+
NIXC_CATCH_ERRS_NULL
322+
}
323+
255324
} // extern "C"

src/libstore-c/nix_api_store.h

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ nix_err
106106
nix_store_get_storedir(nix_c_context * context, Store * store, nix_get_string_callback callback, void * user_data);
107107

108108
/**
109-
* @brief Parse a Nix store path into a StorePath
109+
* @brief Parse a Nix store path that includes the store dir into a StorePath
110110
*
111111
* @note Don't forget to free this path using nix_store_path_free()!
112112
* @param[out] context Optional, stores error information
@@ -188,6 +188,12 @@ nix_store_get_version(nix_c_context * context, Store * store, nix_get_string_cal
188188
/**
189189
* @brief Create a `nix_derivation` from a JSON representation of that derivation.
190190
*
191+
* @note Unlike `nix_derivation_to_json`, this needs a `Store`. This is because
192+
* over time we expect the internal representation of derivations in Nix to
193+
* differ from accepted derivation formats. The store argument is here to help
194+
* any logic needed to convert from JSON to the internal representation, in
195+
* excess of just parsing.
196+
*
191197
* @param[out] context Optional, stores error information.
192198
* @param[in] store nix store reference.
193199
* @param[in] json JSON of the derivation as a string.
@@ -242,6 +248,16 @@ nix_err nix_store_get_fs_closure(
242248
void * userdata,
243249
void (*callback)(nix_c_context * context, void * userdata, const StorePath * store_path));
244250

251+
/**
252+
* @brief Returns the derivation associated with the store path
253+
*
254+
* @param[out] context Optional, stores error information
255+
* @param[in] store The nix store
256+
* @param[in] path The nix store path
257+
* @return A new derivation, or NULL on error
258+
*/
259+
nix_derivation * nix_store_drv_from_store_path(nix_c_context * context, Store * store, const StorePath * path);
260+
245261
// cffi end
246262
#ifdef __cplusplus
247263
}

src/libstore-c/nix_api_store/derivation.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ extern "C" {
2020
/** @brief Nix Derivation */
2121
typedef struct nix_derivation nix_derivation;
2222

23+
/**
24+
* @brief Copy a `nix_derivation`
25+
*
26+
* @param[in] d the derivation to copy
27+
* @return a new `nix_derivation`
28+
*/
29+
nix_derivation * nix_derivation_clone(const nix_derivation * d);
30+
2331
/**
2432
* @brief Deallocate a `nix_derivation`
2533
*
@@ -28,6 +36,17 @@ typedef struct nix_derivation nix_derivation;
2836
*/
2937
void nix_derivation_free(nix_derivation * drv);
3038

39+
/**
40+
* @brief Gets the derivation as a JSON string
41+
*
42+
* @param[out] context Optional, stores error information
43+
* @param[in] drv The derivation
44+
* @param[in] callback Called with the JSON string
45+
* @param[in] userdata Arbitrary data passed to the callback
46+
*/
47+
nix_err nix_derivation_to_json(
48+
nix_c_context * context, const nix_derivation * drv, nix_get_string_callback callback, void * userdata);
49+
3150
// cffi end
3251
#ifdef __cplusplus
3352
}

src/libstore-c/nix_api_store/store_path.h

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
* @brief Store path operations
1111
*/
1212

13+
#include <stddef.h>
14+
#include <stdint.h>
15+
1316
#include "nix_api_util.h"
1417

1518
#ifdef __cplusplus
@@ -44,6 +47,45 @@ void nix_store_path_free(StorePath * p);
4447
*/
4548
void nix_store_path_name(const StorePath * store_path, nix_get_string_callback callback, void * user_data);
4649

50+
/**
51+
* @brief A store path hash
52+
*
53+
* Once decoded from "nix32" encoding, a store path hash is 20 raw bytes.
54+
*/
55+
typedef struct nix_store_path_hash_part
56+
{
57+
uint8_t bytes[20];
58+
} nix_store_path_hash_part;
59+
60+
/**
61+
* @brief Get the path hash (e.g. "<hash>" in /nix/store/<hash>-<name>)
62+
*
63+
* The hash is returned as raw bytes, decoded from "nix32" encoding.
64+
*
65+
* @param[out] context Optional, stores error information
66+
* @param[in] store_path the path to get the hash from
67+
* @param[out] hash_part_out the decoded hash as 20 raw bytes
68+
* @return NIX_OK on success, error code on failure
69+
*/
70+
nix_err
71+
nix_store_path_hash(nix_c_context * context, const StorePath * store_path, nix_store_path_hash_part * hash_part_out);
72+
73+
/**
74+
* @brief Create a StorePath from its constituent parts (hash and name)
75+
*
76+
* This function constructs a store path from a hash and name, without needing
77+
* a Store reference or the store directory prefix.
78+
*
79+
* @note Don't forget to free this path using nix_store_path_free()!
80+
* @param[out] context Optional, stores error information
81+
* @param[in] hash The store path hash (20 raw bytes)
82+
* @param[in] name The store path name (the part after the hash)
83+
* @param[in] name_len Length of the name string
84+
* @return owned store path, NULL on error
85+
*/
86+
StorePath * nix_store_create_from_parts(
87+
nix_c_context * context, const nix_store_path_hash_part * hash, const char name[/*name_len*/], size_t name_len);
88+
4789
// cffi end
4890
#ifdef __cplusplus
4991
}

src/libstore-tests/nix_api_store.cc

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include <fstream>
22

3+
#include <nlohmann/json.hpp>
4+
35
#include "nix_api_util.h"
46
#include "nix_api_store.h"
57

@@ -92,6 +94,69 @@ TEST_F(nix_api_store_test, DoesNotCrashWhenContextIsNull)
9294
nix_store_path_free(path);
9395
}
9496

97+
TEST_F(nix_api_store_test, nix_store_path_hash)
98+
{
99+
StorePath * path = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str());
100+
ASSERT_NE(path, nullptr);
101+
102+
nix_store_path_hash_part hash;
103+
auto ret = nix_store_path_hash(ctx, path, &hash);
104+
assert_ctx_ok();
105+
ASSERT_EQ(ret, NIX_OK);
106+
107+
// Verify it's 20 bytes
108+
static_assert(sizeof(hash.bytes) == 20);
109+
110+
// The hash should be non-zero
111+
bool allZero = true;
112+
for (int i = 0; i < 20; i++) {
113+
if (hash.bytes[i] != 0) {
114+
allZero = false;
115+
break;
116+
}
117+
}
118+
ASSERT_FALSE(allZero);
119+
120+
nix_store_path_free(path);
121+
}
122+
123+
TEST_F(nix_api_store_test, nix_store_create_from_parts_roundtrip)
124+
{
125+
// Parse a path
126+
StorePath * original = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str());
127+
EXPECT_NE(original, nullptr);
128+
129+
// Get its hash
130+
nix_store_path_hash_part hash;
131+
auto ret = nix_store_path_hash(ctx, original, &hash);
132+
assert_ctx_ok();
133+
ASSERT_EQ(ret, NIX_OK);
134+
135+
// Get its name
136+
std::string name;
137+
nix_store_path_name(original, OBSERVE_STRING(name));
138+
139+
// Reconstruct from parts
140+
StorePath * reconstructed = nix_store_create_from_parts(ctx, &hash, name.c_str(), name.size());
141+
assert_ctx_ok();
142+
ASSERT_NE(reconstructed, nullptr);
143+
144+
// Should be equal
145+
EXPECT_EQ(original->path, reconstructed->path);
146+
147+
nix_store_path_free(original);
148+
nix_store_path_free(reconstructed);
149+
}
150+
151+
TEST_F(nix_api_store_test, nix_store_create_from_parts_invalid_name)
152+
{
153+
nix_store_path_hash_part hash = {};
154+
// Invalid name with spaces
155+
StorePath * path = nix_store_create_from_parts(ctx, &hash, "invalid name", 12);
156+
ASSERT_EQ(path, nullptr);
157+
ASSERT_EQ(nix_err_code(ctx), NIX_ERR_NIX_ERROR);
158+
}
159+
95160
TEST_F(nix_api_store_test, get_version)
96161
{
97162
std::string str;
@@ -795,4 +860,97 @@ TEST_F(NixApiStoreTestWithRealisedPath, nix_store_get_fs_closure_error_propagati
795860
ASSERT_EQ(call_count, 1); // Should have been called exactly once, then aborted
796861
}
797862

863+
/**
864+
* @brief Helper function to load JSON from a test data file
865+
*
866+
* @param filename Relative path from _NIX_TEST_UNIT_DATA
867+
* @return JSON string contents of the file
868+
*/
869+
static std::string load_json_from_test_data(const char * filename)
870+
{
871+
std::filesystem::path unitTestData{getenv("_NIX_TEST_UNIT_DATA")};
872+
std::ifstream t{unitTestData / filename};
873+
std::stringstream buffer;
874+
buffer << t.rdbuf();
875+
return buffer.str();
876+
}
877+
878+
TEST_F(nix_api_store_test, nix_derivation_to_json_roundtrip)
879+
{
880+
// Load JSON from test data
881+
auto originalJson = load_json_from_test_data("derivation/invariants/filled-in-deferred-empty-env-var-pre.json");
882+
883+
// Parse to derivation
884+
auto * drv = nix_derivation_from_json(ctx, store, originalJson.c_str());
885+
assert_ctx_ok();
886+
ASSERT_NE(drv, nullptr);
887+
888+
// Convert back to JSON
889+
std::string convertedJson;
890+
auto ret = nix_derivation_to_json(ctx, drv, OBSERVE_STRING(convertedJson));
891+
assert_ctx_ok();
892+
ASSERT_EQ(ret, NIX_OK);
893+
ASSERT_FALSE(convertedJson.empty());
894+
895+
// Parse both JSON strings to compare (ignoring whitespace differences)
896+
auto originalParsed = nlohmann::json::parse(originalJson);
897+
auto convertedParsed = nlohmann::json::parse(convertedJson);
898+
899+
// Remove parts that will be different due to filling-in.
900+
originalParsed.at("outputs").erase("out");
901+
originalParsed.at("env").erase("out");
902+
convertedParsed.at("outputs").erase("out");
903+
convertedParsed.at("env").erase("out");
904+
905+
// They should be equivalent
906+
ASSERT_EQ(originalParsed, convertedParsed);
907+
908+
nix_derivation_free(drv);
909+
}
910+
911+
TEST_F(nix_api_store_test, nix_derivation_store_round_trip)
912+
{
913+
// Load a derivation from JSON
914+
auto json = load_json_from_test_data("derivation/invariants/filled-in-deferred-empty-env-var-pre.json");
915+
auto * drv = nix_derivation_from_json(ctx, store, json.c_str());
916+
assert_ctx_ok();
917+
ASSERT_NE(drv, nullptr);
918+
919+
// Add to store
920+
auto * drvPath = nix_add_derivation(ctx, store, drv);
921+
assert_ctx_ok();
922+
ASSERT_NE(drvPath, nullptr);
923+
924+
// Retrieve from store
925+
auto * drv2 = nix_store_drv_from_store_path(ctx, store, drvPath);
926+
assert_ctx_ok();
927+
ASSERT_NE(drv2, nullptr);
928+
929+
// The round trip should make the same derivation
930+
ASSERT_EQ(drv->drv, drv2->drv);
931+
932+
nix_store_path_free(drvPath);
933+
nix_derivation_free(drv);
934+
nix_derivation_free(drv2);
935+
}
936+
937+
TEST_F(nix_api_store_test, nix_derivation_clone)
938+
{
939+
// Load a derivation from JSON
940+
auto json = load_json_from_test_data("derivation/invariants/filled-in-deferred-empty-env-var-pre.json");
941+
auto * drv = nix_derivation_from_json(ctx, store, json.c_str());
942+
assert_ctx_ok();
943+
ASSERT_NE(drv, nullptr);
944+
945+
// Clone the derivation
946+
auto * drv2 = nix_derivation_clone(drv);
947+
ASSERT_NE(drv2, nullptr);
948+
949+
// The clone should be equal
950+
ASSERT_EQ(drv->drv, drv2->drv);
951+
952+
nix_derivation_free(drv);
953+
nix_derivation_free(drv2);
954+
}
955+
798956
} // namespace nixC

0 commit comments

Comments
 (0)