Skip to content
quarry Artifact [cd0f8ec2fe]
Login

Artifact [cd0f8ec2fe]

Artifact cd0f8ec2fec3d2f906ee89a13fc655fdf3f9509017eefa3e1ac2b3a6757f84af:


package runner

import (
	"archive/tar"
	"compress/gzip"
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
)

// ArchiveCheckout fetches a check-in's tree as the forge's own archive
// (GET <repo>/archive/<checkin>, a gzip tarball) and unpacks it into a
// fresh directory. It needs nothing on the machine but this program:
// no zit, no clone, no history. A task that needs a real repository
// uses ZitCheckout instead.
type ArchiveCheckout struct {
	URL string // the repository's URL on the forge
	// User and Password, for a private repository: an account that may
	// read it and that account's sync token.
	User, Password string
	Client         *http.Client
}

// Checkout replaces dir with checkin's tree. The tree is unpacked
// beside dir and swapped in only once it is whole, so a failed fetch
// never leaves half of one check-in over the rest of another.
func (a ArchiveCheckout) Checkout(ctx context.Context, dir, checkin string) error {
	req, err := http.NewRequestWithContext(ctx, "GET", strings.TrimSuffix(a.URL, "/")+"/archive/"+url.PathEscape(checkin), nil)
	if err != nil {
		return err
	}
	if a.User != "" {
		req.SetBasicAuth(a.User, a.Password)
	}
	c := a.Client
	if c == nil {
		c = http.DefaultClient
	}
	resp, err := c.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(io.LimitReader(resp.Body, 256))
		return fmt.Errorf("archive of %.12s: %d %s", checkin, resp.StatusCode, strings.TrimSpace(string(msg)))
	}
	tmp := dir + ".new"
	os.RemoveAll(tmp)
	if err := os.MkdirAll(tmp, 0o755); err != nil {
		return err
	}
	if err := untar(resp.Body, tmp); err != nil {
		os.RemoveAll(tmp)
		return fmt.Errorf("archive of %.12s: %w", checkin, err)
	}
	if err := os.RemoveAll(dir); err != nil {
		return err
	}
	return os.Rename(tmp, dir)
}

// untar unpacks a gzip tarball into root, dropping the archive's single
// top-level directory (the forge names it <repo>-<checkin>/). Every
// path must land inside root: an absolute name, a "..", or a link that
// points out of the tree is refused rather than skipped, because a
// tree with a file silently missing is a test result nobody can trust.
func untar(r io.Reader, root string) error {
	gz, err := gzip.NewReader(r)
	if err != nil {
		return err
	}
	defer gz.Close()
	tr := tar.NewReader(gz)
	for {
		h, err := tr.Next()
		if errors.Is(err, io.EOF) {
			return nil
		}
		if err != nil {
			return err
		}
		name := strings.TrimPrefix(h.Name, "./")
		_, rel, ok := strings.Cut(name, "/")
		if !ok || rel == "" || strings.Trim(rel, "/") == "" {
			continue // the top-level directory itself
		}
		rel = strings.TrimSuffix(rel, "/")
		if !filepath.IsLocal(filepath.FromSlash(rel)) {
			return fmt.Errorf("refusing %q: it names a path outside the tree", h.Name)
		}
		target := filepath.Join(root, filepath.FromSlash(rel))
		switch h.Typeflag {
		case tar.TypeDir:
			if err := os.MkdirAll(target, 0o755); err != nil {
				return err
			}
		case tar.TypeReg:
			if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
				return err
			}
			mode := os.FileMode(0o644)
			if h.FileInfo().Mode().Perm()&0o111 != 0 {
				mode = 0o755
			}
			f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
			if err != nil {
				return err
			}
			if _, err := io.Copy(f, tr); err != nil {
				f.Close()
				return err
			}
			if err := f.Close(); err != nil {
				return err
			}
		case tar.TypeSymlink:
			link := filepath.FromSlash(h.Linkname)
			if filepath.IsAbs(link) || !filepath.IsLocal(filepath.Join(filepath.Dir(filepath.FromSlash(rel)), link)) {
				return fmt.Errorf("refusing link %q -> %q: it points outside the tree", h.Name, h.Linkname)
			}
			if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
				return err
			}
			if err := os.Symlink(link, target); err != nil {
				return err
			}
		}
	}
}