123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715 |
- package model
- import (
- "bytes"
- "cmp"
- "fmt"
- "log/slog"
- "path/filepath"
- "slices"
- "strings"
- "testing"
- )
- type fields struct {
- host, namespace, model, tag, build string
- digest string
- }
- func fieldsFromName(p Name) fields {
- return fields{
- host: p.parts[PartHost],
- namespace: p.parts[PartNamespace],
- model: p.parts[PartModel],
- tag: p.parts[PartTag],
- build: p.parts[PartBuild],
- digest: p.parts[PartDigest],
- }
- }
- var testNames = map[string]fields{
- "mistral:latest": {model: "mistral", tag: "latest"},
- "mistral": {model: "mistral"},
- "mistral:30B": {model: "mistral", tag: "30B"},
- "mistral:7b": {model: "mistral", tag: "7b"},
- "mistral:7b+Q4_0": {model: "mistral", tag: "7b", build: "Q4_0"},
- "mistral+KQED": {model: "mistral", build: "KQED"},
- "mistral.x-3:7b+Q4_0": {model: "mistral.x-3", tag: "7b", build: "Q4_0"},
- "mistral:7b+q4_0": {model: "mistral", tag: "7b", build: "q4_0"},
- "llama2": {model: "llama2"},
- "user/model": {namespace: "user", model: "model"},
- "example.com/ns/mistral:7b+Q4_0": {host: "example.com", namespace: "ns", model: "mistral", tag: "7b", build: "Q4_0"},
- "example.com/ns/mistral:7b+X": {host: "example.com", namespace: "ns", model: "mistral", tag: "7b", build: "X"},
- "localhost:5000/ns/mistral": {host: "localhost:5000", namespace: "ns", model: "mistral"},
- // invalid digest
- "mistral:latest@invalid256-": {},
- "mistral:latest@-123": {},
- "mistral:latest@!-123": {},
- "mistral:latest@1-!": {},
- "mistral:latest@": {},
- // resolved
- "x@sha123-1": {model: "x", digest: "sha123-1"},
- "@sha456-2": {digest: "sha456-2"},
- "@@sha123-1": {},
- // preserves case for build
- "x+b": {model: "x", build: "b"},
- // invalid (includes fuzzing trophies)
- " / / : + ": {},
- " / : + ": {},
- " : + ": {},
- " + ": {},
- " : ": {},
- " / ": {},
- " /": {},
- "/ ": {},
- "/": {},
- ":": {},
- "+": {},
- // (".") in namepsace is not allowed
- "invalid.com/7b+x": {},
- "invalid:7b+Q4_0:latest": {},
- "in valid": {},
- "invalid/y/z/foo": {},
- "/0": {},
- "0 /0": {},
- "0 /": {},
- "0/": {},
- ":/0": {},
- "+0/00000": {},
- "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": {},
- "0//0": {},
- "m+^^^": {},
- "file:///etc/passwd": {},
- "file:///etc/passwd:latest": {},
- "file:///etc/passwd:latest+u": {},
- ":x": {},
- "+x": {},
- "x+": {},
- // Disallow ("\.+") in any part to prevent path traversal anywhere
- // we convert the name to a path.
- "../etc/passwd": {},
- ".../etc/passwd": {},
- "./../passwd": {},
- "./0+..": {},
- strings.Repeat("a", MaxNamePartLen): {model: strings.Repeat("a", MaxNamePartLen)},
- strings.Repeat("a", MaxNamePartLen+1): {},
- }
- func TestIsValidNameLen(t *testing.T) {
- if IsValidNamePart(PartNamespace, strings.Repeat("a", MaxNamePartLen+1)) {
- t.Errorf("unexpectedly valid long name")
- }
- }
- // TestConsecutiveDots tests that consecutive dots are not allowed in any
- // part, to avoid path traversal. There also are some tests in testNames, but
- // this test is more exhaustive and exists to emphasize the importance of
- // preventing path traversal.
- func TestNameConsecutiveDots(t *testing.T) {
- for i := 1; i < 10; i++ {
- s := strings.Repeat(".", i)
- if i > 1 {
- if g := ParseName(s, FillNothing).DisplayLong(); g != "" {
- t.Errorf("ParseName(%q) = %q; want empty string", s, g)
- }
- } else {
- if g := ParseName(s, FillNothing).DisplayLong(); g != s {
- t.Errorf("ParseName(%q) = %q; want %q", s, g, s)
- }
- }
- }
- }
- func TestNameParts(t *testing.T) {
- var p Name
- if w, g := int(NumParts), len(p.parts); w != g {
- t.Errorf("Parts() = %d; want %d", g, w)
- }
- }
- func TestNamePartString(t *testing.T) {
- if g := PartKind(-2).String(); g != "Unknown" {
- t.Errorf("Unknown part = %q; want %q", g, "Unknown")
- }
- for kind, name := range kindNames {
- if g := kind.String(); g != name {
- t.Errorf("%s = %q; want %q", kind, g, name)
- }
- }
- }
- func TestParseName(t *testing.T) {
- for baseName, want := range testNames {
- for _, prefix := range []string{"", "https://", "http://"} {
- // We should get the same results with or without the
- // http(s) prefixes
- s := prefix + baseName
- t.Run(s, func(t *testing.T) {
- name := ParseName(s, FillNothing)
- got := fieldsFromName(name)
- if got != want {
- t.Errorf("ParseName(%q) = %q; want %q", s, got, want)
- }
- // test round-trip
- if !ParseName(name.DisplayLong(), FillNothing).EqualFold(name) {
- t.Errorf("ParseName(%q).String() = %s; want %s", s, name.DisplayLong(), baseName)
- }
- })
- }
- }
- }
- func TestParseNameFill(t *testing.T) {
- cases := []struct {
- in string
- fill string
- want string
- }{
- {"mistral", "example.com/library/?:latest+Q4_0", "example.com/library/mistral:latest+Q4_0"},
- {"mistral", "example.com/library/?:latest", "example.com/library/mistral:latest"},
- {"llama2:x", "example.com/library/?:latest+Q4_0", "example.com/library/llama2:x+Q4_0"},
- // Invalid
- {"", "example.com/library/?:latest+Q4_0", ""},
- {"llama2:?", "example.com/library/?:latest+Q4_0", ""},
- }
- for _, tt := range cases {
- t.Run(tt.in, func(t *testing.T) {
- name := ParseName(tt.in, tt.fill)
- if g := name.DisplayLong(); g != tt.want {
- t.Errorf("ParseName(%q, %q) = %q; want %q", tt.in, tt.fill, g, tt.want)
- }
- })
- }
- t.Run("invalid fill", func(t *testing.T) {
- defer func() {
- if recover() == nil {
- t.Fatal("expected panic")
- }
- }()
- ParseName("x", "^")
- })
- }
- func TestParseNameHTTPDoublePrefixStrip(t *testing.T) {
- cases := []string{
- "http://https://valid.com/valid/valid:latest",
- "https://http://valid.com/valid/valid:latest",
- }
- for _, s := range cases {
- t.Run(s, func(t *testing.T) {
- name := ParseName(s, FillNothing)
- if name.IsValid() {
- t.Errorf("expected invalid path; got %#v", name)
- }
- })
- }
- }
- func TestCompleteWithAndWithoutBuild(t *testing.T) {
- cases := []struct {
- in string
- complete bool
- completeNoBuild bool
- }{
- {"", false, false},
- {"incomplete/mistral:7b+x", false, false},
- {"incomplete/mistral:7b+Q4_0", false, false},
- {"incomplete:7b+x", false, false},
- {"complete.com/x/mistral:latest+Q4_0", true, true},
- {"complete.com/x/mistral:latest", false, true},
- }
- for _, tt := range cases {
- t.Run(tt.in, func(t *testing.T) {
- p := ParseName(tt.in, FillNothing)
- t.Logf("ParseName(%q) = %#v", tt.in, p)
- if g := p.IsComplete(); g != tt.complete {
- t.Errorf("Complete(%q) = %v; want %v", tt.in, g, tt.complete)
- }
- if g := p.IsCompleteNoBuild(); g != tt.completeNoBuild {
- t.Errorf("CompleteNoBuild(%q) = %v; want %v", tt.in, g, tt.completeNoBuild)
- }
- })
- }
- // Complete uses Parts which returns a slice, but it should be
- // inlined when used in Complete, preventing any allocations or
- // escaping to the heap.
- allocs := testing.AllocsPerRun(1000, func() {
- keep(ParseName("complete.com/x/mistral:latest+Q4_0", FillNothing).IsComplete())
- })
- if allocs > 0 {
- t.Errorf("Complete allocs = %v; want 0", allocs)
- }
- }
- func TestNameLogValue(t *testing.T) {
- cases := []string{
- "example.com/library/mistral:latest+Q4_0",
- "mistral:latest",
- "mistral:7b+Q4_0",
- }
- for _, s := range cases {
- t.Run(s, func(t *testing.T) {
- var b bytes.Buffer
- log := slog.New(slog.NewTextHandler(&b, nil))
- name := ParseName(s, FillNothing)
- log.Info("", "name", name)
- want := fmt.Sprintf("name=%s", name.GoString())
- got := b.String()
- if !strings.Contains(got, want) {
- t.Errorf("expected log output to contain %q; got %q", want, got)
- }
- })
- }
- }
- func TestNameGoString(t *testing.T) {
- cases := []struct {
- name string
- in string
- wantString string
- wantGoString string // default is tt.in
- }{
- {
- name: "Complete Name",
- in: "example.com/library/mistral:latest+Q4_0",
- wantGoString: "example.com/library/mistral:latest+Q4_0@?",
- },
- {
- name: "Short Name",
- in: "mistral:latest",
- wantGoString: "?/?/mistral:latest+?@?",
- },
- {
- name: "Long Name",
- in: "library/mistral:latest",
- wantGoString: "?/library/mistral:latest+?@?",
- },
- {
- name: "Case Preserved",
- in: "Library/Mistral:Latest",
- wantGoString: "?/Library/Mistral:Latest+?@?",
- },
- {
- name: "With digest",
- in: "Library/Mistral:Latest@sha256-123456",
- wantGoString: "?/Library/Mistral:Latest+?@sha256-123456",
- },
- }
- for _, tt := range cases {
- t.Run(tt.name, func(t *testing.T) {
- p := ParseName(tt.in, FillNothing)
- tt.wantGoString = cmp.Or(tt.wantGoString, tt.in)
- if g := fmt.Sprintf("%#v", p); g != tt.wantGoString {
- t.Errorf("GoString() = %q; want %q", g, tt.wantGoString)
- }
- })
- }
- }
- func TestDisplayLongest(t *testing.T) {
- g := ParseName("example.com/library/mistral:latest+Q4_0", FillNothing).DisplayLongest()
- if g != "example.com/library/mistral:latest" {
- t.Errorf("got = %q; want %q", g, "example.com/library/mistral:latest")
- }
- }
- func TestDisplayShortest(t *testing.T) {
- cases := []struct {
- in string
- mask string
- want string
- wantPanic bool
- }{
- {"example.com/library/mistral:latest+Q4_0", "example.com/library/_:latest", "mistral", false},
- {"example.com/library/mistral:latest+Q4_0", "example.com/_/_:latest", "library/mistral", false},
- {"example.com/library/mistral:latest+Q4_0", "", "example.com/library/mistral", false},
- {"example.com/library/mistral:latest+Q4_0", "", "example.com/library/mistral", false},
- // case-insensitive
- {"Example.com/library/mistral:latest+Q4_0", "example.com/library/_:latest", "mistral", false},
- {"example.com/Library/mistral:latest+Q4_0", "example.com/library/_:latest", "mistral", false},
- {"example.com/library/Mistral:latest+Q4_0", "example.com/library/_:latest", "Mistral", false},
- {"example.com/library/mistral:Latest+Q4_0", "example.com/library/_:latest", "mistral", false},
- {"example.com/library/mistral:Latest+q4_0", "example.com/library/_:latest", "mistral", false},
- // zero value
- {"", MaskDefault, "", true},
- // invalid mask
- {"example.com/library/mistral:latest+Q4_0", "example.com/mistral", "", true},
- // DefaultMask
- {"registry.ollama.ai/library/mistral:latest+Q4_0", MaskDefault, "mistral", false},
- // Auto-Fill
- {"x", "example.com/library/_:latest", "x", false},
- {"x", "example.com/library/_:latest+Q4_0", "x", false},
- {"x/y:z", "a.com/library/_:latest+Q4_0", "x/y:z", false},
- {"x/y:z", "a.com/library/_:latest+Q4_0", "x/y:z", false},
- }
- for _, tt := range cases {
- t.Run("", func(t *testing.T) {
- defer func() {
- if tt.wantPanic {
- if recover() == nil {
- t.Errorf("expected panic")
- }
- }
- }()
- p := ParseName(tt.in, FillNothing)
- t.Logf("ParseName(%q) = %#v", tt.in, p)
- if g := p.DisplayShortest(tt.mask); g != tt.want {
- t.Errorf("got = %q; want %q", g, tt.want)
- }
- })
- }
- }
- func TestParseNameAllocs(t *testing.T) {
- allocs := testing.AllocsPerRun(1000, func() {
- keep(ParseName("example.com/mistral:7b+Q4_0", FillNothing))
- })
- if allocs > 0 {
- t.Errorf("ParseName allocs = %v; want 0", allocs)
- }
- }
- func BenchmarkParseName(b *testing.B) {
- b.ReportAllocs()
- for range b.N {
- keep(ParseName("example.com/mistral:7b+Q4_0", FillNothing))
- }
- }
- func FuzzParseNameFromFilepath(f *testing.F) {
- f.Add("example.com/library/mistral/7b/Q4_0")
- f.Add("example.com/../mistral/7b/Q4_0")
- f.Add("example.com/x/../7b/Q4_0")
- f.Add("example.com/x/../7b")
- f.Fuzz(func(t *testing.T, s string) {
- name := ParseNameFromFilepath(s, FillNothing)
- if strings.Contains(s, "..") && !name.IsZero() {
- t.Fatalf("non-zero value for path with '..': %q", s)
- }
- if name.IsValid() == name.IsZero() {
- t.Errorf("expected valid path to be non-zero value; got %#v", name)
- }
- })
- }
- func FuzzParseName(f *testing.F) {
- f.Add("example.com/mistral:7b+Q4_0")
- f.Add("example.com/mistral:7b+q4_0")
- f.Add("example.com/mistral:7b+x")
- f.Add("x/y/z:8n+I")
- f.Add(":x")
- f.Add("@sha256-123456")
- f.Add("example.com/mistral:latest+Q4_0@sha256-123456")
- f.Add(":@!@")
- f.Add("...")
- f.Fuzz(func(t *testing.T, s string) {
- r0 := ParseName(s, FillNothing)
- if strings.Contains(s, "..") && !r0.IsZero() {
- t.Fatalf("non-zero value for path with '..': %q", s)
- }
- if !r0.IsValid() && !r0.IsResolved() {
- if !r0.EqualFold(Name{}) {
- t.Errorf("expected invalid path to be zero value; got %#v", r0)
- }
- t.Skipf("invalid path: %q", s)
- }
- for _, p := range r0.parts {
- if len(p) > MaxNamePartLen {
- t.Errorf("part too long: %q", p)
- }
- }
- if !strings.EqualFold(r0.DisplayLong(), s) {
- t.Errorf("String() did not round-trip with case insensitivity: %q\ngot = %q\nwant = %q", s, r0.DisplayLong(), s)
- }
- r1 := ParseName(r0.DisplayLong(), FillNothing)
- if !r0.EqualFold(r1) {
- t.Errorf("round-trip mismatch: %+v != %+v", r0, r1)
- }
- })
- }
- func TestNameStringAllocs(t *testing.T) {
- name := ParseName("example.com/ns/mistral:latest+Q4_0", FillNothing)
- allocs := testing.AllocsPerRun(1000, func() {
- keep(name.DisplayLong())
- })
- if allocs > 1 {
- t.Errorf("String allocs = %v; want 0", allocs)
- }
- }
- func TestNamePath(t *testing.T) {
- cases := []struct {
- in string
- want string
- }{
- {"example.com/library/mistral:latest+Q4_0", "example.com/library/mistral:latest"},
- // incomplete
- {"example.com/library/mistral:latest", "example.com/library/mistral:latest"},
- {"", ""},
- }
- for _, tt := range cases {
- t.Run(tt.in, func(t *testing.T) {
- p := ParseName(tt.in, FillNothing)
- t.Logf("ParseName(%q) = %#v", tt.in, p)
- if g := p.URLPath(); g != tt.want {
- t.Errorf("got = %q; want %q", g, tt.want)
- }
- })
- }
- }
- func TestNameFilepath(t *testing.T) {
- cases := []struct {
- in string
- want string
- wantNoBuild string
- }{
- {
- in: "example.com/library/mistral:latest+Q4_0",
- want: "example.com/library/mistral/latest/Q4_0",
- wantNoBuild: "example.com/library/mistral/latest",
- },
- {
- in: "Example.Com/Library/Mistral:Latest+Q4_0",
- want: "example.com/library/mistral/latest/Q4_0",
- wantNoBuild: "example.com/library/mistral/latest",
- },
- {
- in: "Example.Com/Library/Mistral:Latest+Q4_0",
- want: "example.com/library/mistral/latest/Q4_0",
- wantNoBuild: "example.com/library/mistral/latest",
- },
- {
- in: "example.com/library/mistral:latest",
- want: "example.com/library/mistral/latest",
- wantNoBuild: "example.com/library/mistral/latest",
- },
- {
- in: "",
- want: "",
- wantNoBuild: "",
- },
- }
- for _, tt := range cases {
- t.Run(tt.in, func(t *testing.T) {
- p := ParseName(tt.in, FillNothing)
- t.Logf("ParseName(%q) = %#v", tt.in, p)
- g := p.Filepath()
- g = filepath.ToSlash(g)
- if g != tt.want {
- t.Errorf("got = %q; want %q", g, tt.want)
- }
- g = p.FilepathNoBuild()
- g = filepath.ToSlash(g)
- if g != tt.wantNoBuild {
- t.Errorf("got = %q; want %q", g, tt.wantNoBuild)
- }
- })
- }
- }
- func TestParseNameFilepath(t *testing.T) {
- cases := []struct {
- in string
- fill string // default is FillNothing
- want string
- }{
- {
- in: "example.com/library/mistral/latest/Q4_0",
- want: "example.com/library/mistral:latest+Q4_0",
- },
- {
- in: "example.com/library/mistral/latest",
- fill: "?/?/?:latest+Q4_0",
- want: "example.com/library/mistral:latest+Q4_0",
- },
- {
- in: "example.com/library/mistral",
- fill: "?/?/?:latest+Q4_0",
- want: "example.com/library/mistral:latest+Q4_0",
- },
- {
- in: "example.com/library",
- want: "",
- },
- {
- in: "example.com/",
- want: "",
- },
- {
- in: "example.com/^/mistral/latest/Q4_0",
- want: "",
- },
- {
- in: "example.com/library/mistral/../Q4_0",
- want: "",
- },
- {
- in: "example.com/library/mistral/latest/Q4_0/extra",
- want: "",
- },
- }
- for _, tt := range cases {
- t.Run(tt.in, func(t *testing.T) {
- in := strings.ReplaceAll(tt.in, "/", string(filepath.Separator))
- fill := cmp.Or(tt.fill, FillNothing)
- want := ParseName(tt.want, fill)
- if g := ParseNameFromFilepath(in, fill); !g.EqualFold(want) {
- t.Errorf("got = %q; want %q", g.DisplayLong(), tt.want)
- }
- })
- }
- }
- func TestParseNameFromPath(t *testing.T) {
- cases := []struct {
- in string
- want string
- fill string // default is FillNothing
- }{
- {
- in: "example.com/library/mistral:latest+Q4_0",
- want: "example.com/library/mistral:latest+Q4_0",
- },
- {
- in: "/example.com/library/mistral:latest+Q4_0",
- want: "example.com/library/mistral:latest+Q4_0",
- },
- {
- in: "/example.com/library/mistral",
- want: "example.com/library/mistral",
- },
- {
- in: "/example.com/library/mistral",
- fill: "?/?/?:latest+Q4_0",
- want: "example.com/library/mistral:latest+Q4_0",
- },
- {
- in: "/example.com/library",
- want: "",
- },
- {
- in: "/example.com/",
- want: "",
- },
- {
- in: "/example.com/^/mistral/latest",
- want: "",
- },
- }
- for _, tt := range cases {
- t.Run(tt.in, func(t *testing.T) {
- fill := cmp.Or(tt.fill, FillNothing)
- if g := ParseNameFromURLPath(tt.in, fill); g.DisplayLong() != tt.want {
- t.Errorf("got = %q; want %q", g.DisplayLong(), tt.want)
- }
- })
- }
- }
- func ExampleName_MapHash() {
- m := map[uint64]bool{}
- // key 1
- m[ParseName("mistral:latest+q4", FillNothing).MapHash()] = true
- m[ParseName("miSTRal:latest+Q4", FillNothing).MapHash()] = true
- m[ParseName("mistral:LATest+Q4", FillNothing).MapHash()] = true
- // key 2
- m[ParseName("mistral:LATest", FillNothing).MapHash()] = true
- fmt.Println(len(m))
- // Output:
- // 2
- }
- func ExampleName_CompareFold_sort() {
- names := []Name{
- ParseName("mistral:latest", FillNothing),
- ParseName("mistRal:7b+q4", FillNothing),
- ParseName("MIstral:7b", FillNothing),
- }
- slices.SortFunc(names, Name.CompareFold)
- for _, n := range names {
- fmt.Println(n.DisplayLong())
- }
- // Output:
- // MIstral:7b
- // mistRal:7b+q4
- // mistral:latest
- }
- func ExampleName_completeAndResolved() {
- for _, s := range []string{
- "x/y/z:latest+q4_0@sha123-1",
- "x/y/z:latest+q4_0",
- "@sha123-1",
- } {
- name := ParseName(s, FillNothing)
- fmt.Printf("complete:%v resolved:%v digest:%s\n", name.IsComplete(), name.IsResolved(), name.Digest())
- }
- // Output:
- // complete:true resolved:true digest:sha123-1
- // complete:true resolved:false digest:
- // complete:false resolved:true digest:sha123-1
- }
- func ExampleName_DisplayShortest() {
- name := ParseName("example.com/jmorganca/mistral:latest+Q4_0", FillNothing)
- fmt.Println(name.DisplayShortest("example.com/jmorganca/_:latest"))
- fmt.Println(name.DisplayShortest("example.com/_/_:latest"))
- fmt.Println(name.DisplayShortest("example.com/_/_:_"))
- fmt.Println(name.DisplayShortest("_/_/_:_"))
- // Default
- name = ParseName("registry.ollama.ai/library/mistral:latest+Q4_0", FillNothing)
- fmt.Println(name.DisplayShortest(""))
- // Output:
- // mistral
- // jmorganca/mistral
- // jmorganca/mistral:latest
- // example.com/jmorganca/mistral:latest
- // mistral
- }
- func keep[T any](v T) T { return v }
|