aboutsummaryrefslogtreecommitdiffhomepage
path: root/pkg/tar.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/tar.go')
-rw-r--r--pkg/tar.go226
1 files changed, 226 insertions, 0 deletions
diff --git a/pkg/tar.go b/pkg/tar.go
new file mode 100644
index 00000000..9a8c4b76
--- /dev/null
+++ b/pkg/tar.go
@@ -0,0 +1,226 @@
+package pkg
+
+import (
+ "archive/tar"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+)
+
+// A tarArtifact is an [Artifact] unpacking a tarball backed by a [FileArtifact].
+type tarArtifact struct {
+ // Caller-supplied backing tarball.
+ f Artifact
+}
+
+var _ CuresExempt = new(tarArtifact)
+
+// tarArtifactNamed embeds tarArtifact for a [fmt.Stringer] tarball.
+type tarArtifactNamed struct {
+ tarArtifact
+ // Copied from tarArtifact.f.
+ name string
+}
+
+var _ fmt.Stringer = new(tarArtifactNamed)
+
+// String returns the name of the underlying [Artifact] prefixed with unpack.
+func (a *tarArtifactNamed) String() string { return "unpack-" + a.name }
+
+// NewTar returns a new [Artifact] unpacking the tar stream produced by the
+// backing [Artifact]. The source [Artifact] must be a [FileArtifact].
+func NewTar(a Artifact) Artifact {
+ ta := tarArtifact{a}
+ if s, ok := a.(fmt.Stringer); ok {
+ if name := s.String(); name != "" {
+ return &tarArtifactNamed{ta, name}
+ }
+ }
+ return &ta
+}
+
+// Kind returns the hardcoded [Kind] constant.
+func (a *tarArtifact) Kind() Kind { return KindTar }
+
+// Params is a noop.
+func (a *tarArtifact) Params(*IContext) {}
+
+func init() {
+ register(KindTar, func(r *IRReader) Artifact {
+ a := NewTar(r.Next())
+ if _, ok := r.Finalise(); ok {
+ panic(ErrUnexpectedChecksum)
+ }
+ return a
+ })
+}
+
+// Inputs returns a slice containing the backing file.
+func (a *tarArtifact) Inputs() []Artifact {
+ return []Artifact{a.f}
+}
+
+// IsExclusive returns false: decompressor and tar reader are fully sequential.
+func (a *tarArtifact) IsExclusive() bool { return false }
+
+// A DisallowedTypeflagError describes a disallowed typeflag encountered while
+// unpacking a tarball.
+type DisallowedTypeflagError byte
+
+func (e DisallowedTypeflagError) Error() string {
+ return "disallowed typeflag '" + string(e) + "'"
+}
+
+// Cure cures the [Artifact], producing a directory located at work.
+func (a *tarArtifact) 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
+ }
+ }()
+
+ type dirTargetPerm struct {
+ path string
+ mode fs.FileMode
+ }
+ var madeDirectories []dirTargetPerm
+
+ if err = os.MkdirAll(t.GetTempDir().String(), 0700); err != nil {
+ return
+ }
+ var root *os.Root
+ if root, err = os.OpenRoot(t.GetTempDir().String()); err != nil {
+ return
+ }
+ defer func() {
+ closeErr := root.Close()
+ if err == nil {
+ err = closeErr
+ }
+ }()
+
+ var header *tar.Header
+ tr := tar.NewReader(r)
+ for header, err = tr.Next(); err == nil; header, err = tr.Next() {
+ typeflag := header.Typeflag
+ if typeflag == 0 {
+ if len(header.Name) > 0 && header.Name[len(header.Name)-1] == '/' {
+ typeflag = tar.TypeDir
+ } else {
+ typeflag = tar.TypeReg
+ }
+ }
+
+ if typeflag >= '0' && typeflag <= '9' && typeflag != tar.TypeDir {
+ if err = root.MkdirAll(filepath.Dir(header.Name), 0700); err != nil {
+ return
+ }
+ }
+
+ switch typeflag {
+ case tar.TypeReg:
+ var f *os.File
+ if f, err = root.OpenFile(
+ header.Name,
+ os.O_CREATE|os.O_EXCL|os.O_WRONLY,
+ header.FileInfo().Mode()&0500,
+ ); err != nil {
+ return
+ }
+ if _, err = io.Copy(f, tr); err != nil {
+ _ = f.Close()
+ return
+ } else if err = f.Close(); err != nil {
+ return
+ }
+ break
+
+ case tar.TypeLink:
+ if err = root.Link(
+ header.Linkname,
+ header.Name,
+ ); err != nil {
+ return
+ }
+ break
+
+ case tar.TypeSymlink:
+ if err = root.Symlink(
+ header.Linkname,
+ header.Name,
+ ); err != nil {
+ return
+ }
+ break
+
+ case tar.TypeDir:
+ madeDirectories = append(madeDirectories, dirTargetPerm{
+ path: header.Name,
+ mode: header.FileInfo().Mode(),
+ })
+ if err = root.MkdirAll(header.Name, 0700); err != nil {
+ return
+ }
+ break
+
+ case tar.TypeChar:
+ t.GetMessage().Verbosef(
+ "%sskipping character device %d, %d %q%s",
+ t.cache.sgrWarn, header.Devmajor, header.Devminor, header.Name, t.cache.sgrRes,
+ )
+ continue
+
+ case tar.TypeXGlobalHeader:
+ continue // ignore
+
+ default:
+ return DisallowedTypeflagError(typeflag)
+ }
+ }
+ if errors.Is(err, io.EOF) {
+ err = nil
+ }
+ if err == nil {
+ for _, e := range madeDirectories {
+ if err = root.Chmod(e.path, e.mode&0500); err != nil {
+ return
+ }
+ }
+ } else {
+ return
+ }
+
+ temp := t.GetTempDir()
+ if err = os.Chmod(temp.String(), 0700); err != nil {
+ return
+ }
+
+ var entries []os.DirEntry
+ if entries, err = os.ReadDir(temp.String()); err != nil {
+ return
+ }
+
+ if len(entries) == 1 && entries[0].IsDir() {
+ p := temp.Append(entries[0].Name())
+ if err = os.Chmod(p.String(), 0700); err != nil {
+ return
+ }
+ err = os.Rename(p.String(), t.GetWorkDir().String())
+ } else {
+ err = os.Rename(temp.String(), t.GetWorkDir().String())
+ }
+ return
+}
+
+// CuresExempt exempts the cheap [KindTar] implementation often at the end of a
+// [FileArtifact] pipeline.
+func (*tarArtifact) CuresExempt() {}