From ff89ae2b80b5acc3707fb53714844a197a141572 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Fri, 20 Dec 2024 11:45:05 -0500 Subject: [PATCH 1/2] flake: add narHash and lastModified attributes Add support for the `narHash` and `lastModified` attributes of flakerefs. The new `Ref.Locked` method uses these attributes to report whether a flake is locked. --- nix/flake/flakeref.go | 167 ++++++++++++++++++++++++++++++++----- nix/flake/flakeref_test.go | 24 +++--- 2 files changed, 161 insertions(+), 30 deletions(-) diff --git a/nix/flake/flakeref.go b/nix/flake/flakeref.go index 96067de6293..047f9f6a92b 100644 --- a/nix/flake/flakeref.go +++ b/nix/flake/flakeref.go @@ -2,9 +2,11 @@ package flake import ( + "maps" "net/url" "path" "slices" + "strconv" "strings" "go.jetpack.io/devbox/internal/redact" @@ -67,6 +69,14 @@ type Ref struct { // or "git". Note that the URL is not the same as the raw unparsed // flake ref. URL string `json:"url,omitempty"` + + // NARHash is the SRI hash of the flake's source. Specify a NAR hash to + // lock flakes that don't otherwise have a revision (such as "path" or + // "tarball" flakes). + NARHash string `json:"narHash,omitempty"` + + // LastModified is the last modification time of the flake. + LastModified int64 `json:"lastModified,omitempty"` } // ParseRef parses a raw flake reference. Nix supports a variety of flake ref @@ -156,24 +166,64 @@ func parseURLRef(ref string) (parsed Ref, fragment string, err error) { } else { parsed.Path = refURL.Path } + + parsed.NARHash = refURL.Query().Get("narHash") + parsed.LastModified, err = atoiOmitZero(refURL.Query().Get("lastModified")) + if err != nil { + return Ref{}, "", redact.Errorf("parse flake reference URL query parameter: lastModified=%s: %v", redact.Safe(parsed.LastModified), redact.Safe(err)) + } case "http", "https", "file": if isArchive(refURL.Path) { parsed.Type = TypeTarball } else { parsed.Type = TypeFile } - parsed.Dir = refURL.Query().Get("dir") + query := refURL.Query() + parsed.Dir = query.Get("dir") + parsed.NARHash = refURL.Query().Get("narHash") + parsed.LastModified, err = atoiOmitZero(refURL.Query().Get("lastModified")) + if err != nil { + return Ref{}, "", redact.Errorf("parse flake reference URL query parameter: lastModified=%s: %v", redact.Safe(parsed.LastModified), redact.Safe(err)) + } + + // lastModified and narHash get stripped from the query + // parameters, but dir stays. + query.Del("lastModified") + query.Del("narHash") + refURL.RawQuery = query.Encode() parsed.URL = refURL.String() case "tarball+http", "tarball+https", "tarball+file": parsed.Type = TypeTarball - parsed.Dir = refURL.Query().Get("dir") + query := refURL.Query() + parsed.Dir = query.Get("dir") + parsed.NARHash = refURL.Query().Get("narHash") + parsed.LastModified, err = atoiOmitZero(refURL.Query().Get("lastModified")) + if err != nil { + return Ref{}, "", redact.Errorf("parse flake reference URL query parameter: lastModified=%s: %v", redact.Safe(parsed.LastModified), redact.Safe(err)) + } + // lastModified and narHash get stripped from the query + // parameters, but dir stays. + query.Del("lastModified") + query.Del("narHash") + refURL.RawQuery = query.Encode() refURL.Scheme = refURL.Scheme[8:] // remove tarball+ parsed.URL = refURL.String() case "file+http", "file+https", "file+file": parsed.Type = TypeFile - parsed.Dir = refURL.Query().Get("dir") + query := refURL.Query() + parsed.Dir = query.Get("dir") + parsed.NARHash = refURL.Query().Get("narHash") + parsed.LastModified, err = atoiOmitZero(refURL.Query().Get("lastModified")) + if err != nil { + return Ref{}, "", redact.Errorf("parse flake reference URL query parameter: lastModified=%s: %v", redact.Safe(parsed.LastModified), redact.Safe(err)) + } + // lastModified and narHash get stripped from the query + // parameters, but dir stays. + query.Del("lastModified") + query.Del("narHash") + refURL.RawQuery = query.Encode() refURL.Scheme = refURL.Scheme[5:] // remove file+ parsed.URL = refURL.String() case "git", "git+http", "git+https", "git+ssh", "git+git", "git+file": @@ -245,9 +295,42 @@ func parseGitHubRef(refURL *url.URL, parsed *Ref) error { parsed.Rev = qRev } parsed.Dir = refURL.Query().Get("dir") + parsed.NARHash = refURL.Query().Get("narHash") return nil } +// Locked reports whether r is locked. Locked flake references always resolve to +// the same content. For some flake types, determining if a Ref is locked +// depends on the local Nix configuration. In these cases, Locked conservatively +// returns false. +func (r Ref) Locked() bool { + // Search for the implementations of InputScheme::isLocked in the nix + // source. + // + // https://github.com/search?q=repo%3ANixOS%2Fnix+language%3AC%2B%2B+symbol%3AisLocked&type=code + + switch r.Type { + case TypeFile, TypePath, TypeTarball: + return r.NARHash != "" + case TypeGit: + return r.Rev != "" + case TypeGitHub: + // We technically can't determine if a github flake is locked + // unless we know the trust-tarballs-from-git-forges Nix setting + // (which defaults to true), so we have to be conservative and + // check for rev and narHash. + // + // https://github.com/NixOS/nix/blob/3f3feae33e3381a2ea5928febe03329f0a578b20/src/libfetchers/github.cc#L304-L313 + return r.Rev != "" && r.NARHash != "" + case TypeIndirect: + // Never locked because they must be resolved against a flake + // registry. + return false + default: + return false + } +} + // String encodes the flake reference as a URL-like string. It normalizes the // result such that if two Ref values are equal, then their strings will also be // equal. @@ -262,7 +345,7 @@ func parseGitHubRef(refURL *url.URL, parsed *Ref) error { // put in the path. // - query parameters are sorted by key. // -// If f is missing a type or has any invalid fields, String returns an empty +// If r is missing a type or has any invalid fields, String returns an empty // string. func (r Ref) String() string { switch r.Type { @@ -270,7 +353,18 @@ func (r Ref) String() string { if r.URL == "" { return "" } - return "file+" + r.URL + + url, err := url.Parse("file+" + r.URL) + if err != nil { + // This should be rare and only happen if the caller + // messed with the parsed URL. + return "" + } + url.RawQuery = buildQueryString(url.Query(), + "lastModified", itoaOmitZero(r.LastModified), + "narHash", r.NARHash, + ) + return url.String() case TypeGit: if r.URL == "" { return "" @@ -292,16 +386,21 @@ func (r Ref) String() string { // messed with the parsed URL. return "" } - url.RawQuery = buildQueryString("ref", r.Ref, "rev", r.Rev, "dir", r.Dir) + url.RawQuery = buildQueryString(url.Query(), "ref", r.Ref, "rev", r.Rev, "dir", r.Dir) return url.String() case TypeGitHub: if r.Owner == "" || r.Repo == "" { return "" } url := &url.URL{ - Scheme: "github", - Opaque: buildEscapedPath(r.Owner, r.Repo, r.Rev, r.Ref), - RawQuery: buildQueryString("host", r.Host, "dir", r.Dir), + Scheme: "github", + Opaque: buildEscapedPath(r.Owner, r.Repo, r.Rev, r.Ref), + RawQuery: buildQueryString(nil, + "host", r.Host, + "dir", r.Dir, + "lastModified", itoaOmitZero(r.LastModified), + "narHash", r.NARHash, + ), } return url.String() case TypeIndirect: @@ -309,9 +408,13 @@ func (r Ref) String() string { return "" } url := &url.URL{ - Scheme: "flake", - Opaque: buildEscapedPath(r.ID, r.Ref, r.Rev), - RawQuery: buildQueryString("dir", r.Dir), + Scheme: "flake", + Opaque: buildEscapedPath(r.ID, r.Ref, r.Rev), + RawQuery: buildQueryString(nil, + "dir", r.Dir, + "lastModified", itoaOmitZero(r.LastModified), + "narHash", r.NARHash, + ), } return url.String() case TypePath: @@ -330,6 +433,11 @@ func (r Ref) String() string { } else if r.Path == "." { url.Opaque = "." } + + url.RawQuery = buildQueryString(nil, + "lastModified", itoaOmitZero(r.LastModified), + "narHash", r.NARHash, + ) return url.String() case TypeTarball: if r.URL == "" { @@ -338,9 +446,6 @@ func (r Ref) String() string { if !strings.HasPrefix(r.URL, "tarball") { r.URL = "tarball+" + r.URL } - if r.Dir == "" { - return r.URL - } url, err := url.Parse(r.URL) if err != nil { @@ -348,7 +453,11 @@ func (r Ref) String() string { // messed with the parsed URL. return "" } - url.RawQuery = buildQueryString("dir", r.Dir) + url.RawQuery = buildQueryString(url.Query(), + "dir", r.Dir, + "lastModified", itoaOmitZero(r.LastModified), + "narHash", r.NARHash, + ) return url.String() default: return "" @@ -425,19 +534,37 @@ func buildEscapedPath(elem ...string) string { // buildQueryString builds a URL query string from a list of key-value string // pairs, omitting any keys with empty values. -func buildQueryString(keyval ...string) string { +func buildQueryString(initial url.Values, keyval ...string) string { if len(keyval)%2 != 0 { panic("buildQueryString: odd number of key-value pairs") } - query := make(url.Values, len(keyval)/2) + q := make(url.Values, len(initial)+len(keyval)/2) + maps.Copy(q, initial) for i := 0; i < len(keyval); i += 2 { k, v := keyval[i], keyval[i+1] if v != "" { - query.Set(k, v) + q.Set(k, v) } } - return query.Encode() + return q.Encode() +} + +// itoaOmitZero returns an empty string if i == 0, otherwise it formats i as a +// string in base-10. +func itoaOmitZero(i int64) string { + if i == 0 { + return "" + } + return strconv.FormatInt(i, 10) +} + +// atoiOmitZero returns 0 if s == "", otherwised it parses s as a base-10 int64. +func atoiOmitZero(s string) (int64, error) { + if s == "" { + return 0, nil + } + return strconv.ParseInt(s, 10, 64) } // Special values for [Installable].Outputs. diff --git a/nix/flake/flakeref_test.go b/nix/flake/flakeref_test.go index e38572cabc4..364ec23ebca 100644 --- a/nix/flake/flakeref_test.go +++ b/nix/flake/flakeref_test.go @@ -34,6 +34,7 @@ func TestParseFlakeRef(t *testing.T) { "path:/": {Type: TypePath, Path: "/"}, "path:/flake": {Type: TypePath, Path: "/flake"}, "path:/absolute/flake": {Type: TypePath, Path: "/absolute/flake"}, + "path:/absolute/flake?lastModified=1734435836&narHash=sha256-kMBQ5PRiFLagltK0sH%2B08aiNt3zGERC2297iB6vrvlU%3D": {Type: TypePath, Path: "/absolute/flake", LastModified: 1734435836, NARHash: "sha256-kMBQ5PRiFLagltK0sH+08aiNt3zGERC2297iB6vrvlU="}, // URL-like paths can omit the "./" prefix for relative // directories. @@ -65,6 +66,7 @@ func TestParseFlakeRef(t *testing.T) { "github:NixOS/nix?rev=5233fd2ba76a3accb5aaa999c00509a11fd0793c": {Type: TypeGitHub, Owner: "NixOS", Repo: "nix", Rev: "5233fd2ba76a3accb5aaa999c00509a11fd0793c"}, "github:NixOS/nix?host=example.com": {Type: TypeGitHub, Owner: "NixOS", Repo: "nix", Host: "example.com"}, "github:NixOS/nix?host=example.com&dir=subdir": {Type: TypeGitHub, Owner: "NixOS", Repo: "nix", Host: "example.com", Dir: "subdir"}, + "github:NixOS/nix?host=example.com&dir=subdir&lastModified=1734435836&narHash=sha256-kMBQ5PRiFLagltK0sH%2B08aiNt3zGERC2297iB6vrvlU%3D": {Type: TypeGitHub, Owner: "NixOS", Repo: "nix", Host: "example.com", Dir: "subdir", NARHash: "sha256-kMBQ5PRiFLagltK0sH+08aiNt3zGERC2297iB6vrvlU="}, // The github type allows clone-style URLs. The username and // host are ignored. @@ -81,7 +83,8 @@ func TestParseFlakeRef(t *testing.T) { "git+ssh://git@example.com/repo/flake": {Type: TypeGit, URL: "ssh://git@example.com/repo/flake"}, "git:/repo/flake": {Type: TypeGit, URL: "git:/repo/flake"}, "git+file:///repo/flake": {Type: TypeGit, URL: "file:///repo/flake"}, - "git://example.com/repo/flake?ref=unstable&rev=e486d8d40e626a20e06d792db8cc5ac5aba9a5b4&dir=subdir": {Type: TypeGit, URL: "git://example.com/repo/flake?dir=subdir", Ref: "unstable", Rev: "e486d8d40e626a20e06d792db8cc5ac5aba9a5b4", Dir: "subdir"}, + "git://example.com/repo/flake?ref=unstable&rev=e486d8d40e626a20e06d792db8cc5ac5aba9a5b4&dir=subdir": {Type: TypeGit, URL: "git://example.com/repo/flake?dir=subdir", Ref: "unstable", Rev: "e486d8d40e626a20e06d792db8cc5ac5aba9a5b4", Dir: "subdir"}, + "git://example.com/repo/flake?ref=unstable&rev=e486d8d40e626a20e06d792db8cc5ac5aba9a5b4&dir=subdir&lastModified=1734435836&narHash=sha256-kMBQ5PRiFLagltK0sH%2B08aiNt3zGERC2297iB6vrvlU%3D": {Type: TypeGit, URL: "git://example.com/repo/flake?dir=subdir&lastModified=1734435836&narHash=sha256-kMBQ5PRiFLagltK0sH%2B08aiNt3zGERC2297iB6vrvlU%3D", Ref: "unstable", Rev: "e486d8d40e626a20e06d792db8cc5ac5aba9a5b4", Dir: "subdir"}, // Tarball references. "tarball+http://example.com/flake": {Type: TypeTarball, URL: "http://example.com/flake"}, @@ -99,14 +102,15 @@ func TestParseFlakeRef(t *testing.T) { "http://example.com/flake.tar.bz2": {Type: TypeTarball, URL: "http://example.com/flake.tar.bz2"}, "http://example.com/flake.tar.zst": {Type: TypeTarball, URL: "http://example.com/flake.tar.zst"}, "http://example.com/flake.tar?dir=subdir": {Type: TypeTarball, URL: "http://example.com/flake.tar?dir=subdir", Dir: "subdir"}, - "file:///flake.zip": {Type: TypeTarball, URL: "file:///flake.zip"}, - "file:///flake.tar": {Type: TypeTarball, URL: "file:///flake.tar"}, - "file:///flake.tgz": {Type: TypeTarball, URL: "file:///flake.tgz"}, - "file:///flake.tar.gz": {Type: TypeTarball, URL: "file:///flake.tar.gz"}, - "file:///flake.tar.xz": {Type: TypeTarball, URL: "file:///flake.tar.xz"}, - "file:///flake.tar.bz2": {Type: TypeTarball, URL: "file:///flake.tar.bz2"}, - "file:///flake.tar.zst": {Type: TypeTarball, URL: "file:///flake.tar.zst"}, - "file:///flake.tar?dir=subdir": {Type: TypeTarball, URL: "file:///flake.tar?dir=subdir", Dir: "subdir"}, + "http://example.com/flake.tar?dir=subdir&lastModified=1734435836&narHash=sha256-kMBQ5PRiFLagltK0sH%2B08aiNt3zGERC2297iB6vrvlU%3D": {Type: TypeTarball, URL: "http://example.com/flake.tar?dir=subdir", Dir: "subdir", LastModified: 1734435836, NARHash: "sha256-kMBQ5PRiFLagltK0sH+08aiNt3zGERC2297iB6vrvlU="}, + "file:///flake.zip": {Type: TypeTarball, URL: "file:///flake.zip"}, + "file:///flake.tar": {Type: TypeTarball, URL: "file:///flake.tar"}, + "file:///flake.tgz": {Type: TypeTarball, URL: "file:///flake.tgz"}, + "file:///flake.tar.gz": {Type: TypeTarball, URL: "file:///flake.tar.gz"}, + "file:///flake.tar.xz": {Type: TypeTarball, URL: "file:///flake.tar.xz"}, + "file:///flake.tar.bz2": {Type: TypeTarball, URL: "file:///flake.tar.bz2"}, + "file:///flake.tar.zst": {Type: TypeTarball, URL: "file:///flake.tar.zst"}, + "file:///flake.tar?dir=subdir": {Type: TypeTarball, URL: "file:///flake.tar?dir=subdir", Dir: "subdir"}, // File URL references. "file+file:///flake": {Type: TypeFile, URL: "file:///flake"}, @@ -403,5 +407,5 @@ func TestBuildQueryString(t *testing.T) { // directives). var elems []string elems = append(elems, "1") - buildQueryString(elems...) + buildQueryString(nil, elems...) } From b321c8e818546d5491ee5611476500557b880856 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Fri, 20 Dec 2024 16:18:16 -0500 Subject: [PATCH 2/2] buildQueryString -> appendQueryString --- nix/flake/flakeref.go | 64 +++++++++++++++++++++----------------- nix/flake/flakeref_test.go | 2 +- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/nix/flake/flakeref.go b/nix/flake/flakeref.go index 047f9f6a92b..8378578d938 100644 --- a/nix/flake/flakeref.go +++ b/nix/flake/flakeref.go @@ -2,7 +2,7 @@ package flake import ( - "maps" + "cmp" "net/url" "path" "slices" @@ -167,8 +167,9 @@ func parseURLRef(ref string) (parsed Ref, fragment string, err error) { parsed.Path = refURL.Path } - parsed.NARHash = refURL.Query().Get("narHash") - parsed.LastModified, err = atoiOmitZero(refURL.Query().Get("lastModified")) + query := refURL.Query() + parsed.NARHash = query.Get("narHash") + parsed.LastModified, err = atoiOmitZero(query.Get("lastModified")) if err != nil { return Ref{}, "", redact.Errorf("parse flake reference URL query parameter: lastModified=%s: %v", redact.Safe(parsed.LastModified), redact.Safe(err)) } @@ -180,8 +181,8 @@ func parseURLRef(ref string) (parsed Ref, fragment string, err error) { } query := refURL.Query() parsed.Dir = query.Get("dir") - parsed.NARHash = refURL.Query().Get("narHash") - parsed.LastModified, err = atoiOmitZero(refURL.Query().Get("lastModified")) + parsed.NARHash = query.Get("narHash") + parsed.LastModified, err = atoiOmitZero(query.Get("lastModified")) if err != nil { return Ref{}, "", redact.Errorf("parse flake reference URL query parameter: lastModified=%s: %v", redact.Safe(parsed.LastModified), redact.Safe(err)) } @@ -196,8 +197,8 @@ func parseURLRef(ref string) (parsed Ref, fragment string, err error) { parsed.Type = TypeTarball query := refURL.Query() parsed.Dir = query.Get("dir") - parsed.NARHash = refURL.Query().Get("narHash") - parsed.LastModified, err = atoiOmitZero(refURL.Query().Get("lastModified")) + parsed.NARHash = query.Get("narHash") + parsed.LastModified, err = atoiOmitZero(query.Get("lastModified")) if err != nil { return Ref{}, "", redact.Errorf("parse flake reference URL query parameter: lastModified=%s: %v", redact.Safe(parsed.LastModified), redact.Safe(err)) } @@ -213,8 +214,8 @@ func parseURLRef(ref string) (parsed Ref, fragment string, err error) { parsed.Type = TypeFile query := refURL.Query() parsed.Dir = query.Get("dir") - parsed.NARHash = refURL.Query().Get("narHash") - parsed.LastModified, err = atoiOmitZero(refURL.Query().Get("lastModified")) + parsed.NARHash = query.Get("narHash") + parsed.LastModified, err = atoiOmitZero(query.Get("lastModified")) if err != nil { return Ref{}, "", redact.Errorf("parse flake reference URL query parameter: lastModified=%s: %v", redact.Safe(parsed.LastModified), redact.Safe(err)) } @@ -228,16 +229,16 @@ func parseURLRef(ref string) (parsed Ref, fragment string, err error) { parsed.URL = refURL.String() case "git", "git+http", "git+https", "git+ssh", "git+git", "git+file": parsed.Type = TypeGit - q := refURL.Query() - parsed.Dir = q.Get("dir") - parsed.Ref = q.Get("ref") - parsed.Rev = q.Get("rev") + query := refURL.Query() + parsed.Dir = query.Get("dir") + parsed.Ref = query.Get("ref") + parsed.Rev = query.Get("rev") // ref and rev get stripped from the query parameters, but dir // stays. - q.Del("ref") - q.Del("rev") - refURL.RawQuery = q.Encode() + query.Del("ref") + query.Del("rev") + refURL.RawQuery = query.Encode() if len(refURL.Scheme) > 3 { refURL.Scheme = refURL.Scheme[4:] // remove git+ } @@ -360,7 +361,7 @@ func (r Ref) String() string { // messed with the parsed URL. return "" } - url.RawQuery = buildQueryString(url.Query(), + url.RawQuery = appendQueryString(url.Query(), "lastModified", itoaOmitZero(r.LastModified), "narHash", r.NARHash, ) @@ -386,7 +387,7 @@ func (r Ref) String() string { // messed with the parsed URL. return "" } - url.RawQuery = buildQueryString(url.Query(), "ref", r.Ref, "rev", r.Rev, "dir", r.Dir) + url.RawQuery = appendQueryString(url.Query(), "ref", r.Ref, "rev", r.Rev, "dir", r.Dir) return url.String() case TypeGitHub: if r.Owner == "" || r.Repo == "" { @@ -395,7 +396,7 @@ func (r Ref) String() string { url := &url.URL{ Scheme: "github", Opaque: buildEscapedPath(r.Owner, r.Repo, r.Rev, r.Ref), - RawQuery: buildQueryString(nil, + RawQuery: appendQueryString(nil, "host", r.Host, "dir", r.Dir, "lastModified", itoaOmitZero(r.LastModified), @@ -410,7 +411,7 @@ func (r Ref) String() string { url := &url.URL{ Scheme: "flake", Opaque: buildEscapedPath(r.ID, r.Ref, r.Rev), - RawQuery: buildQueryString(nil, + RawQuery: appendQueryString(nil, "dir", r.Dir, "lastModified", itoaOmitZero(r.LastModified), "narHash", r.NARHash, @@ -434,7 +435,7 @@ func (r Ref) String() string { url.Opaque = "." } - url.RawQuery = buildQueryString(nil, + url.RawQuery = appendQueryString(nil, "lastModified", itoaOmitZero(r.LastModified), "narHash", r.NARHash, ) @@ -453,7 +454,7 @@ func (r Ref) String() string { // messed with the parsed URL. return "" } - url.RawQuery = buildQueryString(url.Query(), + url.RawQuery = appendQueryString(url.Query(), "dir", r.Dir, "lastModified", itoaOmitZero(r.LastModified), "narHash", r.NARHash, @@ -532,22 +533,27 @@ func buildEscapedPath(elem ...string) string { return u.JoinPath(elem...).String() } -// buildQueryString builds a URL query string from a list of key-value string +// appendQueryString builds a URL query string from a list of key-value string // pairs, omitting any keys with empty values. -func buildQueryString(initial url.Values, keyval ...string) string { +func appendQueryString(query url.Values, keyval ...string) string { if len(keyval)%2 != 0 { - panic("buildQueryString: odd number of key-value pairs") + panic("appendQueryString: odd number of key-value pairs") } - q := make(url.Values, len(initial)+len(keyval)/2) - maps.Copy(q, initial) + appended := make(url.Values, len(query)+len(keyval)/2) + for k, vals := range query { + v := cmp.Or(vals...) + if v != "" { + appended.Set(k, v) + } + } for i := 0; i < len(keyval); i += 2 { k, v := keyval[i], keyval[i+1] if v != "" { - q.Set(k, v) + appended.Set(k, v) } } - return q.Encode() + return appended.Encode() } // itoaOmitZero returns an empty string if i == 0, otherwise it formats i as a diff --git a/nix/flake/flakeref_test.go b/nix/flake/flakeref_test.go index 364ec23ebca..936064658ba 100644 --- a/nix/flake/flakeref_test.go +++ b/nix/flake/flakeref_test.go @@ -407,5 +407,5 @@ func TestBuildQueryString(t *testing.T) { // directives). var elems []string elems = append(elems, "1") - buildQueryString(nil, elems...) + appendQueryString(nil, elems...) }