From d76b9d04b813a361fc8e226480e3b66ca0208d41 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sun, 4 Jan 2026 01:08:59 +0900 Subject: [PATCH] internal/pkg: implement tar artifact This is useful for unpacking tarballs downloaded from the internet. Signed-off-by: Ophestra --- internal/pkg/dir_test.go | 82 +++++++++++++- internal/pkg/pkg_test.go | 12 +- internal/pkg/tar.go | 229 +++++++++++++++++++++++++++++++++++++++ internal/pkg/tar_test.go | 119 ++++++++++++++++++++ 4 files changed, 435 insertions(+), 7 deletions(-) create mode 100644 internal/pkg/tar.go create mode 100644 internal/pkg/tar_test.go diff --git a/internal/pkg/dir_test.go b/internal/pkg/dir_test.go index 6418b54..de2f1a1 100644 --- a/internal/pkg/dir_test.go +++ b/internal/pkg/dir_test.go @@ -95,7 +95,7 @@ func TestFlatten(t *testing.T) { }, []pkg.FlatEntry{ {Mode: fs.ModeDir | 0700, Path: "."}, - {Mode: 0400, Path: "check", Data: []byte{0x0, 0x0}}, + {Mode: 0400, Path: "check", Data: []byte{0, 0}}, {Mode: fs.ModeDir | 0700, Path: "lib"}, {Mode: fs.ModeSymlink | 0777, Path: "lib/libedac.so", Data: []byte("/proc/nonexistent/libedac.so")}, @@ -127,11 +127,11 @@ func TestFlatten(t *testing.T) { "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/check": {Mode: 0400, Data: []byte{0, 0}}, "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib": {Mode: fs.ModeDir | 0700}, "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/pkgconfig": {Mode: fs.ModeDir | 0700}, - "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so": {Mode: 01000000777, Data: []byte("/proc/nonexistent/libedac.so")}, + "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")}, "identifier": {Mode: fs.ModeDir | 0700}, - "identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY": {Mode: 01000000777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, - "identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa": {Mode: 01000000777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, + "identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, + "identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, "work": {Mode: fs.ModeDir | 0700}, }, []pkg.FlatEntry{ @@ -150,6 +150,80 @@ func TestFlatten(t *testing.T) { {Mode: fs.ModeDir | 0700, Path: "work"}, }, pkg.MustDecode("8OP6YxJAdRrhV2WSBt1BPD7oC_n2Qh7JqUMyVMoGvjDX83bDqq2hgVMNcdiBH_64")}, + + {"sample tar step unpack", fstest.MapFS{ + ".": {Mode: fs.ModeDir | 0700}, + + "checksum": {Mode: fs.ModeDir | 0500}, + "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP": {Mode: fs.ModeDir | 0500}, + "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/check": {Mode: 0400, Data: []byte{0, 0}}, + "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib": {Mode: fs.ModeDir | 0500}, + "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/pkgconfig": {Mode: fs.ModeDir | 0500}, + "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")}, + + "identifier": {Mode: fs.ModeDir | 0500}, + "identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, + "identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, + + "work": {Mode: fs.ModeDir | 0500}, + }, []pkg.FlatEntry{ + {Mode: fs.ModeDir | 0700, Path: "."}, + + {Mode: fs.ModeDir | 0500, Path: "checksum"}, + {Mode: fs.ModeDir | 0500, Path: "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP"}, + {Mode: 0400, Path: "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/check", Data: []byte{0, 0}}, + {Mode: fs.ModeDir | 0500, Path: "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib"}, + {Mode: fs.ModeSymlink | 0777, Path: "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so", Data: []byte("/proc/nonexistent/libedac.so")}, + {Mode: fs.ModeDir | 0500, Path: "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/pkgconfig"}, + + {Mode: fs.ModeDir | 0500, Path: "identifier"}, + {Mode: fs.ModeSymlink | 0777, Path: "identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY", Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, + {Mode: fs.ModeSymlink | 0777, Path: "identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa", Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, + + {Mode: fs.ModeDir | 0500, Path: "work"}, + }, pkg.MustDecode("yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o")}, + + {"sample tar", fstest.MapFS{ + ".": {Mode: fs.ModeDir | 0700}, + + "checksum": {Mode: fs.ModeDir | 0700}, + "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o": {Mode: fs.ModeDir | 0500}, + "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/checksum": {Mode: fs.ModeDir | 0500}, + "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP": {Mode: fs.ModeDir | 0500}, + "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/check": {Mode: 0400, Data: []byte{0, 0}}, + "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib": {Mode: fs.ModeDir | 0500}, + "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")}, + "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/pkgconfig": {Mode: fs.ModeDir | 0500}, + "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/identifier": {Mode: fs.ModeDir | 0500}, + "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, + "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, + "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/work": {Mode: fs.ModeDir | 0500}, + + "identifier": {Mode: fs.ModeDir | 0700}, + "identifier/NXP3807i7T1WaRom4ycvaIL8BzBU0awC7eW2_baV9hVS_NpRQMsJApnh0CPNNO8y": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o")}, + + "work": {Mode: fs.ModeDir | 0700}, + }, []pkg.FlatEntry{ + {Mode: fs.ModeDir | 0700, Path: "."}, + + {Mode: fs.ModeDir | 0700, Path: "checksum"}, + {Mode: fs.ModeDir | 0500, Path: "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o"}, + {Mode: fs.ModeDir | 0500, Path: "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/checksum"}, + {Mode: fs.ModeDir | 0500, Path: "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP"}, + {Mode: 0400, Path: "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/check", Data: []byte{0, 0}}, + {Mode: fs.ModeDir | 0500, Path: "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib"}, + {Mode: fs.ModeSymlink | 0777, Path: "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so", Data: []byte("/proc/nonexistent/libedac.so")}, + {Mode: fs.ModeDir | 0500, Path: "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/pkgconfig"}, + {Mode: fs.ModeDir | 0500, Path: "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/identifier"}, + {Mode: fs.ModeSymlink | 0777, Path: "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY", Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, + {Mode: fs.ModeSymlink | 0777, Path: "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa", Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, + {Mode: fs.ModeDir | 0500, Path: "checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o/work"}, + + {Mode: fs.ModeDir | 0700, Path: "identifier"}, + {Mode: fs.ModeSymlink | 0777, Path: "identifier/NXP3807i7T1WaRom4ycvaIL8BzBU0awC7eW2_baV9hVS_NpRQMsJApnh0CPNNO8y", Data: []byte("../checksum/yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o")}, + + {Mode: fs.ModeDir | 0700, Path: "work"}, + }, pkg.MustDecode("p1HdTOQhIeWzpXdZ45xo00H9CFeXNIvazxOhBAfExlhFO64zt7TUbxoLJ2eAL5oc")}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/pkg/pkg_test.go b/internal/pkg/pkg_test.go index 6f4a256..12b0bd9 100644 --- a/internal/pkg/pkg_test.go +++ b/internal/pkg/pkg_test.go @@ -1,6 +1,7 @@ package pkg_test import ( + "archive/tar" "bytes" "crypto/sha512" "encoding/binary" @@ -40,7 +41,7 @@ func TestIdent(t *testing.T) { want pkg.ID }{ {"tar", pkg.KindTar, []byte{ - 1, 0, 0, 0, 0, 0, 0, 0, + pkg.TarGzip, 0, 0, 0, 0, 0, 0, 0, }, []pkg.Artifact{ stubArtifact{pkg.KindHTTP, pkg.ID{}}, }, pkg.MustDecode("HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY")}, @@ -74,6 +75,7 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + t.Helper() t.Parallel() base := check.MustAbs(t.TempDir()) @@ -307,7 +309,7 @@ func TestCache(t *testing.T) { {"directory", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { id := pkg.KindTar.Ident( - binary.LittleEndian.AppendUint64(nil, 1), + binary.LittleEndian.AppendUint64(nil, pkg.TarGzip), stubArtifact{pkg.KindHTTP, testdataChecksum}, ) makeSample := func(work *check.Absolute) error { @@ -371,7 +373,7 @@ func TestCache(t *testing.T) { // check exist id0 := pkg.KindTar.Ident( - binary.LittleEndian.AppendUint64(nil, 1), + binary.LittleEndian.AppendUint64(nil, pkg.TarGzip), stubArtifact{pkg.KindHTTP, pkg.ID{}}, ) wantPathname0 := base.Append( @@ -478,6 +480,10 @@ func TestErrors(t *testing.T) { {"ResponseStatusError", pkg.ResponseStatusError( http.StatusNotAcceptable, ), "the requested URL returned non-OK status: Not Acceptable"}, + + {"DisallowedTypeflagError", pkg.DisallowedTypeflagError( + tar.TypeChar, + ), "disallowed typeflag '3'"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/pkg/tar.go b/internal/pkg/tar.go new file mode 100644 index 0000000..c5b3eb3 --- /dev/null +++ b/internal/pkg/tar.go @@ -0,0 +1,229 @@ +package pkg + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "encoding/binary" + "errors" + "io" + "io/fs" + "net/http" + "os" + "sync" + + "hakurei.app/container/check" +) + +const ( + // TarUncompressed denotes an uncompressed tarball. + TarUncompressed = iota + // TarGzip denotes a tarball compressed via [gzip]. + TarGzip +) + +// A tarArtifact is an [Artifact] unpacking a tarball backed by a [File]. +type tarArtifact struct { + // Computed ahead of time from the checksum of the identifier of f appended + // with parameters of tarArtifact. + id ID + + // Caller-supplied backing tarball. + f File + // Compression on top of the tarball. + compression uint64 + + // Populated when submitting to or loading from [Cache]. + pathname *check.Absolute + // Checksum of cured directory. Valid if pathname is not nil. + checksum Checksum + + // Instance of [Cache] to submit the cured artifact to. + c *Cache + // Protects the Pathname critical section. + mu sync.Mutex +} + +// NewTar returns a new [Artifact] backed by the supplied [File] and +// compression method. +func (c *Cache) NewTar(f File, compression uint64) Artifact { + return &tarArtifact{id: KindTar.Ident( + binary.LittleEndian.AppendUint64(nil, compression), f, + ), f: f, compression: compression, c: c} +} + +// NewHTTPGetTar is abbreviation for NewHTTPGet passed to NewTar. +func (c *Cache) NewHTTPGetTar( + ctx context.Context, + hc *http.Client, + url string, + checksum Checksum, + compression uint64, +) (Artifact, error) { + f, err := c.NewHTTPGet(ctx, hc, url, checksum) + if err != nil { + return nil, err + } + return c.NewTar(f, compression), nil +} + +// Kind returns the hardcoded [Kind] constant. +func (a *tarArtifact) Kind() Kind { return KindTar } + +// ID returns the identifier prepared ahead of time. +func (a *tarArtifact) ID() ID { return a.id } + +// Hash cures the [Artifact] and returns its hash. +func (a *tarArtifact) Hash() (Checksum, error) { + _, err := a.Pathname() + return a.checksum, err +} + +// A DisallowedTypeflagError describes a disallowed typeflag encountered while +// unpacking a tarball. +type DisallowedTypeflagError byte + +func (e DisallowedTypeflagError) Error() string { + return "disallowed typeflag '" + string(e) + "'" +} + +// Pathname cures the [Artifact] and returns its pathname in the [Cache]. +func (a *tarArtifact) Pathname() (*check.Absolute, error) { + a.mu.Lock() + defer a.mu.Unlock() + + if a.pathname != nil { + return a.pathname, nil + } + + pathname, _, err := a.c.Store(a.id, func(work *check.Absolute) (err error) { + var tr io.ReadCloser + + { + var data []byte + data, err = a.f.Data() + if err != nil { + return + } + tr = io.NopCloser(bytes.NewReader(data)) + } + + defer func() { + closeErr := tr.Close() + if err == nil { + err = closeErr + } + }() + + switch a.compression { + case TarUncompressed: + break + + case TarGzip: + if tr, err = gzip.NewReader(tr); err != nil { + return + } + break + + default: + return os.ErrInvalid + } + + type dirTargetPerm struct { + path *check.Absolute + mode fs.FileMode + } + var madeDirectories []dirTargetPerm + + var header *tar.Header + r := tar.NewReader(tr) + for header, err = r.Next(); err == nil; header, err = r.Next() { + typeflag := header.Typeflag + for { + switch typeflag { + case 0: + if len(header.Name) > 0 && header.Name[len(header.Name)-1] == '/' { + typeflag = tar.TypeDir + } else { + typeflag = tar.TypeReg + } + continue + + case tar.TypeReg: + var f *os.File + if f, err = os.OpenFile( + work.Append(header.Name).String(), + os.O_CREATE|os.O_EXCL|os.O_WRONLY, + header.FileInfo().Mode()&0400, + ); err != nil { + return + } + if _, err = io.Copy(f, r); err != nil { + _ = f.Close() + return + } else if err = f.Close(); err != nil { + return + } + break + + case tar.TypeLink: + if err = os.Link( + header.Linkname, + work.Append(header.Name).String(), + ); err != nil { + return + } + break + + case tar.TypeSymlink: + if err = os.Symlink( + header.Linkname, + work.Append(header.Name).String(), + ); err != nil { + return + } + break + + case tar.TypeDir: + pathname := work.Append(header.Name) + madeDirectories = append(madeDirectories, dirTargetPerm{ + path: pathname, + mode: header.FileInfo().Mode(), + }) + if err = os.MkdirAll( + pathname.String(), + 0700, + ); err != nil { + return + } + break + + default: + return DisallowedTypeflagError(typeflag) + } + + break + } + } + if errors.Is(err, io.EOF) { + err = nil + } + + if err == nil { + for _, e := range madeDirectories { + if err = os.Chmod(e.path.String(), e.mode&0500); err != nil { + return + } + } + err = os.Chmod(work.String(), 0500) + } + return + }, &a.checksum, false) + if err != nil { + return nil, err + } + + a.pathname = pathname + return pathname, nil +} diff --git a/internal/pkg/tar_test.go b/internal/pkg/tar_test.go new file mode 100644 index 0000000..a629c9e --- /dev/null +++ b/internal/pkg/tar_test.go @@ -0,0 +1,119 @@ +package pkg_test + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto/sha512" + "io/fs" + "net/http" + "testing" + "testing/fstest" + + "hakurei.app/container/check" + "hakurei.app/internal/pkg" +) + +func TestTar(t *testing.T) { + t.Parallel() + + testdataFsys := fstest.MapFS{ + ".": {Mode: fs.ModeDir | 0700}, + + "checksum": {Mode: fs.ModeDir | 0700}, + "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP": {Mode: fs.ModeDir | 0700}, + "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/check": {Mode: 0400, Data: []byte{0, 0}}, + "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib": {Mode: fs.ModeDir | 0700}, + "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/pkgconfig": {Mode: fs.ModeDir | 0700}, + "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so": {Mode: 01000000777, Data: []byte("/proc/nonexistent/libedac.so")}, + + "identifier": {Mode: fs.ModeDir | 0700}, + "identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY": {Mode: 01000000777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, + "identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa": {Mode: 01000000777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")}, + + "work": {Mode: fs.ModeDir | 0700}, + } + + var testdata string + { + var buf bytes.Buffer + w := tar.NewWriter(&buf) + if err := w.AddFS(testdataFsys); err != nil { + t.Fatalf("AddFS: error = %v", err) + } + if err := w.Close(); err != nil { + t.Fatalf("Close: error = %v", err) + } + + var zbuf bytes.Buffer + gw := gzip.NewWriter(&zbuf) + if _, err := gw.Write(buf.Bytes()); err != nil { + t.Fatalf("Write: error = %v", err) + } + if err := gw.Close(); err != nil { + t.Fatalf("Close: error = %v", err) + } + testdata = zbuf.String() + } + + testdataChecksum := func() pkg.Checksum { + h := sha512.New384() + h.Write([]byte(testdata)) + return (pkg.Checksum)(h.Sum(nil)) + }() + + var transport http.Transport + client := http.Client{Transport: &transport} + transport.RegisterProtocol("file", http.NewFileTransportFS(fstest.MapFS{ + "testdata": {Data: []byte(testdata), Mode: 0400}, + })) + + checkWithCache(t, []cacheTestCase{ + {"http", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { + wantIdent := func() pkg.ID { + h := sha512.New384() + h.Write([]byte{byte(pkg.KindTar), 0, 0, 0, 0, 0, 0, 0}) + h.Write([]byte{pkg.TarGzip, 0, 0, 0, 0, 0, 0, 0}) + h.Write([]byte{byte(pkg.KindHTTP), 0, 0, 0, 0, 0, 0, 0}) + h.Write(testdataChecksum[:]) + return pkg.ID(h.Sum(nil)) + }() + + a, err := c.NewHTTPGetTar( + t.Context(), + &client, + "file:///testdata", + testdataChecksum, + pkg.TarGzip, + ) + + if err != nil { + t.Fatalf("NewHTTPGetTar: error = %v", err) + } else if id := a.ID(); id != wantIdent { + t.Fatalf("ID: %s, want %s", pkg.Encode(id), pkg.Encode(wantIdent)) + } + + var pathname *check.Absolute + wantPathname := base.Append( + "identifier", + pkg.Encode(wantIdent), + ) + if pathname, err = a.Pathname(); err != nil { + t.Fatalf("Pathname: error = %v", err) + } else if !pathname.Is(wantPathname) { + t.Fatalf("Pathname: %q, want %q", pathname, wantPathname) + } + + var checksum pkg.Checksum + wantChecksum := pkg.MustDecode("yJlSb2A3jxaMLuKqwp1GwHOguAHddS9MjygF9ICEeegKfRvgLPdPmNh8mva47f8o") + if checksum, err = a.Hash(); err != nil { + t.Fatalf("Hash: error = %v", err) + } else if checksum != wantChecksum { + t.Fatalf("Hash: %v", &pkg.ChecksumMismatchError{ + Got: checksum, + Want: wantChecksum, + }) + } + }, pkg.MustDecode("p1HdTOQhIeWzpXdZ45xo00H9CFeXNIvazxOhBAfExlhFO64zt7TUbxoLJ2eAL5oc")}, + }) +}