name_test.go 19 KB

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