ref.go 7.5 KB

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