aboutsummaryrefslogtreecommitdiff
package main

import (
	"fmt"

	"lindenii.org/go/furgit/internal/cache/clock"
	"lindenii.org/go/furgit/internal/format/packfile"
	"lindenii.org/go/furgit/internal/format/packfile/delta"
	"lindenii.org/go/furgit/object/header"
	"lindenii.org/go/furgit/object/id"
	"lindenii.org/go/lgo/intconv"
)

const baseCacheMaxWeight = 64 << 20

type resolvedBase struct {
	entryType packfile.EntryType
	content   []byte
}

type baseCache = clock.Clock[int, resolvedBase]

func newBaseCache() *baseCache {
	return clock.New(baseCacheMaxWeight, func(_ int, base resolvedBase) uint64 {
		return uint64(len(base.content)) + 32
	})
}

func (explainer *explainer) reconstruct(offset, depth int) (packfile.EntryType, []byte, bool, error) {
	var zero packfile.EntryType

	if depth > delta.MaxChainDepth {
		return zero, nil, false, fmt.Errorf("delta chain too deep at offset %d", offset)
	}

	if cached, ok := explainer.cache.Get(offset); ok {
		return cached.entryType, cached.content, true, nil
	}

	header, err := packfile.ParseEntryHeader(explainer.data[offset:], explainer.objectFormat.Size())
	if err != nil {
		return zero, nil, false, fmt.Errorf("entry at offset %d: %w", offset, err)
	}

	payloadStart := offset + header.HeaderLen
	if payloadStart > len(explainer.data) {
		return zero, nil, false, fmt.Errorf("entry at offset %d: header runs past end of pack", offset)
	}

	if header.Type.IsBase() {
		content, _, err := inflateAt(explainer.data[payloadStart:])
		if err != nil {
			return zero, nil, false, fmt.Errorf("entry at offset %d: %w", offset, err)
		}

		explainer.cache.Add(offset, resolvedBase{entryType: header.Type, content: content})

		return header.Type, content, true, nil
	}

	baseOffset, ok, err := explainer.baseOffset(offset, header)
	if err != nil {
		return zero, nil, false, err
	}

	if !ok {
		return zero, nil, false, nil
	}

	baseType, baseContent, ok, err := explainer.reconstruct(baseOffset, depth+1)
	if err != nil || !ok {
		return zero, nil, ok, err
	}

	payload, _, err := inflateAt(explainer.data[payloadStart:])
	if err != nil {
		return zero, nil, false, fmt.Errorf("entry at offset %d: %w", offset, err)
	}

	content, err := delta.Apply(baseContent, payload)
	if err != nil {
		return zero, nil, false, fmt.Errorf("entry at offset %d: %w", offset, err)
	}

	explainer.cache.Add(offset, resolvedBase{entryType: baseType, content: content})

	return baseType, content, true, nil
}

func (explainer *explainer) baseOffset(offset int, header packfile.EntryHeader) (int, bool, error) {
	switch header.Type {
	case packfile.EntryTypeOfsDelta:
		dist, err := intconv.Uint64ToInt(header.OfsDistance)
		if err != nil || dist <= 0 || dist > offset {
			return 0, false, fmt.Errorf("entry at offset %d: ofs-delta base out of bounds", offset)
		}

		return offset - dist, true, nil
	case packfile.EntryTypeRefDelta:
		refBytes := header.RefBase[:explainer.objectFormat.Size()]

		if explainer.idx != nil {
			baseOffsetU, found, err := explainer.idx.Lookup(refBytes)
			if err != nil {
				return 0, false, fmt.Errorf("entry at offset %d: index lookup: %w", offset, err)
			}

			if found {
				baseOffset, err := intconv.Uint64ToInt(baseOffsetU)
				if err != nil {
					return 0, false, fmt.Errorf("entry at offset %d: index base offset overflows int: %w", offset, err)
				}

				return baseOffset, true, nil
			}
		}

		baseID, err := explainer.objectFormat.FromBytes(refBytes)
		if err != nil {
			return 0, false, fmt.Errorf("entry at offset %d: %w", offset, err)
		}

		if baseOffset, found := explainer.oidIndex[baseID]; found {
			return baseOffset, true, nil
		}

		return 0, false, nil
	case packfile.EntryTypeInvalid,
		packfile.EntryTypeCommit,
		packfile.EntryTypeTree,
		packfile.EntryTypeBlob,
		packfile.EntryTypeTag,
		packfile.EntryTypeFuture:
	}

	return 0, false, fmt.Errorf("entry at offset %d: not a delta entry", offset)
}

func (explainer *explainer) recomputeOID(entryType packfile.EntryType, content []byte) (id.ObjectID, error) {
	var zero id.ObjectID

	objectType, err := entryType.ObjectType()
	if err != nil {
		return zero, err
	}

	hashImpl, err := explainer.objectFormat.New()
	if err != nil {
		return zero, err
	}

	_, _ = hashImpl.Write(header.Append(nil, objectType, len(content)))
	_, _ = hashImpl.Write(content)

	oid, err := explainer.objectFormat.FromBytes(hashImpl.Sum(nil))
	if err != nil {
		return zero, err
	}

	return oid, nil
}