mirror of
https://github.com/strongdm/comply
synced 2024-11-09 01:14:53 +00:00
246 lines
6.5 KiB
Go
246 lines
6.5 KiB
Go
|
package digest
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
)
|
||
|
|
||
|
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 omitted 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 {
|
||
|
mutex sync.RWMutex
|
||
|
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 Algorithm, hex, shortAlg, shortHex string) bool {
|
||
|
if len(hex) == len(shortHex) {
|
||
|
if hex != shortHex {
|
||
|
return false
|
||
|
}
|
||
|
if len(shortAlg) > 0 && string(alg) != shortAlg {
|
||
|
return false
|
||
|
}
|
||
|
} else if !strings.HasPrefix(hex, shortHex) {
|
||
|
return false
|
||
|
} else if len(shortAlg) > 0 && string(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) {
|
||
|
dst.mutex.RLock()
|
||
|
defer dst.mutex.RUnlock()
|
||
|
if len(dst.entries) == 0 {
|
||
|
return "", ErrDigestNotFound
|
||
|
}
|
||
|
var (
|
||
|
searchFunc func(int) bool
|
||
|
alg Algorithm
|
||
|
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, string(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, string(alg), hex) {
|
||
|
return "", ErrDigestAmbiguous
|
||
|
}
|
||
|
|
||
|
return dst.entries[idx].digest, nil
|
||
|
}
|
||
|
|
||
|
// Add adds the given digest to the set. An error will be returned
|
||
|
// if the given digest is invalid. If the digest already exists in the
|
||
|
// set, this operation will be a no-op.
|
||
|
func (dst *Set) Add(d Digest) error {
|
||
|
if err := d.Validate(); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
dst.mutex.Lock()
|
||
|
defer dst.mutex.Unlock()
|
||
|
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
|
||
|
}
|
||
|
|
||
|
// Remove removes the given digest from the set. An err will be
|
||
|
// returned if the given digest is invalid. If the digest does
|
||
|
// not exist in the set, this operation will be a no-op.
|
||
|
func (dst *Set) Remove(d Digest) error {
|
||
|
if err := d.Validate(); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
dst.mutex.Lock()
|
||
|
defer dst.mutex.Unlock()
|
||
|
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)
|
||
|
// Not found if idx is after or value at idx is not digest
|
||
|
if idx == len(dst.entries) || dst.entries[idx].digest != d {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
entries := dst.entries
|
||
|
copy(entries[idx:], entries[idx+1:])
|
||
|
entries = entries[:len(entries)-1]
|
||
|
dst.entries = entries
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// All returns all the digests in the set
|
||
|
func (dst *Set) All() []Digest {
|
||
|
dst.mutex.RLock()
|
||
|
defer dst.mutex.RUnlock()
|
||
|
retValues := make([]Digest, len(dst.entries))
|
||
|
for i := range dst.entries {
|
||
|
retValues[i] = dst.entries[i].digest
|
||
|
}
|
||
|
|
||
|
return retValues
|
||
|
}
|
||
|
|
||
|
// 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 {
|
||
|
dst.mutex.RLock()
|
||
|
defer dst.mutex.RUnlock()
|
||
|
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 Algorithm
|
||
|
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]
|
||
|
}
|