package pkg import ( "archive/tar" "bytes" "compress/gzip" "context" "encoding/binary" "errors" "io" "io/fs" "net/http" "os" "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 { // Caller-supplied backing tarball. f File // Compression on top of the tarball. compression uint64 } // NewTar returns a new [Artifact] backed by the supplied [File] and // compression method. func NewTar(f File, compression uint64) Artifact { return &tarArtifact{f: f, compression: compression} } // NewHTTPGetTar is abbreviation for NewHTTPGet passed to NewTar. func NewHTTPGetTar( ctx context.Context, hc *http.Client, url string, checksum Checksum, compression uint64, ) Artifact { return NewTar(NewHTTPGet(ctx, hc, url, checksum), compression) } // Kind returns the hardcoded [Kind] constant. func (a *tarArtifact) Kind() Kind { return KindTar } // Params returns compression encoded in little endian. func (a *tarArtifact) Params() []byte { return binary.LittleEndian.AppendUint64(nil, a.compression) } // Dependencies returns a slice containing the backing file. func (a *tarArtifact) Dependencies() []Artifact { return []Artifact{a.f} } // 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(work *check.Absolute, loadData CacheDataFunc) (err error) { var tr io.ReadCloser { var data []byte data, err = loadData(a.f) 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 case tar.TypeXGlobalHeader: // ignore 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 }