aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-01-04 01:08:59 +0900
committerOphestra <cat@gensokyo.uk>2026-01-04 01:34:30 +0900
commitd76b9d04b813a361fc8e226480e3b66ca0208d41 (patch)
tree5489463bedf7ec016ffad12b2c2c228afff83dbc
parentfa93476896700f990e303cf3f60caa77f30df625 (diff)
internal/pkg: implement tar artifact
This is useful for unpacking tarballs downloaded from the internet. Signed-off-by: Ophestra <cat@gensokyo.uk>
-rw-r--r--internal/pkg/dir_test.go82
-rw-r--r--internal/pkg/pkg_test.go12
-rw-r--r--internal/pkg/tar.go229
-rw-r--r--internal/pkg/tar_test.go119
4 files changed, 435 insertions, 7 deletions
diff --git a/internal/pkg/dir_test.go b/internal/pkg/dir_test.go
index 6418b546..de2f1a10 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 6f4a256a..12b0bd9f 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 00000000..c5b3eb38
--- /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 00000000..a629c9ee
--- /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")},
+ })
+}