blob: 71327dca720913f708d73976728c6c0580a83ca5 [file] [log] [blame]
David K. Bainbridge215e0242017-09-05 23:18:24 -07001package digestset
2
3import (
4 "errors"
5 "sort"
6 "strings"
7 "sync"
8
9 digest "github.com/opencontainers/go-digest"
10)
11
12var (
13 // ErrDigestNotFound is used when a matching digest
14 // could not be found in a set.
15 ErrDigestNotFound = errors.New("digest not found")
16
17 // ErrDigestAmbiguous is used when multiple digests
18 // are found in a set. None of the matching digests
19 // should be considered valid matches.
20 ErrDigestAmbiguous = errors.New("ambiguous digest string")
21)
22
23// Set is used to hold a unique set of digests which
24// may be easily referenced by easily referenced by a string
25// representation of the digest as well as short representation.
26// The uniqueness of the short representation is based on other
27// digests in the set. If digests are omitted from this set,
28// collisions in a larger set may not be detected, therefore it
29// is important to always do short representation lookups on
30// the complete set of digests. To mitigate collisions, an
31// appropriately long short code should be used.
32type Set struct {
33 mutex sync.RWMutex
34 entries digestEntries
35}
36
37// NewSet creates an empty set of digests
38// which may have digests added.
39func NewSet() *Set {
40 return &Set{
41 entries: digestEntries{},
42 }
43}
44
45// checkShortMatch checks whether two digests match as either whole
46// values or short values. This function does not test equality,
47// rather whether the second value could match against the first
48// value.
49func checkShortMatch(alg digest.Algorithm, hex, shortAlg, shortHex string) bool {
50 if len(hex) == len(shortHex) {
51 if hex != shortHex {
52 return false
53 }
54 if len(shortAlg) > 0 && string(alg) != shortAlg {
55 return false
56 }
57 } else if !strings.HasPrefix(hex, shortHex) {
58 return false
59 } else if len(shortAlg) > 0 && string(alg) != shortAlg {
60 return false
61 }
62 return true
63}
64
65// Lookup looks for a digest matching the given string representation.
66// If no digests could be found ErrDigestNotFound will be returned
67// with an empty digest value. If multiple matches are found
68// ErrDigestAmbiguous will be returned with an empty digest value.
69func (dst *Set) Lookup(d string) (digest.Digest, error) {
70 dst.mutex.RLock()
71 defer dst.mutex.RUnlock()
72 if len(dst.entries) == 0 {
73 return "", ErrDigestNotFound
74 }
75 var (
76 searchFunc func(int) bool
77 alg digest.Algorithm
78 hex string
79 )
80 dgst, err := digest.Parse(d)
81 if err == digest.ErrDigestInvalidFormat {
82 hex = d
83 searchFunc = func(i int) bool {
84 return dst.entries[i].val >= d
85 }
86 } else {
87 hex = dgst.Hex()
88 alg = dgst.Algorithm()
89 searchFunc = func(i int) bool {
90 if dst.entries[i].val == hex {
91 return dst.entries[i].alg >= alg
92 }
93 return dst.entries[i].val >= hex
94 }
95 }
96 idx := sort.Search(len(dst.entries), searchFunc)
97 if idx == len(dst.entries) || !checkShortMatch(dst.entries[idx].alg, dst.entries[idx].val, string(alg), hex) {
98 return "", ErrDigestNotFound
99 }
100 if dst.entries[idx].alg == alg && dst.entries[idx].val == hex {
101 return dst.entries[idx].digest, nil
102 }
103 if idx+1 < len(dst.entries) && checkShortMatch(dst.entries[idx+1].alg, dst.entries[idx+1].val, string(alg), hex) {
104 return "", ErrDigestAmbiguous
105 }
106
107 return dst.entries[idx].digest, nil
108}
109
110// Add adds the given digest to the set. An error will be returned
111// if the given digest is invalid. If the digest already exists in the
112// set, this operation will be a no-op.
113func (dst *Set) Add(d digest.Digest) error {
114 if err := d.Validate(); err != nil {
115 return err
116 }
117 dst.mutex.Lock()
118 defer dst.mutex.Unlock()
119 entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d}
120 searchFunc := func(i int) bool {
121 if dst.entries[i].val == entry.val {
122 return dst.entries[i].alg >= entry.alg
123 }
124 return dst.entries[i].val >= entry.val
125 }
126 idx := sort.Search(len(dst.entries), searchFunc)
127 if idx == len(dst.entries) {
128 dst.entries = append(dst.entries, entry)
129 return nil
130 } else if dst.entries[idx].digest == d {
131 return nil
132 }
133
134 entries := append(dst.entries, nil)
135 copy(entries[idx+1:], entries[idx:len(entries)-1])
136 entries[idx] = entry
137 dst.entries = entries
138 return nil
139}
140
141// Remove removes the given digest from the set. An err will be
142// returned if the given digest is invalid. If the digest does
143// not exist in the set, this operation will be a no-op.
144func (dst *Set) Remove(d digest.Digest) error {
145 if err := d.Validate(); err != nil {
146 return err
147 }
148 dst.mutex.Lock()
149 defer dst.mutex.Unlock()
150 entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d}
151 searchFunc := func(i int) bool {
152 if dst.entries[i].val == entry.val {
153 return dst.entries[i].alg >= entry.alg
154 }
155 return dst.entries[i].val >= entry.val
156 }
157 idx := sort.Search(len(dst.entries), searchFunc)
158 // Not found if idx is after or value at idx is not digest
159 if idx == len(dst.entries) || dst.entries[idx].digest != d {
160 return nil
161 }
162
163 entries := dst.entries
164 copy(entries[idx:], entries[idx+1:])
165 entries = entries[:len(entries)-1]
166 dst.entries = entries
167
168 return nil
169}
170
171// All returns all the digests in the set
172func (dst *Set) All() []digest.Digest {
173 dst.mutex.RLock()
174 defer dst.mutex.RUnlock()
175 retValues := make([]digest.Digest, len(dst.entries))
176 for i := range dst.entries {
177 retValues[i] = dst.entries[i].digest
178 }
179
180 return retValues
181}
182
183// ShortCodeTable returns a map of Digest to unique short codes. The
184// length represents the minimum value, the maximum length may be the
185// entire value of digest if uniqueness cannot be achieved without the
186// full value. This function will attempt to make short codes as short
187// as possible to be unique.
188func ShortCodeTable(dst *Set, length int) map[digest.Digest]string {
189 dst.mutex.RLock()
190 defer dst.mutex.RUnlock()
191 m := make(map[digest.Digest]string, len(dst.entries))
192 l := length
193 resetIdx := 0
194 for i := 0; i < len(dst.entries); i++ {
195 var short string
196 extended := true
197 for extended {
198 extended = false
199 if len(dst.entries[i].val) <= l {
200 short = dst.entries[i].digest.String()
201 } else {
202 short = dst.entries[i].val[:l]
203 for j := i + 1; j < len(dst.entries); j++ {
204 if checkShortMatch(dst.entries[j].alg, dst.entries[j].val, "", short) {
205 if j > resetIdx {
206 resetIdx = j
207 }
208 extended = true
209 } else {
210 break
211 }
212 }
213 if extended {
214 l++
215 }
216 }
217 }
218 m[dst.entries[i].digest] = short
219 if i >= resetIdx {
220 l = length
221 }
222 }
223 return m
224}
225
226type digestEntry struct {
227 alg digest.Algorithm
228 val string
229 digest digest.Digest
230}
231
232type digestEntries []*digestEntry
233
234func (d digestEntries) Len() int {
235 return len(d)
236}
237
238func (d digestEntries) Less(i, j int) bool {
239 if d[i].val != d[j].val {
240 return d[i].val < d[j].val
241 }
242 return d[i].alg < d[j].alg
243}
244
245func (d digestEntries) Swap(i, j int) {
246 d[i], d[j] = d[j], d[i]
247}