x/model: disallow . in namespace

This commit is contained in:
Blake Mizerany 2024-04-05 16:23:56 -07:00
parent f51197a814
commit a6b8bdf938
2 changed files with 53 additions and 47 deletions

View File

@ -56,7 +56,7 @@ var kindNames = map[NamePart]string{
// To check if a Name is fully qualified, use [Name.Complete]. A fully // To check if a Name is fully qualified, use [Name.Complete]. A fully
// qualified name has all parts present. // qualified name has all parts present.
// //
// To update parts of a Name with defaults, use [Merge]. // To update parts of a Name with defaults, use [Fill].
type Name struct { type Name struct {
_ structs.Incomparable _ structs.Incomparable
@ -122,19 +122,15 @@ func ParseName(s string) Name {
return r return r
} }
// Merge performs a partial merge of src into dst. Only the non-name parts // Fill fills in the missing parts of dst with the parts of src.
// are merged. The name part is always left untouched. Other parts are
// merged if and only if they are missing in dst.
// //
// Use this for merging a fully qualified ref with a partial ref, such as // Use this for merging a fully qualified ref with a partial ref, such as
// when filling in a missing parts with defaults. // when filling in a missing parts with defaults.
// //
// The returned Name will only be valid if dst is valid. // The returned Name will only be valid if dst is valid.
func Merge(dst, src Name) Name { func Fill(dst, src Name) Name {
return Name{ return Name{
// name is left untouched model: cmp.Or(dst.model, src.model),
model: dst.model,
host: cmp.Or(dst.host, src.host), host: cmp.Or(dst.host, src.host),
namespace: cmp.Or(dst.namespace, src.namespace), namespace: cmp.Or(dst.namespace, src.namespace),
tag: cmp.Or(dst.tag, src.tag), tag: cmp.Or(dst.tag, src.tag),
@ -223,24 +219,10 @@ func (r Name) Complete() bool {
return r.Valid() && !slices.Contains(r.Parts(), "") return r.Valid() && !slices.Contains(r.Parts(), "")
} }
// CompleteWithoutBuild reports whether the ref would be complete if it had a // TODO(bmizerany): Compare
// valid build. // TODO(bmizerany): MarshalText/UnmarshalText
func (r Name) CompleteWithoutBuild() bool { // TODO(bmizerany): LogValue
r.build = "x" // TODO(bmizerany): driver.Value? (MarshalText etc should be enough)
return r.Valid() && r.Complete()
}
// Less returns true if r is less concrete than o; false otherwise.
func (r Name) Less(o Name) bool {
rp := r.Parts()
op := o.Parts()
for i := range rp {
if rp[i] < op[i] {
return true
}
}
return false
}
// Parts returns the parts of the ref in order of concreteness. // Parts returns the parts of the ref in order of concreteness.
// //
@ -289,7 +271,7 @@ func NameParts(s string) iter.Seq2[NamePart, string] {
} }
yieldValid := func(kind NamePart, part string) bool { yieldValid := func(kind NamePart, part string) bool {
if !isValidPart(part) { if !isValidPart(kind, part) {
yield(Invalid, "") yield(Invalid, "")
return false return false
} }
@ -338,7 +320,7 @@ func NameParts(s string) iter.Seq2[NamePart, string] {
return return
} }
default: default:
if !isValidPart(s[i : i+1]) { if !isValidPart(state, s[i:i+1]) {
yield(Invalid, "") yield(Invalid, "")
return return
} }
@ -362,20 +344,27 @@ func (r Name) Valid() bool {
} }
// isValidPart returns true if given part is valid ascii [a-zA-Z0-9_\.-] // isValidPart returns true if given part is valid ascii [a-zA-Z0-9_\.-]
func isValidPart(s string) bool { func isValidPart(kind NamePart, s string) bool {
if s == "" { if s == "" {
return false return false
} }
for _, c := range []byte(s) { for _, c := range []byte(s) {
if !isValidByte(kind, c) {
return false
}
}
return true
}
func isValidByte(kind NamePart, c byte) bool {
if kind == Namespace && c == '.' {
return false
}
if c == '.' || c == '-' { if c == '.' || c == '-' {
return true return true
} }
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' { if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' {
continue
} else {
return false
}
}
return true return true
} }
return false
}

View File

@ -92,7 +92,7 @@ func TestParseName(t *testing.T) {
} }
} }
func TestName(t *testing.T) { func TestComplete(t *testing.T) {
cases := []struct { cases := []struct {
in string in string
complete bool complete bool
@ -113,9 +113,6 @@ func TestName(t *testing.T) {
if g := p.Complete(); g != tt.complete { if g := p.Complete(); g != tt.complete {
t.Errorf("Complete(%q) = %v; want %v", tt.in, g, tt.complete) t.Errorf("Complete(%q) = %v; want %v", tt.in, g, tt.complete)
} }
if g := p.CompleteWithoutBuild(); g != tt.completeWithoutBuild {
t.Errorf("CompleteWithoutBuild(%q) = %v; want %v", tt.in, g, tt.completeWithoutBuild)
}
}) })
} }
} }
@ -229,16 +226,36 @@ func FuzzParseName(f *testing.F) {
}) })
} }
func ExampleMerge() { func TestFill(t *testing.T) {
src := ParseName("registry.ollama.com/mistral:latest") cases := []struct {
dst := ParseName("mistral") dst string
r := Merge(dst, src) src string
fmt.Println("src:", src) want string
fmt.Println("dst:", dst) }{
{"mistral", "o.com/library/PLACEHOLDER:latest+Q4_0", "o.com/library/mistral:latest+Q4_0"},
{"o.com/library/mistral", "PLACEHOLDER:latest+Q4_0", "o.com/library/mistral:latest+Q4_0"},
{"", "o.com/library/mistral:latest+Q4_0", "o.com/library/mistral:latest+Q4_0"},
}
for _, tt := range cases {
t.Run(tt.dst, func(t *testing.T) {
r := Fill(ParseName(tt.dst), ParseName(tt.src))
if r.String() != tt.want {
t.Errorf("Fill(%q, %q) = %q; want %q", tt.dst, tt.src, r, tt.want)
}
})
}
}
func ExampleFill() {
r := Fill(
ParseName("mistral"),
ParseName("registry.ollama.com/library/PLACEHOLDER:latest+Q4_0"),
)
fmt.Println(r) fmt.Println(r)
// Output: // Output:
// registry.ollama.com/mistral:latest+Q4_0 // registry.ollama.com/library/mistral:latest+Q4_0
} }
func ExampleName_MapHash() { func ExampleName_MapHash() {