Skip to content
quarry Artifact [203e29a54c]
Login

Artifact [203e29a54c]

Artifact 203e29a54c4aaa20b516cab654ec54892d5e4e0753bfc43f6215da556066e933:


package zitcli

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"strconv"
	"strings"
)

// ArtifactHead returns the FIRST n bytes of one stored artifact.
//
// ArtifactRaw exists for the case where a reader is being handed the
// whole file. This exists for the case where something only needs to
// know what the file IS — which magic number it starts with, whether it
// is text, what its first lines say — and paying a whole 32 MiB read
// through a subprocess for that answer is the difference between a page
// that renders and a page that does not.
//
// The child is cut off rather than drained: once n bytes are in hand
// the pipe is closed and the process killed, so a large blob costs the
// prefix and not the file. A blob shorter than n arrives whole and its
// own exit is waited for, which is what keeps a missing artifact
// distinguishable from a truncated one.
func (c *Client) ArtifactHead(ctx context.Context, hash string, n int) ([]byte, error) {
	return c.headOutput(ctx, n, nil, "artifact", hash, "--raw", "-R", c.repo)
}

// ArtifactSize reports one artifact's byte count WITHOUT reading it.
// zit's summary names the size on its third line; a manifest's summary
// then runs to one line per file, so this stops at the line it came
// for the same way ArtifactHead stops at its bound.
func (c *Client) ArtifactSize(ctx context.Context, hash string) (int, error) {
	// Enough for the hash, the type and the size. A summary whose size
	// line is past this is a zit contract change, and the error below
	// says so rather than answering zero.
	const summaryPrefix = 4 << 10
	// Stop at the line this came for rather than at the byte bound: the
	// size is on the third line of every summary, and a file blob's
	// summary is four lines that the bound alone would never cut short.
	out, err := c.headOutput(ctx, summaryPrefix, sizeLineSeen,
		"artifact", hash, "-R", c.repo)
	if err != nil {
		return 0, err
	}
	for _, line := range strings.Split(string(out), "\n") {
		rest, ok := strings.CutPrefix(strings.TrimSpace(line), "size:")
		if !ok {
			continue
		}
		n, err := strconv.Atoi(strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(rest), "bytes")))
		if err != nil {
			return 0, fmt.Errorf("zitcli: artifact %s: unreadable size line %q", hash, line)
		}
		return n, nil
	}
	return 0, fmt.Errorf("zitcli: artifact %s: summary carried no size line", hash)
}

// headOutput runs a zit command and keeps at most n bytes of what it
// writes, killing it once that much has arrived.
//
// The kill is conditional on the bound actually being reached. Killing
// unconditionally would make every exit status "signal: killed", and
// the exit status is how a missing artifact is told from an empty one.
func (c *Client) headOutput(ctx context.Context, n int, stop func([]byte) bool, args ...string) ([]byte, error) {
	cmd, done := c.command(ctx, args...)
	defer done()
	var stderr bytes.Buffer
	cmd.Stderr = &stderr
	pipe, err := cmd.StdoutPipe()
	if err != nil {
		return nil, c.storeFault(err, args[0]+" "+args[1], "")
	}
	if err := cmd.Start(); err != nil {
		return nil, c.storeFault(err, args[0]+" "+args[1], stderr.String())
	}
	buf, rerr := readUntil(pipe, n, stop)
	// "Enough" is either the bound or the caller's own stopping point.
	full := len(buf) >= n || (stop != nil && stop(buf))
	if full {
		pipe.Close()
		if cmd.Process != nil {
			_ = cmd.Process.Kill()
		}
	}
	werr := cmd.Wait()
	if full {
		// The child was killed on purpose, so its status says nothing.
		// The bytes are what was asked for.
		return buf, nil
	}
	if exitCode(werr) == 1 {
		return nil, fmt.Errorf("zitcli: artifact %s: %w: %s",
			args[1], ErrNoEntry, firstLine(stderr.String()))
	}
	if werr != nil {
		return nil, c.storeFault(werr, args[0]+" "+args[1], stderr.String())
	}
	if rerr != nil {
		return nil, c.storeFault(rerr, args[0]+" "+args[1], stderr.String())
	}
	return buf, nil
}

// readUntil reads at most n bytes, checking stop after each chunk so a
// caller that only wants one line does not wait for a child that has
// more to say.
func readUntil(r io.Reader, n int, stop func([]byte) bool) ([]byte, error) {
	buf := make([]byte, 0, min(n, 32<<10))
	chunk := make([]byte, 4<<10)
	for len(buf) < n {
		if stop != nil && stop(buf) {
			return buf, nil
		}
		want := min(len(chunk), n-len(buf))
		got, err := r.Read(chunk[:want])
		buf = append(buf, chunk[:got]...)
		if err != nil {
			if err == io.EOF {
				return buf, nil
			}
			return buf, err
		}
	}
	return buf, nil
}

// sizeLineSeen reports that a summary's size line has arrived whole —
// the line, plus the newline that proves it is not still being written.
func sizeLineSeen(buf []byte) bool {
	i := bytes.Index(buf, []byte("size:"))
	return i >= 0 && bytes.IndexByte(buf[i:], '\n') >= 0
}