aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/pkg
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-06-04 19:43:50 +0900
committerOphestra <cat@gensokyo.uk>2026-06-04 19:46:36 +0900
commit8e8410ce3850a34d8b79431eb89ce4a7b9f89a60 (patch)
tree7de96628652c65d149c4e1d09306145a298844ad /internal/pkg
parent8fb6fdaa80e793928f80c45d058aa5aebf79e78b (diff)
internal/pkg: archive unpack artifact
This unpacks an internal/pkg archive stream used in the upcoming mirror service. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/pkg')
-rw-r--r--internal/pkg/archive.go114
-rw-r--r--internal/pkg/archive_test.go42
-rw-r--r--internal/pkg/ir_test.go2
-rw-r--r--internal/pkg/pkg.go2
4 files changed, 160 insertions, 0 deletions
diff --git a/internal/pkg/archive.go b/internal/pkg/archive.go
index 8b5398fa..902d2c31 100644
--- a/internal/pkg/archive.go
+++ b/internal/pkg/archive.go
@@ -277,3 +277,117 @@ func SumFS(buf *Checksum, fsys fs.FS, root string) error {
func SumDir(buf *Checksum, pathname *check.Absolute) error {
return SumFS(buf, os.DirFS(pathname.String()), ".")
}
+
+// archiveArtifact is an [Artifact] unpacking an archive supported by [Reader]
+// backed by a [FileArtifact].
+type archiveArtifact struct {
+ // Caller-supplied backing archive.
+ f Artifact
+}
+
+// NewArchive returns a new [Artifact] backed by the supplied [Artifact]. The
+// source [Artifact] must be a [FileArtifact] and produce a stream compatible
+// with [Reader].
+func NewArchive(a Artifact) Artifact {
+ return archiveArtifact{a}
+}
+
+// Kind returns the hardcoded [Kind] constant.
+func (archiveArtifact) Kind() Kind { return KindArchive }
+
+// Params is a noop.
+func (archiveArtifact) Params(*IContext) {}
+
+func init() {
+ register(KindArchive, func(r *IRReader) Artifact {
+ a := NewArchive(r.Next())
+ if _, ok := r.Finalise(); ok {
+ panic(ErrUnexpectedChecksum)
+ }
+ return a
+ })
+}
+
+// Dependencies returns a slice containing the backing file.
+func (a archiveArtifact) Dependencies() []Artifact {
+ return []Artifact{a.f}
+}
+
+// IsExclusive returns false: [Reader] is fully sequential.
+func (archiveArtifact) IsExclusive() bool { return false }
+
+// Cure cures the [Artifact], producing a directory located at work.
+func (a archiveArtifact) Cure(t *TContext) (err error) {
+ var r io.ReadCloser
+ if r, err = t.Open(a.f); err != nil {
+ return
+ }
+
+ defer func() {
+ closeErr := r.Close()
+ if err == nil {
+ err = closeErr
+ }
+ }()
+
+ if err = os.MkdirAll(t.GetWorkDir().String(), 0700); err != nil {
+ return
+ }
+ var root *os.Root
+ if root, err = os.OpenRoot(t.GetWorkDir().String()); err != nil {
+ return
+ }
+ defer func() {
+ closeErr := root.Close()
+ if err == nil {
+ err = closeErr
+ }
+ }()
+
+ var header *ArchiveHeader
+ ar := NewReader(r)
+ for header, err = ar.Next(); err == nil; header, err = ar.Next() {
+ if header.Mode.IsRegular() {
+ var f *os.File
+ if f, err = root.OpenFile(
+ header.Path,
+ os.O_CREATE|os.O_EXCL|os.O_WRONLY,
+ header.Mode.Perm(),
+ ); err != nil {
+ return
+ }
+ if _, err = io.Copy(f, ar); err != nil {
+ _ = f.Close()
+ return
+ } else if err = f.Close(); err != nil {
+ return
+ }
+ } else if header.Mode&fs.ModeSymlink != 0 {
+ var p []byte
+ if p, err = io.ReadAll(ar); err != nil {
+ return
+ }
+
+ if err = root.Symlink(
+ unsafe.String(unsafe.SliceData(p), len(p)),
+ header.Path,
+ ); err != nil {
+ return
+ }
+ } else if header.Mode.IsDir() {
+ if header.Path == "." {
+ continue
+ }
+
+ if err = root.Mkdir(header.Path, header.Mode.Perm()); err != nil {
+ return
+ }
+ } else {
+ return InvalidFileModeError(header.Mode)
+ }
+ }
+ if errors.Is(err, io.EOF) {
+ err = nil
+ }
+ return
+}
diff --git a/internal/pkg/archive_test.go b/internal/pkg/archive_test.go
index e04f3a66..29383eda 100644
--- a/internal/pkg/archive_test.go
+++ b/internal/pkg/archive_test.go
@@ -4,11 +4,13 @@ import (
"bytes"
"io"
"io/fs"
+ "maps"
"reflect"
"testing"
"testing/fstest"
"unsafe"
+ "hakurei.app/check"
"hakurei.app/internal/pkg"
)
@@ -168,6 +170,46 @@ var archiveTestdata = fstest.MapFS{
"block/uevent": {Mode: 0600},
}
+func TestArchiveArtifact(t *testing.T) {
+ t.Parallel()
+
+ want := maps.Clone(archiveTestdata)
+ want["."].Mode = fs.ModeDir | 0500
+
+ checkWithCache(t, []cacheTestCase{
+ {"unpack", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
+ var buf bytes.Buffer
+ if err := pkg.Write(archiveTestdata, ".", &buf); err != nil {
+ t.Fatal(err)
+ }
+
+ cureMany(t, c, []cureStep{
+ {"sample", pkg.NewArchive(
+ pkg.NewFile("", buf.Bytes()),
+ ), ignorePathname, expectsFS(want), nil},
+ })
+ }, expectsFS{
+ ".": {Mode: fs.ModeDir | 0700},
+
+ "checksum": {Mode: fs.ModeDir | 0700},
+ "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F": {Mode: fs.ModeDir | 0500},
+ "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/block": {Mode: fs.ModeDir | 0700},
+ "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/block/uevent": {Mode: 0600},
+ "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices": {Mode: fs.ModeDir | 0700},
+ "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/empty": {Mode: fs.ModeDir | 0700},
+ "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/sub": {Mode: fs.ModeDir | 0700},
+ "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
+ "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/uevent": {Mode: 0600, Data: []byte("add")},
+
+ "identifier": {Mode: fs.ModeDir | 0700},
+ "identifier/3oYyAbRJ_we7AgWo1BRcRcnxXFk3mAQ0Qui2nGQMi8GIJNJQtvUC6P2IeoA5mbjD": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F")},
+
+ "substitute": {Mode: fs.ModeDir | 0700},
+ "work": {Mode: fs.ModeDir | 0700},
+ }},
+ })
+}
+
func BenchmarkArchiveRead(b *testing.B) {
var buf bytes.Buffer
if err := pkg.Write(archiveTestdata, ".", &buf); err != nil {
diff --git a/internal/pkg/ir_test.go b/internal/pkg/ir_test.go
index 0cb4a6ef..ad6baa76 100644
--- a/internal/pkg/ir_test.go
+++ b/internal/pkg/ir_test.go
@@ -98,6 +98,8 @@ func TestIRRoundtrip(t *testing.T) {
{"file", pkg.NewFile("stub", []byte("stub"))},
{"decompress", pkg.NewDecompress(pkg.NewFile("", []byte{0}), pkg.Bzip2)},
+
+ {"archive", pkg.NewArchive(pkg.NewFile("", []byte{0}))},
}
testCasesCache := make([]cacheTestCase, len(testCases))
for i, tc := range testCases {
diff --git a/internal/pkg/pkg.go b/internal/pkg/pkg.go
index 09d91f73..b8a997d8 100644
--- a/internal/pkg/pkg.go
+++ b/internal/pkg/pkg.go
@@ -510,6 +510,8 @@ const (
KindFile
// KindDecompress is the kind of [Artifact] returned by [NewDecompress].
KindDecompress
+ // KindArchive is the kind of [Artifact] returned by [NewArchive].
+ KindArchive
// _kindEnd is the total number of kinds and does not denote a kind.
_kindEnd