1
2
3
4
5
6 package cache
7
8 import (
9 "bytes"
10 "crypto/sha256"
11 "encoding/hex"
12 "errors"
13 "fmt"
14 "io"
15 "io/fs"
16 "os"
17 "path/filepath"
18 "strconv"
19 "strings"
20 "time"
21
22 "github.com/rogpeppe/go-internal/lockedfile"
23 )
24
25
26
27
28 type ActionID [HashSize]byte
29
30
31 type OutputID [HashSize]byte
32
33
34 type Cache struct {
35 dir string
36 now func() time.Time
37 }
38
39
40
41
42
43
44
45
46
47
48
49
50 func Open(dir string) (*Cache, error) {
51 info, err := os.Stat(dir)
52 if err != nil {
53 return nil, err
54 }
55 if !info.IsDir() {
56 return nil, &fs.PathError{Op: "open", Path: dir, Err: fmt.Errorf("not a directory")}
57 }
58 for i := 0; i < 256; i++ {
59 name := filepath.Join(dir, fmt.Sprintf("%02x", i))
60 if err := os.MkdirAll(name, 0777); err != nil {
61 return nil, err
62 }
63 }
64 c := &Cache{
65 dir: dir,
66 now: time.Now,
67 }
68 return c, nil
69 }
70
71
72 func (c *Cache) fileName(id [HashSize]byte, key string) string {
73 return filepath.Join(c.dir, fmt.Sprintf("%02x", id[0]), fmt.Sprintf("%x", id)+"-"+key)
74 }
75
76
77
78 type entryNotFoundError struct {
79 Err error
80 }
81
82 func (e *entryNotFoundError) Error() string {
83 if e.Err == nil {
84 return "cache entry not found"
85 }
86 return fmt.Sprintf("cache entry not found: %v", e.Err)
87 }
88
89 func (e *entryNotFoundError) Unwrap() error {
90 return e.Err
91 }
92
93 const (
94
95 hexSize = HashSize * 2
96 entrySize = 2 + 1 + hexSize + 1 + hexSize + 1 + 20 + 1 + 20 + 1
97 )
98
99
100
101
102
103
104
105
106
107
108 var verify = false
109
110 var errVerifyMode = errors.New("gocacheverify=1")
111
112
113 var DebugTest = false
114
115 func init() { initEnv() }
116
117 func initEnv() {
118 verify = false
119 debugHash = false
120 debug := strings.Split(os.Getenv("GODEBUG"), ",")
121 for _, f := range debug {
122 if f == "gocacheverify=1" {
123 verify = true
124 }
125 if f == "gocachehash=1" {
126 debugHash = true
127 }
128 if f == "gocachetest=1" {
129 DebugTest = true
130 }
131 }
132 }
133
134
135
136
137
138 func (c *Cache) Get(id ActionID) (Entry, error) {
139 if verify {
140 return Entry{}, &entryNotFoundError{Err: errVerifyMode}
141 }
142 return c.get(id)
143 }
144
145 type Entry struct {
146 OutputID OutputID
147 Size int64
148 Time time.Time
149 }
150
151
152 func (c *Cache) get(id ActionID) (Entry, error) {
153 missing := func(reason error) (Entry, error) {
154 return Entry{}, &entryNotFoundError{Err: reason}
155 }
156 f, err := os.Open(c.fileName(id, "a"))
157 if err != nil {
158 return missing(err)
159 }
160 defer f.Close()
161 entry := make([]byte, entrySize+1)
162 if n, err := io.ReadFull(f, entry); n > entrySize {
163 return missing(errors.New("too long"))
164 } else if err != io.ErrUnexpectedEOF {
165 if err == io.EOF {
166 return missing(errors.New("file is empty"))
167 }
168 return missing(err)
169 } else if n < entrySize {
170 return missing(errors.New("entry file incomplete"))
171 }
172 if entry[0] != 'v' || entry[1] != '1' || entry[2] != ' ' || entry[3+hexSize] != ' ' || entry[3+hexSize+1+hexSize] != ' ' || entry[3+hexSize+1+hexSize+1+20] != ' ' || entry[entrySize-1] != '\n' {
173 return missing(errors.New("invalid header"))
174 }
175 eid, entry := entry[3:3+hexSize], entry[3+hexSize:]
176 eout, entry := entry[1:1+hexSize], entry[1+hexSize:]
177 esize, entry := entry[1:1+20], entry[1+20:]
178 etime, entry := entry[1:1+20], entry[1+20:]
179 var buf [HashSize]byte
180 if _, err := hex.Decode(buf[:], eid); err != nil {
181 return missing(fmt.Errorf("decoding ID: %v", err))
182 } else if buf != id {
183 return missing(errors.New("mismatched ID"))
184 }
185 if _, err := hex.Decode(buf[:], eout); err != nil {
186 return missing(fmt.Errorf("decoding output ID: %v", err))
187 }
188 i := 0
189 for i < len(esize) && esize[i] == ' ' {
190 i++
191 }
192 size, err := strconv.ParseInt(string(esize[i:]), 10, 64)
193 if err != nil {
194 return missing(fmt.Errorf("parsing size: %v", err))
195 } else if size < 0 {
196 return missing(errors.New("negative size"))
197 }
198 i = 0
199 for i < len(etime) && etime[i] == ' ' {
200 i++
201 }
202 tm, err := strconv.ParseInt(string(etime[i:]), 10, 64)
203 if err != nil {
204 return missing(fmt.Errorf("parsing timestamp: %v", err))
205 } else if tm < 0 {
206 return missing(errors.New("negative timestamp"))
207 }
208
209 c.used(c.fileName(id, "a"))
210
211 return Entry{buf, size, time.Unix(0, tm)}, nil
212 }
213
214
215
216 func (c *Cache) GetFile(id ActionID) (file string, entry Entry, err error) {
217 entry, err = c.Get(id)
218 if err != nil {
219 return "", Entry{}, err
220 }
221 file = c.OutputFile(entry.OutputID)
222 info, err := os.Stat(file)
223 if err != nil {
224 return "", Entry{}, &entryNotFoundError{Err: err}
225 }
226 if info.Size() != entry.Size {
227 return "", Entry{}, &entryNotFoundError{Err: errors.New("file incomplete")}
228 }
229 return file, entry, nil
230 }
231
232
233
234
235 func (c *Cache) GetBytes(id ActionID) ([]byte, Entry, error) {
236 entry, err := c.Get(id)
237 if err != nil {
238 return nil, entry, err
239 }
240 data, _ := os.ReadFile(c.OutputFile(entry.OutputID))
241 if sha256.Sum256(data) != entry.OutputID {
242 return nil, entry, &entryNotFoundError{Err: errors.New("bad checksum")}
243 }
244 return data, entry, nil
245 }
246
247
268
269
270 func (c *Cache) OutputFile(out OutputID) string {
271 file := c.fileName(out, "d")
272 c.used(file)
273 return file
274 }
275
276
277
278
279
280
281
282
283
284
285
286
287
288 const (
289 mtimeInterval = 1 * time.Hour
290 trimInterval = 24 * time.Hour
291 trimLimit = 5 * 24 * time.Hour
292 )
293
294
295
296
297
298
299
300
301
302
303 func (c *Cache) used(file string) {
304 info, err := os.Stat(file)
305 if err == nil && c.now().Sub(info.ModTime()) < mtimeInterval {
306 return
307 }
308 os.Chtimes(file, c.now(), c.now())
309 }
310
311
312 func (c *Cache) Trim() error {
313 now := c.now()
314
315
316
317
318
319
320
321
322 if data, err := lockedfile.Read(filepath.Join(c.dir, "trim.txt")); err == nil {
323 if t, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64); err == nil {
324 lastTrim := time.Unix(t, 0)
325 if d := now.Sub(lastTrim); d < trimInterval && d > -mtimeInterval {
326 return nil
327 }
328 }
329 }
330
331
332
333
334 cutoff := now.Add(-trimLimit - mtimeInterval)
335 for i := 0; i < 256; i++ {
336 subdir := filepath.Join(c.dir, fmt.Sprintf("%02x", i))
337 c.trimSubdir(subdir, cutoff)
338 }
339
340
341
342 var b bytes.Buffer
343 fmt.Fprintf(&b, "%d", now.Unix())
344 if err := lockedfile.Write(filepath.Join(c.dir, "trim.txt"), &b, 0666); err != nil {
345 return err
346 }
347
348 return nil
349 }
350
351
352 func (c *Cache) trimSubdir(subdir string, cutoff time.Time) {
353
354
355
356
357
358 f, err := os.Open(subdir)
359 if err != nil {
360 return
361 }
362 names, _ := f.Readdirnames(-1)
363 f.Close()
364
365 for _, name := range names {
366
367 if !strings.HasSuffix(name, "-a") && !strings.HasSuffix(name, "-d") {
368 continue
369 }
370 entry := filepath.Join(subdir, name)
371 info, err := os.Stat(entry)
372 if err == nil && info.ModTime().Before(cutoff) {
373 os.Remove(entry)
374 }
375 }
376 }
377
378
379
380 func (c *Cache) putIndexEntry(id ActionID, out OutputID, size int64, allowVerify bool) error {
381
382
383
384
385
386
387
388
389
390
391
392 entry := fmt.Sprintf("v1 %x %x %20d %20d\n", id, out, size, time.Now().UnixNano())
393 if verify && allowVerify {
394 old, err := c.get(id)
395 if err == nil && (old.OutputID != out || old.Size != size) {
396
397 msg := fmt.Sprintf("go: internal cache error: cache verify failed: id=%x changed:<<<\n%s\n>>>\nold: %x %d\nnew: %x %d", id, reverseHash(id), out, size, old.OutputID, old.Size)
398 panic(msg)
399 }
400 }
401 file := c.fileName(id, "a")
402
403
404 mode := os.O_WRONLY | os.O_CREATE
405 f, err := os.OpenFile(file, mode, 0666)
406 if err != nil {
407 return err
408 }
409 _, err = f.WriteString(entry)
410 if err == nil {
411
412
413
414
415
416
417
418 err = f.Truncate(int64(len(entry)))
419 }
420 if closeErr := f.Close(); err == nil {
421 err = closeErr
422 }
423 if err != nil {
424
425
426 os.Remove(file)
427 return err
428 }
429 os.Chtimes(file, c.now(), c.now())
430
431 return nil
432 }
433
434
435
436 func (c *Cache) Put(id ActionID, file io.ReadSeeker) (OutputID, int64, error) {
437 return c.put(id, file, true)
438 }
439
440
441
442
443
444 func (c *Cache) PutNoVerify(id ActionID, file io.ReadSeeker) (OutputID, int64, error) {
445 return c.put(id, file, false)
446 }
447
448 func (c *Cache) put(id ActionID, file io.ReadSeeker, allowVerify bool) (OutputID, int64, error) {
449
450 h := sha256.New()
451 if _, err := file.Seek(0, 0); err != nil {
452 return OutputID{}, 0, err
453 }
454 size, err := io.Copy(h, file)
455 if err != nil {
456 return OutputID{}, 0, err
457 }
458 var out OutputID
459 h.Sum(out[:0])
460
461
462 if err := c.copyFile(file, out, size); err != nil {
463 return out, size, err
464 }
465
466
467 return out, size, c.putIndexEntry(id, out, size, allowVerify)
468 }
469
470
471 func (c *Cache) PutBytes(id ActionID, data []byte) error {
472 _, _, err := c.Put(id, bytes.NewReader(data))
473 return err
474 }
475
476
477
478 func (c *Cache) copyFile(file io.ReadSeeker, out OutputID, size int64) error {
479 name := c.fileName(out, "d")
480 info, err := os.Stat(name)
481 if err == nil && info.Size() == size {
482
483 if f, err := os.Open(name); err == nil {
484 h := sha256.New()
485 io.Copy(h, f)
486 f.Close()
487 var out2 OutputID
488 h.Sum(out2[:0])
489 if out == out2 {
490 return nil
491 }
492 }
493
494 }
495
496
497 mode := os.O_RDWR | os.O_CREATE
498 if err == nil && info.Size() > size {
499 mode |= os.O_TRUNC
500 }
501 f, err := os.OpenFile(name, mode, 0666)
502 if err != nil {
503 return err
504 }
505 defer f.Close()
506 if size == 0 {
507
508
509
510 return nil
511 }
512
513
514
515
516
517
518 if _, err := file.Seek(0, 0); err != nil {
519 f.Truncate(0)
520 return err
521 }
522 h := sha256.New()
523 w := io.MultiWriter(f, h)
524 if _, err := io.CopyN(w, file, size-1); err != nil {
525 f.Truncate(0)
526 return err
527 }
528
529
530
531 buf := make([]byte, 1)
532 if _, err := file.Read(buf); err != nil {
533 f.Truncate(0)
534 return err
535 }
536 h.Write(buf)
537 sum := h.Sum(nil)
538 if !bytes.Equal(sum, out[:]) {
539 f.Truncate(0)
540 return fmt.Errorf("file content changed underfoot")
541 }
542
543
544 if _, err := f.Write(buf); err != nil {
545 f.Truncate(0)
546 return err
547 }
548 if err := f.Close(); err != nil {
549
550
551
552 os.Remove(name)
553 return err
554 }
555 os.Chtimes(name, c.now(), c.now())
556
557 return nil
558 }
559
560
561
562
563
564
565
566
567
568 func (c *Cache) FuzzDir() string {
569 return filepath.Join(c.dir, "fuzz")
570 }
571
View as plain text