diff --git a/lix/legacy/nix-build.cc b/lix/legacy/nix-build.cc index 0642a995b..5423cf5b9 100644 --- a/lix/legacy/nix-build.cc +++ b/lix/legacy/nix-build.cc @@ -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 + )) + )); } } diff --git a/lix/legacy/nix-env.cc b/lix/legacy/nix-env.cc index 3862288f4..1a98bedaa 100644 --- a/lix/legacy/nix-env.cc +++ b/lix/legacy/nix-env.cc @@ -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 diff --git a/lix/legacy/nix-instantiate.cc b/lix/legacy/nix-instantiate.cc index 201de62e4..13a05c845 100644 --- a/lix/legacy/nix-instantiate.cc +++ b/lix/legacy/nix-instantiate.cc @@ -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); } diff --git a/lix/libcmd/editor-for.cc b/lix/libcmd/editor-for.cc index fc0f13b45..68ba5954a 100644 --- a/lix/libcmd/editor-for.cc +++ b/lix/libcmd/editor-for.cc @@ -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(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; } diff --git a/lix/libcmd/installable-value.cc b/lix/libcmd/installable-value.cc index b16e6589a..42d87936b 100644 --- a/lix/libcmd/installable-value.cc +++ b/lix/libcmd/installable-value.cc @@ -47,7 +47,7 @@ std::optional 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), diff --git a/lix/libcmd/installables.cc b/lix/libcmd/installables.cc index 9970d4319..1bf630883 100644 --- a/lix/libcmd/installables.cc +++ b/lix/libcmd/installables.cc @@ -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; diff --git a/lix/libcmd/repl.cc b/lix/libcmd/repl.cc index d1534a3d8..86feec53f 100644 --- a/lix/libcmd/repl.cc +++ b/lix/libcmd/repl.cc @@ -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(&pos.origin)) + if (auto path = std::get_if(&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(&pos.origin)) { + if (auto path = std::get_if(&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)); diff --git a/lix/libexpr/attr-path.cc b/lix/libexpr/attr-path.cc index 926e7c9fe..0aa62d355 100644 --- a/lix/libexpr/attr-path.cc +++ b/lix/libexpr/attr-path.cc @@ -178,7 +178,7 @@ std::pair 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); diff --git a/lix/libexpr/eval-cache.cc b/lix/libexpr/eval-cache.cc index 443177afd..2ca311dbf 100644 --- a/lix/libexpr/eval-cache.cc +++ b/lix/libexpr/eval-cache.cc @@ -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}; diff --git a/lix/libexpr/eval.cc b/lix/libexpr/eval.cc index 6f52566a3..01bb14634 100644 --- a/lix/libexpr/eval.cc +++ b/lix/libexpr/eval.cc @@ -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 componentsBacking; + std::vector 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(&origin)) { + if (auto path = std::get_if(&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("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(&pos.origin)) + if (auto path = std::get_if(&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(&pos.origin)) + if (auto path = std::get_if(&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) +Expr & Evaluator::parseExprFromFile(const CheckedSourcePath & path, std::shared_ptr & staticEnv) { auto buffer = path.readFile(); return *parse(buffer.data(), buffer.size(), Pos::Origin(path), path.parent(), staticEnv); diff --git a/lix/libexpr/eval.hh b/lix/libexpr/eval.hh index b391f68f8..0e37d310d 100644 --- a/lix/libexpr/eval.hh +++ b/lix/libexpr/eval.hh @@ -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 children; + + bool allowAllChildren = false; + }; + /** * The allowed filesystem paths in restricted or pure evaluation * mode. */ - std::optional allowedPaths; + std::optional 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 resolvedPaths; + std::unordered_map 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); + Expr & parseExprFromFile(const CheckedSourcePath & path); + Expr & parseExprFromFile(const CheckedSourcePath & path, std::shared_ptr & 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__/"}; diff --git a/lix/libexpr/flake/flake.cc b/lix/libexpr/flake/flake.cc index 8ccdf7373..33e2951bb 100644 --- a/lix/libexpr/flake/flake.cc +++ b/lix/libexpr/flake/flake.cc @@ -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(&flakeExpr))) { diff --git a/lix/libexpr/parser/parser-impl1.inc.cc b/lix/libexpr/parser/parser-impl1.inc.cc index efea9619d..7f8a9bb88 100644 --- a/lix/libexpr/parser/parser-impl1.inc.cc +++ b/lix/libexpr/parser/parser-impl1.inc.cc @@ -605,7 +605,7 @@ template<> struct BuildAST : BuildAST struct BuildAST { 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 += "/"; diff --git a/lix/libexpr/primops.cc b/lix/libexpr/primops.cc index 328ce250b..cfbf86ced 100644 --- a/lix/libexpr/primops.cc +++ b/lix/libexpr/primops.cc @@ -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 { @@ -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("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, can’t 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( "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); } diff --git a/lix/libexpr/value-to-json.cc b/lix/libexpr/value-to-json.cc index 4a2ad7062..0ba517604 100644 --- a/lix/libexpr/value-to-json.cc +++ b/lix/libexpr/value-to-json.cc @@ -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: diff --git a/lix/libexpr/value-to-xml.cc b/lix/libexpr/value-to-xml.cc index 17426b12e..e0b4357ba 100644 --- a/lix/libexpr/value-to-xml.cc +++ b/lix/libexpr/value-to-xml.cc @@ -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(&pos.origin)) - xmlAttrs["path"] = path->path.abs(); + if (auto path = std::get_if(&pos.origin)) + xmlAttrs["path"] = path->to_string(); xmlAttrs["line"] = fmt("%1%", pos.line); xmlAttrs["column"] = fmt("%1%", pos.column); } diff --git a/lix/libexpr/value.cc b/lix/libexpr/value.cc index ae62db3e8..5aea57096 100644 --- a/lix/libexpr/value.cc +++ b/lix/libexpr/value.cc @@ -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); } } diff --git a/lix/libexpr/value.hh b/lix/libexpr/value.hh index b69049130..5d3a25695 100644 --- a/lix/libexpr/value.hh +++ b/lix/libexpr/value.hh @@ -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) { } diff --git a/lix/libfetchers/fetch-to-store.cc b/lix/libfetchers/fetch-to-store.cc index 3fb53a437..a276a48fd 100644 --- a/lix/libfetchers/fetch-to-store.cc +++ b/lix/libfetchers/fetch-to-store.cc @@ -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); } diff --git a/lix/libfetchers/fetch-to-store.hh b/lix/libfetchers/fetch-to-store.hh index b0472dbaa..6d4d072aa 100644 --- a/lix/libfetchers/fetch-to-store.hh +++ b/lix/libfetchers/fetch-to-store.hh @@ -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, diff --git a/lix/libutil/canon-path.hh b/lix/libutil/canon-path.hh index eefe05ed5..aeedee341 100644 --- a/lix/libutil/canon-path.hh +++ b/lix/libutil/canon-path.hh @@ -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; } }; diff --git a/lix/libutil/position.cc b/lix/libutil/position.cc index cf2ff4b45..d98cd4e93 100644 --- a/lix/libutil/position.cc +++ b/lix/libutil/position.cc @@ -62,7 +62,7 @@ std::optional 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 { + [](const CheckedSourcePath & path) -> std::optional { try { return path.readFile(); } catch (Error &) { diff --git a/lix/libutil/position.hh b/lix/libutil/position.hh index 3f89a0f5a..98fd7f15b 100644 --- a/lix/libutil/position.hh +++ b/lix/libutil/position.hh @@ -42,7 +42,7 @@ struct Pos auto operator<=>(const Hidden &) const = default; }; - typedef std::variant