ref.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. package blob
  2. import (
  3. "cmp"
  4. "fmt"
  5. "slices"
  6. "strings"
  7. )
  8. // Levels of concreteness
  9. const (
  10. domain = iota
  11. namespace
  12. name
  13. tag
  14. build
  15. )
  16. // Ref is an opaque reference to a blob.
  17. //
  18. // It is comparable and can be used as a map key.
  19. //
  20. // Users or Ref must check Valid before using it.
  21. type Ref struct {
  22. domain string
  23. namespace string
  24. name string
  25. tag string
  26. build string
  27. }
  28. // WithDomain returns a copy of r with the provided domain. If the provided
  29. // domain is empty, it returns the short, unqualified copy of r.
  30. func (r Ref) WithDomain(s string) Ref {
  31. return with(r, domain, s)
  32. }
  33. // WithNamespace returns a copy of r with the provided namespace. If the
  34. // provided namespace is empty, it returns the short, unqualified copy of r.
  35. func (r Ref) WithNamespace(s string) Ref {
  36. return with(r, namespace, s)
  37. }
  38. func (r Ref) WithTag(s string) Ref {
  39. return with(r, tag, s)
  40. }
  41. // WithBuild returns a copy of r with the provided build. If the provided
  42. // build is empty, it returns the short, unqualified copy of r.
  43. func (r Ref) WithBuild(s string) Ref {
  44. return with(r, build, s)
  45. }
  46. func with(r Ref, part int, value string) Ref {
  47. if value != "" && !isValidPart(value) {
  48. return Ref{}
  49. }
  50. switch part {
  51. case domain:
  52. r.domain = value
  53. case namespace:
  54. r.namespace = value
  55. case name:
  56. r.name = value
  57. case tag:
  58. r.tag = value
  59. case build:
  60. r.build = value
  61. default:
  62. panic(fmt.Sprintf("invalid completeness: %d", part))
  63. }
  64. return r
  65. }
  66. // Format returns a string representation of the ref with the given
  67. // concreteness. If a part is missing, it is replaced with a loud
  68. // placeholder.
  69. func (r Ref) Full() string {
  70. r.domain = cmp.Or(r.domain, "!(MISSING DOMAIN)")
  71. r.namespace = cmp.Or(r.namespace, "!(MISSING NAMESPACE)")
  72. r.name = cmp.Or(r.name, "!(MISSING NAME)")
  73. r.tag = cmp.Or(r.tag, "!(MISSING TAG)")
  74. r.build = cmp.Or(r.build, "!(MISSING BUILD)")
  75. return r.String()
  76. }
  77. func (r Ref) NameAndTag() string {
  78. r.domain = ""
  79. r.namespace = ""
  80. r.build = ""
  81. return r.String()
  82. }
  83. func (r Ref) NameTagAndBuild() string {
  84. r.domain = ""
  85. r.namespace = ""
  86. return r.String()
  87. }
  88. // String returns the fully qualified ref string.
  89. func (r Ref) String() string {
  90. var b strings.Builder
  91. if r.domain != "" {
  92. b.WriteString(r.domain)
  93. b.WriteString("/")
  94. }
  95. if r.namespace != "" {
  96. b.WriteString(r.namespace)
  97. b.WriteString("/")
  98. }
  99. b.WriteString(r.name)
  100. if r.tag != "" {
  101. b.WriteString(":")
  102. b.WriteString(r.tag)
  103. }
  104. if r.build != "" {
  105. b.WriteString("+")
  106. b.WriteString(r.build)
  107. }
  108. return b.String()
  109. }
  110. // Complete returns true if the ref is valid and has no empty parts.
  111. func (r Ref) Complete() bool {
  112. return r.Valid() && !slices.Contains(r.Parts(), "")
  113. }
  114. func (r Ref) CompleteWithoutBuild() bool {
  115. return r.Valid() && !slices.Contains(r.Parts()[:tag], "")
  116. }
  117. // Less returns true if r is less concrete than o; false otherwise.
  118. func (r Ref) Less(o Ref) bool {
  119. rp := r.Parts()
  120. op := o.Parts()
  121. for i := range rp {
  122. if rp[i] < op[i] {
  123. return true
  124. }
  125. }
  126. return false
  127. }
  128. // Parts returns the parts of the ref in order of concreteness.
  129. //
  130. // The length of the returned slice is always 5.
  131. func (r Ref) Parts() []string {
  132. return []string{
  133. domain: r.domain,
  134. namespace: r.namespace,
  135. name: r.name,
  136. tag: r.tag,
  137. build: r.build,
  138. }
  139. }
  140. func (r Ref) Domain() string { return r.namespace }
  141. func (r Ref) Namespace() string { return r.namespace }
  142. func (r Ref) Name() string { return r.name }
  143. func (r Ref) Tag() string { return r.tag }
  144. func (r Ref) Build() string { return r.build }
  145. // ParseRef parses a ref string into a Ref. A ref string is a name, an
  146. // optional tag, and an optional build, separated by colons and pluses.
  147. //
  148. // The name must be valid ascii [a-zA-Z0-9_].
  149. // The tag must be valid ascii [a-zA-Z0-9_].
  150. // The build must be valid ascii [a-zA-Z0-9_].
  151. //
  152. // It returns then zero value if the ref is invalid.
  153. //
  154. // // Valid Examples:
  155. // ParseRef("mistral:latest") returns ("mistral", "latest", "")
  156. // ParseRef("mistral") returns ("mistral", "", "")
  157. // ParseRef("mistral:30B") returns ("mistral", "30B", "")
  158. // ParseRef("mistral:7b") returns ("mistral", "7b", "")
  159. // ParseRef("mistral:7b+Q4_0") returns ("mistral", "7b", "Q4_0")
  160. // ParseRef("mistral+KQED") returns ("mistral", "latest", "KQED")
  161. // ParseRef(".x.:7b+Q4_0:latest") returns (".x.", "7b", "Q4_0")
  162. // ParseRef("-grok-f.oo:7b+Q4_0") returns ("-grok-f.oo", "7b", "Q4_0")
  163. //
  164. // // Invalid Examples:
  165. // ParseRef("m stral") returns ("", "", "") // zero
  166. // ParseRef("... 129 chars ...") returns ("", "", "") // zero
  167. func ParseRef(s string) Ref {
  168. if len(s) > 128 {
  169. return Ref{}
  170. }
  171. if strings.HasPrefix(s, "http://") {
  172. s = s[len("http://"):]
  173. }
  174. if strings.HasPrefix(s, "https://") {
  175. s = s[len("https://"):]
  176. }
  177. var r Ref
  178. state, j := build, len(s)
  179. for i := len(s) - 1; i >= 0; i-- {
  180. c := s[i]
  181. switch c {
  182. case '+':
  183. switch state {
  184. case build:
  185. r.build = s[i+1 : j]
  186. if r.build == "" {
  187. return Ref{}
  188. }
  189. r.build = strings.ToUpper(r.build)
  190. state, j = tag, i
  191. default:
  192. return Ref{}
  193. }
  194. case ':':
  195. switch state {
  196. case build, tag:
  197. r.tag = s[i+1 : j]
  198. if r.tag == "" {
  199. return Ref{}
  200. }
  201. state, j = name, i
  202. default:
  203. return Ref{}
  204. }
  205. case '/':
  206. switch state {
  207. case name, tag, build:
  208. r.name = s[i+1 : j]
  209. state, j = namespace, i
  210. case namespace:
  211. r.namespace = s[i+1 : j]
  212. state, j = domain, i
  213. default:
  214. return Ref{}
  215. }
  216. }
  217. }
  218. // handle the first part based on final state
  219. switch state {
  220. case domain:
  221. r.domain = s[:j]
  222. case namespace:
  223. r.namespace = s[:j]
  224. default:
  225. r.name = s[:j]
  226. }
  227. if !r.Valid() {
  228. return Ref{}
  229. }
  230. return r
  231. }
  232. // Complete is the same as ParseRef(s).Complete().
  233. //
  234. // Future versions may be faster than calling ParseRef(s).Complete(), so if
  235. // need to know if a ref is complete and don't need the ref, use this
  236. // function.
  237. func Complete(s string) bool {
  238. // TODO(bmizerany): fast-path this with a quick scan withput
  239. // allocating strings
  240. return ParseRef(s).Complete()
  241. }
  242. func (r Ref) Valid() bool {
  243. // Name is required
  244. if !isValidPart(r.name) {
  245. return false
  246. }
  247. // Optional parts must be valid if present
  248. if r.domain != "" && !isValidPart(r.domain) {
  249. return false
  250. }
  251. if r.namespace != "" && !isValidPart(r.namespace) {
  252. return false
  253. }
  254. if r.tag != "" && !isValidPart(r.tag) {
  255. return false
  256. }
  257. if r.build != "" && !isValidPart(r.build) {
  258. return false
  259. }
  260. return true
  261. }
  262. // isValidPart returns true if given part is valid ascii [a-zA-Z0-9_\.-]
  263. func isValidPart(s string) bool {
  264. if len(s) == 0 {
  265. return false
  266. }
  267. for _, c := range []byte(s) {
  268. if c == '.' || c == '-' {
  269. return true
  270. }
  271. if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' {
  272. continue
  273. } else {
  274. return false
  275. }
  276. }
  277. return true
  278. }