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 } // 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 }) } // Dependencies returns a slice containing the backing file. func (a *tarArtifact) Dependencies() []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.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 }