1
1
Fork 0
mirror of https://github.com/NixOS/nix.git synced 2025-11-22 18:29:36 +01:00

nlohmann::json instance and JSON Schema for MemorySourceAccessor

Also do a better JSON and testing for deep and shallow NAR listings.

As documented, this for file system objects themselves, since
`MemorySourceAccessor` is an implementation detail.
This commit is contained in:
John Ericson 2025-09-28 00:29:21 -04:00
parent c4906741a1
commit 7357a654de
26 changed files with 605 additions and 101 deletions

View file

@ -37,6 +37,7 @@ mkMesonDerivation (finalAttrs: {
(fileset.unions [
../../.version
# For example JSON
../../src/libutil-tests/data/memory-source-accessor
../../src/libutil-tests/data/hash
../../src/libstore-tests/data/content-address
../../src/libstore-tests/data/store-path

View file

@ -121,6 +121,7 @@
- [Architecture and Design](architecture/architecture.md)
- [Formats and Protocols](protocols/index.md)
- [JSON Formats](protocols/json/index.md)
- [File System Object](protocols/json/file-system-object.md)
- [Hash](protocols/json/hash.md)
- [Content Address](protocols/json/content-address.md)
- [Store Path](protocols/json/store-path.md)

View file

@ -0,0 +1,21 @@
{{#include file-system-object-v1-fixed.md}}
## Examples
### Simple
```json
{{#include schema/file-system-object-v1/simple.json}}
```
### Complex
```json
{{#include schema/file-system-object-v1/complex.json}}
```
<!-- need to convert YAML to JSON first
## Raw Schema
[JSON Schema for File System Object v1](schema/file-system-object-v1.json)
-->

View file

@ -11,6 +11,7 @@ s/\\`/`/g
#
# As we have more such relative links, more replacements of this nature
# should appear below.
s^#/\$defs/\(regular\|symlink\|directory\)^In this schema^g
s^\(./hash-v1.yaml\)\?#/$defs/algorithm^[JSON format for `Hash`](./hash.html#algorithm)^g
s^\(./hash-v1.yaml\)^[JSON format for `Hash`](./hash.html)^g
s^\(./content-address-v1.yaml\)\?#/$defs/method^[JSON format for `ContentAddress`](./content-address.html#method)^g

View file

@ -9,6 +9,7 @@ json_schema_for_humans = find_program('generate-schema-doc', required : false)
json_schema_config = files('json-schema-for-humans-config.yaml')
schemas = [
'file-system-object-v1',
'hash-v1',
'content-address-v1',
'store-path-v1',

View file

@ -0,0 +1 @@
../../../../../../src/libutil-tests/data/memory-source-accessor

View file

@ -0,0 +1,71 @@
"$schema": http://json-schema.org/draft-04/schema#
"$id": https://nix.dev/manual/nix/latest/protocols/json/schema/file-system-object-v1.json
title: File System Object
description: |
This schema describes the JSON representation of Nix's [File System Object](@docroot@/store/file-system-object.md).
The schema is recursive because file system objects contain other file system objects.
type: object
required: ["type"]
properties:
type:
type: string
enum: ["regular", "symlink", "directory"]
# Enforce conditional structure based on `type`
anyOf:
- $ref: "#/$defs/regular"
required: ["type", "contents"]
- $ref: "#/$defs/directory"
required: ["type", "entries"]
- $ref: "#/$defs/symlink"
required: ["type", "target"]
"$defs":
regular:
title: Regular File
description: |
See [Regular File](@docroot@/store/file-system-object.md#regular) in the manual for details.
required: ["contents"]
properties:
type:
const: "regular"
contents:
type: string
description: File contents
executable:
type: boolean
description: Whether the file is executable.
default: false
additionalProperties: false
directory:
title: Directory
description: |
See [Directory](@docroot@/store/file-system-object.md#directory) in the manual for details.
required: ["entries"]
properties:
type:
const: "directory"
entries:
type: object
description: |
Map of names to nested file system objects (for type=directory)
additionalProperties:
$ref: "#"
additionalProperties: false
symlink:
title: Symbolic Link
description: |
See [Symbolic Link](@docroot@/store/file-system-object.md#symlink) in the manual for details.
required: ["target"]
properties:
type:
const: "symlink"
target:
type: string
description: Target path of the symlink.
additionalProperties: false

View file

@ -3,19 +3,23 @@
Nix uses a simplified model of the file system, which consists of file system objects.
Every file system object is one of the following:
- File
- [**Regular File**]{#regular}
- A possibly empty sequence of bytes for contents
- A single boolean representing the [executable](https://en.m.wikipedia.org/wiki/File-system_permissions#Permissions) permission
- Directory
- [**Directory**]{#directory}
Mapping of names to child file system objects
- [Symbolic link](https://en.m.wikipedia.org/wiki/Symbolic_link)
- [**Symbolic link**]{#symlink}
An arbitrary string.
Nix does not assign any semantics to symbolic links.
An arbitrary string, known as the *target* of the symlink.
In general, Nix does not assign any semantics to symbolic links.
Certain operations however, may make additional assumptions and attempt to use the target to find another file system object.
> See [the Wikpedia article on symbolic links](https://en.m.wikipedia.org/wiki/Symbolic_link) for background information if you are unfamiliar with this Unix concept.
File system objects and their children form a tree.
A bare file or symlink can be a root file system object.

View file

@ -0,0 +1 @@
../../src/libutil-tests/data/memory-source-accessor

View file

@ -20,6 +20,14 @@ schema_dir = meson.current_source_dir() / 'schema'
# Get all example files
schemas = [
{
'stem' : 'file-system-object',
'schema' : schema_dir / 'file-system-object-v1.yaml',
'files' : [
'simple.json',
'complex.json',
],
},
{
'stem' : 'hash',
'schema' : schema_dir / 'hash-v1.yaml',

View file

@ -20,6 +20,7 @@ mkMesonDerivation (finalAttrs: {
fileset = lib.fileset.unions [
../../.version
../../doc/manual/source/protocols/json/schema
../../src/libutil-tests/data/memory-source-accessor
../../src/libutil-tests/data/hash
../../src/libstore-tests/data/content-address
../../src/libstore-tests/data/store-path

View file

@ -0,0 +1,24 @@
{
"entries": {
"bar": {
"entries": {
"baz": {
"contents": "good day,\n\u0000\n\tworld!",
"executable": true,
"type": "regular"
},
"quux": {
"target": "/over/there",
"type": "symlink"
}
},
"type": "directory"
},
"foo": {
"contents": "hello\n\u0000\n\tworld!",
"executable": false,
"type": "regular"
}
},
"type": "directory"
}

View file

@ -0,0 +1,5 @@
{
"contents": "asdf",
"executable": false,
"type": "regular"
}

View file

@ -0,0 +1,23 @@
{
"entries": {
"bar": {
"entries": {
"baz": {
"executable": true,
"size": 19,
"type": "regular"
},
"quux": {
"target": "/over/there",
"type": "symlink"
}
},
"type": "directory"
},
"foo": {
"size": 15,
"type": "regular"
}
},
"type": "directory"
}

View file

@ -0,0 +1,7 @@
{
"entries": {
"bar": {},
"foo": {}
},
"type": "directory"
}

View file

@ -224,42 +224,15 @@ TEST_F(GitTest, tree_sha256_write)
});
}
namespace memory_source_accessor {
extern ref<MemorySourceAccessor> exampleComplex();
}
TEST_F(GitTest, both_roundrip)
{
using File = MemorySourceAccessor::File;
auto files = make_ref<MemorySourceAccessor>();
files->root = File::Directory{
.entries{
{
"foo",
File::Regular{
.contents = "hello\n\0\n\tworld!",
},
},
{
"bar",
File::Directory{
.entries =
{
{
"baz",
File::Regular{
.executable = true,
.contents = "good day,\n\0\n\tworld!",
},
},
{
"quux",
File::Symlink{
.target = "/over/there",
},
},
},
},
},
},
};
auto files = memory_source_accessor::exampleComplex();
for (const auto hashAlgo : {HashAlgorithm::SHA1, HashAlgorithm::SHA256}) {
std::map<Hash, std::string> cas;

View file

@ -0,0 +1,116 @@
#include <string_view>
#include "nix/util/memory-source-accessor.hh"
#include "nix/util/tests/json-characterization.hh"
namespace nix {
namespace memory_source_accessor {
using namespace std::literals;
using File = MemorySourceAccessor::File;
ref<MemorySourceAccessor> exampleSimple()
{
auto sc = make_ref<MemorySourceAccessor>();
sc->root = File{File::Regular{
.executable = false,
.contents = "asdf",
}};
return sc;
}
ref<MemorySourceAccessor> exampleComplex()
{
auto files = make_ref<MemorySourceAccessor>();
files->root = File::Directory{
.entries{
{
"foo",
File::Regular{
.contents = "hello\n\0\n\tworld!"s,
},
},
{
"bar",
File::Directory{
.entries =
{
{
"baz",
File::Regular{
.executable = true,
.contents = "good day,\n\0\n\tworld!"s,
},
},
{
"quux",
File::Symlink{
.target = "/over/there",
},
},
},
},
},
},
};
return files;
}
} // namespace memory_source_accessor
/* ----------------------------------------------------------------------------
* JSON
* --------------------------------------------------------------------------*/
class MemorySourceAccessorTest : public virtual CharacterizationTest
{
std::filesystem::path unitTestData = getUnitTestData() / "memory-source-accessor";
public:
std::filesystem::path goldenMaster(std::string_view testStem) const override
{
return unitTestData / testStem;
}
};
using nlohmann::json;
struct MemorySourceAccessorJsonTest : MemorySourceAccessorTest,
JsonCharacterizationTest<MemorySourceAccessor>,
::testing::WithParamInterface<std::pair<std::string_view, MemorySourceAccessor>>
{};
TEST_P(MemorySourceAccessorJsonTest, from_json)
{
auto & [name, expected] = GetParam();
/* Cannot use `readJsonTest` because need to compare `root` field of
the source accessors for equality. */
readTest(Path{name} + ".json", [&](const auto & encodedRaw) {
auto encoded = json::parse(encodedRaw);
auto decoded = static_cast<MemorySourceAccessor>(encoded);
ASSERT_EQ(decoded.root, expected.root);
});
}
TEST_P(MemorySourceAccessorJsonTest, to_json)
{
auto & [name, value] = GetParam();
writeJsonTest(name, value);
}
INSTANTIATE_TEST_SUITE_P(
MemorySourceAccessorJSON,
MemorySourceAccessorJsonTest,
::testing::Values(
std::pair{
"simple",
*memory_source_accessor::exampleSimple(),
},
std::pair{
"complex",
*memory_source_accessor::exampleComplex(),
}));
} // namespace nix

View file

@ -63,7 +63,9 @@ sources = files(
'json-utils.cc',
'logging.cc',
'lru-cache.cc',
'memory-source-accessor.cc',
'monitorfdhup.cc',
'nar-listing.cc',
'nix_api_util.cc',
'nix_api_util_internal.cc',
'pool.cc',

View file

@ -0,0 +1,83 @@
#include <string_view>
#include "nix/util/nar-accessor.hh"
#include "nix/util/tests/json-characterization.hh"
namespace nix {
// Forward declaration from memory-source-accessor.cc
namespace memory_source_accessor {
ref<MemorySourceAccessor> exampleComplex();
}
/* ----------------------------------------------------------------------------
* JSON
* --------------------------------------------------------------------------*/
class NarListingTest : public virtual CharacterizationTest
{
std::filesystem::path unitTestData = getUnitTestData() / "nar-listing";
public:
std::filesystem::path goldenMaster(std::string_view testStem) const override
{
return unitTestData / testStem;
}
};
using nlohmann::json;
struct NarListingJsonTest : NarListingTest,
JsonCharacterizationTest<NarListing>,
::testing::WithParamInterface<std::pair<std::string_view, NarListing>>
{};
TEST_P(NarListingJsonTest, from_json)
{
auto & [name, expected] = GetParam();
readJsonTest(name, expected);
}
TEST_P(NarListingJsonTest, to_json)
{
auto & [name, value] = GetParam();
writeJsonTest(name, value);
}
INSTANTIATE_TEST_SUITE_P(
NarListingJSON,
NarListingJsonTest,
::testing::Values(
std::pair{
"deep",
listNarDeep(*memory_source_accessor::exampleComplex(), CanonPath::root),
}));
struct ShallowNarListingJsonTest : NarListingTest,
JsonCharacterizationTest<ShallowNarListing>,
::testing::WithParamInterface<std::pair<std::string_view, ShallowNarListing>>
{};
TEST_P(ShallowNarListingJsonTest, from_json)
{
auto & [name, expected] = GetParam();
readJsonTest(name, expected);
}
TEST_P(ShallowNarListingJsonTest, to_json)
{
auto & [name, value] = GetParam();
writeJsonTest(name, value);
}
INSTANTIATE_TEST_SUITE_P(
ShallowNarListingJSON,
ShallowNarListingJsonTest,
::testing::Values(
std::pair{
"shallow",
listNarShallow(*memory_source_accessor::exampleComplex(), CanonPath::root),
}));
} // namespace nix

View file

@ -6,15 +6,18 @@
#include "nix/util/experimental-features.hh"
// Following https://github.com/nlohmann/json#how-can-i-use-get-for-non-default-constructiblenon-copyable-types
#define JSON_IMPL(TYPE) \
namespace nlohmann { \
using namespace nix; \
template<> \
#define JSON_IMPL_INNER(TYPE) \
struct adl_serializer<TYPE> \
{ \
static TYPE from_json(const json & json); \
static void to_json(json & json, const TYPE & t); \
}; \
};
#define JSON_IMPL(TYPE) \
namespace nlohmann { \
using namespace nix; \
template<> \
JSON_IMPL_INNER(TYPE) \
}
#define JSON_IMPL_WITH_XP_FEATURES(TYPE) \

View file

@ -160,4 +160,53 @@ struct MemorySink : FileSystemObjectSink
void createSymlink(const CanonPath & path, const std::string & target) override;
};
template<>
struct json_avoids_null<MemorySourceAccessor::File::Regular> : std::true_type
{};
template<>
struct json_avoids_null<MemorySourceAccessor::File::Directory> : std::true_type
{};
template<>
struct json_avoids_null<MemorySourceAccessor::File::Symlink> : std::true_type
{};
template<>
struct json_avoids_null<MemorySourceAccessor::File> : std::true_type
{};
template<>
struct json_avoids_null<MemorySourceAccessor> : std::true_type
{};
} // namespace nix
namespace nlohmann {
using namespace nix;
#define ARG fso::Regular<RegularContents>
template<typename RegularContents>
JSON_IMPL_INNER(ARG)
#undef ARG
#define ARG fso::DirectoryT<Child>
template<typename Child>
JSON_IMPL_INNER(ARG)
#undef ARG
template<>
JSON_IMPL_INNER(fso::Symlink)
template<>
JSON_IMPL_INNER(fso::Opaque)
#define ARG fso::VariantT<RegularContents, recur>
template<typename RegularContents, bool recur>
JSON_IMPL_INNER(ARG)
#undef ARG
} // namespace nlohmann
JSON_IMPL(MemorySourceAccessor)

View file

@ -75,14 +75,6 @@ NarListing listNarDeep(SourceAccessor & accessor, const CanonPath & path);
*/
ShallowNarListing listNarShallow(SourceAccessor & accessor, const CanonPath & path);
/**
* Serialize a NarListing to JSON.
*/
void to_json(nlohmann::json & j, const NarListing & listing);
/**
* Serialize a ShallowNarListing to JSON.
*/
void to_json(nlohmann::json & j, const ShallowNarListing & listing);
// All json_avoids_null and JSON_IMPL covered by generic templates in memory-source-accessor.hh
} // namespace nix

View file

@ -1,4 +1,5 @@
#include "nix/util/memory-source-accessor.hh"
#include "nix/util/json-utils.hh"
namespace nix {

View file

@ -0,0 +1,162 @@
#include "nix/util/memory-source-accessor.hh"
#include "nix/util/nar-accessor.hh"
#include "nix/util/json-utils.hh"
#include <nlohmann/json.hpp>
namespace nlohmann {
using namespace nix;
// fso::Regular<RegularContents>
template<>
MemorySourceAccessor::File::Regular adl_serializer<MemorySourceAccessor::File::Regular>::from_json(const json & json)
{
auto & obj = getObject(json);
return MemorySourceAccessor::File::Regular{
.executable = getBoolean(valueAt(obj, "executable")),
.contents = getString(valueAt(obj, "contents")),
};
}
template<>
void adl_serializer<MemorySourceAccessor::File::Regular>::to_json(
json & json, const MemorySourceAccessor::File::Regular & r)
{
json = {
{"executable", r.executable},
{"contents", r.contents},
};
}
template<>
NarListing::Regular adl_serializer<NarListing::Regular>::from_json(const json & json)
{
auto & obj = getObject(json);
auto * execPtr = optionalValueAt(obj, "executable");
auto * sizePtr = optionalValueAt(obj, "size");
auto * offsetPtr = optionalValueAt(obj, "narOffset");
return NarListing::Regular{
.executable = execPtr ? getBoolean(*execPtr) : false,
.contents{
.fileSize = ptrToOwned<uint64_t>(sizePtr),
.narOffset = ptrToOwned<uint64_t>(offsetPtr).and_then(
[](auto v) { return v != 0 ? std::optional{v} : std::nullopt; }),
},
};
}
template<>
void adl_serializer<NarListing::Regular>::to_json(json & j, const NarListing::Regular & r)
{
if (r.contents.fileSize)
j["size"] = *r.contents.fileSize;
if (r.executable)
j["executable"] = true;
if (r.contents.narOffset)
j["narOffset"] = *r.contents.narOffset;
}
template<typename Child>
void adl_serializer<fso::DirectoryT<Child>>::to_json(json & j, const fso::DirectoryT<Child> & d)
{
j["entries"] = d.entries;
}
template<typename Child>
fso::DirectoryT<Child> adl_serializer<fso::DirectoryT<Child>>::from_json(const json & json)
{
auto & obj = getObject(json);
return fso::DirectoryT<Child>{
.entries = valueAt(obj, "entries"),
};
}
// fso::Symlink
fso::Symlink adl_serializer<fso::Symlink>::from_json(const json & json)
{
auto & obj = getObject(json);
return fso::Symlink{
.target = getString(valueAt(obj, "target")),
};
}
void adl_serializer<fso::Symlink>::to_json(json & json, const fso::Symlink & s)
{
json = {
{"target", s.target},
};
}
// fso::Opaque
fso::Opaque adl_serializer<fso::Opaque>::from_json(const json &)
{
return fso::Opaque{};
}
void adl_serializer<fso::Opaque>::to_json(json & j, const fso::Opaque &)
{
j = nlohmann::json::object();
}
// fso::VariantT<RegularContents, recur> - generic implementation
template<typename RegularContents, bool recur>
void adl_serializer<fso::VariantT<RegularContents, recur>>::to_json(
json & j, const fso::VariantT<RegularContents, recur> & val)
{
using Variant = fso::VariantT<RegularContents, recur>;
j = nlohmann::json::object();
std::visit(
overloaded{
[&](const typename Variant::Regular & r) {
j = r;
j["type"] = "regular";
},
[&](const typename Variant::Directory & d) {
j = d;
j["type"] = "directory";
},
[&](const typename Variant::Symlink & s) {
j = s;
j["type"] = "symlink";
},
},
val.raw);
}
template<typename RegularContents, bool recur>
fso::VariantT<RegularContents, recur>
adl_serializer<fso::VariantT<RegularContents, recur>>::from_json(const json & json)
{
using Variant = fso::VariantT<RegularContents, recur>;
auto & obj = getObject(json);
auto type = getString(valueAt(obj, "type"));
if (type == "regular")
return static_cast<typename Variant::Regular>(json);
if (type == "directory")
return static_cast<typename Variant::Directory>(json);
if (type == "symlink")
return static_cast<typename Variant::Symlink>(json);
else
throw Error("unknown type of file '%s'", type);
}
// Explicit instantiations for VariantT types we use
template struct adl_serializer<MemorySourceAccessor::File>;
template struct adl_serializer<NarListing>;
template struct adl_serializer<ShallowNarListing>;
// MemorySourceAccessor
MemorySourceAccessor adl_serializer<MemorySourceAccessor>::from_json(const json & json)
{
MemorySourceAccessor res;
res.root = json;
return res;
}
void adl_serializer<MemorySourceAccessor>::to_json(json & json, const MemorySourceAccessor & val)
{
json = val.root;
}
} // namespace nlohmann

View file

@ -146,6 +146,7 @@ sources = [ config_priv_h ] + files(
'json-utils.cc',
'logging.cc',
'memory-source-accessor.cc',
'memory-source-accessor/json.cc',
'mounted-source-accessor.cc',
'nar-accessor.cc',
'pos-table.cc',

View file

@ -324,52 +324,4 @@ ShallowNarListing listNarShallow(SourceAccessor & accessor, const CanonPath & pa
return listNarImpl<false>(accessor, path);
}
template<typename Listing>
static void to_json_impl(nlohmann::json & j, const Listing & listing)
{
std::visit(
overloaded{
[&](const typename Listing::Regular & r) {
j = nlohmann::json::object();
j["type"] = "regular";
if (r.contents.fileSize)
j["size"] = *r.contents.fileSize;
if (r.executable)
j["executable"] = true;
if (r.contents.narOffset)
j["narOffset"] = *r.contents.narOffset;
},
[&](const typename Listing::Directory & d) {
j = nlohmann::json::object();
j["type"] = "directory";
j["entries"] = nlohmann::json::object();
for (const auto & [name, child] : d.entries) {
if constexpr (std::is_same_v<Listing, NarListing>) {
to_json(j["entries"][name], child);
} else if constexpr (std::is_same_v<Listing, ShallowNarListing>) {
j["entries"][name] = nlohmann::json::object();
} else {
static_assert(false);
}
}
},
[&](const typename Listing::Symlink & s) {
j = nlohmann::json::object();
j["type"] = "symlink";
j["target"] = s.target;
},
},
listing.raw);
}
void to_json(nlohmann::json & j, const NarListing & listing)
{
to_json_impl(j, listing);
}
void to_json(nlohmann::json & j, const ShallowNarListing & listing)
{
to_json_impl(j, listing);
}
} // namespace nix