Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
OpenDAS
ollama
Commits
37f9c8ad
Unverified
Commit
37f9c8ad
authored
Apr 26, 2024
by
Blake Mizerany
Committed by
GitHub
Apr 26, 2024
Browse files
types/model: overhaul Name and Digest types (#3924)
parent
2a80f55e
Changes
11
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
528 additions
and
1412 deletions
+528
-1412
server/images.go
server/images.go
+9
-2
types/model/digest.go
types/model/digest.go
+0
-87
types/model/digest_test.go
types/model/digest_test.go
+0
-46
types/model/name.go
types/model/name.go
+297
-625
types/model/name_test.go
types/model/name_test.go
+221
-641
types/model/testdata/fuzz/FuzzName/d37463aa416f6bab
types/model/testdata/fuzz/FuzzName/d37463aa416f6bab
+1
-1
types/model/testdata/fuzz/FuzzParseRef/1d43ee52085cb4aa
types/model/testdata/fuzz/FuzzParseRef/1d43ee52085cb4aa
+0
-2
types/model/testdata/fuzz/FuzzParseRef/27fd759314f0e6d6
types/model/testdata/fuzz/FuzzParseRef/27fd759314f0e6d6
+0
-2
types/model/testdata/fuzz/FuzzParseRef/3e3b70dba384074d
types/model/testdata/fuzz/FuzzParseRef/3e3b70dba384074d
+0
-2
types/model/testdata/fuzz/FuzzParseRef/71f1fdff711b6dab
types/model/testdata/fuzz/FuzzParseRef/71f1fdff711b6dab
+0
-2
types/model/testdata/fuzz/FuzzParseRef/b51b1c875e61a948
types/model/testdata/fuzz/FuzzParseRef/b51b1c875e61a948
+0
-2
No files found.
server/images.go
View file @
37f9c8ad
...
@@ -703,17 +703,24 @@ func convertModel(name, path string, fn func(resp api.ProgressResponse)) (string
...
@@ -703,17 +703,24 @@ func convertModel(name, path string, fn func(resp api.ProgressResponse)) (string
}
}
func
CopyModel
(
src
,
dst
model
.
Name
)
error
{
func
CopyModel
(
src
,
dst
model
.
Name
)
error
{
if
!
dst
.
IsFullyQualified
()
{
return
model
.
Unqualified
(
dst
)
}
if
!
src
.
IsFullyQualified
()
{
return
model
.
Unqualified
(
src
)
}
manifests
,
err
:=
GetManifestPath
()
manifests
,
err
:=
GetManifestPath
()
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
}
}
dstpath
:=
filepath
.
Join
(
manifests
,
dst
.
Filepath
NoBuild
())
dstpath
:=
filepath
.
Join
(
manifests
,
dst
.
Filepath
())
if
err
:=
os
.
MkdirAll
(
filepath
.
Dir
(
dstpath
),
0
o755
);
err
!=
nil
{
if
err
:=
os
.
MkdirAll
(
filepath
.
Dir
(
dstpath
),
0
o755
);
err
!=
nil
{
return
err
return
err
}
}
srcpath
:=
filepath
.
Join
(
manifests
,
src
.
Filepath
NoBuild
())
srcpath
:=
filepath
.
Join
(
manifests
,
src
.
Filepath
())
srcfile
,
err
:=
os
.
Open
(
srcpath
)
srcfile
,
err
:=
os
.
Open
(
srcpath
)
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
...
...
types/model/digest.go
deleted
100644 → 0
View file @
2a80f55e
package
model
import
(
"fmt"
"log/slog"
"strings"
"unicode"
)
// Digest represents a digest of a model Manifest. It is a comparable value
// type and is immutable.
//
// The zero Digest is not a valid digest.
type
Digest
struct
{
s
string
}
// Split returns the digest type and the digest value.
func
(
d
Digest
)
Split
()
(
typ
,
digest
string
)
{
typ
,
digest
,
_
=
strings
.
Cut
(
d
.
s
,
"-"
)
return
}
// String returns the digest in the form of "<digest-type>-<digest>", or the
// empty string if the digest is invalid.
func
(
d
Digest
)
String
()
string
{
return
d
.
s
}
// IsValid returns true if the digest is valid (not zero).
//
// A valid digest may be created only by ParseDigest, or
// ParseName(name).Digest().
func
(
d
Digest
)
IsValid
()
bool
{
return
d
.
s
!=
""
}
// LogValue implements slog.Value.
func
(
d
Digest
)
LogValue
()
slog
.
Value
{
return
slog
.
StringValue
(
d
.
String
())
}
var
(
_
slog
.
LogValuer
=
Digest
{}
)
// ParseDigest parses a string in the form of "<digest-type>-<digest>" into a
// Digest.
func
ParseDigest
(
s
string
)
Digest
{
typ
,
digest
,
ok
:=
strings
.
Cut
(
s
,
"-"
)
if
!
ok
{
typ
,
digest
,
ok
=
strings
.
Cut
(
s
,
":"
)
}
if
ok
&&
isValidDigestType
(
typ
)
&&
isValidHex
(
digest
)
&&
len
(
digest
)
>=
2
{
return
Digest
{
s
:
fmt
.
Sprintf
(
"%s-%s"
,
typ
,
digest
)}
}
return
Digest
{}
}
func
MustParseDigest
(
s
string
)
Digest
{
d
:=
ParseDigest
(
s
)
if
!
d
.
IsValid
()
{
panic
(
fmt
.
Sprintf
(
"invalid digest: %q"
,
s
))
}
return
d
}
func
isValidDigestType
(
s
string
)
bool
{
if
len
(
s
)
==
0
{
return
false
}
for
_
,
r
:=
range
s
{
if
!
unicode
.
IsLower
(
r
)
&&
!
unicode
.
IsDigit
(
r
)
{
return
false
}
}
return
true
}
func
isValidHex
(
s
string
)
bool
{
if
len
(
s
)
==
0
{
return
false
}
for
i
:=
range
s
{
c
:=
s
[
i
]
if
c
<
'0'
||
c
>
'9'
&&
c
<
'a'
||
c
>
'f'
{
return
false
}
}
return
true
}
types/model/digest_test.go
deleted
100644 → 0
View file @
2a80f55e
package
model
import
"testing"
var
testDigests
=
map
[
string
]
Digest
{
""
:
{},
"sha256-1234"
:
{
s
:
"sha256-1234"
},
"sha256-5678"
:
{
s
:
"sha256-5678"
},
"blake2-9abc"
:
{
s
:
"blake2-9abc"
},
"-1234"
:
{},
"sha256-"
:
{},
"sha256-1234-5678"
:
{},
"sha256-P"
:
{},
// invalid hex
"sha256-1234P"
:
{},
"---"
:
{},
}
func
TestDigestParse
(
t
*
testing
.
T
)
{
// Test cases.
for
s
,
want
:=
range
testDigests
{
got
:=
ParseDigest
(
s
)
t
.
Logf
(
"ParseDigest(%q) = %#v"
,
s
,
got
)
if
got
!=
want
{
t
.
Errorf
(
"ParseDigest(%q) = %q; want %q"
,
s
,
got
,
want
)
}
}
}
func
TestDigestString
(
t
*
testing
.
T
)
{
// Test cases.
for
s
,
d
:=
range
testDigests
{
want
:=
s
if
!
d
.
IsValid
()
{
want
=
""
}
got
:=
d
.
String
()
if
got
!=
want
{
t
.
Errorf
(
"ParseDigest(%q).String() = %q; want %q"
,
s
,
got
,
want
)
}
got
=
ParseDigest
(
s
)
.
String
()
if
got
!=
want
{
t
.
Errorf
(
"roundtrip ParseDigest(%q).String() = %q; want %q"
,
s
,
got
,
want
)
}
}
}
types/model/name.go
View file @
37f9c8ad
// Package model contains types and utilities for parsing, validating, and
// working with model names and digests.
package
model
package
model
import
(
import
(
"cmp"
"cmp"
"encoding/hex"
"errors"
"errors"
"fmt"
"fmt"
"hash/maphash"
"io"
"log/slog"
"log/slog"
"path"
"path/filepath"
"path/filepath"
"slices"
"strings"
"strings"
"sync"
"github.com/ollama/ollama/types/structs"
)
)
// Errors
// Errors
var
(
var
(
// ErrInvalidName, ErrIncompleteName, and ErrInvalidDigest are not
// ErrUnqualifiedName represents an error where a name is not fully
// used by this package, but are exported so that other packages can
// qualified. It is not used directly in this package, but is here
// use them, instead of defining their own errors for them.
// to avoid other packages inventing their own error type.
ErrInvalidName
=
errors
.
New
(
"invalid model name"
)
// Additionally, it can be conveniently used via [Unqualified].
ErrIncompleteName
=
errors
.
New
(
"incomplete model name"
)
ErrUnqualifiedName
=
errors
.
New
(
"unqualified name"
)
ErrInvalidDigest
=
errors
.
New
(
"invalid digest"
)
)
// Defaults
const
(
// MaskDefault is the default mask used by [Name.DisplayShortest].
MaskDefault
=
"registry.ollama.ai/library/?:latest"
// MaskNothing is a mask that masks nothing.
MaskNothing
=
"?/?/?:?"
// DefaultFill is the default fill used by [ParseName].
FillDefault
=
"registry.ollama.ai/library/?:latest+Q4_0"
// FillNothing is a fill that fills nothing.
FillNothing
=
"?/?/?:?+?"
)
)
const
MaxNamePartLen
=
128
// Unqualified is a helper function that returns an error with
// ErrUnqualifiedName as the cause and the name as the message.
type
PartKind
int
func
Unqualified
(
n
Name
)
error
{
return
fmt
.
Errorf
(
"%w: %s"
,
ErrUnqualifiedName
,
n
)
// Levels of concreteness
const
(
// Each value aligns with its index in the Name.parts array.
PartHost
PartKind
=
iota
PartNamespace
PartModel
PartTag
PartBuild
PartDigest
// NumParts is the number of parts in a Name. In this list, it must
// follow the final part.
NumParts
PartExtraneous
=
-
1
)
var
kindNames
=
map
[
PartKind
]
string
{
PartHost
:
"Host"
,
PartNamespace
:
"Namespace"
,
PartModel
:
"Name"
,
PartTag
:
"Tag"
,
PartBuild
:
"Build"
,
PartDigest
:
"Digest"
,
}
}
func
(
k
PartKind
)
String
()
string
{
// MissingPart is used to indicate any part of a name that was "promised" by
return
cmp
.
Or
(
kindNames
[
k
],
"Unknown"
)
// the presence of a separator, but is missing.
}
// Name is an opaque reference to a model. It holds the parts of a model
// with the case preserved, but is not directly comparable with other Names
// since model names can be represented with different casing depending on
// the use case. For instance, "Mistral" and "mistral" are the same model
// but each version may have come from different sources (e.g. copied from a
// Web page, or from a file path).
//
// Valid Names can ONLY be constructed by calling [ParseName].
//
// A Name is valid if and only if is have a valid Model part. The other parts
// are optional.
//
// A Name is considered "complete" if it has all parts present. To check if a
// Name is complete, use [Name.IsComplete].
//
// To compare two names in a case-insensitive manner, use [Name.EqualFold].
//
// The parts of a Name are:
//
// - Host: the domain of the model (optional)
// - Namespace: the namespace of the model (optional)
// - Model: the name of the model (required)
// - Tag: the tag of the model (optional)
// - Build: the build of the model; usually the quantization or "file type" (optional)
//
//
// The parts can be obtained in their original form by calling [Name.Parts].
// The value was chosen because it is deemed unlikely to be set by a user,
//
// not a valid part name valid when checked by [Name.IsValid], and easy to
// To check if a Name has at minimum a valid model part, use [Name.IsValid].
// spot in logs.
type
Name
struct
{
const
MissingPart
=
"!MISSING!"
_
structs
.
Incomparable
parts
[
NumParts
]
string
// host, namespace, model, tag, build, digest
// TODO(bmizerany): track offsets and hold s (raw string) here? We
// could pack the offsets all into a single uint64 since the first
// parts take less bits since their max offset is less than the max
// offset of the next part. This would save a ton of bytes per Name
// and mean zero allocations for String.
}
// ParseName parses s into a Name, and returns the result of filling it with
// DefaultName returns a name with the default values for the host, namespace,
// defaults. The input string must be a valid string
// and tag parts. The model and digest parts are empty.
// representation of a model name in the form:
//
// [host/][namespace/]<model>[:tag][+build][@<digest-type>-<digest>]
//
// The name part is required, all others are optional. If a part is missing,
// it is left empty in the returned Name. If a part is invalid, the zero Ref
// value is returned.
//
// The build part is normalized to uppercase.
//
// Examples of valid paths:
//
// "example.com/library/mistral:7b+x"
// "example.com/eva/mistral:7b+Q4_0"
// "mistral:7b+x"
// "example.com/mike/mistral:latest+Q4_0"
// "example.com/bruce/mistral:latest"
// "example.com/pdevine/thisisfine:7b+Q4_0@sha256-1234567890abcdef"
//
// Examples of invalid paths:
//
// "example.com/mistral:7b+"
// "example.com/mistral:7b+Q4_0+"
// "x/y/z/z:8n+I"
// ""
//
// It returns the zero value if any part is invalid.
//
// # Fills
//
//
// For any valid s, the fill string is used to fill in missing parts of the
// - The default host is ("registry.ollama.ai")
// Name. The fill string must be a valid Name with the exception that any part
// - The default namespace is ("library")
// may be the string ("?"), which will not be considered for filling.
// - The default tag is ("latest")
func
ParseNameFill
(
s
,
fill
string
)
Name
{
func
DefaultName
()
Name
{
var
r
Name
return
Name
{
parts
(
s
)(
func
(
kind
PartKind
,
part
string
)
bool
{
Host
:
"registry.ollama.ai"
,
if
kind
==
PartDigest
&&
!
ParseDigest
(
part
)
.
IsValid
()
{
Namespace
:
"library"
,
r
=
Name
{}
Tag
:
"latest"
,
return
false
}
if
kind
==
PartExtraneous
||
!
IsValidNamePart
(
kind
,
part
)
{
r
=
Name
{}
return
false
}
}
r
.
parts
[
kind
]
=
part
return
true
})
if
r
.
IsValid
()
||
r
.
IsResolved
()
{
return
fillName
(
r
,
fill
)
}
return
Name
{}
}
}
// ParseName parses s into a Name, and returns the result of filling it
type
partKind
int
// with FillDefault. The input string must be a valid string representation
// of a model
func
ParseName
(
s
string
)
Name
{
return
ParseNameFill
(
s
,
""
)
}
func
parseMask
(
s
string
)
Name
{
const
(
var
r
Name
kindHost
partKind
=
iota
parts
(
s
)(
func
(
kind
PartKind
,
part
string
)
bool
{
kindNamespace
if
part
==
"?"
{
kindModel
// mask part; treat as empty but valid
kindTag
return
true
kindDigest
}
)
if
!
IsValidNamePart
(
kind
,
part
)
{
panic
(
fmt
.
Errorf
(
"invalid mask part %s: %q"
,
kind
,
part
))
}
r
.
parts
[
kind
]
=
part
return
true
})
return
r
}
func
MustParseName
(
s
,
fill
string
)
Name
{
func
(
k
partKind
)
String
()
string
{
r
:=
ParseNameFill
(
s
,
fill
)
switch
k
{
if
!
r
.
IsValid
()
{
case
kindHost
:
panic
(
"invalid Name: "
+
s
)
return
"host"
case
kindNamespace
:
return
"namespace"
case
kindModel
:
return
"model"
case
kindTag
:
return
"tag"
case
kindDigest
:
return
"digest"
default
:
return
"unknown"
}
}
return
r
}
}
// fillName fills in the missing parts of dst with the parts of src.
// Name is a structured representation of a model name string, as defined by
// [ParseNameNoDefaults].
//
//
// The returned Name will only be valid if dst is valid.
// It is not guaranteed to be valid. Use [Name.IsValid] to check if the name
// is valid.
//
//
// It skipps fill parts that are "?".
// It is not directly comparable with other Names. Use [Name.Equal] and
func
fillName
(
r
Name
,
fill
string
)
Name
{
// [Name.MapHash] for determining equality and using as a map key.
fill
=
cmp
.
Or
(
fill
,
FillDefault
)
type
Name
struct
{
f
:=
parseMask
(
fill
)
Host
string
if
fill
!=
FillNothing
&&
f
.
IsZero
()
{
Namespace
string
panic
(
"invalid fill"
)
Model
string
}
Tag
string
for
i
:=
range
r
.
parts
{
RawDigest
string
if
f
.
parts
[
i
]
==
"?"
{
}
continue
}
// ParseName parses and assembles a Name from a name string. The
r
.
parts
[
i
]
=
cmp
.
Or
(
r
.
parts
[
i
],
f
.
parts
[
i
])
// format of a valid name string is:
}
//
return
r
// s:
}
// { host } "/" { namespace } "/" { model } ":" { tag } "@" { digest }
// { host } "/" { namespace } "/" { model } ":" { tag }
// WithBuild returns a copy of r with the build set to the given string.
// { host } "/" { namespace } "/" { model } "@" { digest }
func
(
r
Name
)
WithBuild
(
build
string
)
Name
{
// { host } "/" { namespace } "/" { model }
r
.
parts
[
PartBuild
]
=
build
// { namespace } "/" { model } ":" { tag } "@" { digest }
return
r
// { namespace } "/" { model } ":" { tag }
}
// { namespace } "/" { model } "@" { digest }
// { namespace } "/" { model }
func
(
r
Name
)
WithDigest
(
digest
Digest
)
Name
{
// { model } ":" { tag } "@" { digest }
r
.
parts
[
PartDigest
]
=
digest
.
String
()
// { model } ":" { tag }
return
r
// { model } "@" { digest }
// { model }
// "@" { digest }
// host:
// pattern: alphanum { alphanum | "-" | "_" | "." | ":" }*
// length: [1, 350]
// namespace:
// pattern: alphanum { alphanum | "-" | "_" }*
// length: [2, 80]
// model:
// pattern: alphanum { alphanum | "-" | "_" | "." }*
// length: [2, 80]
// tag:
// pattern: alphanum { alphanum | "-" | "_" | "." }*
// length: [1, 80]
// digest:
// pattern: alphanum { alphanum | "-" | ":" }*
// length: [2, 80]
//
// Most users should use [ParseName] instead, unless need to support
// different defaults than DefaultName.
//
// The name returned is not guaranteed to be valid. If it is not valid, the
// field values are left in an undefined state. Use [Name.IsValid] to check
// if the name is valid.
func
ParseName
(
s
string
)
Name
{
return
merge
(
parseName
(
s
),
DefaultName
())
}
}
var
mapHashSeed
=
maphash
.
MakeSeed
()
// parseName is the same as [ParseName] without a merge.
func
parseName
(
s
string
)
Name
{
var
n
Name
var
promised
bool
// MapHash returns a case insensitive hash for use in maps and equality
s
,
n
.
RawDigest
,
promised
=
cutLast
(
s
,
"@"
)
// checks. For a convenient way to compare names, use [Name.EqualFold].
if
promised
&&
n
.
RawDigest
==
""
{
//
n
.
RawDigest
=
MissingPart
//nolint:errcheck
func
(
r
Name
)
MapHash
()
uint64
{
// correctly hash the parts with case insensitive comparison
var
h
maphash
.
Hash
h
.
SetSeed
(
mapHashSeed
)
for
_
,
part
:=
range
r
.
parts
{
// downcase the part for hashing
for
i
:=
range
part
{
c
:=
part
[
i
]
if
c
>=
'A'
&&
c
<=
'Z'
{
c
=
c
-
'A'
+
'a'
}
}
h
.
WriteByte
(
c
)
}
}
return
h
.
Sum64
()
}
func
(
r
Name
)
slice
(
from
,
to
PartKind
)
Name
{
s
,
n
.
Tag
,
_
=
cutPromised
(
s
,
":"
)
var
v
Name
s
,
n
.
Model
,
promised
=
cutPromised
(
s
,
"/"
)
copy
(
v
.
parts
[
from
:
to
+
1
],
r
.
parts
[
from
:
to
+
1
])
if
!
promised
{
return
v
n
.
Model
=
s
}
return
n
// DisplayShortest returns the shortest possible, masked display string in form:
//
// [host/][<namespace>/]<model>[:<tag>]
//
// # Masks
//
// The mask is a string that specifies which parts of the name to omit based
// on case-insensitive comparison. [Name.DisplayShortest] omits parts of the name
// that are the same as the mask, moving from left to right until the first
// unequal part is found. It then moves right to left until the first unequal
// part is found. The result is the shortest possible display string.
//
// Unlike a [Name] the mask can contain "?" characters which are treated as
// wildcards. A "?" will never match a part of the name, since a valid name
// can never contain a "?" character.
//
// For example: Given a Name ("registry.ollama.ai/library/mistral:latest") masked
// with ("registry.ollama.ai/library/?:latest") will produce the display string
// ("mistral").
//
// If mask is the empty string, then [MaskDefault] is used.
//
// DisplayShortest panics if the mask is not the empty string, MaskNothing, and
// invalid.
//
// # Builds
//
// For now, DisplayShortest does consider the build or return one in the
// result. We can lift this restriction when needed.
func
(
r
Name
)
DisplayShortest
(
mask
string
)
string
{
mask
=
cmp
.
Or
(
mask
,
MaskDefault
)
d
:=
parseMask
(
mask
)
if
mask
!=
MaskNothing
&&
r
.
IsZero
()
{
panic
(
"invalid Name"
)
}
}
for
i
:=
range
PartTag
{
s
,
n
.
Namespace
,
promised
=
cutPromised
(
s
,
"/"
)
if
!
strings
.
EqualFold
(
r
.
parts
[
i
],
d
.
parts
[
i
])
{
if
!
promised
{
break
n
.
Namespace
=
s
return
n
}
}
r
.
parts
[
i
]
=
""
n
.
Host
=
s
}
for
i
:=
PartTag
;
i
>=
0
;
i
--
{
return
n
if
!
strings
.
EqualFold
(
r
.
parts
[
i
],
d
.
parts
[
i
])
{
break
}
r
.
parts
[
i
]
=
""
}
return
r
.
slice
(
PartHost
,
PartTag
)
.
DisplayLong
()
}
}
// DisplayLongest returns the result of r.DisplayShortest(MaskNothing).
// merge merges the host, namespace, and tag parts of the two names,
func
(
r
Name
)
DisplayLongest
()
string
{
// preferring the non-empty parts of a.
return
r
.
DisplayShortest
(
MaskNothing
)
func
merge
(
a
,
b
Name
)
Name
{
a
.
Host
=
cmp
.
Or
(
a
.
Host
,
b
.
Host
)
a
.
Namespace
=
cmp
.
Or
(
a
.
Namespace
,
b
.
Namespace
)
a
.
Tag
=
cmp
.
Or
(
a
.
Tag
,
b
.
Tag
)
return
a
}
}
var
seps
=
[
...
]
string
{
// Digest returns the result of [ParseDigest] with the RawDigest field.
PartHost
:
"/"
,
func
(
n
Name
)
Digest
()
Digest
{
PartNamespace
:
"/"
,
return
ParseDigest
(
n
.
RawDigest
)
PartModel
:
":"
,
PartTag
:
"+"
,
PartBuild
:
"@"
,
PartDigest
:
""
,
}
}
// WriteTo implements io.WriterTo. It writes the fullest possible display
// String returns the name string, in the format that [ParseNameNoDefaults]
// string in form:
// accepts as valid, if [Name.IsValid] reports true; otherwise the empty
//
// string is returned.
// <host>/<namespace>/<model>:<tag>+<build>@<digest-type>-<digest>
func
(
n
Name
)
String
()
string
{
//
var
b
strings
.
Builder
// Missing parts and their separators are not written.
if
n
.
Host
!=
""
{
//
b
.
WriteString
(
n
.
Host
)
// The full digest is always prefixed with "@". That is if [Name.IsValid]
b
.
WriteByte
(
'/'
)
// reports false and [Name.IsResolved] reports true, then the string is
// returned as "@<digest-type>-<digest>".
func
(
r
Name
)
writeTo
(
w
io
.
StringWriter
)
error
{
var
partsWritten
int
for
i
:=
range
r
.
parts
{
if
r
.
parts
[
i
]
==
""
{
continue
}
if
partsWritten
>
0
||
i
==
int
(
PartDigest
)
{
if
_
,
err
:=
w
.
WriteString
(
seps
[
i
-
1
]);
err
!=
nil
{
return
err
}
}
if
n
.
Namespace
!=
""
{
b
.
WriteString
(
n
.
Namespace
)
b
.
WriteByte
(
'/'
)
}
}
if
_
,
err
:=
w
.
WriteString
(
r
.
parts
[
i
]);
err
!=
nil
{
b
.
WriteString
(
n
.
Model
)
return
err
if
n
.
Tag
!=
""
{
b
.
WriteByte
(
':'
)
b
.
WriteString
(
n
.
Tag
)
}
}
partsWritten
++
if
n
.
RawDigest
!=
""
{
b
.
WriteByte
(
'@'
)
b
.
WriteString
(
n
.
RawDigest
)
}
}
return
nil
}
var
builderPool
=
sync
.
Pool
{
New
:
func
()
interface
{}
{
return
&
strings
.
Builder
{}
},
}
// DisplayLong returns the fullest possible display string in form:
//
// <host>/<namespace>/<model>:<tag>+<build>
//
// If any part is missing, it is omitted from the display string.
func
(
r
Name
)
DisplayLong
()
string
{
b
:=
builderPool
.
Get
()
.
(
*
strings
.
Builder
)
defer
builderPool
.
Put
(
b
)
b
.
Reset
()
b
.
Grow
(
50
)
// arbitrarily long enough for most names
_
=
r
.
writeTo
(
b
)
return
b
.
String
()
return
b
.
String
()
}
}
// GoString implements fmt.GoStringer. It returns a string suitable for
// IsValid reports whether all parts of the name are present and valid. The
// debugging and logging. It is similar to [Name.DisplayLong] but it always
// digest is a special case, and is checked for validity only if present.
// returns a string that includes all parts of the Name, with missing parts
func
(
n
Name
)
IsValid
()
bool
{
// replaced with a ("?").
if
n
.
RawDigest
!=
""
&&
!
ParseDigest
(
n
.
RawDigest
)
.
IsValid
()
{
func
(
r
Name
)
GoString
()
string
{
return
false
for
i
:=
range
r
.
parts
{
r
.
parts
[
i
]
=
cmp
.
Or
(
r
.
parts
[
i
],
"?"
)
}
}
return
r
.
DisplayLong
()
return
n
.
IsFullyQualified
()
}
}
//
LogValue implements slog.Valuer.
//
IsFullyQualified returns true if all parts of the name are present and
func
(
r
Name
)
LogValue
()
slog
.
Value
{
// valid without the digest.
return
slog
.
StringValue
(
r
.
GoString
())
func
(
n
Name
)
IsFullyQualified
()
bool
{
}
var
parts
=
[]
string
{
n
.
Host
,
// IsComplete reports whether the Name is fully qualified. That is it has a
n
.
Namespace
,
// domain, namespace, name, tag, and build.
n
.
Model
,
func
(
r
Name
)
IsComplete
()
bool
{
n
.
Tag
,
return
!
slices
.
Contains
(
r
.
parts
[
:
PartDigest
],
""
)
}
}
for
i
,
part
:=
range
parts
{
if
!
isValidPart
(
partKind
(
i
),
part
)
{
// IsCompleteNoBuild is like [Name.IsComplete] but it does not require th
e
return
fals
e
// build part to be present.
}
func
(
r
Name
)
IsCompleteNoBuild
()
bool
{
}
return
!
slices
.
Contains
(
r
.
parts
[
:
PartBuild
],
""
)
return
true
}
}
// IsResolved reports true if the Name has a valid digest.
// Filepath returns a canonical filepath that represents the name with each part from
// host to tag as a directory in the form:
//
//
// It is possible to have a valid Name, or a complete Name that is not
// {host}/{namespace}/{model}/{tag}
// resolved.
func
(
r
Name
)
IsResolved
()
bool
{
return
r
.
Digest
()
.
IsValid
()
}
// Digest returns the digest part of the Name, if any.
//
//
// If Digest returns a non-empty string, then [Name.IsResolved] will return
// It uses the system's filepath separator and ensures the path is clean.
// true, and digest is considered valid.
func
(
r
Name
)
Digest
()
Digest
{
// This was already validated by ParseName, so we can just return it.
return
Digest
{
r
.
parts
[
PartDigest
]}
}
// EqualFold reports whether r and o are equivalent model names, ignoring
// case.
func
(
r
Name
)
EqualFold
(
o
Name
)
bool
{
return
r
.
CompareFold
(
o
)
==
0
}
// CompareFold performs a case-insensitive cmp.Compare on r and o.
//
// This can be used with [slices.SortFunc].
//
//
// For simple equality checks, use [Name.EqualFold].
// It panics if the name is not fully qualified. Use [Name.IsFullyQualified]
func
(
r
Name
)
CompareFold
(
o
Name
)
int
{
// to check if the name is fully qualified.
return
slices
.
CompareFunc
(
r
.
parts
[
:
],
o
.
parts
[
:
],
compareFold
)
func
(
n
Name
)
Filepath
()
string
{
if
!
n
.
IsFullyQualified
()
{
panic
(
"illegal attempt to get filepath of invalid name"
)
}
return
filepath
.
Join
(
strings
.
ToLower
(
n
.
Host
),
strings
.
ToLower
(
n
.
Namespace
),
strings
.
ToLower
(
n
.
Model
),
strings
.
ToLower
(
n
.
Tag
),
)
}
}
func
compareFold
(
a
,
b
string
)
int
{
// LogValue returns a slog.Value that represents the name as a string.
return
slices
.
CompareFunc
([]
rune
(
a
),
[]
rune
(
b
),
func
(
a
,
b
rune
)
int
{
func
(
n
Name
)
LogValue
()
slog
.
Value
{
return
cmp
.
Compare
(
downcase
(
a
),
downcase
(
b
))
return
slog
.
StringValue
(
n
.
String
())
})
}
}
func
downcase
(
r
rune
)
rune
{
func
isValidLen
(
kind
partKind
,
s
string
)
bool
{
if
r
>=
'A'
&&
r
<=
'Z'
{
switch
kind
{
return
r
-
'A'
+
'a'
case
kindHost
:
return
len
(
s
)
>=
1
&&
len
(
s
)
<=
350
case
kindTag
:
return
len
(
s
)
>=
1
&&
len
(
s
)
<=
80
default
:
return
len
(
s
)
>=
2
&&
len
(
s
)
<=
80
}
}
return
r
}
}
func
(
r
Name
)
Host
()
string
{
return
r
.
parts
[
PartHost
]
}
func
isValidPart
(
kind
partKind
,
s
string
)
bool
{
func
(
r
Name
)
Namespace
()
string
{
return
r
.
parts
[
PartNamespace
]
}
if
!
isValidLen
(
kind
,
s
)
{
func
(
r
Name
)
Model
()
string
{
return
r
.
parts
[
PartModel
]
}
return
false
func
(
r
Name
)
Build
()
string
{
return
r
.
parts
[
PartBuild
]
}
func
(
r
Name
)
Tag
()
string
{
return
r
.
parts
[
PartTag
]
}
// iter_Seq2 is a iter.Seq2 defined here to avoid the current build
// restrictions in the go1.22 iter package requiring the
// goexperiment.rangefunc tag to be set via the GOEXPERIMENT=rangefunc flag,
// which we are not yet ready to support.
//
// Once we are ready to support rangefunc, this can be removed and replaced
// with the iter.Seq2 type.
type
iter_Seq2
[
A
,
B
any
]
func
(
func
(
A
,
B
)
bool
)
// Parts returns a sequence of the parts of a Name string from most specific
// to least specific.
//
// It normalizes the input string by removing "http://" and "https://" only.
// No other normalizations are performed.
func
parts
(
s
string
)
iter_Seq2
[
PartKind
,
string
]
{
return
func
(
yield
func
(
PartKind
,
string
)
bool
)
{
if
strings
.
HasPrefix
(
s
,
"http://"
)
{
s
=
strings
.
TrimPrefix
(
s
,
"http://"
)
}
else
{
s
=
strings
.
TrimPrefix
(
s
,
"https://"
)
}
if
len
(
s
)
>
MaxNamePartLen
||
len
(
s
)
==
0
{
return
}
numConsecutiveDots
:=
0
partLen
:=
0
state
,
j
:=
PartDigest
,
len
(
s
)
for
i
:=
len
(
s
)
-
1
;
i
>=
0
;
i
--
{
if
partLen
++
;
partLen
>
MaxNamePartLen
{
// catch a part that is too long early, so
// we don't keep spinning on it, waiting for
// an isInValidPart check which would scan
// over it again.
yield
(
state
,
s
[
i
+
1
:
j
])
return
}
switch
s
[
i
]
{
case
'@'
:
switch
state
{
case
PartDigest
:
if
!
yield
(
PartDigest
,
s
[
i
+
1
:
j
])
{
return
}
}
for
i
:=
range
s
{
if
i
==
0
{
if
i
==
0
{
// This is the form
if
!
isAlphanumeric
(
s
[
i
])
{
// "@<digest>" which is valid.
return
false
//
// We're done.
return
}
state
,
j
,
partLen
=
PartBuild
,
i
,
0
default
:
yield
(
PartExtraneous
,
s
[
i
+
1
:
j
])
return
}
}
case
'+'
:
continue
switch
state
{
case
PartBuild
,
PartDigest
:
if
!
yield
(
PartBuild
,
s
[
i
+
1
:
j
])
{
return
}
}
state
,
j
,
partLen
=
PartTag
,
i
,
0
switch
s
[
i
]
{
default
:
case
'_'
,
'-'
:
yield
(
PartExtraneous
,
s
[
i
+
1
:
j
])
case
'.'
:
return
if
kind
==
kindNamespace
{
return
false
}
}
case
':'
:
case
':'
:
switch
state
{
if
kind
!=
kindHost
{
case
PartTag
,
PartBuild
,
PartDigest
:
return
false
if
!
yield
(
PartTag
,
s
[
i
+
1
:
j
])
{
return
}
state
,
j
,
partLen
=
PartModel
,
i
,
0
case
PartHost
:
// noop: support for host:port
default
:
yield
(
PartExtraneous
,
s
[
i
+
1
:
j
])
return
}
case
'/'
:
switch
state
{
case
PartModel
,
PartTag
,
PartBuild
,
PartDigest
:
if
!
yield
(
PartModel
,
s
[
i
+
1
:
j
])
{
return
}
state
,
j
=
PartNamespace
,
i
case
PartNamespace
:
if
!
yield
(
PartNamespace
,
s
[
i
+
1
:
j
])
{
return
}
state
,
j
,
partLen
=
PartHost
,
i
,
0
default
:
yield
(
PartExtraneous
,
s
[
i
+
1
:
j
])
return
}
}
default
:
default
:
if
s
[
i
]
==
'.'
{
if
!
isAlphanumeric
(
s
[
i
])
{
if
numConsecutiveDots
++
;
numConsecutiveDots
>
1
{
return
false
yield
(
state
,
""
)
return
}
}
else
{
numConsecutiveDots
=
0
}
}
}
}
if
state
<=
PartNamespace
{
yield
(
state
,
s
[
:
j
])
}
else
{
yield
(
PartModel
,
s
[
:
j
])
}
}
}
}
return
true
}
}
func
(
r
Name
)
IsZero
(
)
bool
{
func
isAlphanumeric
(
c
byte
)
bool
{
return
r
.
parts
==
[
NumParts
]
string
{}
return
c
>=
'A'
&&
c
<=
'Z'
||
c
>=
'a'
&&
c
<=
'z'
||
c
>=
'0'
&&
c
<=
'9'
}
}
// IsValid reports if a model has at minimum a valid model part.
func
cutLast
(
s
,
sep
string
)
(
before
,
after
string
,
ok
bool
)
{
func
(
r
Name
)
IsValid
()
bool
{
i
:=
strings
.
LastIndex
(
s
,
sep
)
// Parts ensures we only have valid parts, so no need to validate
if
i
>=
0
{
// them here, only check if we have a name or not.
return
s
[
:
i
],
s
[
i
+
len
(
sep
)
:
],
true
return
r
.
parts
[
PartModel
]
!=
""
}
return
s
,
""
,
false
}
}
// ParseNameFromURLPath parses forms of a URL path into a Name. Specifically,
// cutPromised cuts the last part of s at the last occurrence of sep. If sep is
// it trims any leading "/" and then calls [ParseName] with fill.
// found, the part before and after sep are returned as-is unless empty, in
func
ParseNameFromURLPath
(
s
,
fill
string
)
Name
{
// which case they are returned as MissingPart, which will cause
s
=
strings
.
TrimPrefix
(
s
,
"/"
)
// [Name.IsValid] to return false.
return
ParseNameFill
(
s
,
fill
)
func
cutPromised
(
s
,
sep
string
)
(
before
,
after
string
,
ok
bool
)
{
before
,
after
,
ok
=
cutLast
(
s
,
sep
)
if
!
ok
{
return
before
,
after
,
false
}
return
cmp
.
Or
(
before
,
MissingPart
),
cmp
.
Or
(
after
,
MissingPart
),
true
}
}
func
ParseNameFromURLPathFill
(
s
,
fill
string
)
Name
{
type
DigestType
int
return
ParseNameFill
(
s
,
fill
)
}
// URLPath returns a complete, canonicalized, relative URL path using the parts of a
const
(
// complete Name.
DigestTypeInvalid
DigestType
=
iota
//
DigestTypeSHA256
// The parts maintain their original case.
)
//
// Example:
func
(
t
DigestType
)
String
()
string
{
//
if
t
==
DigestTypeSHA256
{
// ParseName("example.com/namespace/model:tag+build").URLPath() //
return
s
"
/example.com/namespace/model:tag
"
return
"
sha256
"
func
(
r
Name
)
DisplayURLPath
()
string
{
}
return
r
.
DisplayShortest
(
MaskNothing
)
return
"unknown"
}
}
// URLPath returns a complete, canonicalized, relative URL path using the parts of a
// Digest represents a type and hash of a digest. It is comparable and can
// complete Name in the form:
// be used as a map key.
//
type
Digest
struct
{
// <host>/<namespace>/<model>/<tag>
Type
DigestType
//
Hash
[
32
]
byte
// The parts are downcased.
func
(
r
Name
)
URLPath
()
string
{
return
strings
.
ToLower
(
path
.
Join
(
r
.
parts
[
:
PartBuild
]
...
))
}
}
// Parse
NameFromFilepath parses a file path into a Name. The input string must be a
// Parse
Digest parses a digest string into a Digest struct. It accepts both
//
valid file path representation of a model name in
the form:
// the form
s
:
//
//
// host/namespace/model/tag/build
// sha256:deadbeef
// sha256-deadbeef
//
//
// The zero valid is returned if s does not contain all path elements
// The hash part must be exactly 64 characters long.
// leading up to the model part, or if any path element is an invalid part
// for the its corresponding part kind.
//
//
// The fill string is used to fill in missing parts of any constructed Name.
// The form "type:hash" does not round trip through [Digest.String].
// See [ParseName] for more information on the fill string.
func
ParseDigest
(
s
string
)
Digest
{
func
ParseNameFromFilepath
(
s
,
fill
string
)
Name
{
typ
,
hash
,
ok
:=
cutLast
(
s
,
":"
)
var
r
Name
if
!
ok
{
for
i
:=
range
PartBuild
+
1
{
typ
,
hash
,
ok
=
cutLast
(
s
,
"-"
)
part
,
rest
,
_
:=
strings
.
Cut
(
s
,
string
(
filepath
.
Separator
))
if
!
ok
{
if
!
IsValidNamePart
(
i
,
part
)
{
return
Digest
{}
return
Name
{}
}
r
.
parts
[
i
]
=
part
s
=
rest
if
s
==
""
{
break
}
}
if
s
!=
""
{
return
Name
{}
}
}
if
!
r
.
IsValid
()
{
return
Name
{}
}
}
return
fillName
(
r
,
fill
)
if
typ
!=
"sha256"
{
}
return
Digest
{}
// Filepath returns a complete, canonicalized, relative file path using the
// parts of a complete Name.
//
// Each parts is downcased, except for the build part which is upcased.
//
// Example:
//
// ParseName("example.com/namespace/model:tag+build").Filepath() // returns "example.com/namespace/model/tag/BUILD"
func
(
r
Name
)
Filepath
()
string
{
for
i
:=
range
r
.
parts
{
if
PartKind
(
i
)
==
PartBuild
{
r
.
parts
[
i
]
=
strings
.
ToUpper
(
r
.
parts
[
i
])
}
else
{
r
.
parts
[
i
]
=
strings
.
ToLower
(
r
.
parts
[
i
])
}
}
var
d
Digest
n
,
err
:=
hex
.
Decode
(
d
.
Hash
[
:
],
[]
byte
(
hash
))
if
err
!=
nil
||
n
!=
32
{
return
Digest
{}
}
}
return
filepath
.
Join
(
r
.
parts
[
:
]
...
)
return
Digest
{
Type
:
DigestTypeSHA256
,
Hash
:
d
.
Hash
}
}
}
// FilepathNoBuild returns a complete, canonicalized, relative file path using
// IsValid returns true if the digest has a valid Type and Hash.
// the parts of a complete Name, but without the build part.
func
(
d
Digest
)
IsValid
()
bool
{
func
(
r
Name
)
FilepathNoBuild
()
string
{
if
d
.
Type
!=
DigestTypeSHA256
{
for
i
:=
range
PartBuild
{
return
false
r
.
parts
[
i
]
=
strings
.
ToLower
(
r
.
parts
[
i
])
}
}
return
filepath
.
Join
(
r
.
parts
[
:
PartBuild
]
...
)
return
d
.
Hash
!=
[
32
]
byte
{}
}
}
// IsValidNamePart reports if s contains all valid characters for the given
// String returns the digest as a string in the form "type-hash". The hash
// part kind and is under MaxNamePartLen bytes.
// is encoded as a hex string.
func
IsValidNamePart
(
kind
PartKind
,
s
string
)
bool
{
func
(
d
Digest
)
String
()
string
{
if
len
(
s
)
>
MaxNamePartLen
{
var
b
strings
.
Builder
return
false
b
.
WriteString
(
d
.
Type
.
String
())
}
b
.
WriteByte
(
'-'
)
if
s
==
""
{
b
.
WriteString
(
hex
.
EncodeToString
(
d
.
Hash
[
:
]))
return
false
return
b
.
String
()
}
var
consecutiveDots
int
for
_
,
c
:=
range
[]
byte
(
s
)
{
if
c
==
'.'
{
if
consecutiveDots
++
;
consecutiveDots
>=
2
{
return
false
}
}
else
{
consecutiveDots
=
0
}
if
!
isValidByteFor
(
kind
,
c
)
{
return
false
}
}
return
true
}
}
func
isValidByteFor
(
kind
PartKind
,
c
byte
)
bool
{
// LogValue returns a slog.Value that represents the digest as a string.
if
kind
==
PartNamespace
&&
c
==
'.'
{
func
(
d
Digest
)
LogValue
()
slog
.
Value
{
return
false
return
slog
.
StringValue
(
d
.
String
())
}
if
kind
==
PartHost
&&
c
==
':'
{
return
true
}
if
c
==
'.'
||
c
==
'-'
{
return
true
}
if
c
>=
'a'
&&
c
<=
'z'
||
c
>=
'A'
&&
c
<=
'Z'
||
c
>=
'0'
&&
c
<=
'9'
||
c
==
'_'
{
return
true
}
return
false
}
}
types/model/name_test.go
View file @
37f9c8ad
package
model
package
model
import
(
import
(
"bytes"
"reflect"
"cmp"
"fmt"
"log/slog"
"path/filepath"
"slices"
"strings"
"strings"
"testing"
"testing"
)
)
type
fields
struct
{
const
(
host
,
namespace
,
model
,
tag
,
build
string
part80
=
"88888888888888888888888888888888888888888888888888888888888888888888888888888888"
digest
string
part350
=
"33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333"
}
)
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-12"
:
{
model
:
"x"
,
digest
:
"sha123-12"
},
"@sha456-22"
:
{
digest
:
"sha456-22"
},
"@sha456-1"
:
{},
"@@sha123-22"
:
{},
// 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\x9d
00000
\xe5\x99\xe6\xd9
00
\xd9
0
\xa6
0
\x91\xdc
0
\xff\xbf\x99\xe8
00
\xb9\xdc\xd6\xc3
00
\x97
0
\xfb\xfd
0
\xe0\x8a\xe1\xad\xd4
0
\x97
00
\xa8
0
\x98
0
\xdd
0000
\xb0
0
\x91
000
\xfe
0
\x89\x9b\x90\x93\x9f
0
\xe6
0
\xf7\x84\xb0\x87\xa5\xff
0
\xa0
00
\x9a\x85\xf6\x85\xfe\xa9\xf9\xe9\xde
00
\xf4\xe0\x8f\x81\xad\xde
00
\xd7
00
\xaa\xe0
00000
\xb1\xee
0
\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
:=
ParseNameFill
(
s
,
FillNothing
)
.
DisplayLong
();
g
!=
""
{
t
.
Errorf
(
"ParseName(%q) = %q; want empty string"
,
s
,
g
)
}
}
else
{
if
g
:=
ParseNameFill
(
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
:=
ParseNameFill
(
s
,
FillNothing
)
got
:=
fieldsFromName
(
name
)
if
got
!=
want
{
t
.
Errorf
(
"ParseName(%q) = %q; want %q"
,
s
,
got
,
want
)
}
// test round-trip
if
!
ParseNameFill
(
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
:=
ParseNameFill
(
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"
)
}
}()
ParseNameFill
(
"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
:=
ParseNameFill
(
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
:=
ParseNameFill
(
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
(
ParseNameFill
(
"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
:=
ParseNameFill
(
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
Test
NameGoString
(
t
*
testing
.
T
)
{
func
Test
ParseNameParts
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
cases
:=
[]
struct
{
name
string
in
string
in
string
want
String
string
want
Name
want
GoString
string
// default is tt.in
want
ValidDigest
bool
}{
}{
{
{
name
:
"Complete Name"
,
in
:
"host/namespace/model:tag"
,
in
:
"example.com/library/mistral:latest+Q4_0"
,
want
:
Name
{
wantGoString
:
"example.com/library/mistral:latest+Q4_0@?"
,
Host
:
"host"
,
Namespace
:
"namespace"
,
Model
:
"model"
,
Tag
:
"tag"
,
},
},
{
in
:
"host/namespace/model"
,
want
:
Name
{
Host
:
"host"
,
Namespace
:
"namespace"
,
Model
:
"model"
,
},
},
{
in
:
"namespace/model"
,
want
:
Name
{
Namespace
:
"namespace"
,
Model
:
"model"
,
},
},
{
in
:
"model"
,
want
:
Name
{
Model
:
"model"
,
},
},
{
in
:
"h/nn/mm:t"
,
want
:
Name
{
Host
:
"h"
,
Namespace
:
"nn"
,
Model
:
"mm"
,
Tag
:
"t"
,
},
},
},
{
{
name
:
"Short Name"
,
in
:
part80
+
"/"
+
part80
+
"/"
+
part80
+
":"
+
part80
,
in
:
"mistral:latest"
,
want
:
Name
{
wantGoString
:
"?/?/mistral:latest+?@?"
,
Host
:
part80
,
Namespace
:
part80
,
Model
:
part80
,
Tag
:
part80
,
},
},
},
{
{
name
:
"Long Name"
,
in
:
part350
+
"/"
+
part80
+
"/"
+
part80
+
":"
+
part80
,
in
:
"library/mistral:latest"
,
want
:
Name
{
wantGoString
:
"?/library/mistral:latest+?@?"
,
Host
:
part350
,
Namespace
:
part80
,
Model
:
part80
,
Tag
:
part80
,
},
},
},
{
{
name
:
"Case Preserved"
,
in
:
"@digest"
,
in
:
"Library/Mistral:Latest"
,
want
:
Name
{
wantGoString
:
"?/Library/Mistral:Latest+?@?"
,
RawDigest
:
"digest"
,
},
wantValidDigest
:
false
,
},
},
{
{
name
:
"With digest"
,
in
:
"model@sha256:"
+
validSHA256Hex
,
in
:
"Library/Mistral:Latest@sha256-123456"
,
want
:
Name
{
wantGoString
:
"?/Library/Mistral:Latest+?@sha256-123456"
,
Model
:
"model"
,
RawDigest
:
"sha256:"
+
validSHA256Hex
,
},
wantValidDigest
:
true
,
},
},
}
}
for
_
,
tt
:=
range
cases
{
for
_
,
tt
:=
range
cases
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
t
.
Run
(
tt
.
in
,
func
(
t
*
testing
.
T
)
{
p
:=
ParseNameFill
(
tt
.
in
,
FillNothing
)
got
:=
parseName
(
tt
.
in
)
tt
.
wantGoString
=
cmp
.
Or
(
tt
.
wantGoString
,
tt
.
in
)
if
!
reflect
.
DeepEqual
(
got
,
tt
.
want
)
{
if
g
:=
fmt
.
Sprintf
(
"%#v"
,
p
);
g
!=
tt
.
wantGoString
{
t
.
Errorf
(
"parseName(%q) = %v; want %v"
,
tt
.
in
,
got
,
tt
.
want
)
t
.
Errorf
(
"GoString() = %q; want %q"
,
g
,
tt
.
wantGoString
)
}
if
got
.
Digest
()
.
IsValid
()
!=
tt
.
wantValidDigest
{
t
.
Errorf
(
"parseName(%q).Digest().IsValid() = %v; want %v"
,
tt
.
in
,
got
.
Digest
()
.
IsValid
(),
tt
.
wantValidDigest
)
}
}
})
})
}
}
}
}
func
TestDisplayLongest
(
t
*
testing
.
T
)
{
var
testCases
=
map
[
string
]
bool
{
// name -> valid
g
:=
ParseNameFill
(
"example.com/library/mistral:latest+Q4_0"
,
FillNothing
)
.
DisplayLongest
()
"host/namespace/model:tag"
:
true
,
if
g
!=
"example.com/library/mistral:latest"
{
"host/namespace/model"
:
false
,
t
.
Errorf
(
"got = %q; want %q"
,
g
,
"example.com/library/mistral:latest"
)
"namespace/model"
:
false
,
}
"model"
:
false
,
"@sha256-1000000000000000000000000000000000000000000000000000000000000000"
:
false
,
"model@sha256-1000000000000000000000000000000000000000000000000000000000000000"
:
false
,
"model@sha256:1000000000000000000000000000000000000000000000000000000000000000"
:
false
,
// long (but valid)
part80
+
"/"
+
part80
+
"/"
+
part80
+
":"
+
part80
:
true
,
part350
+
"/"
+
part80
+
"/"
+
part80
+
":"
+
part80
:
true
,
"h/nn/mm:t@sha256-1000000000000000000000000000000000000000000000000000000000000000"
:
true
,
// bare minimum part sizes
"h/nn/mm:t@sha256:1000000000000000000000000000000000000000000000000000000000000000"
:
true
,
// bare minimum part sizes
"m"
:
false
,
// model too short
"n/mm:"
:
false
,
// namespace too short
"h/n/mm:t"
:
false
,
// namespace too short
"@t"
:
false
,
// digest too short
"mm@d"
:
false
,
// digest too short
// invalids
"^"
:
false
,
"mm:"
:
false
,
"/nn/mm"
:
false
,
"//"
:
false
,
"//mm"
:
false
,
"hh//"
:
false
,
"//mm:@"
:
false
,
"00@"
:
false
,
"@"
:
false
,
// not starting with alphanum
"-hh/nn/mm:tt@dd"
:
false
,
"hh/-nn/mm:tt@dd"
:
false
,
"hh/nn/-mm:tt@dd"
:
false
,
"hh/nn/mm:-tt@dd"
:
false
,
"hh/nn/mm:tt@-dd"
:
false
,
""
:
false
,
// hosts
"host:https/namespace/model:tag"
:
true
,
// colon in non-host part before tag
"host/name:space/model:tag"
:
false
,
}
}
func
TestDisplayShortest
(
t
*
testing
.
T
)
{
func
TestNameparseNameDefault
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
const
name
=
"xx"
in
string
n
:=
ParseName
(
name
)
mask
string
got
:=
n
.
String
()
want
string
want
:=
"registry.ollama.ai/library/xx:latest"
wantPanic
bool
if
got
!=
want
{
}{
t
.
Errorf
(
"parseName(%q).String() = %q; want %q"
,
name
,
got
,
want
)
{
"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
{
func
TestNameIsValid
(
t
*
testing
.
T
)
{
t
.
Run
(
""
,
func
(
t
*
testing
.
T
)
{
var
numStringTests
int
defer
func
()
{
for
s
,
want
:=
range
testCases
{
if
tt
.
wantPanic
{
n
:=
parseName
(
s
)
if
recover
()
==
nil
{
t
.
Logf
(
"n: %#v"
,
n
)
t
.
Errorf
(
"expected panic"
)
got
:=
n
.
IsValid
()
}
if
got
!=
want
{
t
.
Errorf
(
"parseName(%q).IsValid() = %v; want %v"
,
s
,
got
,
want
)
}
}
}()
p
:=
ParseNameFill
(
tt
.
in
,
FillNothing
)
// Test roundtrip with String
t
.
Logf
(
"ParseName(%q) = %#v"
,
tt
.
in
,
p
)
if
got
{
if
g
:=
p
.
DisplayShortest
(
tt
.
mask
);
g
!=
tt
.
want
{
got
:=
parseName
(
s
)
.
String
()
t
.
Errorf
(
"got = %q; want %q"
,
g
,
tt
.
want
)
if
got
!=
s
{
t
.
Errorf
(
"parseName(%q).String() = %q; want %q"
,
s
,
got
,
s
)
}
}
})
numStringTests
++
}
}
}
func
TestParseNameAllocs
(
t
*
testing
.
T
)
{
allocs
:=
testing
.
AllocsPerRun
(
1000
,
func
()
{
keep
(
ParseNameFill
(
"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
{
if
numStringTests
==
0
{
keep
(
ParseNameFill
(
"example.com/mistral:7b+Q4_0"
,
FillNoth
ing
)
)
t
.
Errorf
(
"no tests for Name.Str
ing
"
)
}
}
}
}
func
FuzzParseNameFromFilepath
(
f
*
testing
.
F
)
{
func
TestNameIsValidPart
(
t
*
testing
.
T
)
{
f
.
Add
(
"example.com/library/mistral/7b/Q4_0"
)
cases
:=
[]
struct
{
f
.
Add
(
"example.com/../mistral/7b/Q4_0"
)
kind
partKind
f
.
Add
(
"example.com/x/../7b/Q4_0"
)
s
string
f
.
Add
(
"example.com/x/../7b"
)
want
bool
f
.
Fuzz
(
func
(
t
*
testing
.
T
,
s
string
)
{
}{
name
:=
ParseNameFromFilepath
(
s
,
FillNothing
)
{
kind
:
kindHost
,
s
:
""
,
want
:
false
},
if
strings
.
Contains
(
s
,
".."
)
&&
!
name
.
IsZero
()
{
{
kind
:
kindHost
,
s
:
"a"
,
want
:
true
},
t
.
Fatalf
(
"non-zero value for path with '..': %q"
,
s
)
{
kind
:
kindHost
,
s
:
"a."
,
want
:
true
},
{
kind
:
kindHost
,
s
:
"a.b"
,
want
:
true
},
{
kind
:
kindHost
,
s
:
"a:123"
,
want
:
true
},
{
kind
:
kindHost
,
s
:
"a:123/aa/bb"
,
want
:
false
},
{
kind
:
kindNamespace
,
s
:
"bb"
,
want
:
true
},
{
kind
:
kindNamespace
,
s
:
"a."
,
want
:
false
},
{
kind
:
kindModel
,
s
:
"-h"
,
want
:
false
},
{
kind
:
kindDigest
,
s
:
"sha256-1000000000000000000000000000000000000000000000000000000000000000"
,
want
:
true
},
}
}
if
name
.
IsValid
()
==
name
.
IsZero
()
{
for
_
,
tt
:=
range
cases
{
t
.
Errorf
(
"expected valid path to be non-zero value; got %#v"
,
name
)
t
.
Run
(
tt
.
s
,
func
(
t
*
testing
.
T
)
{
got
:=
isValidPart
(
tt
.
kind
,
tt
.
s
)
if
got
!=
tt
.
want
{
t
.
Errorf
(
"isValidPart(%s, %q) = %v; want %v"
,
tt
.
kind
,
tt
.
s
,
got
,
tt
.
want
)
}
}
})
})
}
}
}
func
FuzzParseName
(
f
*
testing
.
F
)
{
func
FuzzName
(
f
*
testing
.
F
)
{
f
.
Add
(
"example.com/mistral:7b+Q4_0"
)
for
s
:=
range
testCases
{
f
.
Add
(
"example.com/mistral:7b+q4_0"
)
f
.
Add
(
s
)
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
)
{
f
.
Fuzz
(
func
(
t
*
testing
.
T
,
s
string
)
{
r0
:=
ParseNameFill
(
s
,
FillNothing
)
n
:=
parseName
(
s
)
if
n
.
IsValid
()
{
if
strings
.
Contains
(
s
,
".."
)
&&
!
r0
.
IsZero
()
{
parts
:=
[
...
]
string
{
n
.
Host
,
n
.
Namespace
,
n
.
Model
,
n
.
Tag
,
n
.
RawDigest
}
t
.
Fatalf
(
"non-zero value for path with '..': %q"
,
s
)
for
_
,
part
:=
range
parts
{
if
part
==
".."
{
t
.
Errorf
(
"unexpected .. as valid part"
)
}
}
if
len
(
part
)
>
350
{
if
!
r0
.
IsValid
()
&&
!
r0
.
IsResolved
()
{
t
.
Errorf
(
"part too long: %q"
,
part
)
if
!
r0
.
EqualFold
(
Name
{})
{
t
.
Errorf
(
"expected invalid path to be zero value; got %#v"
,
r0
)
}
}
t
.
Skipf
(
"invalid path: %q"
,
s
)
}
}
if
n
.
String
()
!=
s
{
for
_
,
p
:=
range
r0
.
parts
{
t
.
Errorf
(
"String() = %q; want %q"
,
n
.
String
(),
s
)
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
\n
got = %q
\n
want = %q"
,
s
,
r0
.
DisplayLong
(),
s
)
}
r1
:=
ParseNameFill
(
r0
.
DisplayLong
(),
FillNothing
)
if
!
r0
.
EqualFold
(
r1
)
{
t
.
Errorf
(
"round-trip mismatch: %+v != %+v"
,
r0
,
r1
)
}
})
})
}
}
func
TestNameStringAllocs
(
t
*
testing
.
T
)
{
const
validSHA256Hex
=
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
name
:=
ParseNameFill
(
"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
)
{
func
TestParseDigest
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
cases
:=
map
[
string
]
bool
{
in
string
"sha256-1000000000000000000000000000000000000000000000000000000000000000"
:
true
,
want
string
"sha256:1000000000000000000000000000000000000000000000000000000000000000"
:
true
,
}{
"sha256:0000000000000000000000000000000000000000000000000000000000000000"
:
false
,
{
"example.com/library/mistral:latest+Q4_0"
,
"example.com/library/mistral:latest"
},
// incomplete
"sha256:"
+
validSHA256Hex
:
true
,
{
"example.com/library/mistral:latest"
,
"example.com/library/mistral:latest"
},
"sha256-"
+
validSHA256Hex
:
true
,
{
""
,
""
},
}
for
_
,
tt
:=
range
cases
{
t
.
Run
(
tt
.
in
,
func
(
t
*
testing
.
T
)
{
p
:=
ParseNameFill
(
tt
.
in
,
FillNothing
)
t
.
Logf
(
"ParseName(%q) = %#v"
,
tt
.
in
,
p
)
if
g
:=
p
.
DisplayURLPath
();
g
!=
tt
.
want
{
t
.
Errorf
(
"got = %q; want %q"
,
g
,
tt
.
want
)
}
})
}
}
func
TestNameFilepath
(
t
*
testing
.
T
)
{
""
:
false
,
cases
:=
[]
struct
{
"sha134:"
+
validSHA256Hex
:
false
,
in
string
"sha256:"
+
validSHA256Hex
+
"x"
:
false
,
want
string
"sha256:x"
+
validSHA256Hex
:
false
,
wantNoBuild
string
"sha256-"
+
validSHA256Hex
+
"x"
:
false
,
}{
"sha256-x"
:
false
,
{
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
:=
ParseNameFill
(
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
)
{
for
s
,
want
:=
range
cases
{
cases
:=
[]
struct
{
t
.
Run
(
s
,
func
(
t
*
testing
.
T
)
{
in
string
d
:=
ParseDigest
(
s
)
fill
string
// default is FillNothing
if
d
.
IsValid
()
!=
want
{
want
string
t
.
Errorf
(
"ParseDigest(%q).IsValid() = %v; want %v"
,
s
,
d
.
IsValid
(),
want
)
}{
{
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
{
norm
:=
strings
.
ReplaceAll
(
s
,
":"
,
"-"
)
t
.
Run
(
tt
.
in
,
func
(
t
*
testing
.
T
)
{
if
d
.
IsValid
()
&&
d
.
String
()
!=
norm
{
in
:=
strings
.
ReplaceAll
(
tt
.
in
,
"/"
,
string
(
filepath
.
Separator
))
t
.
Errorf
(
"ParseDigest(%q).String() = %q; want %q"
,
s
,
d
.
String
(),
norm
)
fill
:=
cmp
.
Or
(
tt
.
fill
,
FillNothing
)
want
:=
ParseNameFill
(
tt
.
want
,
fill
)
if
g
:=
ParseNameFromFilepath
(
in
,
fill
);
!
g
.
EqualFold
(
want
)
{
t
.
Errorf
(
"got = %q; want %q"
,
g
.
DisplayLong
(),
tt
.
want
)
}
}
})
})
}
}
}
}
func
Test
ParseNameFromPath
(
t
*
testing
.
T
)
{
func
Test
DigestString
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
cases
:=
[]
struct
{
in
string
in
string
want
string
want
string
fill
string
// default is FillNothing
}{
}{
{
{
in
:
"sha256:"
+
validSHA256Hex
,
want
:
"sha256-"
+
validSHA256Hex
},
in
:
"example.com/library/mistral:latest+Q4_0"
,
{
in
:
"sha256-"
+
validSHA256Hex
,
want
:
"sha256-"
+
validSHA256Hex
},
want
:
"example.com/library/mistral:latest+Q4_0"
,
{
in
:
""
,
want
:
"unknown-0000000000000000000000000000000000000000000000000000000000000000"
},
},
{
in
:
"blah-100000000000000000000000000000000000000000000000000000000000000"
,
want
:
"unknown-0000000000000000000000000000000000000000000000000000000000000000"
},
{
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
{
for
_
,
tt
:=
range
cases
{
t
.
Run
(
tt
.
in
,
func
(
t
*
testing
.
T
)
{
t
.
Run
(
tt
.
in
,
func
(
t
*
testing
.
T
)
{
fill
:=
cmp
.
Or
(
tt
.
fill
,
FillNoth
in
g
)
d
:=
ParseDigest
(
tt
.
in
)
if
g
:=
ParseNameFromURLPath
(
tt
.
in
,
fill
);
g
.
DisplayLo
ng
()
!=
tt
.
want
{
if
d
.
Stri
ng
()
!=
tt
.
want
{
t
.
Errorf
(
"
got
= %q; want %q"
,
g
.
DisplayLo
ng
(),
tt
.
want
)
t
.
Errorf
(
"
ParseDigest(%q).String()
= %q; want %q"
,
tt
.
in
,
d
.
Stri
ng
(),
tt
.
want
)
}
}
})
})
}
}
}
}
func
ExampleName_MapHash
()
{
m
:=
map
[
uint64
]
bool
{}
// key 1
m
[
ParseNameFill
(
"mistral:latest+q4"
,
FillNothing
)
.
MapHash
()]
=
true
m
[
ParseNameFill
(
"miSTRal:latest+Q4"
,
FillNothing
)
.
MapHash
()]
=
true
m
[
ParseNameFill
(
"mistral:LATest+Q4"
,
FillNothing
)
.
MapHash
()]
=
true
// key 2
m
[
ParseNameFill
(
"mistral:LATest"
,
FillNothing
)
.
MapHash
()]
=
true
fmt
.
Println
(
len
(
m
))
// Output:
// 2
}
func
ExampleName_CompareFold_sort
()
{
names
:=
[]
Name
{
ParseNameFill
(
"mistral:latest"
,
FillNothing
),
ParseNameFill
(
"mistRal:7b+q4"
,
FillNothing
),
ParseNameFill
(
"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-abc"
,
"x/y/z:latest+q4_0"
,
"@sha123-abc"
,
}
{
name
:=
ParseNameFill
(
s
,
FillNothing
)
fmt
.
Printf
(
"complete:%v resolved:%v digest:%s
\n
"
,
name
.
IsComplete
(),
name
.
IsResolved
(),
name
.
Digest
())
}
// Output:
// complete:true resolved:true digest:sha123-abc
// complete:true resolved:false digest:
// complete:false resolved:true digest:sha123-abc
}
func
ExampleName_DisplayShortest
()
{
name
:=
ParseNameFill
(
"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
=
ParseNameFill
(
"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
}
types/model/testdata/fuzz/Fuzz
ParseRef/82c2975c430ac608
→
types/model/testdata/fuzz/Fuzz
Name/d37463aa416f6bab
View file @
37f9c8ad
go test fuzz v1
go test fuzz v1
string("
:
")
string("
00@
")
types/model/testdata/fuzz/FuzzParseRef/1d43ee52085cb4aa
deleted
100644 → 0
View file @
2a80f55e
go test fuzz v1
string("/0")
types/model/testdata/fuzz/FuzzParseRef/27fd759314f0e6d6
deleted
100644 → 0
View file @
2a80f55e
go test fuzz v1
string("0//0")
types/model/testdata/fuzz/FuzzParseRef/3e3b70dba384074d
deleted
100644 → 0
View file @
2a80f55e
go test fuzz v1
string("0 /0")
types/model/testdata/fuzz/FuzzParseRef/71f1fdff711b6dab
deleted
100644 → 0
View file @
2a80f55e
go test fuzz v1
string("+0/00000")
types/model/testdata/fuzz/FuzzParseRef/b51b1c875e61a948
deleted
100644 → 0
View file @
2a80f55e
go test fuzz v1
string("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")
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment