| // Package reference provides a general type to represent any way of referencing images within the registry. | |
| // Its main purpose is to abstract tags and digests (content-addressable hash). | |
| // | |
| // Grammar | |
| // | |
| // reference := name [ ":" tag ] [ "@" digest ] | |
| // name := [domain '/'] remote-name | |
| // domain := host [':' port-number] | |
| // host := domain-name | IPv4address | \[ IPv6address \] ; rfc3986 appendix-A | |
| // domain-name := domain-component ['.' domain-component]* | |
| // domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ | |
| // port-number := /[0-9]+/ | |
| // path-component := alpha-numeric [separator alpha-numeric]* | |
| // path (or "remote-name") := path-component ['/' path-component]* | |
| // alpha-numeric := /[a-z0-9]+/ | |
| // separator := /[_.]|__|[-]*/ | |
| // | |
| // tag := /[\w][\w.-]{0,127}/ | |
| // | |
| // digest := digest-algorithm ":" digest-hex | |
| // digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]* | |
| // digest-algorithm-separator := /[+.-_]/ | |
| // digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/ | |
| // digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value | |
| // | |
| // identifier := /[a-f0-9]{64}/ | |
| package reference | |
| import ( | |
| "errors" | |
| "fmt" | |
| "strings" | |
| "github.com/opencontainers/go-digest" | |
| ) | |
| const ( | |
| // RepositoryNameTotalLengthMax is the maximum total number of characters in a repository name. | |
| RepositoryNameTotalLengthMax = 255 | |
| // NameTotalLengthMax is the maximum total number of characters in a repository name. | |
| // | |
| // Deprecated: use [RepositoryNameTotalLengthMax] instead. | |
| NameTotalLengthMax = RepositoryNameTotalLengthMax | |
| ) | |
| var ( | |
| // ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference. | |
| ErrReferenceInvalidFormat = errors.New("invalid reference format") | |
| // ErrTagInvalidFormat represents an error while trying to parse a string as a tag. | |
| ErrTagInvalidFormat = errors.New("invalid tag format") | |
| // ErrDigestInvalidFormat represents an error while trying to parse a string as a tag. | |
| ErrDigestInvalidFormat = errors.New("invalid digest format") | |
| // ErrNameContainsUppercase is returned for invalid repository names that contain uppercase characters. | |
| ErrNameContainsUppercase = errors.New("repository name must be lowercase") | |
| // 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", RepositoryNameTotalLengthMax) | |
| // ErrNameNotCanonical is returned when a name is not canonical. | |
| ErrNameNotCanonical = errors.New("repository name must be canonical") | |
| ) | |
| // Reference is an opaque object reference identifier that may include | |
| // modifiers such as a hostname, name, tag, and digest. | |
| type Reference interface { | |
| // String returns the full reference | |
| String() string | |
| } | |
| // Field provides a wrapper type for resolving correct reference types when | |
| // working with encoding. | |
| type Field struct { | |
| reference Reference | |
| } | |
| // AsField wraps a reference in a Field for encoding. | |
| func AsField(reference Reference) Field { | |
| return Field{reference} | |
| } | |
| // Reference unwraps the reference type from the field to | |
| // return the Reference object. This object should be | |
| // of the appropriate type to further check for different | |
| // reference types. | |
| func (f Field) Reference() Reference { | |
| return f.reference | |
| } | |
| // MarshalText serializes the field to byte text which | |
| // is the string of the reference. | |
| func (f Field) MarshalText() (p []byte, err error) { | |
| return []byte(f.reference.String()), nil | |
| } | |
| // UnmarshalText parses text bytes by invoking the | |
| // reference parser to ensure the appropriately | |
| // typed reference object is wrapped by field. | |
| func (f *Field) UnmarshalText(p []byte) error { | |
| r, err := Parse(string(p)) | |
| if err != nil { | |
| return err | |
| } | |
| f.reference = r | |
| return nil | |
| } | |
| // Named is an object with a full name | |
| type Named interface { | |
| Reference | |
| Name() string | |
| } | |
| // Tagged is an object which has a tag | |
| type Tagged interface { | |
| Reference | |
| Tag() string | |
| } | |
| // NamedTagged is an object including a name and tag. | |
| type NamedTagged interface { | |
| Named | |
| Tag() string | |
| } | |
| // Digested is an object which has a digest | |
| // in which it can be referenced by | |
| type Digested interface { | |
| Reference | |
| Digest() digest.Digest | |
| } | |
| // Canonical reference is an object with a fully unique | |
| // name including a name with domain and digest | |
| type Canonical interface { | |
| Named | |
| Digest() digest.Digest | |
| } | |
| // namedRepository is a reference to a repository with a name. | |
| // A namedRepository has both domain and path components. | |
| type namedRepository interface { | |
| Named | |
| Domain() string | |
| Path() string | |
| } | |
| // Domain returns the domain part of the [Named] reference. | |
| func Domain(named Named) string { | |
| if r, ok := named.(namedRepository); ok { | |
| return r.Domain() | |
| } | |
| domain, _ := splitDomain(named.Name()) | |
| return domain | |
| } | |
| // Path returns the name without the domain part of the [Named] reference. | |
| func Path(named Named) (name string) { | |
| if r, ok := named.(namedRepository); ok { | |
| return r.Path() | |
| } | |
| _, path := splitDomain(named.Name()) | |
| return path | |
| } | |
| // splitDomain splits a named reference into a hostname and path string. | |
| // If no valid hostname is found, the hostname is empty and the full value | |
| // is returned as name | |
| func splitDomain(name string) (string, string) { | |
| match := anchoredNameRegexp.FindStringSubmatch(name) | |
| if len(match) != 3 { | |
| return "", name | |
| } | |
| return match[1], match[2] | |
| } | |
| // Parse parses s and returns a syntactically valid Reference. | |
| // If an error was encountered it is returned, along with a nil Reference. | |
| func Parse(s string) (Reference, error) { | |
| matches := ReferenceRegexp.FindStringSubmatch(s) | |
| if matches == nil { | |
| if s == "" { | |
| return nil, ErrNameEmpty | |
| } | |
| if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil { | |
| return nil, ErrNameContainsUppercase | |
| } | |
| return nil, ErrReferenceInvalidFormat | |
| } | |
| var repo repository | |
| nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1]) | |
| if len(nameMatch) == 3 { | |
| repo.domain = nameMatch[1] | |
| repo.path = nameMatch[2] | |
| } else { | |
| repo.domain = "" | |
| repo.path = matches[1] | |
| } | |
| if len(repo.path) > RepositoryNameTotalLengthMax { | |
| return nil, ErrNameTooLong | |
| } | |
| ref := reference{ | |
| namedRepository: repo, | |
| tag: matches[2], | |
| } | |
| if matches[3] != "" { | |
| var err error | |
| ref.digest, err = digest.Parse(matches[3]) | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| r := getBestReferenceType(ref) | |
| if r == nil { | |
| return nil, ErrNameEmpty | |
| } | |
| return r, nil | |
| } | |
| // ParseNamed parses s and returns a syntactically valid reference implementing | |
| // the Named interface. The reference must have a name and be in the canonical | |
| // form, otherwise an error is returned. | |
| // If an error was encountered it is returned, along with a nil Reference. | |
| func ParseNamed(s string) (Named, error) { | |
| named, err := ParseNormalizedNamed(s) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if named.String() != s { | |
| return nil, ErrNameNotCanonical | |
| } | |
| return named, nil | |
| } | |
| // WithName returns a named object representing the given string. If the input | |
| // is invalid ErrReferenceInvalidFormat will be returned. | |
| func WithName(name string) (Named, error) { | |
| match := anchoredNameRegexp.FindStringSubmatch(name) | |
| if match == nil || len(match) != 3 { | |
| return nil, ErrReferenceInvalidFormat | |
| } | |
| if len(match[2]) > RepositoryNameTotalLengthMax { | |
| return nil, ErrNameTooLong | |
| } | |
| return repository{ | |
| domain: match[1], | |
| path: match[2], | |
| }, nil | |
| } | |
| // WithTag combines the name from "name" and the tag from "tag" to form a | |
| // reference incorporating both the name and the tag. | |
| func WithTag(name Named, tag string) (NamedTagged, error) { | |
| if !anchoredTagRegexp.MatchString(tag) { | |
| return nil, ErrTagInvalidFormat | |
| } | |
| var repo repository | |
| if r, ok := name.(namedRepository); ok { | |
| repo.domain = r.Domain() | |
| repo.path = r.Path() | |
| } else { | |
| repo.path = name.Name() | |
| } | |
| if canonical, ok := name.(Canonical); ok { | |
| return reference{ | |
| namedRepository: repo, | |
| tag: tag, | |
| digest: canonical.Digest(), | |
| }, nil | |
| } | |
| return taggedReference{ | |
| namedRepository: repo, | |
| tag: tag, | |
| }, nil | |
| } | |
| // WithDigest combines the name from "name" and the digest from "digest" to form | |
| // a reference incorporating both the name and the digest. | |
| func WithDigest(name Named, digest digest.Digest) (Canonical, error) { | |
| if !anchoredDigestRegexp.MatchString(digest.String()) { | |
| return nil, ErrDigestInvalidFormat | |
| } | |
| var repo repository | |
| if r, ok := name.(namedRepository); ok { | |
| repo.domain = r.Domain() | |
| repo.path = r.Path() | |
| } else { | |
| repo.path = name.Name() | |
| } | |
| if tagged, ok := name.(Tagged); ok { | |
| return reference{ | |
| namedRepository: repo, | |
| tag: tagged.Tag(), | |
| digest: digest, | |
| }, nil | |
| } | |
| return canonicalReference{ | |
| namedRepository: repo, | |
| digest: digest, | |
| }, nil | |
| } | |
| // TrimNamed removes any tag or digest from the named reference. | |
| func TrimNamed(ref Named) Named { | |
| repo := repository{} | |
| if r, ok := ref.(namedRepository); ok { | |
| repo.domain, repo.path = r.Domain(), r.Path() | |
| } else { | |
| repo.domain, repo.path = splitDomain(ref.Name()) | |
| } | |
| return repo | |
| } | |
| 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{ | |
| namedRepository: ref.namedRepository, | |
| digest: ref.digest, | |
| } | |
| } | |
| return ref.namedRepository | |
| } | |
| if ref.digest == "" { | |
| return taggedReference{ | |
| namedRepository: ref.namedRepository, | |
| tag: ref.tag, | |
| } | |
| } | |
| return ref | |
| } | |
| type reference struct { | |
| namedRepository | |
| tag string | |
| digest digest.Digest | |
| } | |
| func (r reference) String() string { | |
| return r.Name() + ":" + r.tag + "@" + r.digest.String() | |
| } | |
| func (r reference) Tag() string { | |
| return r.tag | |
| } | |
| func (r reference) Digest() digest.Digest { | |
| return r.digest | |
| } | |
| type repository struct { | |
| domain string | |
| path string | |
| } | |
| func (r repository) String() string { | |
| return r.Name() | |
| } | |
| func (r repository) Name() string { | |
| if r.domain == "" { | |
| return r.path | |
| } | |
| return r.domain + "/" + r.path | |
| } | |
| func (r repository) Domain() string { | |
| return r.domain | |
| } | |
| func (r repository) Path() string { | |
| return r.path | |
| } | |
| type digestReference digest.Digest | |
| func (d digestReference) String() string { | |
| return digest.Digest(d).String() | |
| } | |
| func (d digestReference) Digest() digest.Digest { | |
| return digest.Digest(d) | |
| } | |
| type taggedReference struct { | |
| namedRepository | |
| tag string | |
| } | |
| func (t taggedReference) String() string { | |
| return t.Name() + ":" + t.tag | |
| } | |
| func (t taggedReference) Tag() string { | |
| return t.tag | |
| } | |
| type canonicalReference struct { | |
| namedRepository | |
| digest digest.Digest | |
| } | |
| func (c canonicalReference) String() string { | |
| return c.Name() + "@" + c.digest.String() | |
| } | |
| func (c canonicalReference) Digest() digest.Digest { | |
| return c.digest | |
| } |