From 9c04c629e5e39c7ec55bbaf3590bf3b553faa2c2 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 6 Nov 2025 16:51:19 -0500 Subject: [PATCH 1/2] `UnkeyedValidPathInfo::fromJSON` Remove support for older version It turns out this code path is only used for unit tests (to ensure our JSON formats are possible to parse by other code, elsewhere). No user-facing functionality consumes this format. Therefore, let's drop the old version parsing support. --- doc/manual/rl-next/json-format-changes.md | 2 +- src/libstore/derivation-options.cc | 9 ------- src/libstore/path-info.cc | 30 ++++++++-------------- src/libutil/include/nix/util/json-utils.hh | 9 +++++++ 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/doc/manual/rl-next/json-format-changes.md b/doc/manual/rl-next/json-format-changes.md index c5518ee1b..ef442c27e 100644 --- a/doc/manual/rl-next/json-format-changes.md +++ b/doc/manual/rl-next/json-format-changes.md @@ -22,7 +22,7 @@ The store path info JSON format has been updated from version 1 to version 2: - New: `"ca": {"method": "nar", "hash": {"algorithm": "sha256", "format": "base64", "hash": "EMIJ+giQ..."}}` - Still `null` values for input-addressed store objects -Version 1 format is still accepted when reading for backward compatibility. +Nix currently only produces, and doesn't consume this format. **Affected command**: `nix path-info --json` diff --git a/src/libstore/derivation-options.cc b/src/libstore/derivation-options.cc index 75313841c..265f28e80 100644 --- a/src/libstore/derivation-options.cc +++ b/src/libstore/derivation-options.cc @@ -423,15 +423,6 @@ void adl_serializer::to_json(json & json, const DerivationOpt json["allowSubstitutes"] = o.allowSubstitutes; } -template -static inline std::optional ptrToOwned(const json * ptr) -{ - if (ptr) - return std::optional{*ptr}; - else - return std::nullopt; -} - DerivationOptions::OutputChecks adl_serializer::from_json(const json & json_) { auto & json = getObject(json_); diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index c535d08f4..7d8bf4911 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -192,13 +192,10 @@ UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON(const StoreDirConfig & store auto & json = getObject(_json); - // Check version (optional for backward compatibility) - nlohmann::json::number_unsigned_t version = 1; - if (json.contains("version")) { - version = getUnsigned(valueAt(json, "version")); - if (version != 1 && version != 2) { - throw Error("Unsupported path info JSON format version %d, expected 1 through 2", version); - } + { + auto version = getUnsigned(valueAt(json, "version")); + if (version != 2) + throw Error("Unsupported path info JSON format version %d, only version 2 is currently supported", version); } res.narHash = Hash::parseAny(getString(valueAt(json, "narHash")), std::nullopt); @@ -213,19 +210,12 @@ UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON(const StoreDirConfig & store throw; } - // New format as this as nullable but mandatory field; handling - // missing is for back-compat. - if (auto * rawCa0 = optionalValueAt(json, "ca")) - if (auto * rawCa = getNullable(*rawCa0)) - switch (version) { - case 1: - // old string format also used in SQLite DB and .narinfo - res.ca = ContentAddress::parse(getString(*rawCa)); - break; - case 2 ... std::numeric_limits::max(): - res.ca = *rawCa; - break; - } + try { + res.ca = ptrToOwned(getNullable(valueAt(json, "ca"))); + } catch (Error & e) { + e.addTrace({}, "while reading key 'ca'"); + throw; + } if (auto * rawDeriver0 = optionalValueAt(json, "deriver")) if (auto * rawDeriver = getNullable(*rawDeriver0)) diff --git a/src/libutil/include/nix/util/json-utils.hh b/src/libutil/include/nix/util/json-utils.hh index 7a3fe4f36..ec513ca25 100644 --- a/src/libutil/include/nix/util/json-utils.hh +++ b/src/libutil/include/nix/util/json-utils.hh @@ -114,4 +114,13 @@ struct adl_serializer> } }; +template +static inline std::optional ptrToOwned(const json * ptr) +{ + if (ptr) + return std::optional{*ptr}; + else + return std::nullopt; +} + } // namespace nlohmann From 4f1c8f62c38c5f9325eaa342cc0e625d45703a35 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 6 Nov 2025 17:07:15 -0500 Subject: [PATCH 2/2] Futher cleans up store object info JSON v2 Since we haven't released v2 yet (2.32 has v1) we can just update this in-place and avoid version churn. Note that as a nice side effect of using the standard `Hash` JSON impl, we don't neeed this `hashFormat` parameter anymore. --- doc/manual/rl-next/json-format-changes.md | 8 +++++ .../json/schema/store-object-info-v2.yaml | 4 +-- src/libstore-tests/data/nar-info/impure.json | 12 +++++-- src/libstore-tests/data/nar-info/pure.json | 6 +++- .../data/path-info/empty_impure.json | 6 +++- .../data/path-info/empty_pure.json | 6 +++- src/libstore-tests/data/path-info/impure.json | 6 +++- src/libstore-tests/data/path-info/pure.json | 6 +++- src/libstore-tests/nar-info.cc | 36 +++++++++---------- src/libstore-tests/path-info.cc | 2 +- src/libstore/include/nix/store/nar-info.hh | 2 +- src/libstore/include/nix/store/path-info.hh | 2 +- src/libstore/nar-info.cc | 22 ++++++------ src/libstore/path-info.cc | 7 ++-- src/nix/path-info.cc | 2 +- tests/functional/path-info.sh | 12 +++++-- 16 files changed, 91 insertions(+), 48 deletions(-) diff --git a/doc/manual/rl-next/json-format-changes.md b/doc/manual/rl-next/json-format-changes.md index ef442c27e..bd7e11243 100644 --- a/doc/manual/rl-next/json-format-changes.md +++ b/doc/manual/rl-next/json-format-changes.md @@ -22,6 +22,14 @@ The store path info JSON format has been updated from version 1 to version 2: - New: `"ca": {"method": "nar", "hash": {"algorithm": "sha256", "format": "base64", "hash": "EMIJ+giQ..."}}` - Still `null` values for input-addressed store objects +- **Structured hash fields**: + + Hash values (`narHash` and `downloadHash`) are now structured JSON objects instead of strings: + + - Old: `"narHash": "sha256:FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="` + - New: `"narHash": {"algorithm": "sha256", "format": "base64", "hash": "FePFYIlM..."}` + - Same structure applies to `downloadHash` in NAR info contexts + Nix currently only produces, and doesn't consume this format. **Affected command**: `nix path-info --json` diff --git a/doc/manual/source/protocols/json/schema/store-object-info-v2.yaml b/doc/manual/source/protocols/json/schema/store-object-info-v2.yaml index 4f442e0c3..44d9e5eae 100644 --- a/doc/manual/source/protocols/json/schema/store-object-info-v2.yaml +++ b/doc/manual/source/protocols/json/schema/store-object-info-v2.yaml @@ -71,7 +71,7 @@ $defs: Note: This field may not be present in all contexts, such as when the path is used as the key and the the store object info the value in map. narHash: - type: string + "$ref": "./hash-v1.yaml" title: NAR Hash description: | Hash of the [file system object](@docroot@/store/file-system-object.md) part of the store object when serialized as a [Nix Archive](@docroot@/store/file-system-object/content-address.md#serial-nix-archive). @@ -229,7 +229,7 @@ $defs: > This is an impure "`.narinfo`" field that may not be included in certain contexts. downloadHash: - type: string + "$ref": "./hash-v1.yaml" title: Download Hash description: | A digest for the compressed archive itself, as opposed to the data contained within. diff --git a/src/libstore-tests/data/nar-info/impure.json b/src/libstore-tests/data/nar-info/impure.json index f35ff990b..13cfa8639 100644 --- a/src/libstore-tests/data/nar-info/impure.json +++ b/src/libstore-tests/data/nar-info/impure.json @@ -9,9 +9,17 @@ }, "compression": "xz", "deriver": "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", - "downloadHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", + "downloadHash": { + "algorithm": "sha256", + "format": "base64", + "hash": "FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=" + }, "downloadSize": 4029176, - "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", + "narHash": { + "algorithm": "sha256", + "format": "base64", + "hash": "FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=" + }, "narSize": 34878, "references": [ "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", diff --git a/src/libstore-tests/data/nar-info/pure.json b/src/libstore-tests/data/nar-info/pure.json index 2c5cb3bde..470f92da9 100644 --- a/src/libstore-tests/data/nar-info/pure.json +++ b/src/libstore-tests/data/nar-info/pure.json @@ -7,7 +7,11 @@ }, "method": "nar" }, - "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", + "narHash": { + "algorithm": "sha256", + "format": "base64", + "hash": "FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=" + }, "narSize": 34878, "references": [ "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", diff --git a/src/libstore-tests/data/path-info/empty_impure.json b/src/libstore-tests/data/path-info/empty_impure.json index 381acaa03..2fcd2078c 100644 --- a/src/libstore-tests/data/path-info/empty_impure.json +++ b/src/libstore-tests/data/path-info/empty_impure.json @@ -1,7 +1,11 @@ { "ca": null, "deriver": null, - "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", + "narHash": { + "algorithm": "sha256", + "format": "base64", + "hash": "FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=" + }, "narSize": 0, "references": [], "registrationTime": null, diff --git a/src/libstore-tests/data/path-info/empty_pure.json b/src/libstore-tests/data/path-info/empty_pure.json index 6d3fa646b..365e2f646 100644 --- a/src/libstore-tests/data/path-info/empty_pure.json +++ b/src/libstore-tests/data/path-info/empty_pure.json @@ -1,6 +1,10 @@ { "ca": null, - "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", + "narHash": { + "algorithm": "sha256", + "format": "base64", + "hash": "FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=" + }, "narSize": 0, "references": [], "version": 2 diff --git a/src/libstore-tests/data/path-info/impure.json b/src/libstore-tests/data/path-info/impure.json index 141b38a16..5e9944e5a 100644 --- a/src/libstore-tests/data/path-info/impure.json +++ b/src/libstore-tests/data/path-info/impure.json @@ -8,7 +8,11 @@ "method": "nar" }, "deriver": "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", - "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", + "narHash": { + "algorithm": "sha256", + "format": "base64", + "hash": "FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=" + }, "narSize": 34878, "references": [ "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", diff --git a/src/libstore-tests/data/path-info/pure.json b/src/libstore-tests/data/path-info/pure.json index 2c5cb3bde..470f92da9 100644 --- a/src/libstore-tests/data/path-info/pure.json +++ b/src/libstore-tests/data/path-info/pure.json @@ -7,7 +7,11 @@ }, "method": "nar" }, - "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", + "narHash": { + "algorithm": "sha256", + "format": "base64", + "hash": "FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=" + }, "narSize": 34878, "references": [ "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", diff --git a/src/libstore-tests/nar-info.cc b/src/libstore-tests/nar-info.cc index 751c5e305..41faa9274 100644 --- a/src/libstore-tests/nar-info.cc +++ b/src/libstore-tests/nar-info.cc @@ -59,24 +59,24 @@ static NarInfo makeNarInfo(const Store & store, bool includeImpureInfo) return info; } -#define JSON_TEST(STEM, PURE) \ - TEST_F(NarInfoTest, NarInfo_##STEM##_from_json) \ - { \ - readTest(#STEM, [&](const auto & encoded_) { \ - auto encoded = json::parse(encoded_); \ - auto expected = makeNarInfo(*store, PURE); \ - NarInfo got = NarInfo::fromJSON(*store, expected.path, encoded); \ - ASSERT_EQ(got, expected); \ - }); \ - } \ - \ - TEST_F(NarInfoTest, NarInfo_##STEM##_to_json) \ - { \ - writeTest( \ - #STEM, \ - [&]() -> json { return makeNarInfo(*store, PURE).toJSON(*store, PURE, HashFormat::SRI); }, \ - [](const auto & file) { return json::parse(readFile(file)); }, \ - [](const auto & file, const auto & got) { return writeFile(file, got.dump(2) + "\n"); }); \ +#define JSON_TEST(STEM, PURE) \ + TEST_F(NarInfoTest, NarInfo_##STEM##_from_json) \ + { \ + readTest(#STEM, [&](const auto & encoded_) { \ + auto encoded = json::parse(encoded_); \ + auto expected = makeNarInfo(*store, PURE); \ + NarInfo got = NarInfo::fromJSON(*store, expected.path, encoded); \ + ASSERT_EQ(got, expected); \ + }); \ + } \ + \ + TEST_F(NarInfoTest, NarInfo_##STEM##_to_json) \ + { \ + writeTest( \ + #STEM, \ + [&]() -> json { return makeNarInfo(*store, PURE).toJSON(*store, PURE); }, \ + [](const auto & file) { return json::parse(readFile(file)); }, \ + [](const auto & file, const auto & got) { return writeFile(file, got.dump(2) + "\n"); }); \ } JSON_TEST(pure, false) diff --git a/src/libstore-tests/path-info.cc b/src/libstore-tests/path-info.cc index 63310c1c3..a40b26149 100644 --- a/src/libstore-tests/path-info.cc +++ b/src/libstore-tests/path-info.cc @@ -80,7 +80,7 @@ static UnkeyedValidPathInfo makeFull(const Store & store, bool includeImpureInfo { \ writeTest( \ #STEM, \ - [&]() -> json { return OBJ.toJSON(*store, PURE, HashFormat::SRI); }, \ + [&]() -> json { return OBJ.toJSON(*store, PURE); }, \ [](const auto & file) { return json::parse(readFile(file)); }, \ [](const auto & file, const auto & got) { return writeFile(file, got.dump(2) + "\n"); }); \ } diff --git a/src/libstore/include/nix/store/nar-info.hh b/src/libstore/include/nix/store/nar-info.hh index 1684837c6..34606a89b 100644 --- a/src/libstore/include/nix/store/nar-info.hh +++ b/src/libstore/include/nix/store/nar-info.hh @@ -42,7 +42,7 @@ struct NarInfo : ValidPathInfo std::string to_string(const StoreDirConfig & store) const; - nlohmann::json toJSON(const StoreDirConfig & store, bool includeImpureInfo, HashFormat hashFormat) const override; + nlohmann::json toJSON(const StoreDirConfig & store, bool includeImpureInfo) const override; static NarInfo fromJSON(const StoreDirConfig & store, const StorePath & path, const nlohmann::json & json); }; diff --git a/src/libstore/include/nix/store/path-info.hh b/src/libstore/include/nix/store/path-info.hh index cbc5abdb4..0f00a14b7 100644 --- a/src/libstore/include/nix/store/path-info.hh +++ b/src/libstore/include/nix/store/path-info.hh @@ -117,7 +117,7 @@ struct UnkeyedValidPathInfo * @param includeImpureInfo If true, variable elements such as the * registration time are included. */ - virtual nlohmann::json toJSON(const StoreDirConfig & store, bool includeImpureInfo, HashFormat hashFormat) const; + virtual nlohmann::json toJSON(const StoreDirConfig & store, bool includeImpureInfo) const; static UnkeyedValidPathInfo fromJSON(const StoreDirConfig & store, const nlohmann::json & json); }; diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc index 6f1abb273..4d4fb7de2 100644 --- a/src/libstore/nar-info.cc +++ b/src/libstore/nar-info.cc @@ -130,11 +130,11 @@ std::string NarInfo::to_string(const StoreDirConfig & store) const return res; } -nlohmann::json NarInfo::toJSON(const StoreDirConfig & store, bool includeImpureInfo, HashFormat hashFormat) const +nlohmann::json NarInfo::toJSON(const StoreDirConfig & store, bool includeImpureInfo) const { using nlohmann::json; - auto jsonObject = ValidPathInfo::toJSON(store, includeImpureInfo, hashFormat); + auto jsonObject = ValidPathInfo::toJSON(store, includeImpureInfo); if (includeImpureInfo) { if (!url.empty()) @@ -142,7 +142,7 @@ nlohmann::json NarInfo::toJSON(const StoreDirConfig & store, bool includeImpureI if (!compression.empty()) jsonObject["compression"] = compression; if (fileHash) - jsonObject["downloadHash"] = fileHash->to_string(hashFormat, true); + jsonObject["downloadHash"] = *fileHash; if (fileSize) jsonObject["downloadSize"] = fileSize; } @@ -161,17 +161,17 @@ NarInfo NarInfo::fromJSON(const StoreDirConfig & store, const StorePath & path, auto & obj = getObject(json); - if (json.contains("url")) - res.url = getString(valueAt(obj, "url")); + if (auto * url = get(obj, "url")) + res.url = getString(*url); - if (json.contains("compression")) - res.compression = getString(valueAt(obj, "compression")); + if (auto * compression = get(obj, "compression")) + res.compression = getString(*compression); - if (json.contains("downloadHash")) - res.fileHash = Hash::parseAny(getString(valueAt(obj, "downloadHash")), std::nullopt); + if (auto * downloadHash = get(obj, "downloadHash")) + res.fileHash = *downloadHash; - if (json.contains("downloadSize")) - res.fileSize = getUnsigned(valueAt(obj, "downloadSize")); + if (auto * downloadSize = get(obj, "downloadSize")) + res.fileSize = getUnsigned(*downloadSize); return res; } diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index 7d8bf4911..811c397a4 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -149,8 +149,7 @@ ValidPathInfo ValidPathInfo::makeFromCA( return res; } -nlohmann::json -UnkeyedValidPathInfo::toJSON(const StoreDirConfig & store, bool includeImpureInfo, HashFormat hashFormat) const +nlohmann::json UnkeyedValidPathInfo::toJSON(const StoreDirConfig & store, bool includeImpureInfo) const { using nlohmann::json; @@ -158,7 +157,7 @@ UnkeyedValidPathInfo::toJSON(const StoreDirConfig & store, bool includeImpureInf jsonObject["version"] = 2; - jsonObject["narHash"] = narHash.to_string(hashFormat, true); + jsonObject["narHash"] = narHash; jsonObject["narSize"] = narSize; { @@ -198,7 +197,7 @@ UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON(const StoreDirConfig & store throw Error("Unsupported path info JSON format version %d, only version 2 is currently supported", version); } - res.narHash = Hash::parseAny(getString(valueAt(json, "narHash")), std::nullopt); + res.narHash = valueAt(json, "narHash"); res.narSize = getUnsigned(valueAt(json, "narSize")); try { diff --git a/src/nix/path-info.cc b/src/nix/path-info.cc index 146b775e5..697b73e5c 100644 --- a/src/nix/path-info.cc +++ b/src/nix/path-info.cc @@ -51,7 +51,7 @@ static json pathInfoToJSON(Store & store, const StorePathSet & storePaths, bool // know the name yet until we've read the NAR info. printedStorePath = store.printStorePath(info->path); - jsonObject = info->toJSON(store, true, HashFormat::SRI); + jsonObject = info->toJSON(store, true); if (showClosureSize) { StorePathSet closure; diff --git a/tests/functional/path-info.sh b/tests/functional/path-info.sh index 463ac6214..70ad1a7aa 100755 --- a/tests/functional/path-info.sh +++ b/tests/functional/path-info.sh @@ -17,8 +17,16 @@ diff --unified --color=always \ jq --sort-keys 'map_values(.narHash)') \ <(jq --sort-keys <<-EOF { - "$foo": "sha256-QvtAMbUl/uvi+LCObmqOhvNOapHdA2raiI4xG5zI5pA=", - "$bar": "sha256-9fhYGu9fqxcQC2Kc81qh2RMo1QcLBUBo8U+pPn+jthQ=", + "$foo": { + "algorithm": "sha256", + "format": "base64", + "hash": "QvtAMbUl/uvi+LCObmqOhvNOapHdA2raiI4xG5zI5pA=" + }, + "$bar": { + "algorithm": "sha256", + "format": "base64", + "hash": "9fhYGu9fqxcQC2Kc81qh2RMo1QcLBUBo8U+pPn+jthQ=" + }, "$baz": null } EOF