name_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. package model
  2. import (
  3. "bytes"
  4. "cmp"
  5. "fmt"
  6. "log/slog"
  7. "path/filepath"
  8. "slices"
  9. "strings"
  10. "testing"
  11. )
  12. type fields struct {
  13. host, namespace, model, tag, build string
  14. digest string
  15. }
  16. func fieldsFromName(p Name) fields {
  17. return fields{
  18. host: p.parts[PartHost],
  19. namespace: p.parts[PartNamespace],
  20. model: p.parts[PartModel],
  21. tag: p.parts[PartTag],
  22. build: p.parts[PartBuild],
  23. digest: p.parts[PartDigest],
  24. }
  25. }
  26. var testNames = map[string]fields{
  27. "mistral:latest": {model: "mistral", tag: "latest"},
  28. "mistral": {model: "mistral"},
  29. "mistral:30B": {model: "mistral", tag: "30B"},
  30. "mistral:7b": {model: "mistral", tag: "7b"},
  31. "mistral:7b+Q4_0": {model: "mistral", tag: "7b", build: "Q4_0"},
  32. "mistral+KQED": {model: "mistral", build: "KQED"},
  33. "mistral.x-3:7b+Q4_0": {model: "mistral.x-3", tag: "7b", build: "Q4_0"},
  34. "mistral:7b+q4_0": {model: "mistral", tag: "7b", build: "q4_0"},
  35. "llama2": {model: "llama2"},
  36. "user/model": {namespace: "user", model: "model"},
  37. "example.com/ns/mistral:7b+Q4_0": {host: "example.com", namespace: "ns", model: "mistral", tag: "7b", build: "Q4_0"},
  38. "example.com/ns/mistral:7b+X": {host: "example.com", namespace: "ns", model: "mistral", tag: "7b", build: "X"},
  39. "localhost:5000/ns/mistral": {host: "localhost:5000", namespace: "ns", model: "mistral"},
  40. // invalid digest
  41. "mistral:latest@invalid256-": {},
  42. "mistral:latest@-123": {},
  43. "mistral:latest@!-123": {},
  44. "mistral:latest@1-!": {},
  45. "mistral:latest@": {},
  46. // resolved
  47. "x@sha123-1": {model: "x", digest: "sha123-1"},
  48. "@sha456-2": {digest: "sha456-2"},
  49. "@@sha123-1": {},
  50. // preserves case for build
  51. "x+b": {model: "x", build: "b"},
  52. // invalid (includes fuzzing trophies)
  53. " / / : + ": {},
  54. " / : + ": {},
  55. " : + ": {},
  56. " + ": {},
  57. " : ": {},
  58. " / ": {},
  59. " /": {},
  60. "/ ": {},
  61. "/": {},
  62. ":": {},
  63. "+": {},
  64. // (".") in namepsace is not allowed
  65. "invalid.com/7b+x": {},
  66. "invalid:7b+Q4_0:latest": {},
  67. "in valid": {},
  68. "invalid/y/z/foo": {},
  69. "/0": {},
  70. "0 /0": {},
  71. "0 /": {},
  72. "0/": {},
  73. ":/0": {},
  74. "+0/00000": {},
  75. "0+.\xf2\x80\xf6\x9d00000\xe5\x99\xe6\xd900\xd90\xa60\x91\xdc0\xff\xbf\x99\xe800\xb9\xdc\xd6\xc300\x970\xfb\xfd0\xe0\x8a\xe1\xad\xd40\x9700\xa80\x980\xdd0000\xb00\x91000\xfe0\x89\x9b\x90\x93\x9f0\xe60\xf7\x84\xb0\x87\xa5\xff0\xa000\x9a\x85\xf6\x85\xfe\xa9\xf9\xe9\xde00\xf4\xe0\x8f\x81\xad\xde00\xd700\xaa\xe000000\xb1\xee0\x91": {},
  76. "0//0": {},
  77. "m+^^^": {},
  78. "file:///etc/passwd": {},
  79. "file:///etc/passwd:latest": {},
  80. "file:///etc/passwd:latest+u": {},
  81. ":x": {},
  82. "+x": {},
  83. "x+": {},
  84. // Disallow ("\.+") in any part to prevent path traversal anywhere
  85. // we convert the name to a path.
  86. "../etc/passwd": {},
  87. ".../etc/passwd": {},
  88. "./../passwd": {},
  89. "./0+..": {},
  90. strings.Repeat("a", MaxNamePartLen): {model: strings.Repeat("a", MaxNamePartLen)},
  91. strings.Repeat("a", MaxNamePartLen+1): {},
  92. }
  93. func TestIsValidNameLen(t *testing.T) {
  94. if IsValidNamePart(PartNamespace, strings.Repeat("a", MaxNamePartLen+1)) {
  95. t.Errorf("unexpectedly valid long name")
  96. }
  97. }
  98. // TestConsecutiveDots tests that consecutive dots are not allowed in any
  99. // part, to avoid path traversal. There also are some tests in testNames, but
  100. // this test is more exhaustive and exists to emphasize the importance of
  101. // preventing path traversal.
  102. func TestNameConsecutiveDots(t *testing.T) {
  103. for i := 1; i < 10; i++ {
  104. s := strings.Repeat(".", i)
  105. if i > 1 {
  106. if g := ParseName(s, FillNothing).DisplayLong(); g != "" {
  107. t.Errorf("ParseName(%q) = %q; want empty string", s, g)
  108. }
  109. } else {
  110. if g := ParseName(s, FillNothing).DisplayLong(); g != s {
  111. t.Errorf("ParseName(%q) = %q; want %q", s, g, s)
  112. }
  113. }
  114. }
  115. }
  116. func TestNameParts(t *testing.T) {
  117. var p Name
  118. if w, g := int(NumParts), len(p.parts); w != g {
  119. t.Errorf("Parts() = %d; want %d", g, w)
  120. }
  121. }
  122. func TestNamePartString(t *testing.T) {
  123. if g := PartKind(-2).String(); g != "Unknown" {
  124. t.Errorf("Unknown part = %q; want %q", g, "Unknown")
  125. }
  126. for kind, name := range kindNames {
  127. if g := kind.String(); g != name {
  128. t.Errorf("%s = %q; want %q", kind, g, name)
  129. }
  130. }
  131. }
  132. func TestParseName(t *testing.T) {
  133. for baseName, want := range testNames {
  134. for _, prefix := range []string{"", "https://", "http://"} {
  135. // We should get the same results with or without the
  136. // http(s) prefixes
  137. s := prefix + baseName
  138. t.Run(s, func(t *testing.T) {
  139. name := ParseName(s, FillNothing)
  140. got := fieldsFromName(name)
  141. if got != want {
  142. t.Errorf("ParseName(%q) = %q; want %q", s, got, want)
  143. }
  144. // test round-trip
  145. if !ParseName(name.DisplayLong(), FillNothing).EqualFold(name) {
  146. t.Errorf("ParseName(%q).String() = %s; want %s", s, name.DisplayLong(), baseName)
  147. }
  148. })
  149. }
  150. }
  151. }
  152. func TestParseNameFill(t *testing.T) {
  153. cases := []struct {
  154. in string
  155. fill string
  156. want string
  157. }{
  158. {"mistral", "example.com/library/?:latest+Q4_0", "example.com/library/mistral:latest+Q4_0"},
  159. {"mistral", "example.com/library/?:latest", "example.com/library/mistral:latest"},
  160. {"llama2:x", "example.com/library/?:latest+Q4_0", "example.com/library/llama2:x+Q4_0"},
  161. // Invalid
  162. {"", "example.com/library/?:latest+Q4_0", ""},
  163. {"llama2:?", "example.com/library/?:latest+Q4_0", ""},
  164. }
  165. for _, tt := range cases {
  166. t.Run(tt.in, func(t *testing.T) {
  167. name := ParseName(tt.in, tt.fill)
  168. if g := name.DisplayLong(); g != tt.want {
  169. t.Errorf("ParseName(%q, %q) = %q; want %q", tt.in, tt.fill, g, tt.want)
  170. }
  171. })
  172. }
  173. t.Run("invalid fill", func(t *testing.T) {
  174. defer func() {
  175. if recover() == nil {
  176. t.Fatal("expected panic")
  177. }
  178. }()
  179. ParseName("x", "^")
  180. })
  181. }
  182. func TestParseNameHTTPDoublePrefixStrip(t *testing.T) {
  183. cases := []string{
  184. "http://https://valid.com/valid/valid:latest",
  185. "https://http://valid.com/valid/valid:latest",
  186. }
  187. for _, s := range cases {
  188. t.Run(s, func(t *testing.T) {
  189. name := ParseName(s, FillNothing)
  190. if name.IsValid() {
  191. t.Errorf("expected invalid path; got %#v", name)
  192. }
  193. })
  194. }
  195. }
  196. func TestCompleteWithAndWithoutBuild(t *testing.T) {
  197. cases := []struct {
  198. in string
  199. complete bool
  200. completeNoBuild bool
  201. }{
  202. {"", false, false},
  203. {"incomplete/mistral:7b+x", false, false},
  204. {"incomplete/mistral:7b+Q4_0", false, false},
  205. {"incomplete:7b+x", false, false},
  206. {"complete.com/x/mistral:latest+Q4_0", true, true},
  207. {"complete.com/x/mistral:latest", false, true},
  208. }
  209. for _, tt := range cases {
  210. t.Run(tt.in, func(t *testing.T) {
  211. p := ParseName(tt.in, FillNothing)
  212. t.Logf("ParseName(%q) = %#v", tt.in, p)
  213. if g := p.IsComplete(); g != tt.complete {
  214. t.Errorf("Complete(%q) = %v; want %v", tt.in, g, tt.complete)
  215. }
  216. if g := p.IsCompleteNoBuild(); g != tt.completeNoBuild {
  217. t.Errorf("CompleteNoBuild(%q) = %v; want %v", tt.in, g, tt.completeNoBuild)
  218. }
  219. })
  220. }
  221. // Complete uses Parts which returns a slice, but it should be
  222. // inlined when used in Complete, preventing any allocations or
  223. // escaping to the heap.
  224. allocs := testing.AllocsPerRun(1000, func() {
  225. keep(ParseName("complete.com/x/mistral:latest+Q4_0", FillNothing).IsComplete())
  226. })
  227. if allocs > 0 {
  228. t.Errorf("Complete allocs = %v; want 0", allocs)
  229. }
  230. }
  231. func TestNameLogValue(t *testing.T) {
  232. cases := []string{
  233. "example.com/library/mistral:latest+Q4_0",
  234. "mistral:latest",
  235. "mistral:7b+Q4_0",
  236. }
  237. for _, s := range cases {
  238. t.Run(s, func(t *testing.T) {
  239. var b bytes.Buffer
  240. log := slog.New(slog.NewTextHandler(&b, nil))
  241. name := ParseName(s, FillNothing)
  242. log.Info("", "name", name)
  243. want := fmt.Sprintf("name=%s", name.GoString())
  244. got := b.String()
  245. if !strings.Contains(got, want) {
  246. t.Errorf("expected log output to contain %q; got %q", want, got)
  247. }
  248. })
  249. }
  250. }
  251. func TestNameGoString(t *testing.T) {
  252. cases := []struct {
  253. name string
  254. in string
  255. wantString string
  256. wantGoString string // default is tt.in
  257. }{
  258. {
  259. name: "Complete Name",
  260. in: "example.com/library/mistral:latest+Q4_0",
  261. wantGoString: "example.com/library/mistral:latest+Q4_0@?",
  262. },
  263. {
  264. name: "Short Name",
  265. in: "mistral:latest",
  266. wantGoString: "?/?/mistral:latest+?@?",
  267. },
  268. {
  269. name: "Long Name",
  270. in: "library/mistral:latest",
  271. wantGoString: "?/library/mistral:latest+?@?",
  272. },
  273. {
  274. name: "Case Preserved",
  275. in: "Library/Mistral:Latest",
  276. wantGoString: "?/Library/Mistral:Latest+?@?",
  277. },
  278. {
  279. name: "With digest",
  280. in: "Library/Mistral:Latest@sha256-123456",
  281. wantGoString: "?/Library/Mistral:Latest+?@sha256-123456",
  282. },
  283. }
  284. for _, tt := range cases {
  285. t.Run(tt.name, func(t *testing.T) {
  286. p := ParseName(tt.in, FillNothing)
  287. tt.wantGoString = cmp.Or(tt.wantGoString, tt.in)
  288. if g := fmt.Sprintf("%#v", p); g != tt.wantGoString {
  289. t.Errorf("GoString() = %q; want %q", g, tt.wantGoString)
  290. }
  291. })
  292. }
  293. }
  294. func TestDisplayLongest(t *testing.T) {
  295. g := ParseName("example.com/library/mistral:latest+Q4_0", FillNothing).DisplayLongest()
  296. if g != "example.com/library/mistral:latest" {
  297. t.Errorf("got = %q; want %q", g, "example.com/library/mistral:latest")
  298. }
  299. }
  300. func TestDisplayShortest(t *testing.T) {
  301. cases := []struct {
  302. in string
  303. mask string
  304. want string
  305. wantPanic bool
  306. }{
  307. {"example.com/library/mistral:latest+Q4_0", "example.com/library/_:latest", "mistral", false},
  308. {"example.com/library/mistral:latest+Q4_0", "example.com/_/_:latest", "library/mistral", false},
  309. {"example.com/library/mistral:latest+Q4_0", "", "example.com/library/mistral", false},
  310. {"example.com/library/mistral:latest+Q4_0", "", "example.com/library/mistral", false},
  311. // case-insensitive
  312. {"Example.com/library/mistral:latest+Q4_0", "example.com/library/_:latest", "mistral", false},
  313. {"example.com/Library/mistral:latest+Q4_0", "example.com/library/_:latest", "mistral", false},
  314. {"example.com/library/Mistral:latest+Q4_0", "example.com/library/_:latest", "Mistral", false},
  315. {"example.com/library/mistral:Latest+Q4_0", "example.com/library/_:latest", "mistral", false},
  316. {"example.com/library/mistral:Latest+q4_0", "example.com/library/_:latest", "mistral", false},
  317. // zero value
  318. {"", MaskDefault, "", true},
  319. // invalid mask
  320. {"example.com/library/mistral:latest+Q4_0", "example.com/mistral", "", true},
  321. // DefaultMask
  322. {"registry.ollama.ai/library/mistral:latest+Q4_0", MaskDefault, "mistral", false},
  323. // Auto-Fill
  324. {"x", "example.com/library/_:latest", "x", false},
  325. {"x", "example.com/library/_:latest+Q4_0", "x", false},
  326. {"x/y:z", "a.com/library/_:latest+Q4_0", "x/y:z", false},
  327. {"x/y:z", "a.com/library/_:latest+Q4_0", "x/y:z", false},
  328. }
  329. for _, tt := range cases {
  330. t.Run("", func(t *testing.T) {
  331. defer func() {
  332. if tt.wantPanic {
  333. if recover() == nil {
  334. t.Errorf("expected panic")
  335. }
  336. }
  337. }()
  338. p := ParseName(tt.in, FillNothing)
  339. t.Logf("ParseName(%q) = %#v", tt.in, p)
  340. if g := p.DisplayShortest(tt.mask); g != tt.want {
  341. t.Errorf("got = %q; want %q", g, tt.want)
  342. }
  343. })
  344. }
  345. }
  346. func TestParseNameAllocs(t *testing.T) {
  347. allocs := testing.AllocsPerRun(1000, func() {
  348. keep(ParseName("example.com/mistral:7b+Q4_0", FillNothing))
  349. })
  350. if allocs > 0 {
  351. t.Errorf("ParseName allocs = %v; want 0", allocs)
  352. }
  353. }
  354. func BenchmarkParseName(b *testing.B) {
  355. b.ReportAllocs()
  356. for range b.N {
  357. keep(ParseName("example.com/mistral:7b+Q4_0", FillNothing))
  358. }
  359. }
  360. func FuzzParseNameFromFilepath(f *testing.F) {
  361. f.Add("example.com/library/mistral/7b/Q4_0")
  362. f.Add("example.com/../mistral/7b/Q4_0")
  363. f.Add("example.com/x/../7b/Q4_0")
  364. f.Add("example.com/x/../7b")
  365. f.Fuzz(func(t *testing.T, s string) {
  366. name := ParseNameFromFilepath(s, FillNothing)
  367. if strings.Contains(s, "..") && !name.IsZero() {
  368. t.Fatalf("non-zero value for path with '..': %q", s)
  369. }
  370. if name.IsValid() == name.IsZero() {
  371. t.Errorf("expected valid path to be non-zero value; got %#v", name)
  372. }
  373. })
  374. }
  375. func FuzzParseName(f *testing.F) {
  376. f.Add("example.com/mistral:7b+Q4_0")
  377. f.Add("example.com/mistral:7b+q4_0")
  378. f.Add("example.com/mistral:7b+x")
  379. f.Add("x/y/z:8n+I")
  380. f.Add(":x")
  381. f.Add("@sha256-123456")
  382. f.Add("example.com/mistral:latest+Q4_0@sha256-123456")
  383. f.Add(":@!@")
  384. f.Add("...")
  385. f.Fuzz(func(t *testing.T, s string) {
  386. r0 := ParseName(s, FillNothing)
  387. if strings.Contains(s, "..") && !r0.IsZero() {
  388. t.Fatalf("non-zero value for path with '..': %q", s)
  389. }
  390. if !r0.IsValid() && !r0.IsResolved() {
  391. if !r0.EqualFold(Name{}) {
  392. t.Errorf("expected invalid path to be zero value; got %#v", r0)
  393. }
  394. t.Skipf("invalid path: %q", s)
  395. }
  396. for _, p := range r0.parts {
  397. if len(p) > MaxNamePartLen {
  398. t.Errorf("part too long: %q", p)
  399. }
  400. }
  401. if !strings.EqualFold(r0.DisplayLong(), s) {
  402. t.Errorf("String() did not round-trip with case insensitivity: %q\ngot = %q\nwant = %q", s, r0.DisplayLong(), s)
  403. }
  404. r1 := ParseName(r0.DisplayLong(), FillNothing)
  405. if !r0.EqualFold(r1) {
  406. t.Errorf("round-trip mismatch: %+v != %+v", r0, r1)
  407. }
  408. })
  409. }
  410. func TestNameStringAllocs(t *testing.T) {
  411. name := ParseName("example.com/ns/mistral:latest+Q4_0", FillNothing)
  412. allocs := testing.AllocsPerRun(1000, func() {
  413. keep(name.DisplayLong())
  414. })
  415. if allocs > 1 {
  416. t.Errorf("String allocs = %v; want 0", allocs)
  417. }
  418. }
  419. func TestNamePath(t *testing.T) {
  420. cases := []struct {
  421. in string
  422. want string
  423. }{
  424. {"example.com/library/mistral:latest+Q4_0", "example.com/library/mistral:latest"},
  425. // incomplete
  426. {"example.com/library/mistral:latest", "example.com/library/mistral:latest"},
  427. {"", ""},
  428. }
  429. for _, tt := range cases {
  430. t.Run(tt.in, func(t *testing.T) {
  431. p := ParseName(tt.in, FillNothing)
  432. t.Logf("ParseName(%q) = %#v", tt.in, p)
  433. if g := p.URLPath(); g != tt.want {
  434. t.Errorf("got = %q; want %q", g, tt.want)
  435. }
  436. })
  437. }
  438. }
  439. func TestNameFilepath(t *testing.T) {
  440. cases := []struct {
  441. in string
  442. want string
  443. wantNoBuild string
  444. }{
  445. {
  446. in: "example.com/library/mistral:latest+Q4_0",
  447. want: "example.com/library/mistral/latest/Q4_0",
  448. wantNoBuild: "example.com/library/mistral/latest",
  449. },
  450. {
  451. in: "Example.Com/Library/Mistral:Latest+Q4_0",
  452. want: "example.com/library/mistral/latest/Q4_0",
  453. wantNoBuild: "example.com/library/mistral/latest",
  454. },
  455. {
  456. in: "Example.Com/Library/Mistral:Latest+Q4_0",
  457. want: "example.com/library/mistral/latest/Q4_0",
  458. wantNoBuild: "example.com/library/mistral/latest",
  459. },
  460. {
  461. in: "example.com/library/mistral:latest",
  462. want: "example.com/library/mistral/latest",
  463. wantNoBuild: "example.com/library/mistral/latest",
  464. },
  465. {
  466. in: "",
  467. want: "",
  468. wantNoBuild: "",
  469. },
  470. }
  471. for _, tt := range cases {
  472. t.Run(tt.in, func(t *testing.T) {
  473. p := ParseName(tt.in, FillNothing)
  474. t.Logf("ParseName(%q) = %#v", tt.in, p)
  475. g := p.Filepath()
  476. g = filepath.ToSlash(g)
  477. if g != tt.want {
  478. t.Errorf("got = %q; want %q", g, tt.want)
  479. }
  480. g = p.FilepathNoBuild()
  481. g = filepath.ToSlash(g)
  482. if g != tt.wantNoBuild {
  483. t.Errorf("got = %q; want %q", g, tt.wantNoBuild)
  484. }
  485. })
  486. }
  487. }
  488. func TestParseNameFilepath(t *testing.T) {
  489. cases := []struct {
  490. in string
  491. fill string // default is FillNothing
  492. want string
  493. }{
  494. {
  495. in: "example.com/library/mistral/latest/Q4_0",
  496. want: "example.com/library/mistral:latest+Q4_0",
  497. },
  498. {
  499. in: "example.com/library/mistral/latest",
  500. fill: "?/?/?:latest+Q4_0",
  501. want: "example.com/library/mistral:latest+Q4_0",
  502. },
  503. {
  504. in: "example.com/library/mistral",
  505. fill: "?/?/?:latest+Q4_0",
  506. want: "example.com/library/mistral:latest+Q4_0",
  507. },
  508. {
  509. in: "example.com/library",
  510. want: "",
  511. },
  512. {
  513. in: "example.com/",
  514. want: "",
  515. },
  516. {
  517. in: "example.com/^/mistral/latest/Q4_0",
  518. want: "",
  519. },
  520. {
  521. in: "example.com/library/mistral/../Q4_0",
  522. want: "",
  523. },
  524. {
  525. in: "example.com/library/mistral/latest/Q4_0/extra",
  526. want: "",
  527. },
  528. }
  529. for _, tt := range cases {
  530. t.Run(tt.in, func(t *testing.T) {
  531. in := strings.ReplaceAll(tt.in, "/", string(filepath.Separator))
  532. fill := cmp.Or(tt.fill, FillNothing)
  533. want := ParseName(tt.want, fill)
  534. if g := ParseNameFromFilepath(in, fill); !g.EqualFold(want) {
  535. t.Errorf("got = %q; want %q", g.DisplayLong(), tt.want)
  536. }
  537. })
  538. }
  539. }
  540. func TestParseNameFromPath(t *testing.T) {
  541. cases := []struct {
  542. in string
  543. want string
  544. fill string // default is FillNothing
  545. }{
  546. {
  547. in: "example.com/library/mistral:latest+Q4_0",
  548. want: "example.com/library/mistral:latest+Q4_0",
  549. },
  550. {
  551. in: "/example.com/library/mistral:latest+Q4_0",
  552. want: "example.com/library/mistral:latest+Q4_0",
  553. },
  554. {
  555. in: "/example.com/library/mistral",
  556. want: "example.com/library/mistral",
  557. },
  558. {
  559. in: "/example.com/library/mistral",
  560. fill: "?/?/?:latest+Q4_0",
  561. want: "example.com/library/mistral:latest+Q4_0",
  562. },
  563. {
  564. in: "/example.com/library",
  565. want: "",
  566. },
  567. {
  568. in: "/example.com/",
  569. want: "",
  570. },
  571. {
  572. in: "/example.com/^/mistral/latest",
  573. want: "",
  574. },
  575. }
  576. for _, tt := range cases {
  577. t.Run(tt.in, func(t *testing.T) {
  578. fill := cmp.Or(tt.fill, FillNothing)
  579. if g := ParseNameFromURLPath(tt.in, fill); g.DisplayLong() != tt.want {
  580. t.Errorf("got = %q; want %q", g.DisplayLong(), tt.want)
  581. }
  582. })
  583. }
  584. }
  585. func ExampleName_MapHash() {
  586. m := map[uint64]bool{}
  587. // key 1
  588. m[ParseName("mistral:latest+q4", FillNothing).MapHash()] = true
  589. m[ParseName("miSTRal:latest+Q4", FillNothing).MapHash()] = true
  590. m[ParseName("mistral:LATest+Q4", FillNothing).MapHash()] = true
  591. // key 2
  592. m[ParseName("mistral:LATest", FillNothing).MapHash()] = true
  593. fmt.Println(len(m))
  594. // Output:
  595. // 2
  596. }
  597. func ExampleName_CompareFold_sort() {
  598. names := []Name{
  599. ParseName("mistral:latest", FillNothing),
  600. ParseName("mistRal:7b+q4", FillNothing),
  601. ParseName("MIstral:7b", FillNothing),
  602. }
  603. slices.SortFunc(names, Name.CompareFold)
  604. for _, n := range names {
  605. fmt.Println(n.DisplayLong())
  606. }
  607. // Output:
  608. // MIstral:7b
  609. // mistRal:7b+q4
  610. // mistral:latest
  611. }
  612. func ExampleName_completeAndResolved() {
  613. for _, s := range []string{
  614. "x/y/z:latest+q4_0@sha123-1",
  615. "x/y/z:latest+q4_0",
  616. "@sha123-1",
  617. } {
  618. name := ParseName(s, FillNothing)
  619. fmt.Printf("complete:%v resolved:%v digest:%s\n", name.IsComplete(), name.IsResolved(), name.Digest())
  620. }
  621. // Output:
  622. // complete:true resolved:true digest:sha123-1
  623. // complete:true resolved:false digest:
  624. // complete:false resolved:true digest:sha123-1
  625. }
  626. func ExampleName_DisplayShortest() {
  627. name := ParseName("example.com/jmorganca/mistral:latest+Q4_0", FillNothing)
  628. fmt.Println(name.DisplayShortest("example.com/jmorganca/_:latest"))
  629. fmt.Println(name.DisplayShortest("example.com/_/_:latest"))
  630. fmt.Println(name.DisplayShortest("example.com/_/_:_"))
  631. fmt.Println(name.DisplayShortest("_/_/_:_"))
  632. // Default
  633. name = ParseName("registry.ollama.ai/library/mistral:latest+Q4_0", FillNothing)
  634. fmt.Println(name.DisplayShortest(""))
  635. // Output:
  636. // mistral
  637. // jmorganca/mistral
  638. // jmorganca/mistral:latest
  639. // example.com/jmorganca/mistral:latest
  640. // mistral
  641. }
  642. func keep[T any](v T) T { return v }