package store import ( "fmt" "regexp" "strings" ) // Reference names an image the docker/OCI way: [registry/]repository[:tag]. // The tag defaults to "latest", exactly as docker resolves a bare name. type Reference struct { Repository string Tag string } const defaultTag = "latest" var ( // componentRe is one path component of a repository (docker's // grammar, with the separator runs simplified). componentRe = regexp.MustCompile(`^[a-z0-9]+(?:[._-]+[a-z0-9]+)*$`) // registryRe is a host name with an optional port, allowed as the // first component only. registryRe = regexp.MustCompile(`^[a-z0-9]+(?:[.-][a-z0-9]+)*(?::[0-9]+)?$`) tagRe = regexp.MustCompile(`^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$`) ) // ParseReference validates and normalizes a reference string. func ParseReference(s string) (Reference, error) { if strings.Contains(s, "@") { return Reference{}, fmt.Errorf("invalid image reference %q: digests are not supported, use a tag", s) } repo, tag := s, defaultTag // A colon after the last slash separates the tag; one before it is a // registry port. if i := strings.LastIndex(s, ":"); i > strings.LastIndex(s, "/") { repo, tag = s[:i], s[i+1:] } if repo == "" { return Reference{}, fmt.Errorf("invalid image reference %q: empty repository", s) } components := strings.Split(repo, "/") for i, c := range components { if componentRe.MatchString(c) { continue } if i == 0 && len(components) > 1 && registryRe.MatchString(c) { continue } return Reference{}, fmt.Errorf("invalid image reference %q: repository component %q (allowed: lowercase letters, digits, '.', '_', '-' between them)", s, c) } if !tagRe.MatchString(tag) { return Reference{}, fmt.Errorf("invalid image reference %q: tag %q (allowed: letters, digits, '_', '.', '-')", s, tag) } // ".tmp" is the staging suffix of Import — a tag ending in it would // collide with interrupted imports. if strings.HasSuffix(tag, ".tmp") { return Reference{}, fmt.Errorf("invalid image reference %q: the .tmp tag suffix is reserved for staging", s) } return Reference{Repository: repo, Tag: tag}, nil } // String is the full form, repository:tag. func (r Reference) String() string { return r.Repository + ":" + r.Tag } // Short omits the default tag, the way a user typically writes it. func (r Reference) Short() string { if r.Tag == defaultTag { return r.Repository } return r.String() } // legacyName reports whether the reference could have been an image of // the flat pre-tag store layout (a bare name, no registry or namespace). func (r Reference) legacyName() bool { return r.Tag == defaultTag && !strings.Contains(r.Repository, "/") }