shithub: furgit

ref: c37728bbad8f57288b68559da52ad57abb0141ff
dir: /loose.go/

View raw version
package furgit

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"

	"codeberg.org/lindenii/furgit/internal/bufpool"
	"codeberg.org/lindenii/furgit/internal/zlib"
	"codeberg.org/lindenii/furgit/internal/zlibx"
)

const looseHeaderLimit = 4096

// loosePath returns the path for a loose object, validating hash size.
func (repo *Repository) loosePath(id Hash) (string, error) {
	if id.algo != repo.hashAlgo {
		return "", fmt.Errorf("furgit: hash algorithm mismatch: got %s, expected %s", id.algo.String(), repo.hashAlgo.String())
	}
	hex := id.String()
	return filepath.Join("objects", hex[:2], hex[2:]), nil
}

func (repo *Repository) looseRead(id Hash) (ObjectType, bufpool.Buffer, error) {
	ty, body, err := repo.looseReadTyped(id)
	if err != nil {
		return ObjectTypeInvalid, bufpool.Buffer{}, err
	}
	return ty, body, nil
}

func (repo *Repository) looseReadTyped(id Hash) (ObjectType, bufpool.Buffer, error) {
	path, err := repo.loosePath(id)
	if err != nil {
		return ObjectTypeInvalid, bufpool.Buffer{}, err
	}
	path = repo.repoPath(path)
	f, err := os.Open(path)
	if err != nil {
		if os.IsNotExist(err) {
			return ObjectTypeInvalid, bufpool.Buffer{}, ErrNotFound
		}
		return ObjectTypeInvalid, bufpool.Buffer{}, err
	}
	defer func() { _ = f.Close() }()

	compressed, err := io.ReadAll(f)
	if err != nil {
		return ObjectTypeInvalid, bufpool.Buffer{}, err
	}

	raw, err := zlibx.Decompress(compressed)
	if err != nil {
		return ObjectTypeInvalid, bufpool.Buffer{}, err
	}

	rawBytes := raw.Bytes()
	nul := bytes.IndexByte(rawBytes, 0)
	if nul < 0 {
		raw.Release()
		return ObjectTypeInvalid, bufpool.Buffer{}, ErrInvalidObject
	}

	header := rawBytes[:nul]
	body := rawBytes[nul+1:]

	ty, declaredSize, err := parseLooseHeader(header)
	if err != nil {
		raw.Release()
		return ObjectTypeInvalid, bufpool.Buffer{}, err
	}
	if declaredSize != int64(len(body)) {
		raw.Release()
		return ObjectTypeInvalid, bufpool.Buffer{}, ErrInvalidObject
	}

	copy(rawBytes, body)
	raw.Resize(len(body))

	return ty, raw, nil
}

func (repo *Repository) looseTypeSize(id Hash) (ObjectType, int64, error) {
	path, err := repo.loosePath(id)
	if err != nil {
		return ObjectTypeInvalid, 0, err
	}
	path = repo.repoPath(path)
	// #nosec G304
	f, err := os.Open(path)
	if err != nil {
		if os.IsNotExist(err) {
			return ObjectTypeInvalid, 0, ErrNotFound
		}
		return ObjectTypeInvalid, 0, err
	}
	defer func() { _ = f.Close() }()

	zr, err := zlib.NewReader(f)
	if err != nil {
		return ObjectTypeInvalid, 0, err
	}
	defer func() { _ = zr.Close() }()

	header := make([]byte, 0, 64)
	chunk := make([]byte, 128)
	for {
		n, readErr := zr.Read(chunk)
		if n > 0 {
			data := chunk[:n]
			if nul := bytes.IndexByte(data, 0); nul >= 0 {
				header = append(header, data[:nul]...)
				if len(header) > looseHeaderLimit {
					return ObjectTypeInvalid, 0, ErrInvalidObject
				}
				break
			}
			header = append(header, data...)
			if len(header) > looseHeaderLimit {
				return ObjectTypeInvalid, 0, ErrInvalidObject
			}
		}
		if readErr != nil {
			if readErr == io.EOF {
				return ObjectTypeInvalid, 0, ErrInvalidObject
			}
			return ObjectTypeInvalid, 0, readErr
		}
	}
	return parseLooseHeader(header)
}

func parseLooseHeader(header []byte) (ObjectType, int64, error) {
	space := bytes.IndexByte(header, ' ')
	if space < 0 {
		return ObjectTypeInvalid, 0, ErrInvalidObject
	}
	ty, err := objTypeFromName(string(header[:space]))
	if err != nil {
		return ObjectTypeInvalid, 0, err
	}
	expect := header[space+1:]
	if len(expect) == 0 {
		return ObjectTypeInvalid, 0, ErrInvalidObject
	}
	size, err := strconv.ParseInt(string(expect), 10, 64)
	if err != nil {
		return ObjectTypeInvalid, 0, fmt.Errorf("furgit: loose: size parse: %w", err)
	}
	if size < 0 {
		return ObjectTypeInvalid, 0, ErrInvalidObject
	}
	return ty, size, nil
}

func objTypeFromName(name string) (ObjectType, error) {
	switch name {
	case objectTypeNameBlob:
		return ObjectTypeBlob, nil
	case objectTypeNameTree:
		return ObjectTypeTree, nil
	case objectTypeNameCommit:
		return ObjectTypeCommit, nil
	case objectTypeNameTag:
		return ObjectTypeTag, nil
	default:
		return ObjectTypeInvalid, ErrInvalidObject
	}
}

// WriteLooseObject writes an object to the repository as a loose object.
func (repo *Repository) WriteLooseObject(obj Object) (Hash, error) {
	if obj == nil {
		return Hash{}, ErrInvalidObject
	}
	raw, err := obj.Serialize()
	if err != nil {
		return Hash{}, err
	}

	id := repo.computeRawHash(raw)
	path, err := repo.loosePath(id)
	if err != nil {
		return Hash{}, err
	}
	path = repo.repoPath(path)

	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return Hash{}, err
	}

	var buf bytes.Buffer
	zw := zlib.NewWriter(&buf)
	if _, err := zw.Write(raw); err != nil {
		return Hash{}, err
	}
	if err := zw.Close(); err != nil {
		return Hash{}, err
	}

	if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
		return Hash{}, err
	}

	return id, nil
}