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