aboutsummaryrefslogtreecommitdiff
path: root/object
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-06-07 12:12:39 +0000
committerGravatar Runxi Yu2026-06-07 12:17:59 +0000
commit58698a0e70c61c7447ec9a7b938fc63a94151db1 (patch)
tree808873c6e9ea3a38bcd054ac34eca09bec34703f /object
parentobject/tree: Reject duplicates (diff)
signatureNo signature
object/tree: Add tests
Diffstat (limited to 'object')
-rw-r--r--object/tree/append_test.go48
-rw-r--r--object/tree/helpers_test.go131
-rw-r--r--object/tree/insert.go3
-rw-r--r--object/tree/insert_test.go102
-rw-r--r--object/tree/malformed_test.go59
-rw-r--r--object/tree/parse.go3
-rw-r--r--object/tree/parse_test.go44
-rw-r--r--object/tree/roundtrip_test.go81
8 files changed, 469 insertions, 2 deletions
diff --git a/object/tree/append_test.go b/object/tree/append_test.go
new file mode 100644
index 00000000..babcd03b
--- /dev/null
+++ b/object/tree/append_test.go
@@ -0,0 +1,48 @@
+package tree_test
+
+import (
+ "bytes"
+ "testing"
+
+ "lindenii.org/go/furgit/internal/testgit"
+ "lindenii.org/go/furgit/object/id"
+ "lindenii.org/go/furgit/object/typ"
+)
+
+func TestAppend(t *testing.T) {
+ t.Parallel()
+
+ for _, objectFormat := range id.SupportedObjectFormats() {
+ t.Run(objectFormat.String(), func(t *testing.T) {
+ t.Parallel()
+
+ repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat})
+ if err != nil {
+ t.Fatalf("NewRepo: %v", err)
+ }
+
+ entries := mixedEntries(t, repo)
+ tr := buildTree(t, entries)
+
+ rawBody, err := tr.AppendWithoutHeader(nil)
+ if err != nil {
+ t.Fatalf("AppendWithoutHeader: %v", err)
+ }
+
+ treeID, err := repo.HashObject(t, typ.TypeTree, bytes.NewReader(rawBody))
+ if err != nil {
+ t.Fatalf("HashObject(tree): %v", err)
+ }
+
+ err = repo.Fsck(t, testgit.FsckOptions{
+ Strict: true,
+ NoDangling: true,
+ }, treeID)
+ if err != nil {
+ t.Fatalf("Fsck: %v", err)
+ }
+
+ assertGitDecode(t, repo, treeID, tr.Entries())
+ })
+ }
+}
diff --git a/object/tree/helpers_test.go b/object/tree/helpers_test.go
new file mode 100644
index 00000000..c71f0117
--- /dev/null
+++ b/object/tree/helpers_test.go
@@ -0,0 +1,131 @@
+package tree_test
+
+import (
+ "strconv"
+ "strings"
+ "testing"
+
+ "lindenii.org/go/furgit/internal/testgit"
+ "lindenii.org/go/furgit/object/id"
+ "lindenii.org/go/furgit/object/tree"
+ "lindenii.org/go/furgit/object/tree/mode"
+ "lindenii.org/go/furgit/object/typ"
+)
+
+func mixedEntries(tb testing.TB, repo *testgit.Repo) []tree.Entry {
+ tb.Helper()
+
+ blobA, err := repo.HashObject(tb, typ.TypeBlob, strings.NewReader("blob-A\n"))
+ if err != nil {
+ tb.Fatalf("HashObject(blob-A): %v", err)
+ }
+
+ blobB, err := repo.HashObject(tb, typ.TypeBlob, strings.NewReader("blob-B\n"))
+ if err != nil {
+ tb.Fatalf("HashObject(blob-B): %v", err)
+ }
+
+ blobC, err := repo.HashObject(tb, typ.TypeBlob, strings.NewReader("blob-C\n"))
+ if err != nil {
+ tb.Fatalf("HashObject(blob-C): %v", err)
+ }
+
+ subTree, err := repo.MkTree(tb, []testgit.TreeEntry{
+ {Mode: "100644", Type: typ.TypeBlob, OID: blobA, Name: "leaf"},
+ })
+ if err != nil {
+ tb.Fatalf("MkTree(subtree): %v", err)
+ }
+
+ submodule, err := repo.CommitTree(tb, subTree, testgit.CommitTreeOptions{Message: "submodule"})
+ if err != nil {
+ tb.Fatalf("CommitTree(submodule): %v", err)
+ }
+
+ return []tree.Entry{
+ {Mode: mode.Regular, Name: "z", ID: blobA},
+ {Mode: mode.Regular, Name: "A", ID: blobB},
+ {Mode: mode.Regular, Name: "aa", ID: blobC},
+ {Mode: mode.Regular, Name: "a0", ID: blobA},
+ {Mode: mode.Regular, Name: "a.", ID: blobC},
+ {Mode: mode.Regular, Name: "Z", ID: blobB},
+ {Mode: mode.Regular, Name: "0", ID: blobA},
+ {Mode: mode.Regular, Name: "CAPS", ID: blobB},
+ {Mode: mode.Regular, Name: "caps", ID: blobC},
+ {Mode: mode.Regular, Name: "name with space", ID: blobB},
+ {Mode: mode.Regular, Name: "name.with.dot", ID: blobA},
+ {Mode: mode.Regular, Name: "这是一些非 ASCII 的字符", ID: blobC},
+ {Mode: mode.Regular, Name: "Emoji 👀", ID: blobC},
+ {Mode: mode.Regular, Name: ".hidden", ID: blobA},
+ {Mode: mode.Executable, Name: "exec.sh", ID: blobB},
+ {Mode: mode.Symlink, Name: "sym.link", ID: blobC},
+ {Mode: mode.Gitlink, Name: "submodule", ID: submodule},
+ {Mode: mode.Regular, Name: "dir-", ID: blobA},
+ {Mode: mode.Directory, Name: "dir", ID: subTree},
+ {Mode: mode.Regular, Name: "dir0", ID: blobB},
+ }
+}
+
+func mkTreeEntries(entries []tree.Entry) []testgit.TreeEntry {
+ out := make([]testgit.TreeEntry, len(entries))
+ for i, entry := range entries {
+ out[i] = testgit.TreeEntry{
+ Mode: strconv.FormatUint(uint64(entry.Mode), 8),
+ Type: entry.Mode.ObjectType(),
+ OID: entry.ID,
+ Name: entry.Name,
+ }
+ }
+
+ return out
+}
+
+func buildTree(tb testing.TB, entries []tree.Entry) *tree.Tree {
+ tb.Helper()
+
+ tr := new(tree.Tree)
+ for _, entry := range entries {
+ err := tr.Insert(entry)
+ if err != nil {
+ tb.Fatalf("Insert(%q): %v", entry.Name, err)
+ }
+ }
+
+ return tr
+}
+
+func assertGitDecode(tb testing.TB, repo *testgit.Repo, treeID id.ObjectID, got []tree.Entry) {
+ tb.Helper()
+
+ want, err := repo.LsTree(tb, treeID)
+ if err != nil {
+ tb.Fatalf("LsTree: %v", err)
+ }
+
+ if len(got) != len(want) {
+ tb.Fatalf("entry count = %d, want %d", len(got), len(want))
+ }
+
+ for i := range want {
+ wantMode, err := strconv.ParseUint(want[i].Mode, 8, 32)
+ if err != nil {
+ tb.Fatalf("entry[%d] parse git mode %q: %v", i, want[i].Mode, err)
+ }
+
+ if uint64(got[i].Mode) != wantMode {
+ tb.Fatalf("entry[%d] mode = %o, want %o", i, uint64(got[i].Mode), wantMode)
+ }
+
+ if got[i].Mode.ObjectType() != want[i].Type {
+ tb.Fatalf("entry[%d] type = %v, want %v", i, got[i].Mode.ObjectType(), want[i].Type)
+ }
+
+ if got[i].ID != want[i].OID {
+ tb.Fatalf("entry[%d] id = %s, want %s", i, got[i].ID, want[i].OID)
+ }
+
+ if got[i].Name != want[i].Name {
+ tb.Fatalf("entry[%d] name = %q, want %q", i, got[i].Name, want[i].Name)
+ }
+ }
+}
diff --git a/object/tree/insert.go b/object/tree/insert.go
index e7fce5e9..b5227938 100644
--- a/object/tree/insert.go
+++ b/object/tree/insert.go
@@ -17,7 +17,8 @@ var ErrInvalidTree = errors.New("object/tree: invalid tree")
// It rejects entries with an invalid name or mode,
// and entries whose name conflicts with one already present.
func (tree *Tree) Insert(entry Entry) error {
- if err := validateName(entry.Name); err != nil {
+ err := validateName(entry.Name)
+ if err != nil {
return err
}
diff --git a/object/tree/insert_test.go b/object/tree/insert_test.go
new file mode 100644
index 00000000..fbf65b84
--- /dev/null
+++ b/object/tree/insert_test.go
@@ -0,0 +1,102 @@
+package tree_test
+
+import (
+ "errors"
+ "testing"
+
+ "lindenii.org/go/furgit/object/id"
+ "lindenii.org/go/furgit/object/tree"
+ "lindenii.org/go/furgit/object/tree/mode"
+)
+
+func TestInsertRejects(t *testing.T) {
+ t.Parallel()
+
+ zero := id.SupportedObjectFormats()[0].Zero()
+
+ for _, tc := range []struct {
+ name string
+ entry tree.Entry
+ }{
+ {name: "empty-name", entry: tree.Entry{Mode: mode.Regular, Name: "", ID: zero}},
+ {name: "slash-name", entry: tree.Entry{Mode: mode.Regular, Name: "a/b", ID: zero}},
+ {name: "nul-name", entry: tree.Entry{Mode: mode.Regular, Name: "a\x00b", ID: zero}},
+ {name: "invalid-mode", entry: tree.Entry{Mode: mode.Mode(0o100640), Name: "file", ID: zero}},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ var tr tree.Tree
+
+ err := tr.Insert(tc.entry)
+ if !errors.Is(err, tree.ErrInvalidTree) {
+ t.Fatalf("Insert error = %v, want ErrInvalidTree", err)
+ }
+ })
+ }
+}
+
+func TestInsertRejectsConflict(t *testing.T) {
+ t.Parallel()
+
+ zero := id.SupportedObjectFormats()[0].Zero()
+
+ for _, tc := range []struct {
+ name string
+ first tree.Entry
+ second tree.Entry
+ }{
+ {
+ name: "same-mode",
+ first: tree.Entry{Mode: mode.Regular, Name: "file", ID: zero},
+ second: tree.Entry{Mode: mode.Regular, Name: "file", ID: zero},
+ },
+ {
+ name: "blob-then-tree",
+ first: tree.Entry{Mode: mode.Regular, Name: "name", ID: zero},
+ second: tree.Entry{Mode: mode.Directory, Name: "name", ID: zero},
+ },
+ {
+ name: "tree-then-blob",
+ first: tree.Entry{Mode: mode.Directory, Name: "name", ID: zero},
+ second: tree.Entry{Mode: mode.Regular, Name: "name", ID: zero},
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ var tr tree.Tree
+
+ err := tr.Insert(tc.first)
+ if err != nil {
+ t.Fatalf("Insert(first): %v", err)
+ }
+
+ err = tr.Insert(tc.second)
+ if !errors.Is(err, tree.ErrInvalidTree) {
+ t.Fatalf("Insert(second) error = %v, want ErrInvalidTree", err)
+ }
+ })
+ }
+}
+
+func TestEntriesIsCopy(t *testing.T) {
+ t.Parallel()
+
+ zero := id.SupportedObjectFormats()[0].Zero()
+
+ var tr tree.Tree
+
+ err := tr.Insert(tree.Entry{Mode: mode.Regular, Name: "file", ID: zero})
+ if err != nil {
+ t.Fatalf("Insert: %v", err)
+ }
+
+ entries := tr.Entries()
+ entries[0].Name = "mutated"
+
+ again := tr.Entries()
+ if again[0].Name != "file" {
+ t.Fatalf("Entries()[0].Name = %q, want %q", again[0].Name, "file")
+ }
+}
diff --git a/object/tree/malformed_test.go b/object/tree/malformed_test.go
new file mode 100644
index 00000000..ca00ea94
--- /dev/null
+++ b/object/tree/malformed_test.go
@@ -0,0 +1,59 @@
+package tree_test
+
+import (
+ "bytes"
+ "errors"
+ "testing"
+
+ "lindenii.org/go/furgit/object/id"
+ "lindenii.org/go/furgit/object/tree"
+)
+
+func TestParseMalformed(t *testing.T) {
+ t.Parallel()
+
+ for _, objectFormat := range id.SupportedObjectFormats() {
+ t.Run(objectFormat.String(), func(t *testing.T) {
+ t.Parallel()
+
+ size := objectFormat.Size()
+
+ record := func(mode, name string, idLen int) []byte {
+ var b bytes.Buffer
+ b.WriteString(mode)
+ b.WriteByte(' ')
+ b.WriteString(name)
+ b.WriteByte(0)
+ b.Write(make([]byte, idLen))
+
+ return b.Bytes()
+ }
+
+ for _, tc := range []struct {
+ name string
+ body []byte
+ }{
+ {name: "malformed-mode", body: record("10064x", "file", size)},
+ {name: "zero-padded-mode", body: record("0100644", "file", size)},
+ {name: "unsupported-mode", body: record("100640", "file", size)},
+ {name: "empty-name", body: record("100644", "", size)},
+ {name: "slash-name", body: record("100644", "a/b", size)},
+ {name: "truncated-id", body: record("100644", "file", size-1)},
+ {name: "missing-mode-terminator", body: []byte("100644")},
+ {name: "missing-name-terminator", body: []byte("100644 file")},
+ {name: "unsorted", body: append(record("100644", "b", size), record("100644", "a", size)...)},
+ {name: "duplicate", body: append(record("100644", "a", size), record("100644", "a", size)...)},
+ {name: "conflicting-tree-blob", body: append(record("100644", "foo", size), record("40000", "foo", size)...)},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ _, err := tree.Parse(tc.body, objectFormat)
+ if !errors.Is(err, tree.ErrInvalidTree) {
+ t.Fatalf("Parse error = %v, want ErrInvalidTree", err)
+ }
+ })
+ }
+ })
+ }
+}
diff --git a/object/tree/parse.go b/object/tree/parse.go
index 90519705..5b01fa05 100644
--- a/object/tree/parse.go
+++ b/object/tree/parse.go
@@ -41,7 +41,8 @@ func Parse(body []byte, objectFormat id.ObjectFormat) (*Tree, error) {
name := string(body[i : i+nul])
i += nul + 1
- if err := validateName(name); err != nil {
+ err = validateName(name)
+ if err != nil {
return nil, err
}
diff --git a/object/tree/parse_test.go b/object/tree/parse_test.go
new file mode 100644
index 00000000..8d32b136
--- /dev/null
+++ b/object/tree/parse_test.go
@@ -0,0 +1,44 @@
+package tree_test
+
+import (
+ "testing"
+
+ "lindenii.org/go/furgit/internal/testgit"
+ "lindenii.org/go/furgit/object/id"
+ "lindenii.org/go/furgit/object/tree"
+ "lindenii.org/go/furgit/object/typ"
+)
+
+func TestParse(t *testing.T) {
+ t.Parallel()
+
+ for _, objectFormat := range id.SupportedObjectFormats() {
+ t.Run(objectFormat.String(), func(t *testing.T) {
+ t.Parallel()
+
+ repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat})
+ if err != nil {
+ t.Fatalf("NewRepo: %v", err)
+ }
+
+ entries := mixedEntries(t, repo)
+
+ treeID, err := repo.MkTree(t, mkTreeEntries(entries))
+ if err != nil {
+ t.Fatalf("MkTree: %v", err)
+ }
+
+ rawBody, err := repo.CatFile(t, typ.TypeTree, treeID)
+ if err != nil {
+ t.Fatalf("CatFile: %v", err)
+ }
+
+ parsed, err := tree.Parse(rawBody, objectFormat)
+ if err != nil {
+ t.Fatalf("Parse: %v", err)
+ }
+
+ assertGitDecode(t, repo, treeID, parsed.Entries())
+ })
+ }
+}
diff --git a/object/tree/roundtrip_test.go b/object/tree/roundtrip_test.go
new file mode 100644
index 00000000..7fdc4140
--- /dev/null
+++ b/object/tree/roundtrip_test.go
@@ -0,0 +1,81 @@
+package tree_test
+
+import (
+ "bytes"
+ "testing"
+
+ "lindenii.org/go/furgit/internal/testgit"
+ "lindenii.org/go/furgit/object/id"
+ "lindenii.org/go/furgit/object/tree"
+ "lindenii.org/go/furgit/object/typ"
+)
+
+func TestRoundTrip(t *testing.T) {
+ t.Parallel()
+
+ for _, objectFormat := range id.SupportedObjectFormats() {
+ t.Run(objectFormat.String(), func(t *testing.T) {
+ t.Parallel()
+
+ repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat})
+ if err != nil {
+ t.Fatalf("NewRepo: %v", err)
+ }
+
+ entries := mixedEntries(t, repo)
+ tr := buildTree(t, entries)
+
+ rawBody, err := tr.AppendWithoutHeader(nil)
+ if err != nil {
+ t.Fatalf("AppendWithoutHeader: %v", err)
+ }
+
+ treeID, err := repo.HashObject(t, typ.TypeTree, bytes.NewReader(rawBody))
+ if err != nil {
+ t.Fatalf("HashObject(tree): %v", err)
+ }
+
+ err = repo.Fsck(t, testgit.FsckOptions{
+ Strict: true,
+ NoDangling: true,
+ }, treeID)
+ if err != nil {
+ t.Fatalf("Fsck: %v", err)
+ }
+
+ gitBody, err := repo.CatFile(t, typ.TypeTree, treeID)
+ if err != nil {
+ t.Fatalf("CatFile: %v", err)
+ }
+
+ parsed, err := tree.Parse(gitBody, objectFormat)
+ if err != nil {
+ t.Fatalf("Parse: %v", err)
+ }
+
+ assertEntriesEqual(t, parsed.Entries(), tr.Entries())
+ })
+ }
+}
+
+func assertEntriesEqual(t *testing.T, got []tree.Entry, want []tree.Entry) {
+ t.Helper()
+
+ if len(got) != len(want) {
+ t.Fatalf("entry count = %d, want %d", len(got), len(want))
+ }
+
+ for i := range want {
+ if got[i].Mode != want[i].Mode {
+ t.Fatalf("entry[%d] mode = %o, want %o", i, got[i].Mode, want[i].Mode)
+ }
+
+ if got[i].Name != want[i].Name {
+ t.Fatalf("entry[%d] name = %q, want %q", i, got[i].Name, want[i].Name)
+ }
+
+ if got[i].ID != want[i].ID {
+ t.Fatalf("entry[%d] id = %s, want %s", i, got[i].ID, want[i].ID)
+ }
+ }
+}