Merge pull request #3789 from thaJeztah/reference_consts
reference: clean up regular expressionsmaster
						commit
						ede90db01c
					
				|  | @ -32,7 +32,7 @@ func FamiliarString(ref Reference) string { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // FamiliarMatch reports whether ref matches the specified pattern.
 | // FamiliarMatch reports whether ref matches the specified pattern.
 | ||||||
| // See https://godoc.org/path#Match for supported patterns.
 | // See [path.Match] for supported patterns.
 | ||||||
| func FamiliarMatch(pattern string, ref Reference) (bool, error) { | func FamiliarMatch(pattern string, ref Reference) (bool, error) { | ||||||
| 	matched, err := path.Match(pattern, FamiliarString(ref)) | 	matched, err := path.Match(pattern, FamiliarString(ref)) | ||||||
| 	if namedRef, isNamed := ref.(Named); isNamed && !matched { | 	if namedRef, isNamed := ref.(Named); isNamed && !matched { | ||||||
|  |  | ||||||
|  | @ -8,9 +8,34 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
|  | 	// legacyDefaultDomain is the legacy domain for Docker Hub (which was
 | ||||||
|  | 	// originally named "the Docker Index"). This domain is still used for
 | ||||||
|  | 	// authentication and image search, which were part of the "v1" Docker
 | ||||||
|  | 	// registry specification.
 | ||||||
|  | 	//
 | ||||||
|  | 	// This domain will continue to be supported, but there are plans to consolidate
 | ||||||
|  | 	// legacy domains to new "canonical" domains. Once those domains are decided
 | ||||||
|  | 	// on, we must update the normalization functions, but preserve compatibility
 | ||||||
|  | 	// with existing installs, clients, and user configuration.
 | ||||||
| 	legacyDefaultDomain = "index.docker.io" | 	legacyDefaultDomain = "index.docker.io" | ||||||
|  | 
 | ||||||
|  | 	// defaultDomain is the default domain used for images on Docker Hub.
 | ||||||
|  | 	// It is used to normalize "familiar" names to canonical names, for example,
 | ||||||
|  | 	// to convert "ubuntu" to "docker.io/library/ubuntu:latest".
 | ||||||
|  | 	//
 | ||||||
|  | 	// Note that actual domain of Docker Hub's registry is registry-1.docker.io.
 | ||||||
|  | 	// This domain will continue to be supported, but there are plans to consolidate
 | ||||||
|  | 	// legacy domains to new "canonical" domains. Once those domains are decided
 | ||||||
|  | 	// on, we must update the normalization functions, but preserve compatibility
 | ||||||
|  | 	// with existing installs, clients, and user configuration.
 | ||||||
| 	defaultDomain = "docker.io" | 	defaultDomain = "docker.io" | ||||||
|  | 
 | ||||||
|  | 	// officialRepoPrefix is the namespace used for official images on Docker Hub.
 | ||||||
|  | 	// It is used to normalize "familiar" names to canonical names, for example,
 | ||||||
|  | 	// to convert "ubuntu" to "docker.io/library/ubuntu:latest".
 | ||||||
| 	officialRepoPrefix = "library/" | 	officialRepoPrefix = "library/" | ||||||
|  | 
 | ||||||
|  | 	// defaultTag is the default tag if no tag is provided.
 | ||||||
| 	defaultTag = "latest" | 	defaultTag = "latest" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -33,14 +58,14 @@ func ParseNormalizedNamed(s string) (Named, error) { | ||||||
| 		return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s) | 		return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s) | ||||||
| 	} | 	} | ||||||
| 	domain, remainder := splitDockerDomain(s) | 	domain, remainder := splitDockerDomain(s) | ||||||
| 	var remoteName string | 	var remote string | ||||||
| 	if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 { | 	if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 { | ||||||
| 		remoteName = remainder[:tagSep] | 		remote = remainder[:tagSep] | ||||||
| 	} else { | 	} else { | ||||||
| 		remoteName = remainder | 		remote = remainder | ||||||
| 	} | 	} | ||||||
| 	if strings.ToLower(remoteName) != remoteName { | 	if strings.ToLower(remote) != remote { | ||||||
| 		return nil, fmt.Errorf("invalid reference format: repository name (%s) must be lowercase", remoteName) | 		return nil, fmt.Errorf("invalid reference format: repository name (%s) must be lowercase", remote) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	ref, err := Parse(domain + "/" + remainder) | 	ref, err := Parse(domain + "/" + remainder) | ||||||
|  | @ -54,41 +79,53 @@ func ParseNormalizedNamed(s string) (Named, error) { | ||||||
| 	return named, nil | 	return named, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ParseDockerRef normalizes the image reference following the docker convention. This is added
 | // namedTaggedDigested is a reference that has both a tag and a digest.
 | ||||||
| // mainly for backward compatibility.
 | type namedTaggedDigested interface { | ||||||
| // The reference returned can only be either tagged or digested. For reference contains both tag
 | 	NamedTagged | ||||||
| // and digest, the function returns digested reference, e.g. docker.io/library/busybox:latest@
 | 	Digested | ||||||
| // sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa will be returned as
 | } | ||||||
| // docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa.
 | 
 | ||||||
|  | // ParseDockerRef normalizes the image reference following the docker convention,
 | ||||||
|  | // which allows for references to contain both a tag and a digest. It returns a
 | ||||||
|  | // reference that is either tagged or digested. For references containing both
 | ||||||
|  | // a tag and a digest, it returns a digested reference. For example, the following
 | ||||||
|  | // reference:
 | ||||||
|  | //
 | ||||||
|  | //	docker.io/library/busybox:latest@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa
 | ||||||
|  | //
 | ||||||
|  | // Is returned as a digested reference (with the ":latest" tag removed):
 | ||||||
|  | //
 | ||||||
|  | //	docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa
 | ||||||
|  | //
 | ||||||
|  | // References that are already "tagged" or "digested" are returned unmodified:
 | ||||||
|  | //
 | ||||||
|  | //	// Already a digested reference
 | ||||||
|  | //	docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa
 | ||||||
|  | //
 | ||||||
|  | //	// Already a named reference
 | ||||||
|  | //	docker.io/library/busybox:latest
 | ||||||
| func ParseDockerRef(ref string) (Named, error) { | func ParseDockerRef(ref string) (Named, error) { | ||||||
| 	named, err := ParseNormalizedNamed(ref) | 	named, err := ParseNormalizedNamed(ref) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	if _, ok := named.(NamedTagged); ok { | 	if canonical, ok := named.(namedTaggedDigested); ok { | ||||||
| 		if canonical, ok := named.(Canonical); ok { | 		// The reference is both tagged and digested; only return digested.
 | ||||||
| 			// The reference is both tagged and digested, only
 |  | ||||||
| 			// return digested.
 |  | ||||||
| 		newNamed, err := WithName(canonical.Name()) | 		newNamed, err := WithName(canonical.Name()) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 			newCanonical, err := WithDigest(newNamed, canonical.Digest()) | 		return WithDigest(newNamed, canonical.Digest()) | ||||||
| 			if err != nil { |  | ||||||
| 				return nil, err |  | ||||||
| 			} |  | ||||||
| 			return newCanonical, nil |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 	return TagNameOnly(named), nil | 	return TagNameOnly(named), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // splitDockerDomain splits a repository name to domain and remotename string.
 | // splitDockerDomain splits a repository name to domain and remote-name.
 | ||||||
| // If no valid domain is found, the default domain is used. Repository name
 | // If no valid domain is found, the default domain is used. Repository name
 | ||||||
| // needs to be already validated before.
 | // needs to be already validated before.
 | ||||||
| func splitDockerDomain(name string) (domain, remainder string) { | func splitDockerDomain(name string) (domain, remainder string) { | ||||||
| 	i := strings.IndexRune(name, '/') | 	i := strings.IndexRune(name, '/') | ||||||
| 	if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost" && strings.ToLower(name[:i]) == name[:i]) { | 	if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != localhost && strings.ToLower(name[:i]) == name[:i]) { | ||||||
| 		domain, remainder = defaultDomain, name | 		domain, remainder = defaultDomain, name | ||||||
| 	} else { | 	} else { | ||||||
| 		domain, remainder = name[:i], name[i+1:] | 		domain, remainder = name[:i], name[i+1:] | ||||||
|  | @ -96,11 +133,6 @@ func splitDockerDomain(name string) (domain, remainder string) { | ||||||
| 	if domain == legacyDefaultDomain { | 	if domain == legacyDefaultDomain { | ||||||
| 		domain = defaultDomain | 		domain = defaultDomain | ||||||
| 	} | 	} | ||||||
| 	// TODO(thaJeztah): this check may be too strict, as it assumes the
 |  | ||||||
| 	//  "library/" namespace does not have nested namespaces. While this
 |  | ||||||
| 	//  is true (currently), technically it would be possible for Docker
 |  | ||||||
| 	//  Hub to use those (e.g. "library/distros/ubuntu:latest").
 |  | ||||||
| 	//  See https://github.com/distribution/distribution/pull/3769#issuecomment-1302031785.
 |  | ||||||
| 	if domain == defaultDomain && !strings.ContainsRune(remainder, '/') { | 	if domain == defaultDomain && !strings.ContainsRune(remainder, '/') { | ||||||
| 		remainder = officialRepoPrefix + remainder | 		remainder = officialRepoPrefix + remainder | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -4,13 +4,14 @@ | ||||||
| // Grammar
 | // Grammar
 | ||||||
| //
 | //
 | ||||||
| //	reference                       := name [ ":" tag ] [ "@" digest ]
 | //	reference                       := name [ ":" tag ] [ "@" digest ]
 | ||||||
| //	name                            := [domain '/'] path-component ['/' path-component]*
 | //	name                            := [domain '/'] remote-name
 | ||||||
| //	domain                          := host [':' port-number]
 | //	domain                          := host [':' port-number]
 | ||||||
| //	host                            := domain-name | IPv4address | \[ IPv6address \]	; rfc3986 appendix-A
 | //	host                            := domain-name | IPv4address | \[ IPv6address \]	; rfc3986 appendix-A
 | ||||||
| //	domain-name                     := domain-component ['.' domain-component]*
 | //	domain-name                     := domain-component ['.' domain-component]*
 | ||||||
| //	domain-component                := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
 | //	domain-component                := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
 | ||||||
| //	port-number                     := /[0-9]+/
 | //	port-number                     := /[0-9]+/
 | ||||||
| //	path-component                  := alpha-numeric [separator alpha-numeric]*
 | //	path-component                  := alpha-numeric [separator alpha-numeric]*
 | ||||||
|  | //	path (or "remote-name")         := path-component ['/' path-component]*
 | ||||||
| //	alpha-numeric                   := /[a-z0-9]+/
 | //	alpha-numeric                   := /[a-z0-9]+/
 | ||||||
| //	separator                       := /[_.]|__|[-]*/
 | //	separator                       := /[_.]|__|[-]*/
 | ||||||
| //
 | //
 | ||||||
|  | @ -23,7 +24,6 @@ | ||||||
| //	digest-hex                      := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
 | //	digest-hex                      := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
 | ||||||
| //
 | //
 | ||||||
| //	identifier                      := /[a-f0-9]{64}/
 | //	identifier                      := /[a-f0-9]{64}/
 | ||||||
| //	short-identifier                := /[a-f0-9]{6,64}/
 |  | ||||||
| package reference | package reference | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | @ -147,7 +147,7 @@ type namedRepository interface { | ||||||
| 	Path() string | 	Path() string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Domain returns the domain part of the Named reference
 | // Domain returns the domain part of the [Named] reference.
 | ||||||
| func Domain(named Named) string { | func Domain(named Named) string { | ||||||
| 	if r, ok := named.(namedRepository); ok { | 	if r, ok := named.(namedRepository); ok { | ||||||
| 		return r.Domain() | 		return r.Domain() | ||||||
|  | @ -156,7 +156,7 @@ func Domain(named Named) string { | ||||||
| 	return domain | 	return domain | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Path returns the name without the domain part of the Named reference
 | // Path returns the name without the domain part of the [Named] reference.
 | ||||||
| func Path(named Named) (name string) { | func Path(named Named) (name string) { | ||||||
| 	if r, ok := named.(namedRepository); ok { | 	if r, ok := named.(namedRepository); ok { | ||||||
| 		return r.Path() | 		return r.Path() | ||||||
|  | @ -188,7 +188,6 @@ func SplitHostname(named Named) (string, string) { | ||||||
| 
 | 
 | ||||||
| // 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) { | ||||||
| 	matches := ReferenceRegexp.FindStringSubmatch(s) | 	matches := ReferenceRegexp.FindStringSubmatch(s) | ||||||
| 	if matches == nil { | 	if matches == nil { | ||||||
|  | @ -240,7 +239,6 @@ func Parse(s string) (Reference, error) { | ||||||
| // the Named interface. The reference must have a name and be in the canonical
 | // the Named interface. The reference must have a name and be in the canonical
 | ||||||
| // form, otherwise an error is returned.
 | // form, otherwise an error is returned.
 | ||||||
| // 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: ParseNamed will not handle short digests.
 |  | ||||||
| func ParseNamed(s string) (Named, error) { | func ParseNamed(s string) (Named, error) { | ||||||
| 	named, err := ParseNormalizedNamed(s) | 	named, err := ParseNormalizedNamed(s) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  |  | ||||||
|  | @ -1,14 +1,50 @@ | ||||||
| package reference | package reference | ||||||
| 
 | 
 | ||||||
| import "regexp" | import ( | ||||||
|  | 	"regexp" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| var ( | // DigestRegexp matches well-formed digests, including algorithm (e.g. "sha256:<encoded>").
 | ||||||
| 	// alphaNumeric defines the alpha numeric atom, typically a
 | var DigestRegexp = regexp.MustCompile(digestPat) | ||||||
|  | 
 | ||||||
|  | // DomainRegexp matches hostname or IP-addresses, optionally including a port
 | ||||||
|  | // number. It defines the structure of potential domain components 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. It may be a subset of
 | ||||||
|  | // DNS domain name, an IPv4 address in decimal format, or an IPv6 address between
 | ||||||
|  | // square brackets (excluding zone identifiers as defined by [RFC 6874] or special
 | ||||||
|  | // addresses such as IPv4-Mapped).
 | ||||||
|  | //
 | ||||||
|  | // [RFC 6874]: https://www.rfc-editor.org/rfc/rfc6874.
 | ||||||
|  | var DomainRegexp = regexp.MustCompile(domainAndPort) | ||||||
|  | 
 | ||||||
|  | // IdentifierRegexp is the format for string identifier used as a
 | ||||||
|  | // content addressable identifier using sha256. These identifiers
 | ||||||
|  | // are like digests without the algorithm, since sha256 is used.
 | ||||||
|  | var IdentifierRegexp = regexp.MustCompile(identifier) | ||||||
|  | 
 | ||||||
|  | // NameRegexp is the format for the name component of references, including
 | ||||||
|  | // an optional domain and port, but without tag or digest suffix.
 | ||||||
|  | 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.
 | ||||||
|  | var ReferenceRegexp = regexp.MustCompile(referencePat) | ||||||
|  | 
 | ||||||
|  | // TagRegexp matches valid tag names. From [docker/docker:graph/tags.go].
 | ||||||
|  | //
 | ||||||
|  | // [docker/docker:graph/tags.go]: https://github.com/moby/moby/blob/v1.6.0/graph/tags.go#L26-L28
 | ||||||
|  | var TagRegexp = regexp.MustCompile(tag) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// alphanumeric defines the alphanumeric atom, typically a
 | ||||||
| 	// component of names. This only allows lower case characters and digits.
 | 	// component of names. This only allows lower case characters and digits.
 | ||||||
| 	alphaNumeric = `[a-z0-9]+` | 	alphanumeric = `[a-z0-9]+` | ||||||
| 
 | 
 | ||||||
| 	// separator defines the separators allowed to be embedded in name
 | 	// separator defines the separators allowed to be embedded in name
 | ||||||
| 	// components. This allow one period, one or two underscore and multiple
 | 	// components. This allows one period, one or two underscore and multiple
 | ||||||
| 	// dashes. Repeated dashes and underscores are intentionally treated
 | 	// dashes. Repeated dashes and underscores are intentionally treated
 | ||||||
| 	// differently. In order to support valid hostnames as name components,
 | 	// differently. In order to support valid hostnames as name components,
 | ||||||
| 	// supporting repeated dash was added. Additionally double underscore is
 | 	// supporting repeated dash was added. Additionally double underscore is
 | ||||||
|  | @ -16,33 +52,51 @@ var ( | ||||||
| 	// supported names.
 | 	// supported names.
 | ||||||
| 	separator = `(?:[._]|__|[-]*)` | 	separator = `(?:[._]|__|[-]*)` | ||||||
| 
 | 
 | ||||||
| 	// nameComponent restricts registry path component names to start
 | 	// localhost is treated as a special value for domain-name. Any other
 | ||||||
| 	// with at least one letter or number, with following parts able to be
 | 	// domain-name without a "." or a ":port" are considered a path component.
 | ||||||
| 	// separated by one period, one or two underscore and multiple dashes.
 | 	localhost = `localhost` | ||||||
| 	nameComponent = expression( |  | ||||||
| 		alphaNumeric, |  | ||||||
| 		optional(repeated(separator, alphaNumeric))) |  | ||||||
| 
 | 
 | ||||||
| 	// domainNameComponent restricts the registry domain component of a
 | 	// domainNameComponent restricts the registry domain component of a
 | ||||||
| 	// repository name to start with a component as defined by DomainRegexp.
 | 	// 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])` | 	domainNameComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])` | ||||||
| 
 | 
 | ||||||
|  | 	// optionalPort matches an optional port-number including the port separator
 | ||||||
|  | 	// (e.g. ":80").
 | ||||||
|  | 	optionalPort = `(?::[0-9]+)?` | ||||||
|  | 
 | ||||||
|  | 	// tag matches valid tag names. From docker/docker:graph/tags.go.
 | ||||||
|  | 	tag = `[\w][\w.-]{0,127}` | ||||||
|  | 
 | ||||||
|  | 	// digestPat matches well-formed digests, including algorithm (e.g. "sha256:<encoded>").
 | ||||||
|  | 	//
 | ||||||
|  | 	// TODO(thaJeztah): this should follow the same rules as https://pkg.go.dev/github.com/opencontainers/go-digest@v1.0.0#DigestRegexp
 | ||||||
|  | 	// so that go-digest defines the canonical format. Note that the go-digest is
 | ||||||
|  | 	// more relaxed:
 | ||||||
|  | 	//   - it allows multiple algorithms (e.g. "sha256+b64:<encoded>") to allow
 | ||||||
|  | 	//     future expansion of supported algorithms.
 | ||||||
|  | 	//   - it allows the "<encoded>" value to use urlsafe base64 encoding as defined
 | ||||||
|  | 	//     in [rfc4648, section 5].
 | ||||||
|  | 	//
 | ||||||
|  | 	// [rfc4648, section 5]: https://www.rfc-editor.org/rfc/rfc4648#section-5.
 | ||||||
|  | 	digestPat = `[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}` | ||||||
|  | 
 | ||||||
|  | 	// identifier is the format for a content addressable identifier using sha256.
 | ||||||
|  | 	// These identifiers are like digests without the algorithm, since sha256 is used.
 | ||||||
|  | 	identifier = `([a-f0-9]{64})` | ||||||
|  | 
 | ||||||
| 	// ipv6address are enclosed between square brackets and may be represented
 | 	// ipv6address are enclosed between square brackets and may be represented
 | ||||||
| 	// in many ways, see rfc5952. Only IPv6 in compressed or uncompressed format
 | 	// in many ways, see rfc5952. Only IPv6 in compressed or uncompressed format
 | ||||||
| 	// are allowed, IPv6 zone identifiers (rfc6874) or Special addresses such as
 | 	// are allowed, IPv6 zone identifiers (rfc6874) or Special addresses such as
 | ||||||
| 	// IPv4-Mapped are deliberately excluded.
 | 	// IPv4-Mapped are deliberately excluded.
 | ||||||
| 	ipv6address = expression( | 	ipv6address = `\[(?:[a-fA-F0-9:]+)\]` | ||||||
| 		literal(`[`), `(?:[a-fA-F0-9:]+)`, literal(`]`), |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | var ( | ||||||
| 	// domainName defines the structure of potential domain components
 | 	// domainName defines the structure of potential domain components
 | ||||||
| 	// that may be part of image names. This is purposely a subset of what is
 | 	// 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
 | 	// allowed by DNS to ensure backwards compatibility with Docker image
 | ||||||
| 	// names. This includes IPv4 addresses on decimal format.
 | 	// names. This includes IPv4 addresses on decimal format.
 | ||||||
| 	domainName = expression( | 	domainName = domainNameComponent + anyTimes(`\.`+domainNameComponent) | ||||||
| 		domainNameComponent, |  | ||||||
| 		optional(repeated(literal(`.`), domainNameComponent)), |  | ||||||
| 	) |  | ||||||
| 
 | 
 | ||||||
| 	// host defines the structure of potential domains based on the URI
 | 	// host defines the structure of potential domains based on the URI
 | ||||||
| 	// Host subcomponent on rfc3986. It may be a subset of DNS domain name,
 | 	// Host subcomponent on rfc3986. It may be a subset of DNS domain name,
 | ||||||
|  | @ -53,117 +107,57 @@ var ( | ||||||
| 
 | 
 | ||||||
| 	// allowed by the URI Host subcomponent on rfc3986 to ensure backwards
 | 	// allowed by the URI Host subcomponent on rfc3986 to ensure backwards
 | ||||||
| 	// compatibility with Docker image names.
 | 	// compatibility with Docker image names.
 | ||||||
| 	domain = expression( | 	domainAndPort = host + optionalPort | ||||||
| 		host, |  | ||||||
| 		optional(literal(`:`), `[0-9]+`)) |  | ||||||
| 
 | 
 | ||||||
| 	// DomainRegexp defines the structure of potential domain components
 |  | ||||||
| 	// 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.
 |  | ||||||
| 	DomainRegexp = regexp.MustCompile(domain) |  | ||||||
| 
 |  | ||||||
| 	tag = `[\w][\w.-]{0,127}` |  | ||||||
| 	// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
 |  | ||||||
| 	TagRegexp = regexp.MustCompile(tag) |  | ||||||
| 
 |  | ||||||
| 	anchoredTag = anchored(tag) |  | ||||||
| 	// anchoredTagRegexp matches valid tag names, anchored at the start and
 | 	// anchoredTagRegexp matches valid tag names, anchored at the start and
 | ||||||
| 	// end of the matched string.
 | 	// end of the matched string.
 | ||||||
| 	anchoredTagRegexp = regexp.MustCompile(anchoredTag) | 	anchoredTagRegexp = regexp.MustCompile(anchored(tag)) | ||||||
| 
 | 
 | ||||||
| 	digestPat = `[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}` |  | ||||||
| 	// DigestRegexp matches valid digests.
 |  | ||||||
| 	DigestRegexp = regexp.MustCompile(digestPat) |  | ||||||
| 
 |  | ||||||
| 	anchoredDigest = anchored(digestPat) |  | ||||||
| 	// anchoredDigestRegexp matches valid digests, anchored at the start and
 | 	// anchoredDigestRegexp matches valid digests, anchored at the start and
 | ||||||
| 	// end of the matched string.
 | 	// end of the matched string.
 | ||||||
| 	anchoredDigestRegexp = regexp.MustCompile(anchoredDigest) | 	anchoredDigestRegexp = regexp.MustCompile(anchored(digestPat)) | ||||||
| 
 | 
 | ||||||
| 	namePat = expression( | 	// pathComponent restricts path-components to start with an alphanumeric
 | ||||||
| 		optional(domain, literal(`/`)), | 	// character, with following parts able to be separated by a separator
 | ||||||
| 		nameComponent, | 	// (one period, one or two underscore and multiple dashes).
 | ||||||
| 		optional(repeated(literal(`/`), nameComponent))) | 	pathComponent = alphanumeric + anyTimes(separator+alphanumeric) | ||||||
| 	// NameRegexp is the format for the name component of references. The
 | 
 | ||||||
| 	// regexp has capturing groups for the domain and name part omitting
 | 	// remoteName matches the remote-name of a repository. It consists of one
 | ||||||
| 	// the separating forward slash from either.
 | 	// or more forward slash (/) delimited path-components:
 | ||||||
| 	NameRegexp = regexp.MustCompile(namePat) | 	//
 | ||||||
|  | 	//	pathComponent[[/pathComponent] ...] // e.g., "library/ubuntu"
 | ||||||
|  | 	remoteName = pathComponent + anyTimes(`/`+pathComponent) | ||||||
|  | 	namePat    = optional(domainAndPort+`/`) + remoteName | ||||||
| 
 | 
 | ||||||
| 	anchoredName = anchored( |  | ||||||
| 		optional(capture(domain), literal(`/`)), |  | ||||||
| 		capture(nameComponent, |  | ||||||
| 			optional(repeated(literal(`/`), nameComponent)))) |  | ||||||
| 	// anchoredNameRegexp is used to parse a name value, capturing the
 | 	// anchoredNameRegexp is used to parse a name value, capturing the
 | ||||||
| 	// domain and trailing components.
 | 	// domain and trailing components.
 | ||||||
| 	anchoredNameRegexp = regexp.MustCompile(anchoredName) | 	anchoredNameRegexp = regexp.MustCompile(anchored(optional(capture(domainAndPort), `/`), capture(remoteName))) | ||||||
| 
 | 
 | ||||||
| 	referencePat = anchored(capture(namePat), | 	referencePat = anchored(capture(namePat), optional(`:`, capture(tag)), optional(`@`, capture(digestPat))) | ||||||
| 		optional(literal(":"), capture(tag)), |  | ||||||
| 		optional(literal("@"), capture(digestPat))) |  | ||||||
| 	// ReferenceRegexp is the full supported format of a reference. The regexp
 |  | ||||||
| 	// is anchored and has capturing groups for name, tag, and digest
 |  | ||||||
| 	// components.
 |  | ||||||
| 	ReferenceRegexp = regexp.MustCompile(referencePat) |  | ||||||
| 
 | 
 | ||||||
| 	identifier = `([a-f0-9]{64})` |  | ||||||
| 	// IdentifierRegexp is the format for string identifier used as a
 |  | ||||||
| 	// content addressable identifier using sha256. These identifiers
 |  | ||||||
| 	// are like digests without the algorithm, since sha256 is used.
 |  | ||||||
| 	IdentifierRegexp = regexp.MustCompile(identifier) |  | ||||||
| 
 |  | ||||||
| 	anchoredIdentifier = anchored(identifier) |  | ||||||
| 	// anchoredIdentifierRegexp is used to check or match an
 | 	// anchoredIdentifierRegexp is used to check or match an
 | ||||||
| 	// identifier value, anchored at start and end of string.
 | 	// identifier value, anchored at start and end of string.
 | ||||||
| 	anchoredIdentifierRegexp = regexp.MustCompile(anchoredIdentifier) | 	anchoredIdentifierRegexp = regexp.MustCompile(anchored(identifier)) | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // literal compiles s into a literal regular expression, escaping any regexp
 |  | ||||||
| // reserved characters.
 |  | ||||||
| func literal(s string) string { |  | ||||||
| 	re := regexp.MustCompile(regexp.QuoteMeta(s)) |  | ||||||
| 
 |  | ||||||
| 	if _, complete := re.LiteralPrefix(); !complete { |  | ||||||
| 		panic("must be a literal") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return re.String() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // expression defines a full expression, where each regular expression must
 |  | ||||||
| // follow the previous.
 |  | ||||||
| func expression(res ...string) string { |  | ||||||
| 	var s string |  | ||||||
| 	for _, re := range res { |  | ||||||
| 		s += re |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return s |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // optional wraps the expression in a non-capturing group and makes the
 | // optional wraps the expression in a non-capturing group and makes the
 | ||||||
| // production optional.
 | // production optional.
 | ||||||
| func optional(res ...string) string { | func optional(res ...string) string { | ||||||
| 	return group(expression(res...)) + `?` | 	return `(?:` + strings.Join(res, "") + `)?` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // repeated wraps the regexp in a non-capturing group to get one or more
 | // anyTimes wraps the expression in a non-capturing group that can occur
 | ||||||
| // matches.
 | // any number of times.
 | ||||||
| func repeated(res ...string) string { | func anyTimes(res ...string) string { | ||||||
| 	return group(expression(res...)) + `+` | 	return `(?:` + strings.Join(res, "") + `)*` | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // group wraps the regexp in a non-capturing group.
 |  | ||||||
| func group(res ...string) string { |  | ||||||
| 	return `(?:` + expression(res...) + `)` |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // capture wraps the expression in a capturing group.
 | // capture wraps the expression in a capturing group.
 | ||||||
| func capture(res ...string) string { | func capture(res ...string) string { | ||||||
| 	return `(` + expression(res...) + `)` | 	return `(` + strings.Join(res, "") + `)` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // anchored anchors the regular expression by adding start and end delimiters.
 | // anchored anchors the regular expression by adding start and end delimiters.
 | ||||||
| func anchored(res ...string) string { | func anchored(res ...string) string { | ||||||
| 	return `^` + expression(res...) + `$` | 	return `^` + strings.Join(res, "") + `$` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -20,13 +20,15 @@ import ( | ||||||
| 	"sort" | 	"sort" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Sort sorts string references preferring higher information references
 | // Sort sorts string references preferring higher information references.
 | ||||||
|  | //
 | ||||||
| // The precedence is as follows:
 | // The precedence is as follows:
 | ||||||
| // 1. Name + Tag + Digest
 | //
 | ||||||
| // 2. Name + Tag
 | //  1. [Named] + [Tagged] + [Digested] (e.g., "docker.io/library/busybox:latest@sha256:<digest>")
 | ||||||
| // 3. Name + Digest
 | //  2. [Named] + [Tagged]              (e.g., "docker.io/library/busybox:latest")
 | ||||||
| // 4. Name
 | //  3. [Named] + [Digested]            (e.g., "docker.io/library/busybo@sha256:<digest>")
 | ||||||
| // 5. Digest
 | //  4. [Named]                         (e.g., "docker.io/library/busybox")
 | ||||||
|  | //  5. [Digested]                      (e.g., "docker.io@sha256:<digest>")
 | ||||||
| //  6. Parse error
 | //  6. Parse error
 | ||||||
| func Sort(references []string) []string { | func Sort(references []string) []string { | ||||||
| 	var prefs []Reference | 	var prefs []Reference | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue