aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/pkg
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-01-03 21:24:50 +0900
committerOphestra <cat@gensokyo.uk>2026-01-03 21:26:25 +0900
commit8ad990906555d843d1212c5eb8a1ac194031426c (patch)
tree710c54cad83d8fb20dd6666791249100480a426c /internal/pkg
parentdeda16da38187091ec59d28fb4fbc672ff71e70e (diff)
internal/pkg: compute identifier from deps
This provides infrastructure for computing a deterministic identifier based on current artifact kind, opaque parameters data, and optional dependency kind and identifiers. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/pkg')
-rw-r--r--internal/pkg/net.go3
-rw-r--r--internal/pkg/pkg.go41
-rw-r--r--internal/pkg/pkg_test.go40
3 files changed, 84 insertions, 0 deletions
diff --git a/internal/pkg/net.go b/internal/pkg/net.go
index 16229b25..519a9b0f 100644
--- a/internal/pkg/net.go
+++ b/internal/pkg/net.go
@@ -53,6 +53,9 @@ func (c *Cache) NewHTTPGet(hc *http.Client, url string, checksum Checksum) (File
return c.NewHTTP(hc, req, checksum), nil
}
+// Kind returns the hardcoded [Kind] constant.
+func (a *httpArtifact) Kind() Kind { return KindHTTP }
+
// ID returns the caller-supplied hash of the response body.
func (a *httpArtifact) ID() ID { return a.id }
diff --git a/internal/pkg/pkg.go b/internal/pkg/pkg.go
index 73b93e3e..40bad6ff 100644
--- a/internal/pkg/pkg.go
+++ b/internal/pkg/pkg.go
@@ -2,11 +2,14 @@
package pkg
import (
+ "bytes"
"crypto/sha512"
"encoding/base64"
+ "encoding/binary"
"errors"
"io"
"os"
+ "slices"
"sync"
"hakurei.app/container/check"
@@ -39,6 +42,11 @@ func MustDecode(s string) (checksum Checksum) {
// deterministically but might not currently be available in memory or on the
// filesystem.
type Artifact interface {
+ // Kind returns the [Kind] of artifact. This is usually unique to the
+ // concrete type but two functionally identical implementations of
+ // [Artifact] is allowed to return the same [Kind] value.
+ Kind() Kind
+
// 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].
@@ -71,6 +79,39 @@ type File interface {
Artifact
}
+// Kind corresponds to the concrete type of [Artifact] and is used to create
+// identifier for an [Artifact] with dependencies.
+type Kind uint64
+
+const (
+ // KindHTTP is the kind of [Artifact] returned by [Cache.NewHTTP].
+ KindHTTP Kind = iota
+ KindTar
+)
+
+// Ident returns a deterministic identifier for the supplied params and
+// dependencies. The caller is responsible for ensuring params uniquely and
+// deterministically describes the current [Artifact].
+func (k Kind) Ident(params []byte, deps ...Artifact) ID {
+ type extIdent [len(ID{}) + wordSize]byte
+ identifiers := make([]extIdent, len(deps))
+ for i, a := range deps {
+ id := a.ID()
+ copy(identifiers[i][wordSize:], id[:])
+ binary.LittleEndian.PutUint64(identifiers[i][:], uint64(a.Kind()))
+ }
+ slices.SortFunc(identifiers, func(a, b extIdent) int { return bytes.Compare(a[:], b[:]) })
+ slices.Compact(identifiers)
+
+ h := sha512.New384()
+ h.Write(binary.LittleEndian.AppendUint64(nil, uint64(k)))
+ h.Write(params)
+ for _, e := range identifiers {
+ h.Write(e[:])
+ }
+ return ID(h.Sum(nil))
+}
+
const (
// dirIdentifier is the directory name appended to Cache.base for storing
// artifacts named after their [ID].
diff --git a/internal/pkg/pkg_test.go b/internal/pkg/pkg_test.go
index 79544430..1151a681 100644
--- a/internal/pkg/pkg_test.go
+++ b/internal/pkg/pkg_test.go
@@ -18,6 +18,46 @@ import (
"hakurei.app/internal/pkg"
)
+// A stubArtifact implements [Artifact] with hardcoded kind and identifier.
+type stubArtifact struct {
+ kind pkg.Kind
+ id pkg.ID
+}
+
+func (a stubArtifact) Kind() pkg.Kind { return a.kind }
+func (a stubArtifact) ID() pkg.ID { return a.id }
+func (a stubArtifact) Hash() (pkg.Checksum, error) { panic("unreachable") }
+func (a stubArtifact) Pathname() (*check.Absolute, error) { panic("unreachable") }
+
+func TestIdent(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ kind pkg.Kind
+ params []byte
+ deps []pkg.Artifact
+ want pkg.ID
+ }{
+ {"tar", pkg.KindTar, []byte{
+ 1, 0, 0, 0, 0, 0, 0, 0,
+ }, []pkg.Artifact{
+ stubArtifact{pkg.KindHTTP, pkg.ID{}},
+ }, pkg.MustDecode("HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY")},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ if got := tc.kind.Ident(tc.params, tc.deps...); got != tc.want {
+ t.Errorf("Ident: %s, want %s",
+ base64.URLEncoding.EncodeToString(got[:]),
+ base64.URLEncoding.EncodeToString(tc.want[:]))
+ }
+ })
+ }
+}
+
// cacheTestCase is a test case passed to checkWithCache where a new instance
// of [pkg.Cache] is prepared for the test case, and is validated and removed
// on test completion.