reference: use named capturing groups

Rewrite the regular expressions to use named capturing groups.
This simplifies handling the resulting matches, but does require
some additional handling to associate matches with their names.

Also making some changes to the matching to match how domains
are _actually_ matched; some of that was already handled in
code parsing the results of the regex, but now this is handled
by the regex itself.

Before:

    BenchmarkParse
    BenchmarkParse-10    	   12696	     93805 ns/op	    9311 B/op	     185 allocs/op
    PASS

After:

    BenchmarkParse
    BenchmarkParse-10    	   12486	     94774 ns/op	   18617 B/op	     178 allocs/op
    PASS

Benchstat:

    go test -run='^$' -bench=. -count=10 ./reference/ > old.txt
    go test -run='^$' -bench=. -count=10 ./reference/ > new.txt

    benchstat old.txt new.txt
    name       old time/op    new time/op    delta
    Parse-10   91.7µs ± 0%    97.0µs ±11%   +5.82%  (p=0.000 n=9+10)

    name       old alloc/op   new alloc/op   delta
    Parse-10    9.32kB ± 0%   18.63kB ± 0%  +99.93%  (p=0.000 n=10+10)

    name       old allocs/op  new allocs/op  delta
    Parse-10        185 ± 0%       178 ± 0%   -3.78%  (p=0.000 n=10+10)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2022-11-25 15:30:28 +01:00
parent 8e29e870a4
commit 365c73379f
No known key found for this signature in database
GPG key ID: 76698F39D527CE8C
6 changed files with 145 additions and 117 deletions

View file

