aboutsummaryrefslogtreecommitdiff
package packed

import (
	"bytes"
	"fmt"
	"io"

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

// ReadBytesContent reads an object's type and content bytes,
// fully resolving delta chains.
func (packed *Packed) ReadBytesContent(objectID id.ObjectID) (typ.Type, []byte, error) {
	p, offset, err := packed.lookup(objectID)
	if err != nil {
		return typ.Unknown, nil, err
	}

	entryType, content, err := packed.unpackEntry(p, offset)
	if err != nil {
		return typ.Unknown, nil, err
	}

	ty, err := objectTypeOf(entryType)
	if err != nil {
		return typ.Unknown, nil, err
	}

	return ty, content, nil
}

// ReadBytesFull reads a full serialized object as "type size\x00content",
// fully resolving delta chains.
func (packed *Packed) ReadBytesFull(objectID id.ObjectID) ([]byte, error) {
	ty, content, err := packed.ReadBytesContent(objectID)
	if err != nil {
		return nil, err
	}

	raw := header.Append(make([]byte, 0, len(content)+32), ty, len(content))

	return append(raw, content...), nil
}

// ReadHeader reads an object's type and declared content length.
//
// For delta entries this resolves the chained base type
// and the declared delta result size,
// without reconstructing content.
func (packed *Packed) ReadHeader(objectID id.ObjectID) (typ.Type, int, error) {
	p, offset, err := packed.lookup(objectID)
	if err != nil {
		return typ.Unknown, 0, err
	}

	entryHeader, payload, err := p.entryHeaderAt(offset, packed.objectFormat)
	if err != nil {
		return typ.Unknown, 0, err
	}

	var size int

	if entryHeader.Type.IsDelta() {
		size, err = deltaResultSize(payload, entryHeader.Size)
		if err != nil {
			return typ.Unknown, 0, fmt.Errorf("%w: pack %q: %w", ErrMalformedPackedStore, p.name, err)
		}
	} else {
		size, err = intconv.Uint64ToInt(entryHeader.Size)
		if err != nil {
			return typ.Unknown, 0, fmt.Errorf("%w: pack %q: object size overflows int: %w", ErrMalformedPackedStore, p.name, err)
		}
	}

	entryType, err := packed.resolveType(p, offset, entryHeader)
	if err != nil {
		return typ.Unknown, 0, err
	}

	ty, err := objectTypeOf(entryType)
	if err != nil {
		return typ.Unknown, 0, err
	}

	return ty, size, nil
}

// ReadSize reads an object's declared content length.
//
// Unlike ReadHeader,
// this never walks delta chains.
func (packed *Packed) ReadSize(objectID id.ObjectID) (int, error) {
	p, offset, err := packed.lookup(objectID)
	if err != nil {
		return 0, err
	}

	entryHeader, payload, err := p.entryHeaderAt(offset, packed.objectFormat)
	if err != nil {
		return 0, err
	}

	if !entryHeader.Type.IsDelta() {
		size, err := intconv.Uint64ToInt(entryHeader.Size)
		if err != nil {
			return 0, fmt.Errorf("%w: pack %q: object size overflows int: %w", ErrMalformedPackedStore, p.name, err)
		}

		return size, nil
	}

	size, err := deltaResultSize(payload, entryHeader.Size)
	if err != nil {
		return 0, fmt.Errorf("%w: pack %q: %w", ErrMalformedPackedStore, p.name, err)
	}

	return size, nil
}

// ReadReaderContent reads an object's type,
// declared content length, and content stream.
//
// Non-delta entries stream directly from the pack;
// delta entries are fully resolved in memory first.
// Close releases resources only
// and does not drain unread data for additional validation.
func (packed *Packed) ReadReaderContent(objectID id.ObjectID) (typ.Type, int, io.ReadCloser, error) {
	p, offset, err := packed.lookup(objectID)
	if err != nil {
		return typ.Unknown, 0, nil, err
	}

	entryHeader, payload, err := p.entryHeaderAt(offset, packed.objectFormat)
	if err != nil {
		return typ.Unknown, 0, nil, err
	}

	if !entryHeader.Type.IsBase() {
		entryType, content, err := packed.unpackEntry(p, offset)
		if err != nil {
			return typ.Unknown, 0, nil, err
		}

		ty, err := objectTypeOf(entryType)
		if err != nil {
			return typ.Unknown, 0, nil, err
		}

		return ty, len(content), io.NopCloser(bytes.NewReader(content)), nil
	}

	ty, err := objectTypeOf(entryHeader.Type)
	if err != nil {
		return typ.Unknown, 0, nil, err
	}

	size, err := intconv.Uint64ToInt(entryHeader.Size)
	if err != nil {
		return typ.Unknown, 0, nil, fmt.Errorf("%w: pack %q: object size overflows int: %w", ErrMalformedPackedStore, p.name, err)
	}

	zr, err := zlib.NewReaderBytes(payload)
	if err != nil {
		return typ.Unknown, 0, nil, fmt.Errorf("%w: pack %q: %w", ErrMalformedPackedStore, p.name, err)
	}

	return ty, size, &objectReader{
		reader: iolimit.ExpectLengthReader(zr, size),
		zr:     zr,
	}, nil
}

// ReadReaderFull reads a full serialized object stream
// as "type size\x00content".
//
// Non-delta entries stream directly from the pack;
// delta entries are fully resolved in memory first.
// Close releases resources only
// and does not drain unread data for additional validation.
func (packed *Packed) ReadReaderFull(objectID id.ObjectID) (io.ReadCloser, error) {
	ty, size, reader, err := packed.ReadReaderContent(objectID)
	if err != nil {
		return nil, err
	}

	headerBytes := header.Append(nil, ty, size)

	return &objectReader{
		reader: io.MultiReader(bytes.NewReader(headerBytes), reader),
		zr:     reader,
	}, nil
}

// objectTypeOf converts one packfile entry type
// into an ordinary object type.
func objectTypeOf(entryType packfile.EntryType) (typ.Type, error) {
	ty, err := entryType.ObjectType()
	if err != nil {
		return typ.Unknown, fmt.Errorf("%w: %w", ErrMalformedPackedStore, err)
	}

	return ty, nil
}

// objectReader streams one packed object payload
// and owns the underlying decompressor.
type objectReader struct {
	// reader is the stream exposed by Read.
	reader io.Reader
	// zr is closed by Close.
	zr io.Closer
}

func (reader *objectReader) Read(dst []byte) (int, error) {
	return reader.reader.Read(dst)
}

func (reader *objectReader) Close() error {
	return reader.zr.Close()
}