aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/pkg/pkg.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/pkg/pkg.go')
-rw-r--r--internal/pkg/pkg.go248
1 files changed, 248 insertions, 0 deletions
diff --git a/internal/pkg/pkg.go b/internal/pkg/pkg.go
new file mode 100644
index 00000000..2796b4f6
--- /dev/null
+++ b/internal/pkg/pkg.go
@@ -0,0 +1,248 @@
+// Package pkg provides utilities for packaging software.
+package pkg
+
+import (
+ "crypto/sha512"
+ "encoding/base64"
+ "errors"
+ "io"
+ "os"
+ "sync"
+
+ "hakurei.app/container/check"
+)
+
+type (
+ // A Checksum is a SHA-384 checksum computed for a cured [Artifact].
+ Checksum = [sha512.Size384]byte
+
+ // An ID is a unique identifier returned by [Artifact.ID]. This value must
+ // be deterministically determined ahead of time.
+ ID Checksum
+)
+
+// MustDecode decodes a string representation of [Checksum] and panics if there
+// is a decoding error or the resulting data is too short.
+func MustDecode(s string) (checksum Checksum) {
+ if n, err := base64.URLEncoding.Decode(
+ checksum[:],
+ []byte(s),
+ ); err != nil {
+ panic(err)
+ } else if n != len(Checksum{}) {
+ panic(io.ErrUnexpectedEOF)
+ }
+ return
+}
+
+// An Artifact is a read-only reference to a piece of data that may be created
+// deterministically but might not currently be available in memory or on the
+// filesystem.
+type Artifact interface {
+ // ID returns a globally unique identifier referring to the current
+ // [Artifact]. This value must be known ahead of time and guaranteed to be
+ // unique without having obtained the full contents of the [Artifact].
+ ID() ID
+
+ // Hash returns the [Checksum] created from the full contents of a cured
+ // [Artifact]. This can be stored for future lookup in a [Cache].
+ //
+ // A call to Hash implicitly cures [Artifact].
+ Hash() (Checksum, error)
+
+ // Pathname returns an absolute pathname to a file or directory populated
+ // with the full contents of [Artifact]. This is the most expensive
+ // operation possible on any [Artifact] and should be avoided if possible.
+ //
+ // A call to Pathname implicitly cures [Artifact].
+ //
+ // Callers must only open files read-only. If [Artifact] is a directory,
+ // files must not be created or removed under this directory.
+ Pathname() (*check.Absolute, error)
+}
+
+// A File refers to an [Artifact] backed by a single file.
+type File interface {
+ // Data returns the full contents of [Artifact].
+ //
+ // Callers must not modify the returned byte slice.
+ Data() ([]byte, error)
+
+ Artifact
+}
+
+const (
+ // dirIdentifier is the directory name appended to Cache.base for storing
+ // artifacts named after their [ID].
+ dirIdentifier = "identifier"
+ // dirChecksum is the directory name appended to Cache.base for storing
+ // artifacts named after their [Checksum].
+ dirChecksum = "checksum"
+)
+
+// Cache is a support layer that implementations of [Artifact] can use to store
+// cured [Artifact] data in a content addressed fashion.
+type Cache struct {
+ // Directory where all [Cache] related files are placed.
+ base *check.Absolute
+
+ // Synchronises access to public methods.
+ mu sync.RWMutex
+}
+
+// LoadFile loads the contents of a [File] by its identifier.
+func (c *Cache) LoadFile(id ID) (
+ pathname *check.Absolute,
+ data []byte,
+ err error,
+) {
+ pathname = c.base.Append(
+ dirIdentifier,
+ base64.URLEncoding.EncodeToString(id[:]),
+ )
+
+ c.mu.RLock()
+ data, err = os.ReadFile(pathname.String())
+ c.mu.RUnlock()
+
+ return
+}
+
+// A ChecksumMismatchError describes an [Artifact] with unexpected content.
+type ChecksumMismatchError struct {
+ // Actual and expected checksums.
+ Got, Want Checksum
+}
+
+func (e *ChecksumMismatchError) Error() string {
+ return "got " + base64.URLEncoding.EncodeToString(e.Got[:]) +
+ " instead of " + base64.URLEncoding.EncodeToString(e.Want[:])
+}
+
+// pathname returns the content-addressed pathname for a [Checksum].
+func (c *Cache) pathname(checksum *Checksum) *check.Absolute {
+ return c.base.Append(
+ dirChecksum,
+ base64.URLEncoding.EncodeToString(checksum[:]),
+ )
+}
+
+// pathnameIdent returns the identifier-based pathname for an [ID].
+func (c *Cache) pathnameIdent(id *ID) *check.Absolute {
+ return c.base.Append(
+ dirIdentifier,
+ base64.URLEncoding.EncodeToString(id[:]),
+ )
+}
+
+// storeFile stores the contents of a [File]. An optional checksum can be
+// passed via the result buffer which is used to validate the submitted data.
+//
+// If locking is disabled, the caller is responsible for acquiring a write lock
+// and releasing it after this method returns. This makes LoadOrStoreFile
+// possible without holding the lock while computing hash for store only.
+func (c *Cache) storeFile(
+ identifierPathname *check.Absolute,
+ data []byte,
+ buf *Checksum,
+ validate, lock bool,
+) error {
+ h := sha512.New384()
+ h.Write(data)
+ if validate {
+ if got := (Checksum)(h.Sum(nil)); got != *buf {
+ return &ChecksumMismatchError{got, *buf}
+ }
+ } else {
+ h.Sum(buf[:0])
+ }
+
+ checksumPathname := c.pathname(buf)
+
+ if lock {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ }
+
+ if f, err := os.OpenFile(
+ checksumPathname.String(),
+ os.O_WRONLY|os.O_CREATE|os.O_EXCL,
+ 0400,
+ ); err != nil {
+ // two artifacts may be backed by the same file
+ if !errors.Is(err, os.ErrExist) {
+ return err
+ }
+ } else if _, err = f.Write(data); err != nil {
+ // do not attempt cleanup: this is content-addressed and a partial
+ // write is caught during integrity check
+ return err
+ }
+
+ return os.Link(
+ checksumPathname.String(),
+ identifierPathname.String(),
+ )
+}
+
+// StoreFile stores the contents of a [File]. An optional checksum can be
+// passed via the result buffer which is used to validate the submitted data.
+func (c *Cache) StoreFile(
+ id ID,
+ data []byte,
+ buf *Checksum,
+ validate bool,
+) (pathname *check.Absolute, err error) {
+ pathname = c.pathnameIdent(&id)
+ err = c.storeFile(pathname, data, buf, validate, true)
+ return
+}
+
+// LoadOrStoreFile attempts to load the contents of a [File] by its identifier,
+// and if that file is not present, calls makeData and stores its result
+// instead. Hash validation behaviour is identical to StoreFile.
+func (c *Cache) LoadOrStoreFile(
+ id ID,
+ makeData func() ([]byte, error),
+ buf *Checksum,
+ validate bool,
+) (
+ pathname *check.Absolute,
+ data []byte,
+ store bool,
+ err error,
+) {
+ pathname = c.pathnameIdent(&id)
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ data, err = os.ReadFile(pathname.String())
+ if err == nil || !errors.Is(err, os.ErrNotExist) {
+ return
+ }
+ store = true
+
+ data, err = makeData()
+ if err != nil {
+ return
+ }
+ err = c.storeFile(pathname, data, buf, validate, false)
+ return
+}
+
+// New returns the address to a new instance of [Cache].
+func New(base *check.Absolute) (*Cache, error) {
+ for _, name := range []string{
+ dirIdentifier,
+ dirChecksum,
+ } {
+ if err := os.MkdirAll(base.Append(name).String(), 0700); err != nil &&
+ !errors.Is(err, os.ErrExist) {
+ return nil, err
+ }
+ }
+
+ return &Cache{
+ base: base,
+ }, nil
+}