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.ErrMiss {
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"
SHA384 hashAlgo = "sha384"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public values/functions/etc with private types is an anti pattern.

Also, this already had a name, right above: digestKind. Note how the entire public API below talks about a "digest", not a "hash". The hash is the internal Go types and values used to construct them locally. The overall PR needs to be fixed to make the terminology clear and less conflicting.

Note that if you want to change everything to be "hash", I'm fine with that too. We just cannot sit in the middle and use both terms with no clear lines. If you do that, please don't use "algo" as there are better terms that are naturally short.

)
Comment thread
upils marked this conversation as resolved.

var hashAlgos = []hashAlgo{SHA256, SHA384}

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

var ErrMiss = 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)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function got a call with a piece of data that is coming from a place it has no idea, and then it also got an error that it has no idea about, because it has not inspected it, from a different function. How does it know it's an internal error?

}
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.*")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the pattern from the existing code not working?

} 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, ErrMiss
}
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, ErrMiss
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.ErrMiss)
_, err = cc.Read(data1Digest)
_, err = cc.Read(cache.SHA256, data1Digest)
c.Assert(err, Equals, cache.ErrMiss)
_, err = cc.Read("")
_, err = cc.Read(cache.SHA256, "")
c.Assert(err, Equals, cache.ErrMiss)
}

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.ErrMiss)
_, err = cc.Read("")
_, err = cc.Read(cache.SHA256, "")
c.Assert(err, Equals, cache.ErrMiss)

_, 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.ErrMiss)
_, err = cc.Read(data2Digest)
_, err = cc.Read(cache.SHA256, data2Digest)
c.Assert(err, Equals, cache.ErrMiss)
}

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) TestCacheSHA384(c *C) {
cc := cache.Cache{Dir: c.MkDir()}

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

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

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

// SHA384 entry must not be visible under SHA256.
_, err = cc.Open(cache.SHA256, sha384Digest)
c.Assert(err, Equals, cache.ErrMiss)

// SHA256 entry must not be visible under SHA384.
err = cc.Write(cache.SHA256, data1Digest, []byte("data1"))
c.Assert(err, IsNil)
_, err = cc.Open(cache.SHA384, data1Digest)
c.Assert(err, Equals, cache.ErrMiss)
}

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.SHA384, "")
_, err = w.Write([]byte("sha384data1"))
c.Assert(err, IsNil)
err = w.Close()
c.Assert(err, IsNil)
sha384Digest1 := w.Digest()

w = cc.Create(cache.SHA384, "")
_, err = w.Write([]byte("sha384data2"))
c.Assert(err, IsNil)
err = w.Close()
c.Assert(err, IsNil)
sha384Digest2 := w.Digest()

sha256Expired := filepath.Join(cc.Dir, "sha256", data1Digest)
sha256Fresh := filepath.Join(cc.Dir, "sha256", data2Digest)
sha384Expired := filepath.Join(cc.Dir, "sha384", sha384Digest1)
sha384Fresh := filepath.Join(cc.Dir, "sha384", sha384Digest2)

// 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(sha384Expired, 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(sha384Expired)
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(sha384Fresh)
c.Assert(err, IsNil)
}

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

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

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

// The fresh SHA256 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)
}
Loading