aboutsummaryrefslogtreecommitdiff
package ingest

import (
	"errors"
	"fmt"
	"hash/crc32"
	"io"
	"os"

	"lindenii.org/go/furgit/internal/compress/zlib"
	"lindenii.org/go/furgit/internal/format/packfile"
	"lindenii.org/go/furgit/internal/progress"
	"lindenii.org/go/furgit/object/id"
	"lindenii.org/go/furgit/object/store"
	"lindenii.org/go/furgit/object/typ"
	"lindenii.org/go/lgo/intconv"
)

// fixThin completes a thin pack
// by appending the external bases it references,
// rewriting the pack header and trailer,
// and resolving the deltas reached from the appended bases.
func (ingestion *ingestion) fixThin(external []id.ObjectID, adjacency adjacency, meter *progress.Meter) error {
	if ingestion.opts.ThinBase == nil {
		return ErrThinPackNotPermitted
	}

	hashSize := ingestion.objectFormat.Size()
	if ingestion.scanner.consumed < packfile.HeaderLen+hashSize {
		return fmt.Errorf("%w: pack shorter than trailer", ErrMalformedPack)
	}

	// Drop the trailer from the write cursor.
	ingestion.scanner.consumed -= hashSize

	appended := make([]int, 0, len(external))

	for _, baseOID := range external {
		ty, content, err := ingestion.opts.ThinBase.ReadBytesContent(baseOID)
		if errors.Is(err, store.ErrObjectNotFound) {
			continue
		}

		if err != nil {
			return fmt.Errorf("object/store/packed/internal/ingest: reading thin base %s: %w", baseOID, err)
		}

		index, err := ingestion.appendBaseObject(baseOID, ty, content)
		if err != nil {
			return err
		}

		appended = append(appended, index)
	}

	err := ingestion.rewriteHeaderTrailer()
	if err != nil {
		return err
	}

	err = ingestion.resolveFrom(appended, adjacency, meter)
	if err != nil {
		return err
	}

	missing := ingestion.unresolvedExternalBases()
	if len(missing) > 0 {
		return &ThinBasesMissingError{OIDs: missing}
	}

	if ingestion.countUnresolved() > 0 {
		return fmt.Errorf("%w: unresolvable delta entries after thin completion", ErrMalformedPack)
	}

	ingestion.thinFixed = len(appended) > 0

	return nil
}

// appendBaseObject appends one external thin base
// as a non-delta pack entry at the current write cursor,
// verifying that its content hashes to the requested object ID.
func (ingestion *ingestion) appendBaseObject(objectID id.ObjectID, objectType typ.Type, content []byte) (int, error) {
	entryType, err := packfile.EntryTypeFromObjectType(objectType)
	if err != nil {
		return 0, fmt.Errorf("object/store/packed/internal/ingest: %w", err)
	}

	computed, err := ingestion.hashObject(entryType, content)
	if err != nil {
		return 0, err
	}

	if computed != objectID {
		return 0, fmt.Errorf("%w: thin base %s content hashes to %s", ErrMalformedPack, objectID, computed)
	}

	start := ingestion.scanner.consumed
	startOffset := int64(start)

	headerBytes := packfile.AppendTypeSize(nil, entryType, uint64(len(content)))

	_, err = ingestion.packFile.WriteAt(headerBytes, startOffset)
	if err != nil {
		return 0, fmt.Errorf("object/store/packed/internal/ingest: writing thin base header: %w", err)
	}

	crc := crc32.NewIEEE()
	_, _ = crc.Write(headerBytes)

	dataOffset := startOffset + int64(len(headerBytes))
	writer := &offsetWriter{file: ingestion.packFile, offset: dataOffset}

	zw := zlib.NewWriter(io.MultiWriter(writer, crc))

	_, err = zw.Write(content)
	if err != nil {
		_ = zw.Close()

		return 0, fmt.Errorf("object/store/packed/internal/ingest: compressing thin base: %w", err)
	}

	err = zw.Close()
	if err != nil {
		return 0, fmt.Errorf("object/store/packed/internal/ingest: compressing thin base: %w", err)
	}

	headerLen := len(headerBytes)
	packedLen := headerLen + writer.written
	ingestion.scanner.consumed = start + packedLen

	rec := record{
		offset:       start,
		headerLen:    headerLen,
		packedLen:    packedLen,
		crc32:        crc.Sum32(),
		packedType:   entryType,
		declaredSize: len(content),
		baseOffset:   0,
		baseOID:      id.ObjectID{},
		objectType:   entryType,
		oid:          objectID,
		resolved:     true,
	}

	index := len(ingestion.records)
	ingestion.records = append(ingestion.records, rec)
	ingestion.byOffset[start] = index
	ingestion.byOID[objectID] = index

	return index, nil
}

// rewriteHeaderTrailer updates the pack object count
// and recomputes the pack trailer hash
// over the entries left after thin completion.
func (ingestion *ingestion) rewriteHeaderTrailer() error {
	count, err := intconv.IntToUint32(len(ingestion.records))
	if err != nil {
		return fmt.Errorf("object/store/packed/internal/ingest: %w", err)
	}

	_, err = ingestion.packFile.WriteAt(packfile.AppendHeader(nil, count), 0)
	if err != nil {
		return fmt.Errorf("object/store/packed/internal/ingest: rewriting header: %w", err)
	}

	bodyEnd := int64(ingestion.scanner.consumed)

	hashImpl, err := ingestion.objectFormat.New()
	if err != nil {
		return fmt.Errorf("object/store/packed/internal/ingest: %w", err)
	}

	_, err = io.Copy(hashImpl, io.NewSectionReader(ingestion.packFile, 0, bodyEnd))
	if err != nil {
		return fmt.Errorf("object/store/packed/internal/ingest: rehashing pack: %w", err)
	}

	sum := hashImpl.Sum(nil)

	_, err = ingestion.packFile.WriteAt(sum, bodyEnd)
	if err != nil {
		return fmt.Errorf("object/store/packed/internal/ingest: writing trailer: %w", err)
	}

	packHash, err := ingestion.objectFormat.FromBytes(sum)
	if err != nil {
		return fmt.Errorf("object/store/packed/internal/ingest: %w", err)
	}

	ingestion.packHash = packHash

	return nil
}

// offsetWriter writes to a file via WriteAt,
// advancing sequentially from a base offset
// and counting the bytes written.
type offsetWriter struct {
	file    *os.File
	offset  int64
	written int
}

// Write implements [io.Writer].
func (writer *offsetWriter) Write(p []byte) (int, error) {
	n, err := writer.file.WriteAt(p, writer.offset)
	writer.offset += int64(n)
	writer.written += n

	return n, err //nolint:wrapcheck
}