libutil: add a nar indexer

we'll use this in NarAccessor to provide actually safe indexing of
archives. NarAccessor currently is not fully correct: it relies on
the parser not buffering anything to produce correct file offsets,
but only the nar implementation itself can reasonably expect that.

Change-Id: I64f300b86d8844b876a3ace723546ea7d7b4628b
This commit is contained in:
eldritch horrors
2025-03-03 20:48:59 +01:00
parent 18975fa016
commit 0d81e81dd1
3 changed files with 136 additions and 1 deletions
+63
View File
@@ -583,6 +583,69 @@ Generator<Entry> parse(Source & source)
}
namespace nar_index {
namespace {
struct NarSource : Source
{
Source & source;
uint64_t pos = 0;
NarSource(Source & source) : source(source) {}
size_t read(char * data, size_t len) override
{
auto n = source.read(data, len);
pos += n;
return n;
}
};
struct Indexer : NARParseVisitor
{
NarSource & source;
Directory & parent;
public:
Indexer(NarSource & source, Directory & parent) : source(source), parent(parent) {}
box_ptr<NARParseVisitor> createDirectory(const std::string & name) override
{
auto & dir = std::get<Directory>(parent.contents[name] = Directory{});
return make_box_ptr<Indexer>(source, dir);
}
box_ptr<FileHandle>
createRegularFile(const std::string & name, uint64_t size, bool executable) override
{
struct IgnoringFileHandle : FileHandle
{
void close() override {}
void receiveContents(std::string_view data) override {}
};
parent.contents[name] = File{executable, source.pos, size};
return make_box_ptr<IgnoringFileHandle>();
}
void createSymlink(const std::string & name, const std::string & target) override
{
parent.contents[name] = Symlink{target};
}
};
}
Entry create(Source & source)
{
Directory root;
NarSource wrapper{source};
Indexer index{wrapper, root};
parseDump(index, wrapper);
return root.contents.at("");
}
}
static void restore(NARParseVisitor & sink, nar::Entry entry, const Path & path)
{
return std::visit(
+27
View File
@@ -174,6 +174,33 @@ WireFormatGenerator dump(Entry nar);
Generator<Entry> parse(Source & source);
}
namespace nar_index {
struct File;
struct Symlink;
struct Directory;
using Entry = std::variant<File, Symlink, Directory>;
struct File
{
bool executable;
uint64_t offset, size;
};
struct Symlink
{
Path target;
};
struct Directory
{
std::map<std::string, Entry> contents;
};
Entry create(Source & source);
}
void parseDump(NARParseVisitor & sink, Source & source);
void restorePath(const Path & path, Source & source);
+46 -1
View File
@@ -179,7 +179,34 @@ void assert_eq(Entry & a, Entry & b)
}
class NarTest : public testing::TestWithParam<Fragment>
{};
{
public:
static Entries fromIndex(std::string_view raw, nar_index::Entry e)
{
auto handlers = overloaded{
[&](const nar_index::File & f) -> Entry {
std::span<const char> block{raw.substr(f.offset, f.size)};
return File{
f.executable,
f.size,
[](auto block) -> Generator<Bytes> { co_yield block; }(block),
};
},
[&](const nar_index::Symlink & s) -> Entry { return Symlink{s.target}; },
[&](const nar_index::Directory & d) -> Entry {
return Directory{
[](std::string_view raw, const nar_index::Directory & d
) -> Generator<std::pair<const std::string &, Entry>> {
for (auto & [name, entry] : d.contents) {
co_yield std::pair{std::cref(name), *fromIndex(raw, entry).next()};
}
}(raw, d)
};
},
};
co_yield std::visit(handlers, e);
}
};
TEST_P(NarTest, parse)
{
@@ -208,6 +235,24 @@ TEST_P(NarTest, copy)
ASSERT_EQ(raw, copied);
}
TEST_P(NarTest, index)
{
auto & [raw, entriesF] = GetParam();
StringSource source(raw);
auto entries = entriesF();
auto indexed = fromIndex(raw, nar_index::create(source));
while (true) {
auto e = entries.next();
auto p = indexed.next();
ASSERT_EQ(e.has_value(), p.has_value());
if (!e) {
break;
}
assert_eq(*e, *p);
}
}
INSTANTIATE_TEST_SUITE_P(
,
NarTest,