1
0
Fork 0
mirror of https://github.com/imjasonh/terraform-playground synced 2026-07-12 01:19:39 +00:00
terraform-playground/pymage/internal/cache/cache.go
Cursor Agent 0cc8bc424f
address Copilot review: normalize SBOM names, reject wheel '..' segments, Windows-safe cache/wheel rename, fix lock-not-found message
- sbom: normalize component names (PEP 503) for canonical pkg:pypi PURLs and stable ordering
- wheel: reject any member with a '..' path segment (catches escapes that resolve back under the prefix but out of site-packages)
- cache/wheelhouse: replace file atomically across platforms (os.Rename fails on existing dest on Windows)
- project: include requirements.lock in the no-lock-found error
- tests for each

Co-authored-by: Jason Hall <imjasonh@users.noreply.github.com>
2026-06-11 02:01:37 +00:00

130 lines
3.5 KiB
Go

// Package cache is a content-addressed, on-disk store for built OCI layers.
//
// Building a layer from a wheel means reading every member and gzip-compressing
// the tar — pure CPU that is wasted on every rebuild for dependencies that have
// not changed. Keyed by the wheel's sha256 (plus the install layout), the cache
// lets an unchanged dependency reuse its previously compressed blob verbatim,
// so a rebuild does no decompression/recompression for those layers.
package cache
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/tarball"
)
// Cache stores compressed layer blobs under a directory.
type Cache struct {
dir string
}
// New returns a cache rooted at dir, creating it if necessary.
func New(dir string) (*Cache, error) {
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, fmt.Errorf("cache: create %q: %w", dir, err)
}
return &Cache{dir: dir}, nil
}
func (c *Cache) path(key string) string {
sum := sha256.Sum256([]byte(key))
return filepath.Join(c.dir, hex.EncodeToString(sum[:])+".tar.gz")
}
// Get returns the cached layer for key, or (nil, false) on a miss.
func (c *Cache) Get(key string) (v1.Layer, bool) {
p := c.path(key)
if _, err := os.Stat(p); err != nil {
return nil, false
}
l, err := tarball.LayerFromFile(p)
if err != nil {
return nil, false
}
return l, true
}
// metaPath returns the on-disk path for a small text value keyed by key.
func (c *Cache) metaPath(key string) string {
sum := sha256.Sum256([]byte(key))
return filepath.Join(c.dir, "meta", hex.EncodeToString(sum[:])+".txt")
}
// GetText returns a small cached text value for key (e.g. a detected base
// interpreter version), or ("", false) on a miss.
func (c *Cache) GetText(key string) (string, bool) {
data, err := os.ReadFile(c.metaPath(key))
if err != nil {
return "", false
}
return string(data), true
}
// PutText stores a small text value for key.
func (c *Cache) PutText(key, val string) error {
p := c.metaPath(key)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
return err
}
tmp, err := os.CreateTemp(filepath.Dir(p), ".tmp-*")
if err != nil {
return err
}
tmpName := tmp.Name()
defer func() { _ = os.Remove(tmpName) }()
if _, err := tmp.WriteString(val); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
return replaceFile(tmpName, p)
}
// Put stores the layer's compressed blob under key. Writes are atomic (temp
// file + rename) so concurrent builders and interrupted writes are safe.
func (c *Cache) Put(key string, l v1.Layer) error {
rc, err := l.Compressed()
if err != nil {
return err
}
defer func() { _ = rc.Close() }()
tmp, err := os.CreateTemp(c.dir, ".tmp-*")
if err != nil {
return err
}
tmpName := tmp.Name()
defer func() { _ = os.Remove(tmpName) }()
if _, err := io.Copy(tmp, rc); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
return replaceFile(tmpName, c.path(key))
}
// replaceFile atomically moves src onto dst. os.Rename replaces an existing
// dst on POSIX but fails on Windows, so retry after removing dst there. The
// store is content-addressed (or overwrites an equivalent value), so removing
// an existing dst is safe.
func replaceFile(src, dst string) error {
if err := os.Rename(src, dst); err != nil {
if _, statErr := os.Stat(dst); statErr == nil {
_ = os.Remove(dst)
return os.Rename(src, dst)
}
return err
}
return nil
}