x/build/blob: add fuzz test for ParseRef
This commit is contained in:
parent
4ea3e9efa6
commit
d42c3f6be1
@ -2,17 +2,16 @@ package blob
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"fmt"
|
|
||||||
"iter"
|
"iter"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Kind int
|
type PartKind int
|
||||||
|
|
||||||
// Levels of concreteness
|
// Levels of concreteness
|
||||||
const (
|
const (
|
||||||
Domain Kind = iota
|
Domain PartKind = iota
|
||||||
Namespace
|
Namespace
|
||||||
Name
|
Name
|
||||||
Tag
|
Tag
|
||||||
@ -35,43 +34,35 @@ type Ref struct {
|
|||||||
// WithDomain returns a copy of r with the provided domain. If the provided
|
// WithDomain returns a copy of r with the provided domain. If the provided
|
||||||
// domain is empty, it returns the short, unqualified copy of r.
|
// domain is empty, it returns the short, unqualified copy of r.
|
||||||
func (r Ref) WithDomain(s string) Ref {
|
func (r Ref) WithDomain(s string) Ref {
|
||||||
return with(r, Domain, s)
|
r.domain = s
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithNamespace returns a copy of r with the provided namespace. If the
|
// WithNamespace returns a copy of r with the provided namespace. If the
|
||||||
// provided namespace is empty, it returns the short, unqualified copy of r.
|
// provided namespace is empty, it returns the short, unqualified copy of r.
|
||||||
func (r Ref) WithNamespace(s string) Ref {
|
func (r Ref) WithNamespace(s string) Ref {
|
||||||
return with(r, Namespace, s)
|
r.namespace = s
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithName returns a copy of r with the provided name. If the provided
|
||||||
|
// name is empty, it returns the short, unqualified copy of r.
|
||||||
|
func (r Ref) WithName(s string) Ref {
|
||||||
|
r.name = s
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Ref) WithTag(s string) Ref {
|
func (r Ref) WithTag(s string) Ref {
|
||||||
return with(r, Tag, s)
|
r.tag = s
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithBuild returns a copy of r with the provided build. If the provided
|
// WithBuild returns a copy of r with the provided build. If the provided
|
||||||
// build is empty, it returns the short, unqualified copy of r.
|
// build is empty, it returns the short, unqualified copy of r.
|
||||||
|
//
|
||||||
|
// The build is normalized to uppercase.
|
||||||
func (r Ref) WithBuild(s string) Ref {
|
func (r Ref) WithBuild(s string) Ref {
|
||||||
return with(r, Build, s)
|
r.build = strings.ToUpper(s)
|
||||||
}
|
|
||||||
|
|
||||||
func with(r Ref, kind Kind, value string) Ref {
|
|
||||||
if value != "" && !isValidPart(value) {
|
|
||||||
return Ref{}
|
|
||||||
}
|
|
||||||
switch kind {
|
|
||||||
case Domain:
|
|
||||||
r.domain = value
|
|
||||||
case Namespace:
|
|
||||||
r.namespace = value
|
|
||||||
case Name:
|
|
||||||
r.name = value
|
|
||||||
case Tag:
|
|
||||||
r.tag = value
|
|
||||||
case Build:
|
|
||||||
r.build = value
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("invalid completeness: %d", kind))
|
|
||||||
}
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,15 +181,15 @@ func ParseRef(s string) Ref {
|
|||||||
for kind, part := range Parts(s) {
|
for kind, part := range Parts(s) {
|
||||||
switch kind {
|
switch kind {
|
||||||
case Domain:
|
case Domain:
|
||||||
r.domain = part
|
r = r.WithDomain(part)
|
||||||
case Namespace:
|
case Namespace:
|
||||||
r.namespace = part
|
r = r.WithNamespace(part)
|
||||||
case Name:
|
case Name:
|
||||||
r.name = part
|
r.name = part
|
||||||
case Tag:
|
case Tag:
|
||||||
r.tag = part
|
r = r.WithTag(part)
|
||||||
case Build:
|
case Build:
|
||||||
r.build = part
|
r = r.WithBuild(part)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !r.Valid() {
|
if !r.Valid() {
|
||||||
@ -207,12 +198,8 @@ func ParseRef(s string) Ref {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parts(s string) iter.Seq2[Kind, string] {
|
func Parts(s string) iter.Seq2[PartKind, string] {
|
||||||
return func(yield func(Kind, string) bool) {
|
return func(yield func(PartKind, string) bool) {
|
||||||
if len(s) > 128 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(s, "http://") {
|
if strings.HasPrefix(s, "http://") {
|
||||||
s = s[len("http://"):]
|
s = s[len("http://"):]
|
||||||
}
|
}
|
||||||
@ -220,7 +207,11 @@ func Parts(s string) iter.Seq2[Kind, string] {
|
|||||||
s = s[len("https://"):]
|
s = s[len("https://"):]
|
||||||
}
|
}
|
||||||
|
|
||||||
emit := func(kind Kind, value string) bool {
|
if len(s) > 255 || len(s) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
yieldValid := func(kind PartKind, value string) bool {
|
||||||
if !isValidPart(value) {
|
if !isValidPart(value) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -234,7 +225,7 @@ func Parts(s string) iter.Seq2[Kind, string] {
|
|||||||
switch state {
|
switch state {
|
||||||
case Build:
|
case Build:
|
||||||
v := strings.ToUpper(s[i+1 : j])
|
v := strings.ToUpper(s[i+1 : j])
|
||||||
if !emit(Build, v) {
|
if !yieldValid(Build, v) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state, j = Tag, i
|
state, j = Tag, i
|
||||||
@ -244,7 +235,7 @@ func Parts(s string) iter.Seq2[Kind, string] {
|
|||||||
case ':':
|
case ':':
|
||||||
switch state {
|
switch state {
|
||||||
case Build, Tag:
|
case Build, Tag:
|
||||||
if emit(Tag, s[i+1:j]) {
|
if yieldValid(Tag, s[i+1:j]) {
|
||||||
state, j = Tag, i
|
state, j = Tag, i
|
||||||
}
|
}
|
||||||
state, j = Name, i
|
state, j = Name, i
|
||||||
@ -254,12 +245,12 @@ func Parts(s string) iter.Seq2[Kind, string] {
|
|||||||
case '/':
|
case '/':
|
||||||
switch state {
|
switch state {
|
||||||
case Name, Tag, Build:
|
case Name, Tag, Build:
|
||||||
if !emit(Name, s[i+1:j]) {
|
if !yieldValid(Name, s[i+1:j]) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state, j = Namespace, i
|
state, j = Namespace, i
|
||||||
case Namespace:
|
case Namespace:
|
||||||
if !emit(Namespace, s[i+1:j]) {
|
if !yieldValid(Namespace, s[i+1:j]) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state, j = Domain, i
|
state, j = Domain, i
|
||||||
@ -272,11 +263,11 @@ func Parts(s string) iter.Seq2[Kind, string] {
|
|||||||
// handle the first part based on final state
|
// handle the first part based on final state
|
||||||
switch state {
|
switch state {
|
||||||
case Domain:
|
case Domain:
|
||||||
yield(Domain, s[:j])
|
yieldValid(Domain, s[:j])
|
||||||
case Namespace:
|
case Namespace:
|
||||||
yield(Namespace, s[:j])
|
yieldValid(Namespace, s[:j])
|
||||||
default:
|
default:
|
||||||
yield(Name, s[:j])
|
yieldValid(Name, s[:j])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -316,7 +307,7 @@ func (r Ref) 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(s string) bool {
|
||||||
if len(s) == 0 {
|
if s == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, c := range []byte(s) {
|
for _, c := range []byte(s) {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package blob
|
package blob
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
// test refs
|
// test refs
|
||||||
const (
|
const (
|
||||||
@ -21,6 +24,9 @@ var testRefs = map[string]Ref{
|
|||||||
// invalid
|
// invalid
|
||||||
"mistral:7b+Q4_0:latest": {},
|
"mistral:7b+Q4_0:latest": {},
|
||||||
"mi tral": {},
|
"mi tral": {},
|
||||||
|
|
||||||
|
// From fuzzing
|
||||||
|
"/0": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRefParts(t *testing.T) {
|
func TestRefParts(t *testing.T) {
|
||||||
@ -90,9 +96,36 @@ func TestParseRefAllocs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkParseRef(b *testing.B) {
|
func BenchmarkParseRef(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
var r Ref
|
var r Ref
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
r = ParseRef("example.com/mistral:7b+Q4_0")
|
r = ParseRef("example.com/mistral:7b+Q4_0")
|
||||||
}
|
}
|
||||||
_ = r
|
_ = r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FuzzParseRef(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.Fuzz(func(t *testing.T, s string) {
|
||||||
|
r0 := ParseRef(s)
|
||||||
|
if !r0.Valid() {
|
||||||
|
if r0 != (Ref{}) {
|
||||||
|
t.Errorf("expected invalid ref to be zero value; got %#v", r0)
|
||||||
|
}
|
||||||
|
t.Skipf("invalid ref: %q", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.EqualFold(r0.String(), s) {
|
||||||
|
t.Errorf("String() did not round-trip with case insensitivity: %q\ngot = %q\nwant = %q", s, r0.String(), s)
|
||||||
|
}
|
||||||
|
|
||||||
|
r1 := ParseRef(r0.String())
|
||||||
|
if r0 != r1 {
|
||||||
|
t.Errorf("round-trip mismatch: %q != %q", r0, r1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
2
x/build/blob/testdata/fuzz/FuzzParseRef/1d43ee52085cb4aa
vendored
Normal file
2
x/build/blob/testdata/fuzz/FuzzParseRef/1d43ee52085cb4aa
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
string("/0")
|
Loading…
x
Reference in New Issue
Block a user