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
920a4b07
Commit
920a4b07
authored
May 08, 2024
by
Daniel Hiltgen
Browse files
Merge remote-tracking branch 'upstream/main' into pr3702
parents
c496967e
ee49844d
Changes
131
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
1451 additions
and
1256 deletions
+1451
-1256
types/model/file.go
types/model/file.go
+299
-0
types/model/file_test.go
types/model/file_test.go
+511
-0
types/model/name.go
types/model/name.go
+333
-601
types/model/name_test.go
types/model/name_test.go
+307
-629
types/model/testdata/fuzz/FuzzName/d37463aa416f6bab
types/model/testdata/fuzz/FuzzName/d37463aa416f6bab
+1
-1
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/82c2975c430ac608
types/model/testdata/fuzz/FuzzParseRef/82c2975c430ac608
+0
-2
types/model/testdata/fuzz/FuzzParseRef/b51b1c875e61a948
types/model/testdata/fuzz/FuzzParseRef/b51b1c875e61a948
+0
-2
types/structs/structs.go
types/structs/structs.go
+0
-15
No files found.
types/model/file.go
0 → 100644
View file @
920a4b07
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
}
}
types/model/file_test.go
0 → 100644
View file @
920a4b07
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
\n
FROM 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:
\n
You 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
:
"
\n
This is a
\n
multiline system.
\n
"
},
},
nil
,
},
{
`
FROM foo
SYSTEM """
This is a
multiline system."""
`
,
[]
Command
{
{
Name
:
"model"
,
Args
:
"foo"
},
{
Name
:
"system"
,
Args
:
"
\n
This is a
\n
multiline system."
},
},
nil
,
},
{
`
FROM foo
SYSTEM """This is a
multiline system."""
`
,
[]
Command
{
{
Name
:
"model"
,
Args
:
"foo"
},
{
Name
:
"system"
,
Args
:
"This is a
\n
multiline 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
:
"
\n
This 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
)
})
}
}
types/model/name.go
View file @
920a4b07
// Package model contains types and utilities for parsing, validating, and
// working with model names and digests.
package
model
import
(
"cmp"
"encoding/hex"
"errors"
"fmt"
"hash/maphash"
"io"
"log/slog"
"path/filepath"
"slices"
"strings"
"sync"
"github.com/ollama/ollama/types/structs"
)
// Errors
var
(
// ErrInvalidName, ErrIncompleteName, and ErrInvalidDigest are not
// used by this package, but are exported so that other packages can
// use them, instead of defining their own errors for them.
ErrInvalidName
=
errors
.
New
(
"invalid model name"
)
ErrIncompleteName
=
errors
.
New
(
"incomplete model name"
)
ErrInvalidDigest
=
errors
.
New
(
"invalid digest"
)
// ErrUnqualifiedName represents an error where a name is not fully
// qualified. It is not used directly in this package, but is here
// to avoid other packages inventing their own error type.
// Additionally, it can be conveniently used via [Unqualified].
ErrUnqualifiedName
=
errors
.
New
(
"unqualified name"
)
)
// 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
=
"?/?/?:?"
// Unqualified is a helper function that returns an error with
// ErrUnqualifiedName as the cause and the name as the message.
func
Unqualified
(
n
Name
)
error
{
return
fmt
.
Errorf
(
"%w: %s"
,
ErrUnqualifiedName
,
n
)
}
// DefaultFill is the default fill used by [ParseName].
FillDefault
=
"registry.ollama.ai/library/?:latest+Q4_0"
// MissingPart is used to indicate any part of a name that was "promised" by
// 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.
FillNothing
=
"?/?/?:?+?"
const
(
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
P
artKind
int
type
p
artKind
int
// 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
kindHost
partKind
=
iota
kindNamespace
kindModel
kindTag
kindDigest
)
var
kindNames
=
map
[
PartKind
]
string
{
PartHost
:
"Host"
,
PartNamespace
:
"Namespace"
,
PartModel
:
"Name"
,
PartTag
:
"Tag"
,
PartBuild
:
"Build"
,
PartDigest
:
"Digest"
,
}
func
(
k
PartKind
)
String
()
string
{
return
cmp
.
Or
(
kindNames
[
k
],
"Unknown"
)
func
(
k
partKind
)
String
()
string
{
switch
k
{
case
kindHost
:
return
"host"
case
kindNamespace
:
return
"namespace"
case
kindModel
:
return
"model"
case
kindTag
:
return
"tag"
case
kindDigest
:
return
"digest"
default
:
return
"unknown"
}
}
// 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].
// Name is a structured representation of a model name string, as defined by
// [ParseNameNoDefaults].
//
// 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].
//
// To check if a Name has at minimum a valid model part, use [Name.IsValid].
// It is not guaranteed to be valid. Use [Name.IsValid] to check if the name
// is valid.
type
Name
struct
{
_
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
// defaults. The input string must be a valid string
// 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
// Name. The fill string must be a valid Name with the exception that any part
// may be the string ("?"), which will not be considered for filling.
func
ParseName
(
s
,
fill
string
)
Name
{
var
r
Name
parts
(
s
)(
func
(
kind
PartKind
,
part
string
)
bool
{
if
kind
==
PartDigest
&&
!
ParseDigest
(
part
)
.
IsValid
()
{
r
=
Name
{}
return
false
}
if
kind
==
PartExtraneous
||
!
isValidPart
(
kind
,
part
)
{
r
=
Name
{}
return
false
}
r
.
parts
[
kind
]
=
part
return
true
})
if
r
.
IsValid
()
||
r
.
IsResolved
()
{
return
fillName
(
r
,
fill
)
Host
string
Namespace
string
Model
string
Tag
string
RawDigest
string
}
// ParseName parses and assembles a Name from a name string. The
// format of a valid name string is:
//
// s:
// { host } "/" { namespace } "/" { model } ":" { tag } "@" { digest }
// { host } "/" { namespace } "/" { model } ":" { tag }
// { host } "/" { namespace } "/" { model } "@" { digest }
// { host } "/" { namespace } "/" { model }
// { namespace } "/" { model } ":" { tag } "@" { digest }
// { namespace } "/" { model } ":" { tag }
// { namespace } "/" { model } "@" { digest }
// { namespace } "/" { model }
// { model } ":" { tag } "@" { digest }
// { model } ":" { tag }
// { model } "@" { digest }
// { model }
// "@" { digest }
// host:
// pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." | ":" }*
// length: [1, 350]
// namespace:
// pattern: { alphanum | "_" } { alphanum | "-" | "_" }*
// length: [1, 80]
// model:
// pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." }*
// length: [1, 80]
// tag:
// pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." }*
// length: [1, 80]
// digest:
// pattern: { alphanum | "_" } { alphanum | "-" | ":" }*
// length: [1, 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
(
ParseNameBare
(
s
),
DefaultName
())
}
// ParseNameBare parses s as a name string and returns a Name. No merge with
// [DefaultName] is performed.
func
ParseNameBare
(
s
string
)
Name
{
var
n
Name
var
promised
bool
s
,
n
.
RawDigest
,
promised
=
cutLast
(
s
,
"@"
)
if
promised
&&
n
.
RawDigest
==
""
{
n
.
RawDigest
=
MissingPart
}
return
Name
{}
}
func
parseMask
(
s
string
)
Name
{
var
r
Name
parts
(
s
)(
func
(
kind
PartKind
,
part
string
)
bool
{
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
)
// "/" is an illegal tag character, so we can use it to split the host
if
strings
.
LastIndex
(
s
,
":"
)
>
strings
.
LastIndex
(
s
,
"/"
)
{
s
,
n
.
Tag
,
_
=
cutPromised
(
s
,
":"
)
}
return
r
}
// fillName fills in the missing parts of dst with the parts of src.
//
// The returned Name will only be valid if dst is valid.
//
// 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
])
s
,
n
.
Model
,
promised
=
cutPromised
(
s
,
"/"
)
if
!
promised
{
n
.
Model
=
s
return
n
}
return
r
}
// WithBuild returns a copy of r with the build set to the given string.
func
(
r
Name
)
WithBuild
(
build
string
)
Name
{
r
.
parts
[
PartBuild
]
=
build
return
r
}
func
(
r
Name
)
WithDigest
(
digest
Digest
)
Name
{
r
.
parts
[
PartDigest
]
=
digest
.
String
()
return
r
}
var
mapHashSeed
=
maphash
.
MakeSeed
()
s
,
n
.
Namespace
,
promised
=
cutPromised
(
s
,
"/"
)
if
!
promised
{
n
.
Namespace
=
s
return
n
}
// MapHash returns a case insensitive hash for use in maps and equality
// checks. For a convenient way to compare names, use [Name.EqualFold].
//
//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
)
}
scheme
,
host
,
ok
:=
strings
.
Cut
(
s
,
"://"
)
if
!
ok
{
host
=
scheme
}
return
h
.
Sum64
()
}
n
.
Host
=
host
func
(
r
Name
)
slice
(
from
,
to
PartKind
)
Name
{
var
v
Name
copy
(
v
.
parts
[
from
:
to
+
1
],
r
.
parts
[
from
:
to
+
1
])
return
v
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.
// ParseNameFromFilepath parses a 4-part filepath as a Name. The parts are
// expected to be in the form:
//
// 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
{
if
!
strings
.
EqualFold
(
r
.
parts
[
i
],
d
.
parts
[
i
])
{
break
}
r
.
parts
[
i
]
=
""
// { host } "/" { namespace } "/" { model } "/" { tag }
func
ParseNameFromFilepath
(
s
string
)
(
n
Name
)
{
parts
:=
strings
.
Split
(
s
,
string
(
filepath
.
Separator
))
if
len
(
parts
)
!=
4
{
return
Name
{}
}
for
i
:=
PartTag
;
i
>=
0
;
i
--
{
if
!
strings
.
EqualFold
(
r
.
parts
[
i
],
d
.
parts
[
i
])
{
break
}
r
.
parts
[
i
]
=
""
n
.
Host
=
parts
[
0
]
n
.
Namespace
=
parts
[
1
]
n
.
Model
=
parts
[
2
]
n
.
Tag
=
parts
[
3
]
if
!
n
.
IsFullyQualified
()
{
return
Name
{}
}
return
r
.
slice
(
PartHost
,
PartTag
)
.
DisplayLong
()
}
// DisplayLongest returns the result of r.DisplayShortest(MaskNothing).
func
(
r
Name
)
DisplayLongest
()
string
{
return
r
.
DisplayShortest
(
MaskNothing
)
return
n
}
var
seps
=
[
...
]
string
{
PartHost
:
"/"
,
PartNamespace
:
"/"
,
PartModel
:
":"
,
PartTag
:
"+"
,
PartBuild
:
"@"
,
PartDigest
:
""
,
// Merge merges the host, namespace, and tag parts of the two names,
// preferring the non-empty parts of a.
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
}
// WriteTo implements io.WriterTo. It writes the fullest possible display
// string in form:
//
// <host>/<namespace>/<model>:<tag>+<build>@<digest-type>-<digest>
//
// Missing parts and their separators are not written.
//
// The full digest is always prefixed with "@". That is if [Name.IsValid]
// 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
_
,
err
:=
w
.
WriteString
(
r
.
parts
[
i
]);
err
!=
nil
{
return
err
}
partsWritten
++
// String returns the name string, in the format that [ParseNameNoDefaults]
// accepts as valid, if [Name.IsValid] reports true; otherwise the empty
// string is returned.
func
(
n
Name
)
String
()
string
{
var
b
strings
.
Builder
if
n
.
Host
!=
""
{
b
.
WriteString
(
n
.
Host
)
b
.
WriteByte
(
'/'
)
}
if
n
.
Namespace
!=
""
{
b
.
WriteString
(
n
.
Namespace
)
b
.
WriteByte
(
'/'
)
}
b
.
WriteString
(
n
.
Model
)
if
n
.
Tag
!=
""
{
b
.
WriteByte
(
':'
)
b
.
WriteString
(
n
.
Tag
)
}
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
()
}
// GoString implements fmt.GoStringer. It returns a string suitable for
// debugging and logging. It is similar to [Name.DisplayLong] but it always
// returns a string that includes all parts of the Name, with missing parts
// replaced with a ("?").
func
(
r
Name
)
GoString
()
string
{
for
i
:=
range
r
.
parts
{
r
.
parts
[
i
]
=
cmp
.
Or
(
r
.
parts
[
i
],
"?"
)
}
return
r
.
DisplayLong
()
}
// DisplayShort returns a short string version of the name.
func
(
n
Name
)
DisplayShortest
()
string
{
var
sb
strings
.
Builder
// LogValue implements slog.Valuer.
func
(
r
Name
)
LogValue
()
slog
.
Value
{
return
slog
.
StringValue
(
r
.
GoString
())
}
if
n
.
Host
!=
defaultHost
{
sb
.
WriteString
(
n
.
Host
)
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
// domain, namespace, name, tag, and build.
func
(
r
Name
)
IsComplete
()
bool
{
return
!
slices
.
Contains
(
r
.
parts
[
:
PartDigest
],
""
)
// always include model and tag
sb
.
WriteString
(
n
.
Model
)
sb
.
WriteString
(
":"
)
sb
.
WriteString
(
n
.
Tag
)
return
sb
.
String
()
}
// IsCompleteNoBuild is like [Name.IsComplete] but it does not require the
// build part to be present.
func
(
r
Name
)
IsCompleteNoBuild
()
bool
{
return
!
slices
.
Contains
(
r
.
parts
[
:
PartBuild
],
""
)
// IsValid reports whether all parts of the name are present and valid. The
// digest is a special case, and is checked for validity only if present.
func
(
n
Name
)
IsValid
()
bool
{
if
n
.
RawDigest
!=
""
&&
!
isValidPart
(
kindDigest
,
n
.
RawDigest
)
{
return
false
}
return
n
.
IsFullyQualified
()
}
// IsResolved reports true if the Name has a valid digest.
//
// It is possible to have a valid Name, or a complete Name that is not
// resolved.
func
(
r
Name
)
IsResolved
()
bool
{
return
r
.
Digest
()
.
IsValid
()
// IsFullyQualified returns true if all parts of the name are present and
// valid without the digest.
func
(
n
Name
)
IsFullyQualified
()
bool
{
var
parts
=
[]
string
{
n
.
Host
,
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
// 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.
// {host}/{namespace}/{model}/{tag}
//
//
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].
func
(
r
Name
)
CompareFold
(
o
Name
)
int
{
return
slices
.
CompareFunc
(
r
.
parts
[
:
],
o
.
parts
[
:
],
compareFold
)
}
func
compareFold
(
a
,
b
string
)
int
{
return
slices
.
CompareFunc
([]
rune
(
a
),
[]
rune
(
b
),
func
(
a
,
b
rune
)
int
{
return
cmp
.
Compare
(
downcase
(
a
),
downcase
(
b
))
})
}
func
downcase
(
r
rune
)
rune
{
if
r
>=
'A'
&&
r
<=
'Z'
{
return
r
-
'A'
+
'a'
// It panics if the name is not fully qualified. Use [Name.IsFullyQualified]
// to check if the name is fully qualified.
func
(
n
Name
)
Filepath
()
string
{
if
!
n
.
IsFullyQualified
()
{
panic
(
"illegal attempt to get filepath of invalid name"
)
}
return
filepath
.
Join
(
strings
.
ToLower
(
filepath
.
Join
(
n
.
Host
,
n
.
Namespace
,
n
.
Model
,
)),
n
.
Tag
,
)
}
// 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
(
r
Name
)
Namespace
()
string
{
return
r
.
parts
[
PartNamespace
]
}
func
(
r
Name
)
Model
()
string
{
return
r
.
parts
[
PartModel
]
}
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
func
isValidPart
(
kind
partKind
,
s
string
)
bool
{
if
!
isValidLen
(
kind
,
s
)
{
return
false
}
for
i
:=
range
s
{
if
i
==
0
{
if
!
isAlphanumericOrUnderscore
(
s
[
i
])
{
return
false
}
continue
}
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
'_'
,
'-'
:
case
'.'
:
if
kind
==
kindNamespace
{
return
false
}
switch
s
[
i
]
{
case
'@'
:
switch
state
{
case
PartDigest
:
if
!
yield
(
PartDigest
,
s
[
i
+
1
:
j
])
{
return
}
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
}
case
':'
:
if
kind
!=
kindHost
&&
kind
!=
kindDigest
{
return
false
}
default
:
if
!
isAlphanumericOrUnderscore
(
s
[
i
])
{
return
false
}
}
if
state
<=
PartNamespace
{
yield
(
state
,
s
[
:
j
])
}
else
{
yield
(
PartModel
,
s
[
:
j
])
}
}
return
true
}
func
(
r
Name
)
IsZero
(
)
bool
{
return
r
.
parts
==
[
NumParts
]
string
{}
func
isAlphanumericOrUnderscore
(
c
byte
)
bool
{
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
(
r
Name
)
IsValid
()
bool
{
// Parts ensures we only have valid parts, so no need to validate
// them here, only check if we have a name or not.
return
r
.
parts
[
PartModel
]
!=
""
func
cutLast
(
s
,
sep
string
)
(
before
,
after
string
,
ok
bool
)
{
i
:=
strings
.
LastIndex
(
s
,
sep
)
if
i
>=
0
{
return
s
[
:
i
],
s
[
i
+
len
(
sep
)
:
],
true
}
return
s
,
""
,
false
}
// ParseNameFromURLPath parses forms of a URL path into a Name. Specifically,
// it trims any leading "/" and then calls [ParseName] with fill.
func
ParseNameFromURLPath
(
s
,
fill
string
)
Name
{
s
=
strings
.
TrimPrefix
(
s
,
"/"
)
return
ParseName
(
s
,
fill
)
// cutPromised cuts the last part of s at the last occurrence of sep. If sep is
// found, the part before and after sep are returned as-is unless empty, in
// which case they are returned as MissingPart, which will cause
// [Name.IsValid] to return false.
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
// 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
)
}
type
DigestType
byte
// ParseNameFromFilepath parses a file path into a Name. The input string must be a
// valid file path representation of a model name in the form:
//
// 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
)
}
const
(
DigestTypeInvalid
DigestType
=
iota
DigestTypeSHA256
)
// 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
])
}
func
(
t
DigestType
)
String
()
string
{
switch
t
{
case
DigestTypeSHA256
:
return
"sha256"
default
:
return
"invalid"
}
return
filepath
.
Join
(
r
.
parts
[
:
]
...
)
}
// FilepathNoBuild returns a complete, canonicalized, relative file path using
// the parts of a complete Name, but without the build part.
func
(
r
Name
)
FilepathNoBuild
()
string
{
for
i
:=
range
PartBuild
{
r
.
parts
[
i
]
=
strings
.
ToLower
(
r
.
parts
[
i
])
}
return
filepath
.
Join
(
r
.
parts
[
:
PartBuild
]
...
)
type
Digest
struct
{
Type
DigestType
Sum
[
32
]
byte
}
// isValidPart reports if s contains all valid characters for the given
// part kind.
func
isValidPart
(
kind
PartKind
,
s
string
)
bool
{
if
s
==
""
{
return
false
func
ParseDigest
(
s
string
)
(
Digest
,
error
)
{
i
:=
strings
.
IndexAny
(
s
,
"-:"
)
if
i
<
0
{
return
Digest
{},
fmt
.
Errorf
(
"invalid digest %q"
,
s
)
}
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
}
typ
,
encSum
:=
s
[
:
i
],
s
[
i
+
1
:
]
if
typ
!=
"sha256"
{
return
Digest
{},
fmt
.
Errorf
(
"unsupported digest type %q"
,
typ
)
}
return
true
}
func
isValidByteFor
(
kind
PartKind
,
c
byte
)
bool
{
if
kind
==
PartNamespace
&&
c
==
'.'
{
return
false
d
:=
Digest
{
Type
:
DigestTypeSHA256
,
}
if
kind
==
PartHost
&&
c
==
':'
{
return
true
n
,
err
:=
hex
.
Decode
(
d
.
Sum
[
:
],
[]
byte
(
encSum
))
if
err
!=
nil
{
return
Digest
{},
err
}
if
c
==
'.'
||
c
==
'-'
{
return
true
if
n
!=
32
{
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
true
return
d
,
nil
}
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
}
types/model/name_test.go
View file @
920a4b07
package
model
import
(
"bytes"
"cmp"
"fmt"
"log/slog"
"path/filepath"
"
slices
"
"
strings
"
"
reflect
"
"
runtime
"
"testing"
)
type
fields
struct
{
host
,
namespace
,
model
,
tag
,
build
string
digest
string
}
func
fieldsFromName
(
p
Name
)
fields
{
return
fields
{
host
:
p
.
parts
[
PartHost
],
namespace
:
p
.
parts
[
PartNamespace
],
model
:
p
.
parts
[
PartModel
],
tag
:
p
.
parts
[
PartTag
],
build
:
p
.
parts
[
PartBuild
],
digest
:
p
.
parts
[
PartDigest
],
}
}
var
testNames
=
map
[
string
]
fields
{
"mistral:latest"
:
{
model
:
"mistral"
,
tag
:
"latest"
},
"mistral"
:
{
model
:
"mistral"
},
"mistral:30B"
:
{
model
:
"mistral"
,
tag
:
"30B"
},
"mistral:7b"
:
{
model
:
"mistral"
,
tag
:
"7b"
},
"mistral:7b+Q4_0"
:
{
model
:
"mistral"
,
tag
:
"7b"
,
build
:
"Q4_0"
},
"mistral+KQED"
:
{
model
:
"mistral"
,
build
:
"KQED"
},
"mistral.x-3:7b+Q4_0"
:
{
model
:
"mistral.x-3"
,
tag
:
"7b"
,
build
:
"Q4_0"
},
"mistral:7b+q4_0"
:
{
model
:
"mistral"
,
tag
:
"7b"
,
build
:
"q4_0"
},
"llama2"
:
{
model
:
"llama2"
},
"user/model"
:
{
namespace
:
"user"
,
model
:
"model"
},
"example.com/ns/mistral:7b+Q4_0"
:
{
host
:
"example.com"
,
namespace
:
"ns"
,
model
:
"mistral"
,
tag
:
"7b"
,
build
:
"Q4_0"
},
"example.com/ns/mistral:7b+X"
:
{
host
:
"example.com"
,
namespace
:
"ns"
,
model
:
"mistral"
,
tag
:
"7b"
,
build
:
"X"
},
"localhost:5000/ns/mistral"
:
{
host
:
"localhost:5000"
,
namespace
:
"ns"
,
model
:
"mistral"
},
// invalid digest
"mistral:latest@invalid256-"
:
{},
"mistral:latest@-123"
:
{},
"mistral:latest@!-123"
:
{},
"mistral:latest@1-!"
:
{},
"mistral:latest@"
:
{},
// resolved
"x@sha123-1"
:
{
model
:
"x"
,
digest
:
"sha123-1"
},
"@sha456-2"
:
{
digest
:
"sha456-2"
},
"@@sha123-1"
:
{},
// preserves case for build
"x+b"
:
{
model
:
"x"
,
build
:
"b"
},
// invalid (includes fuzzing trophies)
" / / : + "
:
{},
" / : + "
:
{},
" : + "
:
{},
" + "
:
{},
" : "
:
{},
" / "
:
{},
" /"
:
{},
"/ "
:
{},
"/"
:
{},
":"
:
{},
"+"
:
{},
// (".") in namepsace is not allowed
"invalid.com/7b+x"
:
{},
"invalid:7b+Q4_0:latest"
:
{},
"in valid"
:
{},
"invalid/y/z/foo"
:
{},
"/0"
:
{},
"0 /0"
:
{},
"0 /"
:
{},
"0/"
:
{},
":/0"
:
{},
"+0/00000"
:
{},
"0+.
\xf2\x80\xf6\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
)
:
{},
}
// 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
)
}
})
}
}
const
(
part80
=
"88888888888888888888888888888888888888888888888888888888888888888888888888888888"
part350
=
"33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333"
)
func
Test
CompleteWithAndWithoutBuild
(
t
*
testing
.
T
)
{
func
Test
ParseNameParts
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
in
string
complete
bool
completeNoBuild
bool
}{
{
""
,
false
,
false
},
{
"incomplete/mistral:7b+x"
,
false
,
false
},
{
"incomplete/mistral:7b+Q4_0"
,
false
,
false
},
{
"incomplete:7b+x"
,
false
,
false
},
{
"complete.com/x/mistral:latest+Q4_0"
,
true
,
true
},
{
"complete.com/x/mistral:latest"
,
false
,
true
},
}
for
_
,
tt
:=
range
cases
{
t
.
Run
(
tt
.
in
,
func
(
t
*
testing
.
T
)
{
p
:=
ParseName
(
tt
.
in
,
FillNothing
)
t
.
Logf
(
"ParseName(%q) = %#v"
,
tt
.
in
,
p
)
if
g
:=
p
.
IsComplete
();
g
!=
tt
.
complete
{
t
.
Errorf
(
"Complete(%q) = %v; want %v"
,
tt
.
in
,
g
,
tt
.
complete
)
}
if
g
:=
p
.
IsCompleteNoBuild
();
g
!=
tt
.
completeNoBuild
{
t
.
Errorf
(
"CompleteNoBuild(%q) = %v; want %v"
,
tt
.
in
,
g
,
tt
.
completeNoBuild
)
}
})
}
// Complete uses Parts which returns a slice, but it should be
// inlined when used in Complete, preventing any allocations or
// escaping to the heap.
allocs
:=
testing
.
AllocsPerRun
(
1000
,
func
()
{
keep
(
ParseName
(
"complete.com/x/mistral:latest+Q4_0"
,
FillNothing
)
.
IsComplete
())
})
if
allocs
>
0
{
t
.
Errorf
(
"Complete allocs = %v; want 0"
,
allocs
)
}
}
func
TestNameLogValue
(
t
*
testing
.
T
)
{
cases
:=
[]
string
{
"example.com/library/mistral:latest+Q4_0"
,
"mistral:latest"
,
"mistral:7b+Q4_0"
,
}
for
_
,
s
:=
range
cases
{
t
.
Run
(
s
,
func
(
t
*
testing
.
T
)
{
var
b
bytes
.
Buffer
log
:=
slog
.
New
(
slog
.
NewTextHandler
(
&
b
,
nil
))
name
:=
ParseName
(
s
,
FillNothing
)
log
.
Info
(
""
,
"name"
,
name
)
want
:=
fmt
.
Sprintf
(
"name=%s"
,
name
.
GoString
())
got
:=
b
.
String
()
if
!
strings
.
Contains
(
got
,
want
)
{
t
.
Errorf
(
"expected log output to contain %q; got %q"
,
want
,
got
)
}
})
}
}
func
TestNameGoString
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
name
string
in
string
wantString
string
wantGoString
string
// default is tt.in
want
Name
wantFilepath
string
wantValidDigest
bool
}{
{
name
:
"Complete Name"
,
in
:
"example.com/library/mistral:latest+Q4_0"
,
wantGoString
:
"example.com/library/mistral:latest+Q4_0@?"
,
in
:
"registry.ollama.ai/library/dolphin-mistral:7b-v2.6-dpo-laser-q6_K"
,
want
:
Name
{
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
:
"mistral:latest"
,
wantGoString
:
"?/?/mistral:latest+?@?"
,
in
:
"host/namespace/model:tag"
,
want
:
Name
{
Host
:
"host"
,
Namespace
:
"namespace"
,
Model
:
"model"
,
Tag
:
"tag"
,
},
wantFilepath
:
filepath
.
Join
(
"host"
,
"namespace"
,
"model"
,
"tag"
),
},
{
name
:
"Long Name"
,
in
:
"library/mistral:latest"
,
wantGoString
:
"?/library/mistral:latest+?@?"
,
in
:
"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
:
"Case Preserved"
,
in
:
"Library/Mistral:Latest"
,
wantGoString
:
"?/Library/Mistral:Latest+?@?"
,
in
:
"host/namespace/model"
,
want
:
Name
{
Host
:
"host"
,
Namespace
:
"namespace"
,
Model
:
"model"
,
},
wantFilepath
:
filepath
.
Join
(
"host"
,
"namespace"
,
"model"
,
"latest"
),
},
{
name
:
"With digest"
,
in
:
"Library/Mistral:Latest@sha256-123456"
,
wantGoString
:
"?/Library/Mistral:Latest+?@sha256-123456"
,
in
:
"host:port/namespace/model"
,
want
:
Name
{
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
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
p
:=
ParseName
(
tt
.
in
,
FillNothing
)
tt
.
wantGoString
=
cmp
.
Or
(
tt
.
wantGoString
,
tt
.
in
)
if
g
:=
fmt
.
Sprintf
(
"%#v"
,
p
);
g
!=
tt
.
wantGoString
{
t
.
Errorf
(
"GoString() = %q; want %q"
,
g
,
tt
.
wantGoString
)
t
.
Run
(
tt
.
in
,
func
(
t
*
testing
.
T
)
{
got
:=
ParseNameBare
(
tt
.
in
)
if
!
reflect
.
DeepEqual
(
got
,
tt
.
want
)
{
t
.
Errorf
(
"parseName(%q) = %v; want %v"
,
tt
.
in
,
got
,
tt
.
want
)
}
})
}
}
func
TestDisplayLongest
(
t
*
testing
.
T
)
{
g
:=
ParseName
(
"example.com/library/mistral:latest+Q4_0"
,
FillNothing
)
.
DisplayLongest
()
if
g
!=
"example.com/library/mistral:latest"
{
t
.
Errorf
(
"got = %q; want %q"
,
g
,
"example.com/library/mistral:latest"
)
}
}
func
TestDisplayShortest
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
in
string
mask
string
want
string
wantPanic
bool
}{
{
"example.com/library/mistral:latest+Q4_0"
,
"example.com/library/_:latest"
,
"mistral"
,
false
},
{
"example.com/library/mistral:latest+Q4_0"
,
"example.com/_/_:latest"
,
"library/mistral"
,
false
},
{
"example.com/library/mistral:latest+Q4_0"
,
""
,
"example.com/library/mistral"
,
false
},
{
"example.com/library/mistral:latest+Q4_0"
,
""
,
"example.com/library/mistral"
,
false
},
// case-insensitive
{
"Example.com/library/mistral:latest+Q4_0"
,
"example.com/library/_:latest"
,
"mistral"
,
false
},
{
"example.com/Library/mistral:latest+Q4_0"
,
"example.com/library/_:latest"
,
"mistral"
,
false
},
{
"example.com/library/Mistral:latest+Q4_0"
,
"example.com/library/_:latest"
,
"Mistral"
,
false
},
{
"example.com/library/mistral:Latest+Q4_0"
,
"example.com/library/_:latest"
,
"mistral"
,
false
},
{
"example.com/library/mistral:Latest+q4_0"
,
"example.com/library/_:latest"
,
"mistral"
,
false
},
// zero value
{
""
,
MaskDefault
,
""
,
true
},
// invalid mask
{
"example.com/library/mistral:latest+Q4_0"
,
"example.com/mistral"
,
""
,
true
},
// DefaultMask
{
"registry.ollama.ai/library/mistral:latest+Q4_0"
,
MaskDefault
,
"mistral"
,
false
},
// Auto-Fill
{
"x"
,
"example.com/library/_:latest"
,
"x"
,
false
},
{
"x"
,
"example.com/library/_:latest+Q4_0"
,
"x"
,
false
},
{
"x/y:z"
,
"a.com/library/_:latest+Q4_0"
,
"x/y:z"
,
false
},
{
"x/y:z"
,
"a.com/library/_:latest+Q4_0"
,
"x/y:z"
,
false
},
}
for
_
,
tt
:=
range
cases
{
t
.
Run
(
""
,
func
(
t
*
testing
.
T
)
{
defer
func
()
{
if
tt
.
wantPanic
{
if
recover
()
==
nil
{
t
.
Errorf
(
"expected panic"
)
}
}
}()
p
:=
ParseName
(
tt
.
in
,
FillNothing
)
t
.
Logf
(
"ParseName(%q) = %#v"
,
tt
.
in
,
p
)
if
g
:=
p
.
DisplayShortest
(
tt
.
mask
);
g
!=
tt
.
want
{
t
.
Errorf
(
"got = %q; want %q"
,
g
,
tt
.
want
)
got
=
ParseName
(
tt
.
in
)
if
tt
.
wantFilepath
!=
""
&&
got
.
Filepath
()
!=
tt
.
wantFilepath
{
t
.
Errorf
(
"parseName(%q).Filepath() = %q; want %q"
,
tt
.
in
,
got
.
Filepath
(),
tt
.
wantFilepath
)
}
})
}
}
func
TestParseNameAllocs
(
t
*
testing
.
T
)
{
allocs
:=
testing
.
AllocsPerRun
(
1000
,
func
()
{
keep
(
ParseName
(
"example.com/mistral:7b+Q4_0"
,
FillNothing
))
})
if
allocs
>
0
{
t
.
Errorf
(
"ParseName allocs = %v; want 0"
,
allocs
)
}
var
testCases
=
map
[
string
]
bool
{
// name -> valid
""
:
false
,
"_why/_the/_lucky:_stiff"
:
true
,
// 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
)
{
b
.
ReportAllocs
()
for
range
b
.
N
{
keep
(
ParseName
(
"example.com/mistral:7b+Q4_0"
,
FillNothing
))
func
TestNameparseNameDefault
(
t
*
testing
.
T
)
{
const
name
=
"xx"
n
:=
ParseName
(
name
)
got
:=
n
.
String
()
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
)
{
f
.
Add
(
"example.com/library/mistral/7b/Q4_0"
)
f
.
Add
(
"example.com/../mistral/7b/Q4_0"
)
f
.
Add
(
"example.com/x/../7b/Q4_0"
)
f
.
Add
(
"example.com/x/../7b"
)
f
.
Fuzz
(
func
(
t
*
testing
.
T
,
s
string
)
{
name
:=
ParseNameFromFilepath
(
s
,
FillNothing
)
if
strings
.
Contains
(
s
,
".."
)
&&
!
name
.
IsZero
()
{
t
.
Fatalf
(
"non-zero value for path with '..': %q"
,
s
)
}
if
name
.
IsValid
()
==
name
.
IsZero
()
{
t
.
Errorf
(
"expected valid path to be non-zero value; got %#v"
,
name
)
}
})
}
func
FuzzParseName
(
f
*
testing
.
F
)
{
f
.
Add
(
"example.com/mistral:7b+Q4_0"
)
f
.
Add
(
"example.com/mistral:7b+q4_0"
)
f
.
Add
(
"example.com/mistral:7b+x"
)
f
.
Add
(
"x/y/z:8n+I"
)
f
.
Add
(
":x"
)
f
.
Add
(
"@sha256-123456"
)
f
.
Add
(
"example.com/mistral:latest+Q4_0@sha256-123456"
)
f
.
Add
(
":@!@"
)
f
.
Add
(
"..."
)
f
.
Fuzz
(
func
(
t
*
testing
.
T
,
s
string
)
{
r0
:=
ParseName
(
s
,
FillNothing
)
if
strings
.
Contains
(
s
,
".."
)
&&
!
r0
.
IsZero
()
{
t
.
Fatalf
(
"non-zero value for path with '..': %q"
,
s
)
func
TestNameIsValid
(
t
*
testing
.
T
)
{
var
numStringTests
int
for
s
,
want
:=
range
testCases
{
n
:=
ParseNameBare
(
s
)
got
:=
n
.
IsValid
()
if
got
!=
want
{
t
.
Errorf
(
"parseName(%q).IsValid() = %v; want %v"
,
s
,
got
,
want
)
}
if
!
r0
.
IsValid
()
&&
!
r0
.
IsResolved
()
{
if
!
r0
.
EqualFold
(
Name
{})
{
t
.
Errorf
(
"expected invalid path to be zero value; got %#v"
,
r0
)
// Test roundtrip with String
if
got
{
got
:=
ParseNameBare
(
s
)
.
String
()
if
got
!=
s
{
t
.
Errorf
(
"parseName(%q).String() = %q; want %q"
,
s
,
got
,
s
)
}
t
.
Skipf
(
"invalid path: %q"
,
s
)
}
for
_
,
p
:=
range
r0
.
parts
{
if
len
(
p
)
>
MaxNamePartLen
{
t
.
Errorf
(
"part too long: %q"
,
p
)
}
}
if
!
strings
.
EqualFold
(
r0
.
DisplayLong
(),
s
)
{
t
.
Errorf
(
"String() did not round-trip with case insensitivity: %q
\n
got = %q
\n
want = %q"
,
s
,
r0
.
DisplayLong
(),
s
)
}
r1
:=
ParseName
(
r0
.
DisplayLong
(),
FillNothing
)
if
!
r0
.
EqualFold
(
r1
)
{
t
.
Errorf
(
"round-trip mismatch: %+v != %+v"
,
r0
,
r1
)
numStringTests
++
}
})
}
}
func
TestNameStringAllocs
(
t
*
testing
.
T
)
{
name
:=
ParseName
(
"example.com/ns/mistral:latest+Q4_0"
,
FillNothing
)
allocs
:=
testing
.
AllocsPerRun
(
1000
,
func
()
{
keep
(
name
.
DisplayLong
())
})
if
allocs
>
1
{
t
.
Errorf
(
"String allocs = %v; want 0"
,
allocs
)
if
numStringTests
==
0
{
t
.
Errorf
(
"no tests for Name.String"
)
}
}
func
TestName
Path
(
t
*
testing
.
T
)
{
func
TestName
IsValidPart
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
in
string
want
string
kind
partKind
s
string
want
bool
}{
{
"example.com/library/mistral:latest+Q4_0"
,
"example.com/library/mistral:latest"
},
// incomplete
{
"example.com/library/mistral:latest"
,
"example.com/library/mistral:latest"
},
{
""
,
""
},
{
kind
:
kindHost
,
s
:
""
,
want
:
false
},
{
kind
:
kindHost
,
s
:
"a"
,
want
:
true
},
{
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
},
}
for
_
,
tt
:=
range
cases
{
t
.
Run
(
tt
.
in
,
func
(
t
*
testing
.
T
)
{
p
:=
ParseName
(
tt
.
in
,
FillNothing
)
t
.
Logf
(
"ParseName(%q) = %#v"
,
tt
.
in
,
p
)
if
g
:=
p
.
URLPath
();
g
!=
tt
.
want
{
t
.
Errorf
(
"got = %q; want %q"
,
g
,
tt
.
want
)
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
TestNameFilepath
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
in
string
want
string
wantNoBuild
string
}{
{
in
:
"example.com/library/mistral:latest+Q4_0"
,
want
:
"example.com/library/mistral/latest/Q4_0"
,
wantNoBuild
:
"example.com/library/mistral/latest"
,
},
{
in
:
"Example.Com/Library/Mistral:Latest+Q4_0"
,
want
:
"example.com/library/mistral/latest/Q4_0"
,
wantNoBuild
:
"example.com/library/mistral/latest"
,
},
{
in
:
"Example.Com/Library/Mistral:Latest+Q4_0"
,
want
:
"example.com/library/mistral/latest/Q4_0"
,
wantNoBuild
:
"example.com/library/mistral/latest"
,
},
{
in
:
"example.com/library/mistral:latest"
,
want
:
"example.com/library/mistral/latest"
,
wantNoBuild
:
"example.com/library/mistral/latest"
,
},
{
in
:
""
,
want
:
""
,
wantNoBuild
:
""
,
},
func
TestFilepathAllocs
(
t
*
testing
.
T
)
{
n
:=
ParseNameBare
(
"HOST/NAMESPACE/MODEL:TAG"
)
allocs
:=
testing
.
AllocsPerRun
(
1000
,
func
()
{
n
.
Filepath
()
})
var
allowedAllocs
float64
=
3
if
runtime
.
GOOS
==
"windows"
{
allowedAllocs
=
5
}
for
_
,
tt
:=
range
cases
{
t
.
Run
(
tt
.
in
,
func
(
t
*
testing
.
T
)
{
p
:=
ParseName
(
tt
.
in
,
FillNothing
)
t
.
Logf
(
"ParseName(%q) = %#v"
,
tt
.
in
,
p
)
g
:=
p
.
Filepath
()
g
=
filepath
.
ToSlash
(
g
)
if
g
!=
tt
.
want
{
t
.
Errorf
(
"got = %q; want %q"
,
g
,
tt
.
want
)
}
g
=
p
.
FilepathNoBuild
()
g
=
filepath
.
ToSlash
(
g
)
if
g
!=
tt
.
wantNoBuild
{
t
.
Errorf
(
"got = %q; want %q"
,
g
,
tt
.
wantNoBuild
)
}
})
if
allocs
>
allowedAllocs
{
t
.
Errorf
(
"allocs = %v; allowed %v"
,
allocs
,
allowedAllocs
)
}
}
func
TestParseNameFilepath
(
t
*
testing
.
T
)
{
const
(
validSha256
=
"sha256-1000000000000000000000000000000000000000000000000000000000000000"
validSha256Old
=
"sha256:1000000000000000000000000000000000000000000000000000000000000000"
)
func
TestParseDigest
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
in
string
fill
string
// default is FillNothing
want
string
}{
{
in
:
"example.com/library/mistral/latest/Q4_0"
,
want
:
"example.com/library/mistral:latest+Q4_0"
,
},
{
in
:
"example.com/library/mistral/latest"
,
fill
:
"?/?/?:latest+Q4_0"
,
want
:
"example.com/library/mistral:latest+Q4_0"
,
},
{
in
:
"example.com/library/mistral"
,
fill
:
"?/?/?:latest+Q4_0"
,
want
:
"example.com/library/mistral:latest+Q4_0"
,
},
{
in
:
"example.com/library"
,
want
:
""
,
},
{
in
:
"example.com/"
,
want
:
""
,
},
{
in
:
"example.com/^/mistral/latest/Q4_0"
,
want
:
""
,
},
{
in
:
"example.com/library/mistral/../Q4_0"
,
want
:
""
,
},
{
in
:
"example.com/library/mistral/latest/Q4_0/extra"
,
want
:
""
,
},
{
""
,
""
},
// empty
{
"sha123-12"
,
""
},
// invalid type
{
"sha256-"
,
""
},
// invalid sum
{
"sha256-123"
,
""
},
// invalid odd length sum
{
validSha256
,
validSha256
},
{
validSha256Old
,
validSha256
},
}
for
_
,
tt
:=
range
cases
{
t
.
Run
(
tt
.
in
,
func
(
t
*
testing
.
T
)
{
in
:=
strings
.
ReplaceAll
(
tt
.
in
,
"/"
,
string
(
filepath
.
Separator
))
fill
:=
cmp
.
Or
(
tt
.
fill
,
FillNothing
)
want
:=
ParseName
(
tt
.
want
,
fill
)
if
g
:=
ParseNameFromFilepath
(
in
,
fill
);
!
g
.
EqualFold
(
want
)
{
t
.
Errorf
(
"got = %q; want %q"
,
g
.
DisplayLong
(),
tt
.
want
)
got
,
err
:=
ParseDigest
(
tt
.
in
)
if
err
!=
nil
{
if
tt
.
want
!=
""
{
t
.
Errorf
(
"parseDigest(%q) = %v; want %v"
,
tt
.
in
,
err
,
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
)
{
cases
:=
[]
struct
{
in
string
want
string
fill
string
// default is FillNothing
}{
{
in
:
"example.com/library/mistral:latest+Q4_0"
,
want
:
"example.com/library/mistral:latest+Q4_0"
,
},
{
in
:
"/example.com/library/mistral:latest+Q4_0"
,
want
:
"example.com/library/mistral:latest+Q4_0"
,
},
{
in
:
"/example.com/library/mistral"
,
want
:
"example.com/library/mistral"
,
},
{
in
:
"/example.com/library/mistral"
,
fill
:
"?/?/?:latest+Q4_0"
,
want
:
"example.com/library/mistral:latest+Q4_0"
,
},
{
in
:
"/example.com/library"
,
want
:
""
,
},
{
in
:
"/example.com/"
,
want
:
""
,
},
{
in
:
"/example.com/^/mistral/latest"
,
want
:
""
,
},
}
for
_
,
tt
:=
range
cases
{
t
.
Run
(
tt
.
in
,
func
(
t
*
testing
.
T
)
{
fill
:=
cmp
.
Or
(
tt
.
fill
,
FillNothing
)
if
g
:=
ParseNameFromURLPath
(
tt
.
in
,
fill
);
g
.
DisplayLong
()
!=
tt
.
want
{
t
.
Errorf
(
"got = %q; want %q"
,
g
.
DisplayLong
(),
tt
.
want
)
func
TestParseNameFromFilepath
(
t
*
testing
.
T
)
{
cases
:=
map
[
string
]
Name
{
filepath
.
Join
(
"host"
,
"namespace"
,
"model"
,
"tag"
)
:
{
Host
:
"host"
,
Namespace
:
"namespace"
,
Model
:
"model"
,
Tag
:
"tag"
},
filepath
.
Join
(
"host:port"
,
"namespace"
,
"model"
,
"tag"
)
:
{
Host
:
"host:port"
,
Namespace
:
"namespace"
,
Model
:
"model"
,
Tag
:
"tag"
},
filepath
.
Join
(
"namespace"
,
"model"
,
"tag"
)
:
{},
filepath
.
Join
(
"model"
,
"tag"
)
:
{},
filepath
.
Join
(
"model"
)
:
{},
filepath
.
Join
(
".."
,
".."
,
"model"
,
"tag"
)
:
{},
filepath
.
Join
(
""
,
"namespace"
,
"."
,
"tag"
)
:
{},
filepath
.
Join
(
"."
,
"."
,
"."
,
"."
)
:
{},
filepath
.
Join
(
"/"
,
"path"
,
"to"
,
"random"
,
"file"
)
:
{},
}
for
in
,
want
:=
range
cases
{
t
.
Run
(
in
,
func
(
t
*
testing
.
T
)
{
got
:=
ParseNameFromFilepath
(
in
)
if
!
reflect
.
DeepEqual
(
got
,
want
)
{
t
.
Errorf
(
"parseNameFromFilepath(%q) = %v; want %v"
,
in
,
got
,
want
)
}
})
}
}
func
ExampleName_MapHash
()
{
m
:=
map
[
uint64
]
bool
{}
// key 1
m
[
ParseName
(
"mistral:latest+q4"
,
FillNothing
)
.
MapHash
()]
=
true
m
[
ParseName
(
"miSTRal:latest+Q4"
,
FillNothing
)
.
MapHash
()]
=
true
m
[
ParseName
(
"mistral:LATest+Q4"
,
FillNothing
)
.
MapHash
()]
=
true
// key 2
m
[
ParseName
(
"mistral:LATest"
,
FillNothing
)
.
MapHash
()]
=
true
fmt
.
Println
(
len
(
m
))
// Output:
// 2
}
func
ExampleName_CompareFold_sort
()
{
names
:=
[]
Name
{
ParseName
(
"mistral:latest"
,
FillNothing
),
ParseName
(
"mistRal:7b+q4"
,
FillNothing
),
ParseName
(
"MIstral:7b"
,
FillNothing
),
}
slices
.
SortFunc
(
names
,
Name
.
CompareFold
)
for
_
,
n
:=
range
names
{
fmt
.
Println
(
n
.
DisplayLong
())
func
TestDisplayShortest
(
t
*
testing
.
T
)
{
cases
:=
map
[
string
]
string
{
"registry.ollama.ai/library/model:latest"
:
"model:latest"
,
"registry.ollama.ai/library/model:tag"
:
"model:tag"
,
"registry.ollama.ai/namespace/model:tag"
:
"namespace/model:tag"
,
"host/namespace/model:tag"
:
"host/namespace/model:tag"
,
"host/library/model:tag"
:
"host/library/model:tag"
,
}
for
in
,
want
:=
range
cases
{
t
.
Run
(
in
,
func
(
t
*
testing
.
T
)
{
got
:=
ParseNameBare
(
in
)
.
DisplayShortest
()
if
got
!=
want
{
t
.
Errorf
(
"parseName(%q).DisplayShortest() = %q; want %q"
,
in
,
got
,
want
)
}
})
}
// Output:
// MIstral:7b
// mistRal:7b+q4
// mistral:latest
}
func
ExampleName_completeAndResolved
()
{
for
_
,
s
:=
range
[]
string
{
"x/y/z:latest+q4_0@sha123-1"
,
"x/y/z:latest+q4_0"
,
"@sha123-1"
,
}
{
name
:=
ParseName
(
s
,
FillNothing
)
fmt
.
Printf
(
"complete:%v resolved:%v digest:%s
\n
"
,
name
.
IsComplete
(),
name
.
IsResolved
(),
name
.
Digest
())
func
FuzzName
(
f
*
testing
.
F
)
{
for
s
:=
range
testCases
{
f
.
Add
(
s
)
}
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
}
types/model/testdata/fuzz/Fuzz
ParseRef/1d43ee52085cb4aa
→
types/model/testdata/fuzz/Fuzz
Name/d37463aa416f6bab
View file @
920a4b07
go test fuzz v1
string("
/
0")
string("0
0@
")
types/model/testdata/fuzz/FuzzParseRef/27fd759314f0e6d6
deleted
100644 → 0
View file @
c496967e
go test fuzz v1
string("0//0")
types/model/testdata/fuzz/FuzzParseRef/3e3b70dba384074d
deleted
100644 → 0
View file @
c496967e
go test fuzz v1
string("0 /0")
types/model/testdata/fuzz/FuzzParseRef/71f1fdff711b6dab
deleted
100644 → 0
View file @
c496967e
go test fuzz v1
string("+0/00000")
types/model/testdata/fuzz/FuzzParseRef/82c2975c430ac608
deleted
100644 → 0
View file @
c496967e
go test fuzz v1
string(":")
types/model/testdata/fuzz/FuzzParseRef/b51b1c875e61a948
deleted
100644 → 0
View file @
c496967e
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")
types/structs/structs.go
deleted
100644 → 0
View file @
c496967e
// 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
()
Prev
1
…
3
4
5
6
7
Next
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