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 if data, err := lockedfile.Read(filepath.Join(c.dir, "trim.txt")); err == nil {
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 nil
393 }
394 }
395 }
396
397
398
399
400 cutoff := now.Add(-trimLimit - mtimeInterval)
401 for i := 0; i < 256; i++ {
402 subdir := filepath.Join(c.dir, fmt.Sprintf("%02x", i))
403 c.trimSubdir(subdir, cutoff)
404 }
405
406
407
408 var b bytes.Buffer
409 fmt.Fprintf(&b, "%d", now.Unix())
410 if err := lockedfile.Write(filepath.Join(c.dir, "trim.txt"), &b, 0o666); err != nil {
411 return err
412 }
413
414 return nil
415 }
416
417
418 func (c *DiskCache) trimSubdir(subdir string, cutoff time.Time) {
419
420
421
422
423
424 f, err := os.Open(subdir)
425 if err != nil {
426 return
427 }
428 names, _ := f.Readdirnames(-1)
429 f.Close()
430
431 for _, name := range names {
432
433 if !strings.HasSuffix(name, "-a") && !strings.HasSuffix(name, "-d") {
434 continue
435 }
436 entry := filepath.Join(subdir, name)
437 info, err := os.Stat(entry)
438 if err == nil && info.ModTime().Before(cutoff) {
439 if info.IsDir() {
440 os.RemoveAll(entry)
441 continue
442 }
443 os.Remove(entry)
444 }
445 }
446 }
447
448
449
450 func (c *DiskCache) putIndexEntry(id ActionID, out OutputID, size int64, allowVerify bool) error {
451
452
453
454
455
456
457
458
459
460
461
462 entry := fmt.Sprintf("v1 %x %x %20d %20d\n", id, out, size, time.Now().UnixNano())
463 if verify && allowVerify {
464 old, err := c.get(id)
465 if err == nil && (old.OutputID != out || old.Size != size) {
466
467 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)
468 panic(msg)
469 }
470 }
471 file := c.fileName(id, "a")
472
473
474 mode := os.O_WRONLY | os.O_CREATE
475 f, err := os.OpenFile(file, mode, 0o666)
476 if err != nil {
477 return err
478 }
479 _, err = f.WriteString(entry)
480 if err == nil {
481
482
483
484
485
486
487
488 err = f.Truncate(int64(len(entry)))
489 }
490 if closeErr := f.Close(); err == nil {
491 err = closeErr
492 }
493 if err != nil {
494
495
496 os.Remove(file)
497 return err
498 }
499 os.Chtimes(file, c.now(), c.now())
500
501 return nil
502 }
503
504
505
506
507 type noVerifyReadSeeker struct {
508 io.ReadSeeker
509 }
510
511
512
513 func (c *DiskCache) Put(id ActionID, file io.ReadSeeker) (OutputID, int64, error) {
514 wrapper, isNoVerify := file.(noVerifyReadSeeker)
515 if isNoVerify {
516 file = wrapper.ReadSeeker
517 }
518 return c.put(id, "", file, !isNoVerify)
519 }
520
521
522
523
524 func (c *DiskCache) PutExecutable(id ActionID, name string, file io.ReadSeeker) (OutputID, int64, error) {
525 if name == "" {
526 panic("PutExecutable called without a name")
527 }
528 wrapper, isNoVerify := file.(noVerifyReadSeeker)
529 if isNoVerify {
530 file = wrapper.ReadSeeker
531 }
532 return c.put(id, name, file, !isNoVerify)
533 }
534
535
536
537
538
539 func PutNoVerify(c Cache, id ActionID, file io.ReadSeeker) (OutputID, int64, error) {
540 return c.Put(id, noVerifyReadSeeker{file})
541 }
542
543 func (c *DiskCache) put(id ActionID, executableName string, file io.ReadSeeker, allowVerify bool) (OutputID, int64, error) {
544
545 h := sha256.New()
546 if _, err := file.Seek(0, 0); err != nil {
547 return OutputID{}, 0, err
548 }
549 size, err := io.Copy(h, file)
550 if err != nil {
551 return OutputID{}, 0, err
552 }
553 var out OutputID
554 h.Sum(out[:0])
555
556
557 fileMode := fs.FileMode(0o666)
558 if executableName != "" {
559 fileMode = 0o777
560 }
561 if err := c.copyFile(file, executableName, out, size, fileMode); err != nil {
562 return out, size, err
563 }
564
565
566 return out, size, c.putIndexEntry(id, out, size, allowVerify)
567 }
568
569
570 func PutBytes(c Cache, id ActionID, data []byte) error {
571 _, _, err := c.Put(id, bytes.NewReader(data))
572 return err
573 }
574
575
576
577 func (c *DiskCache) copyFile(file io.ReadSeeker, executableName string, out OutputID, size int64, perm os.FileMode) error {
578 name := c.fileName(out, "d")
579 info, err := os.Stat(name)
580 if executableName != "" {
581
582
583
584
585 if err != nil {
586 if !os.IsNotExist(err) {
587 return err
588 }
589 if err := os.Mkdir(name, 0o777); err != nil {
590 return err
591 }
592 if info, err = os.Stat(name); err != nil {
593 return err
594 }
595 }
596 if !info.IsDir() {
597 return errors.New("internal error: invalid binary cache entry: not a directory")
598 }
599
600
601 name = filepath.Join(name, executableName)
602 info, err = os.Stat(name)
603 }
604 if err == nil && info.Size() == size {
605
606 if f, err := os.Open(name); err == nil {
607 h := sha256.New()
608 io.Copy(h, f)
609 f.Close()
610 var out2 OutputID
611 h.Sum(out2[:0])
612 if out == out2 {
613 return nil
614 }
615 }
616
617 }
618
619
620 mode := os.O_RDWR | os.O_CREATE
621 if err == nil && info.Size() > size {
622 mode |= os.O_TRUNC
623 }
624 f, err := os.OpenFile(name, mode, perm)
625 if err != nil {
626 if base.IsETXTBSY(err) {
627
628
629
630 return nil
631 }
632 return err
633 }
634 defer f.Close()
635 if size == 0 {
636
637
638
639 return nil
640 }
641
642
643
644
645
646
647 if _, err := file.Seek(0, 0); err != nil {
648 f.Truncate(0)
649 return err
650 }
651 h := sha256.New()
652 w := io.MultiWriter(f, h)
653 if _, err := io.CopyN(w, file, size-1); err != nil {
654 f.Truncate(0)
655 return err
656 }
657
658
659
660 buf := make([]byte, 1)
661 if _, err := file.Read(buf); err != nil {
662 f.Truncate(0)
663 return err
664 }
665 h.Write(buf)
666 sum := h.Sum(nil)
667 if !bytes.Equal(sum, out[:]) {
668 f.Truncate(0)
669 return fmt.Errorf("file content changed underfoot")
670 }
671
672
673 if _, err := f.Write(buf); err != nil {
674 f.Truncate(0)
675 return err
676 }
677 if err := f.Close(); err != nil {
678
679
680
681 os.Remove(name)
682 return err
683 }
684 os.Chtimes(name, c.now(), c.now())
685
686 return nil
687 }
688
689
690
691
692
693
694
695
696
697 func (c *DiskCache) FuzzDir() string {
698 return filepath.Join(c.dir, "fuzz")
699 }
700
View as plain text