aboutsummaryrefslogtreecommitdiff
package packidx

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"math"

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

// ErrInvalidEntries reports that
// entries supplied for an index write
// are unsorted, duplicated, or not representable
// in the pack index format.
var ErrInvalidEntries = errors.New("internal/format/packidx: invalid entries")

// Entry is one object record for an index write.
type Entry struct {
	// OID holds the object ID bytes;
	// only the first hash-size bytes are meaningful.
	OID [id.MaxObjectIDSize]byte
	// Offset is the entry's pack file offset.
	Offset uint64
	// CRC32 is the CRC32 of the entry's packed data.
	CRC32 uint32
}

// Write writes one pack index over entries to w.
//
// entries must be sorted by object ID without duplicates.
// packHash must be the pack's trailer hash;
// Write panics when its length does not match the object format.
func Write(w io.Writer, objectFormat id.ObjectFormat, entries []Entry, packHash []byte) error {
	hashSize := objectFormat.Size()
	if hashSize == 0 {
		return id.ErrInvalidObjectFormat
	}

	if len(packHash) != hashSize {
		panic("internal/format/packidx: invalid pack hash length")
	}

	if len(entries) > math.MaxUint32 {
		return fmt.Errorf("%w: too many entries", ErrInvalidEntries)
	}

	for i := 1; i < len(entries); i++ {
		if bytes.Compare(entries[i-1].OID[:hashSize], entries[i].OID[:hashSize]) >= 0 {
			return fmt.Errorf("%w: not sorted by object ID", ErrInvalidEntries)
		}
	}

	hashImpl, err := objectFormat.New()
	if err != nil {
		return fmt.Errorf("internal/format/packidx: %w", err)
	}

	bw := bufio.NewWriter(io.MultiWriter(w, hashImpl))
	sw := stickyio.New(bw)

	sw.PutUint32(signature)
	sw.PutUint32(version)

	var counts [256]uint32
	for i := range entries {
		counts[entries[i].OID[0]]++
	}

	cumulative := uint32(0)
	for _, count := range counts {
		cumulative += count
		sw.PutUint32(cumulative)
	}

	for i := range entries {
		sw.Put(entries[i].OID[:hashSize])
	}

	for i := range entries {
		sw.PutUint32(entries[i].CRC32)
	}

	largeOffsets := make([]uint64, 0, len(entries))

	for i := range entries {
		offset := entries[i].Offset
		if offset < largeOffsetFlag {
			sw.PutUint32(uint32(offset))

			continue
		}

		slot := len(largeOffsets)
		if slot >= largeOffsetFlag {
			return fmt.Errorf("%w: too many large offsets", ErrInvalidEntries)
		}

		sw.PutUint32(largeOffsetFlag | uint32(slot))

		largeOffsets = append(largeOffsets, offset)
	}

	for _, offset := range largeOffsets {
		sw.PutUint64(offset)
	}

	sw.Put(packHash)

	err = sw.Err()
	if err != nil {
		return fmt.Errorf("internal/format/packidx: %w", err)
	}

	err = bw.Flush()
	if err != nil {
		return fmt.Errorf("internal/format/packidx: %w", err)
	}

	_, err = w.Write(hashImpl.Sum(nil))
	if err != nil {
		return fmt.Errorf("internal/format/packidx: %w", err)
	}

	return nil
}