Files
mhoennigandClaude Fable 5.1 877579d79a import and load: docker's split
werkdock import ARCHIVE [REPOSITORY[:TAG]] creates an image from a
plain rootfs archive (docker import semantics; the store method is
renamed Import). load is docker load's slot for saved docker/OCI
image archives, not implemented yet: it refuses without --name and
keeps accepting a rootfs archive with --name for callers that predate
import (Werkator), with a note. Import recognizes an unpacked docker
save or OCI layout and refuses it, so the two do not blur.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 22:37:55 +02:00

79 lines
2.6 KiB
Go

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, "/")
}