x/model: make Digest just hold a string

This is so much easier to reason about and allows Name to contain only
parts, instead of a special field for the digest.

It also removes the allocs in Digest.String().
This commit is contained in:
Blake Mizerany 2024-04-07 18:38:48 -07:00
parent 8b62eaf059
commit 06c21f00eb
4 changed files with 28 additions and 42 deletions

View File

@ -15,20 +15,16 @@ import (
// //
// It is comparable with other Digests and can be used as a map key. // It is comparable with other Digests and can be used as a map key.
type Digest struct { type Digest struct {
typ string s string
digest string
} }
func (d Digest) Type() string { return d.typ } func (d Digest) Type() string {
func (d Digest) Digest() string { return d.digest } typ, _, _ := strings.Cut(d.s, "-")
func (d Digest) IsValid() bool { return d != Digest{} } return typ
}
func (d Digest) String() string { func (d Digest) IsValid() bool { return d.s != "" }
if !d.IsValid() { func (d Digest) String() string { return d.s }
return ""
}
return fmt.Sprintf("%s-%s", d.typ, d.digest)
}
func (d Digest) MarshalText() ([]byte, error) { func (d Digest) MarshalText() ([]byte, error) {
return []byte(d.String()), nil return []byte(d.String()), nil
@ -75,7 +71,7 @@ func (d Digest) Value() (driver.Value, error) {
func ParseDigest(s string) Digest { func ParseDigest(s string) Digest {
typ, digest, ok := strings.Cut(s, "-") typ, digest, ok := strings.Cut(s, "-")
if ok && isValidDigestType(typ) && isValidHex(digest) { if ok && isValidDigestType(typ) && isValidHex(digest) {
return Digest{typ: typ, digest: digest} return Digest{s: s}
} }
return Digest{} return Digest{}
} }

View File

@ -16,9 +16,9 @@ import "testing"
var testDigests = map[string]Digest{ var testDigests = map[string]Digest{
"": {}, "": {},
"sha256-1234": {typ: "sha256", digest: "1234"}, "sha256-1234": {s: "sha256-1234"},
"sha256-5678": {typ: "sha256", digest: "5678"}, "sha256-5678": {s: "sha256-5678"},
"blake2-9abc": {typ: "blake2", digest: "9abc"}, "blake2-9abc": {s: "blake2-9abc"},
"-1234": {}, "-1234": {},
"sha256-": {}, "sha256-": {},
"sha256-1234-5678": {}, "sha256-1234-5678": {},

View File

@ -92,8 +92,7 @@ func (k PartKind) String() string {
// To make a Name by filling in missing parts from another Name, use [Fill]. // To make a Name by filling in missing parts from another Name, use [Fill].
type Name struct { type Name struct {
_ structs.Incomparable _ structs.Incomparable
parts [5]string // host, namespace, model, tag, build parts [6]string // host, namespace, model, tag, build
digest Digest // digest is a special part
// TODO(bmizerany): track offsets and hold s (raw string) here? We // TODO(bmizerany): track offsets and hold s (raw string) here? We
// could pack the offests all into a single uint64 since the first // could pack the offests all into a single uint64 since the first
@ -140,13 +139,9 @@ func ParseName(s string) Name {
if kind == PartInvalid { if kind == PartInvalid {
return Name{} return Name{}
} }
if kind == PartDigest { if kind == PartDigest && !ParseDigest(part).IsValid() {
r.digest = ParseDigest(part)
if !r.digest.IsValid() {
return Name{} return Name{}
} }
continue
}
r.parts[kind] = part r.parts[kind] = part
} }
if r.IsValid() || r.IsResolved() { if r.IsValid() || r.IsResolved() {
@ -173,7 +168,7 @@ func (r Name) WithBuild(build string) Name {
} }
func (r Name) WithDigest(digest Digest) Name { func (r Name) WithDigest(digest Digest) Name {
r.digest = digest r.parts[PartDigest] = digest.String()
return r return r
} }
@ -244,7 +239,8 @@ var seps = [...]string{
PartNamespace: "/", PartNamespace: "/",
PartModel: ":", PartModel: ":",
PartTag: "+", PartTag: "+",
PartBuild: "", PartBuild: "@",
PartDigest: "",
} }
// WriteTo implements io.WriterTo. It writes the fullest possible display // WriteTo implements io.WriterTo. It writes the fullest possible display
@ -263,16 +259,12 @@ func (r Name) writeTo(w io.StringWriter) {
if r.parts[i] == "" { if r.parts[i] == "" {
continue continue
} }
if partsWritten > 0 { if partsWritten > 0 || i == int(PartDigest) {
w.WriteString(seps[i-1]) w.WriteString(seps[i-1])
} }
w.WriteString(r.parts[i]) w.WriteString(r.parts[i])
partsWritten++ partsWritten++
} }
if r.IsResolved() {
w.WriteString("@")
w.WriteString(r.digest.String())
}
} }
var builderPool = sync.Pool{ var builderPool = sync.Pool{
@ -306,9 +298,6 @@ func (r Name) GoString() string {
for i := range r.parts { for i := range r.parts {
r.parts[i] = cmp.Or(r.parts[i], "?") r.parts[i] = cmp.Or(r.parts[i], "?")
} }
if !r.IsResolved() {
r.digest = Digest{"?", "?"}
}
return r.String() return r.String()
} }
@ -399,7 +388,7 @@ func (r Name) IsCompleteNoBuild() bool {
// It is possible to have a valid Name, or a complete Name that is not // It is possible to have a valid Name, or a complete Name that is not
// resolved. // resolved.
func (r Name) IsResolved() bool { func (r Name) IsResolved() bool {
return r.digest.IsValid() return r.Digest().IsValid()
} }
// Digest returns the digest part of the Name, if any. // Digest returns the digest part of the Name, if any.
@ -407,7 +396,8 @@ func (r Name) IsResolved() bool {
// If Digest returns a non-empty string, then [Name.IsResolved] will return // If Digest returns a non-empty string, then [Name.IsResolved] will return
// true, and digest is considered valid. // true, and digest is considered valid.
func (r Name) Digest() Digest { func (r Name) Digest() Digest {
return r.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 // EqualFold reports whether r and o are equivalent model names, ignoring

View File

@ -23,7 +23,7 @@ func fieldsFromName(p Name) fields {
model: p.parts[PartModel], model: p.parts[PartModel],
tag: p.parts[PartTag], tag: p.parts[PartTag],
build: p.parts[PartBuild], build: p.parts[PartBuild],
digest: p.digest.String(), digest: p.parts[PartDigest],
} }
} }
@ -103,7 +103,7 @@ var testNames = map[string]fields{
func TestNameParts(t *testing.T) { func TestNameParts(t *testing.T) {
var p Name var p Name
if w, g := int(PartBuild+1), len(p.Parts()); w != g { if w, g := int(PartDigest+1), len(p.Parts()); w != g {
t.Errorf("Parts() = %d; want %d", g, w) t.Errorf("Parts() = %d; want %d", g, w)
} }
} }
@ -235,7 +235,7 @@ func TestNameDisplay(t *testing.T) {
wantLong: "library/mistral:latest", wantLong: "library/mistral:latest",
wantComplete: "example.com/library/mistral:latest", wantComplete: "example.com/library/mistral:latest",
wantModel: "mistral", wantModel: "mistral",
wantGoString: "example.com/library/mistral:latest+Q4_0@?-?", wantGoString: "example.com/library/mistral:latest+Q4_0@?",
}, },
{ {
name: "Short Name", name: "Short Name",
@ -244,7 +244,7 @@ func TestNameDisplay(t *testing.T) {
wantLong: "mistral:latest", wantLong: "mistral:latest",
wantComplete: "mistral:latest", wantComplete: "mistral:latest",
wantModel: "mistral", wantModel: "mistral",
wantGoString: "?/?/mistral:latest+?@?-?", wantGoString: "?/?/mistral:latest+?@?",
}, },
{ {
name: "Long Name", name: "Long Name",
@ -253,7 +253,7 @@ func TestNameDisplay(t *testing.T) {
wantLong: "library/mistral:latest", wantLong: "library/mistral:latest",
wantComplete: "library/mistral:latest", wantComplete: "library/mistral:latest",
wantModel: "mistral", wantModel: "mistral",
wantGoString: "?/library/mistral:latest+?@?-?", wantGoString: "?/library/mistral:latest+?@?",
}, },
{ {
name: "Case Preserved", name: "Case Preserved",
@ -262,7 +262,7 @@ func TestNameDisplay(t *testing.T) {
wantLong: "Library/Mistral:Latest", wantLong: "Library/Mistral:Latest",
wantComplete: "Library/Mistral:Latest", wantComplete: "Library/Mistral:Latest",
wantModel: "Mistral", wantModel: "Mistral",
wantGoString: "?/Library/Mistral:Latest+?@?-?", wantGoString: "?/Library/Mistral:Latest+?@?",
}, },
{ {
name: "With digest", name: "With digest",