diff --git a/pkg/blob/ref.go b/pkg/blob/ref.go index 1a55858d3..be8f97b6f 100644 --- a/pkg/blob/ref.go +++ b/pkg/blob/ref.go @@ -66,6 +66,7 @@ type digestType interface { digestName() string newHash() hash.Hash equalString(string) bool + hasPrefix(string) bool } func (r Ref) String() string { @@ -97,6 +98,12 @@ func (r Ref) StringMinusOne() string { // It does not allocate. func (r Ref) EqualString(s string) bool { return r.digest.equalString(s) } +// HasPrefix reports whether s is a prefix of r.String(). It returns false if s +// does not contain at least the digest name prefix (e.g. "sha1-") and one byte of +// digest. +// It does not allocate. +func (r Ref) HasPrefix(s string) bool { return r.digest.hasPrefix(s) } + func (r Ref) appendString(buf []byte) []byte { dname := r.digest.digestName() bs := r.digest.bytes() @@ -425,6 +432,40 @@ func (d sha1Digest) equalString(s string) bool { return true } +func (d sha1Digest) hasPrefix(s string) bool { + if len(s) > 45 { + return false + } + if len(s) == 45 { + return d.equalString(s) + } + if !strings.HasPrefix(s, "sha1-") { + return false + } + s = s[len("sha1-"):] + if len(s) == 0 { + // we want at least one digest char to match on + return false + } + for i, b := range d[:] { + even := i * 2 + if even == len(s) { + break + } + if s[even] != hexDigit[b>>4] { + return false + } + odd := i*2 + 1 + if odd == len(s) { + break + } + if s[odd] != hexDigit[b&0xf] { + return false + } + } + return true +} + const maxOtherDigestLen = 128 type otherDigest struct { @@ -460,6 +501,44 @@ func (d otherDigest) equalString(s string) bool { return true } +func (d otherDigest) hasPrefix(s string) bool { + maxLen := len(d.name) + len("-") + 2*d.sumLen + if d.odd { + maxLen-- + } + if len(s) > maxLen || !strings.HasPrefix(s, d.name) || s[len(d.name)] != '-' { + return false + } + if len(s) == maxLen { + return d.equalString(s) + } + s = s[len(d.name)+1:] + if len(s) == 0 { + // we want at least one digest char to match on + return false + } + for i, b := range d.sum[:d.sumLen] { + even := i * 2 + if even == len(s) { + break + } + if s[even] != hexDigit[b>>4] { + return false + } + odd := i*2 + 1 + if odd == len(s) { + break + } + if i == d.sumLen-1 && d.odd { + break + } + if s[odd] != hexDigit[b&0xf] { + return false + } + } + return true +} + var sha1Meta = &digestMeta{ ctor: sha1FromBinary, ctors: sha1FromHexString, diff --git a/pkg/blob/ref_test.go b/pkg/blob/ref_test.go index be5ecbab4..c20786537 100644 --- a/pkg/blob/ref_test.go +++ b/pkg/blob/ref_test.go @@ -334,3 +334,63 @@ func BenchmarkEqualString(b *testing.B) { } } } + +var hasPrefixTests = []struct { + ref Ref + str string + want bool +}{ + {MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659", true}, + {MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1-ce284c167558a9ef22df04390c87a6d0c9ed", true}, + {MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1-ce284c167558a9ef22df04390c87a6d0c9e", true}, + // last digit wrong: + {MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1-ce284c167558a9ef22df04390c87a6d0c9ee", false}, + // second to last digit wrong: + {MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1-ce284c167558a9ef22df04390c87a6d0c9f", false}, + // hyphen wrong: + {MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1xce284c167558a9ef22df04390c87a6d0c9ed", false}, + // truncated: + {MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1-c", true}, + {MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1-", false}, + {MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha1", false}, + {MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "", false}, + // wrong hash: + {MustParse("sha1-ce284c167558a9ef22df04390c87a6d0c9ed9659"), "sha2-ce284c167558a9ef22df04390c87a6d0c9ed96", false}, + + // Other hashes: + {MustParse("foo-cafe"), "foo-cafe", true}, + {MustParse("foo-cafe"), "foo-caf", true}, + {MustParse("foo-cafe"), "foo-ca", true}, + {MustParse("foo-cafe"), "foo-c", true}, + + {MustParse("foo-cafe"), "foo-", false}, + {MustParse("foo-cafe"), "", false}, + {MustParse("foo-cafe"), "foo-beef", false}, + {MustParse("foo-cafe"), "foo-bee", false}, + {MustParse("foo-cafe"), "bar-cafe", false}, + {MustParse("foo-cafe"), "fooxbe", false}, + {MustParse("foo-cafe"), "foo-c", true}, + {MustParse("foo-caf"), "foo-cae", false}, + {MustParse("foo-caf"), "foo-cb", false}, +} + +func TestHasPrefix(t *testing.T) { + for _, tt := range hasPrefixTests { + got := tt.ref.HasPrefix(tt.str) + if got != tt.want { + t.Errorf("ref %q HasPrefix(%q) = %v; want %v", tt.ref, tt.str, got, tt.want) + } + } +} + +func BenchmarkHasPrefix(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + for _, tt := range hasPrefixTests { + got := tt.ref.HasPrefix(tt.str) + if got != tt.want { + b.Fatalf("ref %q HasPrefix(%q) = %v; want %v", tt.ref, tt.str, got, tt.want) + } + } + } +}