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