Skip to content

Commit 2658832

Browse files
committed
devpkg: better handling of escapes in flakeref paths
Split flakeref paths before unescaping to properly handle encoded slashes (%2F). Use `URL.String()` to ensure that unescaped characters get re-escaped for `FlakeRef.URL`.
1 parent deea2f7 commit 2658832

File tree

2 files changed

+49
-30
lines changed

2 files changed

+49
-30
lines changed

internal/devpkg/flakeref.go

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,10 @@ func parseFlakeURLRef(ref string) (parsed FlakeRef, fragment string, err error)
107107
// [flake:]<flake-id>(/<rev-or-ref>(/rev)?)?
108108

109109
parsed.Type = "indirect"
110-
111-
// "indirect" is parsed as a path, "flake:indirect" is parsed as
112-
// opaque because it has a scheme.
113-
path := refURL.Path
114-
if path == "" {
115-
path, err = url.PathUnescape(refURL.Opaque)
116-
if err != nil {
117-
path = refURL.Opaque
118-
}
110+
split, err := splitPathOrOpaque(refURL)
111+
if err != nil {
112+
return FlakeRef{}, "", redact.Errorf("parse flake reference URL path: %v", err)
119113
}
120-
split := strings.SplitN(path, "/", 3)
121114
parsed.ID = split[0]
122115
if len(split) > 1 {
123116
if isGitHash(split[1]) {
@@ -136,7 +129,7 @@ func parseFlakeURLRef(ref string) (parsed FlakeRef, fragment string, err error)
136129
if refURL.Path == "" {
137130
parsed.Path, err = url.PathUnescape(refURL.Opaque)
138131
if err != nil {
139-
parsed.Path = refURL.Opaque
132+
return FlakeRef{}, "", err
140133
}
141134
} else {
142135
parsed.Path = refURL.Path
@@ -147,16 +140,20 @@ func parseFlakeURLRef(ref string) (parsed FlakeRef, fragment string, err error)
147140
} else {
148141
parsed.Type = "file"
149142
}
150-
parsed.URL = ref
151143
parsed.Dir = refURL.Query().Get("dir")
144+
parsed.URL = refURL.String()
152145
case "tarball+http", "tarball+https", "tarball+file":
153146
parsed.Type = "tarball"
154147
parsed.Dir = refURL.Query().Get("dir")
155-
parsed.URL = ref[8:] // remove tarball+
148+
149+
refURL.Scheme = refURL.Scheme[8:] // remove tarball+
150+
parsed.URL = refURL.String()
156151
case "file+http", "file+https", "file+file":
157152
parsed.Type = "file"
158153
parsed.Dir = refURL.Query().Get("dir")
159-
parsed.URL = ref[5:] // remove file+
154+
155+
refURL.Scheme = refURL.Scheme[5:] // remove file+
156+
parsed.URL = refURL.String()
160157
case "git", "git+http", "git+https", "git+ssh", "git+git", "git+file":
161158
parsed.Type = "git"
162159
q := refURL.Query()
@@ -185,17 +182,10 @@ func parseGitHubFlakeRef(refURL *url.URL, parsed *FlakeRef) error {
185182
// github:<owner>/<repo>(/<rev-or-ref>)?(\?<params>)?
186183

187184
parsed.Type = "github"
188-
path := refURL.Path
189-
if path == "" {
190-
var err error
191-
path, err = url.PathUnescape(refURL.Opaque)
192-
if err != nil {
193-
path = refURL.Opaque
194-
}
185+
split, err := splitPathOrOpaque(refURL)
186+
if err != nil {
187+
return err
195188
}
196-
path = strings.TrimPrefix(path, "/")
197-
198-
split := strings.SplitN(path, "/", 3)
199189
parsed.Owner = split[0]
200190
parsed.Repo = split[1]
201191
if len(split) > 2 {
@@ -359,6 +349,37 @@ func isArchive(path string) bool {
359349
strings.HasSuffix(path, ".zip")
360350
}
361351

352+
// splitPathOrOpaque splits a URL path by '/'. If the path is empty, it splits
353+
// the opaque instead. Splitting happens before unescaping the path or opaque,
354+
// ensuring that path elements with an encoded '/' (%2F) are not split.
355+
// For example, "/dir/file%2Fname" becomes the elements "dir" and "file/name".
356+
func splitPathOrOpaque(u *url.URL) ([]string, error) {
357+
upath := u.EscapedPath()
358+
if upath == "" {
359+
upath = u.Opaque
360+
}
361+
upath = strings.TrimSpace(upath)
362+
if upath == "" {
363+
return nil, nil
364+
}
365+
366+
// We don't want an empty element if the path is rooted.
367+
if upath[0] == '/' {
368+
upath = upath[1:]
369+
}
370+
upath = path.Clean(upath)
371+
372+
var err error
373+
split := strings.Split(upath, "/")
374+
for i := range split {
375+
split[i], err = url.PathUnescape(split[i])
376+
if err != nil {
377+
return nil, err
378+
}
379+
}
380+
return split, nil
381+
}
382+
362383
// buildOpaquePath escapes and joins path elements for a URL flakeref. The
363384
// resulting path is cleaned according to url.JoinPath.
364385
func buildOpaquePath(elem ...string) string {

internal/devpkg/flakeref_test.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ func TestParseFlakeRef(t *testing.T) {
1717
// Not a path and not a valid URL.
1818
"://bad/url": {},
1919

20+
// Invalid escape.
21+
"path:./relative/my%flake": {},
22+
2023
// Path-like references start with a '.' or '/'.
2124
// This distinguishes them from indirect references
2225
// (./nixpkgs is a directory; nixpkgs is an indirect).
@@ -60,7 +63,7 @@ func TestParseFlakeRef(t *testing.T) {
6063
// Indirect references.
6164
"flake:indirect": {Type: "indirect", ID: "indirect"},
6265
"flake:indirect/ref": {Type: "indirect", ID: "indirect", Ref: "ref"},
63-
"flake:indirect/my%20ref": {Type: "indirect", ID: "indirect", Ref: "my ref"},
66+
"flake:indirect/my%2Fref": {Type: "indirect", ID: "indirect", Ref: "my/ref"},
6467
"flake:indirect/5233fd2ba76a3accb5aaa999c00509a11fd0793c": {Type: "indirect", ID: "indirect", Rev: "5233fd2ba76a3accb5aaa999c00509a11fd0793c"},
6568
"flake:indirect/ref/5233fd2ba76a3accb5aaa999c00509a11fd0793c": {Type: "indirect", ID: "indirect", Ref: "ref", Rev: "5233fd2ba76a3accb5aaa999c00509a11fd0793c"},
6669

@@ -139,11 +142,6 @@ func TestParseFlakeRef(t *testing.T) {
139142
"http://example.com/flake": {Type: "file", URL: "http://example.com/flake"},
140143
"http://example.com/flake.git": {Type: "file", URL: "http://example.com/flake.git"},
141144
"http://example.com/flake?dir=subdir": {Type: "file", URL: "http://example.com/flake?dir=subdir", Dir: "subdir"},
142-
143-
// Interpret opaques with invalid escapes literally.
144-
"path:./relative/my%flake": {Type: "path", Path: "./relative/my%flake"},
145-
"flake:indirect/my%ref": {Type: "indirect", ID: "indirect", Ref: "my%ref"},
146-
"github:NixOS/nix/my%ref": {Type: "github", Owner: "NixOS", Repo: "nix", Ref: "my%ref"},
147145
}
148146

149147
for ref, want := range cases {

0 commit comments

Comments
 (0)