blob: add Ref.EqualString method

Adds allocation-free way to check if a blob ref is equal to its
stringified form.

For #972 (doesn't fix it, as that bug is about a pending CL)

Change-Id: I49c6dee162698d38bb12314623b1507ee7bb246e
This commit is contained in:
Brad Fitzpatrick 2017-12-18 12:03:34 -08:00
parent 36f351ff50
commit 504604e22d
2 changed files with 96 additions and 5 deletions

View File

@ -65,13 +65,13 @@ type digestType interface {
bytes() []byte bytes() []byte
digestName() string digestName() string
newHash() hash.Hash newHash() hash.Hash
equalString(string) bool
} }
func (r Ref) String() string { func (r Ref) String() string {
if r.digest == nil { if r.digest == nil {
return "<invalid-blob.Ref>" return "<invalid-blob.Ref>"
} }
// TODO: maybe memoize this.
dname := r.digest.digestName() dname := r.digest.digestName()
bs := r.digest.bytes() bs := r.digest.bytes()
buf := getBuf(len(dname) + 1 + len(bs)*2)[:0] buf := getBuf(len(dname) + 1 + len(bs)*2)[:0]
@ -84,7 +84,6 @@ func (r Ref) StringMinusOne() string {
if r.digest == nil { if r.digest == nil {
return "<invalid-blob.Ref>" return "<invalid-blob.Ref>"
} }
// TODO: maybe memoize this.
dname := r.digest.digestName() dname := r.digest.digestName()
bs := r.digest.bytes() bs := r.digest.bytes()
buf := getBuf(len(dname) + 1 + len(bs)*2)[:0] buf := getBuf(len(dname) + 1 + len(bs)*2)[:0]
@ -94,6 +93,10 @@ func (r Ref) StringMinusOne() string {
return string(buf) return string(buf)
} }
// EqualString reports whether r.String() is equal to s.
// It does not allocate.
func (r Ref) EqualString(s string) bool { return r.digest.equalString(s) }
func (r Ref) appendString(buf []byte) []byte { func (r Ref) appendString(buf []byte) []byte {
dname := r.digest.digestName() dname := r.digest.digestName()
bs := r.digest.bytes() bs := r.digest.bytes()
@ -403,9 +406,24 @@ func SHA1FromBytes(b []byte) Ref {
type sha1Digest [20]byte type sha1Digest [20]byte
func (s sha1Digest) digestName() string { return "sha1" } func (d sha1Digest) digestName() string { return "sha1" }
func (s sha1Digest) bytes() []byte { return s[:] } func (d sha1Digest) bytes() []byte { return d[:] }
func (s sha1Digest) newHash() hash.Hash { return sha1.New() } func (d sha1Digest) newHash() hash.Hash { return sha1.New() }
func (d sha1Digest) equalString(s string) bool {
if len(s) != 45 {
return false
}
if !strings.HasPrefix(s, "sha1-") {
return false
}
s = s[len("sha1-"):]
for i, b := range d[:] {
if s[i*2] != hexDigit[b>>4] || s[i*2+1] != hexDigit[b&0xf] {
return false
}
}
return true
}
const maxOtherDigestLen = 128 const maxOtherDigestLen = 128
@ -419,6 +437,28 @@ type otherDigest struct {
func (d otherDigest) digestName() string { return d.name } func (d otherDigest) digestName() string { return d.name }
func (d otherDigest) bytes() []byte { return d.sum[:d.sumLen] } func (d otherDigest) bytes() []byte { return d.sum[:d.sumLen] }
func (d otherDigest) newHash() hash.Hash { return nil } func (d otherDigest) newHash() hash.Hash { return nil }
func (d otherDigest) equalString(s string) bool {
wantLen := len(d.name) + len("-") + 2*d.sumLen
if d.odd {
wantLen--
}
if len(s) != wantLen || !strings.HasPrefix(s, d.name) || s[len(d.name)] != '-' {
return false
}
s = s[len(d.name)+1:]
for i, b := range d.sum[:d.sumLen] {
if s[i*2] != hexDigit[b>>4] {
return false
}
if i == d.sumLen-1 && d.odd {
break
}
if s[i*2+1] != hexDigit[b&0xf] {
return false
}
}
return true
}
var sha1Meta = &digestMeta{ var sha1Meta = &digestMeta{
ctor: sha1FromBinary, ctor: sha1FromBinary,

View File

@ -283,3 +283,54 @@ func TestJSONMarshalSized(t *testing.T) {
t.Fatalf("got %q, want %q", g, e) t.Fatalf("got %q, want %q", g, e)
} }
} }
var equalStringTests = []struct {
ref Ref
str string
want bool
}{
{MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659", true},
// last digit wrong:
{MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1-ce284c167558a9ef22df04390c87a6d0c9ed9658", false},
// second to last digit wrong:
{MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1-ce284c167558a9ef22df04390c87a6d0c9ed9669", false},
// hyphen wrong:
{MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1xce284c167558a9ef22df04390c87a6d0c9ed9659", false},
// truncated:
{MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1-", false},
{MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1", false},
{MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "", false},
// right length, wrong hash:
{MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha2-ce284c167558a9ef22df04390c87a6d0c9ed9659", false},
// Other hashes:
{MustParse("foo-cafe"), "foo-cafe", true},
{MustParse("foo-caf"), "foo-caf", true},
{MustParse("foo-cafe"), "foo-beef", false},
{MustParse("foo-cafe"), "bar-cafe", false},
{MustParse("foo-cafe"), "fooxbeef", false},
{MustParse("foo-caf"), "foo-cae", false},
{MustParse("foo-caf"), "foo-ca", false},
}
func TestEqualString(t *testing.T) {
for _, tt := range equalStringTests {
got := tt.ref.EqualString(tt.str)
if got != tt.want {
t.Errorf("ref %q EqualString(%q) = %v; want %v", tt.ref, tt.str, got, tt.want)
}
}
}
func BenchmarkEqualString(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, tt := range equalStringTests {
got := tt.ref.EqualString(tt.str)
if got != tt.want {
b.Fatalf("ref %q EqualString(%q) = %v; want %v", tt.ref, tt.str, got, tt.want)
}
}
}
}