aboutsummaryrefslogtreecommitdiff
package loose_test

import (
	"os"
	"strings"
	"testing"

	"lindenii.org/go/furgit/internal/testgit"
	"lindenii.org/go/furgit/object/header"
	"lindenii.org/go/furgit/object/id"
	"lindenii.org/go/furgit/object/store/loose"
	"lindenii.org/go/furgit/object/typ"
)

// gitOracleObject is one object created by git,
// paired with its expected content body and full serialized form.
type gitOracleObject struct {
	name string
	ty   typ.Type
	id   id.ObjectID
	body []byte
	raw  []byte
}

// openLooseStore opens a loose store over the repository's objects directory.
func openLooseStore(t *testing.T, repo *testgit.Repo) *loose.Loose {
	t.Helper()

	repoRoot := repo.Root(t)

	objectsRoot, err := repoRoot.OpenRoot(".git/objects")
	if err != nil {
		_ = repoRoot.Close()

		t.Fatalf("OpenRoot(.git/objects): %v", err)
	}

	_ = repoRoot.Close()

	t.Cleanup(func() { _ = objectsRoot.Close() })

	looseStore, err := loose.New(objectsRoot, repo.ObjectFormat(t))
	if err != nil {
		t.Fatalf("loose.New: %v", err)
	}

	return looseStore
}

// gitOracleObjects builds one object of each kind with git
// and precomputes each one's expected content body and full serialized form.
func gitOracleObjects(t *testing.T, repo *testgit.Repo) []gitOracleObject {
	t.Helper()

	blobID, err := repo.HashObject(t, typ.TypeBlob, strings.NewReader("blob body\n"))
	if err != nil {
		t.Fatalf("HashObject(blob): %v", err)
	}

	treeID, err := repo.MkTree(t, []testgit.TreeEntry{
		{Mode: "100644", Type: typ.TypeBlob, OID: blobID, Name: "file"},
	})
	if err != nil {
		t.Fatalf("MkTree: %v", err)
	}

	commitID, err := repo.CommitTree(t, treeID, testgit.CommitTreeOptions{Message: "subject\n\nbody"})
	if err != nil {
		t.Fatalf("CommitTree: %v", err)
	}

	tagID, err := repo.TagAnnotated(t, "v1", commitID, testgit.TagAnnotatedOptions{Message: "tag message"})
	if err != nil {
		t.Fatalf("TagAnnotated: %v", err)
	}

	kinds := []struct {
		name string
		ty   typ.Type
		id   id.ObjectID
	}{
		{name: "blob", ty: typ.TypeBlob, id: blobID},
		{name: "tree", ty: typ.TypeTree, id: treeID},
		{name: "commit", ty: typ.TypeCommit, id: commitID},
		{name: "tag", ty: typ.TypeTag, id: tagID},
	}

	objects := make([]gitOracleObject, 0, len(kinds))

	for _, kind := range kinds {
		body, err := repo.CatFile(t, kind.ty, kind.id)
		if err != nil {
			t.Fatalf("CatFile(%s): %v", kind.name, err)
		}

		raw := header.Append(nil, kind.ty, uint64(len(body)))
		raw = append(raw, body...)

		objects = append(objects, gitOracleObject{
			name: kind.name,
			ty:   kind.ty,
			id:   kind.id,
			body: body,
			raw:  raw,
		})
	}

	return objects
}

// corruptLooseObjectTrailer flips the final byte of a loose object file,
// damaging the zlib Adler-32 trailer.
func corruptLooseObjectTrailer(t *testing.T, repo *testgit.Repo, objectID id.ObjectID) {
	t.Helper()

	root := repo.Root(t)

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

	hex := objectID.String()
	relPath := ".git/objects/" + hex[:2] + "/" + hex[2:]

	file, err := root.OpenFile(relPath, os.O_RDWR, 0)
	if err != nil {
		t.Fatalf("OpenFile(%q): %v", relPath, err)
	}

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

	info, err := file.Stat()
	if err != nil {
		t.Fatalf("Stat(%q): %v", relPath, err)
	}

	if info.Size() == 0 {
		t.Fatalf("corrupt trailer on empty file %q", relPath)
	}

	last := make([]byte, 1)

	_, err = file.ReadAt(last, info.Size()-1)
	if err != nil {
		t.Fatalf("ReadAt(%q): %v", relPath, err)
	}

	last[0] ^= 0xff

	_, err = file.WriteAt(last, info.Size()-1)
	if err != nil {
		t.Fatalf("WriteAt(%q): %v", relPath, err)
	}
}