name.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. // Package model contains types and utilities for parsing, validating, and
  2. // working with model names and digests.
  3. package model
  4. import (
  5. "cmp"
  6. "encoding/hex"
  7. "errors"
  8. "fmt"
  9. "log/slog"
  10. "path/filepath"
  11. "strings"
  12. )
  13. // Errors
  14. var (
  15. // ErrUnqualifiedName represents an error where a name is not fully
  16. // qualified. It is not used directly in this package, but is here
  17. // to avoid other packages inventing their own error type.
  18. // Additionally, it can be conveniently used via [Unqualified].
  19. ErrUnqualifiedName = errors.New("unqualified name")
  20. )
  21. // Unqualified is a helper function that returns an error with
  22. // ErrUnqualifiedName as the cause and the name as the message.
  23. func Unqualified(n Name) error {
  24. return fmt.Errorf("%w: %s", ErrUnqualifiedName, n)
  25. }
  26. // MissingPart is used to indicate any part of a name that was "promised" by
  27. // the presence of a separator, but is missing.
  28. //
  29. // The value was chosen because it is deemed unlikely to be set by a user,
  30. // not a valid part name valid when checked by [Name.IsValid], and easy to
  31. // spot in logs.
  32. const MissingPart = "!MISSING!"
  33. const (
  34. defaultHost = "registry.ollama.ai"
  35. defaultNamespace = "library"
  36. defaultTag = "latest"
  37. )
  38. // DefaultName returns a name with the default values for the host, namespace,
  39. // and tag parts. The model and digest parts are empty.
  40. //
  41. // - The default host is ("registry.ollama.ai")
  42. // - The default namespace is ("library")
  43. // - The default tag is ("latest")
  44. func DefaultName() Name {
  45. return Name{
  46. Host: defaultHost,
  47. Namespace: defaultNamespace,
  48. Tag: defaultTag,
  49. }
  50. }
  51. type partKind int
  52. const (
  53. kindHost partKind = iota
  54. kindNamespace
  55. kindModel
  56. kindTag
  57. kindDigest
  58. )
  59. func (k partKind) String() string {
  60. switch k {
  61. case kindHost:
  62. return "host"
  63. case kindNamespace:
  64. return "namespace"
  65. case kindModel:
  66. return "model"
  67. case kindTag:
  68. return "tag"
  69. case kindDigest:
  70. return "digest"
  71. default:
  72. return "unknown"
  73. }
  74. }
  75. // Name is a structured representation of a model name string, as defined by
  76. // [ParseNameNoDefaults].
  77. //
  78. // It is not guaranteed to be valid. Use [Name.IsValid] to check if the name
  79. // is valid.
  80. type Name struct {
  81. Host string
  82. Namespace string
  83. Model string
  84. Tag string
  85. RawDigest string
  86. }
  87. // ParseName parses and assembles a Name from a name string. The
  88. // format of a valid name string is:
  89. //
  90. // s:
  91. // { host } "/" { namespace } "/" { model } ":" { tag } "@" { digest }
  92. // { host } "/" { namespace } "/" { model } ":" { tag }
  93. // { host } "/" { namespace } "/" { model } "@" { digest }
  94. // { host } "/" { namespace } "/" { model }
  95. // { namespace } "/" { model } ":" { tag } "@" { digest }
  96. // { namespace } "/" { model } ":" { tag }
  97. // { namespace } "/" { model } "@" { digest }
  98. // { namespace } "/" { model }
  99. // { model } ":" { tag } "@" { digest }
  100. // { model } ":" { tag }
  101. // { model } "@" { digest }
  102. // { model }
  103. // "@" { digest }
  104. // host:
  105. // pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." | ":" }*
  106. // length: [1, 350]
  107. // namespace:
  108. // pattern: { alphanum | "_" } { alphanum | "-" | "_" }*
  109. // length: [1, 80]
  110. // model:
  111. // pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." }*
  112. // length: [1, 80]
  113. // tag:
  114. // pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." }*
  115. // length: [1, 80]
  116. // digest:
  117. // pattern: { alphanum | "_" } { alphanum | "-" | ":" }*
  118. // length: [1, 80]
  119. //
  120. // Most users should use [ParseName] instead, unless need to support
  121. // different defaults than DefaultName.
  122. //
  123. // The name returned is not guaranteed to be valid. If it is not valid, the
  124. // field values are left in an undefined state. Use [Name.IsValid] to check
  125. // if the name is valid.
  126. func ParseName(s string) Name {
  127. return Merge(ParseNameBare(s), DefaultName())
  128. }
  129. // ParseNameBare parses s as a name string and returns a Name. No merge with
  130. // [DefaultName] is performed.
  131. func ParseNameBare(s string) Name {
  132. var n Name
  133. var promised bool
  134. s, n.RawDigest, promised = cutLast(s, "@")
  135. if promised && n.RawDigest == "" {
  136. n.RawDigest = MissingPart
  137. }
  138. // "/" is an illegal tag character, so we can use it to split the host
  139. if strings.LastIndex(s, ":") > strings.LastIndex(s, "/") {
  140. s, n.Tag, _ = cutPromised(s, ":")
  141. }
  142. s, n.Model, promised = cutPromised(s, "/")
  143. if !promised {
  144. n.Model = s
  145. return n
  146. }
  147. s, n.Namespace, promised = cutPromised(s, "/")
  148. if !promised {
  149. n.Namespace = s
  150. return n
  151. }
  152. scheme, host, ok := strings.Cut(s, "://")
  153. if !ok {
  154. host = scheme
  155. }
  156. n.Host = host
  157. return n
  158. }
  159. // ParseNameFromFilepath parses a 4-part filepath as a Name. The parts are
  160. // expected to be in the form:
  161. //
  162. // { host } "/" { namespace } "/" { model } "/" { tag }
  163. func ParseNameFromFilepath(s string) (n Name) {
  164. parts := strings.Split(s, string(filepath.Separator))
  165. if len(parts) != 4 {
  166. return Name{}
  167. }
  168. n.Host = parts[0]
  169. n.Namespace = parts[1]
  170. n.Model = parts[2]
  171. n.Tag = parts[3]
  172. if !n.IsFullyQualified() {
  173. return Name{}
  174. }
  175. return n
  176. }
  177. // Merge merges the host, namespace, and tag parts of the two names,
  178. // preferring the non-empty parts of a.
  179. func Merge(a, b Name) Name {
  180. a.Host = cmp.Or(a.Host, b.Host)
  181. a.Namespace = cmp.Or(a.Namespace, b.Namespace)
  182. a.Tag = cmp.Or(a.Tag, b.Tag)
  183. return a
  184. }
  185. // String returns the name string, in the format that [ParseNameNoDefaults]
  186. // accepts as valid, if [Name.IsValid] reports true; otherwise the empty
  187. // string is returned.
  188. func (n Name) String() string {
  189. var b strings.Builder
  190. if n.Host != "" {
  191. b.WriteString(n.Host)
  192. b.WriteByte('/')
  193. }
  194. if n.Namespace != "" {
  195. b.WriteString(n.Namespace)
  196. b.WriteByte('/')
  197. }
  198. b.WriteString(n.Model)
  199. if n.Tag != "" {
  200. b.WriteByte(':')
  201. b.WriteString(n.Tag)
  202. }
  203. if n.RawDigest != "" {
  204. b.WriteByte('@')
  205. b.WriteString(n.RawDigest)
  206. }
  207. return b.String()
  208. }
  209. // DisplayShort returns a short string version of the name.
  210. func (n Name) DisplayShortest() string {
  211. var sb strings.Builder
  212. if n.Host != defaultHost {
  213. sb.WriteString(n.Host)
  214. sb.WriteByte('/')
  215. sb.WriteString(n.Namespace)
  216. sb.WriteByte('/')
  217. } else if n.Namespace != defaultNamespace {
  218. sb.WriteString(n.Namespace)
  219. sb.WriteByte('/')
  220. }
  221. // always include model and tag
  222. sb.WriteString(n.Model)
  223. sb.WriteString(":")
  224. sb.WriteString(n.Tag)
  225. return sb.String()
  226. }
  227. // IsValid reports whether all parts of the name are present and valid. The
  228. // digest is a special case, and is checked for validity only if present.
  229. func (n Name) IsValid() bool {
  230. if n.RawDigest != "" && !isValidPart(kindDigest, n.RawDigest) {
  231. return false
  232. }
  233. return n.IsFullyQualified()
  234. }
  235. // IsFullyQualified returns true if all parts of the name are present and
  236. // valid without the digest.
  237. func (n Name) IsFullyQualified() bool {
  238. var parts = []string{
  239. n.Host,
  240. n.Namespace,
  241. n.Model,
  242. n.Tag,
  243. }
  244. for i, part := range parts {
  245. if !isValidPart(partKind(i), part) {
  246. return false
  247. }
  248. }
  249. return true
  250. }
  251. // Filepath returns a canonical filepath that represents the name with each part from
  252. // host to tag as a directory in the form:
  253. //
  254. // {host}/{namespace}/{model}/{tag}
  255. //
  256. // It uses the system's filepath separator and ensures the path is clean.
  257. //
  258. // It panics if the name is not fully qualified. Use [Name.IsFullyQualified]
  259. // to check if the name is fully qualified.
  260. func (n Name) Filepath() string {
  261. if !n.IsFullyQualified() {
  262. panic("illegal attempt to get filepath of invalid name")
  263. }
  264. return filepath.Join(
  265. n.Host,
  266. n.Namespace,
  267. n.Model,
  268. n.Tag,
  269. )
  270. }
  271. // LogValue returns a slog.Value that represents the name as a string.
  272. func (n Name) LogValue() slog.Value {
  273. return slog.StringValue(n.String())
  274. }
  275. func isValidLen(kind partKind, s string) bool {
  276. switch kind {
  277. case kindHost:
  278. return len(s) >= 1 && len(s) <= 350
  279. case kindTag:
  280. return len(s) >= 1 && len(s) <= 80
  281. default:
  282. return len(s) >= 1 && len(s) <= 80
  283. }
  284. }
  285. func isValidPart(kind partKind, s string) bool {
  286. if !isValidLen(kind, s) {
  287. return false
  288. }
  289. for i := range s {
  290. if i == 0 {
  291. if !isAlphanumericOrUnderscore(s[i]) {
  292. return false
  293. }
  294. continue
  295. }
  296. switch s[i] {
  297. case '_', '-':
  298. case '.':
  299. if kind == kindNamespace {
  300. return false
  301. }
  302. case ':':
  303. if kind != kindHost && kind != kindDigest {
  304. return false
  305. }
  306. default:
  307. if !isAlphanumericOrUnderscore(s[i]) {
  308. return false
  309. }
  310. }
  311. }
  312. return true
  313. }
  314. func isAlphanumericOrUnderscore(c byte) bool {
  315. return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_'
  316. }
  317. func cutLast(s, sep string) (before, after string, ok bool) {
  318. i := strings.LastIndex(s, sep)
  319. if i >= 0 {
  320. return s[:i], s[i+len(sep):], true
  321. }
  322. return s, "", false
  323. }
  324. // cutPromised cuts the last part of s at the last occurrence of sep. If sep is
  325. // found, the part before and after sep are returned as-is unless empty, in
  326. // which case they are returned as MissingPart, which will cause
  327. // [Name.IsValid] to return false.
  328. func cutPromised(s, sep string) (before, after string, ok bool) {
  329. before, after, ok = cutLast(s, sep)
  330. if !ok {
  331. return before, after, false
  332. }
  333. return cmp.Or(before, MissingPart), cmp.Or(after, MissingPart), true
  334. }
  335. type DigestType byte
  336. const (
  337. DigestTypeInvalid DigestType = iota
  338. DigestTypeSHA256
  339. )
  340. func (t DigestType) String() string {
  341. switch t {
  342. case DigestTypeSHA256:
  343. return "sha256"
  344. default:
  345. return "invalid"
  346. }
  347. }
  348. type Digest struct {
  349. Type DigestType
  350. Sum [32]byte
  351. }
  352. func ParseDigest(s string) (Digest, error) {
  353. i := strings.IndexAny(s, "-:")
  354. if i < 0 {
  355. return Digest{}, fmt.Errorf("invalid digest %q", s)
  356. }
  357. typ, encSum := s[:i], s[i+1:]
  358. if typ != "sha256" {
  359. return Digest{}, fmt.Errorf("unsupported digest type %q", typ)
  360. }
  361. d := Digest{
  362. Type: DigestTypeSHA256,
  363. }
  364. n, err := hex.Decode(d.Sum[:], []byte(encSum))
  365. if err != nil {
  366. return Digest{}, err
  367. }
  368. if n != 32 {
  369. return Digest{}, fmt.Errorf("digest %q decoded to %d bytes; want 32", encSum, n)
  370. }
  371. return d, nil
  372. }
  373. func (d Digest) String() string {
  374. if d.Type == DigestTypeInvalid {
  375. return ""
  376. }
  377. return fmt.Sprintf("sha256-%x", d.Sum)
  378. }
  379. func (d Digest) IsValid() bool {
  380. return d.Type != DigestTypeInvalid
  381. }