aboutsummaryrefslogtreecommitdiffhomepage
path: root/pkg/archive.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/archive.go')
-rw-r--r--pkg/archive.go411
1 files changed, 411 insertions, 0 deletions
diff --git a/pkg/archive.go b/pkg/archive.go
new file mode 100644
index 00000000..ea4017d9
--- /dev/null
+++ b/pkg/archive.go
@@ -0,0 +1,411 @@
+package pkg
+
+import (
+ "crypto/sha512"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "unsafe"
+
+ "hakurei.app/check"
+)
+
+/*
+| mode uint32 | path_sz uint32 |
+| data_sz uint64 |
+| path string |
+| data []byte |
+*/
+
+// An ArchiveHeader represents a single header in an archive.
+type ArchiveHeader struct {
+ Mode fs.FileMode // file mode bits
+ Path string // pathname of the file
+ Size uint64 // size of data segment
+}
+
+// Writer implements sequential writing of an archive. [Writer.WriteHeader]
+// begins a new file with the provided [ArchiveHeader], and then Writer can be
+// treated as an [io.Writer] to supply that file's data.
+//
+// It is the caller's responsibility to write entries in lexical order.
+type Writer struct {
+ // Underlying writer.
+ w io.Writer
+ // Current header.
+ h ArchiveHeader
+ // Fixed-size header segment.
+ buf [wordSize * 2]byte
+ // Current position in data segment.
+ n uint64
+}
+
+// NewWriter returns the address of a new [Writer] writing to w.
+func NewWriter(w io.Writer) *Writer { return &Writer{w: w} }
+
+var zero [wordSize]byte
+
+// padSize returns the padding size for aligning sz.
+func padSize[T int | uint64](sz T) T {
+ return (wordSize - (sz)%wordSize) % wordSize
+}
+
+// flush concludes writing to the current file and writes padding.
+func (aw *Writer) flush() error {
+ if aw.h.Size > aw.n {
+ return fmt.Errorf("missed writing %d bytes", aw.h.Size-aw.n)
+ } else if aw.h.Size < aw.n {
+ return fmt.Errorf("wrote %d bytes beyond end of file", aw.n-aw.h.Size)
+ }
+
+ if psz := padSize(aw.h.Size); psz != 0 {
+ if _, err := aw.w.Write(zero[:psz]); err != nil {
+ return err
+ }
+ }
+
+ aw.n = 0
+ return nil
+}
+
+// WriteHeader writes h and begins accepting its corresponding file.
+func (aw *Writer) WriteHeader(h *ArchiveHeader) error {
+ if err := aw.flush(); err != nil {
+ return err
+ }
+
+ aw.h = *h
+ binary.LittleEndian.PutUint32(aw.buf[:], uint32(aw.h.Mode))
+ binary.LittleEndian.PutUint32(aw.buf[wordSize/2:], uint32(len(aw.h.Path)))
+ binary.LittleEndian.PutUint64(aw.buf[wordSize:], aw.h.Size)
+ if _, err := aw.w.Write(aw.buf[:]); err != nil {
+ return err
+ } else if _, err = aw.w.Write(
+ unsafe.Slice(unsafe.StringData(aw.h.Path), len(aw.h.Path)),
+ ); err != nil {
+ return err
+ } else if psz := padSize(len(aw.h.Path)); psz != 0 {
+ if _, err = aw.w.Write(zero[:psz]); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Write writes p to the underlying writer and records the new position. Invalid
+// positions are reported by WriteHeader and Close.
+func (aw *Writer) Write(p []byte) (n int, err error) {
+ n, err = aw.w.Write(p)
+ aw.n += uint64(n)
+ return
+}
+
+// Close concludes writing to the archive stream.
+func (aw *Writer) Close() (err error) {
+ err = aw.flush()
+ aw.w = nil
+ return
+}
+
+// ErrInsecurePath is returned by [FlatEntry.Decode] if validation is requested
+// and a nonlocal path is encountered in the stream.
+var ErrInsecurePath = errors.New("insecure file path")
+
+// Reader implements sequential reading of an archive. [Reader.Next] advances to
+// the next file in the archive (including the first), and then Reader can be
+// treated as an [io.Reader] to access the file's data.
+type Reader struct {
+ // Underlying reader.
+ r io.Reader
+ // Fixed-size header segment.
+ buf [wordSize * 2]byte
+ // Remaining bytes in current data segment.
+ n, pad uint64
+}
+
+// NewReader returns the address of a new [Reader] reading from r.
+func NewReader(r io.Reader) *Reader { return &Reader{r: r} }
+
+// Next advances ar to the next entry. Remaining bytes of the current data
+// segment are discarded. Advancing beyond the final entry returns [io.EOF].
+func (ar *Reader) Next() (*ArchiveHeader, error) {
+ if dsz := int64(ar.n + ar.pad); dsz > 0 {
+ if n, err := io.CopyN(io.Discard, ar.r, dsz); err != nil {
+ if errors.Is(err, io.EOF) && n != dsz {
+ err = io.ErrUnexpectedEOF
+ }
+ return nil, err
+ }
+ }
+
+ if _, err := io.ReadFull(ar.r, ar.buf[:]); err != nil {
+ return nil, err
+ }
+
+ h := ArchiveHeader{
+ Mode: fs.FileMode(binary.LittleEndian.Uint32(ar.buf[:])),
+ Size: binary.LittleEndian.Uint64(ar.buf[wordSize:]),
+ }
+ pathSize := int(binary.LittleEndian.Uint32(ar.buf[wordSize/2:]))
+ pPathSize := alignSize(pathSize)
+
+ buf := make([]byte, pPathSize)
+ if _, err := io.ReadFull(ar.r, buf); err != nil {
+ if errors.Is(err, io.EOF) {
+ err = io.ErrUnexpectedEOF
+ }
+ return nil, err
+ }
+
+ h.Path = unsafe.String(unsafe.SliceData(buf), pathSize)
+ if !filepath.IsLocal(h.Path) {
+ return &h, ErrInsecurePath
+ }
+
+ ar.n = h.Size
+ ar.pad = padSize(h.Size)
+ return &h, nil
+}
+
+// Read implements [io.Reader] for the data segment of the current entry.
+func (ar *Reader) Read(p []byte) (n int, err error) {
+ if uint64(len(p)) > ar.n {
+ p = p[:ar.n]
+ }
+
+ if len(p) > 0 {
+ n, err = ar.r.Read(p)
+ ar.n -= uint64(n)
+ }
+
+ switch err {
+ case io.EOF:
+ if ar.n > 0 {
+ return n, io.ErrUnexpectedEOF
+ }
+
+ case nil:
+ if ar.n == 0 {
+ return n, io.EOF
+ }
+ }
+ return
+}
+
+// Write writes a deterministic representation of the contents of fsys to w.
+// The resulting data can be hashed to produce a deterministic checksum for the
+// directory.
+func Write(fsys fs.FS, root string, w io.Writer) error {
+ aw := NewWriter(w)
+ if err := fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+
+ var fi fs.FileInfo
+ fi, err = d.Info()
+ if err != nil {
+ return err
+ }
+
+ h := ArchiveHeader{
+ Path: path,
+ Mode: fi.Mode(),
+ }
+ if h.Mode.IsRegular() {
+ h.Size = uint64(fi.Size())
+ if err = aw.WriteHeader(&h); err != nil {
+ return err
+ }
+
+ var r fs.File
+ r, err = fsys.Open(path)
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(aw, r)
+ if _err := r.Close(); err == nil {
+ err = _err
+ }
+ return err
+ } else if h.Mode&fs.ModeSymlink != 0 {
+ var newpath string
+ if newpath, err = fs.ReadLink(fsys, path); err != nil {
+ return err
+ }
+
+ h.Size = uint64(len(newpath))
+ if err = aw.WriteHeader(&h); err != nil {
+ return err
+ }
+
+ _, err = aw.Write(unsafe.Slice(unsafe.StringData(newpath), len(newpath)))
+ return err
+ } else if !h.Mode.IsDir() {
+ return InvalidFileModeError(h.Mode)
+ }
+ return aw.WriteHeader(&h)
+ }); err != nil {
+ return err
+ }
+ return aw.Close()
+}
+
+// SumFS saves checksum of the archive of fsys to the value pointed to by buf.
+func SumFS(buf *Checksum, fsys fs.FS, root string) error {
+ h := sha512.New384()
+ if err := Write(fsys, root, h); err != nil {
+ return err
+ }
+ h.Sum(buf[:0])
+ return nil
+}
+
+// SumDir saves checksum of the archive of directory at pathname to the value
+// pointed to by buf.
+func SumDir(buf *Checksum, pathname *check.Absolute) error {
+ return SumFS(buf, os.DirFS(pathname.String()), ".")
+}
+
+// archiveArtifact is an [Artifact] unpacking an archive supported by [Reader]
+// backed by a [FileArtifact].
+type archiveArtifact struct {
+ // Caller-supplied backing archive.
+ f Artifact
+}
+
+var _ CuresExempt = archiveArtifact{}
+
+// NewArchive returns a new [Artifact] backed by the supplied [Artifact]. The
+// source [Artifact] must be a [FileArtifact] and produce a stream compatible
+// with [Reader].
+func NewArchive(a Artifact) Artifact {
+ return archiveArtifact{a}
+}
+
+// Kind returns the hardcoded [Kind] constant.
+func (archiveArtifact) Kind() Kind { return KindArchive }
+
+// Params is a noop.
+func (archiveArtifact) Params(*IContext) {}
+
+func init() {
+ register(KindArchive, func(r *IRReader) Artifact {
+ a := NewArchive(r.Next())
+ if _, ok := r.Finalise(); ok {
+ panic(ErrUnexpectedChecksum)
+ }
+ return a
+ })
+}
+
+// Inputs returns a slice containing the backing file.
+func (a archiveArtifact) Inputs() []Artifact {
+ return []Artifact{a.f}
+}
+
+// IsExclusive returns false: [Reader] is fully sequential.
+func (archiveArtifact) IsExclusive() bool { return false }
+
+// Cure cures the [Artifact], producing a directory located at work.
+func (a archiveArtifact) 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.GetWorkDir().String(), 0700); err != nil {
+ return
+ }
+ var root *os.Root
+ if root, err = os.OpenRoot(t.GetWorkDir().String()); err != nil {
+ return
+ }
+ defer func() {
+ closeErr := root.Close()
+ if err == nil {
+ err = closeErr
+ }
+ }()
+
+ var header *ArchiveHeader
+ ar := NewReader(r)
+ for header, err = ar.Next(); err == nil; header, err = ar.Next() {
+ if header.Mode.IsRegular() {
+ var f *os.File
+ if f, err = root.OpenFile(
+ header.Path,
+ os.O_CREATE|os.O_EXCL|os.O_WRONLY,
+ header.Mode.Perm(),
+ ); err != nil {
+ return
+ }
+ if _, err = io.Copy(f, ar); err != nil {
+ _ = f.Close()
+ return
+ } else if err = f.Close(); err != nil {
+ return
+ }
+ } else if header.Mode&fs.ModeSymlink != 0 {
+ var p []byte
+ if p, err = io.ReadAll(ar); err != nil {
+ return
+ }
+
+ if err = root.Symlink(
+ unsafe.String(unsafe.SliceData(p), len(p)),
+ header.Path,
+ ); err != nil {
+ return
+ }
+ } else if header.Mode.IsDir() {
+ if header.Path == "." {
+ continue
+ }
+
+ madeDirectories = append(madeDirectories, dirTargetPerm{
+ path: header.Path,
+ mode: header.Mode,
+ })
+ if err = root.Mkdir(header.Path, 0700); err != nil {
+ return
+ }
+ } else {
+ return InvalidFileModeError(header.Mode)
+ }
+ }
+ if errors.Is(err, io.EOF) {
+ err = nil
+ }
+ if err == nil {
+ for _, e := range madeDirectories {
+ if err = root.Chmod(e.path, e.mode.Perm()); err != nil {
+ return
+ }
+ }
+ } else {
+ return
+ }
+ return
+}
+
+// CuresExempt exempts the cheap [KindArchive] implementation often found at
+// the end of a [FileArtifact] pipeline.
+func (archiveArtifact) CuresExempt() {}