ref.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  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 reports whether the ref is fully qualified. That is it has a
  114. // domain, namespace, name, tag, and build.
  115. func (r Ref) Complete() bool {
  116. return r.Valid() && !slices.Contains(r.Parts(), "")
  117. }
  118. // CompleteWithoutBuild reports whether the ref would be complete if it had a
  119. // valid build.
  120. func (r Ref) CompleteWithoutBuild() bool {
  121. r.build = "x"
  122. return r.Valid() && r.Complete()
  123. }
  124. // Less returns true if r is less concrete than o; false otherwise.
  125. func (r Ref) Less(o Ref) bool {
  126. rp := r.Parts()
  127. op := o.Parts()
  128. for i := range rp {
  129. if rp[i] < op[i] {
  130. return true
  131. }
  132. }
  133. return false
  134. }
  135. // Parts returns the parts of the ref in order of concreteness.
  136. //
  137. // The length of the returned slice is always 5.
  138. func (r Ref) Parts() []string {
  139. return []string{
  140. r.domain,
  141. r.namespace,
  142. r.name,
  143. r.tag,
  144. r.build,
  145. }
  146. }
  147. func (r Ref) Domain() string { return r.namespace }
  148. func (r Ref) Namespace() string { return r.namespace }
  149. func (r Ref) Name() string { return r.name }
  150. func (r Ref) Tag() string { return r.tag }
  151. func (r Ref) Build() string { return r.build }
  152. // ParseRef parses a ref string into a Ref. A ref string is a name, an
  153. // optional tag, and an optional build, separated by colons and pluses.
  154. //
  155. // The name must be valid ascii [a-zA-Z0-9_].
  156. // The tag must be valid ascii [a-zA-Z0-9_].
  157. // The build must be valid ascii [a-zA-Z0-9_].
  158. //
  159. // It returns then zero value if the ref is invalid.
  160. //
  161. // // Valid Examples:
  162. // ParseRef("mistral:latest") returns ("mistral", "latest", "")
  163. // ParseRef("mistral") returns ("mistral", "", "")
  164. // ParseRef("mistral:30B") returns ("mistral", "30B", "")
  165. // ParseRef("mistral:7b") returns ("mistral", "7b", "")
  166. // ParseRef("mistral:7b+Q4_0") returns ("mistral", "7b", "Q4_0")
  167. // ParseRef("mistral+KQED") returns ("mistral", "latest", "KQED")
  168. // ParseRef(".x.:7b+Q4_0:latest") returns (".x.", "7b", "Q4_0")
  169. // ParseRef("-grok-f.oo:7b+Q4_0") returns ("-grok-f.oo", "7b", "Q4_0")
  170. //
  171. // // Invalid Examples:
  172. // ParseRef("m stral") returns ("", "", "") // zero
  173. // ParseRef("... 129 chars ...") returns ("", "", "") // zero
  174. func ParseRef(s string) Ref {
  175. var r Ref
  176. for kind, part := range Parts(s) {
  177. switch kind {
  178. case Domain:
  179. r = r.WithDomain(part)
  180. case Namespace:
  181. r = r.WithNamespace(part)
  182. case Name:
  183. r.name = part
  184. case Tag:
  185. r = r.WithTag(part)
  186. case Build:
  187. r = r.WithBuild(part)
  188. case Invalid:
  189. return Ref{}
  190. }
  191. }
  192. if !r.Valid() {
  193. return Ref{}
  194. }
  195. return r
  196. }
  197. // Parts returns a sequence of the parts of a ref string from most specific
  198. // to least specific.
  199. //
  200. // It normalizes the input string by removing "http://" and "https://" only.
  201. // No other normalization is done.
  202. func Parts(s string) iter.Seq2[PartKind, string] {
  203. return func(yield func(PartKind, string) bool) {
  204. if strings.HasPrefix(s, "http://") {
  205. s = s[len("http://"):]
  206. }
  207. if strings.HasPrefix(s, "https://") {
  208. s = s[len("https://"):]
  209. }
  210. if len(s) > MaxRefLength || len(s) == 0 {
  211. return
  212. }
  213. yieldValid := func(kind PartKind, part string) bool {
  214. if !isValidPart(part) {
  215. yield(Invalid, "")
  216. return false
  217. }
  218. return yield(kind, part)
  219. }
  220. state, j := Build, len(s)
  221. for i := len(s) - 1; i >= 0; i-- {
  222. switch s[i] {
  223. case '+':
  224. switch state {
  225. case Build:
  226. if !yieldValid(Build, s[i+1:j]) {
  227. return
  228. }
  229. state, j = Tag, i
  230. default:
  231. yield(Invalid, "")
  232. return
  233. }
  234. case ':':
  235. switch state {
  236. case Build, Tag:
  237. if !yieldValid(Tag, s[i+1:j]) {
  238. return
  239. }
  240. state, j = Name, i
  241. default:
  242. yield(Invalid, "")
  243. return
  244. }
  245. case '/':
  246. switch state {
  247. case Name, Tag, Build:
  248. if !yieldValid(Name, s[i+1:j]) {
  249. return
  250. }
  251. state, j = Namespace, i
  252. case Namespace:
  253. if !yieldValid(Namespace, s[i+1:j]) {
  254. return
  255. }
  256. state, j = Domain, i
  257. default:
  258. yield(Invalid, "")
  259. return
  260. }
  261. default:
  262. if !isValidPart(s[i : i+1]) {
  263. yield(Invalid, "")
  264. return
  265. }
  266. }
  267. }
  268. if state <= Namespace {
  269. yieldValid(state, s[:j])
  270. } else {
  271. yieldValid(Name, s[:j])
  272. }
  273. }
  274. }
  275. // Valid returns true if the ref has a valid name. To know if a ref is
  276. // "complete", use Complete.
  277. func (r Ref) Valid() bool {
  278. // Parts ensures we only have valid parts, so no need to validate
  279. // them here, only check if we have a name or not.
  280. return r.name != ""
  281. }
  282. // isValidPart returns true if given part is valid ascii [a-zA-Z0-9_\.-]
  283. func isValidPart(s string) bool {
  284. if s == "" {
  285. return false
  286. }
  287. for _, c := range []byte(s) {
  288. if c == '.' || c == '-' {
  289. return true
  290. }
  291. if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' {
  292. continue
  293. } else {
  294. return false
  295. }
  296. }
  297. return true
  298. }