Skip to content
Draft

Pack #10

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
86 changes: 86 additions & 0 deletions internal/caches/archive/archive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Package archive provides a read-only cache, backed by a zip archive.
package archive

import (
"archive/zip"
"errors"
"io"
"log/slog"
"os"

"github.com/AlekSi/hardcache/internal/go/cache"
"github.com/AlekSi/lazyerrors"
)

// Cache represents a read-only [cache.Cache], backed by a zip archive.
type Cache struct {
zr *zip.Reader
c io.Closer
d

Check failure on line 19 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on macos-26

undefined: d

Check failure on line 19 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on ubuntu-24.04

undefined: d

Check failure on line 19 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on windows-2025

undefined: d
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

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

Incomplete field declaration. The field d has no type specified, which will cause a compilation error.

Copilot uses AI. Check for mistakes.
l *slog.Logger
}

// New creates a new [Cache].
func New(r io.ReaderAt, size int64, l *slog.Logger) (*Cache, error) {
zr, err := zip.NewReader(r, size)
if err != nil {
return nil, lazyerrors.Error(err)
}

os.CreateTemp()

Check failure on line 30 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on macos-26

not enough arguments in call to os.CreateTemp

Check failure on line 30 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on ubuntu-24.04

not enough arguments in call to os.CreateTemp

Check failure on line 30 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on windows-2025

not enough arguments in call to os.CreateTemp
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

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

Unused function call. os.CreateTemp() is called but its return values are ignored and the created temporary file is never used or cleaned up. This will create a resource leak.

Suggested change
os.CreateTemp()

Copilot uses AI. Check for mistakes.

return &Cache{
zr: zr,
l: l,
}, nil
}

Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

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

Missing documentation. The exported function Open lacks a documentation comment, which is required for all exported identifiers in Go.

Suggested change
// Open opens a zip archive file and returns a new [Cache].

