aboutsummaryrefslogtreecommitdiff
package packrev

import (
	"encoding/binary"
	"errors"
	"fmt"

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

// ErrMalformedReverseIndex reports that
// a pack reverse index is truncated,
// has a bad signature, version, or hash function,
// or contains invalid index positions.
var ErrMalformedReverseIndex = errors.New("internal/format/packrev: malformed pack reverse index")

const (
	signature = 0x52494458 // "RIDX"
	version   = 1

	headerLen = 12
)

// hashFunctionID returns the on-disk hash function identifier
// for one object format.
func hashFunctionID(objectFormat id.ObjectFormat) (uint32, error) {
	switch objectFormat {
	case id.ObjectFormatSHA1:
		return 1, nil
	case id.ObjectFormatSHA256:
		return 2, nil
	case id.ObjectFormatUnknown:
	}

	return 0, id.ErrInvalidObjectFormat
}

// Packrev is a parsed pack reverse index view over borrowed bytes.
//
// Labels: Deps-Borrowed, Life-Parent, MT-Safe.
type Packrev struct {
	// data is the entire pack reverse index payload.
	data []byte
	// hashSize is the object ID size of the object format.
	hashSize int
	// numObjects is the number of index position entries.
	numObjects int
}

// Parse parses a pack reverse index from data.
func Parse(data []byte, objectFormat id.ObjectFormat) (Packrev, error) {
	var zero Packrev

	wantHashID, err := hashFunctionID(objectFormat)
	if err != nil {
		return zero, err
	}

	hashSize := objectFormat.Size()

	if len(data) < headerLen+2*hashSize {
		return zero, fmt.Errorf("%w: truncated", ErrMalformedReverseIndex)
	}

	if binary.BigEndian.Uint32(data) != signature {
		return zero, fmt.Errorf("%w: bad signature", ErrMalformedReverseIndex)
	}

	if binary.BigEndian.Uint32(data[4:]) != version {
		return zero, fmt.Errorf("%w: unsupported version", ErrMalformedReverseIndex)
	}

	if binary.BigEndian.Uint32(data[8:]) != wantHashID {
		return zero, fmt.Errorf("%w: hash function mismatch", ErrMalformedReverseIndex)
	}

	positionBytes := len(data) - headerLen - 2*hashSize
	if positionBytes%4 != 0 {
		return zero, fmt.Errorf("%w: position table size not a 32-bit multiple", ErrMalformedReverseIndex)
	}

	return Packrev{
		data:       data,
		hashSize:   hashSize,
		numObjects: positionBytes / 4,
	}, nil
}

// NumObjects returns the number of index position entries.
func (rev *Packrev) NumObjects() int {
	return rev.numObjects
}

// PackHash returns the pack hash recorded in the trailer.
//
// Labels: Life-Parent, Mut-No.
func (rev *Packrev) PackHash() []byte {
	return rev.data[len(rev.data)-2*rev.hashSize : len(rev.data)-rev.hashSize]
}

// PositionAt returns the pack index position
// of the object at a pack offset order position.
//
// PositionAt panics when packOrder is out of range,
// and errors when the stored position is not a valid index position.
func (rev *Packrev) PositionAt(packOrder int) (int, error) {
	if packOrder < 0 || packOrder >= rev.numObjects {
		panic("internal/format/packrev: pack order position out of range")
	}

	stored := binary.BigEndian.Uint32(rev.data[headerLen+4*packOrder:])

	position, err := intconv.Uint32ToInt(stored)
	if err != nil {
		return 0, fmt.Errorf("%w: %w", ErrMalformedReverseIndex, err)
	}

	if position >= rev.numObjects {
		return 0, fmt.Errorf("%w: index position out of range", ErrMalformedReverseIndex)
	}

	return position, nil
}