Skip to content

Commit b05fc25

Browse files
committed
truly...no idea why i went down that rabbit hole in the first place
1 parent 2994677 commit b05fc25

File tree

2 files changed

+302
-81
lines changed

2 files changed

+302
-81
lines changed

nix/flake/flakeref.go

Lines changed: 86 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package flake
33

44
import (
55
"cmp"
6-
"fmt"
76
"net/url"
87
"path"
98
"slices"
@@ -88,6 +87,47 @@ type Ref struct {
8887
Port int32 `json:port,omitempty`
8988
}
9089

90+
// ParseRef parses a raw flake reference. Nix supports a variety of flake ref
91+
// formats, and isn't entirely consistent about how it parses them. ParseRef
92+
// attempts to mimic how Nix parses flake refs on the command line. The raw ref
93+
// can be one of the following:
94+
//
95+
// - Indirect reference such as "nixpkgs" or "nixpkgs/unstable".
96+
// - Path-like reference such as "./flake" or "/path/to/flake". They must
97+
// start with a '.' or '/' and not contain a '#' or '?'.
98+
// - URL-like reference which must be a valid URL with any special characters
99+
// encoded. The scheme can be any valid flake ref type except for mercurial,
100+
// gitlab, and sourcehut.
101+
//
102+
// ParseRef does not guarantee that a parsed flake ref is valid or that an
103+
// error indicates an invalid flake ref. Use the "nix flake metadata" command or
104+
// the builtins.parseFlakeRef Nix function to validate a flake ref.
105+
func ParseRef(ref string) (Ref, error) {
106+
if ref == "" {
107+
return Ref{}, redact.Errorf("empty flake reference")
108+
}
109+
110+
// Handle path-style references first.
111+
parsed := Ref{}
112+
if ref[0] == '.' || ref[0] == '/' {
113+
if strings.ContainsAny(ref, "?#") {
114+
// The Nix CLI does seem to allow paths with a '?'
115+
// (contrary to the manual) but ignores everything that
116+
// comes after it. This is a bit surprising, so we just
117+
// don't allow it at all.
118+
return Ref{}, redact.Errorf("path-style flake reference %q contains a '?' or '#'", ref)
119+
}
120+
parsed.Type = TypePath
121+
parsed.Path = ref
122+
return parsed, nil
123+
}
124+
parsed, fragment, err := parseURLRef(ref)
125+
if fragment != "" {
126+
return Ref{}, redact.Errorf("flake reference %q contains a URL fragment", ref)
127+
}
128+
return parsed, err
129+
}
130+
91131
func parseURLRef(ref string) (parsed Ref, fragment string, err error) {
92132
// A good way to test how Nix parses a flake reference is to run:
93133
//
@@ -196,6 +236,7 @@ func parseURLRef(ref string) (parsed Ref, fragment string, err error) {
196236
refURL.Scheme = refURL.Scheme[5:] // remove file+
197237
parsed.URL = refURL.String()
198238
case "git", "git+http", "git+https", "git+ssh", "git+git", "git+file":
239+
parsed.Type = TypeGit
199240
query := refURL.Query()
200241
parsed.Dir = query.Get("dir")
201242
parsed.Ref = query.Get("ref")
@@ -206,49 +247,25 @@ func parseURLRef(ref string) (parsed Ref, fragment string, err error) {
206247
query.Del("ref")
207248
query.Del("rev")
208249
refURL.RawQuery = query.Encode()
209-
210250
if len(refURL.Scheme) > 3 {
211251
refURL.Scheme = refURL.Scheme[4:] // remove git+
212252
}
213-
214-
if strings.HasPrefix(refURL.Scheme, TypeSSH) {
215-
parsed.Type = TypeSSH
216-
} else if strings.HasPrefix(refURL.Scheme, TypeFile) {
217-
parsed.Type = TypeFile
218-
} else {
219-
parsed.Type = TypeGit
220-
}
221-
222253
parsed.URL = refURL.String()
223-
224-
if err := parseGitRef(refURL, &parsed); err != nil {
225-
return Ref{}, "", err
226-
}
227-
case "bitbucket":
228-
parsed.Type = TypeBitBucket
229-
if err := parseGitRef(refURL, &parsed); err != nil {
230-
return Ref{}, "", err
231-
}
232-
case "gitlab":
233-
parsed.Type = TypeGitLab
234-
if err := parseGitRef(refURL, &parsed); err != nil {
235-
return Ref{}, "", err
236-
}
237254
case "github":
238-
parsed.Type = TypeGitHub
239-
if err := parseGitRef(refURL, &parsed); err != nil {
255+
if err := parseGitHubRef(refURL, &parsed); err != nil {
240256
return Ref{}, "", err
241257
}
242258
default:
243259
return Ref{}, "", redact.Errorf("unsupported flake reference URL scheme: %s", redact.Safe(refURL.Scheme))
244260
}
245-
246261
return parsed, fragment, nil
247262
}
248263

249-
func parseGitRef(refURL *url.URL, parsed *Ref) error {
264+
func parseGitHubRef(refURL *url.URL, parsed *Ref) error {
250265
// github:<owner>/<repo>(/<rev-or-ref>)?(\?<params>)?
251266

267+
parsed.Type = TypeGitHub
268+
252269
// Only split up to 3 times (owner, repo, ref/rev) so that we handle
253270
// refs that have slashes in them. For example,
254271
// "github:jetify-com/devbox/gcurtis/flakeref" parses as "gcurtis/flakeref".
@@ -268,7 +285,6 @@ func parseGitRef(refURL *url.URL, parsed *Ref) error {
268285

269286
parsed.Host = refURL.Query().Get("host")
270287
parsed.Dir = refURL.Query().Get("dir")
271-
272288
if qRef := refURL.Query().Get("ref"); qRef != "" {
273289
if parsed.Rev != "" {
274290
return redact.Errorf("github flake reference has a ref and a rev")
@@ -342,60 +358,60 @@ func (r Ref) Locked() bool {
342358
// string.
343359
func (r Ref) String() string {
344360
switch r.Type {
345-
346361
case TypeFile:
347362
if r.URL == "" {
348363
return ""
349364
}
350-
return "file+" + r.URL
351-
case TypeSSH:
352-
base := fmt.Sprintf("git+ssh://git@%s", r.Host)
353-
if r.Port > 0 {
354-
base = fmt.Sprintf("%s:%d", base, r.Port)
355-
}
356365

357-
queryParams := url.Values{}
358-
359-
if r.Rev != "" {
360-
queryParams.Add("rev", r.Rev)
366+
url, err := url.Parse("file+" + r.URL)
367+
if err != nil {
368+
// This should be rare and only happen if the caller
369+
// messed with the parsed URL.
370+
return ""
361371
}
362-
if r.Ref != "" {
363-
queryParams.Add("ref", r.Ref)
372+
url.RawQuery = appendQueryString(url.Query(),
373+
"lastModified", itoaOmitZero(r.LastModified),
374+
"narHash", r.NARHash,
375+
)
376+
return url.String()
377+
case TypeGit:
378+
if r.URL == "" {
379+
return ""
364380
}
365-
366-
if r.Dir != "" {
367-
queryParams.Add("dir", r.Dir)
381+
if !strings.HasPrefix(r.URL, "git") {
382+
r.URL = "git+" + r.URL
368383
}
369384

370-
return fmt.Sprintf("%s/%s/%s?%s", base, r.Owner, r.Repo, queryParams.Encode())
371-
372-
case TypeGitLab, TypeBitBucket, TypeGitHub:
373-
if r.Owner == "" || r.Repo == "" {
374-
return ""
385+
// Nix removes "ref" and "rev" from the query string
386+
// (but not other parameters) after parsing. If they're empty,
387+
// we can skip parsing the URL. Otherwise, we need to add them
388+
// back.
389+
if r.Ref == "" && r.Rev == "" {
390+
return r.URL
375391
}
376-
377-
scheme := "github" // using as default
378-
if r.Type == TypeGitLab {
379-
scheme = "gitlab"
392+
url, err := url.Parse(r.URL)
393+
if err != nil {
394+
// This should be rare and only happen if the caller
395+
// messed with the parsed URL.
396+
return ""
380397
}
381-
if r.Type == TypeBitBucket {
382-
scheme = "bitbucket"
398+
url.RawQuery = appendQueryString(url.Query(), "ref", r.Ref, "rev", r.Rev, "dir", r.Dir)
399+
return url.String()
400+
case TypeGitHub:
401+
if r.Owner == "" || r.Repo == "" {
402+
return ""
383403
}
384-
385404
url := &url.URL{
386-
Scheme: scheme,
387-
Opaque: buildEscapedPath(r.Owner, r.Repo, r.Rev, r.Ref),
388-
//RawQuery: buildQueryString("host", r.Host, "dir", r.Dir),
405+
Scheme: "github",
406+
Opaque: buildEscapedPath(r.Owner, r.Repo, cmp.Or(r.Rev, r.Ref)),
389407
RawQuery: appendQueryString(nil,
390408
"host", r.Host,
391409
"dir", r.Dir,
392410
"lastModified", itoaOmitZero(r.LastModified),
393411
"narHash", r.NARHash,
394412
),
395413
}
396-
397414
return url.String()
398-
399415
case TypeIndirect:
400416
if r.ID == "" {
401417
return ""
@@ -660,7 +676,13 @@ func ParseInstallable(raw string) (Installable, error) {
660676

661677
// Interpret installables with path-style flake refs as URLs to extract
662678
// the attribute path (fragment). This means that path-style flake refs
663-
// cannot point to files with a '#' or '?' in their name, since those
679+
//
680+
//
681+
//
682+
//
683+
//
684+
//
685+
//// cannot point to files with a '#' or '?' in their name, since those
664686
// would be parsed as the URL fragment or query string. This mimic's
665687
// Nix's CLI behavior.
666688
if raw[0] == '.' || raw[0] == '/' {

0 commit comments

Comments
 (0)