From 504604e22dcbeb86a597b4a75992f34157701673 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 18 Dec 2017 12:03:34 -0800 Subject: [PATCH] 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 --- pkg/blob/ref.go | 50 ++++++++++++++++++++++++++++++++++++++----- pkg/blob/ref_test.go | 51 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/pkg/blob/ref.go b/pkg/blob/ref.go index 21b945dc4..1a55858d3 100644 --- a/pkg/blob/ref.go +++ b/pkg/blob/ref.go @@ -65,13 +65,13 @@ type digestType interface { bytes() []byte digestName() string newHash() hash.Hash + equalString(string) bool } func (r Ref) String() string { if r.digest == nil { return "" } - // TODO: maybe memoize this. dname := r.digest.digestName() bs := r.digest.bytes() buf := getBuf(len(dname) + 1 + len(bs)*2)[:0] @@ -84,7 +84,6 @@ func (r Ref) StringMinusOne() string { if r.digest == nil { return "" } - // TODO: maybe memoize this. dname := r.digest.digestName() bs := r.digest.bytes() buf := getBuf(len(dname) + 1 + len(bs)*2)[:0] @@ -94,6 +93,10 @@ func (r Ref) StringMinusOne() string { 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 { dname := r.digest.digestName() bs := r.digest.bytes() @@ -403,9 +406,24 @@ func SHA1FromBytes(b []byte) Ref { type sha1Digest [20]byte -func (s sha1Digest) digestName() string { return "sha1" } -func (s sha1Digest) bytes() []byte { return s[:] } -func (s sha1Digest) newHash() hash.Hash { return sha1.New() } +func (d sha1Digest) digestName() string { return "sha1" } +func (d sha1Digest) bytes() []byte { return d[:] } +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 @@ -419,6 +437,28 @@ type otherDigest struct { func (d otherDigest) digestName() string { return d.name } func (d otherDigest) bytes() []byte { return d.sum[:d.sumLen] } 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{ ctor: sha1FromBinary, diff --git a/pkg/blob/ref_test.go b/pkg/blob/ref_test.go index 7cae13f08..be5ecbab4 100644 --- a/pkg/blob/ref_test.go +++ b/pkg/blob/ref_test.go @@ -283,3 +283,54 @@ func TestJSONMarshalSized(t *testing.T) { 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) + } + } + } +}