aboutsummaryrefslogtreecommitdiff
package packed

import (
	"bytes"
	"errors"
	"fmt"
	"os"

	"lindenii.org/go/furgit/internal/format/packfile"
	"lindenii.org/go/furgit/internal/format/packidx"
	"lindenii.org/go/furgit/internal/format/packidx/bloom"
	"lindenii.org/go/furgit/internal/mmap"
	"lindenii.org/go/furgit/object/id"
	"lindenii.org/go/lgo/intconv"
)

var (
	errPackTruncated       = errors.New("truncated")
	errPackMalformedHeader = errors.New("malformed header")
	errPackCountMismatch   = errors.New("object count differs from index")
	errPackTrailerMismatch = errors.New("trailer hash differs from index")
)

// pack is one discovered pack:
// its base name, its parsed index, and its mapped data.
// All fields are immutable after openPack.
type pack struct {
	// name is the pack base name, like "pack-<hash>".
	name string

	// idxMapping owns the mapped pack index bytes,
	// and idx is the parsed index view over them.
	idxMapping *mmap.Mmap
	idx        packidx.Packidx

	// dataMapping owns the mapped pack data bytes,
	// and data aliases them.
	dataMapping *mmap.Mmap
	data        []byte

	bloomMapping *mmap.Mmap
	filter       *bloom.Bloom
}

// openPack opens, maps, and validates
// one pack index and its pack data
// by pack base name.
func openPack(root *os.Root, name string, objectFormat id.ObjectFormat) (*pack, error) {
	idxMapping, err := mapFile(root, name+".idx")
	if err != nil {
		return nil, err
	}

	idx, err := packidx.Parse(idxMapping.Data(), objectFormat.Size())
	if err != nil {
		_ = idxMapping.Close()

		return nil, fmt.Errorf("%w: index %q: %w", ErrMalformedPackedStore, name, err)
	}

	dataMapping, err := mapFile(root, name+".pack")
	if err != nil {
		_ = idxMapping.Close()

		return nil, err
	}

	err = validatePackData(dataMapping.Data(), &idx, objectFormat.Size())
	if err != nil {
		_ = idxMapping.Close()
		_ = dataMapping.Close()

		return nil, fmt.Errorf("%w: pack %q: %w", ErrMalformedPackedStore, name, err)
	}

	bloomMapping, filter := openBloom(root, name, objectFormat, idx.PackHash())

	return &pack{
		name:         name,
		idxMapping:   idxMapping,
		idx:          idx,
		dataMapping:  dataMapping,
		data:         dataMapping.Data(),
		bloomMapping: bloomMapping,
		filter:       filter,
	}, nil
}

func openBloom(root *os.Root, name string, objectFormat id.ObjectFormat, packHash []byte) (*mmap.Mmap, *bloom.Bloom) {
	mapping, err := mapFile(root, name+".bloom")
	if err != nil {
		return nil, nil
	}

	filter, err := bloom.Parse(mapping.Data(), objectFormat)
	if err != nil {
		_ = mapping.Close()

		return nil, nil
	}

	if !bytes.Equal(filter.PackHash(), packHash) {
		_ = mapping.Close()

		return nil, nil
	}

	return mapping, &filter
}

// mapFile opens and maps one file under root.
func mapFile(root *os.Root, name string) (*mmap.Mmap, error) {
	file, err := root.Open(name)
	if err != nil {
		return nil, fmt.Errorf("object/store/packed: %w", err)
	}

	defer func() { _ = file.Close() }()

	mapping, err := mmap.Open(file)
	if err != nil {
		return nil, fmt.Errorf("object/store/packed: %q: %w", name, err)
	}

	return mapping, nil
}

// validatePackData checks one mapped pack
// against the pack format and its index.
func validatePackData(data []byte, idx *packidx.Packidx, hashSize int) error {
	if len(data) < packfile.HeaderLen+hashSize {
		return errPackTruncated
	}

	header, err := packfile.ParseHeader(data)
	if err != nil {
		return fmt.Errorf("%w: %w", errPackMalformedHeader, err)
	}

	count := uint64(header.ObjectCount)

	numObjects, err := intconv.IntToUint64(idx.NumObjects())
	if err != nil {
		return fmt.Errorf("object count: %w", err)
	}

	if count != numObjects {
		return errPackCountMismatch
	}

	if !bytes.Equal(data[len(data)-hashSize:], idx.PackHash()) {
		return errPackTrailerMismatch
	}

	return nil
}

// close releases the pack data, index, and filter mappings.
func (pack *pack) close() error {
	errs := []error{
		pack.dataMapping.Close(),
		pack.idxMapping.Close(),
	}

	if pack.bloomMapping != nil {
		errs = append(errs, pack.bloomMapping.Close())
	}

	return errors.Join(errs...)
}