Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions internal/archive/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,8 @@ func (index *ubuntuIndex) distPath(suffix string) string {
}

func (index *ubuntuIndex) fetch(path, digest string, flags fetchFlags) (io.ReadSeekCloser, error) {
reader, err := index.archive.cache.Open(digest)
const algo = cache.SHA256
reader, err := index.archive.cache.Open(algo, digest)
if err == nil {
return reader, nil
} else if err != cache.MissErr {
Expand Down Expand Up @@ -425,7 +426,7 @@ func (index *ubuntuIndex) fetch(path, digest string, flags fetchFlags) (io.ReadS
body = reader
}

writer := index.archive.cache.Create(digest)
writer := index.archive.cache.Create(algo, digest)
defer writer.Close()

_, err = io.Copy(writer, body)
Expand All @@ -436,7 +437,7 @@ func (index *ubuntuIndex) fetch(path, digest string, flags fetchFlags) (io.ReadS
return nil, fmt.Errorf("cannot fetch from archive: %v", err)
}

return index.archive.cache.Open(writer.Digest())
return index.archive.cache.Open(algo, writer.Digest())
}

func sectionPackageInfo(section control.Section) *PackageInfo {
Expand Down
83 changes: 57 additions & 26 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"os"
"path/filepath"
"time"

"golang.org/x/crypto/sha3"
)

func DefaultDir(suffix string) string {
Expand Down Expand Up @@ -89,41 +91,64 @@ func (cw *Writer) Digest() string {
return cw.digest
}

const digestKind = "sha256"
// hashAlgo identifies a supported hash algorithm.
type hashAlgo string

const (
SHA256 hashAlgo = "sha256"
SHA3_384 hashAlgo = "sha3-384"
)
Comment thread
upils marked this conversation as resolved.

var hashAlgos = []hashAlgo{SHA256, SHA3_384}

func newHash(algo hashAlgo) (hash.Hash, error) {
switch algo {
case SHA256:
return sha256.New(), nil
case SHA3_384:
return sha3.New384(), nil
default:
return nil, fmt.Errorf("unsupported hash algorithm: %q", algo)
}
}

var MissErr = fmt.Errorf("not cached")

func (c *Cache) filePath(digest string) string {
return filepath.Join(c.Dir, digestKind, digest)
func (c *Cache) filePath(algo hashAlgo, digest string) string {
return filepath.Join(c.Dir, string(algo), digest)
}
Comment thread
upils marked this conversation as resolved.

func (c *Cache) Create(digest string) *Writer {
func (c *Cache) Create(algo hashAlgo, digest string) *Writer {
if c.Dir == "" {
return &Writer{err: fmt.Errorf("internal error: cache directory is unset")}
}
err := os.MkdirAll(filepath.Join(c.Dir, digestKind), 0755)
h, err := newHash(algo)
if err != nil {
return &Writer{err: fmt.Errorf("internal error: %v", err)}
}
err = os.MkdirAll(filepath.Join(c.Dir, string(algo)), 0755)
if err != nil {
return &Writer{err: fmt.Errorf("cannot create cache directory: %v", err)}
}
var file *os.File
if digest == "" {
file, err = os.CreateTemp(c.filePath(""), "tmp.*")
file, err = os.CreateTemp(filepath.Join(c.Dir, string(algo)), "tmp.*")
} else {
file, err = os.Create(c.filePath(digest + ".tmp"))
file, err = os.Create(c.filePath(algo, digest+".tmp"))
}
if err != nil {
return &Writer{err: fmt.Errorf("cannot create cache file: %v", err)}
}
return &Writer{
dir: c.Dir,
digest: digest,
hash: sha256.New(),
hash: h,
file: file,
}
}

func (c *Cache) Write(digest string, data []byte) error {
f := c.Create(digest)
func (c *Cache) Write(algo hashAlgo, digest string, data []byte) error {
f := c.Create(algo, digest)
_, err1 := f.Write(data)
err2 := f.Close()
if err1 != nil {
Expand All @@ -132,11 +157,11 @@ func (c *Cache) Write(digest string, data []byte) error {
return err2
}

func (c *Cache) Open(digest string) (io.ReadSeekCloser, error) {
func (c *Cache) Open(algo hashAlgo, digest string) (io.ReadSeekCloser, error) {
if c.Dir == "" || digest == "" {
return nil, MissErr
}
filePath := c.filePath(digest)
filePath := c.filePath(algo, digest)
Comment thread
upils marked this conversation as resolved.
file, err := os.Open(filePath)
if os.IsNotExist(err) {
return nil, MissErr
Expand All @@ -151,8 +176,8 @@ func (c *Cache) Open(digest string) (io.ReadSeekCloser, error) {
return file, nil
}

func (c *Cache) Read(digest string) ([]byte, error) {
file, err := c.Open(digest)
func (c *Cache) Read(algo hashAlgo, digest string) ([]byte, error) {
file, err := c.Open(algo, digest)
if err != nil {
return nil, err
}
Expand All @@ -165,22 +190,28 @@ func (c *Cache) Read(digest string) ([]byte, error) {
}

func (c *Cache) Expire(timeout time.Duration) error {
entries, err := os.ReadDir(filepath.Join(c.Dir, digestKind))
if err != nil {
return fmt.Errorf("cannot list cache directory: %v", err)
}
expired := time.Now().Add(-timeout)
for _, entry := range entries {
finfo, err := entry.Info()
if err != nil {
return err
}
if finfo.ModTime().After(expired) {
for _, algo := range hashAlgos {
algoDir := filepath.Join(c.Dir, string(algo))
entries, err := os.ReadDir(algoDir)
Comment thread
upils marked this conversation as resolved.
if os.IsNotExist(err) {
continue
}
Comment thread
upils marked this conversation as resolved.
err = os.Remove(filepath.Join(c.Dir, digestKind, finfo.Name()))
if err != nil {
return fmt.Errorf("cannot expire cache entry: %v", err)
return fmt.Errorf("cannot list cache directory: %v", err)
}
for _, entry := range entries {
finfo, err := entry.Info()
if err != nil {
return err
}
if finfo.ModTime().After(expired) {
continue
}
err = os.Remove(filepath.Join(algoDir, finfo.Name()))
if err != nil {
return fmt.Errorf("cannot expire cache entry: %v", err)
}
}
}
return nil
Expand Down
139 changes: 121 additions & 18 deletions internal/cache/cache_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package cache_test

import (
. "gopkg.in/check.v1"

"io"
"os"
"path/filepath"
"strings"
"time"

. "gopkg.in/check.v1"

"github.com/canonical/chisel/internal/cache"
)

Expand Down Expand Up @@ -45,11 +45,11 @@ func (s *S) TestDefaultDir(c *C) {
func (s *S) TestCacheEmpty(c *C) {
cc := cache.Cache{c.MkDir()}

_, err := cc.Open(data1Digest)
_, err := cc.Open(cache.SHA256, data1Digest)
c.Assert(err, Equals, cache.MissErr)
_, err = cc.Read(data1Digest)
_, err = cc.Read(cache.SHA256, data1Digest)
c.Assert(err, Equals, cache.MissErr)
_, err = cc.Read("")
_, err = cc.Read(cache.SHA256, "")
c.Assert(err, Equals, cache.MissErr)
}

Expand All @@ -60,21 +60,21 @@ func (s *S) TestCacheReadWrite(c *C) {
data2Path := filepath.Join(cc.Dir, "sha256", data2Digest)
data3Path := filepath.Join(cc.Dir, "sha256", data3Digest)

err := cc.Write(data1Digest, []byte("data1"))
err := cc.Write(cache.SHA256, data1Digest, []byte("data1"))
c.Assert(err, IsNil)
data1, err := cc.Read(data1Digest)
data1, err := cc.Read(cache.SHA256, data1Digest)
c.Assert(err, IsNil)
c.Assert(string(data1), Equals, "data1")

err = cc.Write("", []byte("data2"))
err = cc.Write(cache.SHA256, "", []byte("data2"))
c.Assert(err, IsNil)
data2, err := cc.Read(data2Digest)
data2, err := cc.Read(cache.SHA256, data2Digest)
c.Assert(err, IsNil)
c.Assert(string(data2), Equals, "data2")

_, err = cc.Read(data3Digest)
_, err = cc.Read(cache.SHA256, data3Digest)
c.Assert(err, Equals, cache.MissErr)
_, err = cc.Read("")
_, err = cc.Read(cache.SHA256, "")
c.Assert(err, Equals, cache.MissErr)

_, err = os.Stat(data1Path)
Expand All @@ -98,7 +98,7 @@ func (s *S) TestCacheReadWrite(c *C) {
func (s *S) TestCacheCreate(c *C) {
cc := cache.Cache{Dir: c.MkDir()}

w := cc.Create("")
w := cc.Create(cache.SHA256, "")

c.Assert(w.Digest(), Equals, "")

Expand All @@ -113,15 +113,15 @@ func (s *S) TestCacheCreate(c *C) {

c.Assert(w.Digest(), Equals, data1Digest)

data1, err := cc.Read(data1Digest)
data1, err := cc.Read(cache.SHA256, data1Digest)
c.Assert(err, IsNil)
c.Assert(string(data1), Equals, "data1")
}

func (s *S) TestCacheWrongDigest(c *C) {
cc := cache.Cache{Dir: c.MkDir()}

w := cc.Create(data1Digest)
w := cc.Create(cache.SHA256, data1Digest)

c.Assert(w.Digest(), Equals, data1Digest)

Expand All @@ -130,19 +130,19 @@ func (s *S) TestCacheWrongDigest(c *C) {
c.Assert(err, IsNil)
c.Assert(errClose, ErrorMatches, "expected digest "+data1Digest+", got "+data2Digest)

_, err = cc.Read(data1Digest)
_, err = cc.Read(cache.SHA256, data1Digest)
c.Assert(err, Equals, cache.MissErr)
_, err = cc.Read(data2Digest)
_, err = cc.Read(cache.SHA256, data2Digest)
c.Assert(err, Equals, cache.MissErr)
}

func (s *S) TestCacheOpen(c *C) {
cc := cache.Cache{Dir: c.MkDir()}

err := cc.Write(data1Digest, []byte("data1"))
err := cc.Write(cache.SHA256, data1Digest, []byte("data1"))
c.Assert(err, IsNil)

f, err := cc.Open(data1Digest)
f, err := cc.Open(cache.SHA256, data1Digest)
c.Assert(err, IsNil)
data1, err := io.ReadAll(f)
closeErr := f.Close()
Expand All @@ -151,3 +151,106 @@ func (s *S) TestCacheOpen(c *C) {

c.Assert(string(data1), Equals, "data1")
}

func (s *S) TestCacheSHA3384(c *C) {
cc := cache.Cache{Dir: c.MkDir()}

w := cc.Create(cache.SHA3_384, "")
_, err := w.Write([]byte("data1"))
c.Assert(err, IsNil)
err = w.Close()
c.Assert(err, IsNil)
sha3Digest := w.Digest()

c.Assert(sha3Digest, Not(Equals), data1Digest)

data, err := cc.Read(cache.SHA3_384, sha3Digest)
c.Assert(err, IsNil)
c.Assert(string(data), Equals, "data1")

// SHA3-384 entry must not be visible under SHA-256.
_, err = cc.Open(cache.SHA256, sha3Digest)
c.Assert(err, Equals, cache.MissErr)

// SHA-256 entry must not be visible under SHA3-384.
err = cc.Write(cache.SHA256, data1Digest, []byte("data1"))
c.Assert(err, IsNil)
_, err = cc.Open(cache.SHA3_384, data1Digest)
c.Assert(err, Equals, cache.MissErr)
}

func (s *S) TestCacheExpireBothAlgorithms(c *C) {
cc := cache.Cache{Dir: c.MkDir()}

// Write entries under both algorithms.
err := cc.Write(cache.SHA256, data1Digest, []byte("data1"))
c.Assert(err, IsNil)
err = cc.Write(cache.SHA256, data2Digest, []byte("data2"))
c.Assert(err, IsNil)

w := cc.Create(cache.SHA3_384, "")
_, err = w.Write([]byte("sha3data1"))
c.Assert(err, IsNil)
err = w.Close()
c.Assert(err, IsNil)
sha3Digest1 := w.Digest()

w = cc.Create(cache.SHA3_384, "")
_, err = w.Write([]byte("sha3data2"))
c.Assert(err, IsNil)
err = w.Close()
c.Assert(err, IsNil)
sha3Digest2 := w.Digest()

sha256Expired := filepath.Join(cc.Dir, "sha256", data1Digest)
sha256Fresh := filepath.Join(cc.Dir, "sha256", data2Digest)
sha3Expired := filepath.Join(cc.Dir, "sha3-384", sha3Digest1)
sha3Fresh := filepath.Join(cc.Dir, "sha3-384", sha3Digest2)

// Mark one entry per algorithm as expired.
now := time.Now()
expiredTime := now.Add(-2 * time.Hour)
err = os.Chtimes(sha256Expired, now, expiredTime)
c.Assert(err, IsNil)
err = os.Chtimes(sha3Expired, now, expiredTime)
c.Assert(err, IsNil)

err = cc.Expire(time.Hour)
c.Assert(err, IsNil)

// Expired entries must be removed from both algorithm directories.
_, err = os.Stat(sha256Expired)
c.Assert(os.IsNotExist(err), Equals, true)
_, err = os.Stat(sha3Expired)
c.Assert(os.IsNotExist(err), Equals, true)

// Fresh entries must remain in both algorithm directories.
_, err = os.Stat(sha256Fresh)
c.Assert(err, IsNil)
_, err = os.Stat(sha3Fresh)
c.Assert(err, IsNil)
}

func (s *S) TestCacheExpireMissingAlgoDir(c *C) {
cc := cache.Cache{Dir: c.MkDir()}

// Only write under SHA-256; the sha3-384 directory won't exist.
err := cc.Write(cache.SHA256, data1Digest, []byte("data1"))
c.Assert(err, IsNil)

// Expire should succeed despite the missing sha3-384 directory.
err = cc.Expire(time.Hour)
c.Assert(err, IsNil)

// The fresh SHA-256 entry must still be present.
_, err = os.Stat(filepath.Join(cc.Dir, "sha256", data1Digest))
c.Assert(err, IsNil)
}

func (s *S) TestCacheExpireNoAlgoDirs(c *C) {
cc := cache.Cache{Dir: c.MkDir()}

// No algorithm directories exist at all.
err := cc.Expire(time.Hour)
c.Assert(err, IsNil)
}