mirror of
https://github.com/privatevoid-net/nix-super.git
synced 2024-11-15 02:36:16 +02:00
Merge pull request #11492 from DeterminateSystems/canon-nars
Make the NAR parser stricter and add some tests
This commit is contained in:
commit
bb1ce014be
9 changed files with 115 additions and 103 deletions
|
@ -82,7 +82,7 @@ void SourceAccessor::dumpPath(
|
||||||
name.erase(pos);
|
name.erase(pos);
|
||||||
}
|
}
|
||||||
if (!unhacked.emplace(name, i.first).second)
|
if (!unhacked.emplace(name, i.first).second)
|
||||||
throw Error("file name collision in between '%s' and '%s'",
|
throw Error("file name collision between '%s' and '%s'",
|
||||||
(path / unhacked[name]),
|
(path / unhacked[name]),
|
||||||
(path / i.first));
|
(path / i.first));
|
||||||
} else
|
} else
|
||||||
|
@ -128,9 +128,10 @@ void dumpString(std::string_view s, Sink & sink)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static SerialisationError badArchive(const std::string & s)
|
template<typename... Args>
|
||||||
|
static SerialisationError badArchive(std::string_view s, const Args & ... args)
|
||||||
{
|
{
|
||||||
return SerialisationError("bad archive: " + s);
|
return SerialisationError("bad archive: " + s, args...);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -167,120 +168,97 @@ struct CaseInsensitiveCompare
|
||||||
|
|
||||||
static void parse(FileSystemObjectSink & sink, Source & source, const CanonPath & path)
|
static void parse(FileSystemObjectSink & sink, Source & source, const CanonPath & path)
|
||||||
{
|
{
|
||||||
std::string s;
|
|
||||||
|
|
||||||
s = readString(source);
|
|
||||||
if (s != "(") throw badArchive("expected open tag");
|
|
||||||
|
|
||||||
std::map<Path, int, CaseInsensitiveCompare> names;
|
|
||||||
|
|
||||||
auto getString = [&]() {
|
auto getString = [&]() {
|
||||||
checkInterrupt();
|
checkInterrupt();
|
||||||
return readString(source);
|
return readString(source);
|
||||||
};
|
};
|
||||||
|
|
||||||
// For first iteration
|
auto expectTag = [&](std::string_view expected) {
|
||||||
s = getString();
|
auto tag = getString();
|
||||||
|
if (tag != expected)
|
||||||
|
throw badArchive("expected tag '%s', got '%s'", expected, tag);
|
||||||
|
};
|
||||||
|
|
||||||
while (1) {
|
expectTag("(");
|
||||||
|
|
||||||
if (s == ")") {
|
expectTag("type");
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (s == "type") {
|
auto type = getString();
|
||||||
std::string t = getString();
|
|
||||||
|
|
||||||
if (t == "regular") {
|
if (type == "regular") {
|
||||||
sink.createRegularFile(path, [&](auto & crf) {
|
sink.createRegularFile(path, [&](auto & crf) {
|
||||||
while (1) {
|
auto tag = getString();
|
||||||
s = getString();
|
|
||||||
|
|
||||||
if (s == "contents") {
|
if (tag == "executable") {
|
||||||
parseContents(crf, source);
|
auto s2 = getString();
|
||||||
}
|
if (s2 != "") throw badArchive("executable marker has non-empty value");
|
||||||
|
crf.isExecutable();
|
||||||
else if (s == "executable") {
|
tag = getString();
|
||||||
auto s2 = getString();
|
|
||||||
if (s2 != "") throw badArchive("executable marker has non-empty value");
|
|
||||||
crf.isExecutable();
|
|
||||||
}
|
|
||||||
|
|
||||||
else break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (t == "directory") {
|
if (tag == "contents")
|
||||||
sink.createDirectory(path);
|
parseContents(crf, source);
|
||||||
|
|
||||||
std::string prevName;
|
expectTag(")");
|
||||||
|
});
|
||||||
while (1) {
|
|
||||||
s = getString();
|
|
||||||
|
|
||||||
if (s == "entry") {
|
|
||||||
std::string name;
|
|
||||||
|
|
||||||
s = getString();
|
|
||||||
if (s != "(") throw badArchive("expected open tag");
|
|
||||||
|
|
||||||
while (1) {
|
|
||||||
s = getString();
|
|
||||||
|
|
||||||
if (s == ")") {
|
|
||||||
break;
|
|
||||||
} else if (s == "name") {
|
|
||||||
name = getString();
|
|
||||||
if (name.empty() || name == "." || name == ".." || name.find('/') != std::string::npos || name.find((char) 0) != std::string::npos)
|
|
||||||
throw Error("NAR contains invalid file name '%1%'", name);
|
|
||||||
if (name <= prevName)
|
|
||||||
throw Error("NAR directory is not sorted");
|
|
||||||
prevName = name;
|
|
||||||
if (archiveSettings.useCaseHack) {
|
|
||||||
auto i = names.find(name);
|
|
||||||
if (i != names.end()) {
|
|
||||||
debug("case collision between '%1%' and '%2%'", i->first, name);
|
|
||||||
name += caseHackSuffix;
|
|
||||||
name += std::to_string(++i->second);
|
|
||||||
auto j = names.find(name);
|
|
||||||
if (j != names.end())
|
|
||||||
throw Error("NAR contains file name '%s' that collides with case-hacked file name '%s'", prevName, j->first);
|
|
||||||
} else
|
|
||||||
names[name] = 0;
|
|
||||||
}
|
|
||||||
} else if (s == "node") {
|
|
||||||
if (name.empty()) throw badArchive("entry name missing");
|
|
||||||
parse(sink, source, path / name);
|
|
||||||
} else
|
|
||||||
throw badArchive("unknown field " + s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (t == "symlink") {
|
|
||||||
s = getString();
|
|
||||||
|
|
||||||
if (s != "target")
|
|
||||||
throw badArchive("expected 'target' got " + s);
|
|
||||||
|
|
||||||
std::string target = getString();
|
|
||||||
sink.createSymlink(path, target);
|
|
||||||
|
|
||||||
// for the next iteration
|
|
||||||
s = getString();
|
|
||||||
}
|
|
||||||
|
|
||||||
else throw badArchive("unknown file type " + t);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
throw badArchive("unknown field " + s);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else if (type == "directory") {
|
||||||
|
sink.createDirectory(path);
|
||||||
|
|
||||||
|
std::map<Path, int, CaseInsensitiveCompare> names;
|
||||||
|
|
||||||
|
std::string prevName;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
auto tag = getString();
|
||||||
|
|
||||||
|
if (tag == ")") break;
|
||||||
|
|
||||||
|
if (tag != "entry")
|
||||||
|
throw badArchive("expected tag 'entry' or ')', got '%s'", tag);
|
||||||
|
|
||||||
|
expectTag("(");
|
||||||
|
|
||||||
|
expectTag("name");
|
||||||
|
|
||||||
|
auto name = getString();
|
||||||
|
if (name.empty() || name == "." || name == ".." || name.find('/') != std::string::npos || name.find((char) 0) != std::string::npos)
|
||||||
|
throw badArchive("NAR contains invalid file name '%1%'", name);
|
||||||
|
if (name <= prevName)
|
||||||
|
throw badArchive("NAR directory is not sorted");
|
||||||
|
prevName = name;
|
||||||
|
if (archiveSettings.useCaseHack) {
|
||||||
|
auto i = names.find(name);
|
||||||
|
if (i != names.end()) {
|
||||||
|
debug("case collision between '%1%' and '%2%'", i->first, name);
|
||||||
|
name += caseHackSuffix;
|
||||||
|
name += std::to_string(++i->second);
|
||||||
|
auto j = names.find(name);
|
||||||
|
if (j != names.end())
|
||||||
|
throw badArchive("NAR contains file name '%s' that collides with case-hacked file name '%s'", prevName, j->first);
|
||||||
|
} else
|
||||||
|
names[name] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
expectTag("node");
|
||||||
|
|
||||||
|
parse(sink, source, path / name);
|
||||||
|
|
||||||
|
expectTag(")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (type == "symlink") {
|
||||||
|
expectTag("target");
|
||||||
|
|
||||||
|
auto target = getString();
|
||||||
|
sink.createSymlink(path, target);
|
||||||
|
|
||||||
|
expectTag(")");
|
||||||
|
}
|
||||||
|
|
||||||
|
else throw badArchive("unknown file type '%s'", type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
BIN
tests/functional/dot.nar
Normal file
BIN
tests/functional/dot.nar
Normal file
Binary file not shown.
BIN
tests/functional/dotdot.nar
Normal file
BIN
tests/functional/dotdot.nar
Normal file
Binary file not shown.
BIN
tests/functional/empty.nar
Normal file
BIN
tests/functional/empty.nar
Normal file
Binary file not shown.
BIN
tests/functional/executable-after-contents.nar
Normal file
BIN
tests/functional/executable-after-contents.nar
Normal file
Binary file not shown.
BIN
tests/functional/name-after-node.nar
Normal file
BIN
tests/functional/name-after-node.nar
Normal file
Binary file not shown.
|
@ -67,6 +67,12 @@ expectStderr 1 nix-store --restore "$TEST_ROOT/out" < "$TEST_ROOT/tmp.nar" | gre
|
||||||
rm -rf "$TEST_ROOT/case"
|
rm -rf "$TEST_ROOT/case"
|
||||||
opts=("--option" "use-case-hack" "true")
|
opts=("--option" "use-case-hack" "true")
|
||||||
nix-store "${opts[@]}" --restore "$TEST_ROOT/case" < case.nar
|
nix-store "${opts[@]}" --restore "$TEST_ROOT/case" < case.nar
|
||||||
|
[[ -e "$TEST_ROOT/case/xt_CONNMARK.h" ]]
|
||||||
|
[[ -e "$TEST_ROOT/case/xt_CONNmark.h~nix~case~hack~1" ]]
|
||||||
|
[[ -e "$TEST_ROOT/case/xt_connmark.h~nix~case~hack~2" ]]
|
||||||
|
[[ -e "$TEST_ROOT/case/x/FOO" ]]
|
||||||
|
[[ -d "$TEST_ROOT/case/x/Foo~nix~case~hack~1" ]]
|
||||||
|
[[ -e "$TEST_ROOT/case/x/foo~nix~case~hack~2/a~nix~case~hack~1/foo" ]]
|
||||||
nix-store "${opts[@]}" --dump "$TEST_ROOT/case" > "$TEST_ROOT/case.nar"
|
nix-store "${opts[@]}" --dump "$TEST_ROOT/case" > "$TEST_ROOT/case.nar"
|
||||||
cmp case.nar "$TEST_ROOT/case.nar"
|
cmp case.nar "$TEST_ROOT/case.nar"
|
||||||
[ "$(nix-hash "${opts[@]}" --type sha256 "$TEST_ROOT/case")" = "$(nix-hash --flat --type sha256 case.nar)" ]
|
[ "$(nix-hash "${opts[@]}" --type sha256 "$TEST_ROOT/case")" = "$(nix-hash --flat --type sha256 case.nar)" ]
|
||||||
|
@ -92,3 +98,31 @@ else
|
||||||
[[ -e $TEST_ROOT/out/â ]]
|
[[ -e $TEST_ROOT/out/â ]]
|
||||||
[[ -e $TEST_ROOT/out/â ]]
|
[[ -e $TEST_ROOT/out/â ]]
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Unpacking a NAR with a NUL character in a file name should fail.
|
||||||
|
rm -rf "$TEST_ROOT/out"
|
||||||
|
expectStderr 1 nix-store --restore "$TEST_ROOT/out" < nul.nar | grepQuiet "NAR contains invalid file name 'f"
|
||||||
|
|
||||||
|
# Likewise for a '.' filename.
|
||||||
|
rm -rf "$TEST_ROOT/out"
|
||||||
|
expectStderr 1 nix-store --restore "$TEST_ROOT/out" < dot.nar | grepQuiet "NAR contains invalid file name '.'"
|
||||||
|
|
||||||
|
# Likewise for a '..' filename.
|
||||||
|
rm -rf "$TEST_ROOT/out"
|
||||||
|
expectStderr 1 nix-store --restore "$TEST_ROOT/out" < dotdot.nar | grepQuiet "NAR contains invalid file name '..'"
|
||||||
|
|
||||||
|
# Likewise for a filename containing a slash.
|
||||||
|
rm -rf "$TEST_ROOT/out"
|
||||||
|
expectStderr 1 nix-store --restore "$TEST_ROOT/out" < slash.nar | grepQuiet "NAR contains invalid file name 'x/y'"
|
||||||
|
|
||||||
|
# Likewise for an empty filename.
|
||||||
|
rm -rf "$TEST_ROOT/out"
|
||||||
|
expectStderr 1 nix-store --restore "$TEST_ROOT/out" < empty.nar | grepQuiet "NAR contains invalid file name ''"
|
||||||
|
|
||||||
|
# Test that the 'executable' field cannot come before the 'contents' field.
|
||||||
|
rm -rf "$TEST_ROOT/out"
|
||||||
|
expectStderr 1 nix-store --restore "$TEST_ROOT/out" < executable-after-contents.nar | grepQuiet "expected tag ')', got 'executable'"
|
||||||
|
|
||||||
|
# Test that the 'name' field cannot come before the 'node' field in a directory entry.
|
||||||
|
rm -rf "$TEST_ROOT/out"
|
||||||
|
expectStderr 1 nix-store --restore "$TEST_ROOT/out" < name-after-node.nar | grepQuiet "expected tag 'name'"
|
||||||
|
|
BIN
tests/functional/nul.nar
Normal file
BIN
tests/functional/nul.nar
Normal file
Binary file not shown.
BIN
tests/functional/slash.nar
Normal file
BIN
tests/functional/slash.nar
Normal file
Binary file not shown.
Loading…
Reference in a new issue