aboutsummaryrefslogtreecommitdiff
package loose

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"

	"lindenii.org/go/furgit/internal/compress/zlib"
	"lindenii.org/go/furgit/internal/iolimit"
	"lindenii.org/go/furgit/object/id"
	"lindenii.org/go/furgit/object/store"
	"lindenii.org/go/furgit/object/typ"
)

// ReadBytesFull reads a full serialized object as "type size\x00content".
//
// It inflates and parses the full loose object,
// including verifying the zlib Adler-32 trailer.
func (loose *Loose) ReadBytesFull(objectID id.ObjectID) ([]byte, error) {
	raw, _, _, err := loose.readBytesParsed(objectID)
	if err != nil {
		return nil, err
	}

	return raw, nil
}

// ReadBytesContent reads an object's type and content bytes.
//
// Like ReadBytesFull,
// it inflates and parses the full loose object,
// including verifying the zlib Adler-32 trailer.
func (loose *Loose) ReadBytesContent(objectID id.ObjectID) (typ.Type, []byte, error) {
	_, ty, content, err := loose.readBytesParsed(objectID)
	if err != nil {
		return typ.TypeUnknown, nil, err
	}

	return ty, content, nil
}

// ReadHeader reads an object's type and declared content length.
//
// It parses only enough of the zlib-decoded object to recover the object header.
// It does not verify that the remaining object content is readable
// and does not verify the zlib Adler-32 trailer.
func (loose *Loose) ReadHeader(objectID id.ObjectID) (typ.Type, uint64, error) {
	file, err := loose.openObject(objectID)
	if err != nil {
		return typ.TypeUnknown, 0, err
	}

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

	zr, err := zlib.NewReader(file)
	if err != nil {
		return typ.TypeUnknown, 0, fmt.Errorf("object/store/loose: %w", err)
	}

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

	_, ty, size, err := readHeader(bufio.NewReader(zr))
	if err != nil {
		return typ.TypeUnknown, 0, err
	}

	return ty, size, nil
}

// ReadSize reads an object's declared content length.
//
// Like ReadHeader,
// it parses only enough of the zlib-decoded object to recover the header
// and does not verify the zlib Adler-32 trailer.
func (loose *Loose) ReadSize(objectID id.ObjectID) (uint64, error) {
	_, size, err := loose.ReadHeader(objectID)

	return size, err
}

// ReadReaderFull reads a full serialized object stream as "type size\x00content".
//
// Close releases resources only.
// It does not drain unread data for additional validation.
// In particular,
// malformed trailing compressed data,
// trailing bytes past the declared object size,
// and the zlib Adler-32 trailer
// may go unverified unless the caller reads to io.EOF.
func (loose *Loose) ReadReaderFull(objectID id.ObjectID) (io.ReadCloser, error) {
	file, zr, err := loose.openInflated(objectID)
	if err != nil {
		return nil, err
	}

	br := bufio.NewReader(zr)

	headerBytes, _, size, err := readHeader(br)
	if err != nil {
		_ = zr.Close()
		_ = file.Close()

		return nil, err
	}

	return &objectReader{
		reader: io.MultiReader(
			bytes.NewReader(headerBytes),
			iolimit.ExpectLengthReader(br, size),
		),
		file: file,
		zr:   zr,
	}, nil
}

// ReadReaderContent reads an object's type, declared content length,
// and content stream.
//
// Close releases resources only.
// It does not drain unread data for additional validation.
// In particular,
// malformed trailing compressed data,
// trailing bytes past the declared object size,
// and the zlib Adler-32 trailer
// may go unverified unless the caller reads to io.EOF.
func (loose *Loose) ReadReaderContent(objectID id.ObjectID) (typ.Type, uint64, io.ReadCloser, error) {
	file, zr, err := loose.openInflated(objectID)
	if err != nil {
		return typ.TypeUnknown, 0, nil, err
	}

	br := bufio.NewReader(zr)

	_, ty, size, err := readHeader(br)
	if err != nil {
		_ = zr.Close()
		_ = file.Close()

		return typ.TypeUnknown, 0, nil, err
	}

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

// Refresh is a no-op for loose object stores.
func (loose *Loose) Refresh() error {
	return nil
}

// openObject opens the loose object file for objectID.
// Missing files cause store.ErrObjectNotFound.
func (loose *Loose) openObject(objectID id.ObjectID) (*os.File, error) {
	relPath, err := loose.objectPath(objectID)
	if err != nil {
		return nil, err
	}

	file, err := loose.root.Open(relPath)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return nil, store.ErrObjectNotFound
		}

		return nil, fmt.Errorf("object/store/loose: %w", err)
	}

	return file, nil
}

// readBytesParsed reads, inflates, and parses a loose object in one pass.
// It returns the full raw payload and its parsed type and content.
func (loose *Loose) readBytesParsed(objectID id.ObjectID) ([]byte, typ.Type, []byte, error) {
	file, err := loose.openObject(objectID)
	if err != nil {
		return nil, typ.TypeUnknown, nil, err
	}

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

	raw, err := decodeAll(file)
	if err != nil {
		return nil, typ.TypeUnknown, nil, err
	}

	ty, content, err := parseRaw(raw)
	if err != nil {
		return nil, typ.TypeUnknown, nil, err
	}

	return raw, ty, content, nil
}

// openInflated opens and zlib-decodes a loose object file.
// The caller owns both returned closers and must close them.
func (loose *Loose) openInflated(objectID id.ObjectID) (*os.File, io.ReadCloser, error) {
	file, err := loose.openObject(objectID)
	if err != nil {
		return nil, nil, err
	}

	zr, err := zlib.NewReader(file)
	if err != nil {
		_ = file.Close()

		return nil, nil, fmt.Errorf("object/store/loose: %w", err)
	}

	return file, zr, nil
}

// objectReader streams one inflated loose object
// and owns the underlying file and zlib decoder.
type objectReader struct {
	// reader is the stream exposed by Read.
	reader io.Reader
	// file is the underlying loose object file and is closed by Close.
	file *os.File
	// zr is the zlib decoder and is closed by Close.
	zr io.ReadCloser
}

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

func (reader *objectReader) Close() error {
	errZlib := reader.zr.Close()
	errFile := reader.file.Close()

	return errors.Join(errZlib, errFile)
}