carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package objstore
2
3import (
4 "context"
5 "sync"
6 "time"
7)
8
9// MemScanner is the in-memory Scanner every walk test runs against, mirroring
10// MemProvisioner: no network, no clamd, and a failure hook so a test can make
11// any single method fail once and prove the walk survives it.
12type MemScanner struct {
13 // onStatus is OnStatus's hook; see its comment.
14 onStatus func()
15 mu sync.Mutex
16 enabled map[string]bool
17 verdicts map[string]scanRecord
18 failures map[string]error
19 scanAs map[string]Verdict
20 // objects is what a listing would have said about a key. Status needs it:
21 // a caller holding only a key has no listing page to take a version from.
22 objects map[string]ScanObject
23 // unrecordable are keys whose verdict cannot be persisted — the ten-tag
24 // cap, in production. A hook rather than a stub, because the case is the
25 // reason Scan reports `recorded` at all.
26 unrecordable map[string]bool
27 // deniedHead are keys whose HeadObject is refused, so Status answers
28 // denied with a ZERO ScanObject — the one shape that leaves a caller with
29 // no version and no size.
30 deniedHead map[string]bool
31 // scans counts Scan calls per key, so a test can assert that deduping
32 // really stopped the engine reading the same object twice rather than
33 // merely folding two outcomes into one.
34 scans map[string]int
35}
36
37// scanRecord is one object's verdict AND the object version it was judged
38// against. The version is not decoration: Verdict answers Unknown when it
39// disagrees, which is what stops a rewritten object inheriting the old bytes'
40// verdict.
41type scanRecord struct {
42 version string
43 verdict Verdict
44}
45
46func NewMemScanner() *MemScanner {
47 return &MemScanner{
48 enabled: map[string]bool{},
49 verdicts: map[string]scanRecord{},
50 failures: map[string]error{},
51 }
52}
53
54func memScanKey(bucket, key string) string { return bucket + "\x00" + key }
55
56// Fail arms a one-shot failure for the named method, the same shape
57// MemProvisioner.takeFailure gives the provisioner walks.
58func (s *MemScanner) Fail(method string, err error) {
59 s.mu.Lock()
60 defer s.mu.Unlock()
61 s.failures[method] = err
62}
63
64// PutObject seeds what a listing would have said about a key, so Status can
65// answer. Without it Status cannot tell an absent object from an unscanned
66// one, and those are different answers.
67func (s *MemScanner) PutObject(bucket, key, etag string, modified time.Time, size int64) {
68 s.mu.Lock()
69 defer s.mu.Unlock()
70 if s.objects == nil {
71 s.objects = map[string]ScanObject{}
72 }
73 s.objects[memScanKey(bucket, key)] = ScanObject{
74 Key: key, ETag: etag, Modified: modified, Size: size,
75 }
76}
77
78// Unrecordable makes Scan reach a verdict it cannot persist, the way the
79// ten-tag cap does in production.
80func (s *MemScanner) Unrecordable(key string) {
81 s.mu.Lock()
82 defer s.mu.Unlock()
83 if s.unrecordable == nil {
84 s.unrecordable = map[string]bool{}
85 }
86 s.unrecordable[key] = true
87}
88
89// OnStatus installs a hook that runs at the top of Status, BEFORE any lock is
90// taken, so a test can make something else happen while a caller is mid-read.
91//
92// It exists for one assertion that has no other seam: the status door
93// registers its long-poll waiter before its first read, and the only way to
94// catch the opposite order is to run a drain inside that read. The hook must
95// not re-enter the scanner (Status will be called again beneath it), so a test
96// uses sync.Once.
97func (s *MemScanner) OnStatus(f func()) {
98 s.mu.Lock()
99 defer s.mu.Unlock()
100 s.onStatus = f
101}
102
103// Status answers about the CURRENT version, and reports what the record SAYS
104// — denied and failed included. The re-issue rule below is Verdict's, and
105// applying it here would make a failed scan read as pending forever.
106func (s *MemScanner) Status(ctx context.Context, bucket, key string) (Verdict, ScanObject, error) {
107 // The hook runs unlocked, or anything it does that touches this scanner
108 // deadlocks against the lock taken below.
109 s.mu.Lock()
110 hook := s.onStatus
111 s.mu.Unlock()
112 if hook != nil {
113 hook()
114 }
115 s.mu.Lock()
116 defer s.mu.Unlock()
117 if err := s.takeFailure("Status"); err != nil {
118 return VerdictUnknown, ScanObject{}, err
119 }
120 if s.deniedHead[memScanKey(bucket, key)] {
121 // HeadObject itself refused: no version, no size, nothing to scan
122 // with. The production shape is a bucket policy the console is not
123 // covered by, as against the SSE-KMS case where the head succeeds and
124 // the GET is what fails.
125 return VerdictDenied, ScanObject{}, nil
126 }
127 obj, ok := s.objects[memScanKey(bucket, key)]
128 if !ok {
129 return VerdictUnknown, ScanObject{}, ErrObjectGone
130 }
131 rec, ok := s.verdicts[memScanKey(bucket, key)]
132 if !ok || rec.version != obj.Version() {
133 return VerdictUnknown, obj, nil
134 }
135 return rec.verdict, obj, nil
136}
137
138// Put seeds a verdict so a test can start from a partly-scanned bucket.
139func (s *MemScanner) Put(bucket, key, version string, v Verdict) {
140 s.mu.Lock()
141 defer s.mu.Unlock()
142 s.verdicts[memScanKey(bucket, key)] = scanRecord{version: version, verdict: v}
143}
144
145// IsEnabled lets Teardown's test prove the scanner was disabled while the
146// record that named it still existed.
147func (s *MemScanner) IsEnabled(bucket string) bool {
148 s.mu.Lock()
149 defer s.mu.Unlock()
150 return s.enabled[bucket]
151}
152
153func (s *MemScanner) takeFailure(method string) error {
154 if err, ok := s.failures[method]; ok {
155 delete(s.failures, method)
156 return err
157 }
158 return nil
159}
160
161func (s *MemScanner) EnsureEnabled(ctx context.Context, bucket string) error {
162 s.mu.Lock()
163 defer s.mu.Unlock()
164 if err := s.takeFailure("EnsureEnabled"); err != nil {
165 return err
166 }
167 s.enabled[bucket] = true // already enabled is a success
168 return nil
169}
170
171func (s *MemScanner) EnsureDisabled(ctx context.Context, bucket string) error {
172 s.mu.Lock()
173 defer s.mu.Unlock()
174 if err := s.takeFailure("EnsureDisabled"); err != nil {
175 return err
176 }
177 delete(s.enabled, bucket) // already gone is a success
178 return nil
179}
180
181func (s *MemScanner) Verdict(ctx context.Context, bucket, key, version string) (Verdict, error) {
182 s.mu.Lock()
183 defer s.mu.Unlock()
184 if err := s.takeFailure("Verdict"); err != nil {
185 return VerdictUnknown, err
186 }
187 rec, ok := s.verdicts[memScanKey(bucket, key)]
188 if !ok || rec.version != version {
189 return VerdictUnknown, nil
190 }
191 // The SWEEP's reading: denied and failed are re-issued rather than
192 // inherited, because both are transient and the object is not rewritten,
193 // so its version never changes and the repair would never take. Status
194 // deliberately does not do this — see the Scanner interface.
195 switch rec.verdict {
196 case VerdictDenied, VerdictFailed:
197 return VerdictUnknown, nil
198 }
199 return rec.verdict, nil
200}
201
202func (s *MemScanner) Scan(ctx context.Context, bucket, key, version string, size int64) (Verdict, bool, error) {
203 s.mu.Lock()
204 defer s.mu.Unlock()
205 if err := s.takeFailure("Scan"); err != nil {
206 return VerdictUnknown, false, err
207 }
208 if s.scans == nil {
209 s.scans = map[string]int{}
210 }
211 s.scans[key]++
212 v := VerdictClean
213 if got, ok := s.scanAs[key]; ok {
214 v = got
215 }
216 if s.unrecordable[key] {
217 // A verdict reached and not persisted: the engine judged the object,
218 // but nothing on the object says so.
219 return v, false, nil
220 }
221 s.verdicts[memScanKey(bucket, key)] = scanRecord{version: version, verdict: v}
222 return v, true, nil
223}
224
225// DenyHead makes Status refuse the head of one key.
226func (s *MemScanner) DenyHead(bucket, key string) {
227 s.mu.Lock()
228 defer s.mu.Unlock()
229 if s.deniedHead == nil {
230 s.deniedHead = map[string]bool{}
231 }
232 s.deniedHead[memScanKey(bucket, key)] = true
233}
234
235// Scans is how many times the engine was asked to read one key.
236func (s *MemScanner) Scans(key string) int {
237 s.mu.Lock()
238 defer s.mu.Unlock()
239 return s.scans[key]
240}
241
242// ScanAs makes Scan answer v for key rather than clean, so a test can stage a
243// skipped or infected object without a real engine. Skipped in particular is
244// not a hypothetical: it is every object over the size ceiling.
245func (s *MemScanner) ScanAs(key string, v Verdict) {
246 s.mu.Lock()
247 defer s.mu.Unlock()
248 if s.scanAs == nil {
249 s.scanAs = map[string]Verdict{}
250 }
251 s.scanAs[key] = v
252}
253
254func (s *MemScanner) Enforcement() DenyTag {
255 return DenyTag{Key: TagVerdict, Value: string(VerdictInfected)}
256}
257
258func (s *MemScanner) Capacity(ctx context.Context) (int, int, error) { return -1, -1, nil }
259
260// Provenance answers "clamav" rather than "memscanner" ON PURPOSE. The sweep
261// refuses when the record's engine disagrees with the running one (spec §7.4),
262// and test fixtures write Engine: "clamav" — a fake that named itself would
263// make every one of those fixtures refuse instead of sweep.
264func (s *MemScanner) Provenance(ctx context.Context) (string, string, error) {
265 s.mu.Lock()
266 defer s.mu.Unlock()
267 // Armable like the others: an engine that will not say what it is, is a
268 // state the drain and the sweep both have to survive, and a fake that
269 // could never be in it left that path untested.
270 if err := s.takeFailure("Provenance"); err != nil {
271 return "", "", err
272 }
273 return "clamav", "MemScanner/0/never", nil
274}
275
276var _ Scanner = (*MemScanner)(nil)
277