1
2
3
4
5
6 package cache
7
8 import (
9 "bytes"
10 "crypto/sha256"
11 "encoding/hex"
12 "errors"
13 "fmt"
14 "internal/godebug"
15 "io"
16 "io/fs"
17 "os"
18 "path/filepath"
19 "strconv"
20 "strings"
21 "time"
22
23 "cmd/go/internal/base"
24 "cmd/go/internal/lockedfile"
25 "cmd/go/internal/mmap"
26 )
27
28
29
30
31 type ActionID [HashSize]byte
32
33
34 type OutputID [HashSize]byte
35
36
37 type Cache interface {
38
39
40
41
42
43 Get(ActionID) (Entry, error)
44
45
46
47
48
49
50
51
52
53
54
55 Put(ActionID, io.ReadSeeker) (_ OutputID, size int64, _ error)
56
57
58
59
60
61
62
63
64
65 Close() error
66
67
68
69
70
71
72 OutputFile(OutputID) string
73
74
75 FuzzDir() string
76 }
77
78
79 type DiskCache struct {
80 dir string
81 now func() time.Time
82 }
83
84
85
86
87
88
89
90
91
92
93
94
95 func Open(dir string) (*DiskCache, error) {
96 info, err := os.Stat(dir)
97 if err != nil {
98 return nil, err
99 }
100 if !info.IsDir() {
101 return nil, &fs.PathError{Op: "open", Path: dir, Err: fmt.Errorf("not a directory")}
102 }
103 for i := 0; i < 256; i++ {
104 name := filepath.Join(dir, fmt.Sprintf("%02x", i))
105 if err := os.MkdirAll(name, 0o777); err != nil {
106 return nil, err
107 }
108 }
109 c := &DiskCache{
110 dir: dir,
111 now: time.Now,
112 }
113 return c, nil
114 }
115
116
117 func (c *DiskCache) fileName(id [HashSize]byte, key string) string {
118 return filepath.Join(c.dir, fmt.Sprintf("%02x", id[0]), fmt.Sprintf("%x", id)+"-"+key)
119 }
120
121
122
123 type entryNotFoundError struct {
124 Err error
125 }
126
127 func (e *entryNotFoundError) Error() string {
128 if e.Err == nil {
129 return "cache entry not found"
130 }
131 return fmt.Sprintf("cache entry not found: %v", e.Err)
132 }
133
134 func (e *entryNotFoundError) Unwrap() error {
135 return e.Err
136 }
137
138 const (
139
140 hexSize = HashSize * 2
141 entrySize = 2 + 1 + hexSize + 1 + hexSize + 1 + 20 + 1 + 20 + 1
142 )
143
144
145
146
147
148
149
150
151
152
153 var verify = false
154
155 var errVerifyMode = errors.New("gocacheverify=1")
156
157
158 var DebugTest = false
159
160 func init() { initEnv() }
161
162 var (
163 gocacheverify = godebug.New("gocacheverify")
164 gocachehash = godebug.New("gocachehash")
165 gocachetest = godebug.New("gocachetest")
166 )
167
168 func initEnv() {
169 if gocacheverify.Value() == "1" {
170 gocacheverify.IncNonDefault()
171 verify = true
172 }
173 if gocachehash.Value() == "1" {
174 gocachehash.IncNonDefault()
175 debugHash = true
176 }
177 if gocachetest.Value() == "1" {
178 gocachetest.IncNonDefault()
179 DebugTest = true
180 }
181 }
182
183
184
185
186
187 func (c *DiskCache) Get(id ActionID) (Entry, error) {
188 if verify {
189 return Entry{}, &entryNotFoundError{Err: errVerifyMode}
190 }
191 return c.get(id)
192 }
193
194 type Entry struct {
195 OutputID OutputID
196 Size int64
197 Time time.Time
198 }
199
200
201 func (c *DiskCache) get(id ActionID) (Entry, error) {
202 missing := func(reason error) (Entry, error) {
203 return Entry{}, &entryNotFoundError{Err: reason}
204 }
205 f, err := os.Open(c.fileName(id, "a"))
206 if err != nil {
207 return missing(err)
208 }
209 defer f.Close()
210 entry := make([]byte, entrySize+1)
211 if n, err := io.ReadFull(f, entry); n > entrySize {
212 return missing(errors.New("too long"))
213 } else if err != io.ErrUnexpectedEOF {
214 if err == io.EOF {
215 return missing(errors.New("file is empty"))
216 }
217 return missing(err)
218 } else if n < entrySize {
219 return missing(errors.New("entry file incomplete"))
220 }
221 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' {
222 return missing(errors.New("invalid header"))
223 }
224 eid, entry := entry[3:3+hexSize], entry[3+hexSize:]
225 eout, entry := entry[1:1+hexSize], entry[1+hexSize:]
226 esize, entry := entry[1:1+20], entry[1+20:]
227 etime, entry := entry[1:1+20], entry[1+20:]
228 var buf [HashSize]byte
229 if _, err := hex.Decode(buf[:], eid); err != nil {
230 return missing(fmt.Errorf("decoding ID: %v", err))
231 } else if buf != id {
232 return missing(errors.New("mismatched ID"))
233 }
234 if _, err := hex.Decode(buf[:], eout); err != nil {
235 return missing(fmt.Errorf("decoding output ID: %v", err))
236 }
237 i := 0
238 for i < len(esize) && esize[i] == ' ' {
239 i++
240 }
241 size, err := strconv.ParseInt(string(esize[i:]), 10, 64)
242 if err != nil {
243 return missing(fmt.Errorf("parsing size: %v", err))
244 } else if size < 0 {
245 return missing(errors.New("negative size"))
246 }
247 i = 0
248 for i < len(etime) && etime[i] == ' ' {
249 i++
250 }
251 tm, err := strconv.ParseInt(string(etime[i:]), 10, 64)
252 if err != nil {
253 return missing(fmt.Errorf("parsing timestamp: %v", err))
254 } else if tm < 0 {
255 return missing(errors.New("negative timestamp"))
256 }
257
258 c.markUsed(c.fileName(id, "a"))
259
260 return Entry{buf, size, time.Unix(0, tm)}, nil
261 }
262
263
264
265 func GetFile(c Cache, id ActionID) (file string, entry Entry, err error) {
266 entry, err = c.Get(id)
267 if err != nil {
268 return "", Entry{}, err
269 }
270 file = c.OutputFile(entry.OutputID)
271 info, err := os.Stat(file)
272 if err != nil {
273 return "", Entry{}, &entryNotFoundError{Err: err}
274 }
275 if info.Size() != entry.Size {
276 return "", Entry{}, &entryNotFoundError{Err: errors.New("file incomplete")}
277 }
278 return file, entry, nil
279 }
280
281
282
283
284 func GetBytes(c Cache, id ActionID) ([]byte, Entry, error) {
285 entry, err := c.Get(id)
286 if err != nil {
287 return nil, entry, err
288 }
289 data, _ := os.ReadFile(c.OutputFile(entry.OutputID))
290 if sha256.Sum256(data) != entry.OutputID {
291 return nil, entry, &entryNotFoundError{Err: errors.New("bad checksum")}
292 }
293 return data, entry, nil
294 }
295
296
297
298
299
300
301
302
303 func GetMmap(c Cache, id ActionID) ([]byte, Entry, bool, error) {
304 entry, err := c.Get(id)
305 if err != nil {
306 return nil, entry, false, err
307 }
308 md, opened, err := mmap.Mmap(c.OutputFile(entry.OutputID))
309 if err != nil {
310 return nil, Entry{}, opened, err
311 }
312 if int64(len(md.Data)) != entry.Size {
313 return nil, Entry{}, true, &entryNotFoundError{Err: errors.New("file incomplete")}
314 }
315 return md.Data, entry, true, nil
316 }
317
318
319 func (c *DiskCache) OutputFile(out OutputID) string {
320 file := c.fileName(out, "d")
321 isDir := c.markUsed(file)
322 if isDir {
323 entries, err := os.ReadDir(file)
324 if err != nil {
325 return fmt.Sprintf("DO NOT USE - missing binary cache entry: %v", err)
326 }
327 if len(entries) != 1 {
328 return "DO NOT USE - invalid binary cache entry"
329 }
330 return filepath.Join(file, entries[0].Name())
331 }
332 return file
333 }
334
335
336
337
338
339
340
341
342
343
344
345
346
347 const (
348 mtimeInterval = 1 * time.Hour
349 trimInterval = 24 * time.Hour
350 trimLimit = 5 * 24 * time.Hour
351 )
352
353
354
355
356
357
358
359
360
361
362
363
364 func (c *DiskCache) markUsed(file string) (isDir bool) {
365 info, err := os.Stat(file)
366 if err != nil {
367 return false
368 }
369 if now := c.now(); now.Sub(info.ModTime()) >= mtimeInterval {
370 os.Chtimes(file, now, now)
371 }
372 return info.IsDir()
373 }
374
375 func (c *DiskCache) Close() error { return c.Trim() }
376
377
378 func (c *DiskCache) Trim() error {
379 now := c.now()
380
381
382
383
384
385
386
387
388 skipTrim := func(data []byte) bool {
389 if t, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64); err == nil {
390 lastTrim := time.Unix(t, 0)
391 if d := now.Sub(lastTrim); d < trimInterval && d > -mtimeInterval {
392 return true
393 }
394 }
395 return false
396 }
397
398
399 if data, err := lockedfile.Read(filepath.Join(c.dir, "trim.txt")); err == nil {
400 if skipTrim(data) {
401 return nil
402 }
403 }
404
405 errFileChanged := errors.New("file changed")
406
407
408
409 err := lockedfile.Transform(filepath.Join(c.dir, "trim.txt"), func(data []byte) ([]byte, error) {
410 if skipTrim(data) {
411
412
413
414 return nil, errFileChanged
415 }
416 return fmt.Appendf(nil, "%d", now.Unix()), nil
417 })
418 if errors.Is(err, errors.ErrUnsupported) {
419 return err
420 }
421 if errors.Is(err, errFileChanged) {
422
423 return nil
424 }
425
426
427
428
429 cutoff := now.Add(-trimLimit - mtimeInterval)
430 for i := 0; i < 256; i++ {
431 subdir := filepath.Join(c.dir, fmt.Sprintf("%02x", i))
432 c.trimSubdir(subdir, cutoff)
433 }
434
435 return nil
436 }
437
438
439 func (c *DiskCache) trimSubdir(subdir string, cutoff time.Time) {
440
441
442
443
444
445 f, err := os.Open(subdir)
446 if err != nil {
447 return
448 }
449 names, _ := f.Readdirnames(-1)
450 f.Close()
451
452 for _, name := range names {
453
454 if !strings.HasSuffix(name, "-a") && !strings.HasSuffix(name, "-d") {
455 continue
456 }
457 entry := filepath.Join(subdir, name)
458 info, err := os.Stat(entry)
459 if err == nil && info.ModTime().Before(cutoff) {
460 if info.IsDir() {
461 os.RemoveAll(entry)
462 continue
463 }
464 os.Remove(entry)
465 }
466 }
467 }
468
469
470
471 func (c *DiskCache) putIndexEntry(id ActionID, out OutputID, size int64, allowVerify bool) error {
472
473
474
475
476
477
478
479
480
481
482
483 entry := fmt.Sprintf("v1 %x %x %20d %20d\n", id, out, size, time.Now().UnixNano())
484 if verify && allowVerify {
485 old, err := c.get(id)
486 if err == nil && (old.OutputID != out || old.Size != size) {
487
488 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)
489 panic(msg)
490 }
491 }
492 file := c.fileName(id, "a")
493
494
495 mode := os.O_WRONLY | os.O_CREATE
496 f, err := os.OpenFile(file, mode, 0o666)
497 if err != nil {
498 return err
499 }
500 _, err = f.WriteString(entry)
501 if err == nil {
502
503
504
505
506
507
508
509 err = f.Truncate(int64(len(entry)))
510 }
511 if closeErr := f.Close(); err == nil {
512 err = closeErr
513 }
514 if err != nil {
515
516
517 os.Remove(file)
518 return err
519 }
520 os.Chtimes(file, c.now(), c.now())
521
522 return nil
523 }
524
525
526
527
528 type noVerifyReadSeeker struct {
529 io.ReadSeeker
530 }
531
532
533
534 func (c *DiskCache) Put(id ActionID, file io.ReadSeeker) (OutputID, int64, error) {
535 wrapper, isNoVerify := file.(noVerifyReadSeeker)
536 if isNoVerify {
537 file = wrapper.ReadSeeker
538 }
539 return c.put(id, "", file, !isNoVerify)
540 }
541
542
543
544
545 func (c *DiskCache) PutExecutable(id ActionID, name string, file io.ReadSeeker) (OutputID, int64, error) {
546 if name == "" {
547 panic("PutExecutable called without a name")
548 }
549 wrapper, isNoVerify := file.(noVerifyReadSeeker)
550 if isNoVerify {
551 file = wrapper.ReadSeeker
552 }
553 return c.put(id, name, file, !isNoVerify)
554 }
555
556
557
558
559
560 func PutNoVerify(c Cache, id ActionID, file io.ReadSeeker) (OutputID, int64, error) {
561 return c.Put(id, noVerifyReadSeeker{file})
562 }
563
564 func (c *DiskCache) put(id ActionID, executableName string, file io.ReadSeeker, allowVerify bool) (OutputID, int64, error) {
565
566 h := sha256.New()
567 if _, err := file.Seek(0, 0); err != nil {
568 return OutputID{}, 0, err
569 }
570 size, err := io.Copy(h, file)
571 if err != nil {
572 return OutputID{}, 0, err
573 }
574 var out OutputID
575 h.Sum(out[:0])
576
577
578 fileMode := fs.FileMode(0o666)
579 if executableName != "" {
580 fileMode = 0o777
581 }
582 if err := c.copyFile(file, executableName, out, size, fileMode); err != nil {
583 return out, size, err
584 }
585
586
587 return out, size, c.putIndexEntry(id, out, size, allowVerify)
588 }
589
590
591 func PutBytes(c Cache, id ActionID, data []byte) error {
592 _, _, err := c.Put(id, bytes.NewReader(data))
593 return err
594 }
595
596
597
598 func (c *DiskCache) copyFile(file io.ReadSeeker, executableName string, out OutputID, size int64, perm os.FileMode) error {
599 name := c.fileName(out, "d")
600 info, err := os.Stat(name)
601 if executableName != "" {
602
603
604
605
606 if err != nil {
607 if !os.IsNotExist(err) {
608 return err
609 }
610 if err := os.Mkdir(name, 0o777); err != nil {
611 return err
612 }
613 if info, err = os.Stat(name); err != nil {
614 return err
615 }
616 }
617 if !info.IsDir() {
618 return errors.New("internal error: invalid binary cache entry: not a directory")
619 }
620
621
622 name = filepath.Join(name, executableName)
623 info, err = os.Stat(name)
624 }
625 if err == nil && info.Size() == size {
626
627 if f, err := os.Open(name); err == nil {
628 h := sha256.New()
629 io.Copy(h, f)
630 f.Close()
631 var out2 OutputID
632 h.Sum(out2[:0])
633 if out == out2 {
634 return nil
635 }
636 }
637
638 }
639
640
641 mode := os.O_RDWR | os.O_CREATE
642 if err == nil && info.Size() > size {
643 mode |= os.O_TRUNC
644 }
645 f, err := os.OpenFile(name, mode, perm)
646 if err != nil {
647 if base.IsETXTBSY(err) {
648
649
650
651 return nil
652 }
653 return err
654 }
655 defer f.Close()
656 if size == 0 {
657
658
659
660 return nil
661 }
662
663
664
665
666
667
668 if _, err := file.Seek(0, 0); err != nil {
669 f.Truncate(0)
670 return err
671 }
672 h := sha256.New()
673 w := io.MultiWriter(f, h)
674 if _, err := io.CopyN(w, file, size-1); err != nil {
675 f.Truncate(0)
676 return err
677 }
678
679
680
681 buf := make([]byte, 1)
682 if _, err := file.Read(buf); err != nil {
683 f.Truncate(0)
684 return err
685 }
686 h.Write(buf)
687 sum := h.Sum(nil)
688 if !bytes.Equal(sum, out[:]) {
689 f.Truncate(0)
690 return fmt.Errorf("file content changed underfoot")
691 }
692
693
694 if _, err := f.Write(buf); err != nil {
695 f.Truncate(0)
696 return err
697 }
698 if err := f.Close(); err != nil {
699
700
701
702 os.Remove(name)
703 return err
704 }
705 os.Chtimes(name, c.now(), c.now())
706
707 return nil
708 }
709
710
711
712
713
714
715
716
717
718 func (c *DiskCache) FuzzDir() string {
719 return filepath.Join(c.dir, "fuzz")
720 }
721
View as plain text