"backend/apps/vscode:/vscode.git/clone" did not exist on "99d10d1189452ad49fcace219e9c90ae65906cd1"
Commit 920a4b07 authored by Daniel Hiltgen's avatar Daniel Hiltgen
Browse files

Merge remote-tracking branch 'upstream/main' into pr3702

parents c496967e ee49844d
package model
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strconv"
"strings"
)
type File struct {
Commands []Command
}
func (f File) String() string {
var sb strings.Builder
for _, cmd := range f.Commands {
fmt.Fprintln(&sb, cmd.String())
}
return sb.String()
}
type Command struct {
Name string
Args string
}
func (c Command) String() string {
var sb strings.Builder
switch c.Name {
case "model":
fmt.Fprintf(&sb, "FROM %s", c.Args)
case "license", "template", "system", "adapter":
fmt.Fprintf(&sb, "%s %s", strings.ToUpper(c.Name), quote(c.Args))
case "message":
role, message, _ := strings.Cut(c.Args, ": ")
fmt.Fprintf(&sb, "MESSAGE %s %s", role, quote(message))
default:
fmt.Fprintf(&sb, "PARAMETER %s %s", c.Name, quote(c.Args))
}
return sb.String()
}
type state int
const (
stateNil state = iota
stateName
stateValue
stateParameter
stateMessage
stateComment
)
var (
errMissingFrom = errors.New("no FROM line")
errInvalidMessageRole = errors.New("message role must be one of \"system\", \"user\", or \"assistant\"")
errInvalidCommand = errors.New("command must be one of \"from\", \"license\", \"template\", \"system\", \"adapter\", \"parameter\", or \"message\"")
)
func ParseFile(r io.Reader) (*File, error) {
var cmd Command
var curr state
var b bytes.Buffer
var role string
var f File
br := bufio.NewReader(r)
for {
r, _, err := br.ReadRune()
if errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, err
}
next, r, err := parseRuneForState(r, curr)
if errors.Is(err, io.ErrUnexpectedEOF) {
return nil, fmt.Errorf("%w: %s", err, b.String())
} else if err != nil {
return nil, err
}
// process the state transition, some transitions need to be intercepted and redirected
if next != curr {
switch curr {
case stateName:
if !isValidCommand(b.String()) {
return nil, errInvalidCommand
}
// next state sometimes depends on the current buffer value
switch s := strings.ToLower(b.String()); s {
case "from":
cmd.Name = "model"
case "parameter":
// transition to stateParameter which sets command name
next = stateParameter
case "message":
// transition to stateMessage which validates the message role
next = stateMessage
fallthrough
default:
cmd.Name = s
}
case stateParameter:
cmd.Name = b.String()
case stateMessage:
if !isValidMessageRole(b.String()) {
return nil, errInvalidMessageRole
}
role = b.String()
case stateComment, stateNil:
// pass
case stateValue:
s, ok := unquote(b.String())
if !ok || isSpace(r) {
if _, err := b.WriteRune(r); err != nil {
return nil, err
}
continue
}
if role != "" {
s = role + ": " + s
role = ""
}
cmd.Args = s
f.Commands = append(f.Commands, cmd)
}
b.Reset()
curr = next
}
if strconv.IsPrint(r) {
if _, err := b.WriteRune(r); err != nil {
return nil, err
}
}
}
// flush the buffer
switch curr {
case stateComment, stateNil:
// pass; nothing to flush
case stateValue:
s, ok := unquote(b.String())
if !ok {
return nil, io.ErrUnexpectedEOF
}
if role != "" {
s = role + ": " + s
}
cmd.Args = s
f.Commands = append(f.Commands, cmd)
default:
return nil, io.ErrUnexpectedEOF
}
for _, cmd := range f.Commands {
if cmd.Name == "model" {
return &f, nil
}
}
return nil, errMissingFrom
}
func parseRuneForState(r rune, cs state) (state, rune, error) {
switch cs {
case stateNil:
switch {
case r == '#':
return stateComment, 0, nil
case isSpace(r), isNewline(r):
return stateNil, 0, nil
default:
return stateName, r, nil
}
case stateName:
switch {
case isAlpha(r):
return stateName, r, nil
case isSpace(r):
return stateValue, 0, nil
default:
return stateNil, 0, errInvalidCommand
}
case stateValue:
switch {
case isNewline(r):
return stateNil, r, nil
case isSpace(r):
return stateNil, r, nil
default:
return stateValue, r, nil
}
case stateParameter:
switch {
case isAlpha(r), isNumber(r), r == '_':
return stateParameter, r, nil
case isSpace(r):
return stateValue, 0, nil
default:
return stateNil, 0, io.ErrUnexpectedEOF
}
case stateMessage:
switch {
case isAlpha(r):
return stateMessage, r, nil
case isSpace(r):
return stateValue, 0, nil
default:
return stateNil, 0, io.ErrUnexpectedEOF
}
case stateComment:
switch {
case isNewline(r):
return stateNil, 0, nil
default:
return stateComment, 0, nil
}
default:
return stateNil, 0, errors.New("")
}
}
func quote(s string) string {
if strings.Contains(s, "\n") || strings.HasPrefix(s, " ") || strings.HasSuffix(s, " ") {
if strings.Contains(s, "\"") {
return `"""` + s + `"""`
}
return `"` + s + `"`
}
return s
}
func unquote(s string) (string, bool) {
// TODO: single quotes
if len(s) >= 3 && s[:3] == `"""` {
if len(s) >= 6 && s[len(s)-3:] == `"""` {
return s[3 : len(s)-3], true
}
return "", false
}
if len(s) >= 1 && s[0] == '"' {
if len(s) >= 2 && s[len(s)-1] == '"' {
return s[1 : len(s)-1], true
}
return "", false
}
return s, true
}
func isAlpha(r rune) bool {
return r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z'
}
func isNumber(r rune) bool {
return r >= '0' && r <= '9'
}
func isSpace(r rune) bool {
return r == ' ' || r == '\t'
}
func isNewline(r rune) bool {
return r == '\r' || r == '\n'
}
func isValidMessageRole(role string) bool {
return role == "system" || role == "user" || role == "assistant"
}
func isValidCommand(cmd string) bool {
switch strings.ToLower(cmd) {
case "from", "license", "template", "system", "adapter", "parameter", "message":
return true
default:
return false
}
}
package model
import (
"bytes"
"fmt"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseFileFile(t *testing.T) {
input := `
FROM model1
ADAPTER adapter1
LICENSE MIT
PARAMETER param1 value1
PARAMETER param2 value2
TEMPLATE template1
`
reader := strings.NewReader(input)
modelfile, err := ParseFile(reader)
assert.NoError(t, err)
expectedCommands := []Command{
{Name: "model", Args: "model1"},
{Name: "adapter", Args: "adapter1"},
{Name: "license", Args: "MIT"},
{Name: "param1", Args: "value1"},
{Name: "param2", Args: "value2"},
{Name: "template", Args: "template1"},
}
assert.Equal(t, expectedCommands, modelfile.Commands)
}
func TestParseFileFrom(t *testing.T) {
var cases = []struct {
input string
expected []Command
err error
}{
{
"FROM foo",
[]Command{{Name: "model", Args: "foo"}},
nil,
},
{
"FROM /path/to/model",
[]Command{{Name: "model", Args: "/path/to/model"}},
nil,
},
{
"FROM /path/to/model/fp16.bin",
[]Command{{Name: "model", Args: "/path/to/model/fp16.bin"}},
nil,
},
{
"FROM llama3:latest",
[]Command{{Name: "model", Args: "llama3:latest"}},
nil,
},
{
"FROM llama3:7b-instruct-q4_K_M",
[]Command{{Name: "model", Args: "llama3:7b-instruct-q4_K_M"}},
nil,
},
{
"", nil, errMissingFrom,
},
{
"PARAMETER param1 value1",
nil,
errMissingFrom,
},
{
"PARAMETER param1 value1\nFROM foo",
[]Command{{Name: "param1", Args: "value1"}, {Name: "model", Args: "foo"}},
nil,
},
}
for _, c := range cases {
t.Run("", func(t *testing.T) {
modelfile, err := ParseFile(strings.NewReader(c.input))
assert.ErrorIs(t, err, c.err)
if modelfile != nil {
assert.Equal(t, c.expected, modelfile.Commands)
}
})
}
}
func TestParseFileParametersMissingValue(t *testing.T) {
input := `
FROM foo
PARAMETER param1
`
reader := strings.NewReader(input)
_, err := ParseFile(reader)
assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
}
func TestParseFileBadCommand(t *testing.T) {
input := `
FROM foo
BADCOMMAND param1 value1
`
_, err := ParseFile(strings.NewReader(input))
assert.ErrorIs(t, err, errInvalidCommand)
}
func TestParseFileMessages(t *testing.T) {
var cases = []struct {
input string
expected []Command
err error
}{
{
`
FROM foo
MESSAGE system You are a file parser. Always parse things.
`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "message", Args: "system: You are a file parser. Always parse things."},
},
nil,
},
{
`
FROM foo
MESSAGE system You are a file parser. Always parse things.`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "message", Args: "system: You are a file parser. Always parse things."},
},
nil,
},
{
`
FROM foo
MESSAGE system You are a file parser. Always parse things.
MESSAGE user Hey there!
MESSAGE assistant Hello, I want to parse all the things!
`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "message", Args: "system: You are a file parser. Always parse things."},
{Name: "message", Args: "user: Hey there!"},
{Name: "message", Args: "assistant: Hello, I want to parse all the things!"},
},
nil,
},
{
`
FROM foo
MESSAGE system """
You are a multiline file parser. Always parse things.
"""
`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "message", Args: "system: \nYou are a multiline file parser. Always parse things.\n"},
},
nil,
},
{
`
FROM foo
MESSAGE badguy I'm a bad guy!
`,
nil,
errInvalidMessageRole,
},
{
`
FROM foo
MESSAGE system
`,
nil,
io.ErrUnexpectedEOF,
},
{
`
FROM foo
MESSAGE system`,
nil,
io.ErrUnexpectedEOF,
},
}
for _, c := range cases {
t.Run("", func(t *testing.T) {
modelfile, err := ParseFile(strings.NewReader(c.input))
assert.ErrorIs(t, err, c.err)
if modelfile != nil {
assert.Equal(t, c.expected, modelfile.Commands)
}
})
}
}
func TestParseFileQuoted(t *testing.T) {
var cases = []struct {
multiline string
expected []Command
err error
}{
{
`
FROM foo
SYSTEM """
This is a
multiline system.
"""
`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "system", Args: "\nThis is a\nmultiline system.\n"},
},
nil,
},
{
`
FROM foo
SYSTEM """
This is a
multiline system."""
`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "system", Args: "\nThis is a\nmultiline system."},
},
nil,
},
{
`
FROM foo
SYSTEM """This is a
multiline system."""
`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "system", Args: "This is a\nmultiline system."},
},
nil,
},
{
`
FROM foo
SYSTEM """This is a multiline system."""
`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "system", Args: "This is a multiline system."},
},
nil,
},
{
`
FROM foo
SYSTEM """This is a multiline system.""
`,
nil,
io.ErrUnexpectedEOF,
},
{
`
FROM foo
SYSTEM "
`,
nil,
io.ErrUnexpectedEOF,
},
{
`
FROM foo
SYSTEM """
This is a multiline system with "quotes".
"""
`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "system", Args: "\nThis is a multiline system with \"quotes\".\n"},
},
nil,
},
{
`
FROM foo
SYSTEM """"""
`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "system", Args: ""},
},
nil,
},
{
`
FROM foo
SYSTEM ""
`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "system", Args: ""},
},
nil,
},
{
`
FROM foo
SYSTEM "'"
`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "system", Args: "'"},
},
nil,
},
{
`
FROM foo
SYSTEM """''"'""'""'"'''''""'""'"""
`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "system", Args: `''"'""'""'"'''''""'""'`},
},
nil,
},
{
`
FROM foo
TEMPLATE """
{{ .Prompt }}
"""`,
[]Command{
{Name: "model", Args: "foo"},
{Name: "template", Args: "\n{{ .Prompt }}\n"},
},
nil,
},
}
for _, c := range cases {
t.Run("", func(t *testing.T) {
modelfile, err := ParseFile(strings.NewReader(c.multiline))
assert.ErrorIs(t, err, c.err)
if modelfile != nil {
assert.Equal(t, c.expected, modelfile.Commands)
}
})
}
}
func TestParseFileParameters(t *testing.T) {
var cases = map[string]struct {
name, value string
}{
"numa true": {"numa", "true"},
"num_ctx 1": {"num_ctx", "1"},
"num_batch 1": {"num_batch", "1"},
"num_gqa 1": {"num_gqa", "1"},
"num_gpu 1": {"num_gpu", "1"},
"main_gpu 1": {"main_gpu", "1"},
"low_vram true": {"low_vram", "true"},
"f16_kv true": {"f16_kv", "true"},
"logits_all true": {"logits_all", "true"},
"vocab_only true": {"vocab_only", "true"},
"use_mmap true": {"use_mmap", "true"},
"use_mlock true": {"use_mlock", "true"},
"num_thread 1": {"num_thread", "1"},
"num_keep 1": {"num_keep", "1"},
"seed 1": {"seed", "1"},
"num_predict 1": {"num_predict", "1"},
"top_k 1": {"top_k", "1"},
"top_p 1.0": {"top_p", "1.0"},
"tfs_z 1.0": {"tfs_z", "1.0"},
"typical_p 1.0": {"typical_p", "1.0"},
"repeat_last_n 1": {"repeat_last_n", "1"},
"temperature 1.0": {"temperature", "1.0"},
"repeat_penalty 1.0": {"repeat_penalty", "1.0"},
"presence_penalty 1.0": {"presence_penalty", "1.0"},
"frequency_penalty 1.0": {"frequency_penalty", "1.0"},
"mirostat 1": {"mirostat", "1"},
"mirostat_tau 1.0": {"mirostat_tau", "1.0"},
"mirostat_eta 1.0": {"mirostat_eta", "1.0"},
"penalize_newline true": {"penalize_newline", "true"},
"stop ### User:": {"stop", "### User:"},
"stop ### User: ": {"stop", "### User: "},
"stop \"### User:\"": {"stop", "### User:"},
"stop \"### User: \"": {"stop", "### User: "},
"stop \"\"\"### User:\"\"\"": {"stop", "### User:"},
"stop \"\"\"### User:\n\"\"\"": {"stop", "### User:\n"},
"stop <|endoftext|>": {"stop", "<|endoftext|>"},
"stop <|eot_id|>": {"stop", "<|eot_id|>"},
"stop </s>": {"stop", "</s>"},
}
for k, v := range cases {
t.Run(k, func(t *testing.T) {
var b bytes.Buffer
fmt.Fprintln(&b, "FROM foo")
fmt.Fprintln(&b, "PARAMETER", k)
modelfile, err := ParseFile(&b)
assert.NoError(t, err)
assert.Equal(t, []Command{
{Name: "model", Args: "foo"},
{Name: v.name, Args: v.value},
}, modelfile.Commands)
})
}
}
func TestParseFileComments(t *testing.T) {
var cases = []struct {
input string
expected []Command
}{
{
`
# comment
FROM foo
`,
[]Command{
{Name: "model", Args: "foo"},
},
},
}
for _, c := range cases {
t.Run("", func(t *testing.T) {
modelfile, err := ParseFile(strings.NewReader(c.input))
assert.NoError(t, err)
assert.Equal(t, c.expected, modelfile.Commands)
})
}
}
func TestParseFileFormatParseFile(t *testing.T) {
var cases = []string{
`
FROM foo
ADAPTER adapter1
LICENSE MIT
PARAMETER param1 value1
PARAMETER param2 value2
TEMPLATE template1
MESSAGE system You are a file parser. Always parse things.
MESSAGE user Hey there!
MESSAGE assistant Hello, I want to parse all the things!
`,
`
FROM foo
ADAPTER adapter1
LICENSE MIT
PARAMETER param1 value1
PARAMETER param2 value2
TEMPLATE template1
MESSAGE system """
You are a store greeter. Always responsed with "Hello!".
"""
MESSAGE user Hey there!
MESSAGE assistant Hello, I want to parse all the things!
`,
`
FROM foo
ADAPTER adapter1
LICENSE """
Very long and boring legal text.
Blah blah blah.
"Oh look, a quote!"
"""
PARAMETER param1 value1
PARAMETER param2 value2
TEMPLATE template1
MESSAGE system """
You are a store greeter. Always responsed with "Hello!".
"""
MESSAGE user Hey there!
MESSAGE assistant Hello, I want to parse all the things!
`,
`
FROM foo
SYSTEM ""
`,
}
for _, c := range cases {
t.Run("", func(t *testing.T) {
modelfile, err := ParseFile(strings.NewReader(c))
assert.NoError(t, err)
modelfile2, err := ParseFile(strings.NewReader(modelfile.String()))
assert.NoError(t, err)
assert.Equal(t, modelfile, modelfile2)
})
}
}
// 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/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 // Unqualified is a helper function that returns an error with
const ( // ErrUnqualifiedName as the cause and the name as the message.
// MaskDefault is the default mask used by [Name.DisplayShortest]. func Unqualified(n Name) error {
MaskDefault = "registry.ollama.ai/library/?:latest" return fmt.Errorf("%w: %s", ErrUnqualifiedName, n)
}
// MaskNothing is a mask that masks nothing.
MaskNothing = "?/?/?:?"
// DefaultFill is the default fill used by [ParseName]. // MissingPart is used to indicate any part of a name that was "promised" by
FillDefault = "registry.ollama.ai/library/?:latest+Q4_0" // the presence of a separator, but is missing.
//
// 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
// spot in logs.
const MissingPart = "!MISSING!"
// FillNothing is a fill that fills nothing. const (
FillNothing = "?/?/?:?+?" defaultHost = "registry.ollama.ai"
defaultNamespace = "library"
defaultTag = "latest"
) )
const MaxNamePartLen = 128 // DefaultName returns a name with the default values for the host, namespace,
// and tag parts. The model and digest parts are empty.
//
// - The default host is ("registry.ollama.ai")
// - The default namespace is ("library")
// - The default tag is ("latest")
func DefaultName() Name {
return Name{
Host: defaultHost,
Namespace: defaultNamespace,
Tag: defaultTag,
}
}
type PartKind int type partKind int
// Levels of concreteness
const ( const (
// Each value aligns with its index in the Name.parts array. kindHost partKind = iota
kindNamespace
PartHost PartKind = iota kindModel
PartNamespace kindTag
PartModel kindDigest
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{ func (k partKind) String() string {
PartHost: "Host", switch k {
PartNamespace: "Namespace", case kindHost:
PartModel: "Name", return "host"
PartTag: "Tag", case kindNamespace:
PartBuild: "Build", return "namespace"
PartDigest: "Digest", case kindModel:
} return "model"
case kindTag:
func (k PartKind) String() string { return "tag"
return cmp.Or(kindNames[k], "Unknown") case kindDigest:
return "digest"
default:
return "unknown"
}
} }
// Name is an opaque reference to a model. It holds the parts of a model // Name is a structured representation of a model name string, as defined by
// with the case preserved, but is not directly comparable with other Names // [ParseNameNoDefaults].
// 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]. // It is not guaranteed to be valid. Use [Name.IsValid] to check if the name
// // is valid.
// 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].
//
// To check if a Name has at minimum a valid model part, use [Name.IsValid].
type Name struct { type Name struct {
_ structs.Incomparable Host string
parts [NumParts]string // host, namespace, model, tag, build, digest Namespace string
Model string
// TODO(bmizerany): track offsets and hold s (raw string) here? We Tag string
// could pack the offsets all into a single uint64 since the first RawDigest string
// 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 and assembles a Name from a name string. The
} // format of a valid name string is:
//
// ParseName parses s into a Name, and returns the result of filling it with // s:
// defaults. The input string must be a valid string // { host } "/" { namespace } "/" { model } ":" { tag } "@" { digest }
// representation of a model name in the form: // { host } "/" { namespace } "/" { model } ":" { tag }
// // { host } "/" { namespace } "/" { model } "@" { digest }
// [host/][namespace/]<model>[:tag][+build][@<digest-type>-<digest>] // { host } "/" { namespace } "/" { model }
// // { namespace } "/" { model } ":" { tag } "@" { digest }
// The name part is required, all others are optional. If a part is missing, // { namespace } "/" { model } ":" { tag }
// it is left empty in the returned Name. If a part is invalid, the zero Ref // { namespace } "/" { model } "@" { digest }
// value is returned. // { namespace } "/" { model }
// // { model } ":" { tag } "@" { digest }
// The build part is normalized to uppercase. // { model } ":" { tag }
// // { model } "@" { digest }
// Examples of valid paths: // { model }
// // "@" { digest }
// "example.com/library/mistral:7b+x" // host:
// "example.com/eva/mistral:7b+Q4_0" // pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." | ":" }*
// "mistral:7b+x" // length: [1, 350]
// "example.com/mike/mistral:latest+Q4_0" // namespace:
// "example.com/bruce/mistral:latest" // pattern: { alphanum | "_" } { alphanum | "-" | "_" }*
// "example.com/pdevine/thisisfine:7b+Q4_0@sha256-1234567890abcdef" // length: [1, 80]
// // model:
// Examples of invalid paths: // pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." }*
// // length: [1, 80]
// "example.com/mistral:7b+" // tag:
// "example.com/mistral:7b+Q4_0+" // pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." }*
// "x/y/z/z:8n+I" // length: [1, 80]
// "" // digest:
// // pattern: { alphanum | "_" } { alphanum | "-" | ":" }*
// It returns the zero value if any part is invalid. // length: [1, 80]
// //
// # Fills // Most users should use [ParseName] instead, unless need to support
// // different defaults than DefaultName.
// For any valid s, the fill string is used to fill in missing parts of the //
// Name. The fill string must be a valid Name with the exception that any part // The name returned is not guaranteed to be valid. If it is not valid, the
// may be the string ("?"), which will not be considered for filling. // field values are left in an undefined state. Use [Name.IsValid] to check
func ParseName(s, fill string) Name { // if the name is valid.
var r Name func ParseName(s string) Name {
parts(s)(func(kind PartKind, part string) bool { return Merge(ParseNameBare(s), DefaultName())
if kind == PartDigest && !ParseDigest(part).IsValid() { }
r = Name{}
return false // ParseNameBare parses s as a name string and returns a Name. No merge with
} // [DefaultName] is performed.
if kind == PartExtraneous || !isValidPart(kind, part) { func ParseNameBare(s string) Name {
r = Name{} var n Name
return false var promised bool
}
r.parts[kind] = part s, n.RawDigest, promised = cutLast(s, "@")
return true if promised && n.RawDigest == "" {
}) n.RawDigest = MissingPart
if r.IsValid() || r.IsResolved() {
return fillName(r, fill)
} }
return Name{}
}
func parseMask(s string) Name { // "/" is an illegal tag character, so we can use it to split the host
var r Name if strings.LastIndex(s, ":") > strings.LastIndex(s, "/") {
parts(s)(func(kind PartKind, part string) bool { s, n.Tag, _ = cutPromised(s, ":")
if part == "?" {
// mask part; treat as empty but valid
return true
}
if !isValidPart(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 {
r := ParseName(s, fill)
if !r.IsValid() {
panic("invalid Name: " + s)
} }
return r
}
// fillName fills in the missing parts of dst with the parts of src. s, n.Model, promised = cutPromised(s, "/")
// if !promised {
// The returned Name will only be valid if dst is valid. n.Model = s
// return n
// It skipps fill parts that are "?".
func fillName(r Name, fill string) Name {
fill = cmp.Or(fill, FillDefault)
f := parseMask(fill)
if fill != FillNothing && f.IsZero() {
panic("invalid fill")
}
for i := range r.parts {
if f.parts[i] == "?" {
continue
}
r.parts[i] = cmp.Or(r.parts[i], f.parts[i])
} }
return r
}
// WithBuild returns a copy of r with the build set to the given string. s, n.Namespace, promised = cutPromised(s, "/")
func (r Name) WithBuild(build string) Name { if !promised {
r.parts[PartBuild] = build n.Namespace = s
return r return n
} }
func (r Name) WithDigest(digest Digest) Name {
r.parts[PartDigest] = digest.String()
return r
}
var mapHashSeed = maphash.MakeSeed()
// MapHash returns a case insensitive hash for use in maps and equality scheme, host, ok := strings.Cut(s, "://")
// checks. For a convenient way to compare names, use [Name.EqualFold]. if !ok {
// host = scheme
//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() n.Host = host
}
func (r Name) slice(from, to PartKind) Name { return n
var v Name
copy(v.parts[from:to+1], r.parts[from:to+1])
return v
} }
// DisplayShortest returns the shortest possible, masked display string in form: // ParseNameFromFilepath parses a 4-part filepath as a Name. The parts are
// // expected to be in the 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 // { host } "/" { namespace } "/" { model } "/" { tag }
// invalid. func ParseNameFromFilepath(s string) (n Name) {
// parts := strings.Split(s, string(filepath.Separator))
// # Builds if len(parts) != 4 {
// return Name{}
// 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 {
if !strings.EqualFold(r.parts[i], d.parts[i]) {
break
}
r.parts[i] = ""
} }
for i := PartTag; i >= 0; i-- {
if !strings.EqualFold(r.parts[i], d.parts[i]) { n.Host = parts[0]
break n.Namespace = parts[1]
} n.Model = parts[2]
r.parts[i] = "" n.Tag = parts[3]
if !n.IsFullyQualified() {
return Name{}
} }
return r.slice(PartHost, PartTag).DisplayLong()
}
// DisplayLongest returns the result of r.DisplayShortest(MaskNothing). return n
func (r Name) DisplayLongest() string {
return r.DisplayShortest(MaskNothing)
} }
var seps = [...]string{ // Merge merges the host, namespace, and tag parts of the two names,
PartHost: "/", // preferring the non-empty parts of a.
PartNamespace: "/", func Merge(a, b Name) Name {
PartModel: ":", a.Host = cmp.Or(a.Host, b.Host)
PartTag: "+", a.Namespace = cmp.Or(a.Namespace, b.Namespace)
PartBuild: "@", a.Tag = cmp.Or(a.Tag, b.Tag)
PartDigest: "", return a
} }
// 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>". if n.Namespace != "" {
func (r Name) writeTo(w io.StringWriter) error { b.WriteString(n.Namespace)
var partsWritten int b.WriteByte('/')
for i := range r.parts { }
if r.parts[i] == "" { b.WriteString(n.Model)
continue if n.Tag != "" {
} b.WriteByte(':')
if partsWritten > 0 || i == int(PartDigest) { b.WriteString(n.Tag)
if _, err := w.WriteString(seps[i-1]); err != nil { }
return err if n.RawDigest != "" {
} b.WriteByte('@')
} b.WriteString(n.RawDigest)
if _, err := w.WriteString(r.parts[i]); err != nil {
return err
}
partsWritten++
} }
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 // DisplayShort returns a short string version of the name.
// debugging and logging. It is similar to [Name.DisplayLong] but it always func (n Name) DisplayShortest() string {
// returns a string that includes all parts of the Name, with missing parts var sb strings.Builder
// replaced with a ("?").
func (r Name) GoString() string {
for i := range r.parts {
r.parts[i] = cmp.Or(r.parts[i], "?")
}
return r.DisplayLong()
}
// LogValue implements slog.Valuer. if n.Host != defaultHost {
func (r Name) LogValue() slog.Value { sb.WriteString(n.Host)
return slog.StringValue(r.GoString()) sb.WriteByte('/')
} sb.WriteString(n.Namespace)
sb.WriteByte('/')
} else if n.Namespace != defaultNamespace {
sb.WriteString(n.Namespace)
sb.WriteByte('/')
}
// IsComplete reports whether the Name is fully qualified. That is it has a // always include model and tag
// domain, namespace, name, tag, and build. sb.WriteString(n.Model)
func (r Name) IsComplete() bool { sb.WriteString(":")
return !slices.Contains(r.parts[:PartDigest], "") sb.WriteString(n.Tag)
return sb.String()
} }
// IsCompleteNoBuild is like [Name.IsComplete] but it does not require the // IsValid reports whether all parts of the name are present and valid. The
// build part to be present. // digest is a special case, and is checked for validity only if present.
func (r Name) IsCompleteNoBuild() bool { func (n Name) IsValid() bool {
return !slices.Contains(r.parts[:PartBuild], "") if n.RawDigest != "" && !isValidPart(kindDigest, n.RawDigest) {
return false
}
return n.IsFullyQualified()
} }
// IsResolved reports true if the Name has a valid digest. // IsFullyQualified returns true if all parts of the name are present and
// // valid without the digest.
// It is possible to have a valid Name, or a complete Name that is not func (n Name) IsFullyQualified() bool {
// resolved. var parts = []string{
func (r Name) IsResolved() bool { n.Host,
return r.Digest().IsValid() n.Namespace,
n.Model,
n.Tag,
}
for i, part := range parts {
if !isValidPart(partKind(i), part) {
return false
}
}
return true
} }
// Digest returns the digest part of the Name, if any. // Filepath returns a canonical filepath that represents the name with each part from
// host to tag as a directory in the form:
// //
// If Digest returns a non-empty string, then [Name.IsResolved] will return // {host}/{namespace}/{model}/{tag}
// 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]. // It uses the system's filepath separator and ensures the path is clean.
// //
// 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")
func compareFold(a, b string) int { }
return slices.CompareFunc([]rune(a), []rune(b), func(a, b rune) int { return filepath.Join(
return cmp.Compare(downcase(a), downcase(b)) strings.ToLower(filepath.Join(
}) n.Host,
} n.Namespace,
n.Model,
func downcase(r rune) rune { )),
if r >= 'A' && r <= 'Z' { n.Tag,
return r - 'A' + 'a' )
}
// LogValue returns a slog.Value that represents the name as a string.
func (n Name) LogValue() slog.Value {
return slog.StringValue(n.String())
}
func isValidLen(kind partKind, s string) bool {
switch kind {
case kindHost:
return len(s) >= 1 && len(s) <= 350
case kindTag:
return len(s) >= 1 && len(s) <= 80
default:
return len(s) >= 1 && 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] } for i := range s {
if i == 0 {
// iter_Seq2 is a iter.Seq2 defined here to avoid the current build if !isAlphanumericOrUnderscore(s[i]) {
// restrictions in the go1.22 iter package requiring the return false
// goexperiment.rangefunc tag to be set via the GOEXPERIMENT=rangefunc flag, }
// which we are not yet ready to support. continue
//
// 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
} }
switch s[i] {
numConsecutiveDots := 0 case '_', '-':
partLen := 0 case '.':
state, j := PartDigest, len(s) if kind == kindNamespace {
for i := len(s) - 1; i >= 0; i-- { return false
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
} }
case ':':
switch s[i] { if kind != kindHost && kind != kindDigest {
case '@': return false
switch state { }
case PartDigest: default:
if !yield(PartDigest, s[i+1:j]) { if !isAlphanumericOrUnderscore(s[i]) {
return return false
}
if i == 0 {
// This is the form
// "@<digest>" which is valid.
//
// We're done.
return
}
state, j, partLen = PartBuild, i, 0
default:
yield(PartExtraneous, s[i+1:j])
return
}
case '+':
switch state {
case PartBuild, PartDigest:
if !yield(PartBuild, s[i+1:j]) {
return
}
state, j, partLen = PartTag, i, 0
default:
yield(PartExtraneous, s[i+1:j])
return
}
case ':':
switch state {
case PartTag, PartBuild, PartDigest:
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:
if s[i] == '.' {
if numConsecutiveDots++; numConsecutiveDots > 1 {
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 isAlphanumericOrUnderscore(c byte) bool {
return r.parts == [NumParts]string{} return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_'
} }
// 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 ParseName(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
} }
// URLPath returns a complete, canonicalized, relative URL path using the parts of a type DigestType byte
// complete Name.
//
// The parts maintain their original case.
//
// Example:
//
// ParseName("example.com/namespace/model:tag+build").URLPath() // returns "/example.com/namespace/model:tag"
func (r Name) URLPath() string {
return r.DisplayShortest(MaskNothing)
}
// ParseNameFromFilepath parses a file path into a Name. The input string must be a const (
// valid file path representation of a model name in the form: DigestTypeInvalid DigestType = iota
// DigestTypeSHA256
// host/namespace/model/tag/build )
//
// The zero valid is returned if s does not contain all path elements
// 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.
// See [ParseName] for more information on the fill string.
func ParseNameFromFilepath(s, fill string) Name {
var r Name
for i := range PartBuild + 1 {
part, rest, _ := strings.Cut(s, string(filepath.Separator))
if !isValidPart(i, part) {
return Name{}
}
r.parts[i] = part
s = rest
if s == "" {
break
}
}
if s != "" {
return Name{}
}
if !r.IsValid() {
return Name{}
}
return fillName(r, fill)
}
// Filepath returns a complete, canonicalized, relative file path using the func (t DigestType) String() string {
// parts of a complete Name. switch t {
// case DigestTypeSHA256:
// Each parts is downcased, except for the build part which is upcased. return "sha256"
// default:
// Example: return "invalid"
//
// 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])
}
} }
return filepath.Join(r.parts[:]...)
} }
// FilepathNoBuild returns a complete, canonicalized, relative file path using type Digest struct {
// the parts of a complete Name, but without the build part. Type DigestType
func (r Name) FilepathNoBuild() string { Sum [32]byte
for i := range PartBuild {
r.parts[i] = strings.ToLower(r.parts[i])
}
return filepath.Join(r.parts[:PartBuild]...)
} }
// isValidPart reports if s contains all valid characters for the given func ParseDigest(s string) (Digest, error) {
// part kind. i := strings.IndexAny(s, "-:")
func isValidPart(kind PartKind, s string) bool { if i < 0 {
if s == "" { return Digest{}, fmt.Errorf("invalid digest %q", s)
return false
} }
var consecutiveDots int typ, encSum := s[:i], s[i+1:]
for _, c := range []byte(s) { if typ != "sha256" {
if c == '.' { return Digest{}, fmt.Errorf("unsupported digest type %q", typ)
if consecutiveDots++; consecutiveDots >= 2 {
return false
}
} else {
consecutiveDots = 0
}
if !isValidByteFor(kind, c) {
return false
}
} }
return true d := Digest{
} Type: DigestTypeSHA256,
func isValidByteFor(kind PartKind, c byte) bool {
if kind == PartNamespace && c == '.' {
return false
} }
if kind == PartHost && c == ':' { n, err := hex.Decode(d.Sum[:], []byte(encSum))
return true if err != nil {
return Digest{}, err
} }
if c == '.' || c == '-' { if n != 32 {
return true return Digest{}, fmt.Errorf("digest %q decoded to %d bytes; want 32", encSum, n)
} }
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' { return d, nil
return true }
func (d Digest) String() string {
if d.Type == DigestTypeInvalid {
return ""
} }
return false return fmt.Sprintf("sha256-%x", d.Sum)
}
func (d Digest) IsValid() bool {
return d.Type != DigestTypeInvalid
} }
package model package model
import ( import (
"bytes"
"cmp"
"fmt"
"log/slog"
"path/filepath" "path/filepath"
"slices" "reflect"
"strings" "runtime"
"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-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): {},
}
// 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) { func TestParseNameParts(t *testing.T) {
cases := []struct { cases := []struct {
in string in string
complete bool want Name
completeNoBuild bool wantFilepath string
}{ wantValidDigest 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: "registry.ollama.ai/library/dolphin-mistral:7b-v2.6-dpo-laser-q6_K",
in: "example.com/library/mistral:latest+Q4_0", want: Name{
wantGoString: "example.com/library/mistral:latest+Q4_0@?", Host: "registry.ollama.ai",
Namespace: "library",
Model: "dolphin-mistral",
Tag: "7b-v2.6-dpo-laser-q6_K",
},
wantFilepath: filepath.Join("registry.ollama.ai", "library", "dolphin-mistral", "7b-v2.6-dpo-laser-q6_K"),
},
{
in: "scheme://host:port/namespace/model:tag",
want: Name{
Host: "host:port",
Namespace: "namespace",
Model: "model",
Tag: "tag",
},
wantFilepath: filepath.Join("host:port", "namespace", "model", "tag"),
}, },
{ {
name: "Short Name", in: "host/namespace/model:tag",
in: "mistral:latest", want: Name{
wantGoString: "?/?/mistral:latest+?@?", Host: "host",
Namespace: "namespace",
Model: "model",
Tag: "tag",
},
wantFilepath: filepath.Join("host", "namespace", "model", "tag"),
}, },
{ {
name: "Long Name", in: "host:port/namespace/model:tag",
in: "library/mistral:latest", want: Name{
wantGoString: "?/library/mistral:latest+?@?", Host: "host:port",
Namespace: "namespace",
Model: "model",
Tag: "tag",
},
wantFilepath: filepath.Join("host:port", "namespace", "model", "tag"),
}, },
{ {
name: "Case Preserved", in: "host/namespace/model",
in: "Library/Mistral:Latest", want: Name{
wantGoString: "?/Library/Mistral:Latest+?@?", Host: "host",
Namespace: "namespace",
Model: "model",
},
wantFilepath: filepath.Join("host", "namespace", "model", "latest"),
}, },
{ {
name: "With digest", in: "host:port/namespace/model",
in: "Library/Mistral:Latest@sha256-123456", want: Name{
wantGoString: "?/Library/Mistral:Latest+?@sha256-123456", Host: "host:port",
Namespace: "namespace",
Model: "model",
},
wantFilepath: filepath.Join("host:port", "namespace", "model", "latest"),
},
{
in: "namespace/model",
want: Name{
Namespace: "namespace",
Model: "model",
},
wantFilepath: filepath.Join("registry.ollama.ai", "namespace", "model", "latest"),
},
{
in: "model",
want: Name{
Model: "model",
},
wantFilepath: filepath.Join("registry.ollama.ai", "library", "model", "latest"),
},
{
in: "h/nn/mm:t",
want: Name{
Host: "h",
Namespace: "nn",
Model: "mm",
Tag: "t",
},
wantFilepath: filepath.Join("h", "nn", "mm", "t"),
},
{
in: part80 + "/" + part80 + "/" + part80 + ":" + part80,
want: Name{
Host: part80,
Namespace: part80,
Model: part80,
Tag: part80,
},
wantFilepath: filepath.Join(part80, part80, part80, part80),
},
{
in: part350 + "/" + part80 + "/" + part80 + ":" + part80,
want: Name{
Host: part350,
Namespace: part80,
Model: part80,
Tag: part80,
},
wantFilepath: filepath.Join(part350, part80, part80, part80),
},
{
in: "@digest",
want: Name{
RawDigest: "digest",
},
wantValidDigest: false,
},
{
in: "model@sha256:123",
want: Name{
Model: "model",
RawDigest: "sha256:123",
},
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 := ParseName(tt.in, FillNothing) got := ParseNameBare(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)
} }
})
}
}
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) { got = ParseName(tt.in)
cases := []struct { if tt.wantFilepath != "" && got.Filepath() != tt.wantFilepath {
in string t.Errorf("parseName(%q).Filepath() = %q; want %q", tt.in, got.Filepath(), tt.wantFilepath)
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) { var testCases = map[string]bool{ // name -> valid
allocs := testing.AllocsPerRun(1000, func() { "": false,
keep(ParseName("example.com/mistral:7b+Q4_0", FillNothing))
}) "_why/_the/_lucky:_stiff": true,
if allocs > 0 {
t.Errorf("ParseName allocs = %v; want 0", allocs) // minimal
} "h/n/m:t@d": true,
"host/namespace/model:tag": true,
"host/namespace/model": false,
"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
// unqualified
"m": false,
"n/m:": false,
"h/n/m": false,
"@t": false,
"m@d": false,
// 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,
// hosts
"host:https/namespace/model:tag": true,
// colon in non-host part before tag
"host/name:space/model:tag": false,
} }
func BenchmarkParseName(b *testing.B) { func TestNameparseNameDefault(t *testing.T) {
b.ReportAllocs() const name = "xx"
n := ParseName(name)
for range b.N { got := n.String()
keep(ParseName("example.com/mistral:7b+Q4_0", FillNothing)) want := "registry.ollama.ai/library/xx:latest"
if got != want {
t.Errorf("parseName(%q).String() = %q; want %q", name, got, want)
} }
} }
func FuzzParseNameFromFilepath(f *testing.F) { func TestNameIsValid(t *testing.T) {
f.Add("example.com/library/mistral/7b/Q4_0") var numStringTests int
f.Add("example.com/../mistral/7b/Q4_0") for s, want := range testCases {
f.Add("example.com/x/../7b/Q4_0") n := ParseNameBare(s)
f.Add("example.com/x/../7b") got := n.IsValid()
f.Fuzz(func(t *testing.T, s string) { if got != want {
name := ParseNameFromFilepath(s, FillNothing) t.Errorf("parseName(%q).IsValid() = %v; want %v", s, got, want)
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() { // Test roundtrip with String
if !r0.EqualFold(Name{}) { if got {
t.Errorf("expected invalid path to be zero value; got %#v", r0) got := ParseNameBare(s).String()
if got != s {
t.Errorf("parseName(%q).String() = %q; want %q", s, got, s)
} }
t.Skipf("invalid path: %q", s) numStringTests++
}
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) { if numStringTests == 0 {
name := ParseName("example.com/ns/mistral:latest+Q4_0", FillNothing) t.Errorf("no tests for Name.String")
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 TestNameIsValidPart(t *testing.T) {
cases := []struct { cases := []struct {
in string kind partKind
want string s string
want bool
}{ }{
{"example.com/library/mistral:latest+Q4_0", "example.com/library/mistral:latest"}, {kind: kindHost, s: "", want: false},
{kind: kindHost, s: "a", want: true},
// incomplete {kind: kindHost, s: "a.", want: true},
{"example.com/library/mistral:latest", "example.com/library/mistral:latest"}, {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},
} }
for _, tt := range cases { for _, tt := range cases {
t.Run(tt.in, func(t *testing.T) { t.Run(tt.s, func(t *testing.T) {
p := ParseName(tt.in, FillNothing) got := isValidPart(tt.kind, tt.s)
t.Logf("ParseName(%q) = %#v", tt.in, p) if got != tt.want {
if g := p.URLPath(); g != tt.want { t.Errorf("isValidPart(%s, %q) = %v; want %v", tt.kind, tt.s, got, tt.want)
t.Errorf("got = %q; want %q", g, tt.want)
} }
}) })
} }
} }
func TestNameFilepath(t *testing.T) { func TestFilepathAllocs(t *testing.T) {
cases := []struct { n := ParseNameBare("HOST/NAMESPACE/MODEL:TAG")
in string allocs := testing.AllocsPerRun(1000, func() {
want string n.Filepath()
wantNoBuild string })
}{ var allowedAllocs float64 = 3
{ if runtime.GOOS == "windows" {
in: "example.com/library/mistral:latest+Q4_0", allowedAllocs = 5
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 { if allocs > allowedAllocs {
t.Run(tt.in, func(t *testing.T) { t.Errorf("allocs = %v; allowed %v", allocs, allowedAllocs)
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) { const (
validSha256 = "sha256-1000000000000000000000000000000000000000000000000000000000000000"
validSha256Old = "sha256:1000000000000000000000000000000000000000000000000000000000000000"
)
func TestParseDigest(t *testing.T) {
cases := []struct { cases := []struct {
in string in string
fill string // default is FillNothing
want string want string
}{ }{
{ {"", ""}, // empty
in: "example.com/library/mistral/latest/Q4_0", {"sha123-12", ""}, // invalid type
want: "example.com/library/mistral:latest+Q4_0", {"sha256-", ""}, // invalid sum
}, {"sha256-123", ""}, // invalid odd length sum
{
in: "example.com/library/mistral/latest", {validSha256, validSha256},
fill: "?/?/?:latest+Q4_0", {validSha256Old, validSha256},
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 { for _, tt := range cases {
t.Run(tt.in, func(t *testing.T) { t.Run(tt.in, func(t *testing.T) {
in := strings.ReplaceAll(tt.in, "/", string(filepath.Separator)) got, err := ParseDigest(tt.in)
fill := cmp.Or(tt.fill, FillNothing) if err != nil {
want := ParseName(tt.want, fill) if tt.want != "" {
if g := ParseNameFromFilepath(in, fill); !g.EqualFold(want) { t.Errorf("parseDigest(%q) = %v; want %v", tt.in, err, tt.want)
t.Errorf("got = %q; want %q", g.DisplayLong(), tt.want) }
return
}
if got.String() != tt.want {
t.Errorf("parseDigest(%q).String() = %q; want %q", tt.in, got, tt.want)
} }
}) })
} }
} }
func TestParseNameFromPath(t *testing.T) { func TestParseNameFromFilepath(t *testing.T) {
cases := []struct { cases := map[string]Name{
in string filepath.Join("host", "namespace", "model", "tag"): {Host: "host", Namespace: "namespace", Model: "model", Tag: "tag"},
want string filepath.Join("host:port", "namespace", "model", "tag"): {Host: "host:port", Namespace: "namespace", Model: "model", Tag: "tag"},
fill string // default is FillNothing filepath.Join("namespace", "model", "tag"): {},
}{ filepath.Join("model", "tag"): {},
{ filepath.Join("model"): {},
in: "example.com/library/mistral:latest+Q4_0", filepath.Join("..", "..", "model", "tag"): {},
want: "example.com/library/mistral:latest+Q4_0", filepath.Join("", "namespace", ".", "tag"): {},
}, filepath.Join(".", ".", ".", "."): {},
{ filepath.Join("/", "path", "to", "random", "file"): {},
in: "/example.com/library/mistral:latest+Q4_0", }
want: "example.com/library/mistral:latest+Q4_0",
}, for in, want := range cases {
{ t.Run(in, func(t *testing.T) {
in: "/example.com/library/mistral", got := ParseNameFromFilepath(in)
want: "example.com/library/mistral",
}, if !reflect.DeepEqual(got, want) {
{ t.Errorf("parseNameFromFilepath(%q) = %v; want %v", in, got, want)
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() { func TestDisplayShortest(t *testing.T) {
m := map[uint64]bool{} cases := map[string]string{
"registry.ollama.ai/library/model:latest": "model:latest",
// key 1 "registry.ollama.ai/library/model:tag": "model:tag",
m[ParseName("mistral:latest+q4", FillNothing).MapHash()] = true "registry.ollama.ai/namespace/model:tag": "namespace/model:tag",
m[ParseName("miSTRal:latest+Q4", FillNothing).MapHash()] = true "host/namespace/model:tag": "host/namespace/model:tag",
m[ParseName("mistral:LATest+Q4", FillNothing).MapHash()] = true "host/library/model:tag": "host/library/model:tag",
}
// key 2
m[ParseName("mistral:LATest", FillNothing).MapHash()] = true for in, want := range cases {
t.Run(in, func(t *testing.T) {
fmt.Println(len(m)) got := ParseNameBare(in).DisplayShortest()
// Output: if got != want {
// 2 t.Errorf("parseName(%q).DisplayShortest() = %q; want %q", in, got, want)
} }
})
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() { func FuzzName(f *testing.F) {
for _, s := range []string{ for s := range testCases {
"x/y/z:latest+q4_0@sha123-1", f.Add(s)
"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())
} }
f.Fuzz(func(t *testing.T, s string) {
n := ParseNameBare(s)
if n.IsValid() {
parts := [...]string{n.Host, n.Namespace, n.Model, n.Tag, n.RawDigest}
for _, part := range parts {
if part == ".." {
t.Errorf("unexpected .. as valid part")
}
if len(part) > 350 {
t.Errorf("part too long: %q", part)
}
}
if n.String() != s {
t.Errorf("String() = %q; want %q", n.String(), s)
}
}
// 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 }
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")
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package structs contains the Incomparable type.
package structs
// Incomparable is a zero-width incomparable type. If added as the
// first field in a struct, it marks that struct as not comparable
// (can't do == or be a map key) and usually doesn't add any width to
// the struct (unless the struct has only small fields).
//
// By making a struct incomparable, you can prevent misuse (prevent
// people from using ==), but also you can shrink generated binaries,
// as the compiler can omit equality funcs from the binary.
type Incomparable [0]func()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment