#include "flakeref.hh" #include "store-api.hh" #include "url.hh" #include "url-parts.hh" #include "fetchers.hh" #include "registry.hh" namespace nix { #if 0 // 'dir' path elements cannot start with a '.'. We also reject // potentially dangerous characters like ';'. const static std::string subDirElemRegex = "(?:[a-zA-Z0-9_-]+[a-zA-Z0-9._-]*)"; const static std::string subDirRegex = subDirElemRegex + "(?:/" + subDirElemRegex + ")*"; #endif std::string FlakeRef::to_string() const { std::map extraQuery; if (subdir != "") extraQuery.insert_or_assign("dir", subdir); return input.toURLString(extraQuery); } fetchers::Attrs FlakeRef::toAttrs() const { auto attrs = input.toAttrs(); if (subdir != "") attrs.emplace("dir", subdir); return attrs; } std::ostream & operator << (std::ostream & str, const FlakeRef & flakeRef) { str << flakeRef.to_string(); return str; } bool FlakeRef::operator ==(const FlakeRef & other) const { return input == other.input && subdir == other.subdir; } FlakeRef FlakeRef::resolve(ref store) const { auto [input2, extraAttrs] = lookupInRegistries(store, input); return FlakeRef(std::move(input2), fetchers::maybeGetStrAttr(extraAttrs, "dir").value_or(subdir)); } FlakeRef parseFlakeRef( const std::string & url, const std::optional & baseDir, bool allowMissing, bool isFlake) { auto [flakeRef, fragment] = parseFlakeRefWithFragment(url, baseDir, allowMissing, isFlake); if (fragment != "") throw Error("unexpected fragment '%s' in flake reference '%s'", fragment, url); return flakeRef; } std::optional maybeParseFlakeRef( const std::string & url, const std::optional & baseDir) { try { return parseFlakeRef(url, baseDir); } catch (Error &) { return {}; } } std::pair parseFlakeRefWithFragment( const std::string & url, const std::optional & baseDir, bool allowMissing, bool isFlake) { using namespace fetchers; static std::string fnRegex = "[0-9a-zA-Z-._~!$&'\"()*+,;=]+"; static std::regex pathUrlRegex( "(/?" + fnRegex + "(?:/" + fnRegex + ")*/?)" + "(?:\\?(" + queryRegex + "))?" + "(?:#(" + queryRegex + "))?", std::regex::ECMAScript); static std::regex flakeRegex( "((" + flakeIdRegexS + ")(?:/(?:" + refAndOrRevRegex + "))?)" + "(?:#(" + queryRegex + "))?", std::regex::ECMAScript); std::smatch match; /* Check if 'url' is a flake ID. This is an abbreviated syntax for 'flake:?ref=&rev='. */ if (std::regex_match(url, match, flakeRegex)) { auto parsedURL = ParsedURL{ .url = url, .base = "flake:" + match.str(1), .scheme = "flake", .authority = "", .path = match[1], }; return std::make_pair( FlakeRef(Input::fromURL(parsedURL, isFlake), ""), percentDecode(match.str(6))); } else if (std::regex_match(url, match, pathUrlRegex)) { std::string path = match[1]; std::string fragment = percentDecode(match.str(3)); if (baseDir) { /* Check if 'url' is a path (either absolute or relative to 'baseDir'). If so, search upward to the root of the repo (i.e. the directory containing .git). */ path = absPath(path, baseDir); if (isFlake) { if (!allowMissing && !pathExists(path + "/flake.nix")){ notice("path '%s' does not contain a 'flake.nix', searching up",path); // Save device to detect filesystem boundary dev_t device = lstat(path).st_dev; bool found = false; while (path != "/") { if (pathExists(path + "/flake.nix")) { found = true; break; } else if (pathExists(path + "/.git")) throw Error("path '%s' is not part of a flake (neither it nor its parent directories contain a 'flake.nix' file)", path); else { if (lstat(path).st_dev != device) throw Error("unable to find a flake before encountering filesystem boundary at '%s'", path); } path = dirOf(path); } if (!found) throw BadURL("could not find a flake.nix file"); } if (!S_ISDIR(lstat(path).st_mode)) throw BadURL("path '%s' is not a flake (because it's not a directory)", path); if (!allowMissing && !pathExists(path + "/flake.nix")) throw BadURL("path '%s' is not a flake (because it doesn't contain a 'flake.nix' file)", path); auto flakeRoot = path; std::string subdir; while (flakeRoot != "/") { if (pathExists(flakeRoot + "/.git")) { auto base = std::string("git+file://") + flakeRoot; auto parsedURL = ParsedURL{ .url = base, // FIXME .base = base, .scheme = "git+file", .authority = "", .path = flakeRoot, .query = decodeQuery(match[2]), }; if (subdir != "") { if (parsedURL.query.count("dir")) throw Error("flake URL '%s' has an inconsistent 'dir' parameter", url); parsedURL.query.insert_or_assign("dir", subdir); } if (pathExists(flakeRoot + "/.git/shallow")) parsedURL.query.insert_or_assign("shallow", "1"); return std::make_pair( FlakeRef(Input::fromURL(parsedURL, isFlake), getOr(parsedURL.query, "dir", "")), fragment); } subdir = std::string(baseNameOf(flakeRoot)) + (subdir.empty() ? "" : "/" + subdir); flakeRoot = dirOf(flakeRoot); } } } else { if (!path.starts_with("/")) throw BadURL("flake reference '%s' is not an absolute path", url); auto query = decodeQuery(match[2]); path = canonPath(path + "/" + getOr(query, "dir", "")); } fetchers::Attrs attrs; attrs.insert_or_assign("type", "path"); attrs.insert_or_assign("path", path); return std::make_pair(FlakeRef(Input::fromAttrs(std::move(attrs)), ""), fragment); } else { auto parsedURL = parseURL(url); std::string fragment; std::swap(fragment, parsedURL.fragment); // This has a special meaning for flakes and must not be passed to libfetchers. // Of course this means that libfetchers cannot have fetchers // expecting an argument `dir` 🫠 ParsedURL urlForFetchers(parsedURL); urlForFetchers.query.erase("dir"); auto input = Input::fromURL(urlForFetchers, isFlake); input.parent = baseDir; return std::make_pair( FlakeRef(std::move(input), getOr(parsedURL.query, "dir", "")), fragment); } } std::optional> maybeParseFlakeRefWithFragment( const std::string & url, const std::optional & baseDir) { try { return parseFlakeRefWithFragment(url, baseDir); } catch (Error & e) { return {}; } } FlakeRef FlakeRef::fromAttrs(const fetchers::Attrs & attrs) { auto attrs2(attrs); attrs2.erase("dir"); return FlakeRef( fetchers::Input::fromAttrs(std::move(attrs2)), fetchers::maybeGetStrAttr(attrs, "dir").value_or("")); } std::pair FlakeRef::fetchTree(ref store) const { auto [tree, lockedInput] = input.fetch(store); return {std::move(tree), FlakeRef(std::move(lockedInput), subdir)}; } std::tuple parseFlakeRefWithFragmentAndExtendedOutputsSpec( const std::string & url, const std::optional & baseDir, bool allowMissing, bool isFlake) { auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse(url); auto [flakeRef, fragment] = parseFlakeRefWithFragment(std::string { prefix }, baseDir, allowMissing, isFlake); return {std::move(flakeRef), fragment, std::move(extendedOutputsSpec)}; } }