Update to provide small and clear interfaces
Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)master
							parent
							
								
									f8c09b6a7d
								
							
						
					
					
						commit
						31a448a628
					
				|  | @ -7,11 +7,13 @@ | ||||||
| //
 | //
 | ||||||
| //	// repository.go
 | //	// repository.go
 | ||||||
| //	repository			:= hostname ['/' component]+
 | //	repository			:= hostname ['/' component]+
 | ||||||
| //	hostname 			:= component [':' port-number]
 | //	hostname 			:= hostcomponent [':' port-number]
 | ||||||
| //	component			:= alpha-numeric [separator alpha-numeric]*
 | //	component			:= alpha-numeric [separator alpha-numeric]*
 | ||||||
|  | //	hostcomponent                   := [hostpart '.']* hostpart
 | ||||||
| // 	alpha-numeric			:= /[a-zA-Z0-9]+/
 | // 	alpha-numeric			:= /[a-zA-Z0-9]+/
 | ||||||
| //	separator			:= /[._-]/
 | //	separator			:= /[_-]/
 | ||||||
| //	port-number			:= /[0-9]+/
 | //	port-number			:= /[0-9]+/
 | ||||||
|  | //	hostpart                        := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
 | ||||||
| //
 | //
 | ||||||
| //	// tag.go
 | //	// tag.go
 | ||||||
| //	tag                             := /[\w][\w.-]{0,127}/
 | //	tag                             := /[\w][\w.-]{0,127}/
 | ||||||
|  | @ -20,167 +22,224 @@ | ||||||
| //	digest                          := digest-algorithm ":" digest-hex
 | //	digest                          := digest-algorithm ":" digest-hex
 | ||||||
| //	digest-algorithm                := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]
 | //	digest-algorithm                := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]
 | ||||||
| //	digest-algorithm-separator      := /[+.-_]/
 | //	digest-algorithm-separator      := /[+.-_]/
 | ||||||
| //	digest-algorithm-component      := /[A-Za-z]/ /[A-Za-z0-9]*/
 | //	digest-algorithm-component      := /[A-Za-z][A-Za-z0-9]*/
 | ||||||
| //	digest-hex                      := /[A-Za-z0-9_-]+/ ; supports hex bytes or url safe base64
 | //	digest-hex                      := /[0-9a-fA-F]{32,}/ ; Atleast 128 bit digest value
 | ||||||
| package reference | package reference | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"regexp" | 	"fmt" | ||||||
| 
 | 
 | ||||||
