123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- package blob
- import (
- "cmp"
- "fmt"
- "slices"
- "strings"
- )
- // Levels of concreteness
- const (
- domain = iota
- namespace
- name
- tag
- build
- )
- // Ref is an opaque reference to a blob.
- //
- // It is comparable and can be used as a map key.
- //
- // Users or Ref must check Valid before using it.
- type Ref struct {
- domain string
- namespace string
- name string
- tag string
- build string
- }
- // WithDomain returns a copy of r with the provided domain. If the provided
- // domain is empty, it returns the short, unqualified copy of r.
- func (r Ref) WithDomain(s string) Ref {
- return with(r, domain, s)
- }
- // WithNamespace returns a copy of r with the provided namespace. If the
- // provided namespace is empty, it returns the short, unqualified copy of r.
- func (r Ref) WithNamespace(s string) Ref {
- return with(r, namespace, s)
- }
- func (r Ref) WithTag(s string) Ref {
- return with(r, tag, s)
- }
- // WithBuild returns a copy of r with the provided build. If the provided
- // build is empty, it returns the short, unqualified copy of r.
- func (r Ref) WithBuild(s string) Ref {
- return with(r, build, s)
- }
- func with(r Ref, part int, value string) Ref {
- if value != "" && !isValidPart(value) {
- return Ref{}
- }
- switch part {
- case domain:
- r.domain = value
- case namespace:
- r.namespace = value
- case name:
- r.name = value
- case tag:
- r.tag = value
- case build:
- r.build = value
- default:
- panic(fmt.Sprintf("invalid completeness: %d", part))
- }
- return r
- }
- // Format returns a string representation of the ref with the given
- // concreteness. If a part is missing, it is replaced with a loud
- // placeholder.
- func (r Ref) Full() string {
- r.domain = cmp.Or(r.domain, "!(MISSING DOMAIN)")
- r.namespace = cmp.Or(r.namespace, "!(MISSING NAMESPACE)")
- r.name = cmp.Or(r.name, "!(MISSING NAME)")
- r.tag = cmp.Or(r.tag, "!(MISSING TAG)")
- r.build = cmp.Or(r.build, "!(MISSING BUILD)")
- return r.String()
- }
- func (r Ref) NameAndTag() string {
- r.domain = ""
- r.namespace = ""
- r.build = ""
- return r.String()
- }
- func (r Ref) NameTagAndBuild() string {
- r.domain = ""
- r.namespace = ""
- return r.String()
- }
- // String returns the fully qualified ref string.
- func (r Ref) String() string {
- var b strings.Builder
- if r.domain != "" {
- b.WriteString(r.domain)
- b.WriteString("/")
- }
- if r.namespace != "" {
- b.WriteString(r.namespace)
- b.WriteString("/")
- }
- b.WriteString(r.name)
- if r.tag != "" {
- b.WriteString(":")
- b.WriteString(r.tag)
- }
- if r.build != "" {
- b.WriteString("+")
- b.WriteString(r.build)
- }
- return b.String()
- }
- // Complete returns true if the ref is valid and has no empty parts.
- func (r Ref) Complete() bool {
- return r.Valid() && !slices.Contains(r.Parts(), "")
- }
- func (r Ref) CompleteWithoutBuild() bool {
- return r.Valid() && !slices.Contains(r.Parts()[:tag], "")
- }
- // Less returns true if r is less concrete than o; false otherwise.
- func (r Ref) Less(o Ref) bool {
- rp := r.Parts()
- op := o.Parts()
- for i := range rp {
- if rp[i] < op[i] {
- return true
- }
- }
- return false
- }
- // Parts returns the parts of the ref in order of concreteness.
- //
- // The length of the returned slice is always 5.
- func (r Ref) Parts() []string {
- return []string{
- domain: r.domain,
- namespace: r.namespace,
- name: r.name,
- tag: r.tag,
- build: r.build,
- }
- }
- func (r Ref) Domain() string { return r.namespace }
- func (r Ref) Namespace() string { return r.namespace }
- func (r Ref) Name() string { return r.name }
- func (r Ref) Tag() string { return r.tag }
- func (r Ref) Build() string { return r.build }
- // ParseRef parses a ref string into a Ref. A ref string is a name, an
- // optional tag, and an optional build, separated by colons and pluses.
- //
- // The name must be valid ascii [a-zA-Z0-9_].
- // The tag must be valid ascii [a-zA-Z0-9_].
- // The build must be valid ascii [a-zA-Z0-9_].
- //
- // It returns then zero value if the ref is invalid.
- //
- // // Valid Examples:
- // ParseRef("mistral:latest") returns ("mistral", "latest", "")
- // ParseRef("mistral") returns ("mistral", "", "")
- // ParseRef("mistral:30B") returns ("mistral", "30B", "")
- // ParseRef("mistral:7b") returns ("mistral", "7b", "")
- // ParseRef("mistral:7b+Q4_0") returns ("mistral", "7b", "Q4_0")
- // ParseRef("mistral+KQED") returns ("mistral", "latest", "KQED")
- // ParseRef(".x.:7b+Q4_0:latest") returns (".x.", "7b", "Q4_0")
- // ParseRef("-grok-f.oo:7b+Q4_0") returns ("-grok-f.oo", "7b", "Q4_0")
- //
- // // Invalid Examples:
- // ParseRef("m stral") returns ("", "", "") // zero
- // ParseRef("... 129 chars ...") returns ("", "", "") // zero
- func ParseRef(s string) Ref {
- if len(s) > 128 {
- return Ref{}
- }
- if strings.HasPrefix(s, "http://") {
- s = s[len("http://"):]
- }
- if strings.HasPrefix(s, "https://") {
- s = s[len("https://"):]
- }
- var r Ref
- state, j := build, len(s)
- for i := len(s) - 1; i >= 0; i-- {
- c := s[i]
- switch c {
- case '+':
- switch state {
- case build:
- r.build = s[i+1 : j]
- if r.build == "" {
- return Ref{}
- }
- r.build = strings.ToUpper(r.build)
- state, j = tag, i
- default:
- return Ref{}
- }
- case ':':
- switch state {
- case build, tag:
- r.tag = s[i+1 : j]
- if r.tag == "" {
- return Ref{}
- }
- state, j = name, i
- default:
- return Ref{}
- }
- case '/':
- switch state {
- case name, tag, build:
- r.name = s[i+1 : j]
- state, j = namespace, i
- case namespace:
- r.namespace = s[i+1 : j]
- state, j = domain, i
- default:
- return Ref{}
- }
- }
- }
- // handle the first part based on final state
- switch state {
- case domain:
- r.domain = s[:j]
- case namespace:
- r.namespace = s[:j]
- default:
- r.name = s[:j]
- }
- if !r.Valid() {
- return Ref{}
- }
- return r
- }
- func (r Ref) Valid() bool {
- // Name is required
- if !isValidPart(r.name) {
- return false
- }
- // Optional parts must be valid if present
- if r.domain != "" && !isValidPart(r.domain) {
- return false
- }
- if r.namespace != "" && !isValidPart(r.namespace) {
- return false
- }
- if r.tag != "" && !isValidPart(r.tag) {
- return false
- }
- if r.build != "" && !isValidPart(r.build) {
- return false
- }
- return true
- }
- // isValidPart returns true if given part is valid ascii [a-zA-Z0-9_\.-]
- func isValidPart(s string) bool {
- if len(s) == 0 {
- return false
- }
- for _, c := range []byte(s) {
- if c == '.' || c == '-' {
- return true
- }
- if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' {
- continue
- } else {
- return false
- }
- }
- return true
- }
|