commit
						5556cd1ba1
					
				|  | @ -0,0 +1,195 @@ | ||||||
|  | package digest | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"sort" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	// ErrDigestNotFound is used when a matching digest
 | ||||||
|  | 	// could not be found in a set.
 | ||||||
|  | 	ErrDigestNotFound = errors.New("digest not found") | ||||||
|  | 
 | ||||||
|  | 	// ErrDigestAmbiguous is used when multiple digests
 | ||||||
|  | 	// are found in a set. None of the matching digests
 | ||||||
|  | 	// should be considered valid matches.
 | ||||||
|  | 	ErrDigestAmbiguous = errors.New("ambiguous digest string") | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Set is used to hold a unique set of digests which
 | ||||||
|  | // may be easily referenced by easily  referenced by a string
 | ||||||
|  | // representation of the digest as well as short representation.
 | ||||||
|  | // The uniqueness of the short representation is based on other
 | ||||||
|  | // digests in the set. If digests are ommited from this set,
 | ||||||
|  | // collisions in a larger set may not be detected, therefore it
 | ||||||
|  | // is important to always do short representation lookups on
 | ||||||
|  | // the complete set of digests. To mitigate collisions, an
 | ||||||
|  | // appropriately long short code should be used.
 | ||||||
|  | type Set struct { | ||||||
|  | 	entries digestEntries | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewSet creates an empty set of digests
 | ||||||
|  | // which may have digests added.
 | ||||||
|  | func NewSet() *Set { | ||||||
|  | 	return &Set{ | ||||||
|  | 		entries: digestEntries{}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // checkShortMatch checks whether two digests match as either whole
 | ||||||
|  | // values or short values. This function does not test equality,
 | ||||||
|  | // rather whether the second value could match against the first
 | ||||||
|  | // value.
 | ||||||
|  | func checkShortMatch(alg, hex, shortAlg, shortHex string) bool { | ||||||
|  | 	if len(hex) == len(shortHex) { | ||||||
|  | 		if hex != shortHex { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		if len(shortAlg) > 0 && alg != shortAlg { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} else if !strings.HasPrefix(hex, shortHex) { | ||||||
|  | 		return false | ||||||
|  | 	} else if len(shortAlg) > 0 && alg != shortAlg { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Lookup looks for a digest matching the given string representation.
 | ||||||
|  | // If no digests could be found ErrDigestNotFound will be returned
 | ||||||
|  | // with an empty digest value. If multiple matches are found
 | ||||||
|  | // ErrDigestAmbiguous will be returned with an empty digest value.
 | ||||||
|  | func (dst *Set) Lookup(d string) (Digest, error) { | ||||||
|  | 	if len(dst.entries) == 0 { | ||||||
|  | 		return "", ErrDigestNotFound | ||||||
|  | 	} | ||||||
|  | 	var ( | ||||||
|  | 		searchFunc func(int) bool | ||||||
|  | 		alg        string | ||||||
|  | 		hex        string | ||||||
|  | 	) | ||||||
|  | 	dgst, err := ParseDigest(d) | ||||||
|  | 	if err == ErrDigestInvalidFormat { | ||||||
|  | 		hex = d | ||||||
|  | 		searchFunc = func(i int) bool { | ||||||
|  | 			return dst.entries[i].val >= d | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		hex = dgst.Hex() | ||||||
|  | 		alg = dgst.Algorithm() | ||||||
|  | 		searchFunc = func(i int) bool { | ||||||
|  | 			if dst.entries[i].val == hex { | ||||||
|  | 				return dst.entries[i].alg >= alg | ||||||
|  | 			} | ||||||
|  | 			return dst.entries[i].val >= hex | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	idx := sort.Search(len(dst.entries), searchFunc) | ||||||
|  | 	if idx == len(dst.entries) || !checkShortMatch(dst.entries[idx].alg, dst.entries[idx].val, alg, hex) { | ||||||
|  | 		return "", ErrDigestNotFound | ||||||
|  | 	} | ||||||
|  | 	if dst.entries[idx].alg == alg && dst.entries[idx].val == hex { | ||||||
|  | 		return dst.entries[idx].digest, nil | ||||||
|  | 	} | ||||||
|  | 	if idx+1 < len(dst.entries) && checkShortMatch(dst.entries[idx+1].alg, dst.entries[idx+1].val, alg, hex) { | ||||||
|  | 		return "", ErrDigestAmbiguous | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return dst.entries[idx].digest, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Add adds the given digests to the set. An error will be returned
 | ||||||
|  | // if the given digest is invalid. If the digest already exists in the
 | ||||||
|  | // table, this operation will be a no-op.
 | ||||||
|  | func (dst *Set) Add(d Digest) error { | ||||||
|  | 	if err := d.Validate(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d} | ||||||
|  | 	searchFunc := func(i int) bool { | ||||||
|  | 		if dst.entries[i].val == entry.val { | ||||||
|  | 			return dst.entries[i].alg >= entry.alg | ||||||
|  | 		} | ||||||
|  | 		return dst.entries[i].val >= entry.val | ||||||
|  | 	} | ||||||
|  | 	idx := sort.Search(len(dst.entries), searchFunc) | ||||||
|  | 	if idx == len(dst.entries) { | ||||||
|  | 		dst.entries = append(dst.entries, entry) | ||||||
|  | 		return nil | ||||||
|  | 	} else if dst.entries[idx].digest == d { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	entries := append(dst.entries, nil) | ||||||
|  | 	copy(entries[idx+1:], entries[idx:len(entries)-1]) | ||||||
|  | 	entries[idx] = entry | ||||||
|  | 	dst.entries = entries | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ShortCodeTable returns a map of Digest to unique short codes. The
 | ||||||
|  | // length represents the minimum value, the maximum length may be the
 | ||||||
|  | // entire value of digest if uniqueness cannot be achieved without the
 | ||||||
|  | // full value. This function will attempt to make short codes as short
 | ||||||
|  | // as possible to be unique.
 | ||||||
|  | func ShortCodeTable(dst *Set, length int) map[Digest]string { | ||||||
|  | 	m := make(map[Digest]string, len(dst.entries)) | ||||||
|  | 	l := length | ||||||
|  | 	resetIdx := 0 | ||||||
|  | 	for i := 0; i < len(dst.entries); i++ { | ||||||
|  | 		var short string | ||||||
|  | 		extended := true | ||||||
|  | 		for extended { | ||||||
|  | 			extended = false | ||||||
|  | 			if len(dst.entries[i].val) <= l { | ||||||
|  | 				short = dst.entries[i].digest.String() | ||||||
|  | 			} else { | ||||||
|  | 				short = dst.entries[i].val[:l] | ||||||
|  | 				for j := i + 1; j < len(dst.entries); j++ { | ||||||
|  | 					if checkShortMatch(dst.entries[j].alg, dst.entries[j].val, "", short) { | ||||||
|  | 						if j > resetIdx { | ||||||
|  | 							resetIdx = j | ||||||
|  | 						} | ||||||
|  | 						extended = true | ||||||
|  | 					} else { | ||||||
|  | 						break | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				if extended { | ||||||
|  | 					l++ | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		m[dst.entries[i].digest] = short | ||||||
|  | 		if i >= resetIdx { | ||||||
|  | 			l = length | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return m | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type digestEntry struct { | ||||||
|  | 	alg    string | ||||||
|  | 	val    string | ||||||
|  | 	digest Digest | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type digestEntries []*digestEntry | ||||||
|  | 
 | ||||||
|  | func (d digestEntries) Len() int { | ||||||
|  | 	return len(d) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (d digestEntries) Less(i, j int) bool { | ||||||
|  | 	if d[i].val != d[j].val { | ||||||
|  | 		return d[i].val < d[j].val | ||||||
|  | 	} | ||||||
|  | 	return d[i].alg < d[j].alg | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (d digestEntries) Swap(i, j int) { | ||||||
|  | 	d[i], d[j] = d[j], d[i] | ||||||
|  | } | ||||||
|  | @ -0,0 +1,272 @@ | ||||||
|  | package digest | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"math/rand" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func assertEqualDigests(t *testing.T, d1, d2 Digest) { | ||||||
|  | 	if d1 != d2 { | ||||||
|  | 		t.Fatalf("Digests do not match:\n\tActual: %s\n\tExpected: %s", d1, d2) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestLookup(t *testing.T) { | ||||||
|  | 	digests := []Digest{ | ||||||
|  | 		"sha256:12345", | ||||||
|  | 		"sha256:1234", | ||||||
|  | 		"sha256:12346", | ||||||
|  | 		"sha256:54321", | ||||||
|  | 		"sha256:65431", | ||||||
|  | 		"sha256:64321", | ||||||
|  | 		"sha256:65421", | ||||||
|  | 		"sha256:65321", | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dset := NewSet() | ||||||
|  | 	for i := range digests { | ||||||
|  | 		if err := dset.Add(digests[i]); err != nil { | ||||||
|  | 			t.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dgst, err := dset.Lookup("54") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	assertEqualDigests(t, dgst, digests[3]) | ||||||
|  | 
 | ||||||
|  | 	dgst, err = dset.Lookup("1234") | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatal("Expected ambiguous error looking up: 1234") | ||||||
|  | 	} | ||||||
|  | 	if err != ErrDigestAmbiguous { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dgst, err = dset.Lookup("9876") | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatal("Expected ambiguous error looking up: 9876") | ||||||
|  | 	} | ||||||
|  | 	if err != ErrDigestNotFound { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dgst, err = dset.Lookup("sha256:1234") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	assertEqualDigests(t, dgst, digests[1]) | ||||||
|  | 
 | ||||||
|  | 	dgst, err = dset.Lookup("sha256:12345") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	assertEqualDigests(t, dgst, digests[0]) | ||||||
|  | 
 | ||||||
|  | 	dgst, err = dset.Lookup("sha256:12346") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	assertEqualDigests(t, dgst, digests[2]) | ||||||
|  | 
 | ||||||
|  | 	dgst, err = dset.Lookup("12346") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	assertEqualDigests(t, dgst, digests[2]) | ||||||
|  | 
 | ||||||
|  | 	dgst, err = dset.Lookup("12345") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	assertEqualDigests(t, dgst, digests[0]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestAddDuplication(t *testing.T) { | ||||||
|  | 	digests := []Digest{ | ||||||
|  | 		"sha256:1234", | ||||||
|  | 		"sha256:12345", | ||||||
|  | 		"sha256:12346", | ||||||
|  | 		"sha256:54321", | ||||||
|  | 		"sha256:65431", | ||||||
|  | 		"sha512:65431", | ||||||
|  | 		"sha512:65421", | ||||||
|  | 		"sha512:65321", | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dset := NewSet() | ||||||
|  | 	for i := range digests { | ||||||
|  | 		if err := dset.Add(digests[i]); err != nil { | ||||||
|  | 			t.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(dset.entries) != 8 { | ||||||
|  | 		t.Fatal("Invalid dset size") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := dset.Add(Digest("sha256:12345")); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(dset.entries) != 8 { | ||||||
|  | 		t.Fatal("Duplicate digest insert allowed") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := dset.Add(Digest("sha384:12345")); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(dset.entries) != 9 { | ||||||
|  | 		t.Fatal("Insert with different algorithm not allowed") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func assertEqualShort(t *testing.T, actual, expected string) { | ||||||
|  | 	if actual != expected { | ||||||
|  | 		t.Fatalf("Unexpected short value:\n\tExpected: %s\n\tActual: %s", expected, actual) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestShortCodeTable(t *testing.T) { | ||||||
|  | 	digests := []Digest{ | ||||||
|  | 		"sha256:1234", | ||||||
|  | 		"sha256:12345", | ||||||
|  | 		"sha256:12346", | ||||||
|  | 		"sha256:54321", | ||||||
|  | 		"sha256:65431", | ||||||
|  | 		"sha256:64321", | ||||||
|  | 		"sha256:65421", | ||||||
|  | 		"sha256:65321", | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dset := NewSet() | ||||||
|  | 	for i := range digests { | ||||||
|  | 		if err := dset.Add(digests[i]); err != nil { | ||||||
|  | 			t.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dump := ShortCodeTable(dset, 2) | ||||||
|  | 
 | ||||||
|  | 	if len(dump) < len(digests) { | ||||||
|  | 		t.Fatalf("Error unexpected size: %d, expecting %d", len(dump), len(digests)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	assertEqualShort(t, dump[digests[0]], "sha256:1234") | ||||||
|  | 	assertEqualShort(t, dump[digests[1]], "sha256:12345") | ||||||
|  | 	assertEqualShort(t, dump[digests[2]], "sha256:12346") | ||||||
|  | 	assertEqualShort(t, dump[digests[3]], "54") | ||||||
|  | 	assertEqualShort(t, dump[digests[4]], "6543") | ||||||
|  | 	assertEqualShort(t, dump[digests[5]], "64") | ||||||
|  | 	assertEqualShort(t, dump[digests[6]], "6542") | ||||||
|  | 	assertEqualShort(t, dump[digests[7]], "653") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func createDigests(count int) ([]Digest, error) { | ||||||
|  | 	r := rand.New(rand.NewSource(25823)) | ||||||
|  | 	digests := make([]Digest, count) | ||||||
|  | 	for i := range digests { | ||||||
|  | 		h := sha256.New() | ||||||
|  | 		if err := binary.Write(h, binary.BigEndian, r.Int63()); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		digests[i] = NewDigest("sha256", h) | ||||||
|  | 	} | ||||||
|  | 	return digests, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func benchAddNTable(b *testing.B, n int) { | ||||||
|  | 	digests, err := createDigests(n) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	b.ResetTimer() | ||||||
|  | 	for i := 0; i < b.N; i++ { | ||||||
|  | 		dset := &Set{entries: digestEntries(make([]*digestEntry, 0, n))} | ||||||
|  | 		for j := range digests { | ||||||
|  | 			if err = dset.Add(digests[j]); err != nil { | ||||||
|  | 				b.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func benchLookupNTable(b *testing.B, n int, shortLen int) { | ||||||
|  | 	digests, err := createDigests(n) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	dset := &Set{entries: digestEntries(make([]*digestEntry, 0, n))} | ||||||
|  | 	for i := range digests { | ||||||
|  | 		if err := dset.Add(digests[i]); err != nil { | ||||||
|  | 			b.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	shorts := make([]string, 0, n) | ||||||
|  | 	for _, short := range ShortCodeTable(dset, shortLen) { | ||||||
|  | 		shorts = append(shorts, short) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	b.ResetTimer() | ||||||
|  | 	for i := 0; i < b.N; i++ { | ||||||
|  | 		if _, err = dset.Lookup(shorts[i%n]); err != nil { | ||||||
|  | 			b.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func benchShortCodeNTable(b *testing.B, n int, shortLen int) { | ||||||
|  | 	digests, err := createDigests(n) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	dset := &Set{entries: digestEntries(make([]*digestEntry, 0, n))} | ||||||
|  | 	for i := range digests { | ||||||
|  | 		if err := dset.Add(digests[i]); err != nil { | ||||||
|  | 			b.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	b.ResetTimer() | ||||||
|  | 	for i := 0; i < b.N; i++ { | ||||||
|  | 		ShortCodeTable(dset, shortLen) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func BenchmarkAdd10(b *testing.B) { | ||||||
|  | 	benchAddNTable(b, 10) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func BenchmarkAdd100(b *testing.B) { | ||||||
|  | 	benchAddNTable(b, 100) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func BenchmarkAdd1000(b *testing.B) { | ||||||
|  | 	benchAddNTable(b, 1000) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func BenchmarkLookup10(b *testing.B) { | ||||||
|  | 	benchLookupNTable(b, 10, 12) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func BenchmarkLookup100(b *testing.B) { | ||||||
|  | 	benchLookupNTable(b, 100, 12) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func BenchmarkLookup1000(b *testing.B) { | ||||||
|  | 	benchLookupNTable(b, 1000, 12) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func BenchmarkShortCode10(b *testing.B) { | ||||||
|  | 	benchShortCodeNTable(b, 10, 12) | ||||||
|  | } | ||||||
|  | func BenchmarkShortCode100(b *testing.B) { | ||||||
|  | 	benchShortCodeNTable(b, 100, 12) | ||||||
|  | } | ||||||
|  | func BenchmarkShortCode1000(b *testing.B) { | ||||||
|  | 	benchShortCodeNTable(b, 1000, 12) | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue