aboutsummaryrefslogtreecommitdiff
package packfile

import (
	"errors"
	"fmt"

	"lindenii.org/go/furgit/object/id"
)

// ErrMalformedEntryHeader reports that
// a packfile entry header is truncated, overlong,
// has an unsupported entry type,
// or declares a size that overflows uint64.
var ErrMalformedEntryHeader = errors.New("internal/format/packfile: malformed entry header")

// MaxTypeSizeLen is the maximum encoded length
// of the type/size prefix of an entry header.
// Every uint64 size is encodable within this bound,
// and [ParseEntryHeader] rejects longer prefixes.
const MaxTypeSizeLen = 10

// MaxEntryHeaderLen returns the maximum encoded length
// of a full entry header
// for packs whose object IDs are hashSize bytes.
//
// Callers parsing from a stream may buffer
// MaxEntryHeaderLen bytes
// (or fewer if the pack data ends sooner)
// and parse with [ParseEntryHeader];
// no valid entry header is longer.
func MaxEntryHeaderLen(hashSize int) int {
	return MaxTypeSizeLen + max(hashSize, MaxOfsDeltaDistanceLen)
}

// EntryHeader is one parsed packfile entry header:
// everything from the start of the entry
// up to its zlib payload.
type EntryHeader struct {
	// Type is the packfile entry type.
	Type EntryType

	// Size is the declared inflated size
	// of the entry's payload.
	// For delta entries this is the delta size,
	// not the reconstructed object size.
	Size uint64

	// HeaderLen is the number of bytes
	// the header occupies in the pack;
	// the zlib payload begins HeaderLen bytes
	// after the start of the entry.
	HeaderLen int

	// RefBase holds the base object ID
	// for ref-delta entries.
	// Only the first hashSize bytes are meaningful.
	RefBase [id.MaxObjectIDSize]byte

	// OfsDistance is the backward distance
	// from the start of this entry
	// to the start of the base entry,
	// for ofs-delta entries.
	OfsDistance uint64
}

// ParseEntryHeader parses one packfile entry header
// from the beginning of data.
//
// hashSize must be the object ID size
// of the pack's object format;
// ParseEntryHeader panics on implausible hash sizes.
//
// data need not contain the whole entry;
// [MaxEntryHeaderLen] bytes always suffice.
// Headers of types [EntryTypeInvalid] and [EntryTypeFuture]
// are rejected as malformed.
func ParseEntryHeader(data []byte, hashSize int) (EntryHeader, error) {
	var zero EntryHeader

	if hashSize <= 0 || hashSize > id.MaxObjectIDSize {
		panic("internal/format/packfile: invalid hash size")
	}

	if len(data) == 0 {
		return zero, fmt.Errorf("%w: truncated type/size prefix", ErrMalformedEntryHeader)
	}

	first := data[0]
	header := EntryHeader{
		Type:      EntryType((first >> 4) & 0x07),
		Size:      uint64(first & 0x0f),
		HeaderLen: 1,
	}

	shift := uint(4)

	b := first
	for b&0x80 != 0 {
		if header.HeaderLen >= MaxTypeSizeLen {
			return zero, fmt.Errorf("%w: overlong type/size prefix", ErrMalformedEntryHeader)
		}

		if header.HeaderLen >= len(data) {
			return zero, fmt.Errorf("%w: truncated type/size prefix", ErrMalformedEntryHeader)
		}

		b = data[header.HeaderLen]
		header.HeaderLen++

		group := uint64(b & 0x7f)
		if group<<shift>>shift != group {
			return zero, fmt.Errorf("%w: size overflows uint64", ErrMalformedEntryHeader)
		}

		header.Size |= group << shift
		shift += 7
	}

	switch header.Type {
	case EntryTypeCommit, EntryTypeTree, EntryTypeBlob, EntryTypeTag:
		// Base entries have nothing between the type/size prefix and the payload.
	case EntryTypeRefDelta:
		end := header.HeaderLen + hashSize
		if end > len(data) {
			return zero, fmt.Errorf("%w: truncated ref-delta base ID", ErrMalformedEntryHeader)
		}

		copy(header.RefBase[:], data[header.HeaderLen:end])
		header.HeaderLen = end
	case EntryTypeOfsDelta:
		dist, consumed, err := ParseOfsDeltaDistance(data[header.HeaderLen:])
		if err != nil {
			return zero, fmt.Errorf("%w: %w", ErrMalformedEntryHeader, err)
		}

		header.OfsDistance = dist
		header.HeaderLen += consumed
	case EntryTypeInvalid, EntryTypeFuture:
		return zero, fmt.Errorf("%w: unsupported entry type", ErrMalformedEntryHeader)
	default:
		return zero, fmt.Errorf("%w: unsupported entry type", ErrMalformedEntryHeader)
	}

	return header, nil
}

// AppendTypeSize appends the type/size prefix encoding
// of an entry header to dst.
//
// entryType must be a valid on-disk entry type;
// [EntryTypeInvalid] and [EntryTypeFuture] and
// values that do not fit in three bits
// produce garbage encodings.
func AppendTypeSize(dst []byte, entryType EntryType, size uint64) []byte {
	b := byte(entryType)<<4 | byte(size&0x0f)
	size >>= 4

	for size != 0 {
		dst = append(dst, b|0x80)
		b = byte(size & 0x7f)
		size >>= 7
	}

	return append(dst, b)
}