Merge remote-tracking branch 'pennae/path-access' into HEAD

This fixes a bug where flakes do not actually do purity path checks
correctly.

Tested-By: Jade Lovelace <lix@jade.fyi>
Change-Id: If7d131a8e73a5874fb15cfaa0dea3b8811ba35d2
This commit is contained in:
Jade Lovelace
2025-01-13 17:19:54 -08:00
30 changed files with 416 additions and 276 deletions
+6 -2
View File
@@ -253,8 +253,12 @@ static void main_nix_build(std::string programName, Strings argv)
else
/* If we're in a #! script, interpret filenames
relative to the script. */
exprs.push_back(evaluator->parseExprFromFile(resolveExprPath(evaluator->paths.checkSourcePath(lookupFileArg(*evaluator,
inShebang && !packages ? absPath(i, absPath(dirOf(script))) : i)))));
exprs.push_back(evaluator->parseExprFromFile(
evaluator->paths.resolveExprPath(lookupFileArg(
*evaluator,
inShebang && !packages ? absPath(i, absPath(dirOf(script))) : i
))
));
}
}
+19 -13
View File
@@ -96,11 +96,16 @@ static bool parseInstallSourceOptions(Globals & globals,
}
static bool isNixExpr(const SourcePath & path, struct InputAccessor::Stat & st)
static bool isNixExpr(EvalPaths & paths, const CheckedSourcePath & path, struct InputAccessor::Stat & st)
{
return
st.type == InputAccessor::tRegular
|| (st.type == InputAccessor::tDirectory && (path + "default.nix").pathExists());
if (st.type == InputAccessor::tRegular) {
return true;
} else if (st.type != InputAccessor::tDirectory) {
return false;
} else {
auto defaultNix = paths.checkSourcePath(path + "default.nix");
return defaultNix.pathExists();
}
}
@@ -108,7 +113,7 @@ static constexpr size_t maxAttrs = 1024;
static void getAllExprs(Evaluator & state,
const SourcePath & path, StringSet & seen, BindingsBuilder & attrs)
const CheckedSourcePath & path, StringSet & seen, BindingsBuilder & attrs)
{
StringSet namesSorted;
for (auto & [name, _] : path.readDirectory()) namesSorted.insert(name);
@@ -119,16 +124,16 @@ static void getAllExprs(Evaluator & state,
are implemented using profiles). */
if (i == "manifest.nix") continue;
SourcePath path2 = path + i;
auto path2 = state.paths.checkSourcePath(path + i);
InputAccessor::Stat st;
try {
st = path2.resolveSymlinks().lstat();
st = path2.stat();
} catch (Error &) {
continue; // ignore dangling symlinks in ~/.nix-defexpr
}
if (isNixExpr(path2, st) && (st.type != InputAccessor::tRegular || path2.baseName().ends_with(".nix"))) {
if (isNixExpr(state.paths, path2, st) && (st.type != InputAccessor::tRegular || path2.baseName().ends_with(".nix"))) {
/* Strip off the `.nix' filename suffix (if applicable),
otherwise the attribute cannot be selected with the
`-A' option. Useful if you want to stick a Nix
@@ -138,7 +143,7 @@ static void getAllExprs(Evaluator & state,
attrName = std::string(attrName, 0, attrName.size() - 4);
if (!seen.insert(attrName).second) {
std::string suggestionMessage = "";
if (path2.path.abs().find("channels") != std::string::npos && path.path.abs().find("channels") != std::string::npos)
if (path2.to_string().find("channels") != std::string::npos && path.to_string().find("channels") != std::string::npos)
suggestionMessage = fmt("\nsuggestion: remove '%s' from either the root channels or the user channels", attrName);
printError("warning: name collision in input Nix expressions, skipping '%1%'"
"%2%", path2, suggestionMessage);
@@ -146,7 +151,7 @@ static void getAllExprs(Evaluator & state,
}
/* Load the expression on demand. */
auto vArg = state.mem.allocValue();
vArg->mkString(path2.path.abs());
vArg->mkString(path2.canonical().abs());
if (seen.size() == maxAttrs)
throw Error("too many Nix expressions in directory '%1%'", path);
attrs.alloc(attrName).mkApp(&state.builtins.get("import"), vArg);
@@ -160,11 +165,12 @@ static void getAllExprs(Evaluator & state,
static void loadSourceExpr(EvalState & state, const SourcePath & path, Value & v)
static void loadSourceExpr(EvalState & state, const SourcePath & path_, Value & v)
{
auto st = path.resolveSymlinks().lstat();
auto path = state.ctx.paths.checkSourcePath(path_);
auto st = path.stat();
if (isNixExpr(path, st))
if (isNixExpr(state.ctx.paths, path, st))
state.evalFile(path, v);
/* The path is a directory. Put the Nix expressions in the
+2 -5
View File
@@ -168,10 +168,7 @@ static int main_nix_instantiate(std::string programName, Strings argv)
if (findFile) {
for (auto & i : files) {
auto p = evaluator->paths.findFile(i);
if (auto fn = p.getPhysicalPath())
std::cout << fn->abs() << std::endl;
else
throw Error("'%s' has no physical path", p);
std::cout << p.canonical().abs() << std::endl;
}
return 0;
}
@@ -186,7 +183,7 @@ static int main_nix_instantiate(std::string programName, Strings argv)
for (auto & i : files) {
Expr & e = fromArgs
? evaluator->parseExprFromString(i, CanonPath::fromCwd())
: evaluator->parseExprFromFile(resolveExprPath(evaluator->paths.checkSourcePath(lookupFileArg(*evaluator, i))));
: evaluator->parseExprFromFile(evaluator->paths.resolveExprPath(lookupFileArg(*evaluator, i)));
processExpr(*state, attrPaths, parseOnly, strict, autoArgs,
evalOnly, outputKind, xmlOutputSourceLocation, e);
}
+1 -4
View File
@@ -7,9 +7,6 @@ namespace nix {
Strings editorFor(const SourcePath & file, uint32_t line)
{
auto path = file.getPhysicalPath();
if (!path)
throw Error("cannot open '%s' in an editor because it has no physical path", file);
auto editor = getEnv("EDITOR").value_or("cat");
auto args = tokenizeString<Strings>(editor);
if (line > 0 && (
@@ -18,7 +15,7 @@ Strings editorFor(const SourcePath & file, uint32_t line)
editor.find("vim") != std::string::npos ||
editor.find("kak") != std::string::npos))
args.push_back(fmt("+%d", line));
args.push_back(path->abs());
args.push_back(file.canonical().abs());
return args;
}
+1 -1
View File
@@ -47,7 +47,7 @@ std::optional<DerivedPathWithInfo> InstallableValue::trySinglePathToDerivedPaths
)
{
if (v.type() == nPath) {
auto storePath = fetchToStore(*evaluator->store, v.path());
auto storePath = fetchToStore(*evaluator->store, state.ctx.paths.checkSourcePath(v.path()));
return {{
.path = DerivedPath::Opaque {
.path = std::move(storePath),
+1 -1
View File
@@ -215,7 +215,7 @@ void SourceExprCommand::completeInstallable(EvalState & state, AddCompletions &
auto evaluator = getEvaluator();
Expr & e = evaluator->parseExprFromFile(
resolveExprPath(evaluator->paths.checkSourcePath(lookupFileArg(*evaluator, *file)))
state.ctx.paths.resolveExprPath(lookupFileArg(*evaluator, *file))
);
Value root;
+4 -4
View File
@@ -655,7 +655,7 @@ ProcessLineResult NixRepl::processLine(std::string line)
return {path, 0};
} else if (v.isLambda()) {
auto pos = evaluator.positions[v.lambda.fun->pos];
if (auto path = std::get_if<SourcePath>(&pos.origin))
if (auto path = std::get_if<CheckedSourcePath>(&pos.origin))
return {*path, pos.line};
else
throw Error("'%s' cannot be shown in an editor", pos);
@@ -676,7 +676,7 @@ ProcessLineResult NixRepl::processLine(std::string line)
// Reload right after exiting the editor if path is not in store
// Store is immutable, so there could be no changes, so there's no need to reload
if (!evaluator.store->isInStore(path.resolveSymlinks().path.abs())) {
if (!evaluator.store->isInStore(canonPath(path.canonical().abs(), true))) {
state.resetFileCache();
reloadFiles();
}
@@ -820,7 +820,7 @@ ProcessLineResult NixRepl::processLine(std::string line)
logger->cout(trim(renderMarkdownToTerminal(markdown)));
} else if (v.isLambda()) {
auto pos = evaluator.positions[v.lambda.fun->pos];
if (auto path = std::get_if<SourcePath>(&pos.origin)) {
if (auto path = std::get_if<CheckedSourcePath>(&pos.origin)) {
// Path and position have now been obtained, feed to nix-doc library to get data.
auto docComment = lambdaDocsForPos(*path, pos);
if (!docComment) {
@@ -1109,7 +1109,7 @@ void NixRepl::evalString(std::string s, Value & v)
Value * NixRepl::evalFile(SourcePath & path)
{
auto & expr = evaluator.parseExprFromFile(path, staticEnv);
auto & expr = evaluator.parseExprFromFile(evaluator.paths.checkSourcePath(path), staticEnv);
Value * result(evaluator.mem.allocValue());
expr.eval(state, *env, *result);
state.forceValue(*result, result->determinePos(noPos));
+1 -1
View File
@@ -178,7 +178,7 @@ std::pair<SourcePath, uint32_t> findPackageFilename(EvalState & state, Value & v
NixStringContext context;
auto path = state.coerceToPath(noPos, *v2, context, "while evaluating the 'meta.position' attribute of a derivation");
auto fn = path.path.abs();
auto fn = path.canonical().abs();
auto fail = [fn]() {
throw ParseError("cannot parse 'meta.position' attribute '%s'", fn);
+2 -2
View File
@@ -439,8 +439,8 @@ Value & AttrCursor::forceValue(EvalState & state)
cachedValue = {root->db->setString(getKey(), v.string.s, v.string.context),
string_t{v.string.s, {}}};
else if (v.type() == nPath) {
auto path = v.path().path;
cachedValue = {root->db->setString(getKey(), path.abs()), string_t{path.abs(), {}}};
auto path = v.path().canonical().abs();
cachedValue = {root->db->setString(getKey(), path), string_t{path, {}}};
}
else if (v.type() == nBool)
cachedValue = {root->db->setBool(getKey(), v.boolean), v.boolean};
+93 -45
View File
@@ -282,7 +282,7 @@ EvalPaths::EvalPaths(
, errors(errors)
{
if (evalSettings.restrictEval || evalSettings.pureEval) {
allowedPaths = std::optional(PathSet());
allowedPaths = AllowedPath{.allowAllChildren = false};
for (auto & i : searchPath_.elements) {
auto r = resolveSearchPathPath(i.path);
@@ -362,14 +362,23 @@ EvalState::~EvalState()
void EvalPaths::allowPath(const Path & path)
{
if (allowedPaths)
allowedPaths->insert(path);
if (!allowedPaths) {
return;
}
CanonPath p(path);
auto * level = &*allowedPaths;
for (const auto & entry : p) {
level = &level->children.emplace(std::piecewise_construct, std::tuple(entry), std::tuple())
.first->second;
}
level->allowAllChildren = true;
}
void EvalPaths::allowPath(const StorePath & storePath)
{
if (allowedPaths)
allowedPaths->insert(store->toRealPath(storePath));
allowPath(store->toRealPath(storePath));
}
void EvalPaths::allowAndSetStorePathString(const StorePath & storePath, Value & v)
@@ -379,50 +388,86 @@ void EvalPaths::allowAndSetStorePathString(const StorePath & storePath, Value &
mkStorePathString(storePath, v);
}
SourcePath EvalPaths::checkSourcePath(const SourcePath & path_)
CheckedSourcePath EvalPaths::checkSourcePath(const SourcePath & path_)
{
if (!allowedPaths) return path_;
if (!allowedPaths) return auto(path_).unsafeIntoChecked();
auto i = resolvedPaths.find(path_.path.abs());
auto i = resolvedPaths.find(path_.canonical().abs());
if (i != resolvedPaths.end())
return i->second;
bool found = false;
/* First canonicalize the path without symlinks, so we make sure an
* attacker can't append ../../... to a path that would be in allowedPaths
* and thus leak symlink targets.
*/
Path abspath = canonPath(path_.path.abs());
const CanonPath abspath{path_.canonical().abs()};
if (abspath.starts_with(corepkgsPrefix)) return CanonPath(abspath);
for (auto & i : *allowedPaths) {
if (isDirOrInDir(abspath, i)) {
found = true;
break;
}
if (abspath.abs().starts_with(corepkgsPrefix)) {
return SourcePath(std::move(abspath)).unsafeIntoChecked();
}
if (!found) {
auto modeInformation = evalSettings.pureEval
? "in pure eval mode (use '--impure' to override)"
: "in restricted mode";
throw RestrictedPathError("access to absolute path '%1%' is forbidden %2%", abspath, modeInformation);
}
/* Resolve symlinks. */
/* Resolve symlinks. This is mostly restricted copy of canonPath with
resolveSymlinks=true, because we need access to intermediat paths. */
debug("checking access to '%s'", abspath);
SourcePath path = CanonPath(canonPath(abspath, true));
for (auto & i : *allowedPaths) {
if (isDirOrInDir(path.path.abs(), i)) {
resolvedPaths.insert_or_assign(path_.path.abs(), path);
return path;
}
/* Count the number of times we follow a symlink and stop at some
arbitrary (but high) limit to prevent infinite loops. */
unsigned int followCount = 0, maxFollow = 1024;
std::optional<CanonPath> componentsBacking;
std::vector<std::string_view> components(abspath.begin(), abspath.end());
retry:
if (++followCount >= maxFollow) {
throw Error("infinite symlink recursion in path '%1%'", path_);
}
throw RestrictedPathError("access to canonical path '%1%' is forbidden in restricted mode", path);
// TODO: tests for this stuff
const auto * level = &*allowedPaths;
CheckedSourcePath current = SourcePath(CanonPath::root).unsafeIntoChecked();
for (auto ct = components.begin(); ct != components.end(); ct++) {
auto & p = *ct;
// an empty level means all subpaths are allowed, propagate this forwards
// by setting level=nullptr for the subsequent checks. a symlink will set
// level to the "VFS" root and restart the check with a the resolved path
if (level) {
if (level->allowAllChildren) {
level = nullptr;
} else if (auto it = level->children.find(p); it != level->children.end()) {
level = &it->second;
} else {
goto failed;
}
}
auto next = (current + p).unsafeIntoChecked();
auto st = next.maybeLstat();
// resolve symlinks, treating nonexistant components like regular directories.
// this mirrors canonPath behavior and is necessary for `builtins.pathExists`.
if (st && st->type == InputAccessor::tSymlink) {
auto target = next.readLink();
auto levelResolved = target.starts_with("/")
? CanonPath(target)
: CanonPath(current.canonical().abs() + "/" + target);
for (ct++; ct != components.end(); ct++) {
levelResolved.push(*ct);
}
components = {levelResolved.begin(), levelResolved.end()};
componentsBacking = std::move(levelResolved);
followCount += 1;
goto retry;
}
current = std::move(next);
}
resolvedPaths.insert_or_assign(path_.canonical().abs(), current);
return current;
failed:
auto modeInformation = evalSettings.pureEval
? "in pure eval mode (use '--impure' to override)"
: "in restricted mode";
throw RestrictedPathError("access to absolute path '%1%' is forbidden %2%", abspath, modeInformation);
}
@@ -803,9 +848,9 @@ void Evaluator::evalLazily(Expr & e, Value & v)
void EvalState::mkPos(Value & v, PosIdx p)
{
auto origin = ctx.positions.originOf(p);
if (auto path = std::get_if<SourcePath>(&origin)) {
if (auto path = std::get_if<CheckedSourcePath>(&origin)) {
auto attrs = ctx.buildBindings(3);
attrs.alloc(ctx.s.file).mkString(path->path.abs());
attrs.alloc(ctx.s.file).mkString(path->to_string());
makePositionThunks(*this, p, attrs.alloc(ctx.s.line), attrs.alloc(ctx.s.column));
v.mkAttrs(attrs);
} else
@@ -951,7 +996,7 @@ void EvalState::evalFile(const SourcePath & path_, Value & v)
return;
}
auto resolvedPath = resolveExprPath(path);
auto resolvedPath = ctx.paths.resolveExprPath(path);
if (auto i = ctx.caches.fileEval.find(resolvedPath); i != ctx.caches.fileEval.end()) {
v = i->second->result;
return;
@@ -2269,7 +2314,7 @@ BackedStringView EvalState::coerceToString(
v._path
: copyToStore
? ctx.store->printStorePath(ctx.paths.copyPathToStore(context, v.path(), ctx.repair))
: std::string(v.path().path.abs());
: v.path().to_string();
}
if (v.type() == nAttrs) {
@@ -2339,7 +2384,7 @@ BackedStringView EvalState::coerceToString(
StorePath EvalPaths::copyPathToStore(NixStringContext & context, const SourcePath & path, RepairFlag repair)
{
if (nix::isDerivation(path.path.abs()))
if (nix::isDerivation(path.canonical().abs()))
errors.make<EvalError>("file names are not allowed to end in '%1%'", drvExtension).debugThrow();
auto i = srcToStore.find(path);
@@ -2347,7 +2392,7 @@ StorePath EvalPaths::copyPathToStore(NixStringContext & context, const SourcePat
auto dstPath = i != srcToStore.end()
? i->second
: [&]() {
auto dstPath = fetchToStore(*store, path, path.baseName(), FileIngestionMethod::Recursive, nullptr, repair);
auto dstPath = fetchToStore(*store, checkSourcePath(path), path.baseName(), FileIngestionMethod::Recursive, nullptr, repair);
allowPath(dstPath);
srcToStore.insert_or_assign(path, dstPath);
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(dstPath));
@@ -2621,7 +2666,7 @@ void Evaluator::printStatistics()
else
obj["name"] = nullptr;
if (auto pos = positions[fun->pos]) {
if (auto path = std::get_if<SourcePath>(&pos.origin))
if (auto path = std::get_if<CheckedSourcePath>(&pos.origin))
obj["file"] = path->to_string();
obj["line"] = pos.line;
obj["column"] = pos.column;
@@ -2636,7 +2681,7 @@ void Evaluator::printStatistics()
for (auto & i : stats.attrSelects) {
json obj = json::object();
if (auto pos = positions[i.first]) {
if (auto path = std::get_if<SourcePath>(&pos.origin))
if (auto path = std::get_if<CheckedSourcePath>(&pos.origin))
obj["file"] = path->to_string();
obj["line"] = pos.line;
obj["column"] = pos.column;
@@ -2661,8 +2706,9 @@ void Evaluator::printStatistics()
}
SourcePath resolveExprPath(SourcePath path)
CheckedSourcePath EvalPaths::resolveExprPath(SourcePath path_)
{
auto path = checkSourcePath(path_);
unsigned int followCount = 0, maxFollow = 1024;
/* If `path' is a symlink, follow it. This is so that relative
@@ -2672,24 +2718,26 @@ SourcePath resolveExprPath(SourcePath path)
if (++followCount >= maxFollow)
throw Error("too many symbolic links encountered while traversing the path '%s'", path);
if (path.lstat().type != InputAccessor::tSymlink) break;
path = {CanonPath(path.readLink(), path.path.parent().value_or(CanonPath::root))};
path = checkSourcePath(
CanonPath(path.readLink(), path.canonical().parent().value_or(CanonPath::root))
);
}
/* If `path' refers to a directory, append `/default.nix'. */
if (path.lstat().type == InputAccessor::tDirectory)
return path + "default.nix";
return checkSourcePath(path + "default.nix");
return path;
}
Expr & Evaluator::parseExprFromFile(const SourcePath & path)
Expr & Evaluator::parseExprFromFile(const CheckedSourcePath & path)
{
return parseExprFromFile(path, builtins.staticEnv);
}
Expr & Evaluator::parseExprFromFile(const SourcePath & path, std::shared_ptr<StaticEnv> & staticEnv)
Expr & Evaluator::parseExprFromFile(const CheckedSourcePath & path, std::shared_ptr<StaticEnv> & staticEnv)
{
auto buffer = path.readFile();
return *parse(buffer.data(), buffer.size(), Pos::Origin(path), path.parent(), staticEnv);
+24 -11
View File
@@ -376,13 +376,26 @@ public:
const SearchPath & searchPath() const { return searchPath_; }
private:
struct AllowedPath
{
struct ComponentLess : std::less<>
{
// we'll only use this for string-likes, it's fine. trust me sis.
using is_transparent = void;
};
std::map<std::string, AllowedPath, ComponentLess> children;
bool allowAllChildren = false;
};
/**
* The allowed filesystem paths in restricted or pure evaluation
* mode.
*/
std::optional<PathSet> allowedPaths;
std::optional<AllowedPath> allowedPaths;
private:
/* Cache for calls to addToStore(); maps source paths to the store
paths. */
@@ -393,7 +406,7 @@ private:
/**
* Cache used by checkSourcePath().
*/
std::unordered_map<Path, SourcePath> resolvedPaths;
std::unordered_map<Path, CheckedSourcePath> resolvedPaths;
public:
/**
@@ -416,7 +429,12 @@ public:
* Check whether access to a path is allowed and throw an error if
* not. Otherwise return the canonicalised path.
*/
SourcePath checkSourcePath(const SourcePath & path);
CheckedSourcePath checkSourcePath(const SourcePath & path);
/**
* If `path` refers to a directory, then append "/default.nix".
*/
CheckedSourcePath resolveExprPath(SourcePath path);
void checkURI(const std::string & uri);
@@ -531,8 +549,8 @@ public:
/**
* Parse a Nix expression from the specified file.
*/
Expr & parseExprFromFile(const SourcePath & path);
Expr & parseExprFromFile(const SourcePath & path, std::shared_ptr<StaticEnv> & staticEnv);
Expr & parseExprFromFile(const CheckedSourcePath & path);
Expr & parseExprFromFile(const CheckedSourcePath & path, std::shared_ptr<StaticEnv> & staticEnv);
/**
* Parse a Nix expression from the specified string.
@@ -848,11 +866,6 @@ private:
std::string_view showType(ValueType type, bool withArticle = true);
std::string showType(const Value & v);
/**
* If `path` refers to a directory, then append "/default.nix".
*/
SourcePath resolveExprPath(SourcePath path);
static constexpr std::string_view corepkgsPrefix{"/__corepkgs__/"};
+2 -2
View File
@@ -242,8 +242,8 @@ static Flake getFlake(
};
// FIXME: symlink attack
auto resolvedFlakeFile = resolveExprPath(state.ctx.paths.checkSourcePath(CanonPath(flakeFile)));
Expr & flakeExpr = state.ctx.parseExprFromFile(state.ctx.paths.checkSourcePath(resolvedFlakeFile));
auto resolvedFlakeFile = state.ctx.paths.resolveExprPath(CanonPath(flakeFile));
Expr & flakeExpr = state.ctx.parseExprFromFile(resolvedFlakeFile);
// Enforce that 'flake.nix' is a direct attrset, not a computation.
if (!(dynamic_cast<ExprAttrs *>(&flakeExpr))) {
+1 -1
View File
@@ -605,7 +605,7 @@ template<> struct BuildAST<grammar::v1::path::interpolation> : BuildAST<grammar:
template<> struct BuildAST<grammar::v1::path::anchor> {
static void apply(const auto & in, StringState & s, State & ps) {
Path path(absPath(in.string(), ps.basePath.path.abs()));
Path path(absPath(in.string(), ps.basePath.canonical().abs()));
/* add back in the trailing '/' to the first segment */
if (in.string_view().ends_with('/') && in.size() > 1)
path += "/";
+34 -29
View File
@@ -117,12 +117,7 @@ StringMap EvalPaths::realiseContext(const NixStringContext & context)
return res;
}
struct RealisePathFlags {
// Whether to check that the path is allowed in pure eval mode
bool checkForPureEval = true;
};
static SourcePath realisePath(EvalState & state, const PosIdx pos, Value & v, const RealisePathFlags flags = {})
static auto realisePath(EvalState & state, const PosIdx pos, Value & v, auto checkFn)
{
NixStringContext context;
@@ -131,17 +126,20 @@ static SourcePath realisePath(EvalState & state, const PosIdx pos, Value & v, co
try {
StringMap rewrites = state.ctx.paths.realiseContext(context);
auto realPath = CanonPath(state.ctx.paths.toRealPath(rewriteStrings(path.path.abs(), rewrites), context));
return flags.checkForPureEval
? state.ctx.paths.checkSourcePath(realPath)
: realPath;
return checkFn(SourcePath(CanonPath(
state.ctx.paths.toRealPath(rewriteStrings(path.canonical().abs(), rewrites), context)
)));
} catch (Error & e) {
e.addTrace(state.ctx.positions[pos], "while realising the context of path '%s'", path);
throw;
}
}
static CheckedSourcePath realisePath(EvalState & state, const PosIdx pos, Value & v)
{
return realisePath(state, pos, v, [&](auto p) { return state.ctx.paths.checkSourcePath(p); });
}
/**
* Add and attribute to the given attribute map from the output name to
* the output path, or a placeholder.
@@ -176,7 +174,7 @@ static void mkOutputString(
static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * vScope, Value & v)
{
auto path = realisePath(state, pos, vPath);
auto path2 = path.path.abs();
auto path2 = path.canonical().abs();
// FIXME
auto isValidDerivationInStore = [&]() -> std::optional<StorePath> {
@@ -251,7 +249,7 @@ static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * v
// args[0]->attrs is already sorted.
debug("evaluating file '%1%'", path);
Expr & e = state.ctx.parseExprFromFile(resolveExprPath(path), staticEnv);
Expr & e = state.ctx.parseExprFromFile(state.ctx.paths.resolveExprPath(path), staticEnv);
e.eval(state, *env, v);
}
@@ -283,7 +281,7 @@ void prim_importNative(EvalState & state, const PosIdx pos, Value * * args, Valu
std::string sym(state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument passed to builtins.importNative"));
void *handle = dlopen(path.path.c_str(), RTLD_LAZY | RTLD_LOCAL);
void *handle = dlopen(path.canonical().c_str(), RTLD_LAZY | RTLD_LOCAL);
if (!handle)
state.ctx.errors.make<EvalError>("could not open '%1%': %2%", path, dlerror()).debugThrow();
@@ -1153,7 +1151,7 @@ static void prim_toPath(EvalState & state, const PosIdx pos, Value * * args, Val
{
NixStringContext context;
auto path = state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to builtins.toPath");
v.mkString(path.path.abs(), context);
v.mkString(path.to_string(), context);
}
/* Allow a valid store path to be used in an expression. This is
@@ -1173,7 +1171,7 @@ static void prim_storePath(EvalState & state, const PosIdx pos, Value * * args,
).atPos(pos).debugThrow();
NixStringContext context;
auto path = state.ctx.paths.checkSourcePath(state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to builtins.storePath")).path;
auto path = state.ctx.paths.checkSourcePath(state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to builtins.storePath")).canonical();
/* Resolve symlinks in path, unless path itself is a symlink
directly in the store. The latter condition is necessary so
e.g. nix-push does the right thing. */
@@ -1198,7 +1196,7 @@ static void prim_pathExists(EvalState & state, const PosIdx pos, Value * * args,
cant just catch the exception here because we still want to
throw if something in the evaluation of `arg` tries to
access an unauthorized path). */
auto path = realisePath(state, pos, arg, { .checkForPureEval = false });
auto path = realisePath(state, pos, arg, std::identity{});
/* SourcePath doesn't know about trailing slash. */
auto mustBeDir = arg.type() == nString
@@ -1206,12 +1204,13 @@ static void prim_pathExists(EvalState & state, const PosIdx pos, Value * * args,
|| arg.str().ends_with("/."));
try {
auto checked = state
.ctx.paths
.checkSourcePath(path)
.resolveSymlinks(mustBeDir ? SymlinkResolution::Full : SymlinkResolution::Ancestors);
auto checked = state.ctx.paths.checkSourcePath(path);
auto st = checked.maybeLstat();
// previously we fully resolved symlinks in the mustBeDir case or in pure eval
// mode (by accident, since checkSourcePath does this in that case), and up to
// the last component otherwise. this is equivalent to calling stat and lstat,
// respectively. (in neither case do intermediate symlinks affect the result.)
auto st = mustBeDir ? checked.maybeStat() : checked.maybeLstat();
auto exists = st && (!mustBeDir || st->type == InputAccessor::tDirectory);
v.mkBool(exists);
} catch (SysError & e) {
@@ -1241,7 +1240,7 @@ static void prim_dirOf(EvalState & state, const PosIdx pos, Value * * args, Valu
state.forceValue(*args[0], pos);
if (args[0]->type() == nPath) {
auto path = args[0]->path();
v.mkPath(path.path.isRoot() ? path : path.parent());
v.mkPath(path.canonical().isRoot() ? path : path.parent());
} else {
NixStringContext context;
auto path = state.coerceToString(pos, *args[0], context,
@@ -1263,9 +1262,9 @@ static void prim_readFile(EvalState & state, const PosIdx pos, Value * * args, V
path
).atPos(pos).debugThrow();
StorePathSet refs;
if (state.ctx.store->isInStore(path.path.abs())) {
if (state.ctx.store->isInStore(path.canonical().abs())) {
try {
refs = state.ctx.store->queryPathInfo(state.ctx.store->toStorePath(path.path.abs()).first)->references;
refs = state.ctx.store->queryPathInfo(state.ctx.store->toStorePath(path.canonical().abs()).first)->references;
} catch (Error &) { // FIXME: should be InvalidPathError
}
// Re-scan references to filter down to just the ones that actually occur in the file.
@@ -1513,7 +1512,7 @@ static void addPath(
path = evalSettings.pureEval && expectedHash
? path
: state.ctx.paths.checkSourcePath(CanonPath(path)).path.abs();
: state.ctx.paths.checkSourcePath(CanonPath(path)).canonical().abs();
PathFilter filter = filterFun ? ([&](const Path & path) {
auto st = lstat(path);
@@ -1547,7 +1546,13 @@ static void addPath(
if (!expectedHash || !state.ctx.store->isValidPath(*expectedStorePath)) {
auto dstPath = fetchToStore(
*state.ctx.store, CanonPath(path), name, method, &filter, state.ctx.repair);
*state.ctx.store,
state.ctx.paths.checkSourcePath(CanonPath(path)),
name,
method,
&filter,
state.ctx.repair
);
if (expectedHash && expectedStorePath != dstPath)
state.ctx.errors.make<EvalError>(
"store path mismatch in (possibly filtered) path added from '%s'",
@@ -1569,7 +1574,7 @@ static void prim_filterSource(EvalState & state, const PosIdx pos, Value * * arg
auto path = state.coerceToPath(pos, *args[1], context,
"while evaluating the second argument (the path to filter) passed to builtins.filterSource");
state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filterSource");
addPath(state, pos, path.baseName(), path.path.abs(), args[0], FileIngestionMethod::Recursive, std::nullopt, v, context);
addPath(state, pos, path.baseName(), path.canonical().abs(), args[0], FileIngestionMethod::Recursive, std::nullopt, v, context);
}
static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value & v)
@@ -1608,7 +1613,7 @@ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value
if (name.empty())
name = path->baseName();
addPath(state, pos, name, path->path.abs(), filterFun, method, expectedHash, v, context);
addPath(state, pos, name, path->canonical().abs(), filterFun, method, expectedHash, v, context);
}
+1 -1
View File
@@ -39,7 +39,7 @@ json printValueAsJSON(EvalState & state, bool strict,
out = state.ctx.store->printStorePath(
state.ctx.paths.copyPathToStore(context, v.path(), state.ctx.repair));
else
out = v.path().path.abs();
out = v.path().to_string();
break;
case nNull:
+2 -2
View File
@@ -21,8 +21,8 @@ static void printValueAsXML(EvalState & state, bool strict, bool location,
static void posToXML(EvalState & state, XMLAttrs & xmlAttrs, const Pos & pos)
{
if (auto path = std::get_if<SourcePath>(&pos.origin))
xmlAttrs["path"] = path->path.abs();
if (auto path = std::get_if<CheckedSourcePath>(&pos.origin))
xmlAttrs["path"] = path->to_string();
xmlAttrs["line"] = fmt("%1%", pos.line);
xmlAttrs["column"] = fmt("%1%", pos.column);
}
+1 -1
View File
@@ -100,7 +100,7 @@ void Value::mkStringMove(const char * s, const NixStringContext & context)
void Value::mkPath(const SourcePath & path)
{
mkPath(gcCopyStringIfNeeded(path.path.abs()));
*this = Value(NewValueAs::path, path);
}
}
+1 -1
View File
@@ -347,7 +347,7 @@ public:
/// dynamic (GC) allocation to do so.
Value(path_t, SourcePath const & path)
: internalType(tPath)
, _path(gcCopyStringIfNeeded(path.path.abs()))
, _path(gcCopyStringIfNeeded(path.canonical().abs()))
, _path_pad(0)
{ }
+3 -3
View File
@@ -6,7 +6,7 @@ namespace nix {
StorePath fetchToStore(
Store & store,
const SourcePath & path,
const CheckedSourcePath & path,
std::string_view name,
FileIngestionMethod method,
PathFilter * filter,
@@ -18,8 +18,8 @@ StorePath fetchToStore(
return
settings.readOnlyMode
? store.computeStorePathForPath(name, path.path.abs(), method, HashType::SHA256, filter2).first
: store.addToStore(name, path.path.abs(), method, HashType::SHA256, filter2, repair);
? store.computeStorePathForPath(name, path.canonical().abs(), method, HashType::SHA256, filter2).first
: store.addToStore(name, path.canonical().abs(), method, HashType::SHA256, filter2, repair);
}
+1 -1
View File
@@ -13,7 +13,7 @@ namespace nix {
*/
StorePath fetchToStore(
Store & store,
const SourcePath & path,
const CheckedSourcePath & path,
std::string_view name = "source",
FileIngestionMethod method = FileIngestionMethod::Recursive,
PathFilter * filter = nullptr,
+14 -1
View File
@@ -84,6 +84,11 @@ public:
struct Iterator
{
using difference_type = void;
using value_type = std::string_view;
using reference = std::string_view;
using iterator_category = std::input_iterator_tag;
std::string_view remaining;
size_t slash;
@@ -101,7 +106,7 @@ public:
const std::string_view operator * () const
{ return remaining.substr(0, slash); }
void operator ++ ()
Iterator & operator ++ ()
{
if (slash == remaining.npos)
remaining = remaining.substr(remaining.size());
@@ -109,6 +114,14 @@ public:
remaining = remaining.substr(slash + 1);
slash = remaining.find('/');
}
return *this;
}
Iterator operator++(int)
{
auto result = *this;
++*this;
return result;
}
};
+1 -1
View File
@@ -62,7 +62,7 @@ std::optional<std::string> Pos::getSource() const
// Get rid of the null terminators added by the parser.
return std::string(s.source->c_str());
},
[](const SourcePath & path) -> std::optional<std::string> {
[](const CheckedSourcePath & path) -> std::optional<std::string> {
try {
return path.readFile();
} catch (Error &) {
+1 -1
View File
@@ -42,7 +42,7 @@ struct Pos
auto operator<=>(const Hidden &) const = default;
};
typedef std::variant<std::monostate, Stdin, String, SourcePath, Hidden> Origin;
typedef std::variant<std::monostate, Stdin, String, CheckedSourcePath, Hidden> Origin;
Origin origin = std::monostate();
+28 -46
View File
@@ -1,4 +1,5 @@
#include "lix/libutil/source-path.hh"
#include "file-system.hh"
#include "lix/libutil/strings.hh"
namespace nix {
@@ -21,9 +22,8 @@ SourcePath SourcePath::parent() const
return std::move(*p);
}
InputAccessor::Stat SourcePath::lstat() const
static InputAccessor::Stat convertStat(const struct stat & st)
{
auto st = nix::lstat(path.abs());
return InputAccessor::Stat {
.type =
S_ISREG(st.st_mode) ? InputAccessor::tRegular :
@@ -34,15 +34,35 @@ InputAccessor::Stat SourcePath::lstat() const
};
}
std::optional<InputAccessor::Stat> SourcePath::maybeLstat() const
InputAccessor::Stat CheckedSourcePath::lstat() const
{
// FIXME: merge these into one operation.
if (!pathExists())
return {};
return lstat();
return convertStat(nix::lstat(path.abs()));
}
InputAccessor::DirEntries SourcePath::readDirectory() const
std::optional<InputAccessor::Stat> CheckedSourcePath::maybeLstat() const
{
if (auto st = nix::maybeLstat(path.abs())) {
return convertStat(*st);
} else {
return std::nullopt;
}
}
InputAccessor::Stat CheckedSourcePath::stat() const
{
return convertStat(nix::stat(path.abs()));
}
std::optional<InputAccessor::Stat> CheckedSourcePath::maybeStat() const
{
if (auto st = nix::maybeStat(path.abs())) {
return convertStat(*st);
} else {
return std::nullopt;
}
}
InputAccessor::DirEntries CheckedSourcePath::readDirectory() const
{
InputAccessor::DirEntries res;
for (auto & entry : nix::readDirectory(path.abs())) {
@@ -58,42 +78,4 @@ InputAccessor::DirEntries SourcePath::readDirectory() const
return res;
}
SourcePath SourcePath::resolveSymlinks(SymlinkResolution mode) const
{
SourcePath res(CanonPath::root);
int linksAllowed = 1024;
std::list<std::string> todo;
for (auto & c : path)
todo.push_back(std::string(c));
bool resolve_last = mode == SymlinkResolution::Full;
while (!todo.empty()) {
auto c = *todo.begin();
todo.pop_front();
if (c == "" || c == ".")
;
else if (c == "..")
res.path.pop();
else {
res.path.push(c);
if (resolve_last || !todo.empty()) {
if (auto st = res.maybeLstat(); st && st->type == InputAccessor::tSymlink) {
if (!linksAllowed--)
throw Error("infinite symlink recursion in path '%s'", path);
auto target = res.readLink();
res.path.pop();
if (target.starts_with("/"))
res.path = CanonPath::root;
todo.splice(todo.begin(), tokenizeString<std::list<std::string>>(target, "/"));
}
}
}
}
return res;
}
}
+88 -86
View File
@@ -14,36 +14,17 @@
namespace nix {
/**
* Note there is a decent chance this type soon goes away because the problem is solved another way.
* See the discussion in https://github.com/NixOS/nix/pull/9985.
*/
enum class SymlinkResolution {
/**
* Resolve symlinks in the ancestors only.
*
* Only the last component of the result is possibly a symlink.
*/
Ancestors,
/**
* Resolve symlinks fully, realpath(3)-style.
*
* No component of the result will be a symlink.
*/
Full,
};
class CheckedSourcePath;
/**
* An abstraction for accessing source files during
* evaluation. Currently, it's just a wrapper around `CanonPath` that
* accesses files in the regular filesystem, but in the future it will
* support fetching files in other ways.
* An abstraction for manipulating path names during evaluation.
*/
struct SourcePath
{
protected:
CanonPath path;
public:
SourcePath(CanonPath path)
: path(std::move(path))
{ }
@@ -56,63 +37,16 @@ struct SourcePath
*/
SourcePath parent() const;
/**
* If this `SourcePath` denotes a regular file (not a symlink),
* return its contents; otherwise throw an error.
*/
std::string readFile() const
{ return nix::readFile(path.abs()); }
/**
* Return whether this `SourcePath` denotes a file (of any type)
* that exists
*/
bool pathExists() const
{ return nix::pathExists(path.abs()); }
/**
* Return stats about this `SourcePath`, or throw an exception if
* it doesn't exist.
*/
InputAccessor::Stat lstat() const;
/**
* Return stats about this `SourcePath`, or std::nullopt if it
* doesn't exist.
*/
std::optional<InputAccessor::Stat> maybeLstat() const;
/**
* If this `SourcePath` denotes a directory (not a symlink),
* return its directory entries; otherwise throw an error.
*/
InputAccessor::DirEntries readDirectory() const;
/**
* If this `SourcePath` denotes a symlink, return its target;
* otherwise throw an error.
*/
std::string readLink() const
{ return nix::readLink(path.abs()); }
/**
* Dump this `SourcePath` to `sink` as a NAR archive.
*/
void dumpPath(
Sink & sink,
PathFilter & filter = defaultPathFilter) const
{ sink << nix::dumpPath(path.abs(), filter); }
/**
* Return the location of this path in the "real" filesystem, if
* it has a physical location.
*/
std::optional<CanonPath> getPhysicalPath() const
{ return path; }
const CanonPath & canonical() const { return path; }
std::string to_string() const
{ return path.abs(); }
/**
* Converts this `SourcePath` into a checked `SourcePath`, consuming it.
*/
CheckedSourcePath unsafeIntoChecked();
/**
* Append a `CanonPath` to this path.
*/
@@ -141,18 +75,86 @@ struct SourcePath
{
return path < x.path;
}
/**
* Resolve any symlinks in this `SourcePath` according to the
* given resolution mode.
*
* @param mode might only be a temporary solution for this.
* See the discussion in https://github.com/NixOS/nix/pull/9985.
*/
SourcePath resolveSymlinks(
SymlinkResolution mode = SymlinkResolution::Full) const;
};
std::ostream & operator << (std::ostream & str, const SourcePath & path);
/**
* An abstraction for accessing source files during
* evaluation. Currently, it's just a wrapper around `CanonPath` that
* accesses files in the regular filesystem, but in the future it will
* support fetching files in other ways.
*/
class CheckedSourcePath : public SourcePath
{
friend struct SourcePath;
CheckedSourcePath(CanonPath path): SourcePath(std::move(path)) {}
public:
/**
* If this `SourcePath` denotes a regular file (not a symlink),
* return its contents; otherwise throw an error.
*/
std::string readFile() const
{ return nix::readFile(path.abs()); }
/**
* Return whether this `SourcePath` denotes a file (of any type)
* that exists
*/
bool pathExists() const
{ return nix::pathExists(path.abs()); }
/**
* Return stats about this `SourcePath`, or throw an exception if
* it doesn't exist.
*/
InputAccessor::Stat lstat() const;
/**
* Return stats about this `SourcePath`, or std::nullopt if it
* doesn't exist.
*/
std::optional<InputAccessor::Stat> maybeLstat() const;
/**
* Return stats about this `SourcePath`, or throw an exception if
* it doesn't exist. Symlinks are resolved by this function.
*/
InputAccessor::Stat stat() const;
/**
* Return stats about this `SourcePath`, or std::nullopt if it
* doesn't exist. Symlinks are resolved by this function.
*/
std::optional<InputAccessor::Stat> maybeStat() const;
/**
* If this `SourcePath` denotes a directory (not a symlink),
* return its directory entries; otherwise throw an error.
*/
InputAccessor::DirEntries readDirectory() const;
/**
* If this `SourcePath` denotes a symlink, return its target;
* otherwise throw an error.
*/
std::string readLink() const
{ return nix::readLink(path.abs()); }
/**
* Dump this `SourcePath` to `sink` as a NAR archive.
*/
void dumpPath(
Sink & sink,
PathFilter & filter = defaultPathFilter) const
{ sink << nix::dumpPath(path.abs(), filter); }
};
inline CheckedSourcePath SourcePath::unsafeIntoChecked()
{
return CheckedSourcePath(std::move(path));
}
}
+3 -1
View File
@@ -542,7 +542,9 @@ struct CmdFlakeCheck : FlakeCommand
if (auto attr = v.attrs->get(evaluator->symbols.create("path"))) {
if (attr->name == evaluator->symbols.create("path")) {
NixStringContext context;
auto path = state->coerceToPath(attr->pos, *attr->value, context, "");
auto path = state->ctx.paths.checkSourcePath(
state->coerceToPath(attr->pos, *attr->value, context, "")
);
if (!path.pathExists())
throw Error("template '%s' refers to a non-existent path '%s'", attrPath, path);
// TODO: recursively check the flake in 'path'.
+1 -1
View File
@@ -201,7 +201,7 @@ static int main_nix_prefetch_url(std::string programName, Strings argv)
} else {
Value vRoot;
state->evalFile(
resolveExprPath(
evaluator->paths.resolveExprPath(
lookupFileArg(*evaluator, args.empty() ? "." : args[0])),
vRoot);
Value & v(*findAlongAttrPath(*state, attrPath, autoArgs, vRoot).first);
+67
View File
@@ -0,0 +1,67 @@
from pathlib import Path
from textwrap import dedent
from functional2.testlib.fixtures import Nix
import re
def test_purity_traversal(nix: Nix, tmp_path: Path):
ERROR_RE = re.compile(r"error: access to absolute path '.+' is forbidden in pure eval mode")
flake_dir = tmp_path / 'flake'
flake_dir.mkdir()
evilpath = tmp_path / 'sekrit.txt'
evilpath.write_text('kitty kitty')
evilnix = tmp_path / 'default.nix'
evilnix.write_text('"woof"')
goodnix = flake_dir / 'good.nix'
goodnix.write_text("1")
nested_dir = flake_dir / 'nested' / 'nested2'
nested_dir.mkdir(parents=True)
(nested_dir / 'good.nix').write_text("1")
(flake_dir / 'evil-link').symlink_to(evilpath)
(flake_dir / 'evil-default-nix').symlink_to(tmp_path)
(flake_dir / 'less-evil-link').symlink_to(tmp_path / 'link-to-flake')
(tmp_path / 'link-to-flake').symlink_to(flake_dir / 'flake.nix')
(flake_dir / 'nested-good.nix').symlink_to('nested/nested2/good.nix')
(flake_dir / 'nested-bad.nix').symlink_to('nested/../../nested2/good.nix')
(flake_dir / 'flake.nix').write_text(dedent("""
{
inputs = {};
outputs = inputs: {
bad1 = "${@ABSPATH@}";
bad2 = builtins.readFile "${@ABSPATH@}";
bad3 = builtins.readFile @ABSPATH@;
bad4 = builtins.readFile ./evil-link;
bad5 = builtins.readFile "${./evil-link}";
bad6 = builtins.readFile ./less-evil-link;
bad7 = builtins.readFile "${./less-evil-link}";
bad8 = import ./evil-default-nix;
bad9 = import ./nested-bad.nix;
good1 = builtins.readFile "${inputs.self.outPath}/good.nix";
good2 = builtins.readFile ./good.nix;
good3 = builtins.readFile "${./good.nix}";
good4 = toString (import ./good.nix);
good5 = toString (import ./nested-good.nix);
};
}
""").replace('@ABSPATH@', str(evilpath.absolute())))
for idx in range(1, 10):
cmd = nix.nix(['eval', f'.#bad{idx}'], flake=True)
cmd.cwd = flake_dir
res = cmd.run().expect(1)
print(res.stderr_plain)
assert ERROR_RE.search(res.stderr_plain)
for idx in range(1, 6):
cmd = nix.nix(['eval', f'.#good{idx}'], flake=True)
cmd.cwd = flake_dir
res = cmd.run().expect(0)
assert res.stdout_plain == '"1"'
+9 -5
View File
@@ -3,7 +3,7 @@ import json
import subprocess
from typing import Any
from pathlib import Path
from functools import partial, partialmethod
from functools import partialmethod
from functional2.testlib.terminal_code_eater import eat_terminal_codes
import dataclasses
@@ -198,17 +198,21 @@ class Nix:
settings.store = str(store_path)
return settings
def nix_cmd(self, argv: list[str], allow_builds: bool = False):
def nix_cmd(self, argv: list[str], flake: bool = False):
"""
Constructs a NixCommand with the appropriate settings.
"""
settings = self.settings()
if flake:
settings.feature('nix-command', 'flakes')
return NixCommand(argv=argv,
cwd=self.test_root,
env=self.make_env(),
settings=self.settings())
settings=settings)
def nix(self, cmd: list[str], nix_exe: str = 'nix') -> NixCommand:
return self.nix_cmd([nix_exe, *cmd])
def nix(self, cmd: list[str], nix_exe: str = 'nix', flake: bool = False) -> NixCommand:
return self.nix_cmd([nix_exe, *cmd], flake=flake)
nix_build = partialmethod(nix, nix_exe='nix-build')
nix_shell = partialmethod(nix, nix_exe='nix-shell')
+4 -4
View File
@@ -34,12 +34,12 @@ TEST(Arguments, lookupFileArg) {
auto state = std::make_shared<Evaluator>(searchPath, store, store);
SourcePath const foundUnitData = lookupFileArg(*state, "<example>");
EXPECT_EQ(foundUnitData.path, canonDataPath);
EXPECT_EQ(foundUnitData.canonical(), canonDataPath);
// lookupFileArg should not resolve <search paths> if anything else is before or after it.
SourcePath const yepEvenSpaces = lookupFileArg(*state, " <example>");
EXPECT_EQ(yepEvenSpaces.path, CanonPath::fromCwd(" <example>"));
EXPECT_EQ(lookupFileArg(*state, "<example>/nixos").path, CanonPath::fromCwd("<example>/nixos"));
EXPECT_EQ(yepEvenSpaces.canonical(), CanonPath::fromCwd(" <example>"));
EXPECT_EQ(lookupFileArg(*state, "<example>/nixos").canonical(), CanonPath::fromCwd("<example>/nixos"));
try {
lookupFileArg(*state, INVALID_CHANNEL);
@@ -49,7 +49,7 @@ TEST(Arguments, lookupFileArg) {
}
SourcePath const normalFile = lookupFileArg(*state, unitDataPath);
EXPECT_EQ(normalFile.path, canonDataPath);
EXPECT_EQ(normalFile.canonical(), canonDataPath);
}
}