Copilot uses AI. Check for mistakes.
func Open(file string, l *slog.Logger) (*Cache, error) {
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

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

[nitpick] Function naming inconsistency. The function Open should be named consistently with Go conventions, such as OpenFile or NewFromFile, to clearly indicate it opens a cache from a file path. Additionally, it should have a documentation comment explaining its purpose.

Suggested change
func Open(file string, l *slog.Logger) (*Cache, error) {
// OpenFile opens a read-only Cache backed by a zip archive at the given file path.
func OpenFile(file string, l *slog.Logger) (*Cache, error) {

Copilot uses AI. Check for mistakes.
rc, err := zip.OpenReader(file)
if err != nil {
return nil, lazyerrors.Error(err)
}

return &Cache{
zr: &rc.Reader,
c: rc,
l: l,
}, nil
}

// Get implements [cache.Cache].
func (c *Cache) Get(id cache.ActionID) (cache.Entry, error) {
return c.dc.Get(id)

Check failure on line 53 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on macos-26

c.dc undefined (type *Cache has no field or method dc)

Check failure on line 53 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on ubuntu-24.04

c.dc undefined (type *Cache has no field or method dc)

Check failure on line 53 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on windows-2025

c.dc undefined (type *Cache has no field or method dc)
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

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

Undefined field access. The field c.dc is accessed but never initialized in the New or Open functions, which will cause a nil pointer dereference at runtime.

Copilot uses AI. Check for mistakes.
}

// Put implements [cache.Cache] by returning an error.
func (c *Cache) Put(id cache.ActionID, rs io.ReadSeeker) (_ cache.OutputID, _ int64, err error) {
err = errors.New("archive cache is read-only")
return
}

// Close implements [cache.Cache].
func (c *Cache) Close() error {
if c.c != nil {
if err := c.c.Close(); err != nil {
return lazyerrors.Error(err)
}
}

return nil
}

// OutputFile implements [cache.Cache].
func (c *Cache) OutputFile(id cache.OutputID) string {
return c.dc.OutputFile(id)

Check failure on line 75 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on macos-26

c.dc undefined (type *Cache has no field or method dc)

Check failure on line 75 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on ubuntu-24.04

c.dc undefined (type *Cache has no field or method dc)

Check failure on line 75 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on windows-2025

c.dc undefined (type *Cache has no field or method dc)
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

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

Undefined field access. The field c.dc is accessed but never initialized in the New or Open functions, which will cause a nil pointer dereference at runtime.

Copilot uses AI. Check for mistakes.
}

// FuzzDir implements [cache.Cache].
func (c *Cache) FuzzDir() string {
return

Check failure on line 80 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on macos-26

not enough return values

Check failure on line 80 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on ubuntu-24.04

not enough return values

Check failure on line 80 in internal/caches/archive/archive.go

View workflow job for this annotation

GitHub Actions / Test 1.25.x on windows-2025

not enough return values
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

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

Empty return value. The function should return a string representing the fuzz directory, but returns an empty value instead.

Suggested change
return
return ""

Copilot uses AI. Check for mistakes.
}

// check interfaces
var (
_ cache.Cache = (*Cache)(nil)
)
4 changes: 2 additions & 2 deletions internal/caches/local/local.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Package local provides local Go build cache.
// Package local provides local cache, fully compatible with the built-in one.
package local

import (
Expand All @@ -11,7 +11,7 @@ import (
"github.com/AlekSi/hardcache/internal/go/cache"
)

// Cache represents a local Go build cache, compatible with a built-in one.
// Cache represents a local [cache.Cache], fully compatible with the built-in one.
// It provides more configuration options for trimming.
type Cache struct {
dc *cache.DiskCache
Expand Down
167 changes: 167 additions & 0 deletions internal/pack/pack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Package pack provides functionality for packing and unpacking directories.
package pack

import (
"archive/zip"
"compress/flate"
"errors"
"io"
"io/fs"
"os"

"github.com/AlekSi/lazyerrors"
)

// Pack compresses the contents of the specified directory
// and writes it to the provided writer in ZIP format with the given comment.
func Pack(dir, comment string, w io.Writer) (resErr error) {
zw := zip.NewWriter(w)
defer func() {
if e := zw.Close(); resErr == nil {
resErr = lazyerrors.Error(e)
}
}()

zw.RegisterCompressor(zip.Deflate, func(w io.Writer) (io.WriteCloser, error) {
return flate.NewWriter(w, flate.BestCompression)
})

root, err := os.OpenRoot(dir)
if err != nil {
resErr = lazyerrors.Error(err)
return
}

defer func() {
if e := root.Close(); resErr == nil {
resErr = lazyerrors.Error(e)
}
}()

if err = zw.AddFS(root.FS()); err != nil {
resErr = lazyerrors.Error(err)
return
}

if err = zw.SetComment(comment); err != nil {
resErr = lazyerrors.Error(err)
return
}

return
}

// putFile writes the contents of src file to the dst path.
// If dst already exists, it will be touched, but not overwritten.
func putFile(dst string, src fs.File) (resErr error) {
srcFI, err := src.Stat()
if err != nil {
resErr = lazyerrors.Error(err)
return
}

dstFI, err := os.Stat(dst)
if err == nil {
if err = os.Chtimes(dst, dstFI.ModTime(), srcFI.ModTime()); err != nil {
resErr = lazyerrors.Error(err)
}

return
}

if !errors.Is(err, fs.ErrNotExist) {
resErr = lazyerrors.Error(err)
return
}

f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, srcFI.Mode())
if err != nil {
if errors.Is(err, fs.ErrExist) {
// another process created the file in the meantime
return
}

resErr = lazyerrors.Error(err)
return
}

defer func() {
if e := f.Close(); resErr == nil {
resErr = lazyerrors.Error(e)
}
}()

if _, err = io.Copy(f, src); err != nil {
resErr = lazyerrors.Error(err)
return
}

return
}

// Unpack extracts the contents of a ZIP archive from the provided reader
// and writes it to the specified directory.
// It also returns the archive comment.
func Unpack(r io.ReaderAt, size int64, dir string) (comment string, resErr error) {
zr, err := zip.NewReader(r, size)
if err != nil {
resErr = lazyerrors.Error(err)
return
}

comment = zr.Comment

root, err := os.OpenRoot(dir)
if err != nil {
resErr = lazyerrors.Error(err)
return
}
defer func() {
if e := root.Close(); resErr == nil {
resErr = lazyerrors.Error(e)
}
}()

for _, f := range zr.File {
fp := f.Name

if f.FileInfo().IsDir() {
err = root.MkdirAll(fp, f.Mode())
if err != nil {
resErr = lazyerrors.Error(err)
return
}

continue
}

// err = root.MkdirAll(fp[:len(fp)-len(f.FileInfo().Name())], 0o755)
// if err != nil {
// resErr = lazyerrors.Error(err)
// return
// }

Comment on lines +138 to +143
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

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

Commented-out code should be removed. This block of code for creating parent directories appears to be incomplete work that should either be implemented or removed before merging.

Suggested change
// err = root.MkdirAll(fp[:len(fp)-len(f.FileInfo().Name())], 0o755)
// if err != nil {
// resErr = lazyerrors.Error(err)
// return
// }

Copilot uses AI. Check for mistakes.
dst, err := root.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
resErr = lazyerrors.Error(err)
return
}

rc, err := f.Open()
if err != nil {
dst.Close()
resErr = lazyerrors.Error(err)
return
}

_, err = io.Copy(dst, rc)
rc.Close()
dst.Close()
if err != nil {
resErr = lazyerrors.Error(err)
return
}
Comment on lines +159 to +163
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

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

Resource leak on error path. If f.Open() succeeds but an error is returned, the dst file handle is closed but its error is ignored. Consider checking the close error or using a deferred close with proper error handling.

Suggested change
dst.Close()
if err != nil {
resErr = lazyerrors.Error(err)
return
}
closeErr := dst.Close()
if err != nil {
resErr = lazyerrors.Error(err)
return
}
if closeErr != nil && resErr == nil {
resErr = lazyerrors.Error(closeErr)
return
}

Copilot uses AI. Check for mistakes.
}

return
}
Loading