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