Allow special characters in flake paths

Support using nix flakes in paths with spaces or abitrary unicode characters.
This introduces the convention that the path part of the URL should be
percent-encoded when dealing with `path:` urls and not when using
filepaths (following the convention of firefox).

Co-authored-by: Rendal <rasmus@rend.al>
This commit is contained in:
Théophane Hufschmitt 2023-05-31 10:36:43 +02:00
parent d8cebae939
commit 50e61f579c
3 changed files with 57 additions and 39 deletions

View file

@ -77,14 +77,6 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
{ {
using namespace fetchers; 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( static std::regex flakeRegex(
"((" + flakeIdRegexS + ")(?:/(?:" + refAndOrRevRegex + "))?)" "((" + flakeIdRegexS + ")(?:/(?:" + refAndOrRevRegex + "))?)"
+ "(?:#(" + queryRegex + "))?", + "(?:#(" + queryRegex + "))?",
@ -92,26 +84,23 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
std::smatch match; std::smatch match;
/* Check if 'url' is a flake ID. This is an abbreviated syntax for auto parsePathFlakeRef = [&]() {
'flake:<flake-id>?ref=<ref>&rev=<rev>'. */ std::string path = url;
std::string fragment = "";
if (std::regex_match(url, match, flakeRegex)) { std::map<std::string, std::string> query = {};
auto parsedURL = ParsedURL{ auto pathEnd = url.find_first_of("#?");
.url = url, auto fragmentStart = pathEnd;
.base = "flake:" + match.str(1), if (pathEnd != std::string::npos && url[pathEnd] == '?')
.scheme = "flake", fragmentStart = url.find("#");
.authority = "", if (pathEnd != std::string::npos) {
.path = match[1], path = url.substr(0, pathEnd);
}; }
if (fragmentStart != std::string::npos) {
return std::make_pair( fragment = percentDecode(url.substr(fragmentStart+1));
FlakeRef(Input::fromURL(parsedURL, isFlake), ""), }
percentDecode(match.str(6))); if (fragmentStart != std::string::npos && pathEnd != std::string::npos) {
} query = decodeQuery(url.substr(pathEnd+1, fragmentStart));
}
else if (std::regex_match(url, match, pathUrlRegex)) {
std::string path = match[1];
std::string fragment = percentDecode(match.str(3));
if (baseDir) { if (baseDir) {
/* Check if 'url' is a path (either absolute or relative /* Check if 'url' is a path (either absolute or relative
@ -163,7 +152,7 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
.scheme = "git+file", .scheme = "git+file",
.authority = "", .authority = "",
.path = flakeRoot, .path = flakeRoot,
.query = decodeQuery(match[2]), .query = query,
}; };
if (subdir != "") { if (subdir != "") {
@ -188,7 +177,6 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
} else { } else {
if (!hasPrefix(path, "/")) if (!hasPrefix(path, "/"))
throw BadURL("flake reference '%s' is not an absolute path", url); throw BadURL("flake reference '%s' is not an absolute path", url);
auto query = decodeQuery(match[2]);
path = canonPath(path + "/" + getOr(query, "dir", "")); path = canonPath(path + "/" + getOr(query, "dir", ""));
} }
@ -197,19 +185,40 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
attrs.insert_or_assign("path", path); attrs.insert_or_assign("path", path);
return std::make_pair(FlakeRef(Input::fromAttrs(std::move(attrs)), ""), fragment); return std::make_pair(FlakeRef(Input::fromAttrs(std::move(attrs)), ""), fragment);
};
/* Check if 'url' is a flake ID. This is an abbreviated syntax for
'flake:<flake-id>?ref=<ref>&rev=<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), ""),
percentDecode(match.str(6)));
} }
else { else {
auto parsedURL = parseURL(url); try {
std::string fragment; auto parsedURL = parseURL(url);
std::swap(fragment, parsedURL.fragment); std::string fragment;
std::swap(fragment, parsedURL.fragment);
auto input = Input::fromURL(parsedURL, isFlake); auto input = Input::fromURL(parsedURL, isFlake);
input.parent = baseDir; input.parent = baseDir;
return std::make_pair( return std::make_pair(
FlakeRef(std::move(input), getOr(parsedURL.query, "dir", "")), FlakeRef(std::move(input), getOr(parsedURL.query, "dir", "")),
fragment); fragment);
} catch (BadURL &) {
return parsePathFlakeRef();
}
} }
} }

View file

@ -335,4 +335,13 @@ namespace nix {
ASSERT_EQ(d, s); ASSERT_EQ(d, s);
} }
TEST(percentEncode, yen) {
// https://en.wikipedia.org/wiki/Percent-encoding#Character_data
std::string s = reinterpret_cast<const char*>(u8"");
std::string e = "%E5%86%86";
ASSERT_EQ(percentEncode(s), e);
ASSERT_EQ(percentDecode(e), s);
}
} }

View file

@ -103,7 +103,7 @@ std::string percentEncode(std::string_view s, std::string_view keep)
|| keep.find(c) != std::string::npos) || keep.find(c) != std::string::npos)
res += c; res += c;
else else
res += fmt("%%%02X", (unsigned int) c); res += fmt("%%%02X", c & 0xFF);
return res; return res;
} }