@@ -3,8 +3,11 @@ package cache
33import (
44 "errors"
55 "fmt"
6+ "io/fs"
7+ "math/rand/v2"
68 "os"
79 "path/filepath"
10+ "strconv"
811)
912
1013// ErrNotFound is returned when a blob or action result is not in the cache.
@@ -19,21 +22,37 @@ var ErrNotFound = errors.New("cache: not found")
1922// Content with an externally declared checksum (remotely fetched plugins)
2023// needs no action cache entry: the declared sha256 is the address, so it is
2124// stored and loaded directly — see SHA256Digest.
25+ //
26+ // All I/O goes through an os.Root, so no entry name — hash-derived or read
27+ // from an action cache entry — can escape the cache directory.
2228type CAS struct {
23- root string
24- tmp string
29+ root * os.Root
2530}
2631
27- func newCAS (root string ) (* CAS , error ) {
28- tmp := filepath .Join (root , "tmp" )
29- if err := os .MkdirAll (tmp , 0755 ); err != nil {
30- return nil , fmt .Errorf ("cache: create %s: %w" , tmp , err )
32+ func newCAS (root * os.Root ) (* CAS , error ) {
33+ if err := root .MkdirAll ("tmp" , 0755 ); err != nil {
34+ return nil , fmt .Errorf ("cache: create tmp: %w" , err )
3135 }
32- return & CAS {root : root , tmp : tmp }, nil
36+ return & CAS {root : root }, nil
3337}
3438
39+ // path returns a blob's path relative to the cache root.
3540func (c * CAS ) path (d Digest ) string {
36- return filepath .Join (c .root , "cas" , d .Hash [:2 ], d .Hash )
41+ return filepath .Join ("cas" , d .Hash [:2 ], d .Hash )
42+ }
43+
44+ // createTemp creates a staging file under tmp/ in the cache root, returning
45+ // the open file and its root-relative name.
46+ func (c * CAS ) createTemp (prefix string ) (* os.File , string , error ) {
47+ for range 10000 {
48+ name := filepath .Join ("tmp" , prefix + strconv .FormatUint (rand .Uint64 (), 36 ))
49+ f , err := c .root .OpenFile (name , os .O_RDWR | os .O_CREATE | os .O_EXCL , 0644 )
50+ if errors .Is (err , fs .ErrExist ) {
51+ continue
52+ }
53+ return f , name , err
54+ }
55+ return nil , "" , errors .New ("cache: could not create temp file" )
3756}
3857
3958// Put stores a blob and returns its digest. Writing is atomic: the blob is
@@ -45,25 +64,25 @@ func (c *CAS) Put(data []byte) (Digest, error) {
4564 // Skip the write only when an entry of the right size already exists; a
4665 // wrong-sized entry is corrupt and is atomically replaced by the rename
4766 // below.
48- if fi , err := os .Stat (path ); err == nil && fi .Size () == d .SizeBytes {
67+ if fi , err := c . root .Stat (path ); err == nil && fi .Size () == d .SizeBytes {
4968 return d , nil
5069 }
51- if err := os .MkdirAll (filepath .Dir (path ), 0755 ); err != nil {
70+ if err := c . root .MkdirAll (filepath .Dir (path ), 0755 ); err != nil {
5271 return Digest {}, fmt .Errorf ("cache: %w" , err )
5372 }
54- f , err := os . CreateTemp ( c . tmp , d .Hash [:8 ]+ "-* " )
73+ f , name , err := c . createTemp ( d .Hash [:8 ] + "- " )
5574 if err != nil {
5675 return Digest {}, fmt .Errorf ("cache: %w" , err )
5776 }
58- defer os . Remove (f . Name () )
77+ defer c . root . Remove (name )
5978 if _ , err := f .Write (data ); err != nil {
6079 f .Close ()
6180 return Digest {}, fmt .Errorf ("cache: %w" , err )
6281 }
6382 if err := f .Close (); err != nil {
6483 return Digest {}, fmt .Errorf ("cache: %w" , err )
6584 }
66- if err := os . Rename (f . Name () , path ); err != nil {
85+ if err := c . root . Rename (name , path ); err != nil {
6786 return Digest {}, fmt .Errorf ("cache: %w" , err )
6887 }
6988 return d , nil
@@ -77,15 +96,15 @@ func (c *CAS) Get(d Digest) ([]byte, error) {
7796 return nil , ErrNotFound
7897 }
7998 path := c .path (d )
80- data , err := os .ReadFile (path )
99+ data , err := c . root .ReadFile (path )
81100 if err != nil {
82- if os . IsNotExist (err ) {
101+ if errors . Is (err , fs . ErrNotExist ) {
83102 return nil , ErrNotFound
84103 }
85104 return nil , fmt .Errorf ("cache: %w" , err )
86105 }
87106 if DigestOf (data ).Hash != d .Hash {
88- os .Remove (path )
107+ c . root .Remove (path )
89108 return nil , ErrNotFound
90109 }
91110 return data , nil
@@ -98,7 +117,7 @@ func (c *CAS) Contains(d Digest) bool {
98117 if ! d .valid () {
99118 return false
100119 }
101- fi , err := os .Stat (c .path (d ))
120+ fi , err := c . root .Stat (c .path (d ))
102121 if err != nil {
103122 return false
104123 }
0 commit comments