diff options
| author | Ophestra <cat@gensokyo.uk> | 2026-01-04 01:08:59 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2026-01-04 01:34:30 +0900 |
| commit | d76b9d04b813a361fc8e226480e3b66ca0208d41 (patch) | |
| tree | 5489463bedf7ec016ffad12b2c2c228afff83dbc /internal/pkg/tar.go | |
| parent | fa93476896700f990e303cf3f60caa77f30df625 (diff) | |
internal/pkg: implement tar artifact
This is useful for unpacking tarballs downloaded from the internet.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/pkg/tar.go')
| -rw-r--r-- | internal/pkg/tar.go | 229 |
1 files changed, 229 insertions, 0 deletions
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 +} |
