diff --git a/.gitignore b/.gitignore index 01fafa5a9..5a33c00ea 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,9 @@ perl/Makefile.config /src/libexpr/tests /tests/unit/libexpr/libnixexpr-tests +# /src/libfetchers +/tests/unit/libfetchers/libnixfetchers-tests + # /src/libstore/ *.gen.* /src/libstore/tests diff --git a/Makefile b/Makefile index c3dc83c77..a33b8c458 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,7 @@ makefiles += \ tests/unit/libutil-support/local.mk \ tests/unit/libstore/local.mk \ tests/unit/libstore-support/local.mk \ + tests/unit/libfetchers/local.mk \ tests/unit/libexpr/local.mk \ tests/unit/libexpr-support/local.mk endif diff --git a/src/libfetchers/unix/git.cc b/src/libfetchers/unix/git.cc index 34cfd3f5b..0966c4710 100644 --- a/src/libfetchers/unix/git.cc +++ b/src/libfetchers/unix/git.cc @@ -147,9 +147,12 @@ std::vector getPublicKeys(const Attrs & attrs) { std::vector publicKeys; if (attrs.contains("publicKeys")) { - nlohmann::json publicKeysJson = nlohmann::json::parse(getStrAttr(attrs, "publicKeys")); - ensureType(publicKeysJson, nlohmann::json::value_t::array); - publicKeys = publicKeysJson.get>(); + auto pubKeysJson = nlohmann::json::parse(getStrAttr(attrs, "publicKeys")); + auto & pubKeys = getArray(pubKeysJson); + + for (auto & key : pubKeys) { + publicKeys.push_back(key); + } } if (attrs.contains("publicKey")) publicKeys.push_back(PublicKey{maybeGetStrAttr(attrs, "keytype").value_or("ssh-ed25519"),getStrAttr(attrs, "publicKey")}); diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc index df14e979f..fcf813a37 100644 --- a/src/libstore/derivations.cc +++ b/src/libstore/derivations.cc @@ -1239,16 +1239,14 @@ DerivationOutput DerivationOutput::fromJSON( const ExperimentalFeatureSettings & xpSettings) { std::set keys; - ensureType(_json, nlohmann::detail::value_t::object); - auto json = (std::map) _json; + auto & json = getObject(_json); for (const auto & [key, _] : json) keys.insert(key); auto methodAlgo = [&]() -> std::pair { - std::string hashAlgoStr = json["hashAlgo"]; - // remaining to parse, will be mutated by parsers - std::string_view s = hashAlgoStr; + auto & str = getString(valueAt(json, "hashAlgo")); + std::string_view s = str; ContentAddressMethod method = ContentAddressMethod::parsePrefix(s); if (method == TextIngestionMethod {}) xpSettings.require(Xp::DynamicDerivations); @@ -1258,7 +1256,7 @@ DerivationOutput DerivationOutput::fromJSON( if (keys == (std::set { "path" })) { return DerivationOutput::InputAddressed { - .path = store.parseStorePath((std::string) json["path"]), + .path = store.parseStorePath(getString(valueAt(json, "path"))), }; } @@ -1267,10 +1265,10 @@ DerivationOutput DerivationOutput::fromJSON( auto dof = DerivationOutput::CAFixed { .ca = ContentAddress { .method = std::move(method), - .hash = Hash::parseNonSRIUnprefixed((std::string) json["hash"], hashAlgo), + .hash = Hash::parseNonSRIUnprefixed(getString(valueAt(json, "hash")), hashAlgo), }, }; - if (dof.path(store, drvName, outputName) != store.parseStorePath((std::string) json["path"])) + if (dof.path(store, drvName, outputName) != store.parseStorePath(getString(valueAt(json, "path")))) throw Error("Path doesn't match derivation output"); return dof; } @@ -1357,20 +1355,19 @@ nlohmann::json Derivation::toJSON(const StoreDirConfig & store) const Derivation Derivation::fromJSON( const StoreDirConfig & store, - const nlohmann::json & json, + const nlohmann::json & _json, const ExperimentalFeatureSettings & xpSettings) { using nlohmann::detail::value_t; Derivation res; - ensureType(json, value_t::object); + auto & json = getObject(_json); - res.name = ensureType(valueAt(json, "name"), value_t::string); + res.name = getString(valueAt(json, "name")); try { - auto & outputsObj = ensureType(valueAt(json, "outputs"), value_t::object); - for (auto & [outputName, output] : outputsObj.items()) { + for (auto & [outputName, output] : getObject(valueAt(json, "outputs"))) { res.outputs.insert_or_assign( outputName, DerivationOutput::fromJSON(store, res.name, outputName, output)); @@ -1381,8 +1378,7 @@ Derivation Derivation::fromJSON( } try { - auto & inputsList = ensureType(valueAt(json, "inputSrcs"), value_t::array); - for (auto & input : inputsList) + for (auto & input : getArray(valueAt(json, "inputSrcs"))) res.inputSrcs.insert(store.parseStorePath(static_cast(input))); } catch (Error & e) { e.addTrace({}, "while reading key 'inputSrcs'"); @@ -1391,18 +1387,17 @@ Derivation Derivation::fromJSON( try { std::function::ChildNode(const nlohmann::json &)> doInput; - doInput = [&](const auto & json) { + doInput = [&](const auto & _json) { + auto & json = getObject(_json); DerivedPathMap::ChildNode node; - node.value = static_cast( - ensureType(valueAt(json, "outputs"), value_t::array)); - for (auto & [outputId, childNode] : ensureType(valueAt(json, "dynamicOutputs"), value_t::object).items()) { + node.value = getStringSet(valueAt(json, "outputs")); + for (auto & [outputId, childNode] : getObject(valueAt(json, "dynamicOutputs"))) { xpSettings.require(Xp::DynamicDerivations); node.childMap[outputId] = doInput(childNode); } return node; }; - auto & inputDrvsObj = ensureType(valueAt(json, "inputDrvs"), value_t::object); - for (auto & [inputDrvPath, inputOutputs] : inputDrvsObj.items()) + for (auto & [inputDrvPath, inputOutputs] : getObject(valueAt(json, "inputDrvs"))) res.inputDrvs.map[store.parseStorePath(inputDrvPath)] = doInput(inputOutputs); } catch (Error & e) { @@ -1410,10 +1405,10 @@ Derivation Derivation::fromJSON( throw; } - res.platform = ensureType(valueAt(json, "system"), value_t::string); - res.builder = ensureType(valueAt(json, "builder"), value_t::string); - res.args = ensureType(valueAt(json, "args"), value_t::array); - res.env = ensureType(valueAt(json, "env"), value_t::object); + res.platform = getString(valueAt(json, "system")); + res.builder = getString(valueAt(json, "builder")); + res.args = getStringList(valueAt(json, "args")); + res.env = getStringMap(valueAt(json, "env")); return res; } diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc index d9618d04c..0d219a489 100644 --- a/src/libstore/nar-info.cc +++ b/src/libstore/nar-info.cc @@ -172,19 +172,18 @@ NarInfo NarInfo::fromJSON( }; if (json.contains("url")) - res.url = ensureType(valueAt(json, "url"), value_t::string); + res.url = getString(valueAt(json, "url")); if (json.contains("compression")) - res.compression = ensureType(valueAt(json, "compression"), value_t::string); + res.compression = getString(valueAt(json, "compression")); if (json.contains("downloadHash")) res.fileHash = Hash::parseAny( - static_cast( - ensureType(valueAt(json, "downloadHash"), value_t::string)), + getString(valueAt(json, "downloadHash")), std::nullopt); if (json.contains("downloadSize")) - res.fileSize = ensureType(valueAt(json, "downloadSize"), value_t::number_integer); + res.fileSize = getInteger(valueAt(json, "downloadSize")); return res; } diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index d82ccd0c9..6523cb425 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -190,23 +190,18 @@ nlohmann::json UnkeyedValidPathInfo::toJSON( UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON( const Store & store, - const nlohmann::json & json) + const nlohmann::json & _json) { - using nlohmann::detail::value_t; - UnkeyedValidPathInfo res { Hash(Hash::dummy), }; - ensureType(json, value_t::object); - res.narHash = Hash::parseAny( - static_cast( - ensureType(valueAt(json, "narHash"), value_t::string)), - std::nullopt); - res.narSize = ensureType(valueAt(json, "narSize"), value_t::number_integer); + auto & json = getObject(_json); + res.narHash = Hash::parseAny(getString(valueAt(json, "narHash")), std::nullopt); + res.narSize = getInteger(valueAt(json, "narSize")); try { - auto & references = ensureType(valueAt(json, "references"), value_t::array); + auto references = getStringList(valueAt(json, "references")); for (auto & input : references) res.references.insert(store.parseStorePath(static_cast (input))); @@ -216,20 +211,16 @@ UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON( } if (json.contains("ca")) - res.ca = ContentAddress::parse( - static_cast( - ensureType(valueAt(json, "ca"), value_t::string))); + res.ca = ContentAddress::parse(getString(valueAt(json, "ca"))); if (json.contains("deriver")) - res.deriver = store.parseStorePath( - static_cast( - ensureType(valueAt(json, "deriver"), value_t::string))); + res.deriver = store.parseStorePath(getString(valueAt(json, "deriver"))); if (json.contains("registrationTime")) - res.registrationTime = ensureType(valueAt(json, "registrationTime"), value_t::number_integer); + res.registrationTime = getInteger(valueAt(json, "registrationTime")); if (json.contains("ultimate")) - res.ultimate = ensureType(valueAt(json, "ultimate"), value_t::boolean); + res.ultimate = getBoolean(valueAt(json, "ultimate")); if (json.contains("signatures")) res.sigs = valueAt(json, "signatures"); diff --git a/src/libutil/json-utils.cc b/src/libutil/json-utils.cc index 61cef743d..7a7264a9a 100644 --- a/src/libutil/json-utils.cc +++ b/src/libutil/json-utils.cc @@ -1,5 +1,8 @@ #include "json-utils.hh" #include "error.hh" +#include "types.hh" +#include +#include namespace nix { @@ -18,26 +21,115 @@ nlohmann::json * get(nlohmann::json & map, const std::string & key) } const nlohmann::json & valueAt( - const nlohmann::json & map, + const nlohmann::json::object_t & map, const std::string & key) { if (!map.contains(key)) - throw Error("Expected JSON object to contain key '%s' but it doesn't", key); + throw Error("Expected JSON object to contain key '%s' but it doesn't: %s", key, nlohmann::json(map).dump()); - return map[key]; + return map.at(key); } -const nlohmann::json & ensureType( +std::optional optionalValueAt(const nlohmann::json & value, const std::string & key) +{ + try { + auto & v = valueAt(value, key); + return v.get(); + } catch (...) { + return std::nullopt; + } +} + + +std::optional getNullable(const nlohmann::json & value) +{ + if (value.is_null()) + return std::nullopt; + + return value.get(); +} + +/** + * Ensure the type of a JSON object is what you expect, failing with a + * ensure type if it isn't. + * + * Use before type conversions and element access to avoid ugly + * exceptions, but only part of this module to define the other `get*` + * functions. It is too cumbersome and easy to forget to expect regular + * JSON code to use it directly. + */ +static const nlohmann::json & ensureType( const nlohmann::json & value, nlohmann::json::value_type expectedType ) { if (value.type() != expectedType) throw Error( - "Expected JSON value to be of type '%s' but it is of type '%s'", + "Expected JSON value to be of type '%s' but it is of type '%s': %s", nlohmann::json(expectedType).type_name(), - value.type_name()); + value.type_name(), value.dump()); return value; } + +const nlohmann::json::object_t & getObject(const nlohmann::json & value) +{ + return ensureType(value, nlohmann::json::value_t::object).get_ref(); +} + +const nlohmann::json::array_t & getArray(const nlohmann::json & value) +{ + return ensureType(value, nlohmann::json::value_t::array).get_ref(); +} + +const nlohmann::json::string_t & getString(const nlohmann::json & value) +{ + return ensureType(value, nlohmann::json::value_t::string).get_ref(); +} + +const nlohmann::json::number_integer_t & getInteger(const nlohmann::json & value) +{ + return ensureType(value, nlohmann::json::value_t::number_integer).get_ref(); +} + +const nlohmann::json::boolean_t & getBoolean(const nlohmann::json & value) +{ + return ensureType(value, nlohmann::json::value_t::boolean).get_ref(); +} + +Strings getStringList(const nlohmann::json & value) +{ + auto & jsonArray = getArray(value); + + Strings stringList; + + for (const auto & elem : jsonArray) + stringList.push_back(getString(elem)); + + return stringList; +} + +StringMap getStringMap(const nlohmann::json & value) +{ + auto & jsonObject = getObject(value); + + StringMap stringMap; + + for (const auto & [key, value] : jsonObject) + stringMap[getString(key)] = getString(value); + + return stringMap; +} + +StringSet getStringSet(const nlohmann::json & value) +{ + auto & jsonArray = getArray(value); + + StringSet stringSet; + + for (const auto & elem : jsonArray) + stringSet.insert(getString(elem)); + + return stringSet; +} } diff --git a/src/libutil/json-utils.hh b/src/libutil/json-utils.hh index 06dd80cf7..2024624f4 100644 --- a/src/libutil/json-utils.hh +++ b/src/libutil/json-utils.hh @@ -3,6 +3,9 @@ #include #include +#include + +#include "types.hh" namespace nix { @@ -11,26 +14,30 @@ const nlohmann::json * get(const nlohmann::json & map, const std::string & key); nlohmann::json * get(nlohmann::json & map, const std::string & key); /** - * Get the value of a json object at a key safely, failing - * with a Nix Error if the key does not exist. + * Get the value of a json object at a key safely, failing with a nice + * error if the key does not exist. * * Use instead of nlohmann::json::at() to avoid ugly exceptions. - * - * _Does not check whether `map` is an object_, use `ensureType` for that. */ const nlohmann::json & valueAt( - const nlohmann::json & map, + const nlohmann::json::object_t & map, const std::string & key); +std::optional optionalValueAt(const nlohmann::json & value, const std::string & key); + /** - * Ensure the type of a json object is what you expect, failing - * with a Nix Error if it isn't. - * - * Use before type conversions and element access to avoid ugly exceptions. + * Downcast the json object, failing with a nice error if the conversion fails. + * See https://json.nlohmann.me/features/types/ */ -const nlohmann::json & ensureType( - const nlohmann::json & value, - nlohmann::json::value_type expectedType); +std::optional getNullable(const nlohmann::json & value); +const nlohmann::json::object_t & getObject(const nlohmann::json & value); +const nlohmann::json::array_t & getArray(const nlohmann::json & value); +const nlohmann::json::string_t & getString(const nlohmann::json & value); +const nlohmann::json::number_integer_t & getInteger(const nlohmann::json & value); +const nlohmann::json::boolean_t & getBoolean(const nlohmann::json & value); +Strings getStringList(const nlohmann::json & value); +StringMap getStringMap(const nlohmann::json & value); +StringSet getStringSet(const nlohmann::json & value); /** * For `adl_serializer>` below, we need to track what diff --git a/tests/unit/libfetchers/local.mk b/tests/unit/libfetchers/local.mk new file mode 100644 index 000000000..e9f659fd7 --- /dev/null +++ b/tests/unit/libfetchers/local.mk @@ -0,0 +1,32 @@ +check: libfetchers-tests_RUN + +programs += libfetchers-tests + +libfetchers-tests_NAME = libnixfetchers-tests + +libfetchers-tests_ENV := _NIX_TEST_UNIT_DATA=$(d)/data + +libfetchers-tests_DIR := $(d) + +ifeq ($(INSTALL_UNIT_TESTS), yes) + libfetchers-tests_INSTALL_DIR := $(checkbindir) +else + libfetchers-tests_INSTALL_DIR := +endif + +libfetchers-tests_SOURCES := $(wildcard $(d)/*.cc) + +libfetchers-tests_EXTRA_INCLUDES = \ + -I tests/unit/libstore-support \ + -I tests/unit/libutil-support \ + $(INCLUDE_libfetchers) \ + $(INCLUDE_libstore) \ + $(INCLUDE_libutil) + +libfetchers-tests_CXXFLAGS += $(libfetchers-tests_EXTRA_INCLUDES) + +libfetchers-tests_LIBS = \ + libstore-test-support libutil-test-support \ + libfetchers libstore libutil + +libfetchers-tests_LDFLAGS := -lrapidcheck $(GTEST_LIBS) diff --git a/tests/unit/libfetchers/public-key.cc b/tests/unit/libfetchers/public-key.cc new file mode 100644 index 000000000..fcd5c3af0 --- /dev/null +++ b/tests/unit/libfetchers/public-key.cc @@ -0,0 +1,18 @@ +#include +#include "fetchers.hh" +#include "json-utils.hh" + +namespace nix { + TEST(PublicKey, jsonSerialization) { + auto json = nlohmann::json(fetchers::PublicKey { .key = "ABCDE" }); + + ASSERT_EQ(json, R"({ "key": "ABCDE", "type": "ssh-ed25519" })"_json); + } + TEST(PublicKey, jsonDeserialization) { + auto pubKeyJson = R"({ "key": "ABCDE", "type": "ssh-ed25519" })"_json; + fetchers::PublicKey pubKey = pubKeyJson; + + ASSERT_EQ(pubKey.key, "ABCDE"); + ASSERT_EQ(pubKey.type, "ssh-ed25519"); + } +} diff --git a/tests/unit/libutil/json-utils.cc b/tests/unit/libutil/json-utils.cc index f0ce15c93..ffa667806 100644 --- a/tests/unit/libutil/json-utils.cc +++ b/tests/unit/libutil/json-utils.cc @@ -3,6 +3,7 @@ #include +#include "error.hh" #include "json-utils.hh" namespace nix { @@ -55,4 +56,108 @@ TEST(from_json, vectorOfOptionalInts) { ASSERT_FALSE(vals.at(1).has_value()); } +TEST(valueAt, simpleObject) { + auto simple = R"({ "hello": "world" })"_json; + + ASSERT_EQ(valueAt(getObject(simple), "hello"), "world"); + + auto nested = R"({ "hello": { "world": "" } })"_json; + + auto & nestedObject = valueAt(getObject(nested), "hello"); + + ASSERT_EQ(valueAt(nestedObject, "world"), ""); +} + +TEST(valueAt, missingKey) { + auto json = R"({ "hello": { "nested": "world" } })"_json; + + auto & obj = getObject(json); + + ASSERT_THROW(valueAt(obj, "foo"), Error); +} + +TEST(getObject, rightAssertions) { + auto simple = R"({ "object": {} })"_json; + + ASSERT_EQ(getObject(valueAt(getObject(simple), "object")), (nlohmann::json::object_t {})); + + auto nested = R"({ "object": { "object": {} } })"_json; + + auto & nestedObject = getObject(valueAt(getObject(nested), "object")); + + ASSERT_EQ(nestedObject, getObject(nlohmann::json::parse(R"({ "object": {} })"))); + ASSERT_EQ(getObject(valueAt(getObject(nestedObject), "object")), (nlohmann::json::object_t {})); +} + +TEST(getObject, wrongAssertions) { + auto json = R"({ "object": {}, "array": [], "string": "", "int": 0, "boolean": false })"_json; + + auto & obj = getObject(json); + + ASSERT_THROW(getObject(valueAt(obj, "array")), Error); + ASSERT_THROW(getObject(valueAt(obj, "string")), Error); + ASSERT_THROW(getObject(valueAt(obj, "int")), Error); + ASSERT_THROW(getObject(valueAt(obj, "boolean")), Error); +} + +TEST(getArray, rightAssertions) { + auto simple = R"({ "array": [] })"_json; + + ASSERT_EQ(getArray(valueAt(getObject(simple), "array")), (nlohmann::json::array_t {})); +} + +TEST(getArray, wrongAssertions) { + auto json = R"({ "object": {}, "array": [], "string": "", "int": 0, "boolean": false })"_json; + + ASSERT_THROW(getArray(valueAt(json, "object")), Error); + ASSERT_THROW(getArray(valueAt(json, "string")), Error); + ASSERT_THROW(getArray(valueAt(json, "int")), Error); + ASSERT_THROW(getArray(valueAt(json, "boolean")), Error); +} + +TEST(getString, rightAssertions) { + auto simple = R"({ "string": "" })"_json; + + ASSERT_EQ(getString(valueAt(getObject(simple), "string")), ""); +} + +TEST(getString, wrongAssertions) { + auto json = R"({ "object": {}, "array": [], "string": "", "int": 0, "boolean": false })"_json; + + ASSERT_THROW(getString(valueAt(json, "object")), Error); + ASSERT_THROW(getString(valueAt(json, "array")), Error); + ASSERT_THROW(getString(valueAt(json, "int")), Error); + ASSERT_THROW(getString(valueAt(json, "boolean")), Error); +} + +TEST(getInteger, rightAssertions) { + auto simple = R"({ "int": 0 })"_json; + + ASSERT_EQ(getInteger(valueAt(getObject(simple), "int")), 0); +} + +TEST(getInteger, wrongAssertions) { + auto json = R"({ "object": {}, "array": [], "string": "", "int": 0, "boolean": false })"_json; + + ASSERT_THROW(getInteger(valueAt(json, "object")), Error); + ASSERT_THROW(getInteger(valueAt(json, "array")), Error); + ASSERT_THROW(getInteger(valueAt(json, "string")), Error); + ASSERT_THROW(getInteger(valueAt(json, "boolean")), Error); +} + +TEST(getBoolean, rightAssertions) { + auto simple = R"({ "boolean": false })"_json; + + ASSERT_EQ(getBoolean(valueAt(getObject(simple), "boolean")), false); +} + +TEST(getBoolean, wrongAssertions) { + auto json = R"({ "object": {}, "array": [], "string": "", "int": 0, "boolean": false })"_json; + + ASSERT_THROW(getBoolean(valueAt(json, "object")), Error); + ASSERT_THROW(getBoolean(valueAt(json, "array")), Error); + ASSERT_THROW(getBoolean(valueAt(json, "string")), Error); + ASSERT_THROW(getBoolean(valueAt(json, "int")), Error); +} + } /* namespace nix */