@ -123,20 +123,23 @@ func ParseDockerRef(ref string) (Named, error) {
// splitDockerDomain splits a repository name to domain and remote-name.
// If no valid domain is found, the default domain is used. Repository name
// needs to be already validated before.
func splitDockerDomain(name string) (domain, remainder string) {
i := strings.IndexRune(name, '/')
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != localhost && strings.ToLower(name[:i]) == name[:i]) {
domain, remainder = defaultDomain, name
} else {
domain, remainder = name[:i], name[i+1:]
func splitDockerDomain(name string) (domainName, remoteName string) {
domainName, remoteName, ok := strings.Cut(name, "/")
switch domainName {
case legacyDefaultDomain:
domainName = defaultDomain
case defaultDomain, localhost:
// done
default:
if !ok || (!strings.ContainsAny(domainName, ".:") && strings.ToLower(domainName) == domainName) {
domainName = defaultDomain
remoteName = name
}
if domain == legacyDefaultDomain {
domain = defaultDomain
}
if domain == defaultDomain && !strings.ContainsRune(remainder, '/') {
remainder = officialRepoPrefix + remainder
if domainName == defaultDomain && !strings.ContainsRune(remoteName, '/') {
remoteName = officialRepoPrefix + remoteName
}
return
return domainName, remoteName
}
// familiarizeName returns a shortened version of the name familiar

View file

@ -35,8 +35,6 @@ func TestValidateReferenceName(t *testing.T) {
// when specified with a hostname, it removes the ambiguity from about
// whether the value is an identifier or repository name
"docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
"Docker/docker",
"DOCKER/docker",
}
invalidRepoNames := []string{
"https://github.com/docker/docker",
@ -53,6 +51,8 @@ func TestValidateReferenceName(t *testing.T) {
"[fe80::1%eth0]:5000/debian",
"[2001:db8:3:4::192.0.2.33]:5000/debian",
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
"Docker/docker",
"DOCKER/docker",
}
for _, name := range invalidRepoNames {
@ -247,17 +247,17 @@ func TestParseRepositoryInfo(t *testing.T) {
},
{
RemoteName: "bar",
FamiliarName: "Foo/bar",
FullName: "Foo/bar",
FamiliarName: "Foo.com/bar",
FullName: "Foo.com/bar",
AmbiguousName: "",
Domain: "Foo",
Domain: "Foo.com",
},
{
RemoteName: "bar",
FamiliarName: "FOO/bar",
FullName: "FOO/bar",
FamiliarName: "FOO.COM/bar",
FullName: "FOO.COM/bar",
AmbiguousName: "",
Domain: "FOO",
Domain: "FOO.COM",
},
}

View file

@ -166,11 +166,8 @@ func Path(named Named) (name string) {
}
func splitDomain(name string) (string, string) {
match := anchoredNameRegexp.FindStringSubmatch(name)
if len(match) != 3 {
return "", name
}
return match[1], match[2]
named, _ := getNamedMatches(anchoredNameRegexp, name)
return named["domain"], named["repository"]
}
// SplitHostname splits a named reference into a
@ -189,8 +186,8 @@ func SplitHostname(named Named) (string, string) {
// Parse parses s and returns a syntactically valid Reference.
// If an error was encountered it is returned, along with a nil Reference.
func Parse(s string) (Reference, error) {
matches := ReferenceRegexp.FindStringSubmatch(s)
if matches == nil {
namedMatches, ok := getNamedMatches(ReferenceRegexp, s)
if !ok {
if s == "" {
return nil, ErrNameEmpty
}
@ -200,28 +197,20 @@ func Parse(s string) (Reference, error) {
return nil, ErrReferenceInvalidFormat
}
if len(matches[1]) > NameTotalLengthMax {
if len(namedMatches["name"]) > NameTotalLengthMax {
return nil, ErrNameTooLong
}
var repo repository
nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1])
if len(nameMatch) == 3 {
repo.domain = nameMatch[1]
repo.path = nameMatch[2]
} else {
repo.domain = ""
repo.path = matches[1]
}
ref := reference{
namedRepository: repo,
tag: matches[2],
namedRepository: repository{
domain: namedMatches["domain"],
path: namedMatches["repository"],
},
tag: namedMatches["tag"],
}
if matches[3] != "" {
if namedMatches["digest"] != "" {
var err error
ref.digest, err = digest.Parse(matches[3])
ref.digest, err = digest.Parse(namedMatches["digest"])
if err != nil {
return nil, err
}

View file

@ -101,12 +101,10 @@ func TestReferenceParse(t *testing.T) {
input: "Uppercase:tag",
err: ErrNameContainsUppercase,
},
// FIXME "Uppercase" is incorrectly handled as a domain-name here, therefore passes.
// See https://github.com/distribution/distribution/pull/1778, and https://github.com/docker/docker/pull/20175
// {
// input: "Uppercase/lowercase:tag",
// err: ErrNameContainsUppercase,
// },
{
input: "Uppercase/lowercase:tag",
err: ErrNameContainsUppercase,
},
{
input: "test:5000/Uppercase/lowercase:tag",
err: ErrNameContainsUppercase,
@ -122,7 +120,6 @@ func TestReferenceParse(t *testing.T) {
},
{
input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max",
domain: "a",
repository: strings.Repeat("a/", 127) + "a",
tag: "tag-puts-this-over-max",
},
@ -167,7 +164,6 @@ func TestReferenceParse(t *testing.T) {
},
{
input: "foo/foo_bar.com:8080",
domain: "foo",
repository: "foo/foo_bar.com",
tag: "8080",
},
@ -541,7 +537,7 @@ func TestSerialization(t *testing.T) {
b2, err := json.Marshal(st)
if err != nil {
t.Errorf("error marshing serialization type: %v", err)
t.Errorf("error marshaling serialization type: %v", err)
}
if string(b) != string(b2) {

View file

@ -29,8 +29,8 @@ var IdentifierRegexp = regexp.MustCompile(identifier)
var NameRegexp = regexp.MustCompile(namePat)
// ReferenceRegexp is the full supported format of a reference. The regexp
// is anchored and has capturing groups for name, tag, and digest
// components.
// is anchored and has capturing groups for domain, name, repository, tag,
// and digest components.
var ReferenceRegexp = regexp.MustCompile(referencePat)
// TagRegexp matches valid tag names. From [docker/docker:graph/tags.go].
@ -60,9 +60,12 @@ const (
// repository name to start with a component as defined by DomainRegexp.
domainNameComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`
// port matches a port-number including the port separator (e.g. ":80").
port = `(?::[0-9]+)`
// optionalPort matches an optional port-number including the port separator
// (e.g. ":80").
optionalPort = `(?::[0-9]+)?`
optionalPort = port + `?`
// tag matches valid tag names. From docker/docker:graph/tags.go.
tag = `[\w][\w.-]{0,127}`
@ -96,14 +99,23 @@ var (
// that may be part of image names. This is purposely a subset of what is
// allowed by DNS to ensure backwards compatibility with Docker image
// names. This includes IPv4 addresses on decimal format.
domainName = domainNameComponent + anyTimes(`\.`+domainNameComponent)
//
// TODO(thaJeztah): disambiguate: docker requires domain-name to be either;
// - localhost (special case)
// - at least one "."
// - or a ":port"
//
// Any other domain is considered a path-element.
domainName = domainNameComponent + oneOrMore(`\.`+domainNameComponent)
domainWithPort = domainNameComponent + anyTimes(`\.`+domainNameComponent) + port
// host defines the structure of potential domains based on the URI
// Host subcomponent on rfc3986. It may be a subset of DNS domain name,
// or an IPv4 address in decimal format, or an IPv6 address between square
// brackets (excluding zone identifiers as defined by rfc6874 or special
// addresses such as IPv4-Mapped).
host = `(?:` + domainName + `|` + ipv6address + `)`
host = `(?:` + localhost + `|` + domainName + `|` + domainWithPort + `|` + ipv6address + optionalPort + `)`
// allowed by the URI Host subcomponent on rfc3986 to ensure backwards
// compatibility with Docker image names.
@ -127,13 +139,15 @@ var (
//
// pathComponent[[/pathComponent] ...] // e.g., "library/ubuntu"
remoteName = pathComponent + anyTimes(`/`+pathComponent)
namePat = optional(domainAndPort+`/`) + remoteName
domainAndRepo = optional(capture("domain", domainAndPort), `/`) + capture("repository", remoteName)
// anchoredNameRegexp is used to parse a name value, capturing the
// domain and trailing components.
anchoredNameRegexp = regexp.MustCompile(anchored(optional(capture(domainAndPort), `/`), capture(remoteName)))
anchoredNameRegexp = regexp.MustCompile(anchored(domainAndRepo))
referencePat = anchored(capture(namePat), optional(`:`, capture(tag)), optional(`@`, capture(digestPat)))
namePat = optional(domainAndPort+`/`) + remoteName
referencePat = anchored(capture("name", domainAndRepo), optional(`:`, capture("tag", tag)), optional(`@`, capture("digest", digestPat)))
// anchoredIdentifierRegexp is used to check or match an
// identifier value, anchored at start and end of string.
@ -152,12 +166,41 @@ func anyTimes(res ...string) string {
return `(?:` + strings.Join(res, "") + `)*`
}
// oneOrMore wraps the expression in a non-capturing group that can occur
// one or more times.
func oneOrMore(res ...string) string {
return `(?:` + strings.Join(res, "") + `)+`
}
// capture wraps the expression in a capturing group.
func capture(res ...string) string {
return `(` + strings.Join(res, "") + `)`
func capture(name string, res ...string) string {
return `(?P<` + name + `>` + strings.Join(res, "") + `)`
}
// anchored anchors the regular expression by adding start and end delimiters.
func anchored(res ...string) string {
return `^` + strings.Join(res, "") + `$`
}
// getNamedMatches returns a map containing matches for each named subexpression.
// It panics if the given regular expression does not contain named subexpressions.
func getNamedMatches(r *regexp.Regexp, input string) (namedMatches map[string]string, ok bool) {
if len(r.SubexpNames()) == 0 {
panic("regex does not have named subexpressions: " + r.String())
}
matches := r.FindStringSubmatch(input)
if ok = len(matches) > 0; !ok {
return nil, false
}
namedMatches = make(map[string]string, len(matches))
// We loop through matches here, in case there's optional named match-groups.
for i, match := range matches {
if i == 0 {
// first entry is always the full string that was matched
continue
}
namedMatches[r.SubexpNames()[i]] = match
}
return namedMatches, true
}

View file

@ -1,6 +1,7 @@
package reference
import (
"reflect"
"regexp"
"strings"
"testing"
@ -9,27 +10,24 @@ import (
type regexpMatch struct {
input string
match bool
subs []string
named map[string]string
}
func checkRegexp(t *testing.T, r *regexp.Regexp, m regexpMatch) {
t.Helper()
matches := r.FindStringSubmatch(m.input)
if m.match && matches != nil {
if len(matches) != (r.NumSubexp()+1) || matches[0] != m.input {
t.Fatalf("Bad match result %#v for %q", matches, m.input)
var matched bool
if len(m.named) > 0 {
var namedMatches map[string]string
namedMatches, matched = getNamedMatches(r, m.input)
if !reflect.DeepEqual(m.named, namedMatches) {
t.Errorf("Named matches differ:\nExpected: %+v\nGot: %+v", m.named, namedMatches)
}
if len(matches) < (len(m.subs) + 1) {
t.Errorf("Expected %d sub matches, only have %d for %q", len(m.subs), len(matches)-1, m.input)
} else {
matched = len(r.FindStringSubmatch(m.input)) > 0
}
for i := range m.subs {
if m.subs[i] != matches[i+1] {
t.Errorf("Unexpected submatch %d: %q, expected %q for %q", i+1, matches[i+1], m.subs[i], m.input)
}
}
} else if m.match {
if m.match && !matched {
t.Errorf("Expected match for %q", m.input)
} else if matches != nil {
} else if !m.match && matched {
t.Errorf("Unexpected match for %q", m.input)
}
}
@ -62,7 +60,7 @@ func TestDomainRegexp(t *testing.T) {
},
{
input: "a",
match: true,
match: false,
},
{
input: "a.b",
@ -189,37 +187,37 @@ func TestFullNameRegexp(t *testing.T) {
{
input: "short",
match: true,
subs: []string{"", "short"},
named: map[string]string{"domain": "", "repository": "short"},
},
{
input: "simple/name",
match: true,
subs: []string{"simple", "name"},
named: map[string]string{"domain": "", "repository": "simple/name"},
},
{
input: "library/ubuntu",
match: true,
subs: []string{"library", "ubuntu"},
named: map[string]string{"domain": "", "repository": "library/ubuntu"},
},
{
input: "docker/stevvooe/app",
match: true,
subs: []string{"docker", "stevvooe/app"},
named: map[string]string{"domain": "", "repository": "docker/stevvooe/app"},
},
{
input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb",
match: true,
subs: []string{"aa", "aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb"},
named: map[string]string{"domain": "", "repository": "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb"},
},
{
input: "aa/aa/bb/bb/bb",
match: true,
subs: []string{"aa", "aa/bb/bb/bb"},
named: map[string]string{"domain": "", "repository": "aa/aa/bb/bb/bb"},
},
{
input: "a/a/a/a",
match: true,
subs: []string{"a", "a/a/a"},
named: map[string]string{"domain": "", "repository": "a/a/a/a"},
},
{
input: "a/a/a/a/",
@ -232,22 +230,22 @@ func TestFullNameRegexp(t *testing.T) {
{
input: "a",
match: true,
subs: []string{"", "a"},
named: map[string]string{"domain": "", "repository": "a"},
},
{
input: "a/aa",
match: true,
subs: []string{"a", "aa"},
named: map[string]string{"domain": "", "repository": "a/aa"},
},
{
input: "a/aa/a",
match: true,
subs: []string{"a", "aa/a"},
named: map[string]string{"domain": "", "repository": "a/aa/a"},
},
{
input: "foo.com",
match: true,
subs: []string{"", "foo.com"},
named: map[string]string{"domain": "", "repository": "foo.com"},
},
{
input: "foo.com/",
@ -256,7 +254,7 @@ func TestFullNameRegexp(t *testing.T) {
{
input: "foo.com:8080/bar",
match: true,
subs: []string{"foo.com:8080", "bar"},
named: map[string]string{"domain": "foo.com:8080", "repository": "bar"},
},
{
input: "foo.com:http/bar",
@ -265,27 +263,27 @@ func TestFullNameRegexp(t *testing.T) {
{
input: "foo.com/bar",
match: true,
subs: []string{"foo.com", "bar"},
named: map[string]string{"domain": "foo.com", "repository": "bar"},
},
{
input: "foo.com/bar/baz",
match: true,
subs: []string{"foo.com", "bar/baz"},
named: map[string]string{"domain": "foo.com", "repository": "bar/baz"},
},
{
input: "localhost:8080/bar",
match: true,
subs: []string{"localhost:8080", "bar"},
named: map[string]string{"domain": "localhost:8080", "repository": "bar"},
},
{
input: "sub-dom1.foo.com/bar/baz/quux",
match: true,
subs: []string{"sub-dom1.foo.com", "bar/baz/quux"},
named: map[string]string{"domain": "sub-dom1.foo.com", "repository": "bar/baz/quux"},
},
{
input: "blog.foo.com/bar/baz",
match: true,
subs: []string{"blog.foo.com", "bar/baz"},
named: map[string]string{"domain": "blog.foo.com", "repository": "bar/baz"},
},
{
input: "a^a",
@ -302,12 +300,12 @@ func TestFullNameRegexp(t *testing.T) {
{
input: "aa-a/a",
match: true,
subs: []string{"aa-a", "a"},
named: map[string]string{"domain": "", "repository": "aa-a/a"},
},
{
input: strings.Repeat("a/", 128) + "a",
match: true,
subs: []string{"a", strings.Repeat("a/", 127) + "a"},
named: map[string]string{"domain": "", "repository": strings.Repeat("a/", 128) + "a"},
},
{
input: "a-/a/a/a",
@ -340,12 +338,12 @@ func TestFullNameRegexp(t *testing.T) {
{
input: "foo_bar",
match: true,
subs: []string{"", "foo_bar"},
named: map[string]string{"domain": "", "repository": "foo_bar"},
},
{
input: "foo_bar.com",
match: true,
subs: []string{"", "foo_bar.com"},
named: map[string]string{"domain": "", "repository": "foo_bar.com"},
},
{
input: "foo_bar.com:8080",
@ -358,7 +356,7 @@ func TestFullNameRegexp(t *testing.T) {
{
input: "foo.com/foo_bar",
match: true,
subs: []string{"foo.com", "foo_bar"},
named: map[string]string{"domain": "foo.com", "repository": "foo_bar"},
},
{
input: "____/____",
@ -375,27 +373,27 @@ func TestFullNameRegexp(t *testing.T) {
{
input: "b.gcr.io/test.example.com/my-app",
match: true,
subs: []string{"b.gcr.io", "test.example.com/my-app"},
named: map[string]string{"domain": "b.gcr.io", "repository": "test.example.com/my-app"},
},
{
input: "xn--n3h.com/myimage", // ☃.com in punycode
match: true,
subs: []string{"xn--n3h.com", "myimage"},
named: map[string]string{"domain": "xn--n3h.com", "repository": "myimage"},
},
{
input: "xn--7o8h.com/myimage", // 🐳.com in punycode
match: true,
subs: []string{"xn--7o8h.com", "myimage"},
named: map[string]string{"domain": "xn--7o8h.com", "repository": "myimage"},
},
{
input: "example.com/xn--7o8h.com/myimage", // 🐳.com in punycode
match: true,
subs: []string{"example.com", "xn--7o8h.com/myimage"},
named: map[string]string{"domain": "example.com", "repository": "xn--7o8h.com/myimage"},
},
{
input: "example.com/some_separator__underscore/myimage",
match: true,
subs: []string{"example.com", "some_separator__underscore/myimage"},
named: map[string]string{"domain": "example.com", "repository": "some_separator__underscore/myimage"},
},
{
input: "example.com/__underscore/myimage",
@ -444,17 +442,17 @@ func TestFullNameRegexp(t *testing.T) {
{
input: "do__cker/docker",
match: true,
subs: []string{"", "do__cker/docker"},
named: map[string]string{"domain": "", "repository": "do__cker/docker"},
},
{
input: "b.gcr.io/test.example.com/my-app",
match: true,
subs: []string{"b.gcr.io", "test.example.com/my-app"},
named: map[string]string{"domain": "b.gcr.io", "repository": "test.example.com/my-app"},
},
{
input: "registry.io/foo/project--id.module--name.ver---sion--name",
match: true,
subs: []string{"registry.io", "foo/project--id.module--name.ver---sion--name"},
named: map[string]string{"domain": "registry.io", "repository": "foo/project--id.module--name.ver---sion--name"},
},
{
input: "Asdf.com/foo/bar", // uppercase character in hostname
@ -476,26 +474,25 @@ func TestFullNameRegexp(t *testing.T) {
func TestReferenceRegexp(t *testing.T) {
t.Parallel()
if ReferenceRegexp.NumSubexp() != 3 {
t.Fatalf("anchored name regexp should have three submatches: %v, %v != 3",
ReferenceRegexp, ReferenceRegexp.NumSubexp())
if ReferenceRegexp.NumSubexp() != 5 {
t.Fatalf("anchored name regexp should have five submatches: %v, %v != 5", ReferenceRegexp, ReferenceRegexp.NumSubexp())
}
tests := []regexpMatch{
{
input: "registry.com:8080/myapp:tag",
match: true,
subs: []string{"registry.com:8080/myapp", "tag", ""},
named: map[string]string{"domain": "registry.com:8080", "name": "registry.com:8080/myapp", "repository": "myapp", "tag": "tag", "digest": ""},
},
{
input: "registry.com:8080/myapp@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"registry.com:8080/myapp", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
named: map[string]string{"domain": "registry.com:8080", "name": "registry.com:8080/myapp", "repository": "myapp", "tag": "", "digest": "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "registry.com:8080/myapp:tag2@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"registry.com:8080/myapp", "tag2", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
named: map[string]string{"domain": "registry.com:8080", "name": "registry.com:8080/myapp", "repository": "myapp", "tag": "tag2", "digest": "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "registry.com:8080/myapp@sha256:badbadbadbad",
@ -513,12 +510,12 @@ func TestReferenceRegexp(t *testing.T) {
input:// localhost treated as name, missing tag with 8080 as tag
"localhost:8080@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"localhost", "8080", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
named: map[string]string{"domain": "", "name": "localhost", "repository": "localhost", "tag": "8080", "digest": "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "localhost:8080/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"localhost:8080/name", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
named: map[string]string{"domain": "localhost:8080", "name": "localhost:8080/name", "repository": "name", "tag": "", "digest": "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "localhost:http/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
@ -528,7 +525,7 @@ func TestReferenceRegexp(t *testing.T) {
// localhost will be treated as an image name without a host
input: "localhost@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"localhost", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
named: map[string]string{"domain": "", "name": "localhost", "repository": "localhost", "tag": "", "digest": "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "registry.com:8080/myapp@bad",