From 8ad990906555d843d1212c5eb8a1ac194031426c Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sat, 3 Jan 2026 21:24:50 +0900 Subject: [PATCH] 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 --- internal/pkg/net.go | 3 +++ internal/pkg/pkg.go | 41 ++++++++++++++++++++++++++++++++++++++++ internal/pkg/pkg_test.go | 40 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/internal/pkg/net.go b/internal/pkg/net.go index 16229b2..519a9b0 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 73b93e3..40bad6f 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 7954443..1151a68 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.