| 	"github.com/docker/distribution/digest" | 	"github.com/docker/distribution/digest" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference.
 | const ( | ||||||
| var ErrReferenceInvalidFormat = errors.New("invalid reference format") | 	// NameTotalLengthMax is the maximum total number of characters in a repository name.
 | ||||||
|  | 	NameTotalLengthMax = 255 | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| // Reference abstracts types that reference images in a certain way.
 | var ( | ||||||
|  | 	// ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference.
 | ||||||
|  | 	ErrReferenceInvalidFormat = errors.New("invalid reference format") | ||||||
|  | 
 | ||||||
|  | 	// ErrNameEmpty is returned for empty, invalid repository names.
 | ||||||
|  | 	ErrNameEmpty = errors.New("repository name must have at least one component") | ||||||
|  | 
 | ||||||
|  | 	// ErrNameTooLong is returned when a repository name is longer than
 | ||||||
|  | 	// RepositoryNameTotalLengthMax
 | ||||||
|  | 	ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax) | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Reference is an opaque object reference identifier that may include
 | ||||||
|  | // modifiers such as a hostname, name, tag, and digest.
 | ||||||
| type Reference interface { | type Reference interface { | ||||||
| 	// Repository returns the repository part of a reference
 | 	// String returns the full reference
 | ||||||
| 	Repository() Repository |  | ||||||
| 	// String returns the entire reference, including the repository part
 |  | ||||||
| 	String() string | 	String() string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func parseHostname(s string) (hostname, tail string) { | // Named is an object with a full name
 | ||||||
| 	tail = s | type Named interface { | ||||||
| 	i := regexp.MustCompile(`^` + RepositoryNameHostnameRegexp.String()).FindStringIndex(s) | 	Name() string | ||||||
| 	if i == nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	return s[:i[1]], s[i[1]:] |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func parseRepositoryName(s string) (repo, tail string) { | // Tagged is an object which has a tag
 | ||||||
| 	tail = s | type Tagged interface { | ||||||
| 	i := regexp.MustCompile(`^/(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String()).FindStringIndex(s) | 	Tag() string | ||||||
| 	if i == nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	return s[:i[1]], s[i[1]:] |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func parseTag(s string) (tag Tag, tail string) { | // Digested is an object which has a digest
 | ||||||
| 	tail = s | // in which it can be referenced by
 | ||||||
| 	if len(s) == 0 || s[0] != ':' { | type Digested interface { | ||||||
| 		return | 	Digest() digest.Digest | ||||||
| 	} |  | ||||||
| 	tag, err := NewTag(s[1:]) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	tail = s[len(tag)+1:] |  | ||||||
| 	return |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func parseDigest(s string) (dgst digest.Digest, tail string) { | // Canonical reference is an object with a fully unique
 | ||||||
| 	tail = s | // name including a name with hostname and digest
 | ||||||
| 	if len(s) == 0 || s[0] != '@' { | type Canonical interface { | ||||||
| 		return | 	Reference | ||||||
|  | 	Named | ||||||
|  | 	Digested | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SplitHostname splits a named reference into a
 | ||||||
|  | // hostname and name string. If no valid hostname is
 | ||||||
|  | // found, the hostname is empty and the full value
 | ||||||
|  | // is returned as name
 | ||||||
|  | func SplitHostname(named Named) (string, string) { | ||||||
|  | 	name := named.Name() | ||||||
|  | 	match := anchoredNameRegexp.FindStringSubmatch(name) | ||||||
|  | 	if match == nil || len(match) != 3 { | ||||||
|  | 		return "", name | ||||||
| 	} | 	} | ||||||
| 	dgst, err := digest.ParseDigest(s[1:]) | 	return match[1], match[2] | ||||||
| 	if err != nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	tail = s[len(dgst)+1:] |  | ||||||
| 	return |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Parse parses s and returns a syntactically valid Reference.
 | // Parse parses s and returns a syntactically valid Reference.
 | ||||||
| // If an error was encountered it is returned, along with a nil Reference.
 | // If an error was encountered it is returned, along with a nil Reference.
 | ||||||
|  | // NOTE: Parse will not handle short digests.
 | ||||||
| func Parse(s string) (Reference, error) { | func Parse(s string) (Reference, error) { | ||||||
| 	hostname, s := parseHostname(s) | 	matches := ReferenceRegexp.FindStringSubmatch(s) | ||||||
| 	name, s := parseRepositoryName(s) | 	if matches == nil { | ||||||
| 	repository := Repository{Hostname: hostname, Name: name} | 		if s == "" { | ||||||
| 	if err := repository.Validate(); err != nil { | 			return nil, ErrNameEmpty | ||||||
| 		return nil, err | 		} | ||||||
| 	} | 		// TODO(dmcgowan): Provide more specific and helpful error
 | ||||||
| 	tag, s := parseTag(s) |  | ||||||
| 	dgst, s := parseDigest(s) |  | ||||||
| 	if len(s) > 0 { |  | ||||||
| 		return nil, ErrReferenceInvalidFormat | 		return nil, ErrReferenceInvalidFormat | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if dgst != "" { | 	if len(matches[1]) > NameTotalLengthMax { | ||||||
| 		return DigestReference{repository: repository, digest: dgst, tag: tag}, nil | 		return nil, ErrNameTooLong | ||||||
| 	} | 	} | ||||||
| 	if tag != "" { | 
 | ||||||
| 		return TagReference{repository: repository, tag: tag}, nil | 	ref := reference{ | ||||||
|  | 		name: matches[1], | ||||||
|  | 		tag:  matches[2], | ||||||
| 	} | 	} | ||||||
| 	return nil, ErrReferenceInvalidFormat | 	if matches[3] != "" { | ||||||
| } | 		var err error | ||||||
| 
 | 		ref.digest, err = digest.ParseDigest(matches[3]) | ||||||
| // DigestReference represents a reference of the form `repository@sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef`.
 | 		if err != nil { | ||||||
| // Implements the Reference interface.
 | 			return nil, err | ||||||
| type DigestReference struct { |  | ||||||
| 	repository Repository |  | ||||||
| 	digest     digest.Digest |  | ||||||
| 	tag        Tag |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Repository returns the repository part.
 |  | ||||||
| func (r DigestReference) Repository() Repository { return r.repository } |  | ||||||
| 
 |  | ||||||
| // String returns the full string reference.
 |  | ||||||
| func (r DigestReference) String() string { |  | ||||||
| 	return r.repository.String() + "@" + string(r.digest) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // NewDigestReference returns an initialized DigestReference.
 |  | ||||||
| func NewDigestReference(canonicalRepository string, digest digest.Digest, optionalTag Tag) (DigestReference, error) { |  | ||||||
| 	ref := DigestReference{} |  | ||||||
| 
 |  | ||||||
| 	repo, err := NewRepository(canonicalRepository) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return ref, err |  | ||||||
| 	} |  | ||||||
| 	ref.repository = repo |  | ||||||
| 
 |  | ||||||
| 	if err := digest.Validate(); err != nil { |  | ||||||
| 		return ref, err |  | ||||||
| 	} |  | ||||||
| 	ref.digest = digest |  | ||||||
| 
 |  | ||||||
| 	if len(optionalTag) > 0 { |  | ||||||
| 		if err := optionalTag.Validate(); err != nil { |  | ||||||
| 			return ref, err |  | ||||||
| 		} | 		} | ||||||
| 		ref.tag = optionalTag |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return ref, err | 	r := getBestReferenceType(ref) | ||||||
| } | 	if r == nil { | ||||||
| 
 | 		return nil, ErrNameEmpty | ||||||
| // TagReference represents a reference of the form `repository:tag`.
 |  | ||||||
| // Implements the Reference interface.
 |  | ||||||
| type TagReference struct { |  | ||||||
| 	repository Repository |  | ||||||
| 	tag        Tag |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Repository returns the repository part.
 |  | ||||||
| func (r TagReference) Repository() Repository { return r.repository } |  | ||||||
| 
 |  | ||||||
| // String returns the full string reference.
 |  | ||||||
| func (r TagReference) String() string { |  | ||||||
| 	return r.repository.String() + ":" + string(r.tag) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // NewTagReference returns an initialized TagReference.
 |  | ||||||
| func NewTagReference(canonicalRepository string, tagName string) (TagReference, error) { |  | ||||||
| 	ref := TagReference{} |  | ||||||
| 
 |  | ||||||
| 	repo, err := NewRepository(canonicalRepository) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return ref, err |  | ||||||
| 	} | 	} | ||||||
| 	ref.repository = repo |  | ||||||
| 
 | 
 | ||||||
| 	tag, err := NewTag(tagName) | 	return r, nil | ||||||
| 	if err != nil { | } | ||||||
| 		return ref, err | 
 | ||||||
| 	} | // ParseNamed parses the input string and returns a named
 | ||||||
| 	ref.tag = tag | // object representing the given string. If the input is
 | ||||||
| 
 | // invalid ErrReferenceInvalidFormat will be returned.
 | ||||||
| 	return ref, err | func ParseNamed(name string) (Named, error) { | ||||||
|  | 	if !anchoredNameRegexp.MatchString(name) { | ||||||
|  | 		return nil, ErrReferenceInvalidFormat | ||||||
|  | 	} | ||||||
|  | 	return repository(name), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getBestReferenceType(ref reference) Reference { | ||||||
|  | 	if ref.name == "" { | ||||||
|  | 		// Allow digest only references
 | ||||||
|  | 		if ref.digest != "" { | ||||||
|  | 			return digestReference(ref.digest) | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	if ref.tag == "" { | ||||||
|  | 		if ref.digest != "" { | ||||||
|  | 			return canonicalReference{ | ||||||
|  | 				name:   ref.name, | ||||||
|  | 				digest: ref.digest, | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return repository(ref.name) | ||||||
|  | 	} | ||||||
|  | 	if ref.digest == "" { | ||||||
|  | 		return taggedReference{ | ||||||
|  | 			name: ref.name, | ||||||
|  | 			tag:  ref.tag, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ref | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type reference struct { | ||||||
|  | 	name   string | ||||||
|  | 	tag    string | ||||||
|  | 	digest digest.Digest | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r reference) String() string { | ||||||
|  | 	return r.name + ":" + r.tag + "@" + r.digest.String() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r reference) Name() string { | ||||||
|  | 	return r.name | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r reference) Tag() string { | ||||||
|  | 	return r.tag | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r reference) Digest() digest.Digest { | ||||||
|  | 	return r.digest | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type repository string | ||||||
|  | 
 | ||||||
|  | func (r repository) String() string { | ||||||
|  | 	return string(r) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r repository) Name() string { | ||||||
|  | 	return string(r) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type digestReference digest.Digest | ||||||
|  | 
 | ||||||
|  | func (d digestReference) String() string { | ||||||
|  | 	return d.String() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (d digestReference) Digest() digest.Digest { | ||||||
|  | 	return digest.Digest(d) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type taggedReference struct { | ||||||
|  | 	name string | ||||||
|  | 	tag  string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (t taggedReference) String() string { | ||||||
|  | 	return t.name + ":" + t.tag | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (t taggedReference) Name() string { | ||||||
|  | 	return t.name | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (t taggedReference) Tag() string { | ||||||
|  | 	return t.tag | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type canonicalReference struct { | ||||||
|  | 	name   string | ||||||
|  | 	digest digest.Digest | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c canonicalReference) String() string { | ||||||
|  | 	return c.name + "@" + c.digest.String() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c canonicalReference) Name() string { | ||||||
|  | 	return c.name | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c canonicalReference) Digest() digest.Digest { | ||||||
|  | 	return c.digest | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,56 +1,267 @@ | ||||||
| package reference | package reference | ||||||
| 
 | 
 | ||||||
| /* | import ( | ||||||
| var refRegex = regexp.MustCompile(`^([a-z0-9]+(?:[-._][a-z0-9]+)*(?::[0-9]+(?:/[a-z0-9]+(?:[-._][a-z0-9]+)*)+|(?:/[a-z0-9]+(?:[-._][a-z0-9]+)*)+)?)(:[\w][\w.-]{0,127})?(@` + digest.DigestRegexp.String() + `)?$`) | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
| 
 | 
 | ||||||
| func getRepo(s string) string { | 	"github.com/docker/distribution/digest" | ||||||
| 	matches := refRegex.FindStringSubmatch(s) | ) | ||||||
| 	if len(matches) == 0 { | 
 | ||||||
| 		return "" | func TestReferenceParse(t *testing.T) { | ||||||
|  | 	// referenceTestcases is a unified set of testcases for
 | ||||||
|  | 	// testing the parsing of references
 | ||||||
|  | 	referenceTestcases := []struct { | ||||||
|  | 		// input is the repository name or name component testcase
 | ||||||
|  | 		input string | ||||||
|  | 		// err is the error expected from Parse, or nil
 | ||||||
|  | 		err error | ||||||
|  | 		// repository is the string representation for the reference
 | ||||||
|  | 		repository string | ||||||
|  | 		// hostname is the hostname expected in the reference
 | ||||||
|  | 		hostname string | ||||||
|  | 		// tag is the tag for the reference
 | ||||||
|  | 		tag string | ||||||
|  | 		// digest is the digest for the reference (enforces digest reference)
 | ||||||
|  | 		digest string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			input:      "test_com", | ||||||
|  | 			repository: "test_com", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "test.com:tag", | ||||||
|  | 			repository: "test.com", | ||||||
|  | 			tag:        "tag", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "test.com:5000", | ||||||
|  | 			repository: "test.com", | ||||||
|  | 			tag:        "5000", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "test.com/repo:tag", | ||||||
|  | 			hostname:   "test.com", | ||||||
|  | 			repository: "test.com/repo", | ||||||
|  | 			tag:        "tag", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "test:5000/repo", | ||||||
|  | 			hostname:   "test:5000", | ||||||
|  | 			repository: "test:5000/repo", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "test:5000/repo:tag", | ||||||
|  | 			hostname:   "test:5000", | ||||||
|  | 			repository: "test:5000/repo", | ||||||
|  | 			tag:        "tag", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", | ||||||
|  | 			hostname:   "test:5000", | ||||||
|  | 			repository: "test:5000/repo", | ||||||
|  | 			digest:     "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", | ||||||
|  | 			hostname:   "test:5000", | ||||||
|  | 			repository: "test:5000/repo", | ||||||
|  | 			tag:        "tag", | ||||||
|  | 			digest:     "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "test:5000/repo", | ||||||
|  | 			hostname:   "test:5000", | ||||||
|  | 			repository: "test:5000/repo", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "", | ||||||
|  | 			err:   ErrNameEmpty, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: ":justtag", | ||||||
|  | 			err:   ErrReferenceInvalidFormat, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", | ||||||
|  | 			err:   ErrReferenceInvalidFormat, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", | ||||||
|  | 			err:   digest.ErrDigestUnsupported, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: strings.Repeat("a/", 128) + "a:tag", | ||||||
|  | 			err:   ErrNameTooLong, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      strings.Repeat("a/", 127) + "a:tag-puts-this-over-max", | ||||||
|  | 			hostname:   "a", | ||||||
|  | 			repository: strings.Repeat("a/", 127) + "a", | ||||||
|  | 			tag:        "tag-puts-this-over-max", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "aa/asdf$$^/aa", | ||||||
|  | 			err:   ErrReferenceInvalidFormat, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "sub-dom1.foo.com/bar/baz/quux", | ||||||
|  | 			hostname:   "sub-dom1.foo.com", | ||||||
|  | 			repository: "sub-dom1.foo.com/bar/baz/quux", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "sub-dom1.foo.com/bar/baz/quux:some-long-tag", | ||||||
|  | 			hostname:   "sub-dom1.foo.com", | ||||||
|  | 			repository: "sub-dom1.foo.com/bar/baz/quux", | ||||||
|  | 			tag:        "some-long-tag", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "b.gcr.io/test.example.com/my-app:test.example.com", | ||||||
|  | 			hostname:   "b.gcr.io", | ||||||
|  | 			repository: "b.gcr.io/test.example.com/my-app", | ||||||
|  | 			tag:        "test.example.com", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode
 | ||||||
|  | 			hostname:   "xn--n3h.com", | ||||||
|  | 			repository: "xn--n3h.com/myimage", | ||||||
|  | 			tag:        "xn--n3h.com", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode
 | ||||||
|  | 			hostname:   "xn--7o8h.com", | ||||||
|  | 			repository: "xn--7o8h.com/myimage", | ||||||
|  | 			tag:        "xn--7o8h.com", | ||||||
|  | 			digest:     "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "foo_bar.com:8080", | ||||||
|  | 			repository: "foo_bar.com", | ||||||
|  | 			tag:        "8080", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:      "foo/foo_bar.com:8080", | ||||||
|  | 			hostname:   "foo", | ||||||
|  | 			repository: "foo/foo_bar.com", | ||||||
|  | 			tag:        "8080", | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| 	return matches[1] | 	for _, testcase := range referenceTestcases { | ||||||
| } | 		failf := func(format string, v ...interface{}) { | ||||||
|  | 			t.Logf(strconv.Quote(testcase.input)+": "+format, v...) | ||||||
|  | 			t.Fail() | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| func testRepository(prefix string) error { | 		repo, err := Parse(testcase.input) | ||||||
| 	for _, s := range []string{ | 		if testcase.err != nil { | ||||||
| 		prefix + `@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`, | 			if err == nil { | ||||||
| 		prefix + `:frozen@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`, | 				failf("missing expected error: %v", testcase.err) | ||||||
| 		prefix + `:latest`, | 			} else if testcase.err != err { | ||||||
| 		prefix, | 				failf("mismatched error: got %v, expected %v", err, testcase.err) | ||||||
| 	} { |  | ||||||
| 		expected := getRepo(s) |  | ||||||
| 		ref, err := Parse(s) |  | ||||||
| 		if err != nil { |  | ||||||
| 			if expected == "" { |  | ||||||
| 				continue |  | ||||||
| 			} | 			} | ||||||
| 			return err | 			continue | ||||||
|  | 		} else if err != nil { | ||||||
|  | 			failf("unexpected parse error: %v", err) | ||||||
|  | 			continue | ||||||
| 		} | 		} | ||||||
| 		if repo := ref.Repository(); repo.String() != expected { | 		if repo.String() != testcase.input { | ||||||
| 			return fmt.Errorf("repository string: expected %q, got: %q", expected, repo) | 			failf("mismatched repo: got %q, expected %q", repo.String(), testcase.input) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if named, ok := repo.(Named); ok { | ||||||
|  | 			if named.Name() != testcase.repository { | ||||||
|  | 				failf("unexpected repository: got %q, expected %q", named.Name(), testcase.repository) | ||||||
|  | 			} | ||||||
|  | 			hostname, _ := SplitHostname(named) | ||||||
|  | 			if hostname != testcase.hostname { | ||||||
|  | 				failf("unexpected hostname: got %q, expected %q", hostname, testcase.hostname) | ||||||
|  | 			} | ||||||
|  | 		} else if testcase.repository != "" || testcase.hostname != "" { | ||||||
|  | 			failf("expected named type, got %T", repo) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		tagged, ok := repo.(Tagged) | ||||||
|  | 		if testcase.tag != "" { | ||||||
|  | 			if ok { | ||||||
|  | 				if tagged.Tag() != testcase.tag { | ||||||
|  | 					failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag) | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				failf("expected tagged type, got %T", repo) | ||||||
|  | 			} | ||||||
|  | 		} else if ok { | ||||||
|  | 			failf("unexpected tagged type") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		digested, ok := repo.(Digested) | ||||||
|  | 		if testcase.digest != "" { | ||||||
|  | 			if ok { | ||||||
|  | 				if digested.Digest().String() != testcase.digest { | ||||||
|  | 					failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest) | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				failf("expected digested type, got %T", repo) | ||||||
|  | 			} | ||||||
|  | 		} else if ok { | ||||||
|  | 			failf("unexpected digested type") | ||||||
| 		} | 		} | ||||||
| 		if refStr := ref.String(); refStr != s { |  | ||||||
| 			return fmt.Errorf("reference string: expected %q, got: %q", s, refStr) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| func TestSimpleRepository(t *testing.T) { |  | ||||||
| 	if err := testRepository(`busybox`); err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestUrlRepository(t *testing.T) { | func TestSplitHostname(t *testing.T) { | ||||||
| 	if err := testRepository(`docker.io/library/busybox`); err != nil { | 	testcases := []struct { | ||||||
| 		t.Fatal(err) | 		input    string | ||||||
|  | 		hostname string | ||||||
|  | 		name     string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			input:    "test.com/foo", | ||||||
|  | 			hostname: "test.com", | ||||||
|  | 			name:     "foo", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:    "test_com/foo", | ||||||
|  | 			hostname: "", | ||||||
|  | 			name:     "test_com/foo", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:    "test:8080/foo", | ||||||
|  | 			hostname: "test:8080", | ||||||
|  | 			name:     "foo", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:    "test.com:8080/foo", | ||||||
|  | 			hostname: "test.com:8080", | ||||||
|  | 			name:     "foo", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:    "test-com:8080/foo", | ||||||
|  | 			hostname: "test-com:8080", | ||||||
|  | 			name:     "foo", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:    "xn--n3h.com:18080/foo", | ||||||
|  | 			hostname: "xn--n3h.com:18080", | ||||||
|  | 			name:     "foo", | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| } | 	for _, testcase := range testcases { | ||||||
|  | 		failf := func(format string, v ...interface{}) { | ||||||
|  | 			t.Logf(strconv.Quote(testcase.input)+": "+format, v...) | ||||||
|  | 			t.Fail() | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| func TestPort(t *testing.T) { | 		named, err := ParseNamed(testcase.input) | ||||||
| 	if err := testRepository(`busybox:1234`); err != nil { | 		if err != nil { | ||||||
| 		t.Fatal(err) | 			failf("error parsing name: %s", err) | ||||||
|  | 		} | ||||||
|  | 		hostname, name := SplitHostname(named) | ||||||
|  | 		if hostname != testcase.hostname { | ||||||
|  | 			failf("unexpected hostname: got %q, expected %q", hostname, testcase.hostname) | ||||||
|  | 		} | ||||||
|  | 		if name != testcase.name { | ||||||
|  | 			failf("unexpected name: got %q, expected %q", name, testcase.name) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| */ |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,37 @@ | ||||||
|  | package reference | ||||||
|  | 
 | ||||||
|  | import "regexp" | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	// nameComponentRegexp restricts registry path component names to
 | ||||||
|  | 	// start with at least one letter or number, with following parts able to
 | ||||||
|  | 	// be separated by one period, dash or underscore.
 | ||||||
|  | 	nameComponentRegexp = regexp.MustCompile(`[a-zA-Z0-9]+(?:[._-][a-z0-9]+)*`) | ||||||
|  | 
 | ||||||
|  | 	nameRegexp = regexp.MustCompile(`(?:` + nameComponentRegexp.String() + `/)*` + nameComponentRegexp.String()) | ||||||
|  | 
 | ||||||
|  | 	hostnameComponentRegexp = regexp.MustCompile(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`) | ||||||
|  | 
 | ||||||
|  | 	// hostnameComponentRegexp restricts the registry hostname component of a repository name to
 | ||||||
|  | 	// start with a component as defined by hostnameRegexp and followed by an optional port.
 | ||||||
|  | 	hostnameRegexp = regexp.MustCompile(`(?:` + hostnameComponentRegexp.String() + `\.)*` + hostnameComponentRegexp.String() + `(?::[0-9]+)?`) | ||||||
|  | 
 | ||||||
|  | 	// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
 | ||||||
|  | 	TagRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`) | ||||||
|  | 
 | ||||||
|  | 	// anchoredTagRegexp matches valid tag names, anchored at the start and
 | ||||||
|  | 	// end of the matched string.
 | ||||||
|  | 	anchoredTagRegexp = regexp.MustCompile(`^` + TagRegexp.String() + `$`) | ||||||
|  | 
 | ||||||
|  | 	// NameRegexp is the format for the name component of references. The
 | ||||||
|  | 	// regexp has capturing groups for the hostname and name part omitting
 | ||||||
|  | 	// the seperating forward slash from either.
 | ||||||
|  | 	NameRegexp = regexp.MustCompile(`(?:` + hostnameRegexp.String() + `/)?` + nameRegexp.String()) | ||||||
|  | 
 | ||||||
|  | 	// ReferenceRegexp is the full supported format of a reference. The
 | ||||||
|  | 	// regexp has capturing groups for name, tag, and digest components.
 | ||||||
|  | 	ReferenceRegexp = regexp.MustCompile(`^((?:` + hostnameRegexp.String() + `/)?` + nameRegexp.String() + `)(?:[:](` + TagRegexp.String() + `))?(?:[@]([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$`) | ||||||
|  | 
 | ||||||
|  | 	// anchoredNameRegexp is used to parse a name value, capturing hostname
 | ||||||
|  | 	anchoredNameRegexp = regexp.MustCompile(`^(?:(` + hostnameRegexp.String() + `)/)?(` + nameRegexp.String() + `)$`) | ||||||
|  | ) | ||||||
|  | @ -0,0 +1,398 @@ | ||||||
|  | package reference | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"regexp" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type regexpMatch struct { | ||||||
|  | 	input string | ||||||
|  | 	match bool | ||||||
|  | 	subs  []string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func checkRegexp(t *testing.T, r *regexp.Regexp, m regexpMatch) { | ||||||
|  | 	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", matches) | ||||||
|  | 		} | ||||||
|  | 		if len(matches) < (len(m.subs) + 1) { | ||||||
|  | 			t.Errorf("Expected %d sub matches, only have %d", len(m.subs), len(matches)-1) | ||||||
|  | 		} | ||||||
|  | 		for i := range m.subs { | ||||||
|  | 			if m.subs[i] != matches[i+1] { | ||||||
|  | 				t.Errorf("Unexpected submatch %d: %q, expected %q", i+1, matches[i+1], m.subs[i]) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} else if m.match { | ||||||
|  | 		t.Errorf("Expected match for %q", m.input) | ||||||
|  | 	} else if matches != nil { | ||||||
|  | 		t.Errorf("Unexpected match for %q", m.input) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestHostRegexp(t *testing.T) { | ||||||
|  | 	hostcases := []regexpMatch{ | ||||||
|  | 		{ | ||||||
|  | 			input: "test.com", | ||||||
|  | 			match: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "test.com:10304", | ||||||
|  | 			match: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "test.com:http", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "localhost", | ||||||
|  | 			match: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "localhost:8080", | ||||||
|  | 			match: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "a", | ||||||
|  | 			match: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "a.b", | ||||||
|  | 			match: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "ab.cd.com", | ||||||
|  | 			match: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "a-b.com", | ||||||
|  | 			match: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "-ab.com", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "ab-.com", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "ab.c-om", | ||||||
|  | 			match: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "ab.-com", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "ab.com-", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "0101.com", | ||||||
|  | 			match: true, // TODO(dmcgowan): valid if this should be allowed
 | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "001a.com", | ||||||
|  | 			match: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "b.gbc.io:443", | ||||||
|  | 			match: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "b.gbc.io", | ||||||
|  | 			match: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "xn--n3h.com", // ☃.com in punycode
 | ||||||
|  | 			match: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	r := regexp.MustCompile(`^` + hostnameRegexp.String() + `$`) | ||||||
|  | 	for i := range hostcases { | ||||||
|  | 		checkRegexp(t, r, hostcases[i]) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestFullNameRegexp(t *testing.T) { | ||||||
|  | 	testcases := []regexpMatch{ | ||||||
|  | 		{ | ||||||
|  | 			input: "", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "short", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"", "short"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "simple/name", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"simple", "name"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "library/ubuntu", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"library", "ubuntu"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "docker/stevvooe/app", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"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"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "aa/aa/bb/bb/bb", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"aa", "aa/bb/bb/bb"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "a/a/a/a", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"a", "a/a/a"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "a/a/a/a/", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "a//a/a", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "a", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"", "a"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "a/aa", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"a", "aa"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "a/aa/a", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"a", "aa/a"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo.com", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"", "foo.com"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo.com/", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo.com:8080/bar", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"foo.com:8080", "bar"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo.com:http/bar", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo.com/bar", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"foo.com", "bar"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo.com/bar/baz", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"foo.com", "bar/baz"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "localhost:8080/bar", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"localhost:8080", "bar"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "sub-dom1.foo.com/bar/baz/quux", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"sub-dom1.foo.com", "bar/baz/quux"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "blog.foo.com/bar/baz", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"blog.foo.com", "bar/baz"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "a^a", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "aa/asdf$$^/aa", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "asdf$$^/aa", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "aa-a/a", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"aa-a", "a"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: strings.Repeat("a/", 128) + "a", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"a", strings.Repeat("a/", 127) + "a"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "a-/a/a/a", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo.com/a-/a/a", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "-foo/bar", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo/bar-", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo-/bar", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo/-bar", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "_foo/bar", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo_bar", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"", "foo_bar"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo_bar.com", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"", "foo_bar.com"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo_bar.com:8080", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo_bar.com:8080/app", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "foo.com/foo_bar", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"foo.com", "foo_bar"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "____/____", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "_docker/_docker", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "docker_/docker_", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "b.gcr.io/test.example.com/my-app", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"b.gcr.io", "test.example.com/my-app"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "xn--n3h.com/myimage", // ☃.com in punycode
 | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"xn--n3h.com", "myimage"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "xn--7o8h.com/myimage", // 🐳.com in punycode
 | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"xn--7o8h.com", "myimage"}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for i := range testcases { | ||||||
|  | 		checkRegexp(t, anchoredNameRegexp, testcases[i]) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestReferenceRegexp(t *testing.T) { | ||||||
|  | 	testcases := []regexpMatch{ | ||||||
|  | 		{ | ||||||
|  | 			input: "registry.com:8080/myapp:tag", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"registry.com:8080/myapp", "tag", ""}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "registry.com:8080/myapp@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"registry.com:8080/myapp", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "registry.com:8080/myapp:tag2@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"registry.com:8080/myapp", "tag2", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "registry.com:8080/myapp@sha256:badbadbadbad", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "registry.com:8080/myapp:invalid~tag", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "bad_hostname.com:8080/myapp:tag", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:// localhost treated as name, missing tag with 8080 as tag
 | ||||||
|  | 			"localhost:8080@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"localhost", "8080", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "localhost:8080/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"localhost:8080/name", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "localhost:http/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// localhost will be treated as an image name without a host
 | ||||||
|  | 			input: "localhost@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", | ||||||
|  | 			match: true, | ||||||
|  | 			subs:  []string{"localhost", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "registry.com:8080/myapp@bad", | ||||||
|  | 			match: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "registry.com:8080/myapp@2bad", | ||||||
|  | 			match: false, // TODO(dmcgowan): Support this as valid
 | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for i := range testcases { | ||||||
|  | 		checkRegexp(t, ReferenceRegexp, testcases[i]) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -1,136 +0,0 @@ | ||||||
| package reference |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"regexp" |  | ||||||
| 	"strings" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	// RepositoryNameTotalLengthMax is the maximum total number of characters in a repository name.
 |  | ||||||
| 	RepositoryNameTotalLengthMax = 255 |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // RepositoryNameComponentRegexp restricts registry path component names to
 |  | ||||||
| // start with at least one letter or number, with following parts able to
 |  | ||||||
| // be separated by one period, dash or underscore.
 |  | ||||||
| var RepositoryNameComponentRegexp = regexp.MustCompile(`[a-zA-Z0-9]+(?:[._-][a-z0-9]+)*`) |  | ||||||
| 
 |  | ||||||
| // RepositoryNameComponentAnchoredRegexp is the version of
 |  | ||||||
| // RepositoryNameComponentRegexp which must completely match the content
 |  | ||||||
| var RepositoryNameComponentAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameComponentRegexp.String() + `$`) |  | ||||||
| 
 |  | ||||||
| // RepositoryNameHostnameRegexp restricts the registry hostname component of a repository name to
 |  | ||||||
| // start with a component as defined by RepositoryNameComponentRegexp and followed by an optional port.
 |  | ||||||
| var RepositoryNameHostnameRegexp = regexp.MustCompile(RepositoryNameComponentRegexp.String() + `(?::[0-9]+)?`) |  | ||||||
| 
 |  | ||||||
| // RepositoryNameHostnameAnchoredRegexp is the version of
 |  | ||||||
| // RepositoryNameHostnameRegexp which must completely match the content.
 |  | ||||||
| var RepositoryNameHostnameAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameHostnameRegexp.String() + `$`) |  | ||||||
| 
 |  | ||||||
| // RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow
 |  | ||||||
| // multiple path components, separated by a forward slash.
 |  | ||||||
| var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameHostnameRegexp.String() + `/)?(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String()) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	// ErrRepositoryNameEmpty is returned for empty, invalid repository names.
 |  | ||||||
| 	ErrRepositoryNameEmpty = errors.New("repository name must have at least one component") |  | ||||||
| 
 |  | ||||||
| 	// ErrRepositoryNameMissingHostname is returned when a repository name
 |  | ||||||
| 	// does not start with a hostname
 |  | ||||||
| 	ErrRepositoryNameMissingHostname = errors.New("repository name must start with a hostname") |  | ||||||
| 
 |  | ||||||
| 	// ErrRepositoryNameHostnameInvalid is returned when a repository name
 |  | ||||||
| 	// does not match RepositoryNameHostnameRegexp
 |  | ||||||
| 	ErrRepositoryNameHostnameInvalid = fmt.Errorf("repository name must match %q", RepositoryNameHostnameRegexp.String()) |  | ||||||
| 
 |  | ||||||
| 	// ErrRepositoryNameLong is returned when a repository name is longer than
 |  | ||||||
| 	// RepositoryNameTotalLengthMax
 |  | ||||||
| 	ErrRepositoryNameLong = fmt.Errorf("repository name must not be more than %v characters", RepositoryNameTotalLengthMax) |  | ||||||
| 
 |  | ||||||
| 	// ErrRepositoryNameComponentInvalid is returned when a repository name does
 |  | ||||||
| 	// not match RepositoryNameComponentRegexp
 |  | ||||||
| 	ErrRepositoryNameComponentInvalid = fmt.Errorf("repository name component must match %q", RepositoryNameComponentRegexp.String()) |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Repository represents a reference to a Repository.
 |  | ||||||
| type Repository struct { |  | ||||||
| 	// Hostname refers to the registry hostname where the repository resides.
 |  | ||||||
| 	Hostname string |  | ||||||
| 	// Name is a slash (`/`) separated list of string components.
 |  | ||||||
| 	Name string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // String returns the string representation of a repository.
 |  | ||||||
| func (r Repository) String() string { |  | ||||||
| 	// Hostname is not supposed to be empty, but let's be nice.
 |  | ||||||
| 	if len(r.Hostname) == 0 { |  | ||||||
| 		return r.Name |  | ||||||
| 	} |  | ||||||
| 	return r.Hostname + "/" + r.Name |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Validate ensures the repository name is valid for use in the
 |  | ||||||
| // registry. This function accepts a superset of what might be accepted by
 |  | ||||||
| // docker core or docker hub. If the name does not pass validation, an error,
 |  | ||||||
| // describing the conditions, is returned.
 |  | ||||||
| //
 |  | ||||||
| // Effectively, the name should comply with the following grammar:
 |  | ||||||
| //
 |  | ||||||
| //	repository			:= hostname ['/' component]+
 |  | ||||||
| //	hostname 			:= component [':' port-number]
 |  | ||||||
| //	component			:= alpha-numeric [separator alpha-numeric]*
 |  | ||||||
| // 	alpha-numeric			:= /[a-zA-Z0-9]+/
 |  | ||||||
| //	separator			:= /[._-]/
 |  | ||||||
| //	port-number			:= /[0-9]+/
 |  | ||||||
| //
 |  | ||||||
| // The result of the production should be limited to 255 characters.
 |  | ||||||
| func (r Repository) Validate() error { |  | ||||||
| 	n := len(r.String()) |  | ||||||
| 	switch { |  | ||||||
| 	case n == 0: |  | ||||||
| 		return ErrRepositoryNameEmpty |  | ||||||
| 	case n > RepositoryNameTotalLengthMax: |  | ||||||
| 		return ErrRepositoryNameLong |  | ||||||
| 	case len(r.Hostname) <= 0: |  | ||||||
| 		return ErrRepositoryNameMissingHostname |  | ||||||
| 	case !RepositoryNameHostnameAnchoredRegexp.MatchString(r.Hostname): |  | ||||||
| 		return ErrRepositoryNameHostnameInvalid |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	components := r.Name |  | ||||||
| 	for { |  | ||||||
| 		var component string |  | ||||||
| 		sep := strings.Index(components, "/") |  | ||||||
| 		if sep >= 0 { |  | ||||||
| 			component = components[:sep] |  | ||||||
| 			components = components[sep+1:] |  | ||||||
| 		} else { // if no more slashes
 |  | ||||||
| 			component = components |  | ||||||
| 			components = "" |  | ||||||
| 		} |  | ||||||
| 		if !RepositoryNameComponentAnchoredRegexp.MatchString(component) { |  | ||||||
| 			return ErrRepositoryNameComponentInvalid |  | ||||||
| 		} |  | ||||||
| 		if sep < 0 { |  | ||||||
| 			return nil |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // NewRepository returns a valid Repository from an input string representing
 |  | ||||||
| // the canonical form of a repository name.
 |  | ||||||
| // If the validation fails, an error is returned.
 |  | ||||||
| func NewRepository(canonicalName string) (repo Repository, err error) { |  | ||||||
| 	if len(canonicalName) == 0 { |  | ||||||
| 		return repo, ErrRepositoryNameEmpty |  | ||||||
| 	} |  | ||||||
| 	i := strings.Index(canonicalName, "/") |  | ||||||
| 	if i <= 0 { |  | ||||||
| 		return repo, ErrRepositoryNameMissingHostname |  | ||||||
| 	} |  | ||||||
| 	repo.Hostname = canonicalName[:i] |  | ||||||
| 	repo.Name = canonicalName[i+1:] |  | ||||||
| 	return repo, repo.Validate() |  | ||||||
| } |  | ||||||
|  | @ -1,286 +0,0 @@ | ||||||
| package reference |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"regexp" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	// regexpTestcases is a unified set of testcases for
 |  | ||||||
| 	// TestValidateRepositoryName and TestRepositoryNameRegexp.
 |  | ||||||
| 	// Some of them are valid inputs for one and not the other.
 |  | ||||||
| 	regexpTestcases = []struct { |  | ||||||
| 		// input is the repository name or name component testcase
 |  | ||||||
| 		input string |  | ||||||
| 		// err is the error expected from ValidateRepositoryName, or nil
 |  | ||||||
| 		err error |  | ||||||
| 		// invalid should be true if the testcase is *not* expected to
 |  | ||||||
| 		// match RepositoryNameRegexp
 |  | ||||||
| 		invalid bool |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			input:   "", |  | ||||||
| 			err:     ErrRepositoryNameEmpty, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "short", |  | ||||||
| 			err:   ErrRepositoryNameMissingHostname, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "simple/name", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "library/ubuntu", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "docker/stevvooe/app", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "aa/aa/bb/bb/bb", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "a/a/a/b/b", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "a/a/a/a/", |  | ||||||
| 			err:     ErrRepositoryNameComponentInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "a//a/a", |  | ||||||
| 			err:     ErrRepositoryNameComponentInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "a", |  | ||||||
| 			err:   ErrRepositoryNameMissingHostname, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "a/aa", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "aa/a", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "a/aa/a", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "foo.com/", |  | ||||||
| 			err:     ErrRepositoryNameComponentInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "foo.com:8080/bar", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "foo.com/bar", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "foo.com/bar/baz", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "foo.com/bar/baz/quux", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "blog.foo.com/bar/baz", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "asdf", |  | ||||||
| 			err:   ErrRepositoryNameMissingHostname, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "aa/asdf$$^/aa", |  | ||||||
| 			err:     ErrRepositoryNameComponentInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "asdf$$^/aa", |  | ||||||
| 			err:     ErrRepositoryNameHostnameInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "aa-a/aa", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "aa/aa", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "a-a/a-a", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "a", |  | ||||||
| 			err:   ErrRepositoryNameMissingHostname, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "a/image", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "a-/a/a/a", |  | ||||||
| 			err:     ErrRepositoryNameHostnameInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "a/a-/a/a/a", |  | ||||||
| 			err:     ErrRepositoryNameComponentInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			// total length = 255
 |  | ||||||
| 			input: "a/" + strings.Repeat("a", 253), |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			// total length = 256
 |  | ||||||
| 			input: "b/" + strings.Repeat("a", 254), |  | ||||||
| 			err:   ErrRepositoryNameLong, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "-foo/bar", |  | ||||||
| 			err:     ErrRepositoryNameHostnameInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "foo/bar-", |  | ||||||
| 			err:     ErrRepositoryNameComponentInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "foo-/bar", |  | ||||||
| 			err:     ErrRepositoryNameHostnameInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "foo/-bar", |  | ||||||
| 			err:     ErrRepositoryNameComponentInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "_foo/bar", |  | ||||||
| 			err:     ErrRepositoryNameHostnameInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "foo/bar_", |  | ||||||
| 			err:     ErrRepositoryNameComponentInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "____/____", |  | ||||||
| 			err:     ErrRepositoryNameHostnameInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "_docker/_docker", |  | ||||||
| 			err:     ErrRepositoryNameHostnameInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "docker_/docker_", |  | ||||||
| 			err:     ErrRepositoryNameHostnameInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "do__cker/docker", |  | ||||||
| 			err:     ErrRepositoryNameComponentInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "docker./docker", |  | ||||||
| 			err:     ErrRepositoryNameComponentInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   ".docker/docker", |  | ||||||
| 			err:     ErrRepositoryNameComponentInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "do..cker/docker", |  | ||||||
| 			err:     ErrRepositoryNameComponentInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "docker-/docker", |  | ||||||
| 			err:     ErrRepositoryNameComponentInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "-docker/docker", |  | ||||||
| 			err:   ErrRepositoryNameComponentInvalid, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "xn--n3h.com/myimage", // http://☃.com in punycode
 |  | ||||||
| 			err:     ErrRepositoryNameHostnameInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input:   "xn--7o8h.com/myimage", // http://🐳.com in punycode
 |  | ||||||
| 			err:     ErrRepositoryNameHostnameInvalid, |  | ||||||
| 			invalid: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "b.gcr.io/test.example.com/my-app", // embedded domain component
 |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "xn--n3h.com/myimage", // http://☃.com in punycode
 |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "xn--7o8h.com/myimage", // http://🐳.com in punycode
 |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "registry.io/foo/project--id.module--name.ver---sion--name", // image with hostname
 |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // TestValidateRepositoryName tests the ValidateRepositoryName function,
 |  | ||||||
| // which uses RepositoryNameComponentAnchoredRegexp for validation
 |  | ||||||
| func TestValidateRepositoryName(t *testing.T) { |  | ||||||
| 	for _, testcase := range regexpTestcases { |  | ||||||
| 		failf := func(format string, v ...interface{}) { |  | ||||||
| 			t.Logf(strconv.Quote(testcase.input)+": "+format, v...) |  | ||||||
| 			t.Fail() |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if _, err := NewRepository(testcase.input); err != testcase.err { |  | ||||||
| 			if testcase.err != nil { |  | ||||||
| 				if err != nil { |  | ||||||
| 					failf("unexpected error for invalid repository: got %v, expected %v", err, testcase.err) |  | ||||||
| 				} else { |  | ||||||
| 					failf("expected invalid repository: %v", testcase.err) |  | ||||||
| 				} |  | ||||||
| 			} else { |  | ||||||
| 				if err != nil { |  | ||||||
| 					// Wrong error returned.
 |  | ||||||
| 					failf("unexpected error validating repository name: %v, expected %v", err, testcase.err) |  | ||||||
| 				} else { |  | ||||||
| 					failf("unexpected error validating repository name: %v", err) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestRepositoryNameRegexp(t *testing.T) { |  | ||||||
| 	AnchoredRepositoryNameRegexp := regexp.MustCompile(`^` + RepositoryNameRegexp.String() + `$`) |  | ||||||
| 	for _, testcase := range regexpTestcases { |  | ||||||
| 		failf := func(format string, v ...interface{}) { |  | ||||||
| 			t.Logf(strconv.Quote(testcase.input)+": "+format, v...) |  | ||||||
| 			t.Fail() |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		matches := AnchoredRepositoryNameRegexp.MatchString(testcase.input) |  | ||||||
| 		if matches == testcase.invalid { |  | ||||||
| 			if testcase.invalid { |  | ||||||
| 				failf("expected invalid repository name %s", testcase.input) |  | ||||||
| 			} else { |  | ||||||
| 				failf("expected valid repository name %s", testcase.input) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -1,38 +0,0 @@ | ||||||
| package reference |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"regexp" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
 |  | ||||||
| 	TagRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`) |  | ||||||
| 
 |  | ||||||
| 	// TagAnchoredRegexp matches valid tag names, anchored at the start and
 |  | ||||||
| 	// end of the matched string.
 |  | ||||||
| 	TagAnchoredRegexp = regexp.MustCompile(`^` + TagRegexp.String() + `$`) |  | ||||||
| 
 |  | ||||||
| 	// ErrTagInvalid is returned when a tag does not match TagAnchoredRegexp.
 |  | ||||||
| 	ErrTagInvalid = fmt.Errorf("tag name must match %q", TagRegexp.String()) |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Tag represents an image's tag name.
 |  | ||||||
| type Tag string |  | ||||||
| 
 |  | ||||||
| // NewTag returns a valid Tag from an input string s.
 |  | ||||||
| // If the validation fails, an error is returned.
 |  | ||||||
| func NewTag(s string) (Tag, error) { |  | ||||||
| 	tag := Tag(s) |  | ||||||
| 	return tag, tag.Validate() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Validate returns ErrTagInvalid if tag does not match TagAnchoredRegexp.
 |  | ||||||
| //
 |  | ||||||
| //	tag	:= [\w][\w.-]{0,127}
 |  | ||||||
| func (tag Tag) Validate() error { |  | ||||||
| 	if !TagAnchoredRegexp.MatchString(string(tag)) { |  | ||||||
| 		return ErrTagInvalid |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  | @ -13,7 +13,7 @@ var ( | ||||||
| 	nameParameterDescriptor = ParameterDescriptor{ | 	nameParameterDescriptor = ParameterDescriptor{ | ||||||
| 		Name:        "name", | 		Name:        "name", | ||||||
| 		Type:        "string", | 		Type:        "string", | ||||||
| 		Format:      reference.RepositoryNameRegexp.String(), | 		Format:      reference.NameRegexp.String(), | ||||||
| 		Required:    true, | 		Required:    true, | ||||||
| 		Description: `Name of the target repository.`, | 		Description: `Name of the target repository.`, | ||||||
| 	} | 	} | ||||||
|  | @ -390,7 +390,7 @@ var routeDescriptors = []RouteDescriptor{ | ||||||
| 	}, | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		Name:        RouteNameTags, | 		Name:        RouteNameTags, | ||||||
| 		Path:        "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/tags/list", | 		Path:        "/v2/{name:" + reference.NameRegexp.String() + "}/tags/list", | ||||||
| 		Entity:      "Tags", | 		Entity:      "Tags", | ||||||
| 		Description: "Retrieve information about tags.", | 		Description: "Retrieve information about tags.", | ||||||
| 		Methods: []MethodDescriptor{ | 		Methods: []MethodDescriptor{ | ||||||
|  | @ -518,7 +518,7 @@ var routeDescriptors = []RouteDescriptor{ | ||||||
| 	}, | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		Name:        RouteNameManifest, | 		Name:        RouteNameManifest, | ||||||
| 		Path:        "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/manifests/{reference:" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + "}", | 		Path:        "/v2/{name:" + reference.NameRegexp.String() + "}/manifests/{reference:" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + "}", | ||||||
| 		Entity:      "Manifest", | 		Entity:      "Manifest", | ||||||
| 		Description: "Create, update, delete and retrieve manifests.", | 		Description: "Create, update, delete and retrieve manifests.", | ||||||
| 		Methods: []MethodDescriptor{ | 		Methods: []MethodDescriptor{ | ||||||
|  | @ -783,7 +783,7 @@ var routeDescriptors = []RouteDescriptor{ | ||||||
| 
 | 
 | ||||||
| 	{ | 	{ | ||||||
| 		Name:        RouteNameBlob, | 		Name:        RouteNameBlob, | ||||||
| 		Path:        "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}", | 		Path:        "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}", | ||||||
| 		Entity:      "Blob", | 		Entity:      "Blob", | ||||||
| 		Description: "Operations on blobs identified by `name` and `digest`. Used to fetch or delete layers by digest.", | 		Description: "Operations on blobs identified by `name` and `digest`. Used to fetch or delete layers by digest.", | ||||||
| 		Methods: []MethodDescriptor{ | 		Methods: []MethodDescriptor{ | ||||||
|  | @ -1007,7 +1007,7 @@ var routeDescriptors = []RouteDescriptor{ | ||||||
| 
 | 
 | ||||||
| 	{ | 	{ | ||||||
| 		Name:        RouteNameBlobUpload, | 		Name:        RouteNameBlobUpload, | ||||||
| 		Path:        "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/blobs/uploads/", | 		Path:        "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/uploads/", | ||||||
| 		Entity:      "Initiate Blob Upload", | 		Entity:      "Initiate Blob Upload", | ||||||
| 		Description: "Initiate a blob upload. This endpoint can be used to create resumable uploads or monolithic uploads.", | 		Description: "Initiate a blob upload. This endpoint can be used to create resumable uploads or monolithic uploads.", | ||||||
| 		Methods: []MethodDescriptor{ | 		Methods: []MethodDescriptor{ | ||||||
|  | @ -1129,7 +1129,7 @@ var routeDescriptors = []RouteDescriptor{ | ||||||
| 
 | 
 | ||||||
| 	{ | 	{ | ||||||
| 		Name:        RouteNameBlobUploadChunk, | 		Name:        RouteNameBlobUploadChunk, | ||||||
| 		Path:        "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid:[a-zA-Z0-9-_.=]+}", | 		Path:        "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/uploads/{uuid:[a-zA-Z0-9-_.=]+}", | ||||||
| 		Entity:      "Blob Upload", | 		Entity:      "Blob Upload", | ||||||
| 		Description: "Interact with blob uploads. Clients should never assemble URLs for this endpoint and should only take it through the `Location` header on related API requests. The `Location` header and its parameters should be preserved by clients, using the latest value returned via upload related API calls.", | 		Description: "Interact with blob uploads. Clients should never assemble URLs for this endpoint and should only take it through the `Location` header on related API requests. The `Location` header and its parameters should be preserved by clients, using the latest value returned via upload related API calls.", | ||||||
| 		Methods: []MethodDescriptor{ | 		Methods: []MethodDescriptor{ | ||||||
|  |  | ||||||
|  | @ -170,6 +170,14 @@ func TestRouter(t *testing.T) { | ||||||
| 				"name": "foo/bar/manifests", | 				"name": "foo/bar/manifests", | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			RouteName:  RouteNameManifest, | ||||||
|  | 			RequestURI: "/v2/locahost:8080/foo/bar/baz/manifests/tag", | ||||||
|  | 			Vars: map[string]string{ | ||||||
|  | 				"name":      "locahost:8080/foo/bar/baz", | ||||||
|  | 				"reference": "tag", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	checkTestRouter(t, testCases, "", true) | 	checkTestRouter(t, testCases, "", true) | ||||||
|  |  | ||||||
|  | @ -97,9 +97,9 @@ func (r *registry) Repositories(ctx context.Context, entries []string, last stri | ||||||
| 	return numFilled, returnErr | 	return numFilled, returnErr | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewRepository creates a new Repository for the given canonical repository name and base URL.
 | // NewRepository creates a new Repository for the given repository name and base URL.
 | ||||||
| func NewRepository(ctx context.Context, canonicalName, baseURL string, transport http.RoundTripper) (distribution.Repository, error) { | func NewRepository(ctx context.Context, name, baseURL string, transport http.RoundTripper) (distribution.Repository, error) { | ||||||
| 	if _, err := reference.NewRepository(canonicalName); err != nil { | 	if _, err := reference.ParseNamed(name); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -116,7 +116,7 @@ func NewRepository(ctx context.Context, canonicalName, baseURL string, transport | ||||||
| 	return &repository{ | 	return &repository{ | ||||||
| 		client:  client, | 		client:  client, | ||||||
| 		ub:      ub, | 		ub:      ub, | ||||||
| 		name:    canonicalName, | 		name:    name, | ||||||
| 		context: ctx, | 		context: ctx, | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -25,8 +25,8 @@ func NewInMemoryBlobDescriptorCacheProvider() cache.BlobDescriptorCacheProvider | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(canonicalName string) (distribution.BlobDescriptorService, error) { | func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) { | ||||||
| 	if _, err := reference.NewRepository(canonicalName); err != nil { | 	if _, err := reference.ParseNamed(repo); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -34,9 +34,9 @@ func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(canonicalNam | ||||||
| 	defer imbdcp.mu.RUnlock() | 	defer imbdcp.mu.RUnlock() | ||||||
| 
 | 
 | ||||||
| 	return &repositoryScopedInMemoryBlobDescriptorCache{ | 	return &repositoryScopedInMemoryBlobDescriptorCache{ | ||||||
| 		repo:       canonicalName, | 		repo:       repo, | ||||||
| 		parent:     imbdcp, | 		parent:     imbdcp, | ||||||
| 		repository: imbdcp.repositories[canonicalName], | 		repository: imbdcp.repositories[repo], | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -40,13 +40,13 @@ func NewRedisBlobDescriptorCacheProvider(pool *redis.Pool) cache.BlobDescriptorC | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RepositoryScoped returns the scoped cache.
 | // RepositoryScoped returns the scoped cache.
 | ||||||
| func (rbds *redisBlobDescriptorService) RepositoryScoped(canonicalName string) (distribution.BlobDescriptorService, error) { | func (rbds *redisBlobDescriptorService) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) { | ||||||
| 	if _, err := reference.NewRepository(canonicalName); err != nil { | 	if _, err := reference.ParseNamed(repo); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return &repositoryScopedRedisBlobDescriptorService{ | 	return &repositoryScopedRedisBlobDescriptorService{ | ||||||
| 		repo:     canonicalName, | 		repo:     repo, | ||||||
| 		upstream: rbds, | 		upstream: rbds, | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -108,7 +108,7 @@ func (reg *registry) Scope() distribution.Scope { | ||||||
| // Instances should not be shared between goroutines but are cheap to
 | // Instances should not be shared between goroutines but are cheap to
 | ||||||
| // allocate. In general, they should be request scoped.
 | // allocate. In general, they should be request scoped.
 | ||||||
| func (reg *registry) Repository(ctx context.Context, canonicalName string) (distribution.Repository, error) { | func (reg *registry) Repository(ctx context.Context, canonicalName string) (distribution.Repository, error) { | ||||||
| 	if _, err := reference.NewRepository(canonicalName); err != nil { | 	if _, err := reference.ParseNamed(canonicalName); err != nil { | ||||||
| 		return nil, distribution.ErrRepositoryNameInvalid{ | 		return nil, distribution.ErrRepositoryNameInvalid{ | ||||||
| 			Name:   canonicalName, | 			Name:   canonicalName, | ||||||
| 			Reason: err, | 			Reason: err, | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue