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...)
}