diff options
| author | 2026-04-02 06:23:30 +0000 | |
|---|---|---|
| committer | 2026-04-02 06:28:39 +0000 | |
| commit | a041d523de389b65b98a5373a8034041db2a8d83 (patch) | |
| tree | 7b423dc735f463be616045f2c3c2095a7737aca7 /object | |
| parent | research: Add dynamic pack resources (diff) | |
| signature | No signature | |
*: Remove
Diffstat (limited to 'object')
312 files changed, 0 insertions, 13837 deletions
diff --git a/object/blob/blob.go b/object/blob/blob.go deleted file mode 100644 index 93856c51..00000000 --- a/object/blob/blob.go +++ /dev/null @@ -1,14 +0,0 @@ -// Package blob provides representations, parsers, and serializers for blob objects. -package blob - -// Blob represents a Git blob object. -// -// Blob is fully materialized in memory. -// -// Consider using objectstore.Reader.ReadReaderContent, -// or appropriate streaming write APIs. -// -// Labels: MT-Unsafe. -type Blob struct { - Data []byte -} diff --git a/object/blob/parse.go b/object/blob/parse.go deleted file mode 100644 index faee9e46..00000000 --- a/object/blob/parse.go +++ /dev/null @@ -1,6 +0,0 @@ -package blob - -// Parse decodes a blob object body. -func Parse(body []byte) (*Blob, error) { - return &Blob{Data: append([]byte(nil), body...)}, nil -} diff --git a/object/blob/parse_test.go b/object/blob/parse_test.go deleted file mode 100644 index 09d5d5d0..00000000 --- a/object/blob/parse_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package blob_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/blob" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestBlobParseFromGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - body := []byte("hello\nblob\n") - blobID := testRepo.HashObject(t, "blob", body) - - rawBody := testRepo.CatFile(t, "blob", blobID) - - parsed, err := blob.Parse(rawBody) - if err != nil { - t.Fatalf("ParseBlob: %v", err) - } - - if !bytes.Equal(parsed.Data, body) { - t.Fatalf("blob body mismatch") - } - }) -} diff --git a/object/blob/serialize.go b/object/blob/serialize.go deleted file mode 100644 index 80cce8dc..00000000 --- a/object/blob/serialize.go +++ /dev/null @@ -1,32 +0,0 @@ -package blob - -import ( - "errors" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// SerializeWithoutHeader renders the raw blob body bytes. -func (blob *Blob) SerializeWithoutHeader() ([]byte, error) { - return append([]byte(nil), blob.Data...), nil -} - -// SerializeWithHeader renders the raw object (header + body). -func (blob *Blob) SerializeWithHeader() ([]byte, error) { - body, err := blob.SerializeWithoutHeader() - if err != nil { - return nil, err - } - - header, ok := objectheader.Encode(objecttype.TypeBlob, int64(len(body))) - if !ok { - return nil, errors.New("object: blob: failed to encode object header") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return raw, nil -} diff --git a/object/blob/serialize_test.go b/object/blob/serialize_test.go deleted file mode 100644 index 4292abad..00000000 --- a/object/blob/serialize_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package blob_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/blob" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestBlobSerialize(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - body := []byte("hello\nblob\n") - wantID := testRepo.HashObject(t, "blob", body) - - obj := &blob.Blob{Data: body} - - rawObj, err := obj.SerializeWithHeader() - if err != nil { - t.Fatalf("SerializeWithHeader: %v", err) - } - - gotID := algo.Sum(rawObj) - if gotID != wantID { - t.Fatalf("object id mismatch: got %s want %s", gotID, wantID) - } - }) -} diff --git a/object/blob/test.go b/object/blob/test.go deleted file mode 100644 index 9e538219..00000000 --- a/object/blob/test.go +++ /dev/null @@ -1,10 +0,0 @@ -package blob - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// ObjectType returns TypeBlob. -func (blob *Blob) ObjectType() objecttype.Type { - _ = blob - - return objecttype.TypeBlob -} diff --git a/object/commit/commit.go b/object/commit/commit.go deleted file mode 100644 index 0f7649e1..00000000 --- a/object/commit/commit.go +++ /dev/null @@ -1,25 +0,0 @@ -// Package commit provides parsed commit objects and commit serialization. -// -// It parses commits into ordinary Go values for reading and construction. It -// does not preserve the exact original byte layout needed for signature -// verification; callers that need signature-verification payload fidelity -// should use [codeberg.org/lindenii/furgit/object/signed/commit]. -package commit - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objectsignature "codeberg.org/lindenii/furgit/object/signature" -) - -// Commit represents a fully materialized Git commit object. -// -// Labels: MT-Unsafe. -type Commit struct { - Tree objectid.ObjectID - Parents []objectid.ObjectID - Author objectsignature.Signature - Committer objectsignature.Signature - Message []byte - ChangeID string - ExtraHeaders []ExtraHeader -} diff --git a/object/commit/extraheader.go b/object/commit/extraheader.go deleted file mode 100644 index 79d4f9cc..00000000 --- a/object/commit/extraheader.go +++ /dev/null @@ -1,7 +0,0 @@ -package commit - -// ExtraHeader represents an extra header in a Git object. -type ExtraHeader struct { - Key string - Value []byte -} diff --git a/object/commit/parse.go b/object/commit/parse.go deleted file mode 100644 index 9dcc930d..00000000 --- a/object/commit/parse.go +++ /dev/null @@ -1,94 +0,0 @@ -package commit - -import ( - "bytes" - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectsignature "codeberg.org/lindenii/furgit/object/signature" -) - -// Parse decodes a commit object body. -func Parse(body []byte, algo objectid.Algorithm) (*Commit, error) { - c := new(Commit) - - i := 0 - for i < len(body) { - rel := bytes.IndexByte(body[i:], '\n') - if rel < 0 { - return nil, errors.New("object: commit: missing newline") - } - - line := body[i : i+rel] - i += rel + 1 - - if len(line) == 0 { - break - } - - key, value, found := bytes.Cut(line, []byte{' '}) - if !found { - return nil, errors.New("object: commit: malformed header") - } - - switch string(key) { - case "tree": - id, err := objectid.ParseHex(algo, string(value)) - if err != nil { - return nil, fmt.Errorf("object: commit: tree: %w", err) - } - - c.Tree = id - case "parent": - id, err := objectid.ParseHex(algo, string(value)) - if err != nil { - return nil, fmt.Errorf("object: commit: parent: %w", err) - } - - c.Parents = append(c.Parents, id) - case "author": - idt, err := objectsignature.Parse(value) - if err != nil { - return nil, fmt.Errorf("object: commit: author: %w", err) - } - - c.Author = *idt - case "committer": - idt, err := objectsignature.Parse(value) - if err != nil { - return nil, fmt.Errorf("object: commit: committer: %w", err) - } - - c.Committer = *idt - case "change-id": - c.ChangeID = string(value) - case "gpgsig", "gpgsig-sha256": - for i < len(body) { - nextRel := bytes.IndexByte(body[i:], '\n') - if nextRel < 0 { - return nil, errors.New("object: commit: unterminated gpgsig") - } - - if body[i] != ' ' { - break - } - - i += nextRel + 1 - } - default: - c.ExtraHeaders = append(c.ExtraHeaders, ExtraHeader{ - Key: string(key), - Value: append([]byte(nil), value...), - }) - } - } - - if i > len(body) { - return nil, errors.New("object: commit: parser position out of bounds") - } - - c.Message = append([]byte(nil), body[i:]...) - - return c, nil -} diff --git a/object/commit/parse_test.go b/object/commit/parse_test.go deleted file mode 100644 index ad2c7aed..00000000 --- a/object/commit/parse_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package commit_test - -import ( - "bytes" - "fmt" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestCommitParseFromGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, treeID, commitID := testRepo.MakeCommit(t, "subject\n\nbody") - - rawBody := testRepo.CatFile(t, "commit", commitID) - - parsed, err := commit.Parse(rawBody, algo) - if err != nil { - t.Fatalf("ParseCommit: %v", err) - } - - if parsed.Tree != treeID { - t.Fatalf("tree id mismatch: got %s want %s", parsed.Tree, treeID) - } - - if len(parsed.Parents) != 0 { - t.Fatalf("parent count = %d, want 0", len(parsed.Parents)) - } - - if !bytes.Equal(parsed.Author.Name, []byte("Test Author")) { - t.Fatalf("author name = %q, want %q", parsed.Author.Name, "Test Author") - } - - if !bytes.Equal(parsed.Committer.Name, []byte("Test Committer")) { - t.Fatalf("committer name = %q, want %q", parsed.Committer.Name, "Test Committer") - } - - if !bytes.Contains(parsed.Message, []byte("subject")) { - t.Fatalf("commit message missing subject: %q", parsed.Message) - } - }) -} - -func TestCommitParseMultipleParents(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - - _, treeID := testRepo.MakeSingleFileTree(t, "file.txt", []byte("merge-content\n")) - parent1 := testRepo.CommitTree(t, treeID, "parent-one") - parent2 := testRepo.CommitTree(t, treeID, "parent-two", parent1) - - rawCommit := fmt.Sprintf( - "tree %s\nparent %s\nparent %s\nauthor Test Author <test@example.org> 1234567890 +0000\ncommitter Test Committer <committer@example.org> 1234567890 +0000\n\nMerge commit\n", - treeID, - parent1, - parent2, - ) - mergeID := testRepo.HashObject(t, "commit", []byte(rawCommit)) - rawBody := testRepo.CatFile(t, "commit", mergeID) - - parsed, err := commit.Parse(rawBody, algo) - if err != nil { - t.Fatalf("ParseCommit(merge): %v", err) - } - - if parsed.Tree != treeID { - t.Fatalf("merge tree = %s, want %s", parsed.Tree, treeID) - } - - if len(parsed.Parents) != 2 { - t.Fatalf("merge parent count = %d, want 2", len(parsed.Parents)) - } - - if parsed.Parents[0] != parent1 { - t.Fatalf("merge parent[0] = %s, want %s", parsed.Parents[0], parent1) - } - - if parsed.Parents[1] != parent2 { - t.Fatalf("merge parent[1] = %s, want %s", parsed.Parents[1], parent2) - } - - if !bytes.Equal(parsed.Message, []byte("Merge commit\n")) { - t.Fatalf("merge message = %q, want %q", parsed.Message, "Merge commit\n") - } - }) -} diff --git a/object/commit/serialize.go b/object/commit/serialize.go deleted file mode 100644 index 3f141550..00000000 --- a/object/commit/serialize.go +++ /dev/null @@ -1,84 +0,0 @@ -package commit - -import ( - "bytes" - "errors" - "fmt" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// SerializeWithoutHeader renders the raw commit body bytes. -func (commit *Commit) SerializeWithoutHeader() ([]byte, error) { - var buf bytes.Buffer - - if commit.Tree.Algorithm().Size() == 0 { - return nil, errors.New("object: commit: missing tree id") - } - - fmt.Fprintf(&buf, "tree %s\n", commit.Tree.String()) - - for _, parent := range commit.Parents { - fmt.Fprintf(&buf, "parent %s\n", parent.String()) - } - - authorBytes, err := commit.Author.Serialize() - if err != nil { - return nil, err - } - - buf.WriteString("author ") - buf.Write(authorBytes) - buf.WriteByte('\n') - - committerBytes, err := commit.Committer.Serialize() - if err != nil { - return nil, err - } - - buf.WriteString("committer ") - buf.Write(committerBytes) - buf.WriteByte('\n') - - if commit.ChangeID != "" { - buf.WriteString("change-id ") - buf.WriteString(commit.ChangeID) - buf.WriteByte('\n') - } - - for _, h := range commit.ExtraHeaders { - if h.Key == "" { - return nil, errors.New("object: commit: extra header has empty key") - } - - buf.WriteString(h.Key) - buf.WriteByte(' ') - buf.Write(h.Value) - buf.WriteByte('\n') - } - - buf.WriteByte('\n') - buf.Write(commit.Message) - - return buf.Bytes(), nil -} - -// SerializeWithHeader renders the raw object (header + body). -func (commit *Commit) SerializeWithHeader() ([]byte, error) { - body, err := commit.SerializeWithoutHeader() - if err != nil { - return nil, err - } - - header, ok := objectheader.Encode(objecttype.TypeCommit, int64(len(body))) - if !ok { - return nil, errors.New("object: commit: failed to encode object header") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return raw, nil -} diff --git a/object/commit/serialize_test.go b/object/commit/serialize_test.go deleted file mode 100644 index e58a8078..00000000 --- a/object/commit/serialize_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package commit_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestCommitSerialize(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, commitID := testRepo.MakeCommit(t, "subject\n\nbody") - - rawBody := testRepo.CatFile(t, "commit", commitID) - - parsed, err := commit.Parse(rawBody, algo) - if err != nil { - t.Fatalf("ParseCommit: %v", err) - } - - rawObj, err := parsed.SerializeWithHeader() - if err != nil { - t.Fatalf("SerializeWithHeader: %v", err) - } - - gotID := algo.Sum(rawObj) - if gotID != commitID { - t.Fatalf("commit id mismatch: got %s want %s", gotID, commitID) - } - }) -} diff --git a/object/commit/type.go b/object/commit/type.go deleted file mode 100644 index b8aa11e8..00000000 --- a/object/commit/type.go +++ /dev/null @@ -1,10 +0,0 @@ -package commit - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// ObjectType returns TypeCommit. -func (commit *Commit) ObjectType() objecttype.Type { - _ = commit - - return objecttype.TypeCommit -} diff --git a/object/doc.go b/object/doc.go deleted file mode 100644 index f675b963..00000000 --- a/object/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package object provides the shared [Object] interface and parsing functions -// for Git object values. -// -// Concrete object forms such as [blob], [tree], [commit], and [tag] live in -// subpackages. Use [codeberg.org/lindenii/furgit/object/stored] when object -// values need to be paired with the object IDs they were loaded under. -package object diff --git a/object/fetch/doc.go b/object/fetch/doc.go deleted file mode 100644 index 89bf9a98..00000000 --- a/object/fetch/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -// Package fetch loads typed Git objects from object storage and provides -// higher-level object queries. -// -// Fetching is above [objectstore]: it parses stored objects into blobs, trees, -// commits, and tags, exposes object metadata, peels tree-ish or commit-ish -// objects, resolves paths within trees, and can expose one tree as an [io/fs] -// view. -package fetch diff --git a/object/fetch/exact_blob.go b/object/fetch/exact_blob.go deleted file mode 100644 index ef4b84fe..00000000 --- a/object/fetch/exact_blob.go +++ /dev/null @@ -1,26 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/object/blob" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ExactBlob reads, parses, and wraps the blob at id. -// -// Labels: Life-Parent. -func (r *Fetcher) ExactBlob(id objectid.ObjectID) (*stored.Stored[*blob.Blob], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - blob, ok := parsed.(*blob.Blob) - if !ok { - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeBlob} - } - - return stored.New(id, blob), nil -} diff --git a/object/fetch/exact_blob_reader.go b/object/fetch/exact_blob_reader.go deleted file mode 100644 index 4a313d3e..00000000 --- a/object/fetch/exact_blob_reader.go +++ /dev/null @@ -1,16 +0,0 @@ -package fetch - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ExactBlobReader returns a reader for the content of the blob at id, -// together with its content size in bytes. -// -// Labels: Life-Parent, Close-Caller. -func (r *Fetcher) ExactBlobReader(id objectid.ObjectID) (io.ReadCloser, int64, error) { - return r.exactReader(id, objecttype.TypeBlob) -} diff --git a/object/fetch/exact_commit.go b/object/fetch/exact_commit.go deleted file mode 100644 index 9483b2b1..00000000 --- a/object/fetch/exact_commit.go +++ /dev/null @@ -1,26 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ExactCommit reads, parses, and wraps the commit at id. -// -// Labels: Life-Parent. -func (r *Fetcher) ExactCommit(id objectid.ObjectID) (*stored.Stored[*commit.Commit], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - commit, ok := parsed.(*commit.Commit) - if !ok { - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeCommit} - } - - return stored.New(id, commit), nil -} diff --git a/object/fetch/exact_object.go b/object/fetch/exact_object.go deleted file mode 100644 index 2e4a8217..00000000 --- a/object/fetch/exact_object.go +++ /dev/null @@ -1,20 +0,0 @@ -package fetch - -import ( - "codeberg.org/lindenii/furgit/object" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" -) - -// ExactObject reads, parses, and wraps the object at id without constraining -// its concrete object kind. -// -// Labels: Life-Parent. -func (r *Fetcher) ExactObject(id objectid.ObjectID) (*stored.Stored[object.Object], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - return stored.New(id, parsed), nil -} diff --git a/object/fetch/exact_reader.go b/object/fetch/exact_reader.go deleted file mode 100644 index d588480d..00000000 --- a/object/fetch/exact_reader.go +++ /dev/null @@ -1,26 +0,0 @@ -package fetch - -import ( - "io" - - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// exactReader reads one object's content stream and verifies that its header -// type matches wantType. -func (r *Fetcher) exactReader(id objectid.ObjectID, wantType objecttype.Type) (io.ReadCloser, int64, error) { - gotType, size, rc, err := r.store.ReadReaderContent(id) - if err != nil { - return nil, 0, wrapObjectReadError(id, err) - } - - if gotType != wantType { - _ = rc.Close() - - return nil, 0, &giterrors.ObjectTypeError{OID: id, Got: gotType, Want: wantType} - } - - return rc, size, nil -} diff --git a/object/fetch/exact_tag.go b/object/fetch/exact_tag.go deleted file mode 100644 index 230e7d57..00000000 --- a/object/fetch/exact_tag.go +++ /dev/null @@ -1,26 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ExactTag reads, parses, and wraps the tag at id. -// -// Labels: Life-Parent. -func (r *Fetcher) ExactTag(id objectid.ObjectID) (*stored.Stored[*tag.Tag], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - tag, ok := parsed.(*tag.Tag) - if !ok { - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeTag} - } - - return stored.New(id, tag), nil -} diff --git a/object/fetch/exact_tree.go b/object/fetch/exact_tree.go deleted file mode 100644 index 8bfc87ea..00000000 --- a/object/fetch/exact_tree.go +++ /dev/null @@ -1,26 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tree" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ExactTree reads, parses, and wraps the tree at id. -// -// Labels: Life-Parent. -func (r *Fetcher) ExactTree(id objectid.ObjectID) (*stored.Stored[*tree.Tree], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - tree, ok := parsed.(*tree.Tree) - if !ok { - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeTree} - } - - return stored.New(id, tree), nil -} diff --git a/object/fetch/fetcher.go b/object/fetch/fetcher.go deleted file mode 100644 index fcd64d88..00000000 --- a/object/fetch/fetcher.go +++ /dev/null @@ -1,20 +0,0 @@ -package fetch - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -// Fetcher provides ordinary object access above an object store. -// -// It exposes object metadata, typed object loading, tree-ish and commit-ish -// peeling, path resolution, one-tree fs views, and blob content streaming. -// -// Labels: MT-Safe. -type Fetcher struct { - store objectstore.Reader -} - -// New returns a Fetcher that reads objects from store. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(store objectstore.Reader) *Fetcher { - return &Fetcher{store: store} -} diff --git a/object/fetch/header.go b/object/fetch/header.go deleted file mode 100644 index 0a535dd9..00000000 --- a/object/fetch/header.go +++ /dev/null @@ -1,18 +0,0 @@ -package fetch - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// Header returns the object type and content size at id. -// -// Labels: Life-Parent. -func (r *Fetcher) Header(id objectid.ObjectID) (objecttype.Type, int64, error) { - ty, size, err := r.store.ReadHeader(id) - if err != nil { - return objecttype.TypeInvalid, 0, wrapObjectReadError(id, err) - } - - return ty, size, nil -} diff --git a/object/fetch/object_errors.go b/object/fetch/object_errors.go deleted file mode 100644 index 08de6f75..00000000 --- a/object/fetch/object_errors.go +++ /dev/null @@ -1,19 +0,0 @@ -package fetch - -import ( - stderrors "errors" - - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// wrapObjectReadError maps raw object-store lookup failures to fetcher-level -// object lookup errors. -func wrapObjectReadError(id objectid.ObjectID, err error) error { - if stderrors.Is(err, objectstore.ErrObjectNotFound) { - return &giterrors.ObjectMissingError{OID: id} - } - - return err -} diff --git a/object/fetch/object_parse.go b/object/fetch/object_parse.go deleted file mode 100644 index 0a61bb3d..00000000 --- a/object/fetch/object_parse.go +++ /dev/null @@ -1,27 +0,0 @@ -package fetch - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/object" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func (r *Fetcher) parseObject(id objectid.ObjectID) (object.Object, error) { - ty, content, err := r.store.ReadBytesContent(id) - if err != nil { - return nil, wrapObjectReadError(id, err) - } - - parsed, err := object.ParseWithoutHeader(ty, content, id.Algorithm()) - if err != nil { - tyName, ok := ty.Name() - if !ok { - tyName = fmt.Sprintf("type %d", ty) - } - - return nil, fmt.Errorf("object/fetch: parse object %s (%s): %w", id, tyName, err) - } - - return parsed, nil -} diff --git a/object/fetch/path.go b/object/fetch/path.go deleted file mode 100644 index e3c468db..00000000 --- a/object/fetch/path.go +++ /dev/null @@ -1,105 +0,0 @@ -package fetch - -import ( - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -// PathEmptyError indicates that Path received no segments. -type PathEmptyError struct{} - -func (err *PathEmptyError) Error() string { - return "object/fetch: empty tree path" -} - -// PathSegmentEmptyError indicates that one path segment is empty. -type PathSegmentEmptyError struct { - Index int -} - -func (err *PathSegmentEmptyError) Error() string { - return fmt.Sprintf("object/fetch: empty tree path segment at index %d", err.Index) -} - -// PathNotFoundError indicates that one tree path segment was not found. -type PathNotFoundError struct { - Index int - Name []byte -} - -func (err *PathNotFoundError) Error() string { - return fmt.Sprintf("object/fetch: tree entry %q not found at index %d", err.Name, err.Index) -} - -// PathNotTreeError indicates that one intermediate path segment was not a tree. -type PathNotTreeError struct { - Index int - Name []byte -} - -func (err *PathNotTreeError) Error() string { - return fmt.Sprintf("object/fetch: path segment %q at index %d is not a tree", err.Name, err.Index) -} - -// Path resolves parts within the tree identified by root and returns the final -// tree entry. -// -// The root object may be any tree-ish object accepted by PeelToTree. -// -// parts must contain at least one path segment. Intermediate path segments -// must resolve to tree entries. The final entry is returned without loading -// its object. Path segments may not contain \x00. -// -// The path cannot be accurately represented as a string or a single []byte -// because Git tree entry names may include slashes. While []string is -// technically possible (since Go strings are not necessarily UTF-8), they -// do often imply UTF-8 in practice, which would be undesirable. -// -// If your entry names are valid UTF-8 and uses / solely as segment separators, -// it may be convenient to use TreeFS for an io/fs.FS-like interface. -// -// Labels: Life-Parent. -func (r *Fetcher) Path(root objectid.ObjectID, parts [][]byte) (tree.TreeEntry, error) { - if len(parts) == 0 { - return tree.TreeEntry{}, &PathEmptyError{} - } - - current, err := r.PeelToTree(root) - if err != nil { - return tree.TreeEntry{}, err - } - - for i, part := range parts { - if len(part) == 0 { - return tree.TreeEntry{}, &PathSegmentEmptyError{Index: i} - } - - entry := current.Object().Entry(part) - if entry == nil { - return tree.TreeEntry{}, &PathNotFoundError{ - Index: i, - Name: append([]byte(nil), part...), - } - } - - if i == len(parts)-1 { - return *entry, nil - } - - if entry.Mode != tree.FileModeDir { - return tree.TreeEntry{}, &PathNotTreeError{ - Index: i, - Name: append([]byte(nil), part...), - } - } - - current, err = r.ExactTree(entry.ID) - if err != nil { - return tree.TreeEntry{}, err - } - } - - return tree.TreeEntry{}, &PathNotFoundError{Index: len(parts) - 1} -} diff --git a/object/fetch/peel_to_blob.go b/object/fetch/peel_to_blob.go deleted file mode 100644 index adf86495..00000000 --- a/object/fetch/peel_to_blob.go +++ /dev/null @@ -1,31 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/object/blob" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// PeelToBlob peels tags until it reaches a blob. -// -// Labels: Life-Parent. -func (r *Fetcher) PeelToBlob(id objectid.ObjectID) (*stored.Stored[*blob.Blob], error) { - for { - obj, err := r.ExactObject(id) - if err != nil { - return nil, err - } - - switch parsed := obj.Object().(type) { - case *blob.Blob: - return stored.New(id, parsed), nil - case *tag.Tag: - id = parsed.Target - default: - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeBlob} - } - } -} diff --git a/object/fetch/peel_to_blob_id.go b/object/fetch/peel_to_blob_id.go deleted file mode 100644 index 7a43b4cc..00000000 --- a/object/fetch/peel_to_blob_id.go +++ /dev/null @@ -1,38 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// PeelToBlobID peels tags until it reaches a blob object ID. -func (r *Fetcher) PeelToBlobID(id objectid.ObjectID) (objectid.ObjectID, error) { - for { - ty, _, err := r.Header(id) - if err != nil { - return objectid.ObjectID{}, err - } - - switch ty { - case objecttype.TypeBlob: - return id, nil - case objecttype.TypeTag: - tag, err := r.ExactTag(id) - if err != nil { - return objectid.ObjectID{}, err - } - - id = tag.Object().Target - case objecttype.TypeInvalid, - objecttype.TypeCommit, - objecttype.TypeTree, - objecttype.TypeFuture, - objecttype.TypeOfsDelta, - objecttype.TypeRefDelta: - return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeBlob} - default: - return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeBlob} - } - } -} diff --git a/object/fetch/peel_to_blob_reader.go b/object/fetch/peel_to_blob_reader.go deleted file mode 100644 index dedffd01..00000000 --- a/object/fetch/peel_to_blob_reader.go +++ /dev/null @@ -1,20 +0,0 @@ -package fetch - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// PeelToBlobReader returns a reader for the content of the peeled blob at id, -// together with its content size in bytes. -// -// Labels: Life-Parent, Close-Caller. -func (r *Fetcher) PeelToBlobReader(id objectid.ObjectID) (io.ReadCloser, int64, error) { - blobID, err := r.PeelToBlobID(id) - if err != nil { - return nil, 0, err - } - - return r.ExactBlobReader(blobID) -} diff --git a/object/fetch/peel_to_commit.go b/object/fetch/peel_to_commit.go deleted file mode 100644 index e5fdce2b..00000000 --- a/object/fetch/peel_to_commit.go +++ /dev/null @@ -1,31 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// PeelToCommit peels tags until it reaches a commit. -// -// Labels: Life-Parent. -func (r *Fetcher) PeelToCommit(id objectid.ObjectID) (*stored.Stored[*commit.Commit], error) { - for { - obj, err := r.ExactObject(id) - if err != nil { - return nil, err - } - - switch parsed := obj.Object().(type) { - case *commit.Commit: - return stored.New(id, parsed), nil - case *tag.Tag: - id = parsed.Target - default: - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeCommit} - } - } -} diff --git a/object/fetch/peel_to_commit_id.go b/object/fetch/peel_to_commit_id.go deleted file mode 100644 index 7b58bdea..00000000 --- a/object/fetch/peel_to_commit_id.go +++ /dev/null @@ -1,38 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// PeelToCommitID peels tags until it reaches a commit object ID. -func (r *Fetcher) PeelToCommitID(id objectid.ObjectID) (objectid.ObjectID, error) { - for { - ty, _, err := r.Header(id) - if err != nil { - return objectid.ObjectID{}, err - } - - switch ty { - case objecttype.TypeCommit: - return id, nil - case objecttype.TypeTag: - tag, err := r.ExactTag(id) - if err != nil { - return objectid.ObjectID{}, err - } - - id = tag.Object().Target - case objecttype.TypeInvalid, - objecttype.TypeTree, - objecttype.TypeBlob, - objecttype.TypeFuture, - objecttype.TypeOfsDelta, - objecttype.TypeRefDelta: - return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeCommit} - default: - return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeCommit} - } - } -} diff --git a/object/fetch/peel_to_tree.go b/object/fetch/peel_to_tree.go deleted file mode 100644 index adc87e6b..00000000 --- a/object/fetch/peel_to_tree.go +++ /dev/null @@ -1,35 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" - "codeberg.org/lindenii/furgit/object/tree" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// PeelToTree peels tags until it reaches a tree or commit. If it reaches a -// commit, it returns the commit's root tree. -// -// Labels: Life-Parent. -func (r *Fetcher) PeelToTree(id objectid.ObjectID) (*stored.Stored[*tree.Tree], error) { - for { - obj, err := r.ExactObject(id) - if err != nil { - return nil, err - } - - switch parsed := obj.Object().(type) { - case *tree.Tree: - return stored.New(id, parsed), nil - case *commit.Commit: - return r.ExactTree(parsed.Tree) - case *tag.Tag: - id = parsed.Target - default: - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeTree} - } - } -} diff --git a/object/fetch/peel_to_tree_id.go b/object/fetch/peel_to_tree_id.go deleted file mode 100644 index 4c9bdac9..00000000 --- a/object/fetch/peel_to_tree_id.go +++ /dev/null @@ -1,45 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// PeelToTreeID peels tags until it reaches a tree object ID, or a commit whose -// root tree object ID is then returned. -func (r *Fetcher) PeelToTreeID(id objectid.ObjectID) (objectid.ObjectID, error) { - for { - ty, _, err := r.Header(id) - if err != nil { - return objectid.ObjectID{}, err - } - - switch ty { - case objecttype.TypeTree: - return id, nil - case objecttype.TypeCommit: - commit, err := r.ExactCommit(id) - if err != nil { - return objectid.ObjectID{}, err - } - - return commit.Object().Tree, nil - case objecttype.TypeTag: - tag, err := r.ExactTag(id) - if err != nil { - return objectid.ObjectID{}, err - } - - id = tag.Object().Target - case objecttype.TypeInvalid, - objecttype.TypeBlob, - objecttype.TypeFuture, - objecttype.TypeOfsDelta, - objecttype.TypeRefDelta: - return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeTree} - default: - return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeTree} - } - } -} diff --git a/object/fetch/size.go b/object/fetch/size.go deleted file mode 100644 index da59e12a..00000000 --- a/object/fetch/size.go +++ /dev/null @@ -1,15 +0,0 @@ -package fetch - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Size returns the object content size at id. -// -// Labels: Life-Parent. -func (r *Fetcher) Size(id objectid.ObjectID) (int64, error) { - size, err := r.store.ReadSize(id) - if err != nil { - return 0, wrapObjectReadError(id, err) - } - - return size, nil -} diff --git a/object/fetch/treefs.go b/object/fetch/treefs.go deleted file mode 100644 index 39ea7ad5..00000000 --- a/object/fetch/treefs.go +++ /dev/null @@ -1,30 +0,0 @@ -package fetch - -import ( - "io/fs" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -// TreeFS exposes one Git tree as an fs.FS view backed by a Fetcher. -// -// TreeFS interprets names using io/fs path rules. Those rules do not match raw -// Git tree entry naming exactly: names are UTF-8, slash-separated, and must be -// valid fs.FS paths. Tree entries that cannot be represented under those rules -// are not addressable through this API. -// -// Labels: MT-Safe. -type TreeFS struct { - fetcher *Fetcher - rootTree objectid.ObjectID - rootEntry *tree.TreeEntry -} - -var ( - _ fs.FS = (*TreeFS)(nil) - _ fs.ReadFileFS = (*TreeFS)(nil) - _ fs.ReadDirFS = (*TreeFS)(nil) - _ fs.StatFS = (*TreeFS)(nil) - _ fs.SubFS = (*TreeFS)(nil) -) diff --git a/object/fetch/treefs_entry.go b/object/fetch/treefs_entry.go deleted file mode 100644 index e577d86c..00000000 --- a/object/fetch/treefs_entry.go +++ /dev/null @@ -1,85 +0,0 @@ -package fetch - -import ( - "errors" - "fmt" - "io/fs" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -func (treeFS *TreeFS) resolvePath(op treeFSOp, name string) (treeEntryValue, error) { - if !treeFSValidPath(name) { - return treeEntryValue{}, treeFSPathError(op, name, fs.ErrInvalid) - } - - if name == "." { - return treeEntryValue{ - name: ".", - mode: tree.FileModeDir, - treeID: treeFS.rootTree, - treeEntry: treeFS.rootEntry, - }, nil - } - - entry, err := treeFS.fetcher.Path(treeFS.rootTree, tree.SplitPath([]byte(name))) - if err != nil { - return treeEntryValue{}, treeFS.pathResolveError(op, name, err) - } - - return treeEntryValue{ - name: string(entry.Name), - mode: entry.Mode, - objectID: entry.ID, - treeEntry: &entry, - }, nil -} - -func (treeFS *TreeFS) pathResolveError(op treeFSOp, name string, err error) error { - if _, ok := errors.AsType[*PathNotFoundError](err); ok { - return treeFSPathError(op, name, fs.ErrNotExist) - } - - if _, ok := errors.AsType[*PathNotTreeError](err); ok { - return treeFSPathError(op, name, fs.ErrInvalid) - } - - if _, ok := errors.AsType[*PathEmptyError](err); ok { - return treeFSPathError(op, name, fs.ErrInvalid) - } - - if _, ok := errors.AsType[*PathSegmentEmptyError](err); ok { - return treeFSPathError(op, name, fs.ErrInvalid) - } - - return treeFSPathError(op, name, err) -} - -type treeEntryValue struct { - name string - mode tree.FileMode - objectID objectid.ObjectID - treeID objectid.ObjectID - treeEntry *tree.TreeEntry -} - -func (entry treeEntryValue) isDir() bool { - return entry.mode == tree.FileModeDir -} - -func (entry treeEntryValue) blobSize(fetcher *Fetcher) (int64, error) { - return fetcher.Size(entry.objectID) -} - -func (entry treeEntryValue) subtreeID() (objectid.ObjectID, error) { - if entry.name == "." { - return entry.treeID, nil - } - - if entry.mode != tree.FileModeDir { - return objectid.ObjectID{}, fmt.Errorf("object/fetch: path %q is not a tree", entry.name) - } - - return entry.objectID, nil -} diff --git a/object/fetch/treefs_info.go b/object/fetch/treefs_info.go deleted file mode 100644 index f1db7e9a..00000000 --- a/object/fetch/treefs_info.go +++ /dev/null @@ -1,75 +0,0 @@ -package fetch - -import ( - "io/fs" - "time" - - "codeberg.org/lindenii/furgit/object/tree" -) - -type treeFSInfo struct { - name string - mode fs.FileMode - size int64 - sys any - isDir bool -} - -var ( - _ fs.FileInfo = (*treeFSInfo)(nil) - _ fs.DirEntry = (*treeFSInfo)(nil) -) - -func (info *treeFSInfo) Name() string { return info.name } -func (info *treeFSInfo) Size() int64 { return info.size } -func (info *treeFSInfo) Mode() fs.FileMode { return info.mode } -func (info *treeFSInfo) Type() fs.FileMode { return info.mode.Type() } -func (info *treeFSInfo) IsDir() bool { return info.isDir } -func (info *treeFSInfo) ModTime() time.Time { return time.Time{} } -func (info *treeFSInfo) Sys() any { return info.sys } -func (info *treeFSInfo) Info() (fs.FileInfo, error) { - return info, nil -} - -func treeFSEntryMode(mode tree.FileMode) fs.FileMode { - switch mode { - case tree.FileModeDir: - return fs.ModeDir | 0o555 - case tree.FileModeRegular: - return 0o444 - case tree.FileModeExecutable: - return 0o555 - case tree.FileModeSymlink: - return fs.ModeSymlink | 0o444 - case tree.FileModeGitlink: - return fs.ModeIrregular - default: - return fs.ModeIrregular - } -} - -func (treeFS *TreeFS) statEntry(entry treeEntryValue) (*treeFSInfo, error) { - size := int64(0) - - if entry.mode == tree.FileModeRegular || entry.mode == tree.FileModeExecutable || entry.mode == tree.FileModeSymlink { - var err error - - size, err = entry.blobSize(treeFS.fetcher) - if err != nil { - return nil, err - } - } - - var sys any - if entry.treeEntry != nil { - sys = *entry.treeEntry - } - - return &treeFSInfo{ - name: entry.name, - mode: treeFSEntryMode(entry.mode), - size: size, - sys: sys, - isDir: entry.isDir(), - }, nil -} diff --git a/object/fetch/treefs_new.go b/object/fetch/treefs_new.go deleted file mode 100644 index f1096a3c..00000000 --- a/object/fetch/treefs_new.go +++ /dev/null @@ -1,19 +0,0 @@ -package fetch - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// TreeFS returns a new filesystem view rooted at root, which may be any -// tree-ish object accepted by PeelToTreeID. -// -// Labels: Deps-Borrowed, Life-Parent. -func (r *Fetcher) TreeFS(root objectid.ObjectID) (*TreeFS, error) { - rootTree, err := r.PeelToTreeID(root) - if err != nil { - return nil, err - } - - return &TreeFS{ - fetcher: r, - rootTree: rootTree, - }, nil -} diff --git a/object/fetch/treefs_op.go b/object/fetch/treefs_op.go deleted file mode 100644 index f0472923..00000000 --- a/object/fetch/treefs_op.go +++ /dev/null @@ -1,28 +0,0 @@ -package fetch - -type treeFSOp uint8 - -const ( - treeFSOpOpen treeFSOp = iota - treeFSOpReadFile - treeFSOpReadDir - treeFSOpStat - treeFSOpSub -) - -func (op treeFSOp) pathErrorOp() string { - switch op { - case treeFSOpOpen: - return "open" - case treeFSOpReadFile: - return "readfile" - case treeFSOpReadDir: - return "readdir" - case treeFSOpStat: - return "stat" - case treeFSOpSub: - return "sub" - default: - return "treefs" - } -} diff --git a/object/fetch/treefs_open.go b/object/fetch/treefs_open.go deleted file mode 100644 index fc0f7635..00000000 --- a/object/fetch/treefs_open.go +++ /dev/null @@ -1,122 +0,0 @@ -package fetch - -import ( - "fmt" - "io" - "io/fs" - - "codeberg.org/lindenii/furgit/object/tree" -) - -// Open opens name for reading. -// -// Directories are returned as fs.ReadDirFile values. Gitlink entries are not -// readable through TreeFS. -func (treeFS *TreeFS) Open(name string) (fs.File, error) { - entry, err := treeFS.resolvePath(treeFSOpOpen, name) - if err != nil { - return nil, err - } - - info, err := treeFS.statEntry(entry) - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - if entry.isDir() { - treeID, err := entry.subtreeID() - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - tree, err := treeFS.fetcher.ExactTree(treeID) - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - entries := make([]fs.DirEntry, 0, len(tree.Object().Entries)) - for _, child := range tree.Object().Entries { - childEntry := treeEntryValue{ - name: string(child.Name), - mode: child.Mode, - objectID: child.ID, - treeEntry: &child, - } - - childInfo, err := treeFS.statEntry(childEntry) - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - entries = append(entries, childInfo) - } - - return &treeFSDir{ - info: info, - entries: entries, - }, nil - } - - if entry.mode == tree.FileModeGitlink { - return nil, treeFSPathError(treeFSOpOpen, name, fmt.Errorf("object/fetch: gitlink entries are not readable as files")) - } - - reader, _, err := treeFS.fetcher.ExactBlobReader(entry.objectID) - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - return &treeFSBlob{ - info: info, - reader: reader, - }, nil -} - -type treeFSBlob struct { - info *treeFSInfo - reader io.ReadCloser -} - -var _ fs.File = (*treeFSBlob)(nil) - -func (file *treeFSBlob) Stat() (fs.FileInfo, error) { return file.info, nil } -func (file *treeFSBlob) Read(p []byte) (int, error) { return file.reader.Read(p) } -func (file *treeFSBlob) Close() error { return file.reader.Close() } - -type treeFSDir struct { - info *treeFSInfo - entries []fs.DirEntry - offset int -} - -var ( - _ fs.File = (*treeFSDir)(nil) - _ fs.ReadDirFile = (*treeFSDir)(nil) -) - -func (dir *treeFSDir) Stat() (fs.FileInfo, error) { return dir.info, nil } -func (dir *treeFSDir) Close() error { return nil } - -func (dir *treeFSDir) Read(_ []byte) (int, error) { - return 0, fs.ErrInvalid -} - -func (dir *treeFSDir) ReadDir(n int) ([]fs.DirEntry, error) { - if dir.offset >= len(dir.entries) && n > 0 { - return nil, io.EOF - } - - if n <= 0 { - out := append([]fs.DirEntry(nil), dir.entries[dir.offset:]...) - dir.offset = len(dir.entries) - - return out, nil - } - - end := min(dir.offset+n, len(dir.entries)) - - out := append([]fs.DirEntry(nil), dir.entries[dir.offset:end]...) - dir.offset = end - - return out, nil -} diff --git a/object/fetch/treefs_path.go b/object/fetch/treefs_path.go deleted file mode 100644 index a2dc3155..00000000 --- a/object/fetch/treefs_path.go +++ /dev/null @@ -1,11 +0,0 @@ -package fetch - -import "io/fs" - -func treeFSValidPath(name string) bool { - return name == "." || fs.ValidPath(name) -} - -func treeFSPathError(op treeFSOp, path string, err error) error { - return &fs.PathError{Op: op.pathErrorOp(), Path: path, Err: err} -} diff --git a/object/fetch/treefs_readdir.go b/object/fetch/treefs_readdir.go deleted file mode 100644 index 7518c607..00000000 --- a/object/fetch/treefs_readdir.go +++ /dev/null @@ -1,20 +0,0 @@ -package fetch - -import "io/fs" - -// ReadDir reads and returns all directory entries for name. -func (treeFS *TreeFS) ReadDir(name string) ([]fs.DirEntry, error) { - file, err := treeFS.Open(name) - if err != nil { - return nil, err - } - - defer func() { _ = file.Close() }() - - readDirFile, ok := file.(fs.ReadDirFile) - if !ok { - return nil, treeFSPathError(treeFSOpReadDir, name, fs.ErrInvalid) - } - - return readDirFile.ReadDir(-1) -} diff --git a/object/fetch/treefs_readfile.go b/object/fetch/treefs_readfile.go deleted file mode 100644 index b248135f..00000000 --- a/object/fetch/treefs_readfile.go +++ /dev/null @@ -1,40 +0,0 @@ -package fetch - -import ( - "fmt" - "io" - - "codeberg.org/lindenii/furgit/object/tree" -) - -// ReadFile reads the blob contents at name. -// -// Directories and gitlink entries are not readable through TreeFS. -func (treeFS *TreeFS) ReadFile(name string) ([]byte, error) { - entry, err := treeFS.resolvePath(treeFSOpReadFile, name) - if err != nil { - return nil, err - } - - if entry.isDir() { - return nil, treeFSPathError(treeFSOpReadFile, name, fmt.Errorf("is a directory")) - } - - if entry.mode == tree.FileModeGitlink { - return nil, treeFSPathError(treeFSOpReadFile, name, fmt.Errorf("object/fetch: gitlink entries are not readable as files")) - } - - reader, _, err := treeFS.fetcher.ExactBlobReader(entry.objectID) - if err != nil { - return nil, treeFSPathError(treeFSOpReadFile, name, err) - } - - defer func() { _ = reader.Close() }() - - data, err := io.ReadAll(reader) - if err != nil { - return nil, treeFSPathError(treeFSOpReadFile, name, err) - } - - return data, nil -} diff --git a/object/fetch/treefs_stat.go b/object/fetch/treefs_stat.go deleted file mode 100644 index 7d7a6418..00000000 --- a/object/fetch/treefs_stat.go +++ /dev/null @@ -1,22 +0,0 @@ -package fetch - -import "io/fs" - -// Stat returns synthetic file metadata for name. -// -// TreeFS metadata reflects Git tree entry mode and blob size where applicable. -// It does not represent filesystem stat metadata: ModTime is zero, ownership is -// unavailable, and Sys returns the underlying tree.TreeEntry when one exists. -func (treeFS *TreeFS) Stat(name string) (fs.FileInfo, error) { - entry, err := treeFS.resolvePath(treeFSOpStat, name) - if err != nil { - return nil, err - } - - info, err := treeFS.statEntry(entry) - if err != nil { - return nil, treeFSPathError(treeFSOpStat, name, err) - } - - return info, nil -} diff --git a/object/fetch/treefs_sub.go b/object/fetch/treefs_sub.go deleted file mode 100644 index c303d16d..00000000 --- a/object/fetch/treefs_sub.go +++ /dev/null @@ -1,22 +0,0 @@ -package fetch - -import "io/fs" - -// Sub returns a new TreeFS rooted at dir. -func (treeFS *TreeFS) Sub(dir string) (fs.FS, error) { - entry, err := treeFS.resolvePath(treeFSOpSub, dir) - if err != nil { - return nil, err - } - - treeID, err := entry.subtreeID() - if err != nil { - return nil, treeFSPathError(treeFSOpSub, dir, fs.ErrInvalid) - } - - return &TreeFS{ - fetcher: treeFS.fetcher, - rootTree: treeID, - rootEntry: entry.treeEntry, - }, nil -} diff --git a/object/fetch/treefs_test.go b/object/fetch/treefs_test.go deleted file mode 100644 index ba5d4127..00000000 --- a/object/fetch/treefs_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package fetch_test - -import ( - "errors" - "io/fs" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/fetch" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" - "codeberg.org/lindenii/furgit/repository" -) - -func TestTreeFS(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - repoData := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - repoData.WriteFile(t, "plain.txt", []byte("plain\n"), 0o644) - repoData.WriteFileAll(t, "dir/exec.sh", []byte("#!/bin/sh\nexit 0\n"), 0o755, 0o755) - repoData.SymbolicRef(t, "HEAD", "refs/heads/main") - _ = repoData.Run(t, "add", ".") - treeHex := repoData.Run(t, "write-tree") - - treeID, err := objectid.ParseHex(algo, treeHex) - if err != nil { - t.Fatalf("ParseHex(write-tree): %v", err) - } - - commitID := repoData.CommitTree(t, treeID, "treefs") - - root := repoData.OpenGitRoot(t) - - repo, err := repository.Open(root) - if err != nil { - t.Fatalf("repository.Open: %v", err) - } - - defer func() { _ = repo.Close() }() - - fetcher := fetch.New(repo.Objects()) - - treeFS, err := fetcher.TreeFS(commitID) - if err != nil { - t.Fatalf("fetcher.TreeFS: %v", err) - } - - content, err := treeFS.ReadFile("plain.txt") - if err != nil { - t.Fatalf("ReadFile(plain.txt): %v", err) - } - - if string(content) != "plain\n" { - t.Fatalf("ReadFile(plain.txt) = %q, want %q", string(content), "plain\n") - } - - entries, err := treeFS.ReadDir(".") - if err != nil { - t.Fatalf("ReadDir(.): %v", err) - } - - if len(entries) != 2 { - t.Fatalf("len(ReadDir(.)) = %d, want 2", len(entries)) - } - - info, err := treeFS.Stat("plain.txt") - if err != nil { - t.Fatalf("Stat(plain.txt): %v", err) - } - - entry, ok := info.Sys().(tree.TreeEntry) - if !ok { - t.Fatalf("Stat(plain.txt).Sys() type = %T, want tree.TreeEntry", info.Sys()) - } - - if entry.Mode != tree.FileModeRegular { - t.Fatalf("Stat(plain.txt).Sys().Mode = %o, want %o", entry.Mode, tree.FileModeRegular) - } - - subFS, err := treeFS.Sub("dir") - if err != nil { - t.Fatalf("Sub(dir): %v", err) - } - - subReadFileFS, ok := subFS.(fs.ReadFileFS) - if !ok { - t.Fatalf("Sub(dir) type does not implement fs.ReadFileFS") - } - - subContent, err := subReadFileFS.ReadFile("exec.sh") - if err != nil { - t.Fatalf("Sub(dir).ReadFile(exec.sh): %v", err) - } - - if string(subContent) != "#!/bin/sh\nexit 0\n" { - t.Fatalf("Sub(dir).ReadFile(exec.sh) = %q", string(subContent)) - } - - _, err = treeFS.ReadFile("dir") - if err == nil { - t.Fatal("ReadFile(dir) unexpectedly succeeded") - } - - if _, ok := errors.AsType[*fs.PathError](err); !ok { - t.Fatalf("ReadFile(dir) err type = %T, want *fs.PathError", err) - } - }) -} diff --git a/object/header/append.go b/object/header/append.go deleted file mode 100644 index 6d824740..00000000 --- a/object/header/append.go +++ /dev/null @@ -1,29 +0,0 @@ -package objectheader - -import ( - "strconv" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// Append appends a canonical loose-object header ("type size\\x00") to dst. -func Append(dst []byte, ty objecttype.Type, size int64) ([]byte, bool) { - if size < 0 { - return nil, false - } - - tyName, ok := ty.Name() - if !ok { - return nil, false - } - - sizeStr := strconv.FormatInt(size, 10) - out := make([]byte, 0, len(dst)+len(tyName)+len(sizeStr)+2) - out = append(out, dst...) - out = append(out, tyName...) - out = append(out, ' ') - out = append(out, sizeStr...) - out = append(out, 0) - - return out, true -} diff --git a/object/header/doc.go b/object/header/doc.go deleted file mode 100644 index 9c953ebb..00000000 --- a/object/header/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package objectheader parses and serializes loose-object headers -// ("type size\x00"). -package objectheader diff --git a/object/header/encode.go b/object/header/encode.go deleted file mode 100644 index a03c1f05..00000000 --- a/object/header/encode.go +++ /dev/null @@ -1,8 +0,0 @@ -package objectheader - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// Encode returns a canonical loose-object header ("type size\\x00"). -func Encode(ty objecttype.Type, size int64) ([]byte, bool) { - return Append(nil, ty, size) -} diff --git a/object/header/parse.go b/object/header/parse.go deleted file mode 100644 index cad521e5..00000000 --- a/object/header/parse.go +++ /dev/null @@ -1,42 +0,0 @@ -package objectheader - -import ( - "bytes" - "strconv" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// Parse parses a canonical loose-object header ("type size\\x00"). -// It returns the parsed type, size, bytes consumed (including trailing NUL), -// and whether parsing succeeded. -func Parse(data []byte) (objecttype.Type, int64, int, bool) { - space := bytes.IndexByte(data, ' ') - if space <= 0 { - return objecttype.TypeInvalid, 0, 0, false - } - - nulRel := bytes.IndexByte(data[space+1:], 0) - if nulRel < 0 { - return objecttype.TypeInvalid, 0, 0, false - } - - nul := space + 1 + nulRel - - ty, ok := objecttype.Parse(string(data[:space])) - if !ok { - return objecttype.TypeInvalid, 0, 0, false - } - - sizeBytes := data[space+1 : nul] - if len(sizeBytes) == 0 { - return objecttype.TypeInvalid, 0, 0, false - } - - size, err := strconv.ParseInt(string(sizeBytes), 10, 64) - if err != nil || size < 0 { - return objecttype.TypeInvalid, 0, 0, false - } - - return ty, size, nul + 1, true -} diff --git a/object/id/algorithm.go b/object/id/algorithm.go deleted file mode 100644 index a695889c..00000000 --- a/object/id/algorithm.go +++ /dev/null @@ -1,12 +0,0 @@ -package objectid - -//#nosec gosec - -// Algorithm identifies the hash algorithm used for Git object IDs. -type Algorithm uint8 - -const ( - AlgorithmUnknown Algorithm = iota - AlgorithmSHA1 - AlgorithmSHA256 -) diff --git a/object/id/algorithm_details.go b/object/id/algorithm_details.go deleted file mode 100644 index 15e96292..00000000 --- a/object/id/algorithm_details.go +++ /dev/null @@ -1,17 +0,0 @@ -package objectid - -import "hash" - -type algorithmDetails struct { - name string - size int - packHashID uint32 - signatureHeaderName string - sum func([]byte) ObjectID - new func() hash.Hash - emptyTree ObjectID -} - -func (algo Algorithm) info() algorithmDetails { - return algorithmTable[algo] -} diff --git a/object/id/algorithm_emptytree.go b/object/id/algorithm_emptytree.go deleted file mode 100644 index 32f57385..00000000 --- a/object/id/algorithm_emptytree.go +++ /dev/null @@ -1,7 +0,0 @@ -package objectid - -// EmptyTree returns the object ID of an empty tree ("tree 0\x00") for this -// algorithm. -func (algo Algorithm) EmptyTree() ObjectID { - return algo.info().emptyTree -} diff --git a/object/id/algorithm_hexlen.go b/object/id/algorithm_hexlen.go deleted file mode 100644 index 2b7fa0fa..00000000 --- a/object/id/algorithm_hexlen.go +++ /dev/null @@ -1,6 +0,0 @@ -package objectid - -// HexLen returns the encoded hexadecimal length. -func (algo Algorithm) HexLen() int { - return algo.Size() * 2 -} diff --git a/object/id/algorithm_new.go b/object/id/algorithm_new.go deleted file mode 100644 index 8abbaeda..00000000 --- a/object/id/algorithm_new.go +++ /dev/null @@ -1,13 +0,0 @@ -package objectid - -import "hash" - -// New returns a new hash.Hash for this algorithm. -func (algo Algorithm) New() (hash.Hash, error) { - newFn := algo.info().new - if newFn == nil { - return nil, ErrInvalidAlgorithm - } - - return newFn(), nil -} diff --git a/object/id/algorithm_packhashid.go b/object/id/algorithm_packhashid.go deleted file mode 100644 index 93c0f61b..00000000 --- a/object/id/algorithm_packhashid.go +++ /dev/null @@ -1,8 +0,0 @@ -package objectid - -// PackHashID returns the Git pack/rev hash-id encoding for this algorithm. -// -// Unknown algorithms return 0. -func (algo Algorithm) PackHashID() uint32 { - return algo.info().packHashID -} diff --git a/object/id/algorithm_parse.go b/object/id/algorithm_parse.go deleted file mode 100644 index d5fb0c64..00000000 --- a/object/id/algorithm_parse.go +++ /dev/null @@ -1,8 +0,0 @@ -package objectid - -// ParseAlgorithm parses a canonical algorithm name (e.g. "sha1", "sha256"). -func ParseAlgorithm(s string) (Algorithm, bool) { - algo, ok := algorithmByName[s] - - return algo, ok -} diff --git a/object/id/algorithm_signatureheadername.go b/object/id/algorithm_signatureheadername.go deleted file mode 100644 index 34fa41ce..00000000 --- a/object/id/algorithm_signatureheadername.go +++ /dev/null @@ -1,6 +0,0 @@ -package objectid - -// SignatureHeaderName returns the signature header name for this algorithm. -func (algo Algorithm) SignatureHeaderName() string { - return algo.info().signatureHeaderName -} diff --git a/object/id/algorithm_size.go b/object/id/algorithm_size.go deleted file mode 100644 index 104bfeb2..00000000 --- a/object/id/algorithm_size.go +++ /dev/null @@ -1,6 +0,0 @@ -package objectid - -// Size returns the hash size in bytes. -func (algo Algorithm) Size() int { - return algo.info().size -} diff --git a/object/id/algorithm_string.go b/object/id/algorithm_string.go deleted file mode 100644 index 410ee8a3..00000000 --- a/object/id/algorithm_string.go +++ /dev/null @@ -1,11 +0,0 @@ -package objectid - -// String returns the canonical algorithm name. -func (algo Algorithm) String() string { - inf := algo.info() - if inf.name == "" { - return "unknown" - } - - return inf.name -} diff --git a/object/id/algorithm_sum.go b/object/id/algorithm_sum.go deleted file mode 100644 index 26ad2ff6..00000000 --- a/object/id/algorithm_sum.go +++ /dev/null @@ -1,6 +0,0 @@ -package objectid - -// Sum computes an object ID from raw data using the selected algorithm. -func (algo Algorithm) Sum(data []byte) ObjectID { - return algo.info().sum(data) -} diff --git a/object/id/algorithm_supported.go b/object/id/algorithm_supported.go deleted file mode 100644 index 1f61e771..00000000 --- a/object/id/algorithm_supported.go +++ /dev/null @@ -1,7 +0,0 @@ -package objectid - -// SupportedAlgorithms returns all object ID algorithms supported by furgit. -// Do not mutate. -func SupportedAlgorithms() []Algorithm { - return supportedAlgorithms -} diff --git a/object/id/algorithm_tables.go b/object/id/algorithm_tables.go deleted file mode 100644 index e4ec3257..00000000 --- a/object/id/algorithm_tables.go +++ /dev/null @@ -1,72 +0,0 @@ -package objectid - -import ( - "crypto/sha1" //#nosec:G505 - "crypto/sha256" -) - -//nolint:gochecknoglobals -var algorithmTable = [...]algorithmDetails{ - AlgorithmUnknown: {}, - AlgorithmSHA1: { - name: "sha1", - size: sha1.Size, - packHashID: 1, - signatureHeaderName: "gpgsig", - sum: func(data []byte) ObjectID { - sum := sha1.Sum(data) //#nosec G401 - - var id ObjectID - copy(id.data[:], sum[:]) - id.algo = AlgorithmSHA1 - - return id - }, - new: sha1.New, - }, - AlgorithmSHA256: { - name: "sha256", - size: sha256.Size, - packHashID: 2, - signatureHeaderName: "gpgsig-sha256", - sum: func(data []byte) ObjectID { - sum := sha256.Sum256(data) - - var id ObjectID - copy(id.data[:], sum[:]) - id.algo = AlgorithmSHA256 - - return id - }, - new: sha256.New, - }, -} - -var ( - //nolint:gochecknoglobals - algorithmByName = map[string]Algorithm{} - //nolint:gochecknoglobals - algorithmBySignatureHeaderName = map[string]Algorithm{} - //nolint:gochecknoglobals - supportedAlgorithms []Algorithm -) - -func init() { //nolint:gochecknoinits - emptyTreeInput := []byte("tree 0\x00") - - for algo := Algorithm(0); int(algo) < len(algorithmTable); algo++ { - info := &algorithmTable[algo] - if info.name == "" { - continue - } - - info.emptyTree = info.sum(emptyTreeInput) - - algorithmByName[info.name] = algo - if info.signatureHeaderName != "" { - algorithmBySignatureHeaderName[info.signatureHeaderName] = algo - } - - supportedAlgorithms = append(supportedAlgorithms, algo) - } -} diff --git a/object/id/algorithm_zero.go b/object/id/algorithm_zero.go deleted file mode 100644 index e8c0abf2..00000000 --- a/object/id/algorithm_zero.go +++ /dev/null @@ -1,11 +0,0 @@ -package objectid - -// Zero returns the all-zero object ID for this algorithm. -func (algo Algorithm) Zero() ObjectID { - id, err := FromBytes(algo, make([]byte, algo.Size())) - if err != nil { - panic(err) - } - - return id -} diff --git a/object/id/doc.go b/object/id/doc.go deleted file mode 100644 index 1436535d..00000000 --- a/object/id/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package objectid provides Git object IDs and object-ID hash algorithms. -package objectid diff --git a/object/id/errors.go b/object/id/errors.go deleted file mode 100644 index 8e604c44..00000000 --- a/object/id/errors.go +++ /dev/null @@ -1,10 +0,0 @@ -package objectid - -import "errors" - -var ( - // ErrInvalidAlgorithm indicates an unsupported object ID algorithm. - ErrInvalidAlgorithm = errors.New("objectid: invalid algorithm") - // ErrInvalidObjectID indicates malformed object ID data. - ErrInvalidObjectID = errors.New("objectid: invalid object id") -) diff --git a/object/id/max_size.go b/object/id/max_size.go deleted file mode 100644 index d2a64a10..00000000 --- a/object/id/max_size.go +++ /dev/null @@ -1,6 +0,0 @@ -package objectid - -import "crypto/sha256" - -// maxObjectIDSize MUST be >= the largest supported algorithm size. -const maxObjectIDSize = sha256.Size diff --git a/object/id/objectid.go b/object/id/objectid.go deleted file mode 100644 index 33a54225..00000000 --- a/object/id/objectid.go +++ /dev/null @@ -1,11 +0,0 @@ -package objectid - -//#nosec G505 - -// ObjectID represents a Git object ID. -// -//nolint:recvcheck -type ObjectID struct { - algo Algorithm - data [maxObjectIDSize]byte -} diff --git a/object/id/objectid_algorithm.go b/object/id/objectid_algorithm.go deleted file mode 100644 index cb694b7c..00000000 --- a/object/id/objectid_algorithm.go +++ /dev/null @@ -1,6 +0,0 @@ -package objectid - -// Algorithm returns the object ID's hash algorithm. -func (id ObjectID) Algorithm() Algorithm { - return id.algo -} diff --git a/object/id/objectid_byte.go b/object/id/objectid_byte.go deleted file mode 100644 index 8bd8ab82..00000000 --- a/object/id/objectid_byte.go +++ /dev/null @@ -1,19 +0,0 @@ -package objectid - -// Bytes returns a copy of the object ID bytes. -func (id ObjectID) Bytes() []byte { - size := id.Algorithm().Size() - - return append([]byte(nil), id.data[:size]...) -} - -// RawBytes returns a direct byte slice view of the object ID bytes. -// -// Use Bytes when an independent copy is required. -// -// Labels: Mut-Never. -func (id *ObjectID) RawBytes() []byte { - size := id.Algorithm().Size() - - return id.data[:size:size] -} diff --git a/object/id/objectid_compare.go b/object/id/objectid_compare.go deleted file mode 100644 index a40bcc89..00000000 --- a/object/id/objectid_compare.go +++ /dev/null @@ -1,9 +0,0 @@ -package objectid - -import "bytes" - -// Compare lexicographically compares two object IDs by their canonical byte -// representation. -func Compare(left, right ObjectID) int { - return bytes.Compare(left.RawBytes(), right.RawBytes()) -} diff --git a/object/id/objectid_frombytes.go b/object/id/objectid_frombytes.go deleted file mode 100644 index ea8dacfe..00000000 --- a/object/id/objectid_frombytes.go +++ /dev/null @@ -1,20 +0,0 @@ -package objectid - -import "fmt" - -// FromBytes builds an object ID from raw bytes for the specified algorithm. -func FromBytes(algo Algorithm, b []byte) (ObjectID, error) { - var id ObjectID - if algo.Size() == 0 { - return id, ErrInvalidAlgorithm - } - - if len(b) != algo.Size() { - return id, fmt.Errorf("%w: got %d bytes, expected %d", ErrInvalidObjectID, len(b), algo.Size()) - } - - copy(id.data[:], b) - id.algo = algo - - return id, nil -} diff --git a/object/id/objectid_parse.go b/object/id/objectid_parse.go deleted file mode 100644 index e6cbb641..00000000 --- a/object/id/objectid_parse.go +++ /dev/null @@ -1,32 +0,0 @@ -package objectid - -import ( - "encoding/hex" - "fmt" -) - -// ParseHex parses an object ID from hex for the specified algorithm. -func ParseHex(algo Algorithm, s string) (ObjectID, error) { - var id ObjectID - if algo.Size() == 0 { - return id, ErrInvalidAlgorithm - } - - if len(s)%2 != 0 { - return id, fmt.Errorf("%w: odd hex length %d", ErrInvalidObjectID, len(s)) - } - - if len(s) != algo.HexLen() { - return id, fmt.Errorf("%w: got %d chars, expected %d", ErrInvalidObjectID, len(s), algo.HexLen()) - } - - decoded, err := hex.DecodeString(s) - if err != nil { - return id, fmt.Errorf("%w: decode: %w", ErrInvalidObjectID, err) - } - - copy(id.data[:], decoded) - id.algo = algo - - return id, nil -} diff --git a/object/id/objectid_string.go b/object/id/objectid_string.go deleted file mode 100644 index 36a7177d..00000000 --- a/object/id/objectid_string.go +++ /dev/null @@ -1,10 +0,0 @@ -package objectid - -import "encoding/hex" - -// String returns the canonical hex representation. -func (id ObjectID) String() string { - size := id.Algorithm().Size() - - return hex.EncodeToString(id.data[:size]) -} diff --git a/object/id/objectid_test.go b/object/id/objectid_test.go deleted file mode 100644 index 9d179fb5..00000000 --- a/object/id/objectid_test.go +++ /dev/null @@ -1,229 +0,0 @@ -package objectid_test - -import ( - "bytes" - "strings" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestParseAlgorithm(t *testing.T) { - t.Parallel() - - algo, ok := objectid.ParseAlgorithm("sha1") - if !ok || algo != objectid.AlgorithmSHA1 { - t.Fatalf("ParseAlgorithm(sha1) = (%v,%v)", algo, ok) - } - - algo, ok = objectid.ParseAlgorithm("sha256") - if !ok || algo != objectid.AlgorithmSHA256 { - t.Fatalf("ParseAlgorithm(sha256) = (%v,%v)", algo, ok) - } - - if _, ok := objectid.ParseAlgorithm("md5"); ok { - t.Fatalf("ParseAlgorithm(md5) should fail") - } -} - -func TestParseHexRoundtrip(t *testing.T) { - t.Parallel() - - for _, algo := range objectid.SupportedAlgorithms() { - t.Run(algo.String(), func(t *testing.T) { - t.Parallel() - - hex := strings.Repeat("01", algo.Size()) - - id, err := objectid.ParseHex(algo, hex) - if err != nil { - t.Fatalf("ParseHex failed: %v", err) - } - - if got := id.String(); got != hex { - t.Fatalf("String() = %q, want %q", got, hex) - } - - if got := id.Algorithm().Size(); got != algo.Size() { - t.Fatalf("Size() = %d, want %d", got, algo.Size()) - } - - raw := id.Bytes() - if len(raw) != algo.Size() { - t.Fatalf("Bytes len = %d, want %d", len(raw), algo.Size()) - } - - id2, err := objectid.FromBytes(algo, raw) - if err != nil { - t.Fatalf("FromBytes failed: %v", err) - } - - if id2.String() != hex { - t.Fatalf("FromBytes roundtrip = %q, want %q", id2.String(), hex) - } - }) - } -} - -func TestParseHexErrors(t *testing.T) { - t.Parallel() - - t.Run("unknown algo", func(t *testing.T) { - t.Parallel() - - _, err := objectid.ParseHex(objectid.AlgorithmUnknown, "00") - if err == nil { - t.Fatalf("expected ParseHex error") - } - }) - - for _, algo := range objectid.SupportedAlgorithms() { - t.Run(algo.String(), func(t *testing.T) { - t.Parallel() - - _, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen()-1)) - if err == nil { - t.Fatalf("expected ParseHex odd-len error") - } - - _, err = objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen()-2)) - if err == nil { - t.Fatalf("expected ParseHex wrong-len error") - } - - _, err = objectid.ParseHex(algo, "z"+strings.Repeat("0", algo.HexLen()-1)) - if err == nil { - t.Fatalf("expected ParseHex invalid-hex error") - } - }) - } -} - -func TestFromBytesErrors(t *testing.T) { - t.Parallel() - - _, err := objectid.FromBytes(objectid.AlgorithmUnknown, []byte{1, 2}) - if err == nil { - t.Fatalf("expected FromBytes unknown algo error") - } - - for _, algo := range objectid.SupportedAlgorithms() { - _, err = objectid.FromBytes(algo, []byte{1, 2}) - if err == nil { - t.Fatalf("expected FromBytes wrong size error") - } - } -} - -func TestBytesReturnsCopy(t *testing.T) { - t.Parallel() - - for _, algo := range objectid.SupportedAlgorithms() { - id, err := objectid.ParseHex(algo, strings.Repeat("01", algo.Size())) - if err != nil { - t.Fatalf("ParseHex failed: %v", err) - } - - b1 := id.Bytes() - - b2 := id.Bytes() - if !bytes.Equal(b1, b2) { - t.Fatalf("Bytes mismatch") - } - - b1[0] ^= 0xff - if bytes.Equal(b1, b2) { - t.Fatalf("Bytes should return independent copies") - } - } -} - -func TestRawBytesAliasesStorage(t *testing.T) { - t.Parallel() - - for _, algo := range objectid.SupportedAlgorithms() { - id, err := objectid.ParseHex(algo, strings.Repeat("01", algo.Size())) - if err != nil { - t.Fatalf("ParseHex failed: %v", err) - } - - b := id.RawBytes() - if len(b) != id.Algorithm().Size() { - t.Fatalf("RawBytes len = %d, want %d", len(b), id.Algorithm().Size()) - } - - if cap(b) != len(b) { - t.Fatalf("RawBytes cap = %d, want %d", cap(b), len(b)) - } - - orig := id.String() - b[0] ^= 0xff - - if id.String() == orig { - t.Fatalf("RawBytes should alias object ID storage") - } - } -} - -func TestAlgorithmSum(t *testing.T) { - t.Parallel() - - id1 := objectid.AlgorithmSHA1.Sum([]byte("hello")) - if id1.Algorithm() != objectid.AlgorithmSHA1 || id1.Algorithm().Size() != objectid.AlgorithmSHA1.Size() { - t.Fatalf("sha1 sum produced invalid object id") - } - - id2 := objectid.AlgorithmSHA256.Sum([]byte("hello")) - if id2.Algorithm() != objectid.AlgorithmSHA256 || id2.Algorithm().Size() != objectid.AlgorithmSHA256.Size() { - t.Fatalf("sha256 sum produced invalid object id") - } - - if id1.String() == id2.String() { - t.Fatalf("sha1 and sha256 should differ") - } -} - -func TestAlgorithmEmptyTree(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - algo objectid.Algorithm - want string - }{ - { - name: "sha1", - algo: objectid.AlgorithmSHA1, - want: "4b825dc642cb6eb9a060e54bf8d69288fbee4904", - }, - { - name: "sha256", - algo: objectid.AlgorithmSHA256, - want: "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got := tt.algo.EmptyTree() - if got.Algorithm() != tt.algo { - t.Fatalf("EmptyTree() algorithm = %v, want %v", got.Algorithm(), tt.algo) - } - - if got.String() != tt.want { - t.Fatalf("EmptyTree() = %q, want %q", got.String(), tt.want) - } - }) - } -} - -func TestUnknownAlgorithmEmptyTree(t *testing.T) { - t.Parallel() - - got := objectid.AlgorithmUnknown.EmptyTree() - if got != (objectid.ObjectID{}) { - t.Fatalf("EmptyTree() for unknown algorithm = %#v, want zero value", got) - } -} diff --git a/object/id/signatureheadername_parse.go b/object/id/signatureheadername_parse.go deleted file mode 100644 index dbe0636a..00000000 --- a/object/id/signatureheadername_parse.go +++ /dev/null @@ -1,9 +0,0 @@ -package objectid - -// ParseSignatureHeaderName parses one canonical signature header name such as -// "gpgsig" or "gpgsig-sha256". -func ParseSignatureHeaderName(s string) (Algorithm, bool) { - algo, ok := algorithmBySignatureHeaderName[s] - - return algo, ok -} diff --git a/object/object.go b/object/object.go deleted file mode 100644 index d1b1bc4f..00000000 --- a/object/object.go +++ /dev/null @@ -1,10 +0,0 @@ -package object - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// Object is a Git object. -type Object interface { - ObjectType() objecttype.Type - SerializeWithoutHeader() ([]byte, error) - SerializeWithHeader() ([]byte, error) -} diff --git a/object/parse_with_header.go b/object/parse_with_header.go deleted file mode 100644 index 9bcf5a4c..00000000 --- a/object/parse_with_header.go +++ /dev/null @@ -1,25 +0,0 @@ -package object - -import ( - "fmt" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// ParseWithHeader parses a loose object in "type size\x00body" format. -// -//nolint:ireturn -func ParseWithHeader(raw []byte, algo objectid.Algorithm) (Object, error) { - ty, size, headerLen, ok := objectheader.Parse(raw) - if !ok { - return nil, fmt.Errorf("object: malformed object header") - } - - body := raw[headerLen:] - if int64(len(body)) != size { - return nil, fmt.Errorf("object: size mismatch: header says %d bytes, body has %d", size, len(body)) - } - - return ParseWithoutHeader(ty, body, algo) -} diff --git a/object/parse_without_header.go b/object/parse_without_header.go deleted file mode 100644 index c889cb40..00000000 --- a/object/parse_without_header.go +++ /dev/null @@ -1,32 +0,0 @@ -package object - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/object/blob" - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tag" - "codeberg.org/lindenii/furgit/object/tree" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ParseWithoutHeader parses a typed object body. -// -//nolint:ireturn -func ParseWithoutHeader(ty objecttype.Type, body []byte, algo objectid.Algorithm) (Object, error) { - switch ty { - case objecttype.TypeBlob: - return blob.Parse(body) - case objecttype.TypeTree: - return tree.Parse(body, algo) - case objecttype.TypeCommit: - return commit.Parse(body, algo) - case objecttype.TypeTag: - return tag.Parse(body, algo) - case objecttype.TypeInvalid, objecttype.TypeFuture, objecttype.TypeOfsDelta, objecttype.TypeRefDelta: - return nil, fmt.Errorf("object: unsupported object type %d", ty) - default: - return nil, fmt.Errorf("object: unsupported object type %d", ty) - } -} diff --git a/object/signature/parse.go b/object/signature/parse.go deleted file mode 100644 index a6880eee..00000000 --- a/object/signature/parse.go +++ /dev/null @@ -1,97 +0,0 @@ -package signature - -import ( - "bytes" - "errors" - "fmt" - "strconv" - - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// Parse parses a canonical Git signature line: -// "Name <email> 123456789 +0000". -func Parse(line []byte) (*Signature, error) { - lt := bytes.IndexByte(line, '<') - if lt < 0 { - return nil, errors.New("object: signature: missing opening <") - } - - gtRel := bytes.IndexByte(line[lt+1:], '>') - if gtRel < 0 { - return nil, errors.New("object: signature: missing closing >") - } - - gt := lt + 1 + gtRel - - nameBytes := append([]byte(nil), bytes.TrimRight(line[:lt], " ")...) - emailBytes := append([]byte(nil), line[lt+1:gt]...) - - rest := line[gt+1:] - if len(rest) == 0 || rest[0] != ' ' { - return nil, errors.New("object: signature: missing timestamp separator") - } - - rest = rest[1:] - - before, after, ok := bytes.Cut(rest, []byte{' '}) - if !ok { - return nil, errors.New("object: signature: missing timezone separator") - } - - when, err := strconv.ParseInt(string(before), 10, 64) - if err != nil { - return nil, fmt.Errorf("object: signature: invalid timestamp: %w", err) - } - - tz := after - if len(tz) < 5 { - return nil, errors.New("object: signature: invalid timezone encoding") - } - - sign := 1 - - switch tz[0] { - case '-': - sign = -1 - case '+': - default: - return nil, errors.New("object: signature: invalid timezone sign") - } - - hh, err := strconv.Atoi(string(tz[1:3])) - if err != nil { - return nil, fmt.Errorf("object: signature: invalid timezone hours: %w", err) - } - - mm, err := strconv.Atoi(string(tz[3:5])) - if err != nil { - return nil, fmt.Errorf("object: signature: invalid timezone minutes: %w", err) - } - - if hh < 0 || hh > 23 { - return nil, errors.New("object: signature: invalid timezone hours range") - } - - if mm < 0 || mm > 59 { - return nil, errors.New("object: signature: invalid timezone minutes range") - } - - total := int64(hh)*60 + int64(mm) - - offset, err := intconv.Int64ToInt32(total) - if err != nil { - return nil, errors.New("object: signature: timezone overflow") - } - - if sign < 0 { - offset = -offset - } - - return &Signature{ - Name: nameBytes, - Email: emailBytes, - WhenUnix: when, - OffsetMinutes: offset, - }, nil -} diff --git a/object/signature/serialize.go b/object/signature/serialize.go deleted file mode 100644 index 3f60d20d..00000000 --- a/object/signature/serialize.go +++ /dev/null @@ -1,33 +0,0 @@ -package signature - -import ( - "fmt" - "strconv" - "strings" -) - -// Serialize renders the signature in canonical Git format. -func (signature Signature) Serialize() ([]byte, error) { - var b strings.Builder - b.Grow(len(signature.Name) + len(signature.Email) + 32) - b.Write(signature.Name) - b.WriteString(" <") - b.Write(signature.Email) - b.WriteString("> ") - b.WriteString(strconv.FormatInt(signature.WhenUnix, 10)) - b.WriteByte(' ') - - offset := signature.OffsetMinutes - - sign := '+' - if offset < 0 { - sign = '-' - offset = -offset - } - - hh := offset / 60 - mm := offset % 60 - fmt.Fprintf(&b, "%c%02d%02d", sign, hh, mm) - - return []byte(b.String()), nil -} diff --git a/object/signature/signature.go b/object/signature/signature.go deleted file mode 100644 index bd8b8d87..00000000 --- a/object/signature/signature.go +++ /dev/null @@ -1,10 +0,0 @@ -// Package signature provides Git author, committer, and tagger signatures. -package signature - -// Signature represents a Git signature (author/committer/tagger). -type Signature struct { - Name []byte - Email []byte - WhenUnix int64 - OffsetMinutes int32 -} diff --git a/object/signature/when.go b/object/signature/when.go deleted file mode 100644 index 0a252f68..00000000 --- a/object/signature/when.go +++ /dev/null @@ -1,10 +0,0 @@ -package signature - -import "time" - -// When returns a time.Time with the signature's timezone offset. -func (signature Signature) When() time.Time { - loc := time.FixedZone("git", int(signature.OffsetMinutes)*60) - - return time.Unix(signature.WhenUnix, 0).In(loc) -} diff --git a/object/signed/commit/commit.go b/object/signed/commit/commit.go deleted file mode 100644 index cd0ff197..00000000 --- a/object/signed/commit/commit.go +++ /dev/null @@ -1,15 +0,0 @@ -package signedcommit - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Commit represents the payload and signatures parsed from a raw comit object. -type Commit struct { - body []byte - payload []byteRange - signatures map[objectid.Algorithm][]byteRange -} - -type byteRange struct { - start int - end int -} diff --git a/object/signed/commit/doc.go b/object/signed/commit/doc.go deleted file mode 100644 index 91da6fa8..00000000 --- a/object/signed/commit/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package signedcommit extracts commit signing payloads and signatures from raw -// commit object bodies. -package signedcommit - -// TODO: Consider whether we want to fully copy the bytes into here. -// The Append functions are a bit weird ergonomically. diff --git a/object/signed/commit/integration_test.go b/object/signed/commit/integration_test.go deleted file mode 100644 index 82b34b14..00000000 --- a/object/signed/commit/integration_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package signedcommit_test - -import ( - "bytes" - "os" - "os/exec" - "path/filepath" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - signedcommit "codeberg.org/lindenii/furgit/object/signed/commit" -) - -func setupSSHSignedCommit( - t *testing.T, - algo objectid.Algorithm, -) (payload []byte, allowedSignersPath string, signaturePath string) { - t.Helper() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - - signDir := t.TempDir() - - signRoot, err := os.OpenRoot(signDir) - if err != nil { - t.Fatalf("os.OpenRoot(%q): %v", signDir, err) - } - - t.Cleanup(func() { _ = signRoot.Close() }) - - privateKeyPath := filepath.Join(signDir, "signing_key") - allowedSignersPath = filepath.Join(signDir, "allowed_signers") - signaturePath = filepath.Join(signDir, "commit.sig") - - cmd := exec.Command( //nolint:noctx - "ssh-keygen", - "-q", - "-t", "ed25519", - "-N", "", - "-C", "runxiyu@umich.edu", - "-f", privateKeyPath, - ) //#nosec G204 - - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("ssh-keygen generate failed: %v\n%s", err, out) - } - - publicKey, err := signRoot.ReadFile("signing_key.pub") - if err != nil { - t.Fatalf("ReadFile(signing_key.pub): %v", err) - } - - err = signRoot.WriteFile( - "allowed_signers", - append([]byte("runxiyu@umich.edu "), publicKey...), - 0o600, - ) - if err != nil { - t.Fatalf("WriteFile(allowed_signers): %v", err) - } - - testRepo.Run(t, "config", "gpg.format", "ssh") - testRepo.Run(t, "config", "user.signingkey", privateKeyPath) - - testRepo.WriteFile(t, "file.txt", []byte("signed\n"), 0o644) - testRepo.Run(t, "add", "file.txt") - testRepo.Run(t, "commit", "-S", "-m", "signed commit") - - commitID := testRepo.RevParse(t, "HEAD^{commit}") - body := testRepo.CatFile(t, "commit", commitID) - - commit, err := signedcommit.Parse(body) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - signature, ok := commit.AppendSignature(nil, algo) - if !ok { - t.Fatalf("missing %s signature", algo) - } - - err = signRoot.WriteFile("commit.sig", signature, 0o600) - if err != nil { - t.Fatalf("WriteFile(commit.sig): %v", err) - } - - return commit.AppendPayload(nil), allowedSignersPath, signaturePath -} - -func TestSSHSignedCommitIntegration(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - payload, allowedSignersPath, signaturePath := setupSSHSignedCommit(t, algo) - - cmd := exec.Command( //nolint:noctx - "ssh-keygen", - "-Y", "verify", - "-n", "git", - "-f", allowedSignersPath, - "-I", "runxiyu@umich.edu", - "-s", signaturePath, - ) //#nosec G204 - cmd.Stdin = bytes.NewReader(payload) - - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("ssh-keygen verify failed: %v\n%s", err, out) - } - }) -} - -func TestSSHSignedCommitIntegrationRejectsTamperedPayload(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - payload, allowedSignersPath, signaturePath := setupSSHSignedCommit(t, algo) - payload = append([]byte(nil), payload...) - payload[len(payload)-2] ^= 1 - - cmd := exec.Command( //nolint:noctx - "ssh-keygen", - "-Y", "verify", - "-n", "git", - "-f", allowedSignersPath, - "-I", "runxiyu@umich.edu", - "-s", signaturePath, - ) //#nosec G204 - cmd.Stdin = bytes.NewReader(payload) - - out, err := cmd.CombinedOutput() - if err == nil { - t.Fatalf("ssh-keygen verify unexpectedly succeeded:\n%s", out) - } - }) -} diff --git a/object/signed/commit/parse.go b/object/signed/commit/parse.go deleted file mode 100644 index fa498093..00000000 --- a/object/signed/commit/parse.go +++ /dev/null @@ -1,107 +0,0 @@ -package signedcommit - -import ( - "bytes" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Parse parses one raw commit object body for signature extraction. -// -// The returned Commit remains valid only while body remains unchanged. -// -// Labels: Deps-Borrowed, Life-Parent. -func Parse(body []byte) (*Commit, error) { - commit := &Commit{ - body: body, - signatures: make(map[objectid.Algorithm][]byteRange), - } - - payloadStart := 0 - i := 0 - - for i < len(body) { - lineStart := i - - rel := bytes.IndexByte(body[i:], '\n') - next := len(body) - - lineEnd := len(body) - if rel >= 0 { - lineEnd = i + rel - next = lineEnd + 1 - } - - line := body[lineStart:lineEnd] - i = next - - if len(line) == 0 { - commit.appendPayloadRange(payloadStart, len(body)) - - return commit, nil - } - - if line[0] == ' ' { - continue - } - - if !bytes.HasPrefix(line, []byte("gpgsig")) { - continue - } - - commit.appendPayloadRange(payloadStart, lineStart) - - key, valueStart, found := bytes.Cut(line, []byte{' '}) - if found { - if algo, ok := objectid.ParseSignatureHeaderName(string(key)); ok { - commit.signatures[algo] = append(commit.signatures[algo], byteRange{ - start: lineEnd - len(valueStart), - end: next, - }) - } - } - - for i < len(body) { - rel := bytes.IndexByte(body[i:], '\n') - next = len(body) - - lineEnd = len(body) - if rel >= 0 { - lineEnd = i + rel - next = lineEnd + 1 - } - - contStart := i - - cont := body[contStart:lineEnd] - if len(cont) == 0 || cont[0] != ' ' { - break - } - - if found { - if algo, ok := objectid.ParseSignatureHeaderName(string(key)); ok { - commit.signatures[algo] = append(commit.signatures[algo], byteRange{ - start: contStart + 1, - end: next, - }) - } - } - - i = next - } - - payloadStart = i - } - - commit.appendPayloadRange(payloadStart, len(body)) - - return commit, nil -} - -func (commit *Commit) appendPayloadRange(start, end int) { - if start >= end { - return - } - - commit.payload = append(commit.payload, byteRange{start: start, end: end}) -} diff --git a/object/signed/commit/payload_append.go b/object/signed/commit/payload_append.go deleted file mode 100644 index c261910a..00000000 --- a/object/signed/commit/payload_append.go +++ /dev/null @@ -1,11 +0,0 @@ -package signedcommit - -// AppendPayload appends the commit verification payload to dst, omitting all -// embedded signature headers. -func (commit *Commit) AppendPayload(dst []byte) []byte { - for _, part := range commit.payload { - dst = append(dst, commit.body[part.start:part.end]...) - } - - return dst -} diff --git a/object/signed/commit/signature_algorithms.go b/object/signed/commit/signature_algorithms.go deleted file mode 100644 index ac763706..00000000 --- a/object/signed/commit/signature_algorithms.go +++ /dev/null @@ -1,16 +0,0 @@ -package signedcommit - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Algorithms returns the algorithms for which the commit carries signatures. -func (commit *Commit) Algorithms() []objectid.Algorithm { - var algorithms []objectid.Algorithm - - for _, algo := range objectid.SupportedAlgorithms() { - if _, ok := commit.signatures[algo]; ok { - algorithms = append(algorithms, algo) - } - } - - return algorithms -} diff --git a/object/signed/commit/signature_append.go b/object/signed/commit/signature_append.go deleted file mode 100644 index 7f9144b7..00000000 --- a/object/signed/commit/signature_append.go +++ /dev/null @@ -1,17 +0,0 @@ -package signedcommit - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// AppendSignature appends the unfolded signature for algo to dst. -func (commit *Commit) AppendSignature(dst []byte, algo objectid.Algorithm) ([]byte, bool) { - signature, ok := commit.signatures[algo] - if !ok { - return dst, false - } - - for _, part := range signature { - dst = append(dst, commit.body[part.start:part.end]...) - } - - return dst, true -} diff --git a/object/signed/commit/unit_test.go b/object/signed/commit/unit_test.go deleted file mode 100644 index 88d4fa3b..00000000 --- a/object/signed/commit/unit_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package signedcommit_test - -import ( - "slices" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" - signedcommit "codeberg.org/lindenii/furgit/object/signed/commit" -) - -func TestParseUpstreamMultiplySignedCommit(t *testing.T) { - t.Parallel() - - // t/t7510-signed-commit.sh - body := []byte("" + - "tree 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f\n" + - "parent 9da738312d24ef0a29be2c8c2b6fc5cf7085a293\n" + - "author A U Thor <author@example.com> 1112912653 -0700\n" + - "committer C O Mitter <committer@example.com> 1112912653 -0700\n" + - "gpgsig -----BEGIN PGP SIGNATURE-----\n" + - " \n" + - " iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n" + - " QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n" + - " AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n" + - " =tQ0N\n" + - " -----END PGP SIGNATURE-----\n" + - "gpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n" + - " \n" + - " iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n" + - " QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n" + - " AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n" + - " =pIwP\n" + - " -----END PGP SIGNATURE-----\n" + - "\n" + - "second\n") - - commit, err := signedcommit.Parse(body) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(commit.AppendPayload(nil)) - - wantPayload := "" + - "tree 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f\n" + - "parent 9da738312d24ef0a29be2c8c2b6fc5cf7085a293\n" + - "author A U Thor <author@example.com> 1112912653 -0700\n" + - "committer C O Mitter <committer@example.com> 1112912653 -0700\n" + - "\n" + - "second\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } - - gotSHA1, ok := commit.AppendSignature(nil, objectid.AlgorithmSHA1) - if !ok { - t.Fatal("missing sha1 signature") - } - - wantSHA1 := "" + - "-----BEGIN PGP SIGNATURE-----\n" + - "\n" + - "iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n" + - "QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n" + - "AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n" + - "=tQ0N\n" + - "-----END PGP SIGNATURE-----\n" - if string(gotSHA1) != wantSHA1 { - t.Fatalf("sha1 signature mismatch:\n got: %q\nwant: %q", string(gotSHA1), wantSHA1) - } - - gotSHA256, ok := commit.AppendSignature(nil, objectid.AlgorithmSHA256) - if !ok { - t.Fatal("missing sha256 signature") - } - - wantSHA256 := "" + - "-----BEGIN PGP SIGNATURE-----\n" + - "\n" + - "iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n" + - "QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n" + - "AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n" + - "=pIwP\n" + - "-----END PGP SIGNATURE-----\n" - if string(gotSHA256) != wantSHA256 { - t.Fatalf("sha256 signature mismatch:\n got: %q\nwant: %q", string(gotSHA256), wantSHA256) - } - - gotAlgorithms := commit.Algorithms() - - wantAlgorithms := []objectid.Algorithm{ - objectid.AlgorithmSHA1, - objectid.AlgorithmSHA256, - } - if !slices.Equal(gotAlgorithms, wantAlgorithms) { - t.Fatalf("Algorithms() = %v, want %v", gotAlgorithms, wantAlgorithms) - } -} - -func TestParseStripsUnknownGpgsigHeadersFromPayload(t *testing.T) { - t.Parallel() - - body := []byte("" + - "tree deadbeef\n" + - "gpgsig-future header\n" + - " continued\n" + - "\n" + - "message\n") - - commit, err := signedcommit.Parse(body) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(commit.AppendPayload(nil)) - - wantPayload := "" + - "tree deadbeef\n" + - "\n" + - "message\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } - - if gotAlgorithms := commit.Algorithms(); len(gotAlgorithms) != 0 { - t.Fatalf("Algorithms() = %v, want none", gotAlgorithms) - } -} - -func TestParseAllowsDuplicateSignatureHeaders(t *testing.T) { - t.Parallel() - - body := []byte("" + - "tree deadbeef\n" + - "gpgsig one\n" + - " two\n" + - "gpgsig three\n" + - " four\n" + - "\n" + - "message\n") - - commit, err := signedcommit.Parse(body) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(commit.AppendPayload(nil)) - - wantPayload := "" + - "tree deadbeef\n" + - "\n" + - "message\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } - - gotSignature, ok := commit.AppendSignature(nil, objectid.AlgorithmSHA1) - if !ok { - t.Fatal("missing sha1 signature") - } - - wantSignature := "" + - "one\n" + - "two\n" + - "three\n" + - "four\n" - if string(gotSignature) != wantSignature { - t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature) - } -} diff --git a/object/signed/doc.go b/object/signed/doc.go deleted file mode 100644 index fb6fc3f8..00000000 --- a/object/signed/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package signed encapsulates raw signed-object processing. -// -// Its subpackages extract verification payloads and embedded signatures from -// raw commit and tag object bodies, without depending on the parsed -// object models in [codeberg.org/lindenii/furgit/object/commit] and -// [codeberg.org/lindenii/furgit/object/tag]. -package signed diff --git a/object/signed/tag/doc.go b/object/signed/tag/doc.go deleted file mode 100644 index 22b1098a..00000000 --- a/object/signed/tag/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package signedtag extracts tag signing payloads and signatures from raw tag -// object bodies. -package signedtag diff --git a/object/signed/tag/integration_test.go b/object/signed/tag/integration_test.go deleted file mode 100644 index af32aa02..00000000 --- a/object/signed/tag/integration_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package signedtag_test - -import ( - "bytes" - "os" - "os/exec" - "path/filepath" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - signedtag "codeberg.org/lindenii/furgit/object/signed/tag" -) - -func setupSSHSignedTag( - t *testing.T, - algo objectid.Algorithm, -) (payload []byte, allowedSignersPath string, signaturePath string) { - t.Helper() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - - signDir := t.TempDir() - - signRoot, err := os.OpenRoot(signDir) - if err != nil { - t.Fatalf("os.OpenRoot(%q): %v", signDir, err) - } - - t.Cleanup(func() { _ = signRoot.Close() }) - - privateKeyPath := filepath.Join(signDir, "signing_key") - allowedSignersPath = filepath.Join(signDir, "allowed_signers") - signaturePath = filepath.Join(signDir, "tag.sig") - - cmd := exec.Command( //nolint:noctx - "ssh-keygen", - "-q", - "-t", "ed25519", - "-N", "", - "-C", "runxiyu@umich.edu", - "-f", privateKeyPath, - ) //#nosec G204 - - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("ssh-keygen generate failed: %v\n%s", err, out) - } - - publicKey, err := signRoot.ReadFile("signing_key.pub") - if err != nil { - t.Fatalf("ReadFile(signing_key.pub): %v", err) - } - - err = signRoot.WriteFile( - "allowed_signers", - append([]byte("runxiyu@umich.edu "), publicKey...), - 0o600, - ) - if err != nil { - t.Fatalf("WriteFile(allowed_signers): %v", err) - } - - testRepo.Run(t, "config", "gpg.format", "ssh") - testRepo.Run(t, "config", "user.signingkey", privateKeyPath) - - testRepo.WriteFile(t, "file.txt", []byte("signed\n"), 0o644) - testRepo.Run(t, "add", "file.txt") - testRepo.Run(t, "commit", "-m", "base commit") - testRepo.Run(t, "tag", "-s", "-m", "signed tag", "signed-tag") - - tagID := testRepo.RevParse(t, "signed-tag^{tag}") - body := testRepo.CatFile(t, "tag", tagID) - - tag, err := signedtag.Parse(body, algo) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - signature, ok := tag.AppendSignature(nil, algo) - if !ok { - t.Fatal("missing signature") - } - - err = signRoot.WriteFile("tag.sig", signature, 0o600) - if err != nil { - t.Fatalf("WriteFile(tag.sig): %v", err) - } - - return tag.AppendPayload(nil), allowedSignersPath, signaturePath -} - -func TestSSHSignedTagIntegration(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - payload, allowedSignersPath, signaturePath := setupSSHSignedTag(t, algo) - - cmd := exec.Command( //nolint:noctx - "ssh-keygen", - "-Y", "verify", - "-n", "git", - "-f", allowedSignersPath, - "-I", "runxiyu@umich.edu", - "-s", signaturePath, - ) //#nosec G204 - cmd.Stdin = bytes.NewReader(payload) - - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("ssh-keygen verify failed: %v\n%s", err, out) - } - }) -} - -func TestSSHSignedTagIntegrationRejectsTamperedPayload(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - payload, allowedSignersPath, signaturePath := setupSSHSignedTag(t, algo) - payload = append([]byte(nil), payload...) - payload[len(payload)-2] ^= 1 - - cmd := exec.Command( //nolint:noctx - "ssh-keygen", - "-Y", "verify", - "-n", "git", - "-f", allowedSignersPath, - "-I", "runxiyu@umich.edu", - "-s", signaturePath, - ) //#nosec G204 - cmd.Stdin = bytes.NewReader(payload) - - out, err := cmd.CombinedOutput() - if err == nil { - t.Fatalf("ssh-keygen verify unexpectedly succeeded:\n%s", out) - } - }) -} diff --git a/object/signed/tag/parse.go b/object/signed/tag/parse.go deleted file mode 100644 index b2061d3f..00000000 --- a/object/signed/tag/parse.go +++ /dev/null @@ -1,143 +0,0 @@ -package signedtag - -import ( - "bytes" - "slices" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -var signatureBeginLines = [][]byte{ //nolint:gochecknoglobals - []byte("-----BEGIN PGP SIGNATURE-----"), - []byte("-----BEGIN PGP MESSAGE-----"), - []byte("-----BEGIN SSH SIGNATURE-----"), - []byte("-----BEGIN SIGNED MESSAGE-----"), -} - -// Parse parses one raw tag object body for signature extraction. -// -// Git stores the signature for storageAlgo as an in-body ASCII-armored -// trailer, and may store additional signatures for other algorithms in -// gpgsig* headers. -// -// The returned Tag remains valid only while body remains unchanged. -// -// Labels: Deps-Borrowed, Life-Parent. -func Parse(body []byte, storageAlgo objectid.Algorithm) (*Tag, error) { - tag := &Tag{ - body: body, - signatures: make(map[objectid.Algorithm][]byteRange), - } - - signatureStart := len(body) - for i := 0; i < len(body); { - lineStart := i - rel := bytes.IndexByte(body[i:], '\n') - next := len(body) - - lineEnd := len(body) - if rel >= 0 { - lineEnd = i + rel - next = lineEnd + 1 - } - - line := body[lineStart:lineEnd] - if slices.ContainsFunc(signatureBeginLines, func(begin []byte) bool { - return bytes.HasPrefix(line, begin) - }) { - signatureStart = lineStart - } - - i = next - } - - payloadStart := 0 - - payloadEnd := signatureStart - if signatureStart == len(body) { - payloadEnd = len(body) - } - - for i := 0; i < payloadEnd; { - lineStart := i - rel := bytes.IndexByte(body[i:payloadEnd], '\n') - next := payloadEnd - - lineEnd := payloadEnd - if rel >= 0 { - lineEnd = i + rel - next = lineEnd + 1 - } - - line := body[lineStart:lineEnd] - i = next - - if len(line) == 0 { - break - } - - if line[0] == ' ' { - continue - } - - key, valueStart, found := bytes.Cut(line, []byte{' '}) - if !found { - continue - } - - algo, ok := objectid.ParseSignatureHeaderName(string(key)) - if !ok { - continue - } - - tag.appendPayloadRange(payloadStart, lineStart) - tag.signatures[algo] = append(tag.signatures[algo], byteRange{ - start: lineEnd - len(valueStart), - end: next, - }) - - for i < payloadEnd { - rel := bytes.IndexByte(body[i:payloadEnd], '\n') - next = payloadEnd - - lineEnd = payloadEnd - if rel >= 0 { - lineEnd = i + rel - next = lineEnd + 1 - } - - cont := body[i:lineEnd] - if len(cont) == 0 || cont[0] != ' ' { - break - } - - tag.signatures[algo] = append(tag.signatures[algo], byteRange{ - start: i + 1, - end: next, - }) - - i = next - } - - payloadStart = i - } - - tag.appendPayloadRange(payloadStart, payloadEnd) - - if signatureStart != len(body) { - tag.signatures[storageAlgo] = append(tag.signatures[storageAlgo], byteRange{ - start: signatureStart, - end: len(body), - }) - } - - return tag, nil -} - -func (tag *Tag) appendPayloadRange(start, end int) { - if start >= end { - return - } - - tag.payload = append(tag.payload, byteRange{start: start, end: end}) -} diff --git a/object/signed/tag/payload_append.go b/object/signed/tag/payload_append.go deleted file mode 100644 index dae29dd8..00000000 --- a/object/signed/tag/payload_append.go +++ /dev/null @@ -1,11 +0,0 @@ -package signedtag - -// AppendPayload appends the tag verification payload to dst, omitting all -// embedded signatures. -func (tag *Tag) AppendPayload(dst []byte) []byte { - for _, part := range tag.payload { - dst = append(dst, tag.body[part.start:part.end]...) - } - - return dst -} diff --git a/object/signed/tag/signature_algorithms.go b/object/signed/tag/signature_algorithms.go deleted file mode 100644 index bc178bce..00000000 --- a/object/signed/tag/signature_algorithms.go +++ /dev/null @@ -1,16 +0,0 @@ -package signedtag - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Algorithms returns the algorithms for which the tag carries signatures. -func (tag *Tag) Algorithms() []objectid.Algorithm { - var algorithms []objectid.Algorithm - - for _, algo := range objectid.SupportedAlgorithms() { - if _, ok := tag.signatures[algo]; ok { - algorithms = append(algorithms, algo) - } - } - - return algorithms -} diff --git a/object/signed/tag/signature_append.go b/object/signed/tag/signature_append.go deleted file mode 100644 index 101816eb..00000000 --- a/object/signed/tag/signature_append.go +++ /dev/null @@ -1,17 +0,0 @@ -package signedtag - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// AppendSignature appends the signature for algo to dst. -func (tag *Tag) AppendSignature(dst []byte, algo objectid.Algorithm) ([]byte, bool) { - signature, ok := tag.signatures[algo] - if !ok { - return dst, false - } - - for _, part := range signature { - dst = append(dst, tag.body[part.start:part.end]...) - } - - return dst, true -} diff --git a/object/signed/tag/tag.go b/object/signed/tag/tag.go deleted file mode 100644 index 2ebf9369..00000000 --- a/object/signed/tag/tag.go +++ /dev/null @@ -1,15 +0,0 @@ -package signedtag - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Tag represents the payload and signatures parsed from a raw tag object. -type Tag struct { - body []byte - payload []byteRange - signatures map[objectid.Algorithm][]byteRange -} - -type byteRange struct { - start int - end int -} diff --git a/object/signed/tag/unit_test.go b/object/signed/tag/unit_test.go deleted file mode 100644 index dd4ae66f..00000000 --- a/object/signed/tag/unit_test.go +++ /dev/null @@ -1,257 +0,0 @@ -package signedtag_test - -import ( - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" - signedtag "codeberg.org/lindenii/furgit/object/signed/tag" -) - -func TestParseSignedTag(t *testing.T) { - t.Parallel() - - body := []byte("" + - "object 04b871796dc0420f8e7561a895b52484b701d51a\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger C O Mitter <committer@example.com> 1465981006 +0000\n" + - "gpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n" + - " Version: GnuPG v1\n" + - " \n" + - " header-signature\n" + - " -----END PGP SIGNATURE-----\n" + - "\n" + - "signed tag\n" + - "\n" + - "signed tag message body\n" + - "-----BEGIN PGP SIGNATURE-----\n" + - "Version: GnuPG v1\n" + - "\n" + - "body-signature\n" + - "-----END PGP SIGNATURE-----\n") - - tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(tag.AppendPayload(nil)) - - wantPayload := "" + - "object 04b871796dc0420f8e7561a895b52484b701d51a\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger C O Mitter <committer@example.com> 1465981006 +0000\n" + - "\n" + - "signed tag\n" + - "\n" + - "signed tag message body\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } - - gotAlgorithms := tag.Algorithms() - if len(gotAlgorithms) != 2 || gotAlgorithms[0] != objectid.AlgorithmSHA1 || gotAlgorithms[1] != objectid.AlgorithmSHA256 { - t.Fatalf("algorithms mismatch: got %v", gotAlgorithms) - } - - gotSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA1) - if !ok { - t.Fatal("missing sha1 signature") - } - - wantSignature := "" + - "-----BEGIN PGP SIGNATURE-----\n" + - "Version: GnuPG v1\n" + - "\n" + - "body-signature\n" + - "-----END PGP SIGNATURE-----\n" - if string(gotSignature) != wantSignature { - t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature) - } - - gotHeaderSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA256) - if !ok { - t.Fatal("missing sha256 signature") - } - - wantHeaderSignature := "" + - "-----BEGIN PGP SIGNATURE-----\n" + - "Version: GnuPG v1\n" + - "\n" + - "header-signature\n" + - "-----END PGP SIGNATURE-----\n" - if string(gotHeaderSignature) != wantHeaderSignature { - t.Fatalf("header signature mismatch:\n got: %q\nwant: %q", string(gotHeaderSignature), wantHeaderSignature) - } -} - -func TestParseHeaderOnlyTagStripsHeaderAndKeepsHeaderSignature(t *testing.T) { - t.Parallel() - - body := []byte("" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + - "gpgsig-sha256 header\n" + - " continued\n" + - "\n" + - "message\n") - - tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(tag.AppendPayload(nil)) - - wantPayload := "" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + - "\n" + - "message\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } - - gotSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA256) - if !ok { - t.Fatal("missing sha256 signature") - } - - wantSignature := "" + - "header\n" + - "continued\n" - if string(gotSignature) != wantSignature { - t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature) - } - - if _, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA1); ok { - t.Fatal("unexpected sha1 signature") - } -} - -func TestParseKeepsUnknownHeaderSignatureTextInPayload(t *testing.T) { - t.Parallel() - - body := []byte("" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + - "gpgsig-future header\n" + - " continued\n" + - "\n" + - "message line\n" + - "-----BEGIN PGP SIGNATURE-----\n" + - "body-signature\n" + - "-----END PGP SIGNATURE-----\n") - - tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(tag.AppendPayload(nil)) - - wantPayload := "" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + - "gpgsig-future header\n" + - " continued\n" + - "\n" + - "message line\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } -} - -func TestParseKeepsMessageGpgsigTextInPayload(t *testing.T) { - t.Parallel() - - body := []byte("" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + - "\n" + - "message line\n" + - "gpgsig-future header\n" + - " continued\n" + - "-----BEGIN PGP SIGNATURE-----\n" + - "body-signature\n" + - "-----END PGP SIGNATURE-----\n") - - tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(tag.AppendPayload(nil)) - - wantPayload := "" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + - "\n" + - "message line\n" + - "gpgsig-future header\n" + - " continued\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } -} - -func TestParseUsesLastSignatureBeginByPrefix(t *testing.T) { - t.Parallel() - - body := []byte("" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + - "\n" + - "message\n" + - "-----BEGIN PGP SIGNATURE----- stray\n" + - "still message\n" + - "-----BEGIN PGP SIGNATURE----- trailing\n" + - "body-signature\n") - - tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(tag.AppendPayload(nil)) - - wantPayload := "" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + - "\n" + - "message\n" + - "-----BEGIN PGP SIGNATURE----- stray\n" + - "still message\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } - - gotSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA1) - if !ok { - t.Fatal("missing signature") - } - - wantSignature := "" + - "-----BEGIN PGP SIGNATURE----- trailing\n" + - "body-signature\n" - if string(gotSignature) != wantSignature { - t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature) - } -} diff --git a/object/store/base_quarantine.go b/object/store/base_quarantine.go deleted file mode 100644 index 754fb3ee..00000000 --- a/object/store/base_quarantine.go +++ /dev/null @@ -1,17 +0,0 @@ -package objectstore - -// BaseQuarantine is one quarantined write. It is intended to be embedded. -type BaseQuarantine interface { - // Reader exposes the objects written into this quarantine. - Reader - - // Promote publishes quarantined writes into their final destination. - // - // Promote invalidates the receiver. - Promote() error - - // Discard abandons quarantined writes. - // - // Discard invalidates the receiver. - Discard() error -} diff --git a/object/store/chain/bytes.go b/object/store/chain/bytes.go deleted file mode 100644 index dc9b7906..00000000 --- a/object/store/chain/bytes.go +++ /dev/null @@ -1,46 +0,0 @@ -package chain - -import ( - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadBytesFull reads a full serialized object from the first backend that has it. -func (chain *Chain) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - for i, backend := range chain.backends { - full, err := backend.ReadBytesFull(id) - if err == nil { - return full, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return nil, fmt.Errorf("objectstore: backend %d read bytes full: %w", i, err) - } - - return nil, objectstore.ErrObjectNotFound -} - -// ReadBytesContent reads an object's type and content bytes from the first backend that has it. -func (chain *Chain) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - for i, backend := range chain.backends { - ty, content, err := backend.ReadBytesContent(id) - if err == nil { - return ty, content, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return objecttype.TypeInvalid, nil, fmt.Errorf("objectstore: backend %d read bytes content: %w", i, err) - } - - return objecttype.TypeInvalid, nil, objectstore.ErrObjectNotFound -} diff --git a/object/store/chain/chain.go b/object/store/chain/chain.go deleted file mode 100644 index 218c8abd..00000000 --- a/object/store/chain/chain.go +++ /dev/null @@ -1,12 +0,0 @@ -// Package chain provides a wrapper object storage backend to query a chain of -// backends. -package chain - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -// Chain queries multiple object databases in order. -// -// Labels: Close-Caller. -type Chain struct { - backends []objectstore.Reader -} diff --git a/object/store/chain/header.go b/object/store/chain/header.go deleted file mode 100644 index f6c92459..00000000 --- a/object/store/chain/header.go +++ /dev/null @@ -1,28 +0,0 @@ -package chain - -import ( - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadHeader reads object header data from the first backend that has it. -func (chain *Chain) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - for i, backend := range chain.backends { - ty, size, err := backend.ReadHeader(id) - if err == nil { - return ty, size, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore: backend %d read header: %w", i, err) - } - - return objecttype.TypeInvalid, 0, objectstore.ErrObjectNotFound -} diff --git a/object/store/chain/new.go b/object/store/chain/new.go deleted file mode 100644 index dd499d38..00000000 --- a/object/store/chain/new.go +++ /dev/null @@ -1,14 +0,0 @@ -package chain - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -// New creates an ordered object database chain. -// -// The provided backends must be non-nil and distinct. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(backends ...objectstore.Reader) *Chain { - return &Chain{ - backends: append([]objectstore.Reader(nil), backends...), - } -} diff --git a/object/store/chain/reader.go b/object/store/chain/reader.go deleted file mode 100644 index 3991ee9a..00000000 --- a/object/store/chain/reader.go +++ /dev/null @@ -1,47 +0,0 @@ -package chain - -import ( - "errors" - "fmt" - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadReaderFull reads a full serialized object stream from the first backend that has it. -func (chain *Chain) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - for i, backend := range chain.backends { - reader, err := backend.ReadReaderFull(id) - if err == nil { - return reader, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return nil, fmt.Errorf("objectstore: backend %d read reader full: %w", i, err) - } - - return nil, objectstore.ErrObjectNotFound -} - -// ReadReaderContent reads an object's type, declared content length, and content stream from the first backend that has it. -func (chain *Chain) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - for i, backend := range chain.backends { - ty, size, reader, err := backend.ReadReaderContent(id) - if err == nil { - return ty, size, reader, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return objecttype.TypeInvalid, 0, nil, fmt.Errorf("objectstore: backend %d read reader content: %w", i, err) - } - - return objecttype.TypeInvalid, 0, nil, objectstore.ErrObjectNotFound -} diff --git a/object/store/chain/refresh.go b/object/store/chain/refresh.go deleted file mode 100644 index c47352dc..00000000 --- a/object/store/chain/refresh.go +++ /dev/null @@ -1,17 +0,0 @@ -package chain - -import "errors" - -// Refresh forwards refresh calls to all backends. -func (chain *Chain) Refresh() error { - var errs []error - - for _, backend := range chain.backends { - err := backend.Refresh() - if err != nil { - errs = append(errs, err) - } - } - - return errors.Join(errs...) -} diff --git a/object/store/chain/size.go b/object/store/chain/size.go deleted file mode 100644 index f0099028..00000000 --- a/object/store/chain/size.go +++ /dev/null @@ -1,27 +0,0 @@ -package chain - -import ( - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// ReadSize reads object content length from the first backend that has it. -func (chain *Chain) ReadSize(id objectid.ObjectID) (int64, error) { - for i, backend := range chain.backends { - size, err := backend.ReadSize(id) - if err == nil { - return size, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return 0, fmt.Errorf("objectstore: backend %d read size: %w", i, err) - } - - return 0, objectstore.ErrObjectNotFound -} diff --git a/object/store/cursor.go b/object/store/cursor.go deleted file mode 100644 index c6008ccd..00000000 --- a/object/store/cursor.go +++ /dev/null @@ -1,7 +0,0 @@ -package objectstore - -// type Cursor any -// -// Then make all read functions accept and provide a Cursor -// nil must always be accepted and would exhibit the same behavior as right now -// Non-nil behavior is implementation-defined: e.g., pack selection diff --git a/object/store/doc.go b/object/store/doc.go deleted file mode 100644 index 45acc47c..00000000 --- a/object/store/doc.go +++ /dev/null @@ -1,19 +0,0 @@ -// Package objectstore provides interfaces for object storage backends. -// -// Reading stores only respond to object-ID queries in terms of headers (type -// and size), raw bytes, and streaming payloads, but they do not parse commits, -// trees, blobs, or tags into typed values. Turning stored objects into typed -// objects is the job of [codeberg.org/lindenii/furgit/object/fetch]. -// -// This package does not define one unified writing interface. Backends have -// very different write models: writing one loose object is natural, while -// writing one object into a packfile backend is wasteful. Instead, we define -// distinct optional capabilities for object-wise writes, pack-wise writes, -// and compose them against quarantined writes. Where one logical quarantine -// supports multiple write shapes together, this package also defines a -// coordinated writer quarantine capability. -// -// Concrete implementations generally inherit the contract documented by the -// interfaces they satisfy. Implementation docs focus on additional guarantees -// and implementation-specific behavior. -package objectstore diff --git a/object/store/dual/doc.go b/object/store/dual/doc.go deleted file mode 100644 index 104120ec..00000000 --- a/object/store/dual/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -// Package dual provides one logical object store backed by separate object-wise -// and pack-wise stores. -// -// Dual composes a store that handles individual object writes with a store that -// handles pack-wise writes, while exposing one mixed reader over both. -// Coordinated quarantine operations span both stores, but quarantine promotion -// is non-atomic. -package dual diff --git a/object/store/dual/dual.go b/object/store/dual/dual.go deleted file mode 100644 index 3072ae77..00000000 --- a/object/store/dual/dual.go +++ /dev/null @@ -1,33 +0,0 @@ -package dual - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -type objectSide interface { - objectstore.Reader - objectstore.ObjectWriter - objectstore.ObjectQuarantiner -} - -type packSide interface { - objectstore.Reader - objectstore.PackWriter - objectstore.PackQuarantiner -} - -// Dual composes one object-wise store and one pack-wise store into one logical -// object store. -// -// Reads are served from the combined object reader of both stores. Individual -// object writes are routed to the object-wise store, and pack writes are routed -// to the pack-wise store. Coordinated quarantines go across both stores. -type Dual struct { - object objectSide - pack packSide - reader objectstore.Reader -} - -var ( - _ objectstore.Reader = (*Dual)(nil) - _ objectstore.Writer = (*Dual)(nil) - _ objectstore.Quarantiner = (*Dual)(nil) -) diff --git a/object/store/dual/dual_test.go b/object/store/dual/dual_test.go deleted file mode 100644 index 1d25a775..00000000 --- a/object/store/dual/dual_test.go +++ /dev/null @@ -1,266 +0,0 @@ -package dual_test - -import ( - "bytes" - "os" - "path/filepath" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - "codeberg.org/lindenii/furgit/object/store/dual" - "codeberg.org/lindenii/furgit/object/store/loose" - "codeberg.org/lindenii/furgit/object/store/packed" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func fixturePath(t *testing.T, algo objectid.Algorithm, name string) string { - t.Helper() - - return filepath.Join("..", "packed", "internal", "ingest", "testdata", "fixtures", algo.String(), name) -} - -func fixtureBytes(t *testing.T, algo objectid.Algorithm, name string) []byte { - t.Helper() - - path := fixturePath(t, algo, name) - dir := filepath.Dir(path) - base := filepath.Base(path) - - root, err := os.OpenRoot(dir) - if err != nil { - t.Fatalf("open fixture root %q: %v", dir, err) - } - - defer func() { - err := root.Close() - if err != nil { - t.Fatalf("close fixture root %q: %v", dir, err) - } - }() - - data, err := root.ReadFile(base) - if err != nil { - t.Fatalf("read fixture %q: %v", base, err) - } - - return data -} - -func fixtureMetadata(t *testing.T, algo objectid.Algorithm) map[string]string { - t.Helper() - - data := fixtureBytes(t, algo, "METADATA.txt") - out := make(map[string]string) - - for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - key, value, ok := strings.Cut(line, "=") - if !ok { - t.Fatalf("invalid fixture metadata line %q", line) - } - - out[strings.TrimSpace(key)] = strings.TrimSpace(value) - } - - return out -} - -func fixtureOID(t *testing.T, algo objectid.Algorithm, key string) objectid.ObjectID { - t.Helper() - - meta := fixtureMetadata(t, algo) - - hex, ok := meta[key] - if !ok { - t.Fatalf("missing fixture metadata key %q", key) - } - - id, err := objectid.ParseHex(algo, hex) - if err != nil { - t.Fatalf("parse fixture metadata oid %q: %v", hex, err) - } - - return id -} - -func newDualStore(t *testing.T, repo *testgit.TestRepo, algo objectid.Algorithm) *dual.Dual { - t.Helper() - - objectsRoot := repo.OpenObjectsRoot(t) - - looseStore, err := loose.New(objectsRoot, algo) - if err != nil { - t.Fatalf("loose.New: %v", err) - } - - packRoot := repo.OpenPackRoot(t) - - packedStore, err := packed.New(packRoot, algo, packed.Options{WriteRev: true}) - if err != nil { - t.Fatalf("packed.New: %v", err) - } - - return dual.New(looseStore, packedStore) -} - -func TestDualReadsWritesAndQuarantine(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - packBytes := fixtureBytes(t, algo, "nonthin.pack") - - repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := newDualStore(t, repo, algo) - - quarantiner, ok := any(store).(objectstore.Quarantiner) - if !ok { - t.Fatal("dual does not implement Quarantiner") - } - - quarantine, err := quarantiner.BeginQuarantine(objectstore.QuarantineOptions{}) - if err != nil { - t.Fatalf("BeginQuarantine: %v", err) - } - - err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true}) - if err != nil { - t.Fatalf("quarantine.WritePack: %v", err) - } - - objectQ, ok := any(quarantine).(objectstore.ObjectQuarantine) - if !ok { - t.Fatal("pack quarantine does not also implement ObjectQuarantine") - } - - looseContent := []byte("dual quarantine loose object\n") - - looseID, err := objectQ.WriteBytesContent(objecttype.TypeBlob, looseContent) - if err != nil { - t.Fatalf("quarantine.WriteBytesContent: %v", err) - } - - ty, _, err := quarantine.ReadHeader(head) - if err != nil { - t.Fatalf("quarantine.ReadHeader(pack): %v", err) - } - - if ty != objecttype.TypeCommit { - t.Fatalf("quarantine.ReadHeader(pack) type = %v, want commit", ty) - } - - ty, got, err := quarantine.ReadBytesContent(looseID) - if err != nil { - t.Fatalf("quarantine.ReadBytesContent(loose): %v", err) - } - - if ty != objecttype.TypeBlob { - t.Fatalf("quarantine.ReadBytesContent(loose) type = %v, want blob", ty) - } - - if !bytes.Equal(got, looseContent) { - t.Fatal("quarantine.ReadBytesContent(loose) mismatch") - } - - _, _, err = store.ReadHeader(head) - if err == nil { - t.Fatal("store.ReadHeader unexpectedly saw quarantined pack object before promote") - } - - _, _, err = store.ReadBytesContent(looseID) - if err == nil { - t.Fatal("store.ReadBytesContent unexpectedly saw quarantined loose object before promote") - } - - err = quarantine.Promote() - if err != nil { - t.Fatalf("quarantine.Promote: %v", err) - } - - err = store.Refresh() - if err != nil { - t.Fatalf("store.Refresh: %v", err) - } - - ty, _, err = store.ReadHeader(head) - if err != nil { - t.Fatalf("store.ReadHeader(pack): %v", err) - } - - if ty != objecttype.TypeCommit { - t.Fatalf("store.ReadHeader(pack) type = %v, want commit", ty) - } - - ty, got, err = store.ReadBytesContent(looseID) - if err != nil { - t.Fatalf("store.ReadBytesContent(loose): %v", err) - } - - if ty != objecttype.TypeBlob { - t.Fatalf("store.ReadBytesContent(loose) type = %v, want blob", ty) - } - - if !bytes.Equal(got, looseContent) { - t.Fatal("store.ReadBytesContent(loose) mismatch") - } - }) -} - -func TestDualQuarantineDiscardDropsBothHalves(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - packBytes := fixtureBytes(t, algo, "nonthin.pack") - - repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := newDualStore(t, repo, algo) - - quarantiner, ok := any(store).(objectstore.Quarantiner) - if !ok { - t.Fatal("expected objectstore.Quarantiner") - } - - quarantine, err := quarantiner.BeginQuarantine(objectstore.QuarantineOptions{}) - if err != nil { - t.Fatalf("BeginQuarantine: %v", err) - } - - err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true}) - if err != nil { - t.Fatalf("quarantine.WritePack: %v", err) - } - - looseID, err := quarantine.WriteBytesContent(objecttype.TypeBlob, []byte("discarded dual object\n")) - if err != nil { - t.Fatalf("quarantine.WriteBytesContent: %v", err) - } - - err = quarantine.Discard() - if err != nil { - t.Fatalf("quarantine.Discard: %v", err) - } - - err = store.Refresh() - if err != nil { - t.Fatalf("store.Refresh: %v", err) - } - - _, _, err = store.ReadHeader(head) - if err == nil { - t.Fatal("store.ReadHeader unexpectedly saw discarded pack object") - } - - _, _, err = store.ReadBytesContent(looseID) - if err == nil { - t.Fatal("store.ReadBytesContent unexpectedly saw discarded loose object") - } - }) -} diff --git a/object/store/dual/new.go b/object/store/dual/new.go deleted file mode 100644 index ef38bc7a..00000000 --- a/object/store/dual/new.go +++ /dev/null @@ -1,29 +0,0 @@ -package dual - -import ( - objectstore "codeberg.org/lindenii/furgit/object/store" - objectmix "codeberg.org/lindenii/furgit/object/store/mix" -) - -// New creates one dual object store from borrowed object-wise and pack-wise -// stores. -// -// Labels: Deps-Borrowed, Life-Parent. -func New( - object interface { - objectstore.Reader - objectstore.ObjectWriter - objectstore.ObjectQuarantiner - }, - pack interface { - objectstore.Reader - objectstore.PackWriter - objectstore.PackQuarantiner - }, -) *Dual { - return &Dual{ - object: object, - pack: pack, - reader: objectmix.New(object, pack), - } -} diff --git a/object/store/dual/quarantine.go b/object/store/dual/quarantine.go deleted file mode 100644 index fb1048af..00000000 --- a/object/store/dual/quarantine.go +++ /dev/null @@ -1,114 +0,0 @@ -package dual - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objectmix "codeberg.org/lindenii/furgit/object/store/mix" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// quarantine is one coordinated dual quarantine over both stores. -type quarantine struct { - objectQ objectstore.ObjectQuarantine - packQ objectstore.PackQuarantine - reader objectstore.Reader -} - -var ( - _ objectstore.ObjectQuarantine = (*quarantine)(nil) - _ objectstore.PackQuarantine = (*quarantine)(nil) - _ objectstore.Quarantine = (*quarantine)(nil) -) - -func newQuarantine( - objectQ objectstore.ObjectQuarantine, - packQ objectstore.PackQuarantine, -) *quarantine { - return &quarantine{ - objectQ: objectQ, - packQ: packQ, - reader: objectmix.New(objectQ, packQ), - } -} - -// ReadBytesFull reads a full serialized object as "type size\0content" from -// either quarantined store. -func (quarantine *quarantine) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - return quarantine.reader.ReadBytesFull(id) -} - -// ReadBytesContent reads an object's type and content bytes from either -// quarantined store. -func (quarantine *quarantine) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - return quarantine.reader.ReadBytesContent(id) -} - -// ReadReaderFull reads a full serialized object stream as -// "type size\0content" from either quarantined store. -func (quarantine *quarantine) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - return quarantine.reader.ReadReaderFull(id) -} - -// ReadReaderContent reads an object's type, declared content length, and -// content stream from either quarantined store. -func (quarantine *quarantine) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - return quarantine.reader.ReadReaderContent(id) -} - -// ReadSize reads an object's declared content length from either quarantined -// store. -func (quarantine *quarantine) ReadSize(id objectid.ObjectID) (int64, error) { - return quarantine.reader.ReadSize(id) -} - -// ReadHeader reads an object's type and declared content length from either -// quarantined store. -func (quarantine *quarantine) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - return quarantine.reader.ReadHeader(id) -} - -// Refresh refreshes both quarantined stores and the combined quarantined reader. -func (quarantine *quarantine) Refresh() error { - err := quarantine.objectQ.Refresh() - if err != nil { - return err - } - - err = quarantine.packQ.Refresh() - if err != nil { - return err - } - - return quarantine.reader.Refresh() -} - -// WriteReaderContent writes one typed object content stream to the quarantined -// object-wise store. -func (quarantine *quarantine) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) { - return quarantine.objectQ.WriteReaderContent(ty, size, src) -} - -// WriteReaderFull writes one full serialized object stream as -// "type size\0content" to the quarantined object-wise store. -func (quarantine *quarantine) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) { - return quarantine.objectQ.WriteReaderFull(src) -} - -// WriteBytesContent writes one typed object content byte slice to the -// quarantined object-wise store. -func (quarantine *quarantine) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) { - return quarantine.objectQ.WriteBytesContent(ty, content) -} - -// WriteBytesFull writes one full serialized object byte slice as -// "type size\0content" to the quarantined object-wise store. -func (quarantine *quarantine) WriteBytesFull(raw []byte) (objectid.ObjectID, error) { - return quarantine.objectQ.WriteBytesFull(raw) -} - -// WritePack ingests one pack stream into the quarantined pack-wise store. -func (quarantine *quarantine) WritePack(src io.Reader, opts objectstore.PackWriteOptions) error { - return quarantine.packQ.WritePack(src, opts) -} diff --git a/object/store/dual/quarantine_begin.go b/object/store/dual/quarantine_begin.go deleted file mode 100644 index 5c6bc934..00000000 --- a/object/store/dual/quarantine_begin.go +++ /dev/null @@ -1,22 +0,0 @@ -package dual - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -// BeginQuarantine creates one coordinated dual quarantine spanning both stores. -// -// Labels: Deps-Borrowed, Life-Parent, Close-No. -func (dual *Dual) BeginQuarantine(opts objectstore.QuarantineOptions) (objectstore.Quarantine, error) { - objectQ, err := dual.object.BeginObjectQuarantine(opts.Object) - if err != nil { - return nil, err - } - - packQ, err := dual.pack.BeginPackQuarantine(opts.Pack) - if err != nil { - _ = objectQ.Discard() - - return nil, err - } - - return newQuarantine(objectQ, packQ), nil -} diff --git a/object/store/dual/quarantine_discard.go b/object/store/dual/quarantine_discard.go deleted file mode 100644 index 67f15d6c..00000000 --- a/object/store/dual/quarantine_discard.go +++ /dev/null @@ -1,11 +0,0 @@ -package dual - -// Discard abandons both quarantine halves and invalidates the receiver. -func (quarantine *quarantine) Discard() error { - err := quarantine.packQ.Discard() - if err != nil { - return err - } - - return quarantine.objectQ.Discard() -} diff --git a/object/store/dual/quarantine_promote.go b/object/store/dual/quarantine_promote.go deleted file mode 100644 index 4d0a45b8..00000000 --- a/object/store/dual/quarantine_promote.go +++ /dev/null @@ -1,13 +0,0 @@ -package dual - -// Promote publishes both quarantine halves and invalidates the receiver. -// -// Promotion is coordinated and ordered, but not atomic. -func (quarantine *quarantine) Promote() error { - err := quarantine.packQ.Promote() - if err != nil { - return err - } - - return quarantine.objectQ.Promote() -} diff --git a/object/store/dual/reader.go b/object/store/dual/reader.go deleted file mode 100644 index 7b499d5d..00000000 --- a/object/store/dual/reader.go +++ /dev/null @@ -1,57 +0,0 @@ -package dual - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadBytesFull reads a full serialized object as "type size\0content" from -// either store. -func (dual *Dual) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - return dual.reader.ReadBytesFull(id) -} - -// ReadBytesContent reads an object's type and content bytes from either store. -func (dual *Dual) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - return dual.reader.ReadBytesContent(id) -} - -// ReadReaderFull reads a full serialized object stream as "type size\0content" -// from either store. -func (dual *Dual) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - return dual.reader.ReadReaderFull(id) -} - -// ReadReaderContent reads an object's type, declared content length, and -// content stream from either store. -func (dual *Dual) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - return dual.reader.ReadReaderContent(id) -} - -// ReadSize reads an object's declared content length from either store. -func (dual *Dual) ReadSize(id objectid.ObjectID) (int64, error) { - return dual.reader.ReadSize(id) -} - -// ReadHeader reads an object's type and declared content length from either -// store. -func (dual *Dual) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - return dual.reader.ReadHeader(id) -} - -// Refresh refreshes both underlying stores and the combined read view. -func (dual *Dual) Refresh() error { - err := dual.object.Refresh() - if err != nil { - return err - } - - err = dual.pack.Refresh() - if err != nil { - return err - } - - return dual.reader.Refresh() -} diff --git a/object/store/dual/writer_object.go b/object/store/dual/writer_object.go deleted file mode 100644 index 7aefe9ea..00000000 --- a/object/store/dual/writer_object.go +++ /dev/null @@ -1,32 +0,0 @@ -package dual - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// WriteReaderContent writes one typed object content stream to the object-wise -// store. -func (dual *Dual) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) { - return dual.object.WriteReaderContent(ty, size, src) -} - -// WriteReaderFull writes one full serialized object stream as -// "type size\0content" to the object-wise store. -func (dual *Dual) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) { - return dual.object.WriteReaderFull(src) -} - -// WriteBytesContent writes one typed object content byte slice to the -// object-wise store. -func (dual *Dual) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) { - return dual.object.WriteBytesContent(ty, content) -} - -// WriteBytesFull writes one full serialized object byte slice as -// "type size\0content" to the object-wise store. -func (dual *Dual) WriteBytesFull(raw []byte) (objectid.ObjectID, error) { - return dual.object.WriteBytesFull(raw) -} diff --git a/object/store/dual/writer_pack.go b/object/store/dual/writer_pack.go deleted file mode 100644 index 5ac8648b..00000000 --- a/object/store/dual/writer_pack.go +++ /dev/null @@ -1,12 +0,0 @@ -package dual - -import ( - "io" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// WritePack ingests one pack stream into the pack-wise store. -func (dual *Dual) WritePack(src io.Reader, opts objectstore.PackWriteOptions) error { - return dual.pack.WritePack(src, opts) -} diff --git a/object/store/errors.go b/object/store/errors.go deleted file mode 100644 index 0e36b400..00000000 --- a/object/store/errors.go +++ /dev/null @@ -1,8 +0,0 @@ -package objectstore - -import "errors" - -// ErrObjectNotFound indicates that an object does not exist in a backend. -// This error must only be produced by object stores, when it has no -// specified object ID, but no other unexpected conditions were encountered. -var ErrObjectNotFound = errors.New("objectstore: object not found") diff --git a/object/store/loose/helpers_test.go b/object/store/loose/helpers_test.go deleted file mode 100644 index 97cec9d7..00000000 --- a/object/store/loose/helpers_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package loose_test - -import ( - "io" - "os" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/loose" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func openLooseStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *loose.Store { - t.Helper() - - root := testRepo.OpenObjectsRoot(t) - - store, err := loose.New(root, algo) - if err != nil { - t.Fatalf("loose.New: %v", err) - } - - return store -} - -func mustReadAllAndClose(t *testing.T, reader io.ReadCloser) []byte { - t.Helper() - - data, err := io.ReadAll(reader) - if err != nil { - _ = reader.Close() - - t.Fatalf("ReadAll: %v", err) - } - - err = reader.Close() - if err != nil { - t.Fatalf("Close: %v", err) - } - - return data -} - -func expectedRawObject(t *testing.T, testRepo *testgit.TestRepo, id objectid.ObjectID) (objecttype.Type, []byte, []byte) { - t.Helper() - - typeName := testRepo.Run(t, "cat-file", "-t", id.String()) - - ty, ok := objecttype.Parse(typeName) - if !ok { - t.Fatalf("ParseName(%q) failed", typeName) - } - - body := testRepo.CatFile(t, typeName, id) - - header, ok := objectheader.Encode(ty, int64(len(body))) - if !ok { - t.Fatalf("objectheader.Encode failed") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return ty, body, raw -} - -func corruptLooseObjectTrailer(t *testing.T, testRepo *testgit.TestRepo, id objectid.ObjectID) { - t.Helper() - - root := testRepo.OpenObjectsRoot(t) - - hex := id.String() - relPath := 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) - } -} diff --git a/object/store/loose/parse.go b/object/store/loose/parse.go deleted file mode 100644 index dfb420ba..00000000 --- a/object/store/loose/parse.go +++ /dev/null @@ -1,55 +0,0 @@ -package loose - -import ( - "bufio" - "errors" - "io" - "os" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" - objectheader "codeberg.org/lindenii/furgit/object/header" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// decodeAll inflates the full loose object payload from file. -func decodeAll(file *os.File) ([]byte, error) { - zr, err := zlib.NewReader(file) - if err != nil { - return nil, err - } - - defer func() { _ = zr.Close() }() - - return io.ReadAll(zr) -} - -// parseRaw parses a loose object payload in "type size\0content" format. -func parseRaw(raw []byte) (objecttype.Type, []byte, error) { - ty, size, headerLen, ok := objectheader.Parse(raw) - if !ok { - return objecttype.TypeInvalid, nil, errors.New("objectstore/loose: malformed object header") - } - - content := raw[headerLen:] - if int64(len(content)) != size { - return objecttype.TypeInvalid, nil, errors.New("objectstore/loose: object header size/content mismatch") - } - - return ty, content, nil -} - -// readHeader reads and parses a loose object header from br, and returns -// the raw header bytes including the trailing NUL. -func readHeader(br *bufio.Reader) ([]byte, objecttype.Type, int64, error) { - header, err := br.ReadSlice(0) - if err != nil { - return nil, objecttype.TypeInvalid, 0, err - } - - ty, size, _, ok := objectheader.Parse(header) - if !ok { - return nil, objecttype.TypeInvalid, 0, errors.New("objectstore/loose: malformed object header") - } - - return header, ty, size, nil -} diff --git a/object/store/loose/paths.go b/object/store/loose/paths.go deleted file mode 100644 index 0593cc0d..00000000 --- a/object/store/loose/paths.go +++ /dev/null @@ -1,43 +0,0 @@ -package loose - -import ( - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// objectPath returns the loose object path for id relative to the objects root. -func (store *Store) objectPath(id objectid.ObjectID) (string, error) { - if id.Algorithm() != store.algo { - return "", fmt.Errorf("objectstore/loose: object id algorithm mismatch: got %s want %s", id.Algorithm(), store.algo) - } - - hex := id.String() - - return filepath.Join(hex[:2], hex[2:]), nil -} - -// openObject opens the loose object file for id. -// Missing files cause objectstore.ErrObjectNotFound. -func (store *Store) openObject(id objectid.ObjectID) (*os.File, error) { - relPath, err := store.objectPath(id) - if err != nil { - return nil, err - } - - file, err := store.root.Open(relPath) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return nil, objectstore.ErrObjectNotFound - } - - return nil, err - } - - return file, nil -} diff --git a/object/store/loose/quarantine.go b/object/store/loose/quarantine.go deleted file mode 100644 index 52fb8120..00000000 --- a/object/store/loose/quarantine.go +++ /dev/null @@ -1,19 +0,0 @@ -package loose - -import ( - "os" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -var _ objectstore.ObjectQuarantiner = (*Store)(nil) - -type objectQuarantine struct { - *Store - - parent *Store - tempName string - tempRoot *os.Root -} - -var _ objectstore.ObjectQuarantine = (*objectQuarantine)(nil) diff --git a/object/store/loose/quarantine_begin.go b/object/store/loose/quarantine_begin.go deleted file mode 100644 index dd27f968..00000000 --- a/object/store/loose/quarantine_begin.go +++ /dev/null @@ -1,63 +0,0 @@ -package loose - -import ( - "crypto/rand" - "errors" - "fmt" - "io/fs" - "os" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// BeginObjectQuarantine creates one quarantined loose store rooted privately -// beneath the destination loose root. -// -// Labels: Deps-Borrowed, Life-Parent, Close-No. -func (store *Store) BeginObjectQuarantine(_ objectstore.ObjectQuarantineOptions) (objectstore.ObjectQuarantine, error) { - tempName, tempRoot, err := createLooseQuarantineRoot(store.root) - if err != nil { - return nil, err - } - - quarantineStore, err := New(tempRoot, store.algo) - if err != nil { - _ = tempRoot.Close() - _ = store.root.RemoveAll(tempName) - - return nil, err - } - - return &objectQuarantine{ - Store: quarantineStore, - parent: store, - tempName: tempName, - tempRoot: tempRoot, - }, nil -} - -func createLooseQuarantineRoot(parent *os.Root) (string, *os.Root, error) { - for range 32 { - name := "tmp_looseq_" + rand.Text() - - err := parent.Mkdir(name, 0o700) - if err == nil { - root, err := parent.OpenRoot(name) - if err == nil { - return name, root, nil - } - - _ = parent.RemoveAll(name) - - return "", nil, err - } - - if errors.Is(err, fs.ErrExist) { - continue - } - - return "", nil, err - } - - return "", nil, fmt.Errorf("objectstore/loose: unable to create quarantine directory") -} diff --git a/object/store/loose/quarantine_discard.go b/object/store/loose/quarantine_discard.go deleted file mode 100644 index 3e783d0e..00000000 --- a/object/store/loose/quarantine_discard.go +++ /dev/null @@ -1,18 +0,0 @@ -package loose - -// Discard removes the quarantine and invalidates the receiver. -func (quarantine *objectQuarantine) Discard() error { - closeErr := quarantine.Close() - tempRootErr := quarantine.tempRoot.Close() - removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) - - if closeErr != nil { - return closeErr - } - - if tempRootErr != nil { - return tempRootErr - } - - return removeErr -} diff --git a/object/store/loose/quarantine_promote.go b/object/store/loose/quarantine_promote.go deleted file mode 100644 index 66bb41df..00000000 --- a/object/store/loose/quarantine_promote.go +++ /dev/null @@ -1,116 +0,0 @@ -package loose - -import ( - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" -) - -// Promote publishes all quarantined loose objects into the parent loose store -// and invalidates the receiver. -func (quarantine *objectQuarantine) Promote() error { - closeErr := quarantine.Close() - promoteErr := promoteLooseQuarantine(quarantine.parent, quarantine.tempName, quarantine.tempRoot) - tempRootErr := quarantine.tempRoot.Close() - removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) - - if closeErr != nil { - return closeErr - } - - if promoteErr != nil { - return promoteErr - } - - if tempRootErr != nil { - return tempRootErr - } - - return removeErr -} - -func promoteLooseQuarantine(parent *Store, tempName string, tempRoot *os.Root) error { - entries, err := fs.ReadDir(tempRoot.FS(), ".") - if err != nil && !errors.Is(err, fs.ErrNotExist) { - return err - } - - for _, entry := range entries { - if !entry.IsDir() { - return fmt.Errorf("objectstore/loose: quarantine contains unexpected file %q", entry.Name()) - } - - if len(entry.Name()) != 2 || !isHexString(entry.Name()) { - return fmt.Errorf("objectstore/loose: quarantine contains invalid shard %q", entry.Name()) - } - - err := promoteLooseQuarantineShard(parent, tempName, tempRoot, entry.Name()) - if err != nil { - return err - } - } - - return nil -} - -func promoteLooseQuarantineShard(parent *Store, tempName string, tempRoot *os.Root, shard string) error { - entries, err := fs.ReadDir(tempRoot.FS(), shard) - if err != nil { - return err - } - - err = parent.root.MkdirAll(shard, 0o755) - if err != nil { - return err - } - - wantNameLen := parent.algo.HexLen() - 2 - - for _, entry := range entries { - if entry.IsDir() { - return fmt.Errorf("objectstore/loose: quarantine shard %q contains unexpected directory %q", shard, entry.Name()) - } - - if len(entry.Name()) != wantNameLen || !isHexString(entry.Name()) { - return fmt.Errorf("objectstore/loose: quarantine shard %q contains invalid object path %q", shard, entry.Name()) - } - - err := promoteLooseQuarantineObject(parent.root, filepath.Join(tempName, shard, entry.Name()), filepath.Join(shard, entry.Name())) - if err != nil { - return err - } - } - - return nil -} - -func promoteLooseQuarantineObject(root *os.Root, src, dst string) error { - err := root.Link(src, dst) - if err == nil { - _ = root.Remove(src) - - return nil - } - - if errors.Is(err, fs.ErrExist) { - _ = root.Remove(src) - - return nil - } - - return fmt.Errorf("objectstore/loose: promote quarantine %q -> %q: %w", src, dst, err) -} - -func isHexString(s string) bool { - for _, ch := range s { - if ('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F') { - continue - } - - return false - } - - return true -} diff --git a/object/store/loose/quarantine_test.go b/object/store/loose/quarantine_test.go deleted file mode 100644 index 4fd1b8f9..00000000 --- a/object/store/loose/quarantine_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package loose_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func TestLooseQuarantinePromotePublishesWrittenObjects(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - quarantiner, ok := any(store).(objectstore.ObjectQuarantiner) - if !ok { - t.Fatal("loose store does not implement ObjectQuarantiner") - } - - quarantine, err := quarantiner.BeginObjectQuarantine(objectstore.ObjectQuarantineOptions{}) - if err != nil { - t.Fatalf("BeginObjectQuarantine: %v", err) - } - - content := []byte("quarantined loose object\n") - - id, err := quarantine.WriteBytesContent(objecttype.TypeBlob, content) - if err != nil { - t.Fatalf("quarantine.WriteBytesContent: %v", err) - } - - ty, got, err := quarantine.ReadBytesContent(id) - if err != nil { - t.Fatalf("quarantine.ReadBytesContent: %v", err) - } - - if ty != objecttype.TypeBlob { - t.Fatalf("quarantine.ReadBytesContent type = %v, want %v", ty, objecttype.TypeBlob) - } - - if !bytes.Equal(got, content) { - t.Fatal("quarantine.ReadBytesContent mismatch") - } - - _, _, err = store.ReadBytesContent(id) - if err == nil { - t.Fatal("store.ReadBytesContent unexpectedly saw quarantined object before promote") - } - - err = quarantine.Promote() - if err != nil { - t.Fatalf("quarantine.Promote: %v", err) - } - - err = store.Refresh() - if err != nil { - t.Fatalf("store.Refresh: %v", err) - } - - ty, got, err = store.ReadBytesContent(id) - if err != nil { - t.Fatalf("store.ReadBytesContent after promote: %v", err) - } - - if ty != objecttype.TypeBlob { - t.Fatalf("store.ReadBytesContent type = %v, want %v", ty, objecttype.TypeBlob) - } - - if !bytes.Equal(got, content) { - t.Fatal("store.ReadBytesContent mismatch") - } - }) -} - -func TestLooseQuarantineDiscardDropsWrittenObjects(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - quarantiner, ok := any(store).(objectstore.ObjectQuarantiner) - if !ok { - t.Fatal("expected objectstore.ObjectQuarantiner") - } - - quarantine, err := quarantiner.BeginObjectQuarantine(objectstore.ObjectQuarantineOptions{}) - if err != nil { - t.Fatalf("BeginObjectQuarantine: %v", err) - } - - content := []byte("discarded loose object\n") - - id, err := quarantine.WriteBytesContent(objecttype.TypeBlob, content) - if err != nil { - t.Fatalf("quarantine.WriteBytesContent: %v", err) - } - - err = quarantine.Discard() - if err != nil { - t.Fatalf("quarantine.Discard: %v", err) - } - - err = store.Refresh() - if err != nil { - t.Fatalf("store.Refresh: %v", err) - } - - _, _, err = store.ReadBytesContent(id) - if err == nil { - t.Fatal("store.ReadBytesContent unexpectedly saw discarded object") - } - }) -} diff --git a/object/store/loose/read_bytes.go b/object/store/loose/read_bytes.go deleted file mode 100644 index 5ed3b82b..00000000 --- a/object/store/loose/read_bytes.go +++ /dev/null @@ -1,55 +0,0 @@ -package loose - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// readBytesParsed reads, inflates, and parses a loose object in one pass. -// It returns the full raw payload and its parsed type and content. -func (store *Store) readBytesParsed(id objectid.ObjectID) ([]byte, objecttype.Type, []byte, error) { - file, err := store.openObject(id) - if err != nil { - return nil, objecttype.TypeInvalid, nil, err - } - - defer func() { _ = file.Close() }() - - raw, err := decodeAll(file) - if err != nil { - return nil, objecttype.TypeInvalid, nil, err - } - - ty, content, err := parseRaw(raw) - if err != nil { - return nil, objecttype.TypeInvalid, nil, err - } - - return raw, ty, content, nil -} - -// ReadBytesFull reads a full serialized object as "type size\0content". -// -// It inflates and parses the full loose object, including verifying the zlib -// Adler-32 trailer. -func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - raw, _, _, err := store.readBytesParsed(id) - 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 (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - _, ty, content, err := store.readBytesParsed(id) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - return ty, content, nil -} diff --git a/object/store/loose/read_header.go b/object/store/loose/read_header.go deleted file mode 100644 index 37bf40de..00000000 --- a/object/store/loose/read_header.go +++ /dev/null @@ -1,37 +0,0 @@ -package loose - -import ( - "bufio" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// 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 (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - file, err := store.openObject(id) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - defer func() { _ = file.Close() }() - - zr, err := zlib.NewReader(file) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - defer func() { _ = zr.Close() }() - - _, ty, size, err := readHeader(bufio.NewReader(zr)) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - return ty, size, nil -} diff --git a/object/store/loose/read_reader.go b/object/store/loose/read_reader.go deleted file mode 100644 index c8c8d736..00000000 --- a/object/store/loose/read_reader.go +++ /dev/null @@ -1,114 +0,0 @@ -package loose - -import ( - "bufio" - "bytes" - "errors" - "io" - "os" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" - "codeberg.org/lindenii/furgit/internal/iolimit" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -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) -} - -func (reader *objectReader) Close() error { - errZlib := reader.zr.Close() - errFile := reader.file.Close() - - return errors.Join(errZlib, errFile) -} - -// openInflated opens and zlib-decodes a loose object file. -// The caller owns both returned closers and must close them. -func (store *Store) openInflated(id objectid.ObjectID) (*os.File, io.ReadCloser, error) { - file, err := store.openObject(id) - if err != nil { - return nil, nil, err - } - - zr, err := zlib.NewReader(file) - if err != nil { - _ = file.Close() - - return nil, nil, err - } - - return file, zr, nil -} - -// ReadReaderFull reads a full serialized object stream as "type size\0content". -// -// 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 (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - file, zr, err := store.openInflated(id) - if err != nil { - return nil, err - } - - br := bufio.NewReader(zr) - - header, _, size, err := readHeader(br) - if err != nil { - _ = zr.Close() - _ = file.Close() - - return nil, err - } - - return &objectReader{ - reader: io.MultiReader( - bytes.NewReader(header), - 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 (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - file, zr, err := store.openInflated(id) - if err != nil { - return objecttype.TypeInvalid, 0, nil, err - } - - br := bufio.NewReader(zr) - - _, ty, size, err := readHeader(br) - if err != nil { - _ = zr.Close() - _ = file.Close() - - return objecttype.TypeInvalid, 0, nil, err - } - - return ty, size, &objectReader{ - reader: iolimit.ExpectLengthReader(br, size), - file: file, - zr: zr, - }, nil -} diff --git a/object/store/loose/read_size.go b/object/store/loose/read_size.go deleted file mode 100644 index 2ececc49..00000000 --- a/object/store/loose/read_size.go +++ /dev/null @@ -1,13 +0,0 @@ -package loose - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// 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 (store *Store) ReadSize(id objectid.ObjectID) (int64, error) { - _, size, err := store.ReadHeader(id) - - return size, err -} diff --git a/object/store/loose/read_test.go b/object/store/loose/read_test.go deleted file mode 100644 index fcb4fe17..00000000 --- a/object/store/loose/read_test.go +++ /dev/null @@ -1,212 +0,0 @@ -package loose_test - -import ( - "bytes" - "errors" - "os" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - "codeberg.org/lindenii/furgit/object/store/loose" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func TestLooseStoreReadAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - blobID := testRepo.HashObject(t, "blob", []byte("blob body\n")) - _, treeID, commitID := testRepo.MakeCommit(t, "subject\n\nbody") - tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message") - - store := openLooseStore(t, testRepo, algo) - - tests := []struct { - name string - id objectid.ObjectID - }{ - {name: "blob", id: blobID}, - {name: "tree", id: treeID}, - {name: "commit", id: commitID}, - {name: "tag", id: tagID}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - wantType, wantBody, wantRaw := expectedRawObject(t, testRepo, tt.id) - - gotRaw, err := store.ReadBytesFull(tt.id) - if err != nil { - t.Fatalf("ReadBytesFull: %v", err) - } - - if !bytes.Equal(gotRaw, wantRaw) { - t.Fatalf("ReadBytesFull mismatch") - } - - gotType, gotBody, err := store.ReadBytesContent(tt.id) - if err != nil { - t.Fatalf("ReadBytesContent: %v", err) - } - - if gotType != wantType { - t.Fatalf("ReadBytesContent type = %v, want %v", gotType, wantType) - } - - if !bytes.Equal(gotBody, wantBody) { - t.Fatalf("ReadBytesContent body mismatch") - } - - headType, headSize, err := store.ReadHeader(tt.id) - if err != nil { - t.Fatalf("ReadHeader: %v", err) - } - - if headType != wantType { - t.Fatalf("ReadHeader type = %v, want %v", headType, wantType) - } - - if headSize != int64(len(wantBody)) { - t.Fatalf("ReadHeader size = %d, want %d", headSize, len(wantBody)) - } - - fullReader, err := store.ReadReaderFull(tt.id) - if err != nil { - t.Fatalf("ReadReaderFull: %v", err) - } - - got := mustReadAllAndClose(t, fullReader) - if !bytes.Equal(got, wantRaw) { - t.Fatalf("ReadReaderFull stream mismatch") - } - - contentType, contentSize, contentReader, err := store.ReadReaderContent(tt.id) - if err != nil { - t.Fatalf("ReadReaderContent: %v", err) - } - - if contentType != wantType { - t.Fatalf("ReadReaderContent type = %v, want %v", contentType, wantType) - } - - if contentSize != int64(len(wantBody)) { - t.Fatalf("ReadReaderContent size = %d, want %d", contentSize, len(wantBody)) - } - - got = mustReadAllAndClose(t, contentReader) - if !bytes.Equal(got, wantBody) { - t.Fatalf("ReadReaderContent stream mismatch") - } - }) - } - }) -} - -func TestLooseStoreErrors(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - notFoundID, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen())) - if err != nil { - t.Fatalf("ParseHex(notFoundID): %v", err) - } - - _, err = store.ReadBytesFull(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadBytesFull not-found error = %v", err) - } - - _, _, err = store.ReadBytesContent(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadBytesContent not-found error = %v", err) - } - - _, err = store.ReadReaderFull(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadReaderFull not-found error = %v", err) - } - - _, _, _, err = store.ReadReaderContent(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadReaderContent not-found error = %v", err) - } - - _, _, err = store.ReadHeader(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadHeader not-found error = %v", err) - } - - var otherAlgo objectid.Algorithm - if algo == objectid.AlgorithmSHA1 { - otherAlgo = objectid.AlgorithmSHA256 - } else { - otherAlgo = objectid.AlgorithmSHA1 - } - - otherID, err := objectid.ParseHex(otherAlgo, strings.Repeat("1", otherAlgo.HexLen())) - if err != nil { - t.Fatalf("ParseHex(otherID): %v", err) - } - - _, err = store.ReadBytesFull(otherID) - if err == nil || !strings.Contains(err.Error(), "algorithm mismatch") { - t.Fatalf("ReadBytesFull algorithm-mismatch error = %v", err) - } - }) -} - -func TestLooseStoreNewValidation(t *testing.T) { - t.Parallel() - - root, err := os.OpenRoot(t.TempDir()) - if err != nil { - t.Fatalf("OpenRoot: %v", err) - } - - defer func() { _ = root.Close() }() - - _, err = loose.New(root, objectid.AlgorithmUnknown) - if err == nil { - t.Fatalf("loose.New(root, unknown) expected error") - } -} - -func TestLooseStoreReadHeaderDoesNotVerifyAdler32(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - content := []byte("header-only-check\n") - - id, err := store.WriteBytesContent(objecttype.TypeBlob, content) - if err != nil { - t.Fatalf("WriteBytesContent: %v", err) - } - - corruptLooseObjectTrailer(t, testRepo, id) - - ty, size, err := store.ReadHeader(id) - if err != nil { - t.Fatalf("ReadHeader: %v", err) - } - - if ty != objecttype.TypeBlob { - t.Fatalf("ReadHeader type = %v, want %v", ty, objecttype.TypeBlob) - } - - if size != int64(len(content)) { - t.Fatalf("ReadHeader size = %d, want %d", size, len(content)) - } - - _, err = store.ReadBytesFull(id) - if err == nil { - t.Fatalf("ReadBytesFull on corrupted trailer succeeded") - } - }) -} diff --git a/object/store/loose/refresh.go b/object/store/loose/refresh.go deleted file mode 100644 index b720ebc6..00000000 --- a/object/store/loose/refresh.go +++ /dev/null @@ -1,6 +0,0 @@ -package loose - -// Refresh is a no-op for loose object stores. -func (store *Store) Refresh() error { - return nil -} diff --git a/object/store/loose/store.go b/object/store/loose/store.go deleted file mode 100644 index ea466284..00000000 --- a/object/store/loose/store.go +++ /dev/null @@ -1,43 +0,0 @@ -// Package loose provides a loose object backend (objects/XX/YYYYY..). -package loose - -import ( - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Store reads loose Git objects from an objects directory root. -// -// Loose objects are zlib streams whose trailer uses Adler-32. Which reads -// consume enough of the stream to reach and verify that trailer is documented -// on the individual methods. -// -// Labels: Close-Caller. -type Store struct { - // root is the objects directory capability used for all object file access. - // Object files are opened by relative paths like "<first2>/<rest>". - // Store borrows this root. - root *os.Root - // algo is the expected object ID algorithm for lookups. - algo objectid.Algorithm -} - -// New creates a loose-object store rooted at an objects directory for algo. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(root *os.Root, algo objectid.Algorithm) (*Store, error) { - if algo.Size() == 0 { - return nil, objectid.ErrInvalidAlgorithm - } - - return &Store{ - root: root, - algo: algo, - }, nil -} - -// Close releases resources associated with the backend. -// -// Labels: MT-Unsafe. -func (store *Store) Close() error { return nil } diff --git a/object/store/loose/write_bytes.go b/object/store/loose/write_bytes.go deleted file mode 100644 index ffc65117..00000000 --- a/object/store/loose/write_bytes.go +++ /dev/null @@ -1,18 +0,0 @@ -package loose - -import ( - "bytes" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// WriteBytesFull writes a full serialized object as "type size\0content". -func (store *Store) WriteBytesFull(raw []byte) (objectid.ObjectID, error) { - return store.WriteReaderFull(bytes.NewReader(raw)) -} - -// WriteBytesContent writes typed content bytes as a loose object. -func (store *Store) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) { - return store.WriteReaderContent(ty, int64(len(content)), bytes.NewReader(content)) -} diff --git a/object/store/loose/write_reader.go b/object/store/loose/write_reader.go deleted file mode 100644 index f686f279..00000000 --- a/object/store/loose/write_reader.go +++ /dev/null @@ -1,81 +0,0 @@ -package loose - -import ( - "fmt" - "io" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// WriteReaderContent writes one loose object from typed content bytes read from src. -// src must provide exactly size bytes. -// size is required because loose object headers are "type size\0content", so the -// header must be emitted before streaming content without buffering. -func (store *Store) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) { - if size < 0 { - return objectid.ObjectID{}, fmt.Errorf("objectstore/loose: negative content size: %d", size) - } - - header, ok := objectheader.Encode(ty, size) - if !ok { - return objectid.ObjectID{}, fmt.Errorf("objectstore/loose: failed to encode object header for type %v", ty) - } - - writer, err := store.newStreamWriter(false) - if err != nil { - return objectid.ObjectID{}, err - } - - writer.headerDone = true - writer.expectedContentLeft = size - - err = writer.writeRawChunk(header) - if err != nil { - _ = writer.Close() - _ = store.root.Remove(writer.tmpRelPath) - - return objectid.ObjectID{}, err - } - - return writeReaderIntoStreamWriter(writer, src) -} - -// WriteReaderFull writes one loose object from raw bytes "type size\0content" -// read from src. -func (store *Store) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) { - writer, err := store.newStreamWriter(true) - if err != nil { - return objectid.ObjectID{}, err - } - - return writeReaderIntoStreamWriter(writer, src) -} - -// writeReaderIntoStreamWriter copies src into writer and publishes the object. -func writeReaderIntoStreamWriter(writer *streamWriter, src io.Reader) (objectid.ObjectID, error) { - _, err := io.Copy(writer, src) - if err != nil { - _ = writer.Close() - _ = writer.store.root.Remove(writer.tmpRelPath) - - return objectid.ObjectID{}, err - } - - err = writer.Close() - if err != nil { - _ = writer.store.root.Remove(writer.tmpRelPath) - - return objectid.ObjectID{}, err - } - - id, err := writer.finalize() - if err != nil { - _ = writer.store.root.Remove(writer.tmpRelPath) - - return objectid.ObjectID{}, err - } - - return id, nil -} diff --git a/object/store/loose/write_temp_object_file.go b/object/store/loose/write_temp_object_file.go deleted file mode 100644 index 1a78db48..00000000 --- a/object/store/loose/write_temp_object_file.go +++ /dev/null @@ -1,30 +0,0 @@ -package loose - -import ( - "crypto/rand" - "errors" - "io/fs" - "os" - "path/filepath" -) - -// createTempObjectFile creates a unique temporary object file within dir. -// The returned path is relative to the objects root. -func (store *Store) createTempObjectFile(dir string) (string, *os.File, error) { - for range 16 { - relPath := filepath.Join(dir, tempObjectFilePrefix+rand.Text()) - - file, err := store.root.OpenFile(relPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) - if err == nil { - return relPath, file, nil - } - - if errors.Is(err, fs.ErrExist) { - continue - } - - return "", nil, err - } - - return "", nil, errors.New("objectstore/loose: failed to create temporary object file") -} diff --git a/object/store/loose/write_test.go b/object/store/loose/write_test.go deleted file mode 100644 index 30d8dbdb..00000000 --- a/object/store/loose/write_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package loose_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func TestLooseStoreWriteReaderContentAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - content := []byte("written-by-content-reader\n") - expectedHex := testRepo.RunInput(t, content, "hash-object", "-t", "blob", "--stdin") - - expectedID, err := objectid.ParseHex(algo, expectedHex) - if err != nil { - t.Fatalf("ParseHex(expected): %v", err) - } - - writtenID, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content)) - if err != nil { - t.Fatalf("WriteReaderContent: %v", err) - } - - if writtenID != expectedID { - t.Fatalf("WriteReaderContent id = %s, want %s", writtenID, expectedID) - } - - gotBody := testRepo.CatFile(t, "blob", writtenID) - if !bytes.Equal(gotBody, content) { - t.Fatalf("git cat-file body mismatch") - } - - // Writing the same object again should succeed and return the same ID. - writtenID2, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content)) - if err != nil { - t.Fatalf("WriteReaderContent second: %v", err) - } - - if writtenID2 != expectedID { - t.Fatalf("WriteReaderContent second id = %s, want %s", writtenID2, expectedID) - } - }) -} - -func TestLooseStoreWriteReaderFullAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - body := []byte("full-reader-body\n") - - header, ok := objectheader.Encode(objecttype.TypeBlob, int64(len(body))) - if !ok { - t.Fatalf("objectheader.Encode failed") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - wantID := algo.Sum(raw) - - gotID, err := store.WriteReaderFull(bytes.NewReader(raw)) - if err != nil { - t.Fatalf("WriteReaderFull: %v", err) - } - - if gotID != wantID { - t.Fatalf("WriteReaderFull id = %s, want %s", gotID, wantID) - } - - gotBody := testRepo.CatFile(t, "blob", gotID) - if !bytes.Equal(gotBody, body) { - t.Fatalf("git cat-file body mismatch") - } - }) -} - -func TestLooseStoreReaderValidationErrors(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Run("content overflow", func(t *testing.T) { - t.Parallel() - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - _, err := store.WriteReaderContent(objecttype.TypeBlob, 1, bytes.NewReader([]byte("hello"))) - if err == nil { - t.Fatalf("expected error after overflow") - } - }) - - t.Run("content short", func(t *testing.T) { - t.Parallel() - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - _, err := store.WriteReaderContent(objecttype.TypeBlob, 5, bytes.NewReader([]byte("x"))) - if err == nil { - t.Fatalf("expected error for short content") - } - }) - - t.Run("full malformed header", func(t *testing.T) { - t.Parallel() - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - _, err := store.WriteReaderFull(bytes.NewReader([]byte("not-a-header"))) - if err == nil { - t.Fatalf("expected error for malformed header") - } - }) - - t.Run("full size mismatch", func(t *testing.T) { - t.Parallel() - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - raw := []byte("blob 1\x00hello") - - _, err := store.WriteReaderFull(bytes.NewReader(raw)) - if err == nil { - t.Fatalf("expected error after mismatch") - } - }) - }) -} diff --git a/object/store/loose/write_writer.go b/object/store/loose/write_writer.go deleted file mode 100644 index 0d6b5b80..00000000 --- a/object/store/loose/write_writer.go +++ /dev/null @@ -1,94 +0,0 @@ -package loose - -import ( - "errors" - "hash" - "os" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" -) - -const tempObjectFilePrefix = "tmp_obj_" - -// streamWriter incrementally hashes and deflates an object into a temp file. -// Finalize validates size accounting and atomically renames the temp file. -type streamWriter struct { - // store owns path and root operations used by this write session. - store *Store - // file is the temporary destination file under objects/. - file *os.File - // zw compresses raw object bytes into file. - zw *zlib.Writer - // hash receives the same raw bytes used to compute the resulting object ID. - hash hash.Hash - - // tmpRelPath is the relative path of file under the objects root. - tmpRelPath string - - // fullMode selects full-object input ("type size\0content") as opposed to content-only input. - fullMode bool - - // headerBuf accumulates header bytes while fullMode parses up to the first NUL. - headerBuf []byte - // headerDone reports whether the full-object header has been parsed. - headerDone bool - // expectedContentLeft tracks remaining declared content bytes. - expectedContentLeft int64 - - closed bool - finalized bool -} - -// newStreamWriter creates a stream writer with a temp file rooted in objects/. -func (store *Store) newStreamWriter(fullMode bool) (*streamWriter, error) { - hashFn, err := store.algo.New() - if err != nil { - return nil, err - } - - tmpRelPath, file, err := store.createTempObjectFile(".") - if err != nil { - return nil, err - } - - return &streamWriter{ - store: store, - file: file, - zw: zlib.NewWriter(file), - hash: hashFn, - tmpRelPath: tmpRelPath, - fullMode: fullMode, - headerBuf: make([]byte, 0, 64), - }, nil -} - -// Write validates and writes raw bytes into the stream. -// In full mode, it parses and enforces the streamed header-declared content size. -func (writer *streamWriter) Write(src []byte) (int, error) { - if writer.finalized { - return 0, errors.New("objectstore/loose: write after finalize") - } - - if writer.closed { - return 0, errors.New("objectstore/loose: write after close") - } - - if writer.fullMode { - err := writer.acceptFull(src) - if err != nil { - return 0, err - } - } else { - err := writer.acceptContent(int64(len(src))) - if err != nil { - return 0, err - } - } - - err := writer.writeRawChunk(src) - if err != nil { - return 0, err - } - - return len(src), nil -} diff --git a/object/store/loose/write_writer_accept.go b/object/store/loose/write_writer_accept.go deleted file mode 100644 index bf55966a..00000000 --- a/object/store/loose/write_writer_accept.go +++ /dev/null @@ -1,61 +0,0 @@ -package loose - -import ( - "bytes" - "errors" - - objectheader "codeberg.org/lindenii/furgit/object/header" -) - -// acceptFull validates and accounts raw full-object input. -func (writer *streamWriter) acceptFull(src []byte) error { - if !writer.headerDone { - nul := bytes.IndexByte(src, 0) - if nul >= 0 { - headerChunkLen := nul + 1 - writer.headerBuf = append(writer.headerBuf, src[:headerChunkLen]...) - - _, size, _, ok := objectheader.Parse(writer.headerBuf) - if !ok { - return errors.New("objectstore/loose: malformed object header") - } - - writer.headerDone = true - writer.expectedContentLeft = size - - return writer.acceptContent(int64(len(src) - headerChunkLen)) - } - - writer.headerBuf = append(writer.headerBuf, src...) - - return nil - } - - return writer.acceptContent(int64(len(src))) -} - -// acceptContent validates and accounts content byte counts. -func (writer *streamWriter) acceptContent(n int64) error { - if n > writer.expectedContentLeft { - return errors.New("objectstore/loose: object content exceeds declared size") - } - - writer.expectedContentLeft -= n - - return nil -} - -// writeRawChunk forwards raw bytes to the hash and deflate pipeline. -func (writer *streamWriter) writeRawChunk(src []byte) error { - _, err := writer.hash.Write(src) - if err != nil { - return err - } - - _, err = writer.zw.Write(src) - if err != nil { - return err - } - - return nil -} diff --git a/object/store/loose/write_writer_finalize.go b/object/store/loose/write_writer_finalize.go deleted file mode 100644 index 71e275db..00000000 --- a/object/store/loose/write_writer_finalize.go +++ /dev/null @@ -1,89 +0,0 @@ -package loose - -import ( - "errors" - "io/fs" - "path/filepath" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Close flushes and closes the underlying zlib stream and temp file. -func (writer *streamWriter) Close() error { - errZlib := writer.zw.Close() - errSync := writer.file.Sync() - errFile := writer.file.Close() - - writer.closed = true - writer.file = nil - - return errors.Join(errZlib, errSync, errFile) -} - -// finalize validates write completeness and atomically publishes the object. -// Publication is no-clobber: it links tmpRelPath to the object path and treats -// existing destination objects as success. -func (writer *streamWriter) finalize() (objectid.ObjectID, error) { - writer.finalized = true - - var zero objectid.ObjectID - - if !writer.closed { - err := writer.Close() - if err != nil { - return zero, err - } - } - - if writer.fullMode && !writer.headerDone { - return zero, errors.New("objectstore/loose: missing full object header") - } - - if writer.expectedContentLeft != 0 { - return zero, errors.New("objectstore/loose: object content shorter than declared size") - } - - idBytes := writer.hash.Sum(nil) - - id, err := objectid.FromBytes(writer.store.algo, idBytes) - if err != nil { - return zero, err - } - - relPath, err := writer.store.objectPath(id) - if err != nil { - return zero, err - } - - dir := filepath.Dir(relPath) - - err = writer.store.root.MkdirAll(dir, 0o755) - if err != nil { - return zero, err - } - - cleanup := true - - defer func() { - if cleanup { - _ = writer.store.root.Remove(writer.tmpRelPath) - } - }() - - err = writer.store.root.Link(writer.tmpRelPath, relPath) - if err != nil { - if errors.Is(err, fs.ErrExist) { - cleanup = false - _ = writer.store.root.Remove(writer.tmpRelPath) - - return id, nil - } - - return zero, err - } - - cleanup = false - _ = writer.store.root.Remove(writer.tmpRelPath) - - return id, nil -} diff --git a/object/store/memory/algorithm.go b/object/store/memory/algorithm.go deleted file mode 100644 index bf7f3a82..00000000 --- a/object/store/memory/algorithm.go +++ /dev/null @@ -1,8 +0,0 @@ -package memory - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Algorithm returns the object ID algorithm used by the store. -func (store *Store) Algorithm() objectid.Algorithm { - return store.algo -} diff --git a/object/store/memory/doc.go b/object/store/memory/doc.go deleted file mode 100644 index cb40d466..00000000 --- a/object/store/memory/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package memory provides one in-memory object store. -package memory diff --git a/object/store/memory/object.go b/object/store/memory/object.go deleted file mode 100644 index a85175c7..00000000 --- a/object/store/memory/object.go +++ /dev/null @@ -1,9 +0,0 @@ -package memory - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// storedObject is one in-memory object entry. -type storedObject struct { - ty objecttype.Type - content []byte -} diff --git a/object/store/memory/read_bytes.go b/object/store/memory/read_bytes.go deleted file mode 100644 index 48d3694a..00000000 --- a/object/store/memory/read_bytes.go +++ /dev/null @@ -1,37 +0,0 @@ -package memory - -import ( - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadBytesFull reads one full object, including the object header. -func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - obj, ok := store.objects[id] - if !ok { - return nil, objectstore.ErrObjectNotFound - } - - header, ok := objectheader.Encode(obj.ty, int64(len(obj.content))) - if !ok { - panic("failed to encode object header") - } - - raw := make([]byte, len(header)+len(obj.content)) - copy(raw, header) - copy(raw[len(header):], obj.content) - - return raw, nil -} - -// ReadBytesContent reads one object body. -func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - obj, ok := store.objects[id] - if !ok { - return objecttype.TypeInvalid, nil, objectstore.ErrObjectNotFound - } - - return obj.ty, append([]byte(nil), obj.content...), nil -} diff --git a/object/store/memory/read_header.go b/object/store/memory/read_header.go deleted file mode 100644 index da3acd1c..00000000 --- a/object/store/memory/read_header.go +++ /dev/null @@ -1,17 +0,0 @@ -package memory - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadHeader reads one object header. -func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - obj, ok := store.objects[id] - if !ok { - return objecttype.TypeInvalid, 0, objectstore.ErrObjectNotFound - } - - return obj.ty, int64(len(obj.content)), nil -} diff --git a/object/store/memory/read_reader.go b/object/store/memory/read_reader.go deleted file mode 100644 index 425c3034..00000000 --- a/object/store/memory/read_reader.go +++ /dev/null @@ -1,29 +0,0 @@ -package memory - -import ( - "bytes" - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadReaderFull reads one full object through a reader. -func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - raw, err := store.ReadBytesFull(id) - if err != nil { - return nil, err - } - - return io.NopCloser(bytes.NewReader(raw)), nil -} - -// ReadReaderContent reads one object body through a reader. -func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - ty, content, err := store.ReadBytesContent(id) - if err != nil { - return objecttype.TypeInvalid, 0, nil, err - } - - return ty, int64(len(content)), io.NopCloser(bytes.NewReader(content)), nil -} diff --git a/object/store/memory/read_size.go b/object/store/memory/read_size.go deleted file mode 100644 index 7045bd61..00000000 --- a/object/store/memory/read_size.go +++ /dev/null @@ -1,13 +0,0 @@ -package memory - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// ReadSize reads one object size. -func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) { - _, size, err := store.ReadHeader(id) - if err != nil { - return 0, err - } - - return size, nil -} diff --git a/object/store/memory/refresh.go b/object/store/memory/refresh.go deleted file mode 100644 index 1e18eef3..00000000 --- a/object/store/memory/refresh.go +++ /dev/null @@ -1,6 +0,0 @@ -package memory - -// Refresh is a no-op for in-memory object stores. -func (store *Store) Refresh() error { - return nil -} diff --git a/object/store/memory/store.go b/object/store/memory/store.go deleted file mode 100644 index ff66da50..00000000 --- a/object/store/memory/store.go +++ /dev/null @@ -1,28 +0,0 @@ -package memory - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Store is one in-memory object store. -// -// Labels: Close-Caller. -type Store struct { - algo objectid.Algorithm - objects map[objectid.ObjectID]storedObject -} - -// New builds one empty in-memory store for one object format. -func New(algo objectid.Algorithm) *Store { - return &Store{ - algo: algo, - objects: make(map[objectid.ObjectID]storedObject), - } -} - -// Close closes the in-memory store. -// -// Labels: MT-Unsafe. -func (store *Store) Close() error { - return nil -} diff --git a/object/store/memory/write_bytes.go b/object/store/memory/write_bytes.go deleted file mode 100644 index 241169d9..00000000 --- a/object/store/memory/write_bytes.go +++ /dev/null @@ -1,35 +0,0 @@ -package memory - -import ( - "bytes" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// WriteBytesContent writes one typed object content byte slice. -func (store *Store) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) { - id := store.algo.Sum(buildRawObject(ty, content)) - store.objects[id] = storedObject{ty: ty, content: append([]byte(nil), content...)} - - return id, nil -} - -// WriteBytesFull writes one full serialized object byte slice as "type size\0content". -func (store *Store) WriteBytesFull(raw []byte) (objectid.ObjectID, error) { - return store.WriteReaderFull(bytes.NewReader(raw)) -} - -func buildRawObject(ty objecttype.Type, body []byte) []byte { - header, ok := objectheader.Encode(ty, int64(len(body))) - if !ok { - panic("failed to encode object header") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return raw -} diff --git a/object/store/memory/write_reader.go b/object/store/memory/write_reader.go deleted file mode 100644 index 0fa6a13f..00000000 --- a/object/store/memory/write_reader.go +++ /dev/null @@ -1,55 +0,0 @@ -package memory - -import ( - "errors" - "fmt" - "io" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// WriteReaderContent writes one typed object content stream. -func (store *Store) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) { - if size < 0 { - return objectid.ObjectID{}, fmt.Errorf("objectstore/memory: negative content size: %d", size) - } - - content, err := io.ReadAll(io.LimitReader(src, size+1)) - if err != nil { - return objectid.ObjectID{}, err - } - - switch { - case int64(len(content)) > size: - return objectid.ObjectID{}, errors.New("objectstore/memory: object content longer than declared size") - case int64(len(content)) < size: - return objectid.ObjectID{}, errors.New("objectstore/memory: object content shorter than declared size") - } - - return store.WriteBytesContent(ty, content) -} - -// WriteReaderFull writes one full serialized object stream as "type size\0content". -func (store *Store) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) { - raw, err := io.ReadAll(src) - if err != nil { - return objectid.ObjectID{}, err - } - - ty, size, headerLen, ok := objectheader.Parse(raw) - if !ok { - return objectid.ObjectID{}, errors.New("objectstore/memory: malformed object header") - } - - content := raw[headerLen:] - if int64(len(content)) != size { - return objectid.ObjectID{}, errors.New("objectstore/memory: object header size/content mismatch") - } - - id := store.algo.Sum(raw) - store.objects[id] = storedObject{ty: ty, content: append([]byte(nil), content...)} - - return id, nil -} diff --git a/object/store/memory/write_test.go b/object/store/memory/write_test.go deleted file mode 100644 index 9f38a14b..00000000 --- a/object/store/memory/write_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package memory_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/memory" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func TestStoreWriteReaderContent(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - content := []byte("memory-content\n") - - gotID, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content)) - if err != nil { - t.Fatalf("WriteReaderContent: %v", err) - } - - wantID := algo.Sum(buildRawObject(t, objecttype.TypeBlob, content)) - if gotID != wantID { - t.Fatalf("WriteReaderContent id = %s, want %s", gotID, wantID) - } - - gotType, gotContent, err := store.ReadBytesContent(gotID) - if err != nil { - t.Fatalf("ReadBytesContent: %v", err) - } - - if gotType != objecttype.TypeBlob { - t.Fatalf("ReadBytesContent type = %v, want %v", gotType, objecttype.TypeBlob) - } - - if !bytes.Equal(gotContent, content) { - t.Fatalf("ReadBytesContent content mismatch") - } - }) -} - -func TestStoreWriteReaderFull(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - content := []byte("memory-full\n") - raw := buildRawObject(t, objecttype.TypeBlob, content) - - gotID, err := store.WriteReaderFull(bytes.NewReader(raw)) - if err != nil { - t.Fatalf("WriteReaderFull: %v", err) - } - - wantID := algo.Sum(raw) - if gotID != wantID { - t.Fatalf("WriteReaderFull id = %s, want %s", gotID, wantID) - } - - gotRaw, err := store.ReadBytesFull(gotID) - if err != nil { - t.Fatalf("ReadBytesFull: %v", err) - } - - if !bytes.Equal(gotRaw, raw) { - t.Fatalf("ReadBytesFull mismatch") - } - }) -} - -func TestStoreWriteBytes(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - content := []byte("memory-bytes\n") - - gotID, err := store.WriteBytesContent(objecttype.TypeBlob, content) - if err != nil { - t.Fatalf("WriteBytesContent: %v", err) - } - - wantID := algo.Sum(buildRawObject(t, objecttype.TypeBlob, content)) - if gotID != wantID { - t.Fatalf("WriteBytesContent id = %s, want %s", gotID, wantID) - } - - raw := buildRawObject(t, objecttype.TypeBlob, content) - - gotID2, err := store.WriteBytesFull(raw) - if err != nil { - t.Fatalf("WriteBytesFull: %v", err) - } - - if gotID2 != wantID { - t.Fatalf("WriteBytesFull id = %s, want %s", gotID2, wantID) - } - }) -} - -func TestStoreWriteReaderValidationErrors(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Run("content overflow", func(t *testing.T) { - t.Parallel() - - store := memory.New(algo) - - _, err := store.WriteReaderContent(objecttype.TypeBlob, 1, bytes.NewReader([]byte("hello"))) - if err == nil { - t.Fatalf("expected error after overflow") - } - }) - - t.Run("content short", func(t *testing.T) { - t.Parallel() - - store := memory.New(algo) - - _, err := store.WriteReaderContent(objecttype.TypeBlob, 5, bytes.NewReader([]byte("x"))) - if err == nil { - t.Fatalf("expected error for short content") - } - }) - - t.Run("full malformed header", func(t *testing.T) { - t.Parallel() - - store := memory.New(algo) - - _, err := store.WriteReaderFull(bytes.NewReader([]byte("not-a-header"))) - if err == nil { - t.Fatalf("expected error for malformed header") - } - }) - - t.Run("full size mismatch", func(t *testing.T) { - t.Parallel() - - store := memory.New(algo) - - _, err := store.WriteReaderFull(bytes.NewReader([]byte("blob 1\x00hello"))) - if err == nil { - t.Fatalf("expected error after mismatch") - } - }) - - t.Run("bytes malformed header", func(t *testing.T) { - t.Parallel() - - store := memory.New(algo) - - _, err := store.WriteBytesFull([]byte("not-a-header")) - if err == nil { - t.Fatalf("expected error for malformed byte header") - } - }) - }) -} - -func TestBuildRawObjectMatchesObjectHeaderEncode(t *testing.T) { - t.Parallel() - - content := []byte("body") - raw := buildRawObject(t, objecttype.TypeBlob, content) - - header, ok := objectheader.Encode(objecttype.TypeBlob, int64(len(content))) - if !ok { - t.Fatalf("objectheader.Encode failed") - } - - want := append(append([]byte(nil), header...), content...) - if !bytes.Equal(raw, want) { - t.Fatalf("buildRawObject mismatch") - } -} - -func buildRawObject(tb testing.TB, ty objecttype.Type, body []byte) []byte { //nolint:unparam - tb.Helper() - - header, ok := objectheader.Encode(ty, int64(len(body))) - if !ok { - tb.Fatalf("objectheader.Encode(%v, %d) failed", ty, len(body)) - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return raw -} diff --git a/object/store/mix/bytes.go b/object/store/mix/bytes.go deleted file mode 100644 index 5b62ff06..00000000 --- a/object/store/mix/bytes.go +++ /dev/null @@ -1,51 +0,0 @@ -package mix - -import ( - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadBytesFull reads a full serialized object from one backend that has it. -func (mix *Mix) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { - full, err := backend.ReadBytesFull(id) - if err == nil { - mix.touchBackend(backend) - - return full, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return nil, fmt.Errorf("objectstore: backend %d read bytes full: %w", i, err) - } - - return nil, objectstore.ErrObjectNotFound -} - -// ReadBytesContent reads an object's type and content bytes from one backend -// that has it. -func (mix *Mix) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { - ty, content, err := backend.ReadBytesContent(id) - if err == nil { - mix.touchBackend(backend) - - return ty, content, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return objecttype.TypeInvalid, nil, fmt.Errorf("objectstore: backend %d read bytes content: %w", i, err) - } - - return objecttype.TypeInvalid, nil, objectstore.ErrObjectNotFound -} diff --git a/object/store/mix/header.go b/object/store/mix/header.go deleted file mode 100644 index d57375ec..00000000 --- a/object/store/mix/header.go +++ /dev/null @@ -1,30 +0,0 @@ -package mix - -import ( - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadHeader reads object header data from one backend that has it. -func (mix *Mix) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { - ty, size, err := backend.ReadHeader(id) - if err == nil { - mix.touchBackend(backend) - - return ty, size, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore: backend %d read header: %w", i, err) - } - - return objecttype.TypeInvalid, 0, objectstore.ErrObjectNotFound -} diff --git a/object/store/mix/mix.go b/object/store/mix/mix.go deleted file mode 100644 index 65ed97c8..00000000 --- a/object/store/mix/mix.go +++ /dev/null @@ -1,20 +0,0 @@ -// Package mix provides an adaptive wrapper over multiple object storage -// backends. -package mix - -import ( - "sync" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// Mix queries multiple object databases with an MRU backend preference. -// -// Labels: Close-Caller. -type Mix struct { - mu sync.RWMutex - - backendHead *backendNode - backendTail *backendNode - backendNodeByStore map[objectstore.Reader]*backendNode -} diff --git a/object/store/mix/mru.go b/object/store/mix/mru.go deleted file mode 100644 index b48f1448..00000000 --- a/object/store/mix/mru.go +++ /dev/null @@ -1,74 +0,0 @@ -package mix - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -type backendNode struct { - backend objectstore.Reader - prev *backendNode - next *backendNode -} - -//nolint:ireturn -func (mix *Mix) firstBackend() objectstore.Reader { - mix.mu.RLock() - defer mix.mu.RUnlock() - - if mix.backendHead == nil { - return nil - } - - return mix.backendHead.backend -} - -//nolint:ireturn -func (mix *Mix) nextBackend(current objectstore.Reader) objectstore.Reader { - mix.mu.RLock() - defer mix.mu.RUnlock() - - node := mix.backendNodeByStore[current] - if node == nil || node.next == nil { - return nil - } - - return node.next.backend -} - -func (mix *Mix) touchBackend(backend objectstore.Reader) { - if backend == nil { - return - } - - if !mix.mu.TryLock() { - return - } - defer mix.mu.Unlock() - - node := mix.backendNodeByStore[backend] - if node == nil || node == mix.backendHead { - return - } - - if node.prev != nil { - node.prev.next = node.next - } - - if node.next != nil { - node.next.prev = node.prev - } - - if mix.backendTail == node { - mix.backendTail = node.prev - } - - node.prev = nil - - node.next = mix.backendHead - if mix.backendHead != nil { - mix.backendHead.prev = node - } - - mix.backendHead = node - if mix.backendTail == nil { - mix.backendTail = node - } -} diff --git a/object/store/mix/new.go b/object/store/mix/new.go deleted file mode 100644 index abc6c8ee..00000000 --- a/object/store/mix/new.go +++ /dev/null @@ -1,40 +0,0 @@ -package mix - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -// New creates a Mix from backends. -// -// The provided backends must be non-nil and distinct. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(backends ...objectstore.Reader) *Mix { - nodeByStore := make(map[objectstore.Reader]*backendNode, len(backends)) - - var ( - head *backendNode - tail *backendNode - ) - - for _, backend := range backends { - node := &backendNode{ - backend: backend, - prev: tail, - } - if tail != nil { - tail.next = node - } - - if head == nil { - head = node - } - - tail = node - nodeByStore[backend] = node - } - - return &Mix{ - backendHead: head, - backendTail: tail, - backendNodeByStore: nodeByStore, - } -} diff --git a/object/store/mix/reader.go b/object/store/mix/reader.go deleted file mode 100644 index 8d515c50..00000000 --- a/object/store/mix/reader.go +++ /dev/null @@ -1,53 +0,0 @@ -package mix - -import ( - "errors" - "fmt" - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadReaderFull reads a full serialized object stream from one backend that -// has it. -func (mix *Mix) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { - reader, err := backend.ReadReaderFull(id) - if err == nil { - mix.touchBackend(backend) - - return reader, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return nil, fmt.Errorf("objectstore: backend %d read reader full: %w", i, err) - } - - return nil, objectstore.ErrObjectNotFound -} - -// ReadReaderContent reads an object's type, declared content length, and -// content stream from one backend that has it. -func (mix *Mix) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { - ty, size, reader, err := backend.ReadReaderContent(id) - if err == nil { - mix.touchBackend(backend) - - return ty, size, reader, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return objecttype.TypeInvalid, 0, nil, fmt.Errorf("objectstore: backend %d read reader content: %w", i, err) - } - - return objecttype.TypeInvalid, 0, nil, objectstore.ErrObjectNotFound -} diff --git a/object/store/mix/refresh.go b/object/store/mix/refresh.go deleted file mode 100644 index bbae6efe..00000000 --- a/object/store/mix/refresh.go +++ /dev/null @@ -1,30 +0,0 @@ -package mix - -import ( - "errors" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// Refresh forwards refresh calls to refresh-capable backends. -func (mix *Mix) Refresh() error { - mix.mu.RLock() - - backends := make([]objectstore.Reader, 0, len(mix.backendNodeByStore)) - for node := mix.backendHead; node != nil; node = node.next { - backends = append(backends, node.backend) - } - - mix.mu.RUnlock() - - var errs []error - - for _, backend := range backends { - err := backend.Refresh() - if err != nil { - errs = append(errs, err) - } - } - - return errors.Join(errs...) -} diff --git a/object/store/mix/size.go b/object/store/mix/size.go deleted file mode 100644 index 4feb142e..00000000 --- a/object/store/mix/size.go +++ /dev/null @@ -1,29 +0,0 @@ -package mix - -import ( - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// ReadSize reads object content length from one backend that has it. -func (mix *Mix) ReadSize(id objectid.ObjectID) (int64, error) { - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { - size, err := backend.ReadSize(id) - if err == nil { - mix.touchBackend(backend) - - return size, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return 0, fmt.Errorf("objectstore: backend %d read size: %w", i, err) - } - - return 0, objectstore.ErrObjectNotFound -} diff --git a/object/store/packed/doc.go b/object/store/packed/doc.go deleted file mode 100644 index 55189aa1..00000000 --- a/object/store/packed/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package packed provides Git object reading from, and pack writing to, -// an objects/pack directory. -package packed diff --git a/object/store/packed/internal/doc.go b/object/store/packed/internal/doc.go deleted file mode 100644 index 05a9c2be..00000000 --- a/object/store/packed/internal/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package internal encapsulates packed store implementation details. -// -// We have separate internal subpackages for ingest vs read and such, -// because these operations are so different that they almost share -// no code. This makes things clearer. -package internal diff --git a/object/store/packed/internal/ingest/TODO b/object/store/packed/internal/ingest/TODO deleted file mode 100644 index bfb722c1..00000000 --- a/object/store/packed/internal/ingest/TODO +++ /dev/null @@ -1 +0,0 @@ -multi-threaded delta resolution and index computation? diff --git a/object/store/packed/internal/ingest/byteslice_reader.go b/object/store/packed/internal/ingest/byteslice_reader.go deleted file mode 100644 index a1570ef3..00000000 --- a/object/store/packed/internal/ingest/byteslice_reader.go +++ /dev/null @@ -1,21 +0,0 @@ -package ingest - -import "io" - -// byteSliceReader implements io.ByteReader on []byte. -type byteSliceReader struct { - data []byte - pos int -} - -// ReadByte reads one byte from receiver. -func (reader *byteSliceReader) ReadByte() (byte, error) { - if reader.pos >= len(reader.data) { - return 0, io.EOF - } - - b := reader.data[reader.pos] - reader.pos++ - - return b, nil -} diff --git a/object/store/packed/internal/ingest/cache.go b/object/store/packed/internal/ingest/cache.go deleted file mode 100644 index 9a15f55f..00000000 --- a/object/store/packed/internal/ingest/cache.go +++ /dev/null @@ -1,53 +0,0 @@ -package ingest - -import ( - "codeberg.org/lindenii/furgit/internal/lru" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// deltaBaseCacheKey identifies one resolved base by record index. -type deltaBaseCacheKey struct { - recordIdx int -} - -// deltaBaseCacheValue stores one resolved base object payload. -type deltaBaseCacheValue struct { - realType objecttype.Type - content []byte -} - -// deltaBaseCache is a bounded LRU for resolved base payloads. -type deltaBaseCache struct { - lru *lru.Cache[deltaBaseCacheKey, deltaBaseCacheValue] -} - -// newDeltaBaseCache creates one bounded base cache. -func newDeltaBaseCache(maxBytes int64) *deltaBaseCache { - return &deltaBaseCache{ - lru: lru.New( - maxBytes, - func(_ deltaBaseCacheKey, value deltaBaseCacheValue) int64 { - return int64(len(value.content)) - }, - nil, - ), - } -} - -// get returns one cache entry for recordIdx. -func (cache *deltaBaseCache) get(recordIdx int) (objecttype.Type, []byte, bool) { - value, ok := cache.lru.Get(deltaBaseCacheKey{recordIdx: recordIdx}) - if !ok { - return objecttype.TypeInvalid, nil, false - } - - return value.realType, value.content, true -} - -// add stores one cache entry for recordIdx. -func (cache *deltaBaseCache) add(recordIdx int, realType objecttype.Type, content []byte) { - cache.lru.Add(deltaBaseCacheKey{recordIdx: recordIdx}, deltaBaseCacheValue{ - realType: realType, - content: content, - }) -} diff --git a/object/store/packed/internal/ingest/counting_writer.go b/object/store/packed/internal/ingest/counting_writer.go deleted file mode 100644 index 051ad9d1..00000000 --- a/object/store/packed/internal/ingest/counting_writer.go +++ /dev/null @@ -1,17 +0,0 @@ -package ingest - -import "io" - -// countingWriter counts bytes written to dst. -type countingWriter struct { - dst io.Writer - n int -} - -// Write writes src to dst and tracks output byte count. -func (writer *countingWriter) Write(src []byte) (int, error) { - n, err := writer.dst.Write(src) - writer.n += n - - return n, err -} diff --git a/object/store/packed/internal/ingest/crc.go b/object/store/packed/internal/ingest/crc.go deleted file mode 100644 index f55af4ff..00000000 --- a/object/store/packed/internal/ingest/crc.go +++ /dev/null @@ -1,22 +0,0 @@ -package ingest - -import "fmt" - -// beginEntryCRC starts inline CRC accumulation for one packed entry. -func (scanner *streamScanner) beginEntryCRC() { - scanner.entryCRC = 0 - scanner.inEntryCRC = true -} - -// endEntryCRC finishes inline CRC accumulation for one packed entry. -func (scanner *streamScanner) endEntryCRC() (uint32, error) { - if !scanner.inEntryCRC { - return 0, fmt.Errorf("packfile/ingest: entry CRC not started") - } - - crc := scanner.entryCRC - scanner.entryCRC = 0 - scanner.inEntryCRC = false - - return crc, nil -} diff --git a/object/store/packed/internal/ingest/delta_header.go b/object/store/packed/internal/ingest/delta_header.go deleted file mode 100644 index 110cf83b..00000000 --- a/object/store/packed/internal/ingest/delta_header.go +++ /dev/null @@ -1,11 +0,0 @@ -package ingest - -import deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply" - -// finalizeStreamPackHash consumes trailer bytes and verifies stream integrity. -// readDeltaHeaderSizes reads source and destination sizes from one delta payload. -func readDeltaHeaderSizes(payload []byte) (int, int, error) { - reader := &byteSliceReader{data: payload} - - return deltaapply.ReadHeaderSizes(reader) -} diff --git a/object/store/packed/internal/ingest/distance.go b/object/store/packed/internal/ingest/distance.go deleted file mode 100644 index 9bc4d886..00000000 --- a/object/store/packed/internal/ingest/distance.go +++ /dev/null @@ -1,30 +0,0 @@ -package ingest - -import ( - "fmt" - "io" -) - -// readOfsDistanceFromStream reads one ofs-delta encoded distance. -func readOfsDistanceFromStream(reader io.ByteReader) (uint64, int, error) { - first, err := reader.ReadByte() - if err != nil { - return 0, 0, fmt.Errorf("read ofs distance first byte: %w", err) - } - - dist := uint64(first & 0x7f) - consumed := 1 - - b := first - for b&0x80 != 0 { - b, err = reader.ReadByte() - if err != nil { - return 0, 0, fmt.Errorf("read ofs distance continuation: %w", err) - } - - consumed++ - dist = ((dist + 1) << 7) + uint64(b&0x7f) - } - - return dist, consumed, nil -} diff --git a/object/store/packed/internal/ingest/doc.go b/object/store/packed/internal/ingest/doc.go deleted file mode 100644 index 074012de..00000000 --- a/object/store/packed/internal/ingest/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package ingest implements streaming ingestion of one Git pack stream into a -// packed destination root, producing .pack/.idx and optionally .rev. -package ingest diff --git a/object/store/packed/internal/ingest/drain.go b/object/store/packed/internal/ingest/drain.go deleted file mode 100644 index 7179a823..00000000 --- a/object/store/packed/internal/ingest/drain.go +++ /dev/null @@ -1,67 +0,0 @@ -package ingest - -import ( - "fmt" - "io" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// drainEntryPayload inflates one entry payload from stream and returns -// (inflatedLength, oidForBaseEntry). -func drainEntryPayload(state *ingestState, record objectRecord) (int64, objectid.ObjectID, error) { - var zero objectid.ObjectID - - reader, err := zlib.NewReader(state.stream) - if err != nil { - return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("open zlib stream: %v", err)} - } - - defer func() { _ = reader.Close() }() - - var total int64 - - if record.packedType.IsBaseObject() { - header, ok := objectheader.Encode(record.packedType, record.declaredSize) - if !ok { - return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: "encode object header"} - } - - hashImpl, err := state.algo.New() - if err != nil { - return 0, zero, err - } - - _, _ = hashImpl.Write(header) - - n, err := io.Copy(hashImpl, reader) - if err != nil { - return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("inflate base object: %v", err)} - } - - total = n - - oid, err := objectid.FromBytes(state.algo, hashImpl.Sum(nil)) - if err != nil { - return 0, zero, err - } - - return total, oid, nil - } - - if record.packedType == objecttype.TypeOfsDelta || record.packedType == objecttype.TypeRefDelta { - n, err := io.Copy(io.Discard, reader) - if err != nil { - return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("inflate delta payload: %v", err)} - } - - total = n - - return total, zero, nil - } - - return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: "unsupported payload type"} -} diff --git a/object/store/packed/internal/ingest/entry.go b/object/store/packed/internal/ingest/entry.go deleted file mode 100644 index 363e213c..00000000 --- a/object/store/packed/internal/ingest/entry.go +++ /dev/null @@ -1,91 +0,0 @@ -package ingest - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// scanOneEntry scans one pack entry from stream and appends one record. -func scanOneEntry(state *ingestState, startOffset uint64) (uint64, error) { - state.stream.beginEntryCRC() - - record, err := parseEntryPrefix(state, startOffset) - if err != nil { - return 0, err - } - - payloadStartConsumed := state.stream.consumed - - contentLen, oid, err := drainEntryPayload(state, record) - if err != nil { - return 0, err - } - - consumedInput := state.stream.consumed - payloadStartConsumed - - if contentLen != record.declaredSize { - return 0, &MalformedPackEntryError{ - Offset: startOffset, - Reason: fmt.Sprintf("inflated size mismatch got %d want %d", contentLen, record.declaredSize), - } - } - - endOffset := startOffset + uint64(record.headerLen) + consumedInput - if endOffset > state.stream.consumed { - return 0, &MalformedPackEntryError{ - Offset: startOffset, - Reason: fmt.Sprintf("entry end offset overflow got %d > stream %d", endOffset, state.stream.consumed), - } - } - - record.packedLen = endOffset - startOffset - - record.dataOffset = startOffset + uint64(record.headerLen) - if record.packedLen < uint64(record.headerLen) { - return 0, &MalformedPackEntryError{Offset: startOffset, Reason: "negative payload span"} - } - - crc, err := state.stream.endEntryCRC() - if err != nil { - return 0, err - } - - record.crc32 = crc - - if record.packedType.IsBaseObject() { - record.objectID = oid - record.realType = record.packedType - record.resolved = true - } - - recordIdx := len(state.records) - state.records = append(state.records, record) - - state.offsetToRecord[record.offset] = recordIdx - if record.resolved { - state.objectToRecord[record.objectID] = recordIdx - } - - switch record.packedType { - case objecttype.TypeOfsDelta: - state.ofsDeltas = append(state.ofsDeltas, ofsDeltaRef{ - baseOffset: record.baseOffset, - recordIdx: recordIdx, - }) - case objecttype.TypeRefDelta: - state.refDeltas = append(state.refDeltas, refDeltaRef{ - baseObject: record.baseObject, - recordIdx: recordIdx, - }) - case objecttype.TypeInvalid, - objecttype.TypeCommit, - objecttype.TypeTree, - objecttype.TypeBlob, - objecttype.TypeTag, - objecttype.TypeFuture: - default: - } - - return endOffset, nil -} diff --git a/object/store/packed/internal/ingest/entry_header.go b/object/store/packed/internal/ingest/entry_header.go deleted file mode 100644 index c74fdc16..00000000 --- a/object/store/packed/internal/ingest/entry_header.go +++ /dev/null @@ -1,33 +0,0 @@ -package ingest - -import ( - "codeberg.org/lindenii/furgit/internal/intconv" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// encodePackEntryHeader encodes one non-delta packed entry header. -func encodePackEntryHeader(ty objecttype.Type, size int64) []byte { - var out [16]byte - - n := 0 - - s, err := intconv.Int64ToUint64(size) - if err != nil { - panic(err) - } - - c := (uint8(ty) << 4) | byte(s&0x0f) - - s >>= 4 - for s != 0 { - out[n] = c | 0x80 - n++ - c = byte(s & 0x7f) - s >>= 7 - } - - out[n] = c - n++ - - return append([]byte(nil), out[:n]...) -} diff --git a/object/store/packed/internal/ingest/entry_prefix.go b/object/store/packed/internal/ingest/entry_prefix.go deleted file mode 100644 index a107b4e8..00000000 --- a/object/store/packed/internal/ingest/entry_prefix.go +++ /dev/null @@ -1,95 +0,0 @@ -package ingest - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// parseEntryPrefix parses one entry prefix from stream. -func parseEntryPrefix(state *ingestState, startOffset uint64) (objectRecord, error) { - var record objectRecord - - record.offset = startOffset - - first, err := state.stream.ReadByte() - if err != nil { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("read first header byte: %v", err)} - } - - record.packedType = objecttype.Type((first >> 4) & 0x07) - size := int64(first & 0x0f) - headerLen := uint32(1) - shift := uint(4) - b := first - - for b&0x80 != 0 { - b, err = state.stream.ReadByte() - if err != nil { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("read size continuation: %v", err)} - } - - headerLen++ - size |= int64(b&0x7f) << shift - shift += 7 - } - - if size < 0 { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: "negative declared size"} - } - - record.declaredSize = size - - switch record.packedType { - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: - case objecttype.TypeRefDelta: - baseRaw := make([]byte, state.algo.Size()) - - err := state.stream.readFull(baseRaw) - if err != nil { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("read ref base: %v", err)} - } - - baseID, err := objectid.FromBytes(state.algo, baseRaw) - if err != nil { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("parse ref base: %v", err)} - } - - record.baseObject = baseID - - baseRawLen, err := intconv.IntToUint32(len(baseRaw)) - if err != nil { - return record, err - } - - headerLen += baseRawLen - case objecttype.TypeOfsDelta: - dist, consumed, err := readOfsDistanceFromStream(state.stream) - if err != nil { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: err.Error()} - } - - if startOffset <= dist { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: "ofs base offset out of bounds"} - } - - record.baseOffset = startOffset - dist - - consumedUint32, err := intconv.IntToUint32(consumed) - if err != nil { - return record, err - } - - headerLen += consumedUint32 - case objecttype.TypeInvalid, objecttype.TypeFuture: - return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("unsupported object type %d", record.packedType)} - default: - return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("unsupported object type %d", record.packedType)} - } - - record.headerLen = headerLen - - return record, nil -} diff --git a/object/store/packed/internal/ingest/errors.go b/object/store/packed/internal/ingest/errors.go deleted file mode 100644 index cbad1e77..00000000 --- a/object/store/packed/internal/ingest/errors.go +++ /dev/null @@ -1,68 +0,0 @@ -package ingest - -import ( - "errors" - "fmt" -) - -// InvalidPackHeaderError reports an invalid or unsupported pack header. -type InvalidPackHeaderError struct { - Reason string -} - -// Error implements error. -func (err *InvalidPackHeaderError) Error() string { - return "packfile/ingest: invalid pack header: " + err.Reason -} - -// PackTrailerMismatchError reports a mismatch between computed and trailer pack hash. -type PackTrailerMismatchError struct{} - -// Error implements error. -func (err *PackTrailerMismatchError) Error() string { - return "packfile/ingest: pack trailer hash mismatch" -} - -// ThinPackUnresolvedError reports unresolved REF deltas when fixThin is disabled -// or when required bases cannot be found in base. -type ThinPackUnresolvedError struct { - Count int -} - -// Error implements error. -func (err *ThinPackUnresolvedError) Error() string { - return fmt.Sprintf("packfile/ingest: unresolved thin deltas: %d", err.Count) -} - -// MalformedPackEntryError reports malformed entry encoding at one pack offset. -type MalformedPackEntryError struct { - Offset uint64 - Reason string -} - -// Error implements error. -func (err *MalformedPackEntryError) Error() string { - return fmt.Sprintf("packfile/ingest: malformed pack entry at offset %d: %s", err.Offset, err.Reason) -} - -// DeltaCycleError reports a detected cycle in delta dependency resolution. -type DeltaCycleError struct { - Offset uint64 -} - -// Error implements error. -func (err *DeltaCycleError) Error() string { - return fmt.Sprintf("packfile/ingest: delta cycle detected at offset %d", err.Offset) -} - -// DestinationWriteError reports destination I/O failures. -type DestinationWriteError struct { - Op string -} - -// Error implements error. -func (err *DestinationWriteError) Error() string { - return "packfile/ingest: destination write failure: " + err.Op -} - -var errExternalThinBase = errors.New("packfile/ingest: external thin base required") diff --git a/object/store/packed/internal/ingest/file_section_writer.go b/object/store/packed/internal/ingest/file_section_writer.go deleted file mode 100644 index fa28c1a9..00000000 --- a/object/store/packed/internal/ingest/file_section_writer.go +++ /dev/null @@ -1,22 +0,0 @@ -package ingest - -import "os" - -// fileSectionWriter writes sequentially to file via WriteAt at one base offset. -type fileSectionWriter struct { - file *os.File - off int64 - pos int64 -} - -// Write writes src at current section position. -func (writer *fileSectionWriter) Write(src []byte) (int, error) { - if len(src) == 0 { - return 0, nil - } - - n, err := writer.file.WriteAt(src, writer.off+writer.pos) - writer.pos += int64(n) - - return n, err -} diff --git a/object/store/packed/internal/ingest/fill.go b/object/store/packed/internal/ingest/fill.go deleted file mode 100644 index eca4e4d6..00000000 --- a/object/store/packed/internal/ingest/fill.go +++ /dev/null @@ -1,44 +0,0 @@ -package ingest - -import ( - "errors" - "fmt" - "io" -) - -// fill ensures at least min unread bytes are available in receiver's buffer. -func (scanner *streamScanner) fill(minLen int) error { - if minLen <= 0 { - return nil - } - - if minLen > len(scanner.buf) { - return fmt.Errorf("packfile/ingest: fill(%d) exceeds scanner buffer", minLen) - } - - for scanner.n-scanner.off < minLen { - err := scanner.flushConsumedPrefix() - if err != nil { - return err - } - - readN, err := scanner.src.Read(scanner.buf[scanner.n:]) - if readN > 0 { - scanner.n += readN - } - - if err != nil { - if errors.Is(err, io.EOF) && scanner.n-scanner.off >= minLen { - return nil - } - - return err - } - - if readN == 0 { - return io.ErrNoProgress - } - } - - return nil -} diff --git a/object/store/packed/internal/ingest/finalize.go b/object/store/packed/internal/ingest/finalize.go deleted file mode 100644 index 6fe4edb2..00000000 --- a/object/store/packed/internal/ingest/finalize.go +++ /dev/null @@ -1,94 +0,0 @@ -package ingest - -import ( - "errors" - "fmt" - "io/fs" - "strings" - - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// finalizeArtifacts links temporary files to final names and returns Result. -func finalizeArtifacts(state *ingestState) (Result, error) { - base := "pack-" + state.packHash.String() - packFinal := base + ".pack" - idxFinal := base + ".idx" - - revFinal := "" - if state.opts.WriteRev { - revFinal = base + ".rev" - } - - err := linkTempToFinal(state, state.packTmpName, packFinal) - if err != nil { - return Result{}, err - } - - err = linkTempToFinal(state, state.idxTmpName, idxFinal) - if err != nil { - return Result{}, err - } - - if state.opts.WriteRev { - err := linkTempToFinal(state, state.revTmpName, revFinal) - if err != nil { - return Result{}, err - } - } - - objectCount, err := intconv.IntToUint32(len(state.records)) - if err != nil { - return Result{}, err - } - - return Result{ - PackName: packFinal, - IdxName: idxFinal, - RevName: revFinal, - PackHash: state.packHash, - ObjectCount: objectCount, - ThinFixed: state.thinFixed, - }, nil -} - -// rollbackTemporaryArtifacts removes temporary files after failure. -func rollbackTemporaryArtifacts(state *ingestState) { - if state.packTmpName != "" { - _ = state.destination.Remove(state.packTmpName) - } - - if state.idxTmpName != "" { - _ = state.destination.Remove(state.idxTmpName) - } - - if state.revTmpName != "" { - _ = state.destination.Remove(state.revTmpName) - } -} - -// linkTempToFinal hard-links tmp to final, tolerating existing final paths. -func linkTempToFinal(state *ingestState, tmp, final string) error { - if tmp == "" || final == "" { - return fmt.Errorf("packfile/ingest: invalid finalize names tmp=%q final=%q", tmp, final) - } - - if strings.Contains(final, "/") { - return fmt.Errorf("packfile/ingest: final name must be leaf: %q", final) - } - - err := state.destination.Link(tmp, final) - if err == nil { - _ = state.destination.Remove(tmp) - - return nil - } - - if errors.Is(err, fs.ErrExist) { - _ = state.destination.Remove(tmp) - - return nil - } - - return err -} diff --git a/object/store/packed/internal/ingest/flush.go b/object/store/packed/internal/ingest/flush.go deleted file mode 100644 index 96753170..00000000 --- a/object/store/packed/internal/ingest/flush.go +++ /dev/null @@ -1,37 +0,0 @@ -package ingest - -import "fmt" - -// flush writes all consumed-but-unflushed bytes to destination pack file. -func (scanner *streamScanner) flush() error { - return scanner.flushConsumedPrefix() -} - -// flushConsumedPrefix writes scanner.buf[:scanner.off] and compacts unread -// bytes to the start of buffer. -func (scanner *streamScanner) flushConsumedPrefix() error { - if scanner.off == 0 { - return nil - } - - written := 0 - for written < scanner.off { - n, err := scanner.dstFile.Write(scanner.buf[written:scanner.off]) - if err != nil { - return &DestinationWriteError{Op: fmt.Sprintf("write pack: %v", err)} - } - - if n == 0 { - return &DestinationWriteError{Op: "write pack: short write"} - } - - written += n - } - - unread := scanner.n - scanner.off - copy(scanner.buf[:unread], scanner.buf[scanner.off:scanner.n]) - scanner.off = 0 - scanner.n = unread - - return nil -} diff --git a/object/store/packed/internal/ingest/hash.go b/object/store/packed/internal/ingest/hash.go deleted file mode 100644 index 4b739c20..00000000 --- a/object/store/packed/internal/ingest/hash.go +++ /dev/null @@ -1,27 +0,0 @@ -package ingest - -import ( - "fmt" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// hashCanonicalObject hashes canonical object bytes (header+content). -func hashCanonicalObject(algo objectid.Algorithm, ty objecttype.Type, content []byte) (objectid.ObjectID, error) { - header, ok := objectheader.Encode(ty, int64(len(content))) - if !ok { - return objectid.ObjectID{}, fmt.Errorf("packfile/ingest: encode object header for type %d", ty) - } - - hashImpl, err := algo.New() - if err != nil { - return objectid.ObjectID{}, err - } - - _, _ = hashImpl.Write(header) - _, _ = hashImpl.Write(content) - - return objectid.FromBytes(algo, hashImpl.Sum(nil)) -} diff --git a/object/store/packed/internal/ingest/header.go b/object/store/packed/internal/ingest/header.go deleted file mode 100644 index 6b90becc..00000000 --- a/object/store/packed/internal/ingest/header.go +++ /dev/null @@ -1,54 +0,0 @@ -package ingest - -import ( - "encoding/binary" - "fmt" - "io" - - "codeberg.org/lindenii/furgit/format/packfile" -) - -const packHeaderSize = 12 - -type packHeader struct { - Version uint32 - ObjectCount uint32 -} - -// readAndValidatePackHeader reads one PACK header from src and validates it. -func readAndValidatePackHeader(src io.Reader) (packHeader, [packHeaderSize]byte, error) { - var hdr [packHeaderSize]byte - - _, err := io.ReadFull(src, hdr[:]) - if err != nil { - return packHeader{}, [packHeaderSize]byte{}, &InvalidPackHeaderError{ - Reason: fmt.Sprintf("read header: %v", err), - } - } - - header, err := parseAndValidatePackHeader(hdr) - if err != nil { - return packHeader{}, [packHeaderSize]byte{}, err - } - - return header, hdr, nil -} - -// parseAndValidatePackHeader validates one already-read PACK header. -func parseAndValidatePackHeader(hdr [packHeaderSize]byte) (packHeader, error) { - if binary.BigEndian.Uint32(hdr[:4]) != packfile.Signature { - return packHeader{}, &InvalidPackHeaderError{Reason: "signature mismatch"} - } - - version := binary.BigEndian.Uint32(hdr[4:8]) - if !packfile.SupportedVersion(version) { - return packHeader{}, &InvalidPackHeaderError{ - Reason: fmt.Sprintf("unsupported version %d", version), - } - } - - return packHeader{ - Version: version, - ObjectCount: binary.BigEndian.Uint32(hdr[8:12]), - }, nil -} diff --git a/object/store/packed/internal/ingest/idx_write.go b/object/store/packed/internal/ingest/idx_write.go deleted file mode 100644 index fa139264..00000000 --- a/object/store/packed/internal/ingest/idx_write.go +++ /dev/null @@ -1,262 +0,0 @@ -package ingest - -import ( - "bytes" - "encoding/binary" - "fmt" - "hash" - "io" - "slices" - - "codeberg.org/lindenii/furgit/internal/intconv" - "codeberg.org/lindenii/furgit/internal/progress" -) - -const ( - idxMagicV2 = 0xff744f63 - idxVersionV2 = 2 -) - -// writeIdx writes idx v2 for resolved records. -func writeIdx(state *ingestState) error { - order := buildIdxOrder(state) - - hashImpl, err := state.algo.New() - if err != nil { - return err - } - - write := func(src []byte) error { - _, writeErr := state.idxFile.Write(src) - if writeErr != nil { - return writeErr - } - - _, writeErr = hashImpl.Write(src) - if writeErr != nil { - return writeErr - } - - return nil - } - - var ( - scratch [8]byte - fanout [256]uint32 - ) - - writeProgressf(state, "writing index fanout...\r") - - for _, recordIdx := range order { - idRaw := state.records[recordIdx].objectID.Bytes() - fanout[idRaw[0]]++ - } - - binary.BigEndian.PutUint32(scratch[:4], idxMagicV2) - binary.BigEndian.PutUint32(scratch[4:8], idxVersionV2) - - err = write(scratch[:8]) - if err != nil { - return err - } - - var cumulative uint32 - for i := range fanout { - cumulative += fanout[i] - binary.BigEndian.PutUint32(scratch[:4], cumulative) - - err := write(scratch[:4]) - if err != nil { - return err - } - } - - writeProgressf(state, "writing index fanout: done.\n") - - largeOffsetCount := 0 - - for idx := range state.records { - if state.records[idx].offset >= 0x80000000 { - largeOffsetCount++ - } - } - - oidMeter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "writing index object ids", - Total: uint64(len(order)), - }) - - var oidDone uint64 - - for _, recordIdx := range order { - idRaw := state.records[recordIdx].objectID.Bytes() - - err := write(idRaw) - if err != nil { - return err - } - - oidDone++ - oidMeter.Set(oidDone, 0) - } - - if oidDone > 0 { - oidMeter.Stop("done") - } - - crcMeter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "writing index crc32", - Total: uint64(len(order)), - }) - - var crcDone uint64 - - for _, recordIdx := range order { - binary.BigEndian.PutUint32(scratch[:4], state.records[recordIdx].crc32) - - err := write(scratch[:4]) - if err != nil { - return err - } - - crcDone++ - crcMeter.Set(crcDone, 0) - } - - if crcDone > 0 { - crcMeter.Stop("done") - } - - largeOffsets := make([]uint64, 0) - offsetMeter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "writing index offsets", - Total: uint64(len(order)), - }) - - var offsetDone uint64 - - for _, recordIdx := range order { - offset := state.records[recordIdx].offset - if offset >= 0x80000000 { - largeOffsetIdx, err := intconv.IntToUint32(len(largeOffsets)) - if err != nil { - return err - } - - word := 0x80000000 | largeOffsetIdx - - largeOffsets = append(largeOffsets, offset) - - binary.BigEndian.PutUint32(scratch[:4], word) - } else { - binary.BigEndian.PutUint32(scratch[:4], uint32(offset)) - } - - err := write(scratch[:4]) - if err != nil { - return err - } - - offsetDone++ - offsetMeter.Set(offsetDone, 0) - } - - if offsetDone > 0 { - offsetMeter.Stop("done") - } - - total, err := intconv.IntToUint64(largeOffsetCount) - if err != nil { - return err - } - - largeOffsetMeter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "writing index large offsets", - Total: total, - }) - - var largeOffsetDone uint64 - - for _, off := range largeOffsets { - binary.BigEndian.PutUint64(scratch[:8], off) - - err := write(scratch[:8]) - if err != nil { - return err - } - - largeOffsetDone++ - largeOffsetMeter.Set(largeOffsetDone, 0) - } - - if largeOffsetDone > 0 { - largeOffsetMeter.Stop("done") - } - - writeProgressf(state, "writing index trailer...\r") - - err = write(state.packHash.Bytes()) - if err != nil { - return err - } - - idxHash := hashImpl.Sum(nil) - - _, err = state.idxFile.Write(idxHash) - if err != nil { - return err - } - - err = state.idxFile.Sync() - if err != nil { - return err - } - - writeProgressf(state, "writing index trailer: done.\n") - - return nil -} - -// buildIdxOrder returns record indexes sorted by ObjectID. -func buildIdxOrder(state *ingestState) []int { - out := make([]int, 0, len(state.records)) - for idx := range state.records { - out = append(out, idx) - } - - slices.SortFunc(out, func(a, b int) int { - return bytes.Compare(state.records[a].objectID.Bytes(), state.records[b].objectID.Bytes()) - }) - - return out -} - -// verifyResolvedRecords checks that all records are fully resolved before index writing. -func verifyResolvedRecords(state *ingestState) error { - for idx, record := range state.records { - if !record.resolved { - return fmt.Errorf("packfile/ingest: unresolved record %d at offset %d", idx, record.offset) - } - } - - return nil -} - -// writeAndHash writes src to dst and updates hash. -func writeAndHash(dst io.Writer, hashImpl hash.Hash, src []byte) error { - _, err := dst.Write(src) - if err != nil { - return err - } - - _, err = hashImpl.Write(src) - if err != nil { - return err - } - - return nil -} diff --git a/object/store/packed/internal/ingest/ingest.go b/object/store/packed/internal/ingest/ingest.go deleted file mode 100644 index be65ff5f..00000000 --- a/object/store/packed/internal/ingest/ingest.go +++ /dev/null @@ -1,68 +0,0 @@ -package ingest - -import ( - "fmt" -) - -// ingest initializes transaction state and executes the ingest pipeline. -func ingest(state *ingestState) (out Result, err error) { - err = openTemporaryArtifacts(state) - if err != nil { - return Result{}, err - } - - defer func() { - _ = closeTemporaryArtifacts(state) - if err != nil { - rollbackTemporaryArtifacts(state) - } - }() - - err = streamPackAndScan(state) - if err != nil { - return Result{}, err - } - - err = resolveAll(state) - if err != nil { - return Result{}, err - } - - err = maybeFixThin(state) - if err != nil { - return Result{}, err - } - - if state.thinFixed { - err = resolveAll(state) - if err != nil { - return Result{}, err - } - } - - if len(state.unresolvedRefDeltas) > 0 { - return Result{}, &ThinPackUnresolvedError{Count: len(state.unresolvedRefDeltas)} - } - - err = verifyResolvedRecords(state) - if err != nil { - return Result{}, err - } - - err = state.packFile.Sync() - if err != nil { - return Result{}, &DestinationWriteError{Op: fmt.Sprintf("sync pack: %v", err)} - } - - err = writeIdx(state) - if err != nil { - return Result{}, err - } - - err = writeRev(state) - if err != nil { - return Result{}, err - } - - return finalizeArtifacts(state) -} diff --git a/object/store/packed/internal/ingest/ingest_test.go b/object/store/packed/internal/ingest/ingest_test.go deleted file mode 100644 index c99afe65..00000000 --- a/object/store/packed/internal/ingest/ingest_test.go +++ /dev/null @@ -1,411 +0,0 @@ -package ingest_test - -import ( - "bytes" - "encoding/binary" - "errors" - "io/fs" - "os" - "path/filepath" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/packed/internal/ingest" -) - -type noExtraReadReader struct { - reader *bytes.Reader -} - -func (r *noExtraReadReader) Read(p []byte) (int, error) { - if r.reader.Len() == 0 { - return 0, errors.New("unexpected extra read after pack trailer") - } - - return r.reader.Read(p) -} - -// fixturePath returns one fixture file path for the selected algorithm. -func fixturePath(t *testing.T, algo objectid.Algorithm, name string) string { - t.Helper() - - dir := algo.String() - if dir == "" { - t.Fatalf("unsupported fixture algorithm: %v", algo) - } - - return filepath.Join("testdata", "fixtures", dir, name) -} - -// fixtureBytes reads one fixture file fully. -func fixtureBytes(t *testing.T, algo objectid.Algorithm, name string) []byte { - t.Helper() - - path := fixturePath(t, algo, name) - dir := filepath.Dir(path) - base := filepath.Base(path) - - root, err := os.OpenRoot(dir) - if err != nil { - t.Fatalf("open fixture root %q: %v", dir, err) - } - - defer func() { - err := root.Close() - if err != nil { - t.Fatalf("close fixture root %q: %v", dir, err) - } - }() - - data, err := root.ReadFile(base) - if err != nil { - t.Fatalf("read fixture %q: %v", base, err) - } - - return data -} - -// fixtureMetadata parses key=value metadata for one algorithm fixture set. -func fixtureMetadata(t *testing.T, algo objectid.Algorithm) map[string]string { - t.Helper() - - data := fixtureBytes(t, algo, "METADATA.txt") - - out := make(map[string]string) - for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - key, value, ok := strings.Cut(line, "=") - if !ok { - t.Fatalf("invalid fixture metadata line %q", line) - } - - out[strings.TrimSpace(key)] = strings.TrimSpace(value) - } - - return out -} - -// fixtureOID returns one fixture metadata object ID value. -func fixtureOID(t *testing.T, algo objectid.Algorithm, key string) objectid.ObjectID { - t.Helper() - - meta := fixtureMetadata(t, algo) - - hex, ok := meta[key] - if !ok { - t.Fatalf("missing fixture metadata key %q", key) - } - - id, err := objectid.ParseHex(algo, hex) - if err != nil { - t.Fatalf("parse fixture metadata oid %q: %v", hex, err) - } - - return id -} - -// verifyReindexOracle regenerates idx/rev with upstream git index-pack and -// compares bytes with files produced by ingest. -func verifyReindexOracle(t *testing.T, repo *testgit.TestRepo, packName, idxName, revName string) { - t.Helper() - - oracleDir := t.TempDir() - oracleIdxPath := filepath.Join(oracleDir, "oracle.idx") - _ = repo.Run(t, "index-pack", "--rev-index", "-o", oracleIdxPath, filepath.Join("objects", "pack", packName)) - oracleRevPath := strings.TrimSuffix(oracleIdxPath, ".idx") + ".rev" - - packRoot := repo.OpenPackRoot(t) - - gotIdx, err := packRoot.ReadFile(idxName) - if err != nil { - t.Fatalf("read idx: %v", err) - } - - oracleRoot, err := os.OpenRoot(oracleDir) - if err != nil { - t.Fatalf("open oracle root: %v", err) - } - - defer func() { - err := oracleRoot.Close() - if err != nil { - t.Fatalf("close oracle root: %v", err) - } - }() - - wantIdx, err := oracleRoot.ReadFile(filepath.Base(oracleIdxPath)) - if err != nil { - t.Fatalf("read oracle idx: %v", err) - } - - if !bytes.Equal(gotIdx, wantIdx) { - t.Fatal("idx bytes differ from git index-pack output") - } - - gotRev, err := packRoot.ReadFile(revName) - if err != nil { - t.Fatalf("read rev: %v", err) - } - - wantRev, err := oracleRoot.ReadFile(filepath.Base(oracleRevPath)) - if err != nil { - t.Fatalf("read oracle rev: %v", err) - } - - if !bytes.Equal(gotRev, wantRev) { - t.Fatal("rev bytes differ from git index-pack output") - } -} - -func TestIngestNonThinPackWritesPackIdxRev(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - packBytes := fixtureBytes(t, algo, "nonthin.pack") - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - - packRoot := receiver.OpenPackRoot(t) - - result, err := ingest.WritePack(packRoot, algo, bytes.NewReader(packBytes), ingest.Options{ - WriteRev: true, - RequireTrailingEOF: true, - }) - if err != nil { - t.Fatalf("Ingest: %v", err) - } - - if result.ThinFixed { - t.Fatalf("ThinFixed = true, want false") - } - - if result.RevName == "" { - t.Fatal("RevName is empty") - } - - _, err = packRoot.Stat(result.PackName) - if err != nil { - t.Fatalf("stat pack: %v", err) - } - - _, err = packRoot.Stat(result.IdxName) - if err != nil { - t.Fatalf("stat idx: %v", err) - } - - _, err = packRoot.Stat(result.RevName) - if err != nil { - t.Fatalf("stat rev: %v", err) - } - - _ = receiver.Run(t, "verify-pack", "-v", filepath.Join("objects", "pack", result.IdxName)) - verifyReindexOracle(t, receiver, result.PackName, result.IdxName, result.RevName) - - receiver.UpdateRef(t, "refs/heads/main", head) - _ = receiver.Run(t, "fsck", "--full", "--strict", "--no-progress", "--no-dangling") - }) -} - -func TestIngestThinPackWithoutFixReturnsUnresolved(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - thinPack := fixtureBytes(t, algo, "thin.pack") - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - packRoot := receiver.OpenPackRoot(t) - - _, err := ingest.WritePack(packRoot, algo, bytes.NewReader(thinPack), ingest.Options{ - WriteRev: true, - RequireTrailingEOF: true, - }) - if err == nil { - t.Fatal("Ingest error = nil, want error") - } - - if _, ok := errors.AsType[*ingest.ThinPackUnresolvedError](err); !ok { - t.Fatalf("Ingest error type = %T (%v), want *ThinPackUnresolvedError", err, err) - } - - entries, err := fs.ReadDir(packRoot.FS(), ".") - if err != nil { - t.Fatalf("ReadDir(pack): %v", err) - } - - for _, entry := range entries { - if strings.HasSuffix(entry.Name(), ".pack") { - t.Fatalf("found finalized pack file after failure: %v", entry.Name()) - } - } - }) -} - -func TestIngestThinPackWithFixThin(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - basePack := fixtureBytes(t, algo, "base.pack") - thinPack := fixtureBytes(t, algo, "thin.pack") - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - - packRoot := receiver.OpenPackRoot(t) - - _, err := ingest.WritePack(packRoot, algo, bytes.NewReader(basePack), ingest.Options{ - RequireTrailingEOF: true, - }) - if err != nil { - t.Fatalf("ingest base pack: %v", err) - } - - receiverRepo := receiver.OpenRepository(t) - - result, err := ingest.WritePack(packRoot, algo, bytes.NewReader(thinPack), ingest.Options{ - FixThin: true, - WriteRev: true, - ThinBase: receiverRepo.Objects(), - RequireTrailingEOF: true, - }) - if err != nil { - t.Fatalf("Ingest(thin): %v", err) - } - - if !result.ThinFixed { - t.Fatal("ThinFixed = false, want true") - } - - _ = receiver.Run(t, "verify-pack", "-v", filepath.Join("objects", "pack", result.IdxName)) - verifyReindexOracle(t, receiver, result.PackName, result.IdxName, result.RevName) - receiver.UpdateRef(t, "refs/heads/main", head) - _ = receiver.Run(t, "fsck", "--full", "--strict", "--no-progress", "--no-dangling") - }) -} - -func TestIngestPackTrailerMismatch(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - packBytes := fixtureBytes(t, algo, "nonthin.pack") - if len(packBytes) == 0 { - t.Fatal("empty pack stream") - } - - packBytes[len(packBytes)-1] ^= 0xff - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - packRoot := receiver.OpenPackRoot(t) - - _, err := ingest.WritePack(packRoot, algo, bytes.NewReader(packBytes), ingest.Options{ - WriteRev: true, - RequireTrailingEOF: true, - }) - if err == nil { - t.Fatal("Ingest error = nil, want error") - } - - if _, ok := errors.AsType[*ingest.PackTrailerMismatchError](err); !ok { - t.Fatalf("Ingest error type = %T (%v), want *PackTrailerMismatchError", err, err) - } - - entries, err := fs.ReadDir(packRoot.FS(), ".") - if err != nil { - t.Fatalf("ReadDir(pack): %v", err) - } - - for _, entry := range entries { - if strings.HasSuffix(entry.Name(), ".pack") { - t.Fatalf("found finalized pack file after failure: %v", entry.Name()) - } - } - }) -} - -func zeroObjectPackBytes(t *testing.T, algo objectid.Algorithm) []byte { - t.Helper() - - hashImpl, err := algo.New() - if err != nil { - t.Fatalf("algo.New: %v", err) - } - - var header [12]byte - copy(header[:4], []byte{'P', 'A', 'C', 'K'}) - binary.BigEndian.PutUint32(header[4:8], 2) - binary.BigEndian.PutUint32(header[8:12], 0) - - _, _ = hashImpl.Write(header[:]) - - return append(header[:], hashImpl.Sum(nil)...) -} - -func TestIngestZeroObjectPackIsDiscardedInternally(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - packBytes := zeroObjectPackBytes(t, algo) - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - packRoot := receiver.OpenPackRoot(t) - - result, err := ingest.WritePack(packRoot, algo, bytes.NewReader(packBytes), ingest.Options{ - RequireTrailingEOF: true, - }) - if err != nil { - t.Fatalf("WritePack: %v", err) - } - - if result.ObjectCount != 0 { - t.Fatalf("ObjectCount = %d, want 0", result.ObjectCount) - } - - if result.PackName != "" { - t.Fatalf("PackName = %q, want empty", result.PackName) - } - - if result.IdxName != "" { - t.Fatalf("IdxName = %q, want empty", result.IdxName) - } - - if result.RevName != "" { - t.Fatalf("RevName = %q, want empty", result.RevName) - } - - entries, err := fs.ReadDir(packRoot.FS(), ".") - if err != nil { - t.Fatalf("ReadDir(pack): %v", err) - } - - if len(entries) != 0 { - t.Fatalf("unexpected files after zero-object pack: %d", len(entries)) - } - }) -} - -func TestIngestCanFinishWithoutTrailingEOF(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - packBytes := fixtureBytes(t, algo, "nonthin.pack") - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - packRoot := receiver.OpenPackRoot(t) - - result, err := ingest.WritePack(packRoot, algo, &noExtraReadReader{reader: bytes.NewReader(packBytes)}, ingest.Options{ - WriteRev: true, - }) - if err != nil { - t.Fatalf("Ingest without trailing EOF: %v", err) - } - - receiver.UpdateRef(t, "refs/heads/main", head) - _ = receiver.Run(t, "verify-pack", "-v", filepath.Join("objects", "pack", result.IdxName)) - _ = receiver.Run(t, "fsck", "--full", "--strict", "--no-progress", "--no-dangling") - }) -} diff --git a/object/store/packed/internal/ingest/options.go b/object/store/packed/internal/ingest/options.go deleted file mode 100644 index 06c334c0..00000000 --- a/object/store/packed/internal/ingest/options.go +++ /dev/null @@ -1,26 +0,0 @@ -package ingest - -import ( - "codeberg.org/lindenii/furgit/common/iowrap" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// Options controls one pack ingest operation. -type Options struct { - // FixThin appends missing local bases for thin packs. - FixThin bool - // WriteRev writes a .rev alongside the .pack and .idx. - WriteRev bool - // ThinBase supplies existing objects for thin-pack fixup. - ThinBase objectstore.Reader - // Progress receives human-readable progress messages. - // - // When nil, no progress output is emitted. - Progress iowrap.WriteFlusher - // RequireTrailingEOF requires the source to hit EOF after the pack trailer. - // - // This is suitable for exact pack-file readers, but should be disabled for - // full-duplex transport streams like receive-pack where the peer keeps the - // connection open to read the server response. - RequireTrailingEOF bool -} diff --git a/object/store/packed/internal/ingest/progress_write.go b/object/store/packed/internal/ingest/progress_write.go deleted file mode 100644 index afb39305..00000000 --- a/object/store/packed/internal/ingest/progress_write.go +++ /dev/null @@ -1,11 +0,0 @@ -package ingest - -import "codeberg.org/lindenii/furgit/internal/utils" - -func writeProgressf(state *ingestState, format string, args ...any) { - utils.BestEffortFprintf(state.opts.Progress, format, args...) - - if state.opts.Progress != nil { - _ = state.opts.Progress.Flush() - } -} diff --git a/object/store/packed/internal/ingest/record_content.go b/object/store/packed/internal/ingest/record_content.go deleted file mode 100644 index c66a1234..00000000 --- a/object/store/packed/internal/ingest/record_content.go +++ /dev/null @@ -1,29 +0,0 @@ -package ingest - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// readBaseRecordContent reads canonical base content for one non-delta record. -func readBaseRecordContent(state *ingestState, idx int) (objecttype.Type, []byte, error) { - record := state.records[idx] - if !record.packedType.IsBaseObject() { - return objecttype.TypeInvalid, nil, fmt.Errorf("packfile/ingest: record %d is not a base object", idx) - } - - content, err := inflateRecordPayload(state, idx) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - if int64(len(content)) != record.declaredSize { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: fmt.Sprintf("base content size mismatch got %d want %d", len(content), record.declaredSize), - } - } - - return record.packedType, content, nil -} diff --git a/object/store/packed/internal/ingest/record_delta.go b/object/store/packed/internal/ingest/record_delta.go deleted file mode 100644 index bc40367f..00000000 --- a/object/store/packed/internal/ingest/record_delta.go +++ /dev/null @@ -1,60 +0,0 @@ -package ingest - -import ( - "fmt" - - deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// applyDeltaRecord applies one delta record onto base content. -func applyDeltaRecord(state *ingestState, idx int, baseType objecttype.Type, baseContent []byte) (objecttype.Type, []byte, error) { - record := state.records[idx] - if record.packedType != objecttype.TypeOfsDelta && record.packedType != objecttype.TypeRefDelta { - return objecttype.TypeInvalid, nil, fmt.Errorf("packfile/ingest: record %d is not a delta record", idx) - } - - deltaPayload, err := inflateRecordPayload(state, idx) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - if int64(len(deltaPayload)) != record.declaredSize { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: fmt.Sprintf("delta payload size mismatch got %d want %d", len(deltaPayload), record.declaredSize), - } - } - - srcSize, dstSize, err := readDeltaHeaderSizes(deltaPayload) - if err != nil { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: fmt.Sprintf("read delta header: %v", err), - } - } - - if srcSize != len(baseContent) { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: fmt.Sprintf("delta source size mismatch got %d want %d", srcSize, len(baseContent)), - } - } - - content, err := deltaapply.Apply(baseContent, deltaPayload) - if err != nil { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: fmt.Sprintf("apply delta: %v", err), - } - } - - if len(content) != dstSize { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: fmt.Sprintf("delta result size mismatch got %d want %d", len(content), dstSize), - } - } - - return baseType, content, nil -} diff --git a/object/store/packed/internal/ingest/record_inflate.go b/object/store/packed/internal/ingest/record_inflate.go deleted file mode 100644 index b8eca25b..00000000 --- a/object/store/packed/internal/ingest/record_inflate.go +++ /dev/null @@ -1,46 +0,0 @@ -package ingest - -import ( - "compress/zlib" - "fmt" - "io" - - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// inflateRecordPayload inflates one record's zlib payload from pack file. -func inflateRecordPayload(state *ingestState, idx int) ([]byte, error) { - record := state.records[idx] - if record.packedLen < uint64(record.headerLen) { - return nil, &MalformedPackEntryError{Offset: record.offset, Reason: "entry packed span underflow"} - } - - compressedOffset := record.offset + uint64(record.headerLen) - compressedLen := record.packedLen - uint64(record.headerLen) - - compressedOffsetInt64, err := intconv.Uint64ToInt64(compressedOffset) - if err != nil { - return nil, err - } - - compressedLenInt64, err := intconv.Uint64ToInt64(compressedLen) - if err != nil { - return nil, err - } - - section := io.NewSectionReader(state.packFile, compressedOffsetInt64, compressedLenInt64) - - reader, err := zlib.NewReader(section) - if err != nil { - return nil, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("open payload zlib: %v", err)} - } - - defer func() { _ = reader.Close() }() - - out, err := io.ReadAll(reader) - if err != nil { - return nil, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("inflate payload: %v", err)} - } - - return out, nil -} diff --git a/object/store/packed/internal/ingest/record_resolve.go b/object/store/packed/internal/ingest/record_resolve.go deleted file mode 100644 index 7a9471dc..00000000 --- a/object/store/packed/internal/ingest/record_resolve.go +++ /dev/null @@ -1,116 +0,0 @@ -package ingest - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// resolveRecord resolves one record and returns canonical type/content. -func resolveRecord(state *ingestState, idx int, visiting map[int]struct{}) (objecttype.Type, []byte, error) { - if idx < 0 || idx >= len(state.records) { - return objecttype.TypeInvalid, nil, fmt.Errorf("packfile/ingest: record index out of bounds") - } - - if _, ok := visiting[idx]; ok { - return objecttype.TypeInvalid, nil, &DeltaCycleError{Offset: state.records[idx].offset} - } - - visiting[idx] = struct{}{} - defer delete(visiting, idx) - - record := &state.records[idx] - if ty, content, ok := state.baseCache.get(idx); ok { - return ty, content, nil - } - - if record.packedType.IsBaseObject() { - ty, content, err := readBaseRecordContent(state, idx) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - if record.resolved { - state.baseCache.add(idx, record.realType, content) - - return record.realType, content, nil - } - - id, err := hashCanonicalObject(state.algo, ty, content) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - record.objectID = id - record.realType = ty - record.resolved = true - state.objectToRecord[id] = idx - state.baseCache.add(idx, ty, content) - - return ty, content, nil - } - - var ( - baseType objecttype.Type - baseContent []byte - err error - ) - switch record.packedType { - case objecttype.TypeOfsDelta: - baseIdx, ok := state.offsetToRecord[record.baseOffset] - if !ok { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: "missing ofs-delta base entry", - } - } - - baseType, baseContent, err = resolveRecord(state, baseIdx, visiting) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - case objecttype.TypeRefDelta: - baseIdx, ok := state.objectToRecord[record.baseObject] - if ok { - baseType, baseContent, err = resolveRecord(state, baseIdx, visiting) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - } else { - return objecttype.TypeInvalid, nil, errExternalThinBase - } - case objecttype.TypeInvalid, - objecttype.TypeCommit, - objecttype.TypeTree, - objecttype.TypeBlob, - objecttype.TypeTag, - objecttype.TypeFuture: - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: "unsupported delta type", - } - default: - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: "unsupported delta type", - } - } - - ty, content, err := applyDeltaRecord(state, idx, baseType, baseContent) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - id, err := hashCanonicalObject(state.algo, ty, content) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - record.objectID = id - record.realType = ty - record.resolved = true - state.objectToRecord[id] = idx - state.baseCache.add(idx, ty, content) - - return ty, content, nil -} diff --git a/object/store/packed/internal/ingest/records.go b/object/store/packed/internal/ingest/records.go deleted file mode 100644 index 75f157fa..00000000 --- a/object/store/packed/internal/ingest/records.go +++ /dev/null @@ -1,46 +0,0 @@ -package ingest - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// objectRecord stores metadata for one packed object entry. -type objectRecord struct { - // offset is the entry start offset in the pack file. - offset uint64 - // headerLen is packed entry header length in bytes. - headerLen uint32 - // packedLen is total packed entry length in bytes. - packedLen uint64 - // crc32 is the CRC over the full packed entry. - crc32 uint32 - // packedType is the entry type tag from the pack stream. - packedType objecttype.Type - // realType is canonical object type after delta resolution. - realType objecttype.Type - // declaredSize is the declared output object size for this entry. - declaredSize int64 - // dataOffset is compressed payload start offset for this entry. - dataOffset uint64 - // baseOffset is OFS base offset when packedType is OFS delta. - baseOffset uint64 - // baseObject is REF base object ID when packedType is REF delta. - baseObject objectid.ObjectID - // objectID is final resolved object ID. - objectID objectid.ObjectID - // resolved reports whether objectID/realType are finalized. - resolved bool -} - -// ofsDeltaRef maps one OFS delta record to its base offset. -type ofsDeltaRef struct { - baseOffset uint64 - recordIdx int -} - -// refDeltaRef maps one REF delta record to its base object ID. -type refDeltaRef struct { - baseObject objectid.ObjectID - recordIdx int -} diff --git a/object/store/packed/internal/ingest/resolve_all.go b/object/store/packed/internal/ingest/resolve_all.go deleted file mode 100644 index 90464015..00000000 --- a/object/store/packed/internal/ingest/resolve_all.go +++ /dev/null @@ -1,70 +0,0 @@ -package ingest - -import ( - "errors" - - "codeberg.org/lindenii/furgit/internal/progress" -) - -// resolveAll resolves all delta records and finalizes ObjectID/RealType for every record. -func resolveAll(state *ingestState) error { - state.unresolvedRefDeltas = state.unresolvedRefDeltas[:0] - - var pending uint32 - - for idx := range state.records { - if !state.records[idx].resolved { - pending++ - } - } - - if pending == 0 { - return nil - } - - var done uint32 - - meter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "resolving deltas", - Total: uint64(pending), - }) - - for idx := range state.records { - if state.records[idx].resolved { - continue - } - - done++ - meter.Set(uint64(done), 0) - - visiting := make(map[int]struct{}) - - ty, content, err := resolveRecord(state, idx, visiting) - if err != nil { - if errors.Is(err, errExternalThinBase) { - state.unresolvedRefDeltas = append(state.unresolvedRefDeltas, idx) - - continue - } - - return err - } - - id, err := hashCanonicalObject(state.algo, ty, content) - if err != nil { - return err - } - - record := &state.records[idx] - record.realType = ty - record.objectID = id - record.resolved = true - state.objectToRecord[id] = idx - state.baseCache.add(idx, ty, content) - } - - meter.Stop("done") - - return nil -} diff --git a/object/store/packed/internal/ingest/result.go b/object/store/packed/internal/ingest/result.go deleted file mode 100644 index 9a285f09..00000000 --- a/object/store/packed/internal/ingest/result.go +++ /dev/null @@ -1,23 +0,0 @@ -package ingest - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Result describes one successful ingest transaction. -type Result struct { - // PackName is the destination-relative filename of the written .pack. - PackName string - // IdxName is the destination-relative filename of the written .idx. - IdxName string - // RevName is the destination-relative filename of the written .rev. - // - // RevName is empty when writeRev is false. - RevName string - // PackHash is the final pack hash (same hash embedded in .idx/.rev trailers). - PackHash objectid.ObjectID - // ObjectCount is the final object count in the resulting pack. - // - // If thin fixup appends objects, this includes appended base objects. - ObjectCount uint32 - // ThinFixed reports whether thin fixup appended local bases. - ThinFixed bool -} diff --git a/object/store/packed/internal/ingest/rev_write.go b/object/store/packed/internal/ingest/rev_write.go deleted file mode 100644 index 16d27085..00000000 --- a/object/store/packed/internal/ingest/rev_write.go +++ /dev/null @@ -1,137 +0,0 @@ -package ingest - -import ( - "encoding/binary" - "slices" - - "codeberg.org/lindenii/furgit/internal/intconv" - "codeberg.org/lindenii/furgit/internal/progress" -) - -const ( - revMagic = 0x52494458 - revVersion = 1 -) - -// writeRev writes rev index for resolved records. -func writeRev(state *ingestState) error { - if !state.opts.WriteRev { - return nil - } - - idxOrder := buildIdxOrder(state) - - recordToIdxPos := make([]int, len(state.records)) - for pos, recordIdx := range idxOrder { - recordToIdxPos[recordIdx] = pos - } - - packOrder := buildPackOrder(state) - - hashImpl, err := state.algo.New() - if err != nil { - return err - } - - var scratch [8]byte - - writeProgressf(state, "writing reverse index header...\r") - binary.BigEndian.PutUint32(scratch[:4], revMagic) - - err = writeAndHash(state.revFile, hashImpl, scratch[:4]) - if err != nil { - return err - } - - binary.BigEndian.PutUint32(scratch[:4], revVersion) - - err = writeAndHash(state.revFile, hashImpl, scratch[:4]) - if err != nil { - return err - } - - binary.BigEndian.PutUint32(scratch[:4], state.algo.PackHashID()) - - err = writeAndHash(state.revFile, hashImpl, scratch[:4]) - if err != nil { - return err - } - - writeProgressf(state, "writing reverse index header: done.\n") - - entriesMeter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "writing reverse index entries", - Total: uint64(len(packOrder)), - }) - - var entriesDone uint64 - - for _, recordIdx := range packOrder { - recordPos, err := intconv.IntToUint32(recordToIdxPos[recordIdx]) - if err != nil { - return err - } - - binary.BigEndian.PutUint32(scratch[:4], recordPos) - - err = writeAndHash(state.revFile, hashImpl, scratch[:4]) - if err != nil { - return err - } - - entriesDone++ - entriesMeter.Set(entriesDone, 0) - } - - if entriesDone > 0 { - entriesMeter.Stop("done") - } - - writeProgressf(state, "writing reverse index trailer...\r") - - err = writeAndHash(state.revFile, hashImpl, state.packHash.Bytes()) - if err != nil { - return err - } - - revHash := hashImpl.Sum(nil) - - _, err = state.revFile.Write(revHash) - if err != nil { - return err - } - - err = state.revFile.Sync() - if err != nil { - return err - } - - writeProgressf(state, "writing reverse index trailer: done.\n") - - return nil -} - -// buildPackOrder returns record indexes sorted by pack offset. -func buildPackOrder(state *ingestState) []int { - out := make([]int, 0, len(state.records)) - for idx := range state.records { - out = append(out, idx) - } - - slices.SortFunc(out, func(a, b int) int { - offA := state.records[a].offset - - offB := state.records[b].offset - switch { - case offA < offB: - return -1 - case offA > offB: - return 1 - default: - return 0 - } - }) - - return out -} diff --git a/object/store/packed/internal/ingest/rewrite_header_trailer.go b/object/store/packed/internal/ingest/rewrite_header_trailer.go deleted file mode 100644 index f1f18a39..00000000 --- a/object/store/packed/internal/ingest/rewrite_header_trailer.go +++ /dev/null @@ -1,89 +0,0 @@ -package ingest - -import ( - "encoding/binary" - "io" - - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// rewritePackHeaderAndTrailer rewrites object count and trailer hash using ReadAt/WriteAt. -func rewritePackHeaderAndTrailer(state *ingestState) error { - var countRaw [4]byte - - recordCountUint32, err := intconv.IntToUint32(len(state.records)) - if err != nil { - return err - } - - binary.BigEndian.PutUint32(countRaw[:], recordCountUint32) - - _, err = state.packFile.WriteAt(countRaw[:], 8) - if err != nil { - return err - } - - info, err := state.packFile.Stat() - if err != nil { - return err - } - - endWithoutTrailer := info.Size() - - hashImpl, err := state.algo.New() - if err != nil { - return err - } - - var ( - buf [128 << 10]byte - pos int64 - ) - for pos < endWithoutTrailer { - want := int64(len(buf)) - - remaining := endWithoutTrailer - pos - if remaining < want { - want = remaining - } - - n, err := state.packFile.ReadAt(buf[:want], pos) - if err != nil && err != io.EOF { - return err - } - - if n == 0 { - return io.ErrUnexpectedEOF - } - - _, _ = hashImpl.Write(buf[:n]) - pos += int64(n) - } - - sum := hashImpl.Sum(nil) - - _, err = state.packFile.WriteAt(sum, endWithoutTrailer) - if err != nil { - return err - } - - packHash, err := objectid.FromBytes(state.algo, sum) - if err != nil { - return err - } - - state.packHash = packHash - state.objectCountHeader = recordCountUint32 - - sumLenInt64 := int64(len(sum)) - - newConsumed, err := intconv.Int64ToUint64(endWithoutTrailer + sumLenInt64) - if err != nil { - return err - } - - state.stream.consumed = newConsumed - - return nil -} diff --git a/object/store/packed/internal/ingest/scan.go b/object/store/packed/internal/ingest/scan.go deleted file mode 100644 index ddd1eaf3..00000000 --- a/object/store/packed/internal/ingest/scan.go +++ /dev/null @@ -1,105 +0,0 @@ -package ingest - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/internal/progress" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// streamPackAndScan copies src into temp .pack while scanning packed entries. -func streamPackAndScan(state *ingestState) error { - hashImpl, err := state.algo.New() - if err != nil { - return err - } - - state.stream = newStreamScanner( - state.src, - state.packFile, - hashImpl, - state.algo.Size(), - ) - - writeProgressf(state, "validating pack header...\r") - - err = seedStreamWithPackHeader(state) - if err != nil { - return err - } - - writeProgressf(state, "validating pack header: done.\n") - - state.records = make([]objectRecord, 0, state.objectCountHeader) - state.ofsDeltas = make([]ofsDeltaRef, 0, state.objectCountHeader) - state.refDeltas = make([]refDeltaRef, 0, state.objectCountHeader) - - total := state.objectCountHeader - meter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "receiving objects", - Total: uint64(total), - Throughput: true, - }) - - for i := range total { - nextOffset, err := scanOneEntry(state, state.stream.consumed) - if err != nil { - return err - } - - if nextOffset != state.stream.consumed { - return fmt.Errorf("packfile/ingest: internal stream offset mismatch") - } - - done := i + 1 - meter.Set(uint64(done), state.stream.consumed) - } - - meter.Stop("done") - - err = state.stream.finishAndFlushTrailer(state.opts.RequireTrailingEOF) - if err != nil { - return err - } - - if len(state.stream.packTrailer) != state.algo.Size() { - return fmt.Errorf("packfile/ingest: invalid trailer size") - } - - packHash, err := objectid.FromBytes(state.algo, state.stream.packTrailer) - if err != nil { - return err - } - - state.packHash = packHash - - return state.stream.flush() -} - -// seedStreamWithPackHeader writes the already-validated PACK header to output, -// seeds the running pack hash, and advances stream offset accounting. -func seedStreamWithPackHeader(state *ingestState) error { - written := 0 - for written < len(state.packHeaderRaw) { - n, err := state.packFile.Write(state.packHeaderRaw[written:]) - if err != nil { - return &DestinationWriteError{Op: fmt.Sprintf("write pack header: %v", err)} - } - - if n == 0 { - return &DestinationWriteError{Op: "write pack header: short write"} - } - - written += n - } - - _, err := state.stream.hash.Write(state.packHeaderRaw[:]) - if err != nil { - return err - } - - state.stream.consumed = packHeaderSize - - return nil -} diff --git a/object/store/packed/internal/ingest/state.go b/object/store/packed/internal/ingest/state.go deleted file mode 100644 index 0412eb32..00000000 --- a/object/store/packed/internal/ingest/state.go +++ /dev/null @@ -1,70 +0,0 @@ -package ingest - -import ( - "io" - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -const ( - defaultDeltaBaseCacheMaxBytes = 32 << 20 -) - -// ingestState holds mutable state for one Ingest call. -type ingestState struct { - src io.Reader - destination *os.Root - algo objectid.Algorithm - opts Options - - packHeaderRaw [packHeaderSize]byte - - packFile *os.File - packTmpName string - idxFile *os.File - idxTmpName string - revFile *os.File - revTmpName string - - stream *streamScanner - - records []objectRecord - ofsDeltas []ofsDeltaRef - refDeltas []refDeltaRef - unresolvedRefDeltas []int - offsetToRecord map[uint64]int - objectToRecord map[objectid.ObjectID]int - - baseCache *deltaBaseCache - packHash objectid.ObjectID - - objectCountHeader uint32 - thinFixed bool -} - -// newIngestState constructs one call-local ingest state. -func newIngestState( - src io.Reader, - destination *os.Root, - algo objectid.Algorithm, - opts Options, - header packHeader, - headerRaw [packHeaderSize]byte, -) (*ingestState, error) { - if algo.Size() == 0 { - return nil, objectid.ErrInvalidAlgorithm - } - - return &ingestState{ - src: src, - destination: destination, - algo: algo, - opts: opts, - packHeaderRaw: headerRaw, - objectCountHeader: header.ObjectCount, - offsetToRecord: make(map[uint64]int), - objectToRecord: make(map[objectid.ObjectID]int), - baseCache: newDeltaBaseCache(defaultDeltaBaseCacheMaxBytes), - }, nil -} diff --git a/object/store/packed/internal/ingest/stream.go b/object/store/packed/internal/ingest/stream.go deleted file mode 100644 index a403087a..00000000 --- a/object/store/packed/internal/ingest/stream.go +++ /dev/null @@ -1,111 +0,0 @@ -package ingest - -import ( - "errors" - "hash" - "io" - "os" -) - -const streamScannerBufferSize = 64 << 10 - -// streamScanner incrementally reads/consumes one pack stream while mirroring -// consumed bytes into one destination pack file. -type streamScanner struct { - src io.Reader - dstFile *os.File - - // Input buffer window: buf[off:n] is unread. - buf []byte - off int - n int - - // Absolute consumed stream bytes. - consumed uint64 - - // Running pack hash over consumed bytes while hashEnabled is true. - hash hash.Hash - hashSize int - hashEnabled bool - - // Entry CRC state while one entry is being consumed. - entryCRC uint32 - inEntryCRC bool - - packTrailer []byte -} - -// newStreamScanner constructs one scanner with fixed input buffering. -func newStreamScanner(src io.Reader, dstFile *os.File, hash hash.Hash, hashSize int) *streamScanner { - return &streamScanner{ - src: src, - dstFile: dstFile, - buf: make([]byte, streamScannerBufferSize), - hash: hash, - hashSize: hashSize, - hashEnabled: true, - } -} - -// Read implements io.Reader. -func (scanner *streamScanner) Read(dst []byte) (int, error) { - if len(dst) == 0 { - return 0, nil - } - - if scanner.n-scanner.off == 0 { - err := scanner.fill(1) - if err != nil { - if errors.Is(err, io.EOF) { - return 0, io.EOF - } - - return 0, err - } - } - - unread := scanner.n - scanner.off - if unread == 0 { - return 0, io.EOF - } - - n := min(len(dst), unread) - - copy(dst, scanner.buf[scanner.off:scanner.off+n]) - - err := scanner.use(n) - if err != nil { - return 0, err - } - - return n, nil -} - -// ReadByte implements io.ByteReader without allocation. -func (scanner *streamScanner) ReadByte() (byte, error) { - if scanner.n-scanner.off == 0 { - err := scanner.fill(1) - if err != nil { - return 0, err - } - } - - b := scanner.buf[scanner.off] - - err := scanner.use(1) - if err != nil { - return 0, err - } - - return b, nil -} - -// readFull reads exactly len(dst) bytes through receiver. -func (scanner *streamScanner) readFull(dst []byte) error { - _, err := io.ReadFull(scanner, dst) - if err != nil { - return err - } - - return nil -} diff --git a/object/store/packed/internal/ingest/temp.go b/object/store/packed/internal/ingest/temp.go deleted file mode 100644 index d0b7862c..00000000 --- a/object/store/packed/internal/ingest/temp.go +++ /dev/null @@ -1,103 +0,0 @@ -package ingest - -import ( - "crypto/rand" - "errors" - "fmt" - "io/fs" - "os" -) - -// openTemporaryArtifacts creates/open temp pack/idx/(rev) files under destination. -func openTemporaryArtifacts(state *ingestState) error { - packName, packFile, err := createTempFile(state.destination, "tmp_pack_") - if err != nil { - return err - } - - idxName, idxFile, err := createTempFile(state.destination, "tmp_idx_") - if err != nil { - _ = packFile.Close() - _ = state.destination.Remove(packName) - - return err - } - - revName := "" - - var revFile *os.File - if state.opts.WriteRev { - revName, revFile, err = createTempFile(state.destination, "tmp_rev_") - if err != nil { - _ = idxFile.Close() - _ = state.destination.Remove(idxName) - _ = packFile.Close() - _ = state.destination.Remove(packName) - - return err - } - } - - state.packTmpName = packName - state.packFile = packFile - state.idxTmpName = idxName - state.idxFile = idxFile - state.revTmpName = revName - state.revFile = revFile - - return nil -} - -// closeTemporaryArtifacts closes all temporary artifact file descriptors. -func closeTemporaryArtifacts(state *ingestState) error { - var out error - - if state.packFile != nil { - err := state.packFile.Close() - if err != nil && out == nil { - out = err - } - - state.packFile = nil - } - - if state.idxFile != nil { - err := state.idxFile.Close() - if err != nil && out == nil { - out = err - } - - state.idxFile = nil - } - - if state.revFile != nil { - err := state.revFile.Close() - if err != nil && out == nil { - out = err - } - - state.revFile = nil - } - - return out -} - -// createTempFile creates one temporary file under root using prefix. -func createTempFile(root *os.Root, prefix string) (string, *os.File, error) { - for range 32 { - name := prefix + rand.Text() - - file, err := root.OpenFile(name, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o644) - if err == nil { - return name, file, nil - } - - if errors.Is(err, fs.ErrExist) { - continue - } - - return "", nil, fmt.Errorf("packfile/ingest: create temp file %q: %w", name, err) - } - - return "", nil, fmt.Errorf("packfile/ingest: unable to create temporary file for prefix %q", prefix) -} diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha1/METADATA.txt b/object/store/packed/internal/ingest/testdata/fixtures/sha1/METADATA.txt deleted file mode 100644 index 5fcbfe26..00000000 --- a/object/store/packed/internal/ingest/testdata/fixtures/sha1/METADATA.txt +++ /dev/null @@ -1,3 +0,0 @@ -format=sha1 -head=200c960359dad025b4170284c518919eb4a24305 -base=4bc507fc631ea78474d83c47548743c9f1dda0dc diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha1/base.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha1/base.pack Binary files differdeleted file mode 100644 index 3d7a4903..00000000 --- a/object/store/packed/internal/ingest/testdata/fixtures/sha1/base.pack +++ /dev/null diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha1/nonthin.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha1/nonthin.pack Binary files differdeleted file mode 100644 index ea07c9a0..00000000 --- a/object/store/packed/internal/ingest/testdata/fixtures/sha1/nonthin.pack +++ /dev/null diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha1/thin.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha1/thin.pack Binary files differdeleted file mode 100644 index 95084feb..00000000 --- a/object/store/packed/internal/ingest/testdata/fixtures/sha1/thin.pack +++ /dev/null diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha256/METADATA.txt b/object/store/packed/internal/ingest/testdata/fixtures/sha256/METADATA.txt deleted file mode 100644 index 8a5ea0a2..00000000 --- a/object/store/packed/internal/ingest/testdata/fixtures/sha256/METADATA.txt +++ /dev/null @@ -1,3 +0,0 @@ -format=sha256 -head=35cc0f4cd1c73524187540494058d233a2ecbd071c85d496a2250d8e0c805ef8 -base=b4abe46895f0bb5aa22fd42d28d428413f265359734c288752e3c2d270eec276 diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha256/base.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha256/base.pack Binary files differdeleted file mode 100644 index 52ceef74..00000000 --- a/object/store/packed/internal/ingest/testdata/fixtures/sha256/base.pack +++ /dev/null diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha256/nonthin.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha256/nonthin.pack Binary files differdeleted file mode 100644 index 50db05d0..00000000 --- a/object/store/packed/internal/ingest/testdata/fixtures/sha256/nonthin.pack +++ /dev/null diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha256/thin.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha256/thin.pack Binary files differdeleted file mode 100644 index b331b915..00000000 --- a/object/store/packed/internal/ingest/testdata/fixtures/sha256/thin.pack +++ /dev/null diff --git a/object/store/packed/internal/ingest/thin_append.go b/object/store/packed/internal/ingest/thin_append.go deleted file mode 100644 index 779d477f..00000000 --- a/object/store/packed/internal/ingest/thin_append.go +++ /dev/null @@ -1,91 +0,0 @@ -package ingest - -import ( - "compress/zlib" - "hash/crc32" - "io" - - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// appendBaseObject appends one base object as a new packed non-delta entry. -func appendBaseObject(state *ingestState, id objectid.ObjectID, realType objecttype.Type, content []byte) (int, error) { - start := state.stream.consumed - - header := encodePackEntryHeader(realType, int64(len(content))) - - startInt64, err := intconv.Uint64ToInt64(start) - if err != nil { - return 0, err - } - - _, err = state.packFile.WriteAt(header, startInt64) - if err != nil { - return 0, err - } - - headerLenInt64 := int64(len(header)) - section := &fileSectionWriter{file: state.packFile, off: startInt64 + headerLenInt64} - crc := crc32.NewIEEE() - - _, err = crc.Write(header) - if err != nil { - return 0, err - } - - counting := &countingWriter{dst: section} - - zw := zlib.NewWriter(io.MultiWriter(counting, crc)) - - _, err = zw.Write(content) - if err != nil { - return 0, err - } - - err = zw.Close() - if err != nil { - return 0, err - } - - headerLenUint64, err := intconv.IntToUint64(len(header)) - if err != nil { - return 0, err - } - - countingNUint64, err := intconv.IntToUint64(counting.n) - if err != nil { - return 0, err - } - - packedLen := headerLenUint64 + countingNUint64 - end := start + packedLen - state.stream.consumed = end - - headerLenUint32, err := intconv.IntToUint32(len(header)) - if err != nil { - return 0, err - } - - record := objectRecord{ - offset: start, - headerLen: headerLenUint32, - packedLen: packedLen, - crc32: crc.Sum32(), - packedType: realType, - realType: realType, - declaredSize: int64(len(content)), - dataOffset: start + headerLenUint64, - objectID: id, - resolved: true, - } - - recordIdx := len(state.records) - state.records = append(state.records, record) - state.offsetToRecord[start] = recordIdx - state.objectToRecord[id] = recordIdx - state.baseCache.add(recordIdx, realType, content) - - return recordIdx, nil -} diff --git a/object/store/packed/internal/ingest/thin_fix.go b/object/store/packed/internal/ingest/thin_fix.go deleted file mode 100644 index 5d701c52..00000000 --- a/object/store/packed/internal/ingest/thin_fix.go +++ /dev/null @@ -1,99 +0,0 @@ -package ingest - -import ( - "errors" - "fmt" - - "codeberg.org/lindenii/furgit/internal/intconv" - "codeberg.org/lindenii/furgit/internal/progress" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// maybeFixThin appends missing bases and rewrites pack header/trailer when needed. -func maybeFixThin(state *ingestState) error { - if len(state.unresolvedRefDeltas) == 0 { - return nil - } - - writeProgressf( - state, - "fixing thin pack: %d unresolved bases\r", - len(state.unresolvedRefDeltas), - ) - - if !state.opts.FixThin { - return &ThinPackUnresolvedError{Count: len(state.unresolvedRefDeltas)} - } - - if state.opts.ThinBase == nil { - return &ThinPackUnresolvedError{Count: len(state.unresolvedRefDeltas)} - } - - hashSize := int64(state.algo.Size()) - - info, err := state.packFile.Stat() - if err != nil { - return err - } - - size := info.Size() - if size < hashSize { - return fmt.Errorf("packfile/ingest: pack too short to trim trailer") - } - - newEnd := size - hashSize - - err = state.packFile.Truncate(newEnd) - if err != nil { - return err - } - - consumed, err := intconv.Int64ToUint64(newEnd) - if err != nil { - return err - } - - state.stream.consumed = consumed - - baseIDs := unresolvedThinBaseIDs(state) - - total := len(baseIDs) - meter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "fixing thin pack", - Total: uint64(total), - }) - meter.Set(0, 0) - - var appended uint64 - - for _, id := range baseIDs { - ty, content, err := state.opts.ThinBase.ReadBytesContent(id) - if err != nil { - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return fmt.Errorf("packfile/ingest: read thin base %s: %w", id, err) - } - - _, err = appendBaseObject(state, id, ty, content) - if err != nil { - return err - } - - state.thinFixed = true - - appended++ - meter.Set(appended, 0) - } - - err = rewritePackHeaderAndTrailer(state) - if err != nil { - return err - } - - meter.Stop(fmt.Sprintf("appended %d/%d, done", appended, total)) - - return nil -} diff --git a/object/store/packed/internal/ingest/thin_unresolved.go b/object/store/packed/internal/ingest/thin_unresolved.go deleted file mode 100644 index 757cc0e2..00000000 --- a/object/store/packed/internal/ingest/thin_unresolved.go +++ /dev/null @@ -1,34 +0,0 @@ -package ingest - -import ( - "bytes" - "slices" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// unresolvedThinBaseIDs returns sorted unique unresolved ref base IDs. -func unresolvedThinBaseIDs(state *ingestState) []objectid.ObjectID { - seen := make(map[objectid.ObjectID]struct{}) - - for _, idx := range state.unresolvedRefDeltas { - record := state.records[idx] - if record.packedType != objecttype.TypeRefDelta { - continue - } - - seen[record.baseObject] = struct{}{} - } - - out := make([]objectid.ObjectID, 0, len(seen)) - for id := range seen { - out = append(out, id) - } - - slices.SortFunc(out, func(a, b objectid.ObjectID) int { - return bytes.Compare(a.RawBytes(), b.RawBytes()) - }) - - return out -} diff --git a/object/store/packed/internal/ingest/trailer.go b/object/store/packed/internal/ingest/trailer.go deleted file mode 100644 index 7a26a8f2..00000000 --- a/object/store/packed/internal/ingest/trailer.go +++ /dev/null @@ -1,58 +0,0 @@ -package ingest - -import ( - "bytes" - "errors" - "fmt" - "io" -) - -// finishAndFlushTrailer reads trailer hash bytes, verifies trailer checksum, -// and optionally requires the source stream to hit EOF afterward. -func (scanner *streamScanner) finishAndFlushTrailer(requireTrailingEOF bool) error { - if scanner.hashSize <= 0 { - return fmt.Errorf("packfile/ingest: invalid hash size") - } - - trailer := make([]byte, scanner.hashSize) - - scanner.hashEnabled = false - - err := scanner.readFull(trailer) - if err != nil { - return &PackTrailerMismatchError{} - } - - scanner.packTrailer = append(scanner.packTrailer[:0], trailer...) - - if scanner.n-scanner.off > 0 { - return fmt.Errorf("packfile/ingest: pack has trailing garbage") - } - - if !requireTrailingEOF { - computed := scanner.hash.Sum(nil) - if !bytes.Equal(computed, trailer) { - return &PackTrailerMismatchError{} - } - - return nil - } - - var probe [1]byte - - n, err := scanner.Read(probe[:]) - if n > 0 || err == nil { - return fmt.Errorf("packfile/ingest: pack has trailing garbage") - } - - if !errors.Is(err, io.EOF) { - return err - } - - computed := scanner.hash.Sum(nil) - if !bytes.Equal(computed, trailer) { - return &PackTrailerMismatchError{} - } - - return nil -} diff --git a/object/store/packed/internal/ingest/use.go b/object/store/packed/internal/ingest/use.go deleted file mode 100644 index 97f8757a..00000000 --- a/object/store/packed/internal/ingest/use.go +++ /dev/null @@ -1,34 +0,0 @@ -package ingest - -import ( - "fmt" - "hash/crc32" -) - -// use consumes n unread bytes and updates accounting/checksum state. -func (scanner *streamScanner) use(n int) error { - if n < 0 || n > scanner.n-scanner.off { - return fmt.Errorf("packfile/ingest: invalid consume length %d", n) - } - - if n == 0 { - return nil - } - - chunk := scanner.buf[scanner.off : scanner.off+n] - if scanner.hashEnabled { - _, err := scanner.hash.Write(chunk) - if err != nil { - return err - } - } - - if scanner.inEntryCRC { - scanner.entryCRC = crc32.Update(scanner.entryCRC, crc32.IEEETable, chunk) - } - - scanner.off += n - scanner.consumed += uint64(n) - - return nil -} diff --git a/object/store/packed/internal/ingest/write.go b/object/store/packed/internal/ingest/write.go deleted file mode 100644 index efd27323..00000000 --- a/object/store/packed/internal/ingest/write.go +++ /dev/null @@ -1,50 +0,0 @@ -package ingest - -import ( - "bufio" - "io" - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// WritePack ingests one pack stream into destination and writes pack artifacts. -// -// Artifacts are published under content-addressed final names derived from the -// resulting pack hash. If those final names already exist, WritePack treats -// that as success and removes its temporary files. -func WritePack( - destination *os.Root, - algo objectid.Algorithm, - src io.Reader, - opts Options, -) (Result, error) { - if algo.Size() == 0 { - return Result{}, objectid.ErrInvalidAlgorithm - } - - reader := bufio.NewReader(src) - - header, headerRaw, err := readAndValidatePackHeader(reader) - if err != nil { - return Result{}, err - } - - if header.ObjectCount == 0 { - return discardZeroObjectPack(reader, algo, opts, headerRaw) - } - - state, err := newIngestState( - reader, - destination, - algo, - opts, - header, - headerRaw, - ) - if err != nil { - return Result{}, err - } - - return ingest(state) -} diff --git a/object/store/packed/internal/ingest/write_empty.go b/object/store/packed/internal/ingest/write_empty.go deleted file mode 100644 index 0d3401f0..00000000 --- a/object/store/packed/internal/ingest/write_empty.go +++ /dev/null @@ -1,58 +0,0 @@ -package ingest - -import ( - "bytes" - "errors" - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func discardZeroObjectPack( - src io.Reader, - algo objectid.Algorithm, - opts Options, - headerRaw [packHeaderSize]byte, -) (Result, error) { - hashImpl, err := algo.New() - if err != nil { - return Result{}, err - } - - _, _ = hashImpl.Write(headerRaw[:]) - - trailer := make([]byte, algo.Size()) - - _, err = io.ReadFull(src, trailer) - if err != nil { - return Result{}, &PackTrailerMismatchError{} - } - - computed := hashImpl.Sum(nil) - if !bytes.Equal(computed, trailer) { - return Result{}, &PackTrailerMismatchError{} - } - - if opts.RequireTrailingEOF { - var probe [1]byte - - n, err := src.Read(probe[:]) - if n > 0 || err == nil { - return Result{}, errors.New("packfile/ingest: pack has trailing garbage") - } - - if err != io.EOF { - return Result{}, err - } - } - - packHash, err := objectid.FromBytes(algo, trailer) - if err != nil { - return Result{}, err - } - - return Result{ - PackHash: packHash, - ObjectCount: 0, - }, nil -} diff --git a/object/store/packed/internal/reading/TODO b/object/store/packed/internal/reading/TODO deleted file mode 100644 index f4a5f48e..00000000 --- a/object/store/packed/internal/reading/TODO +++ /dev/null @@ -1,3 +0,0 @@ -* Per delta-plan memo map -* Internal handle/request context (might expose it externally later and add to global interface) -* Audit on mutex diff --git a/object/store/packed/internal/reading/close.go b/object/store/packed/internal/reading/close.go deleted file mode 100644 index 62c62025..00000000 --- a/object/store/packed/internal/reading/close.go +++ /dev/null @@ -1,35 +0,0 @@ -package reading - -// Close releases mapped pack/index resources associated with the store. -// -// Labels: MT-Unsafe. -func (store *Store) Close() error { - store.stateMu.Lock() - packs := store.packs - store.stateMu.Unlock() - store.idxMu.RLock() - indexes := store.idxByPack - store.idxMu.RUnlock() - - var closeErr error - - for _, pack := range packs { - err := pack.close() - if err != nil && closeErr == nil { - closeErr = err - } - } - - for _, index := range indexes { - err := index.close() - if err != nil && closeErr == nil { - closeErr = err - } - } - - store.cacheMu.Lock() - store.deltaCache.clear() - store.cacheMu.Unlock() - - return closeErr -} diff --git a/object/store/packed/internal/reading/delta_build_chain.go b/object/store/packed/internal/reading/delta_build_chain.go deleted file mode 100644 index a0e3151d..00000000 --- a/object/store/packed/internal/reading/delta_build_chain.go +++ /dev/null @@ -1,65 +0,0 @@ -package reading - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// deltaBuildChain walks one object's chain and builds a reconstruction chain. -func (store *Store) deltaBuildChain(start location) (deltaChain, error) { - visited := make(map[location]struct{}) - current := start - - var chain deltaChain - - for { - if _, ok := visited[current]; ok { - return deltaChain{}, fmt.Errorf("objectstore/packed: delta cycle while resolving object") - } - - visited[current] = struct{}{} - - _, meta, err := store.entryMetaAt(current) - if err != nil { - return deltaChain{}, err - } - - if meta.ty.IsBaseObject() { - chain.baseLoc = current - chain.baseType = meta.ty - - return chain, nil - } - - switch meta.ty { - case objecttype.TypeRefDelta: - chain.deltas = append(chain.deltas, deltaNode{ - loc: current, - dataOffset: meta.dataOffset, - }) - - next, err := store.lookup(meta.baseRefID) - if err != nil { - return deltaChain{}, err - } - - current = next - case objecttype.TypeOfsDelta: - chain.deltas = append(chain.deltas, deltaNode{ - loc: current, - dataOffset: meta.dataOffset, - }) - current = location{ - packName: current.packName, - offset: meta.baseOfs, - } - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: - return deltaChain{}, fmt.Errorf("objectstore/packed: internal invariant violation for base type %d", meta.ty) - case objecttype.TypeInvalid, objecttype.TypeFuture: - return deltaChain{}, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty) - default: - return deltaChain{}, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty) - } - } -} diff --git a/object/store/packed/internal/reading/delta_cache.go b/object/store/packed/internal/reading/delta_cache.go deleted file mode 100644 index 4259eb81..00000000 --- a/object/store/packed/internal/reading/delta_cache.go +++ /dev/null @@ -1,61 +0,0 @@ -package reading - -import ( - "codeberg.org/lindenii/furgit/internal/lru" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -const defaultDeltaCacheMaxBytes = 32 << 20 - -// deltaBaseKey identifies one base object by pack location. -type deltaBaseKey struct { - packName string - offset uint64 -} - -// deltaBaseValue stores one cached base object body. -type deltaBaseValue struct { - ty objecttype.Type - content []byte -} - -// deltaCache wraps a weighted LRU for resolved delta bases. -type deltaCache struct { - lru *lru.Cache[deltaBaseKey, deltaBaseValue] -} - -// newDeltaCache creates a delta base cache with a byte budget. -func newDeltaCache(maxBytes int64) *deltaCache { - return &deltaCache{ - lru: lru.New( - maxBytes, - func(_ deltaBaseKey, value deltaBaseValue) int64 { - return int64(len(value.content)) - }, - nil, - ), - } -} - -// get returns a cloned cached base object value. -func (cache *deltaCache) get(key deltaBaseKey) (objecttype.Type, []byte, bool) { - value, ok := cache.lru.Get(key) - if !ok { - return objecttype.TypeInvalid, nil, false - } - - return value.ty, append([]byte(nil), value.content...), true -} - -// add stores a cloned base object value. -func (cache *deltaCache) add(key deltaBaseKey, ty objecttype.Type, content []byte) { - cache.lru.Add(key, deltaBaseValue{ - ty: ty, - content: append([]byte(nil), content...), - }) -} - -// clear removes all cached entries. -func (cache *deltaCache) clear() { - cache.lru.Clear() -} diff --git a/object/store/packed/internal/reading/delta_chain.go b/object/store/packed/internal/reading/delta_chain.go deleted file mode 100644 index 6e82873e..00000000 --- a/object/store/packed/internal/reading/delta_chain.go +++ /dev/null @@ -1,13 +0,0 @@ -package reading - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// deltaChain describes how to reconstruct one requested object. -type deltaChain struct { - // baseLoc points to the innermost base object. - baseLoc location - // baseType is the canonical object type resolved from baseLoc. - baseType objecttype.Type - // deltas contains delta objects from target down toward base. - deltas []deltaNode -} diff --git a/object/store/packed/internal/reading/delta_node.go b/object/store/packed/internal/reading/delta_node.go deleted file mode 100644 index 56f7b078..00000000 --- a/object/store/packed/internal/reading/delta_node.go +++ /dev/null @@ -1,9 +0,0 @@ -package reading - -// deltaNode describes one delta object in a reconstruction chain. -type deltaNode struct { - // loc identifies the delta object's pack location. - loc location - // dataOffset points to the start of the delta zlib payload in pack. - dataOffset int -} diff --git a/object/store/packed/internal/reading/delta_resolve_chain.go b/object/store/packed/internal/reading/delta_resolve_chain.go deleted file mode 100644 index ec9c39e2..00000000 --- a/object/store/packed/internal/reading/delta_resolve_chain.go +++ /dev/null @@ -1,61 +0,0 @@ -package reading - -import ( - "fmt" - - deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// deltaResolveChain resolves one object chain into content bytes. -func (store *Store) deltaResolveChain(chain deltaChain, declaredSize int64) (objecttype.Type, []byte, error) { - ty, out, nextDelta, err := store.deltaResolveChainStart(chain) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - for i := nextDelta; i >= 0; i-- { - node := chain.deltas[i] - - pack, err := store.openPack(node.loc.packName) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - delta, err := inflateAt(pack, node.dataOffset, -1) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - out, err = deltaapply.Apply(out, delta) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - store.cacheMu.Lock() - store.deltaCache.add( - deltaBaseKey{packName: node.loc.packName, offset: node.loc.offset}, - ty, - out, - ) - store.cacheMu.Unlock() - } - - if int64(len(out)) != declaredSize { - return objecttype.TypeInvalid, nil, fmt.Errorf( - "objectstore/packed: resolved content size mismatch: got %d want %d", - len(out), - declaredSize, - ) - } - - if ty != chain.baseType { - return objecttype.TypeInvalid, nil, fmt.Errorf( - "objectstore/packed: resolved content type mismatch: got %d want %d", - ty, - chain.baseType, - ) - } - - return ty, out, nil -} diff --git a/object/store/packed/internal/reading/delta_resolve_chain_start.go b/object/store/packed/internal/reading/delta_resolve_chain_start.go deleted file mode 100644 index 17274027..00000000 --- a/object/store/packed/internal/reading/delta_resolve_chain_start.go +++ /dev/null @@ -1,58 +0,0 @@ -package reading - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// deltaResolveChainStart finds the nearest cached chain node or inflates the -// innermost base object. It returns the starting bytes and the next delta index -// to apply in reverse order. -func (store *Store) deltaResolveChainStart(chain deltaChain) (objecttype.Type, []byte, int, error) { - for i, node := range chain.deltas { - store.cacheMu.RLock() - ty, out, ok := store.deltaCache.get( - deltaBaseKey{packName: node.loc.packName, offset: node.loc.offset}, - ) - store.cacheMu.RUnlock() - - if ok { - return ty, out, i - 1, nil - } - } - - store.cacheMu.RLock() - ty, out, ok := store.deltaCache.get( - deltaBaseKey{packName: chain.baseLoc.packName, offset: chain.baseLoc.offset}, - ) - store.cacheMu.RUnlock() - - if ok { - return ty, out, len(chain.deltas) - 1, nil - } - - pack, meta, err := store.entryMetaAt(chain.baseLoc) - if err != nil { - return objecttype.TypeInvalid, nil, 0, err - } - - if !meta.ty.IsBaseObject() { - return objecttype.TypeInvalid, nil, 0, fmt.Errorf("objectstore/packed: delta chain base is not a base object") - } - - base, err := inflateAt(pack, meta.dataOffset, meta.size) - if err != nil { - return objecttype.TypeInvalid, nil, 0, err - } - - store.cacheMu.Lock() - store.deltaCache.add( - deltaBaseKey{packName: chain.baseLoc.packName, offset: chain.baseLoc.offset}, - meta.ty, - base, - ) - store.cacheMu.Unlock() - - return meta.ty, base, len(chain.deltas) - 1, nil -} diff --git a/object/store/packed/internal/reading/delta_resolve_content.go b/object/store/packed/internal/reading/delta_resolve_content.go deleted file mode 100644 index 71eb69cf..00000000 --- a/object/store/packed/internal/reading/delta_resolve_content.go +++ /dev/null @@ -1,26 +0,0 @@ -package reading - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// deltaResolveContent resolves one object's content bytes from its pack location. -func (store *Store) deltaResolveContent(start location) (objecttype.Type, []byte, error) { - chain, err := store.deltaBuildChain(start) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - pack, meta, err := store.entryMetaAt(start) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - declaredSize := meta.size - if !meta.ty.IsBaseObject() { - declaredSize, err = deltaDeclaredSizeAt(pack, meta.dataOffset) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - } - - return store.deltaResolveChain(chain, declaredSize) -} diff --git a/object/store/packed/internal/reading/delta_size.go b/object/store/packed/internal/reading/delta_size.go deleted file mode 100644 index 8a85fad9..00000000 --- a/object/store/packed/internal/reading/delta_size.go +++ /dev/null @@ -1,27 +0,0 @@ -package reading - -import ( - "bufio" - - deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply" -) - -// deltaDeclaredSizeAt returns the resolved object size declared by one delta -// stream header at dataOffset. -func deltaDeclaredSizeAt(pack *packFile, dataOffset int) (int64, error) { - reader, err := zlibReaderAt(pack, dataOffset) - if err != nil { - return 0, err - } - - defer func() { _ = reader.Close() }() - - br := bufio.NewReaderSize(reader, 32) - - _, size, err := deltaapply.ReadHeaderSizes(br) - if err != nil { - return 0, err - } - - return int64(size), nil -} diff --git a/object/store/packed/internal/reading/doc.go b/object/store/packed/internal/reading/doc.go deleted file mode 100644 index a513d3bd..00000000 --- a/object/store/packed/internal/reading/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package reading implements the packed-store read path: pack and index -// discovery, lookup, caching, and object reads from existing packfiles. -// -// Obviously, this internal package is not meant to be used by anyone -// other than object/store/packed. -package reading diff --git a/object/store/packed/internal/reading/entry_inflate.go b/object/store/packed/internal/reading/entry_inflate.go deleted file mode 100644 index 82b2a7a8..00000000 --- a/object/store/packed/internal/reading/entry_inflate.go +++ /dev/null @@ -1,64 +0,0 @@ -package reading - -import ( - "bytes" - "fmt" - "io" - "math" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" - "codeberg.org/lindenii/furgit/internal/iolimit" -) - -// zlibReaderAt opens a zlib reader starting at data offset within pack. -func zlibReaderAt(pack *packFile, offset int) (io.ReadCloser, error) { - if offset < 0 || offset > len(pack.data) { - return nil, fmt.Errorf("objectstore/packed: pack %q zlib offset out of bounds", pack.name) - } - - return zlib.NewReader(bytes.NewReader(pack.data[offset:])) -} - -// inflateAt inflates one entry payload from data offset. -func inflateAt(pack *packFile, offset int, expectedSize int64) ([]byte, error) { - reader, err := zlibReaderAt(pack, offset) - if err != nil { - return nil, err - } - - defer func() { _ = reader.Close() }() - - if expectedSize >= 0 { - if expectedSize > int64(math.MaxInt) { - return nil, fmt.Errorf( - "objectstore/packed: pack %q expected inflated size overflows int: %d", - pack.name, - expectedSize, - ) - } - - reader := iolimit.ExpectLengthReader(reader, expectedSize) - body := make([]byte, int(expectedSize)) - - _, err := io.ReadFull(reader, body) - if err != nil { - return nil, err - } - - var probe [1]byte - - _, err = reader.Read(probe[:]) - if err != nil && err != io.EOF { - return nil, err - } - - return body, nil - } - - body, err := io.ReadAll(reader) - if err != nil { - return nil, err - } - - return body, nil -} diff --git a/object/store/packed/internal/reading/entry_meta.go b/object/store/packed/internal/reading/entry_meta.go deleted file mode 100644 index 336dc3b9..00000000 --- a/object/store/packed/internal/reading/entry_meta.go +++ /dev/null @@ -1,16 +0,0 @@ -package reading - -// entryMetaAt parses one pack entry header at location. -func (store *Store) entryMetaAt(loc location) (*packFile, entryMeta, error) { - pack, err := store.openPack(loc.packName) - if err != nil { - return nil, entryMeta{}, err - } - - meta, err := parseEntryMeta(pack, store.algo, loc.offset) - if err != nil { - return nil, entryMeta{}, err - } - - return pack, meta, nil -} diff --git a/object/store/packed/internal/reading/entry_parse.go b/object/store/packed/internal/reading/entry_parse.go deleted file mode 100644 index ecbfb6cb..00000000 --- a/object/store/packed/internal/reading/entry_parse.go +++ /dev/null @@ -1,71 +0,0 @@ -package reading - -import ( - "fmt" - - packfmt "codeberg.org/lindenii/furgit/format/packfile" - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// entryMeta describes one parsed pack entry header. -type entryMeta struct { - // ty is the pack entry type tag. - ty objecttype.Type - // size is the declared resulting content size. - size int64 - // dataOffset points to the zlib payload start. - dataOffset int - // baseRefID is set for ref-delta entries. - baseRefID objectid.ObjectID - // baseOfs is set for ofs-delta entries. - baseOfs uint64 -} - -// parseEntryMeta parses one pack entry header at offset. -func parseEntryMeta(pack *packFile, algo objectid.Algorithm, offset uint64) (entryMeta, error) { - var zero entryMeta - if offset >= uint64(len(pack.data)) { - return zero, fmt.Errorf("objectstore/packed: pack %q offset %d out of bounds", pack.name, offset) - } - - pos, err := intconv.Uint64ToInt(offset) - if err != nil { - return zero, fmt.Errorf("objectstore/packed: pack %q offset conversion: %w", pack.name, err) - } - - entry, err := packfmt.ParseEntry(pack.data[pos:], algo.Size()) - if err != nil { - return zero, fmt.Errorf("objectstore/packed: pack %q: %w", pack.name, err) - } - - meta := entryMeta{ - ty: entry.Type, - size: entry.Size, - dataOffset: pos + entry.DataOffset, - } - switch meta.ty { - case objecttype.TypeRefDelta: - baseID, err := objectid.FromBytes(algo, entry.RefBaseID) - if err != nil { - return zero, fmt.Errorf("objectstore/packed: pack %q invalid ref-delta base id: %w", pack.name, err) - } - - meta.baseRefID = baseID - case objecttype.TypeOfsDelta: - if offset <= entry.OfsBaseDistance { - return zero, fmt.Errorf("objectstore/packed: pack %q has invalid ofs-delta base", pack.name) - } - - meta.baseOfs = offset - entry.OfsBaseDistance - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: - // Base object types do not have delta base metadata. - case objecttype.TypeInvalid, objecttype.TypeFuture: - return zero, fmt.Errorf("objectstore/packed: pack %q has unsupported entry type %d", pack.name, meta.ty) - default: - return zero, fmt.Errorf("objectstore/packed: pack %q has unsupported entry type %d", pack.name, meta.ty) - } - - return meta, nil -} diff --git a/object/store/packed/internal/reading/helpers_test.go b/object/store/packed/internal/reading/helpers_test.go deleted file mode 100644 index 5a37d2f1..00000000 --- a/object/store/packed/internal/reading/helpers_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package reading_test - -import ( - "fmt" - "io" - "strconv" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/packed" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func openPackedStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *packed.Store { - t.Helper() - - root := testRepo.OpenPackRoot(t) - - store, err := packed.New(root, algo, packed.Options{}) - if err != nil { - t.Fatalf("packed.New: %v", err) - } - - return store -} - -func mustReadAllAndClose(t *testing.T, reader io.ReadCloser) []byte { - t.Helper() - - data, err := io.ReadAll(reader) - if err != nil { - _ = reader.Close() - - t.Fatalf("ReadAll: %v", err) - } - - err = reader.Close() - if err != nil { - t.Fatalf("Close: %v", err) - } - - return data -} - -func expectedRawObject(t *testing.T, testRepo *testgit.TestRepo, id objectid.ObjectID) (objecttype.Type, []byte, []byte) { - t.Helper() - - typeName := testRepo.Run(t, "cat-file", "-t", id.String()) - - ty, ok := objecttype.Parse(typeName) - if !ok { - t.Fatalf("ParseName(%q) failed", typeName) - } - - body := testRepo.CatFile(t, typeName, id) - - header, ok := objectheader.Encode(ty, int64(len(body))) - if !ok { - t.Fatalf("objectheader.Encode failed") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return ty, body, raw -} - -func createPackedFixtureRepo(t *testing.T, algo objectid.Algorithm) (*testgit.TestRepo, []objectid.ObjectID) { - t.Helper() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - blobID, treeID, commitID := testRepo.MakeCommit(t, "packed store base commit") - testRepo.Run(t, "update-ref", "refs/heads/main", commitID.String()) - tagID := testRepo.TagAnnotated(t, "v1.0.0", commitID, "packed-store-tag") - - parent := commitID - - for i := range 24 { - content := "common-prefix\n" + strings.Repeat("line-"+strconv.Itoa(i%3)+"\n", 256) + fmt.Sprintf("tail-%d\n", i) - nextBlob, nextTree := testRepo.MakeSingleFileTree(t, fmt.Sprintf("file-%02d.txt", i), []byte(content)) - nextCommit := testRepo.CommitTree(t, nextTree, fmt.Sprintf("commit-%02d", i), parent) - testRepo.Run(t, "update-ref", "refs/heads/main", nextCommit.String()) - parent = nextCommit - - _ = nextBlob - _ = nextTree - } - - testRepo.Repack(t, "-a", "-d", "-f", "--window=64", "--depth=64") - - return testRepo, []objectid.ObjectID{ - blobID, - treeID, - commitID, - tagID, - parent, - } -} diff --git a/object/store/packed/internal/reading/idx.go b/object/store/packed/internal/reading/idx.go deleted file mode 100644 index 3c91e1a2..00000000 --- a/object/store/packed/internal/reading/idx.go +++ /dev/null @@ -1,36 +0,0 @@ -package reading - -import ( - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// idxFile stores one mapped and validated idx v2 file. -type idxFile struct { - // idxName is the basename of this .idx file. - idxName string - // packName is the matching .pack basename. - packName string - // algo is the hash algorithm encoded by the index. - algo objectid.Algorithm - - // file is the opened index file descriptor. - file *os.File - // data is the mapped index bytes. - data []byte - - // fanout stores fanout table values. - fanout [256]uint32 - // numObjects equals fanout[255]. - numObjects int - - // namesOffset starts the sorted object-id table. - namesOffset int - // offset32Offset starts the 32-bit offset table. - offset32Offset int - // offset64Offset starts the 64-bit offset table. - offset64Offset int - // offset64Count is the number of 64-bit offset entries. - offset64Count int -} diff --git a/object/store/packed/internal/reading/idx_candidates_mru.go b/object/store/packed/internal/reading/idx_candidates_mru.go deleted file mode 100644 index 08ab6f85..00000000 --- a/object/store/packed/internal/reading/idx_candidates_mru.go +++ /dev/null @@ -1,136 +0,0 @@ -package reading - -// packCandidateNode is one node in the candidate MRU order list. -type packCandidateNode struct { - packName string - prev *packCandidateNode - next *packCandidateNode -} - -func (store *Store) reconcileMRU(candidates []packCandidate) { - store.mruMu.Lock() - defer store.mruMu.Unlock() - - if store.mruNodeByPack == nil { - store.mruNodeByPack = make(map[string]*packCandidateNode, len(candidates)) - } - - present := make(map[string]struct{}, len(candidates)) - for _, candidate := range candidates { - present[candidate.packName] = struct{}{} - } - - ordered := make([]string, 0, len(candidates)) - - for node := store.mruHead; node != nil; node = node.next { - if _, ok := present[node.packName]; !ok { - continue - } - - ordered = append(ordered, node.packName) - delete(present, node.packName) - } - - for _, candidate := range candidates { - if _, ok := present[candidate.packName]; !ok { - continue - } - - ordered = append(ordered, candidate.packName) - delete(present, candidate.packName) - } - - store.mruHead = nil - store.mruTail = nil - store.mruNodeByPack = make(map[string]*packCandidateNode, len(ordered)) - - for _, packName := range ordered { - node := &packCandidateNode{ - packName: packName, - prev: store.mruTail, - } - if store.mruTail != nil { - store.mruTail.next = node - } - - if store.mruHead == nil { - store.mruHead = node - } - - store.mruTail = node - store.mruNodeByPack[packName] = node - } -} - -// touchCandidate moves one candidate to the front of the lookup order. -// This is done on a best-effort basis. -func (store *Store) touchCandidate(packName string) { - if !store.mruMu.TryLock() { - return - } - defer store.mruMu.Unlock() - - node := store.mruNodeByPack[packName] - if node == nil || node == store.mruHead { - return - } - - if node.prev != nil { - node.prev.next = node.next - } - - if node.next != nil { - node.next.prev = node.prev - } - - if store.mruTail == node { - store.mruTail = node.prev - } - - node.prev = nil - - node.next = store.mruHead - if store.mruHead != nil { - store.mruHead.prev = node - } - - store.mruHead = node - if store.mruTail == nil { - store.mruTail = node - } -} - -// firstCandidatePackName returns the current head pack name, or "" when none -// are available. -func (store *Store) firstCandidatePackName(snapshot *candidateSnapshot) string { - store.mruMu.RLock() - defer store.mruMu.RUnlock() - - for node := store.mruHead; node != nil; node = node.next { - if _, ok := snapshot.candidateByPack[node.packName]; ok { - return node.packName - } - } - - return "" -} - -// nextCandidatePackName returns the pack name after currentPack in current MRU -// order, or "" at end / when currentPack is not present. -func (store *Store) nextCandidatePackName(currentPack string, snapshot *candidateSnapshot) string { - store.mruMu.RLock() - defer store.mruMu.RUnlock() - - node := store.mruNodeByPack[currentPack] - if node == nil { - return "" - } - - for node = node.next; node != nil; node = node.next { - if _, ok := snapshot.candidateByPack[node.packName]; ok { - return node.packName - } - } - - return "" -} diff --git a/object/store/packed/internal/reading/idx_close.go b/object/store/packed/internal/reading/idx_close.go deleted file mode 100644 index 1590854c..00000000 --- a/object/store/packed/internal/reading/idx_close.go +++ /dev/null @@ -1,28 +0,0 @@ -package reading - -import "syscall" - -// close unmaps and closes one idx handle. -func (index *idxFile) close() error { - var closeErr error - - if index.data != nil { - err := syscall.Munmap(index.data) - if err != nil && closeErr == nil { - closeErr = err - } - - index.data = nil - } - - if index.file != nil { - err := index.file.Close() - if err != nil && closeErr == nil { - closeErr = err - } - - index.file = nil - } - - return closeErr -} diff --git a/object/store/packed/internal/reading/idx_lookup.go b/object/store/packed/internal/reading/idx_lookup.go deleted file mode 100644 index bb02fb20..00000000 --- a/object/store/packed/internal/reading/idx_lookup.go +++ /dev/null @@ -1,91 +0,0 @@ -package reading - -import ( - "bytes" - "encoding/binary" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// lookup resolves one object ID to its pack offset within this index. -func (index *idxFile) lookup(id objectid.ObjectID) (uint64, bool, error) { - if id.Algorithm() != index.algo { - return 0, false, fmt.Errorf("objectstore/packed: object id algorithm mismatch") - } - - idBytes := (&id).RawBytes() - - hashSize := len(idBytes) - if hashSize != index.algo.Size() { - return 0, false, fmt.Errorf("objectstore/packed: unexpected object id length") - } - - first := int(idBytes[0]) - - lo := 0 - if first > 0 { - lo = int(index.fanout[first-1]) - } - - hi := int(index.fanout[first]) - if lo < 0 || hi < 0 || lo > hi || hi > index.numObjects { - return 0, false, fmt.Errorf("objectstore/packed: idx %q has invalid fanout bounds", index.idxName) - } - - for lo < hi { - mid := lo + (hi-lo)/2 - - nameOffset := index.namesOffset + mid*hashSize - if nameOffset < 0 || nameOffset+hashSize > len(index.data) { - return 0, false, fmt.Errorf("objectstore/packed: idx %q truncated name table", index.idxName) - } - - cmp := bytes.Compare(index.data[nameOffset:nameOffset+hashSize], idBytes) - if cmp == 0 { - offset, err := index.offsetAt(mid) - if err != nil { - return 0, false, err - } - - return offset, true, nil - } - - if cmp < 0 { - lo = mid + 1 - } else { - hi = mid - } - } - - return 0, false, nil -} - -// offsetAt resolves the pack offset for one object index entry. -func (index *idxFile) offsetAt(objectIndex int) (uint64, error) { - if objectIndex < 0 || objectIndex >= index.numObjects { - return 0, fmt.Errorf("objectstore/packed: idx %q offset index out of bounds", index.idxName) - } - - wordOffset := index.offset32Offset + objectIndex*4 - if wordOffset < 0 || wordOffset+4 > len(index.data) { - return 0, fmt.Errorf("objectstore/packed: idx %q truncated 32-bit offset table", index.idxName) - } - - word := binary.BigEndian.Uint32(index.data[wordOffset : wordOffset+4]) - if word&0x80000000 == 0 { - return uint64(word), nil - } - - pos := int(word & 0x7fffffff) - if pos < 0 || pos >= index.offset64Count { - return 0, fmt.Errorf("objectstore/packed: idx %q invalid 64-bit offset position", index.idxName) - } - - offOffset := index.offset64Offset + pos*8 - if offOffset < 0 || offOffset+8 > len(index.data)-2*index.algo.Size() { - return 0, fmt.Errorf("objectstore/packed: idx %q truncated 64-bit offset table", index.idxName) - } - - return binary.BigEndian.Uint64(index.data[offOffset : offOffset+8]), nil -} diff --git a/object/store/packed/internal/reading/idx_lookup_candidates.go b/object/store/packed/internal/reading/idx_lookup_candidates.go deleted file mode 100644 index c89ada7a..00000000 --- a/object/store/packed/internal/reading/idx_lookup_candidates.go +++ /dev/null @@ -1,126 +0,0 @@ -package reading - -import ( - "fmt" - "os" - "slices" - "strings" -) - -// packCandidate describes one discovered pack/index pair. -type packCandidate struct { - // packName is the .pack basename. - packName string - // idxName is the .idx basename. - idxName string - // mtime is the pack file modification time for initial ordering. - mtime int64 -} - -type candidateSnapshot struct { - candidates []packCandidate - candidateByPack map[string]packCandidate -} - -// Refresh rescans objects/pack and atomically installs a fresh candidate list -// for future lookups. -// -// Refresh does not invalidate existing readers. Cached pack/index mappings, -// including ones for previously visible candidates, may be retained until -// Close. -func (store *Store) Refresh() error { - store.refreshMu.Lock() - defer store.refreshMu.Unlock() - - candidates, err := store.discoverCandidates() - if err != nil { - return err - } - - candidateByPack := make(map[string]packCandidate, len(candidates)) - for _, candidate := range candidates { - candidateByPack[candidate.packName] = candidate - } - - store.reconcileMRU(candidates) - - store.candidates.Store(&candidateSnapshot{ - candidates: candidates, - candidateByPack: candidateByPack, - }) - - return nil -} - -func (store *Store) ensureCandidates() (*candidateSnapshot, error) { - snapshot := store.candidates.Load() - if snapshot != nil { - return snapshot, nil - } - - err := store.Refresh() - if err != nil { - return nil, err - } - - return store.candidates.Load(), nil -} - -// discoverCandidates scans the objects/pack root and returns sorted pack/index -// pairs. -func (store *Store) discoverCandidates() ([]packCandidate, error) { - dir, err := store.root.Open(".") - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - - return nil, err - } - - defer func() { _ = dir.Close() }() - - entries, err := dir.ReadDir(-1) - if err != nil { - return nil, err - } - - candidates := make([]packCandidate, 0, len(entries)) - for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".idx") { - continue - } - - idxName := entry.Name() - packName := strings.TrimSuffix(idxName, ".idx") + ".pack" - - packInfo, err := store.root.Stat(packName) - if err != nil { - if os.IsNotExist(err) { - return nil, fmt.Errorf("objectstore/packed: missing pack file for index %q", idxName) - } - - return nil, err - } - - candidates = append(candidates, packCandidate{ - packName: packName, - idxName: idxName, - mtime: packInfo.ModTime().UnixNano(), - }) - } - - slices.SortFunc(candidates, func(a, b packCandidate) int { - if a.mtime != b.mtime { - if a.mtime > b.mtime { - return -1 - } - - return 1 - } - - return strings.Compare(a.packName, b.packName) - }) - - return candidates, nil -} diff --git a/object/store/packed/internal/reading/idx_open.go b/object/store/packed/internal/reading/idx_open.go deleted file mode 100644 index 8f73c867..00000000 --- a/object/store/packed/internal/reading/idx_open.go +++ /dev/null @@ -1,98 +0,0 @@ -package reading - -import ( - "fmt" - "os" - "syscall" - - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// openIndex returns one opened and parsed index, caching it by pack basename. -func (store *Store) openIndex(candidate packCandidate) (*idxFile, error) { - store.idxMu.RLock() - - index, ok := store.idxByPack[candidate.packName] - if ok { - store.idxMu.RUnlock() - - return index, nil - } - - store.idxMu.RUnlock() - - index, err := openIdxFile(store.root, candidate.idxName, candidate.packName, store.algo) - if err != nil { - return nil, err - } - - store.idxMu.Lock() - - existing, ok := store.idxByPack[candidate.packName] - if ok { - store.idxMu.Unlock() - - _ = index.close() - - return existing, nil - } - - store.idxByPack[candidate.packName] = index - store.idxMu.Unlock() - - return index, nil -} - -// openIdxFile maps and validates one idx v2 file. -func openIdxFile(root *os.Root, idxName, packName string, algo objectid.Algorithm) (*idxFile, error) { - file, err := root.Open(idxName) - if err != nil { - return nil, err - } - - info, err := file.Stat() - if err != nil { - _ = file.Close() - - return nil, err - } - - size := info.Size() - if size < 0 || size > int64(int(^uint(0)>>1)) { - _ = file.Close() - - return nil, fmt.Errorf("objectstore/packed: idx %q has unsupported size", idxName) - } - - fd, err := intconv.UintptrToInt(file.Fd()) - if err != nil { - _ = file.Close() - - return nil, err - } - - data, err := syscall.Mmap(fd, 0, int(size), syscall.PROT_READ, syscall.MAP_PRIVATE) - if err != nil { - _ = file.Close() - - return nil, err - } - - index := &idxFile{ - idxName: idxName, - packName: packName, - algo: algo, - file: file, - data: data, - } - - err = index.parse() - if err != nil { - _ = index.close() - - return nil, err - } - - return index, nil -} diff --git a/object/store/packed/internal/reading/idx_parse.go b/object/store/packed/internal/reading/idx_parse.go deleted file mode 100644 index d38aaf4d..00000000 --- a/object/store/packed/internal/reading/idx_parse.go +++ /dev/null @@ -1,78 +0,0 @@ -package reading - -import ( - "encoding/binary" - "fmt" -) - -const ( - idxMagicV2 = 0xff744f63 - idxVersionV2 = 2 -) - -// parse validates mapped idx v2 structure and stores table boundaries. -func (index *idxFile) parse() error { - hashSize := index.algo.Size() - if hashSize <= 0 { - return fmt.Errorf("objectstore/packed: idx %q has invalid hash algorithm", index.idxName) - } - - minLen := 8 + 256*4 + 2*hashSize - if len(index.data) < minLen { - return fmt.Errorf("objectstore/packed: idx %q too short", index.idxName) - } - - if binary.BigEndian.Uint32(index.data[:4]) != idxMagicV2 { - return fmt.Errorf("objectstore/packed: idx %q invalid magic", index.idxName) - } - - if binary.BigEndian.Uint32(index.data[4:8]) != idxVersionV2 { - return fmt.Errorf("objectstore/packed: idx %q unsupported version", index.idxName) - } - - prev := uint32(0) - - for i := range 256 { - base := 8 + i*4 - - cur := binary.BigEndian.Uint32(index.data[base : base+4]) - if cur < prev { - return fmt.Errorf("objectstore/packed: idx %q has non-monotonic fanout table", index.idxName) - } - - index.fanout[i] = cur - prev = cur - } - - index.numObjects = int(index.fanout[255]) - if index.numObjects < 0 { - return fmt.Errorf("objectstore/packed: idx %q has invalid object count", index.idxName) - } - - namesBytes := index.numObjects * hashSize - crcBytes := index.numObjects * 4 - offset32Bytes := index.numObjects * 4 - - minSize := 8 + 256*4 + namesBytes + crcBytes + offset32Bytes + 2*hashSize - if minSize < 0 || len(index.data) < minSize { - return fmt.Errorf("objectstore/packed: idx %q has truncated tables", index.idxName) - } - - index.namesOffset = 8 + 256*4 - index.offset32Offset = index.namesOffset + namesBytes + crcBytes - index.offset64Offset = index.offset32Offset + offset32Bytes - - offset64Bytes := len(index.data) - index.offset64Offset - 2*hashSize - if offset64Bytes < 0 || offset64Bytes%8 != 0 { - return fmt.Errorf("objectstore/packed: idx %q has malformed 64-bit offset table", index.idxName) - } - - index.offset64Count = offset64Bytes / 8 - - maxOffset64Count := max(index.numObjects-1, 0) - if index.offset64Count > maxOffset64Count { - return fmt.Errorf("objectstore/packed: idx %q has oversized 64-bit offset table", index.idxName) - } - - return nil -} diff --git a/object/store/packed/internal/reading/location.go b/object/store/packed/internal/reading/location.go deleted file mode 100644 index f315dd1d..00000000 --- a/object/store/packed/internal/reading/location.go +++ /dev/null @@ -1,7 +0,0 @@ -package reading - -// location identifies one object entry in a specific pack file. -type location struct { - packName string - offset uint64 -} diff --git a/object/store/packed/internal/reading/new.go b/object/store/packed/internal/reading/new.go deleted file mode 100644 index d8a12db3..00000000 --- a/object/store/packed/internal/reading/new.go +++ /dev/null @@ -1,33 +0,0 @@ -package reading - -import ( - "fmt" - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// New creates a packed-object store rooted at an objects/pack directory. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(root *os.Root, algo objectid.Algorithm, opts Options) (*Store, error) { - if algo.Size() == 0 { - return nil, objectid.ErrInvalidAlgorithm - } - - switch opts.RefreshPolicy { - case RefreshPolicyOnMissing, RefreshPolicyNever: - default: - return nil, fmt.Errorf("objectstore/packed: invalid refresh policy %d", opts.RefreshPolicy) - } - - return &Store{ - root: root, - algo: algo, - refreshPolicy: opts.RefreshPolicy, - mruNodeByPack: make(map[string]*packCandidateNode), - idxByPack: make(map[string]*idxFile), - packs: make(map[string]*packFile), - deltaCache: newDeltaCache(defaultDeltaCacheMaxBytes), - }, nil -} diff --git a/object/store/packed/internal/reading/options.go b/object/store/packed/internal/reading/options.go deleted file mode 100644 index 0c5b76af..00000000 --- a/object/store/packed/internal/reading/options.go +++ /dev/null @@ -1,16 +0,0 @@ -package reading - -// RefreshPolicy configures when candidate pack/index discovery refreshes. -type RefreshPolicy uint8 - -const ( - // RefreshPolicyOnMissing refreshes candidates once after a lookup miss. - RefreshPolicyOnMissing RefreshPolicy = iota - // RefreshPolicyNever disables automatic refresh after lookup misses. - RefreshPolicyNever -) - -// Options configures a packed object store. -type Options struct { - RefreshPolicy RefreshPolicy -} diff --git a/object/store/packed/internal/reading/pack.go b/object/store/packed/internal/reading/pack.go deleted file mode 100644 index 431ed5f9..00000000 --- a/object/store/packed/internal/reading/pack.go +++ /dev/null @@ -1,82 +0,0 @@ -package reading - -import ( - "encoding/binary" - "fmt" - "os" - "syscall" - - packfmt "codeberg.org/lindenii/furgit/format/packfile" - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// packFile stores one mapped and validated .pack file. -type packFile struct { - // name is the .pack basename. - name string - // file is the opened pack file descriptor. - file *os.File - // data is the mapped pack bytes. - data []byte -} - -// openPackFile maps and validates one pack file. -func openPackFile(name string, file *os.File, size int64) (*packFile, error) { - if size < 12 { - return nil, fmt.Errorf("objectstore/packed: pack %q too short", name) - } - - if size > int64(int(^uint(0)>>1)) { - return nil, fmt.Errorf("objectstore/packed: pack %q has unsupported size", name) - } - - fd, err := intconv.UintptrToInt(file.Fd()) - if err != nil { - return nil, err - } - - data, err := syscall.Mmap(fd, 0, int(size), syscall.PROT_READ, syscall.MAP_PRIVATE) - if err != nil { - return nil, err - } - - if binary.BigEndian.Uint32(data[:4]) != packfmt.Signature { - _ = syscall.Munmap(data) - - return nil, fmt.Errorf("objectstore/packed: pack %q invalid signature", name) - } - - version := binary.BigEndian.Uint32(data[4:8]) - if !packfmt.SupportedVersion(version) { - _ = syscall.Munmap(data) - - return nil, fmt.Errorf("objectstore/packed: pack %q unsupported version %d", name, version) - } - - return &packFile{name: name, file: file, data: data}, nil -} - -// close unmaps and closes one pack handle. -func (pack *packFile) close() error { - var closeErr error - - if pack.data != nil { - err := syscall.Munmap(pack.data) - if err != nil && closeErr == nil { - closeErr = err - } - - pack.data = nil - } - - if pack.file != nil { - err := pack.file.Close() - if err != nil && closeErr == nil { - closeErr = err - } - - pack.file = nil - } - - return closeErr -} diff --git a/object/store/packed/internal/reading/pack_idx_checksum.go b/object/store/packed/internal/reading/pack_idx_checksum.go deleted file mode 100644 index b2ad09f1..00000000 --- a/object/store/packed/internal/reading/pack_idx_checksum.go +++ /dev/null @@ -1,34 +0,0 @@ -package reading - -import ( - "bytes" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// verifyMappedPackMatchesMappedIdx compares one mapped pack trailer hash with -// the pack hash recorded in one mapped idx trailer. -func verifyMappedPackMatchesMappedIdx(packData, idxData []byte, algo objectid.Algorithm) error { - hashSize := algo.Size() - if hashSize <= 0 { - return objectid.ErrInvalidAlgorithm - } - - if len(packData) < hashSize { - return fmt.Errorf("objectstore/packed: pack too short for trailer hash") - } - - if len(idxData) < hashSize*2 { - return fmt.Errorf("objectstore/packed: idx too short for trailer hashes") - } - - packTrailerHash := packData[len(packData)-hashSize:] - - idxPackHash := idxData[len(idxData)-hashSize*2 : len(idxData)-hashSize] - if !bytes.Equal(packTrailerHash, idxPackHash) { - return fmt.Errorf("objectstore/packed: pack hash does not match idx") - } - - return nil -} diff --git a/object/store/packed/internal/reading/read_bytes.go b/object/store/packed/internal/reading/read_bytes.go deleted file mode 100644 index f0821687..00000000 --- a/object/store/packed/internal/reading/read_bytes.go +++ /dev/null @@ -1,46 +0,0 @@ -package reading - -import ( - "fmt" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadBytesContent reads an object's type and content bytes. -// -// It fully resolves the requested object bytes. For base pack entries, this -// includes verifying that the zlib stream inflates to exactly the declared -// object size and verifying the Adler-32 trailer. -func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - loc, err := store.lookup(id) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - return store.deltaResolveContent(loc) -} - -// ReadBytesFull reads a full serialized object as "type size\0content". -// -// Like ReadBytesContent, it fully resolves the requested object bytes. For -// base pack entries, this includes verifying that the zlib stream inflates to -// exactly the declared object size and verifying the Adler-32 trailer. -func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - ty, content, err := store.ReadBytesContent(id) - if err != nil { - return nil, err - } - - header, ok := objectheader.Encode(ty, int64(len(content))) - if !ok { - return nil, fmt.Errorf("objectstore/packed: failed to encode object header for type %d", ty) - } - - out := make([]byte, len(header)+len(content)) - copy(out, header) - copy(out[len(header):], content) - - return out, nil -} diff --git a/object/store/packed/internal/reading/read_closer.go b/object/store/packed/internal/reading/read_closer.go deleted file mode 100644 index 4ef4c039..00000000 --- a/object/store/packed/internal/reading/read_closer.go +++ /dev/null @@ -1,19 +0,0 @@ -package reading - -import "io" - -// readCloser proxies reads and closes one underlying closer. -type readCloser struct { - reader io.Reader - closer io.Closer -} - -// Read proxies reads to the underlying reader. -func (reader *readCloser) Read(dst []byte) (int, error) { - return reader.reader.Read(dst) -} - -// Close closes the underlying closer. -func (reader *readCloser) Close() error { - return reader.closer.Close() -} diff --git a/object/store/packed/internal/reading/read_header.go b/object/store/packed/internal/reading/read_header.go deleted file mode 100644 index d627a6b3..00000000 --- a/object/store/packed/internal/reading/read_header.go +++ /dev/null @@ -1,20 +0,0 @@ -package reading - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadHeader reads an object's type and declared content size. -// -// It resolves header metadata only. It does not verify that the full pack entry -// payload is readable and does not verify any zlib Adler-32 trailer for -// compressed entry data. -func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - loc, err := store.lookup(id) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - return store.resolveHeaderAt(loc) -} diff --git a/object/store/packed/internal/reading/read_header_resolve.go b/object/store/packed/internal/reading/read_header_resolve.go deleted file mode 100644 index a2916b73..00000000 --- a/object/store/packed/internal/reading/read_header_resolve.go +++ /dev/null @@ -1,65 +0,0 @@ -package reading - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// resolveHeaderAt resolves one object's canonical type and declared content size. -func (store *Store) resolveHeaderAt(start location) (objecttype.Type, int64, error) { - visited := make(map[location]struct{}) - current := start - declaredSize := int64(-1) - - for { - if _, ok := visited[current]; ok { - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore/packed: delta cycle while resolving object header") - } - - visited[current] = struct{}{} - - pack, meta, err := store.entryMetaAt(current) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - if declaredSize < 0 { - if meta.ty.IsBaseObject() { - declaredSize = meta.size - } else { - size, err := deltaDeclaredSizeAt(pack, meta.dataOffset) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - declaredSize = size - } - } - - if meta.ty.IsBaseObject() { - return meta.ty, declaredSize, nil - } - - switch meta.ty { - case objecttype.TypeRefDelta: - next, err := store.lookup(meta.baseRefID) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - current = next - case objecttype.TypeOfsDelta: - current = location{ - packName: current.packName, - offset: meta.baseOfs, - } - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore/packed: internal invariant violation for base type %d", meta.ty) - case objecttype.TypeInvalid, objecttype.TypeFuture: - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty) - default: - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty) - } - } -} diff --git a/object/store/packed/internal/reading/read_reader.go b/object/store/packed/internal/reading/read_reader.go deleted file mode 100644 index 3fa0f592..00000000 --- a/object/store/packed/internal/reading/read_reader.go +++ /dev/null @@ -1,92 +0,0 @@ -package reading - -import ( - "bytes" - "fmt" - "io" - - "codeberg.org/lindenii/furgit/internal/iolimit" - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadReaderContent reads an object's type, declared content size, and content -// stream. -// -// Close releases reader-local 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 (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - loc, err := store.lookup(id) - if err != nil { - return objecttype.TypeInvalid, 0, nil, err - } - - pack, meta, err := store.entryMetaAt(loc) - if err != nil { - return objecttype.TypeInvalid, 0, nil, err - } - - if meta.ty.IsBaseObject() { - zr, err := zlibReaderAt(pack, meta.dataOffset) - if err != nil { - return objecttype.TypeInvalid, 0, nil, err - } - - return meta.ty, meta.size, &readCloser{ - reader: iolimit.ExpectLengthReader(zr, meta.size), - closer: zr, - }, nil - } - - ty, content, err := store.deltaResolveContent(loc) - if err != nil { - return objecttype.TypeInvalid, 0, nil, err - } - - return ty, int64(len(content)), io.NopCloser(bytes.NewReader(content)), nil -} - -// ReadReaderFull reads a full serialized object stream as "type size\0content". -// -// Close releases reader-local 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 (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - loc, err := store.lookup(id) - if err != nil { - return nil, err - } - - pack, meta, err := store.entryMetaAt(loc) - if err != nil { - return nil, err - } - - if meta.ty.IsBaseObject() { - header, ok := objectheader.Encode(meta.ty, meta.size) - if !ok { - return nil, fmt.Errorf("objectstore/packed: failed to encode object header for type %d", meta.ty) - } - - zr, err := zlibReaderAt(pack, meta.dataOffset) - if err != nil { - return nil, err - } - - return &readCloser{ - reader: io.MultiReader(bytes.NewReader(header), iolimit.ExpectLengthReader(zr, meta.size)), - closer: zr, - }, nil - } - - raw, err := store.ReadBytesFull(id) - if err != nil { - return nil, err - } - - return io.NopCloser(bytes.NewReader(raw)), nil -} diff --git a/object/store/packed/internal/reading/read_size.go b/object/store/packed/internal/reading/read_size.go deleted file mode 100644 index 3c1e05b1..00000000 --- a/object/store/packed/internal/reading/read_size.go +++ /dev/null @@ -1,45 +0,0 @@ -package reading - -import ( - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadSize reads an object's declared content size. -// -// Like ReadHeader, it resolves header metadata only. It does not verify that -// the full pack entry payload is readable and does not verify any zlib -// Adler-32 trailer for compressed entry data. -func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) { - loc, err := store.lookup(id) - if err != nil { - return 0, err - } - - return store.resolveSizeAt(loc) -} - -// resolveSizeAt resolves one object's declared content size from location. -func (store *Store) resolveSizeAt(start location) (int64, error) { - pack, meta, err := store.entryMetaAt(start) - if err != nil { - return 0, err - } - - if meta.ty.IsBaseObject() { - return meta.size, nil - } - - switch meta.ty { - case objecttype.TypeRefDelta, objecttype.TypeOfsDelta: - return deltaDeclaredSizeAt(pack, meta.dataOffset) - case objecttype.TypeInvalid, objecttype.TypeFuture: - return 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty) - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: - return 0, fmt.Errorf("objectstore/packed: internal invariant violation for base type %d", meta.ty) - default: - return 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty) - } -} diff --git a/object/store/packed/internal/reading/read_test.go b/object/store/packed/internal/reading/read_test.go deleted file mode 100644 index 8a92b603..00000000 --- a/object/store/packed/internal/reading/read_test.go +++ /dev/null @@ -1,301 +0,0 @@ -package reading_test - -import ( - "bytes" - "errors" - "fmt" - "io/fs" - "strconv" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - "codeberg.org/lindenii/furgit/object/store/packed" -) - -func TestPackedStoreReadAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo, ids := createPackedFixtureRepo(t, algo) - store := openPackedStore(t, testRepo, algo) - - for _, id := range ids { - t.Run(id.String(), func(t *testing.T) { - wantType, wantBody, wantRaw := expectedRawObject(t, testRepo, id) - - gotHeaderType, gotHeaderSize, err := store.ReadHeader(id) - if err != nil { - t.Fatalf("ReadHeader: %v", err) - } - - if gotHeaderType != wantType { - t.Fatalf("ReadHeader type = %v, want %v", gotHeaderType, wantType) - } - - if gotHeaderSize != int64(len(wantBody)) { - t.Fatalf("ReadHeader size = %d, want %d", gotHeaderSize, len(wantBody)) - } - - gotSize, err := store.ReadSize(id) - if err != nil { - t.Fatalf("ReadSize: %v", err) - } - - if gotSize != int64(len(wantBody)) { - t.Fatalf("ReadSize = %d, want %d", gotSize, len(wantBody)) - } - - gotRaw, err := store.ReadBytesFull(id) - if err != nil { - t.Fatalf("ReadBytesFull: %v", err) - } - - if !bytes.Equal(gotRaw, wantRaw) { - t.Fatalf("ReadBytesFull mismatch") - } - - gotType, gotBody, err := store.ReadBytesContent(id) - if err != nil { - t.Fatalf("ReadBytesContent: %v", err) - } - - if gotType != wantType { - t.Fatalf("ReadBytesContent type = %v, want %v", gotType, wantType) - } - - if !bytes.Equal(gotBody, wantBody) { - t.Fatalf("ReadBytesContent mismatch") - } - - fullReader, err := store.ReadReaderFull(id) - if err != nil { - t.Fatalf("ReadReaderFull: %v", err) - } - - got := mustReadAllAndClose(t, fullReader) - if !bytes.Equal(got, wantRaw) { - t.Fatalf("ReadReaderFull mismatch") - } - - contentType, contentSize, contentReader, err := store.ReadReaderContent(id) - if err != nil { - t.Fatalf("ReadReaderContent: %v", err) - } - - if contentType != wantType { - t.Fatalf("ReadReaderContent type = %v, want %v", contentType, wantType) - } - - if contentSize != int64(len(wantBody)) { - t.Fatalf("ReadReaderContent size = %d, want %d", contentSize, len(wantBody)) - } - - got = mustReadAllAndClose(t, contentReader) - if !bytes.Equal(got, wantBody) { - t.Fatalf("ReadReaderContent mismatch") - } - }) - } - }) -} - -func TestPackedStoreErrors(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo, _ := createPackedFixtureRepo(t, algo) - store := openPackedStore(t, testRepo, algo) - - notFoundID, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen())) - if err != nil { - t.Fatalf("ParseHex(notFound): %v", err) - } - - _, err = store.ReadBytesFull(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadBytesFull not-found error = %v", err) - } - - _, _, err = store.ReadBytesContent(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadBytesContent not-found error = %v", err) - } - - _, err = store.ReadReaderFull(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadReaderFull not-found error = %v", err) - } - - _, _, _, err = store.ReadReaderContent(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadReaderContent not-found error = %v", err) - } - - _, _, err = store.ReadHeader(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadHeader not-found error = %v", err) - } - - _, err = store.ReadSize(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadSize not-found error = %v", err) - } - - var otherAlgo objectid.Algorithm - - for _, candidate := range objectid.SupportedAlgorithms() { - if candidate != algo { - otherAlgo = candidate - - break - } - } - - if otherAlgo != objectid.AlgorithmUnknown { - mismatchID, err := objectid.ParseHex(otherAlgo, strings.Repeat("0", otherAlgo.HexLen())) - if err != nil { - t.Fatalf("ParseHex(mismatch): %v", err) - } - - _, err = store.ReadBytesFull(mismatchID) - if err == nil || !strings.Contains(err.Error(), "algorithm mismatch") { - t.Fatalf("ReadBytesFull algorithm-mismatch error = %v", err) - } - } - }) -} - -func TestPackedStoreNewValidation(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo, _ := createPackedFixtureRepo(t, algo) - - store := openPackedStore(t, testRepo, algo) - - err := store.Close() - if err != nil { - t.Fatalf("Close: %v", err) - } - }) -} - -func TestPackedStoreInvalidAlgorithm(t *testing.T) { - t.Parallel() - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectid.AlgorithmSHA1, Bare: true}) - - root := testRepo.OpenPackRoot(t) - - _, err := packed.New(root, objectid.AlgorithmUnknown, packed.Options{}) - if !errors.Is(err, objectid.ErrInvalidAlgorithm) { - t.Fatalf("packed.New invalid algorithm error = %v", err) - } -} - -func TestPackedStoreReadHeaderUsesResolvedObjectSizeForDelta(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - - var parent objectid.ObjectID - - for i := range 96 { - content := strings.Repeat("common-line-"+strconv.Itoa(i%7)+"\n", 384) + fmt.Sprintf("tail-%03d\n", i) - - _, treeID := testRepo.MakeSingleFileTree(t, "file.txt", []byte(content)) - if i == 0 { - parent = testRepo.CommitTree(t, treeID, "delta-header-size-0") - - continue - } - - parent = testRepo.CommitTree(t, treeID, fmt.Sprintf("delta-header-size-%03d", i), parent) - } - - testRepo.UpdateRef(t, "refs/heads/main", parent) - testRepo.Repack(t, "-a", "-d", "-f", "--window=128", "--depth=128") - - deltaID, wantResolvedSize := findDeltaObjectWithResolvedSizeMismatch(t, testRepo, algo) - store := openPackedStore(t, testRepo, algo) - - _, gotSize, err := store.ReadHeader(deltaID) - if err != nil { - t.Fatalf("ReadHeader(%s): %v", deltaID, err) - } - - if gotSize != wantResolvedSize { - t.Fatalf("ReadHeader(%s) size = %d, want resolved size %d", deltaID, gotSize, wantResolvedSize) - } - - gotReadSize, err := store.ReadSize(deltaID) - if err != nil { - t.Fatalf("ReadSize(%s): %v", deltaID, err) - } - - if gotReadSize != wantResolvedSize { - t.Fatalf("ReadSize(%s) = %d, want resolved size %d", deltaID, gotReadSize, wantResolvedSize) - } - }) -} - -func findDeltaObjectWithResolvedSizeMismatch(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) (objectid.ObjectID, int64) { - t.Helper() - - packRoot := testRepo.OpenPackRoot(t) - - entries, err := fs.ReadDir(packRoot.FS(), ".") - if err != nil { - t.Fatalf("ReadDir(pack): %v", err) - } - - var idxName string - - for _, entry := range entries { - if strings.HasSuffix(entry.Name(), ".idx") { - idxName = entry.Name() - - break - } - } - - if idxName == "" { - t.Fatalf("no idx files found") - } - - verifyOut := testRepo.Run(t, "verify-pack", "-v", "objects/pack/"+idxName) - for line := range strings.SplitSeq(strings.TrimSpace(verifyOut), "\n") { - fields := strings.Fields(line) - if len(fields) < 7 { - continue - } - - idHex := fields[0] - - deltaStreamSize, err := strconv.ParseInt(fields[2], 10, 64) - if err != nil { - continue - } - - resolvedSizeStr := testRepo.Run(t, "cat-file", "-s", idHex) - - resolvedSize, err := strconv.ParseInt(strings.TrimSpace(resolvedSizeStr), 10, 64) - if err != nil { - t.Fatalf("parse cat-file size for %s: %v", idHex, err) - } - - if deltaStreamSize == resolvedSize { - continue - } - - id, err := objectid.ParseHex(algo, idHex) - if err != nil { - t.Fatalf("ParseHex(%s): %v", idHex, err) - } - - return id, resolvedSize - } - - t.Fatalf("did not find a delta object with mismatched stream/resolved size") - - return objectid.ObjectID{}, 0 -} diff --git a/object/store/packed/internal/reading/store.go b/object/store/packed/internal/reading/store.go deleted file mode 100644 index cb4829ab..00000000 --- a/object/store/packed/internal/reading/store.go +++ /dev/null @@ -1,52 +0,0 @@ -package reading - -import ( - "os" - "sync" - "sync/atomic" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// Store reads Git objects from pack/index files under an objects/pack root. -// -// Cached pack/index mappings are retained until Close. -// -// Labels: Close-Caller. -type Store struct { - // root is the borrowed objects/pack capability used for all file access. - root *os.Root - // algo is the expected object ID algorithm for lookups. - algo objectid.Algorithm - // refreshPolicy controls automatic candidate refresh on lookup misses. - refreshPolicy RefreshPolicy - - // candidates stores the latest immutable candidate snapshot. - candidates atomic.Pointer[candidateSnapshot] - // refreshMu serializes candidate refresh. - refreshMu sync.Mutex - // mruMu guards candidate MRU linked-list state. - mruMu sync.RWMutex - // mruHead is the first pack in MRU order. - mruHead *packCandidateNode - // mruTail is the last pack in MRU order. - mruTail *packCandidateNode - // mruNodeByPack maps pack basename to MRU node. - mruNodeByPack map[string]*packCandidateNode - // idxByPack caches opened and parsed indexes by pack basename. - idxByPack map[string]*idxFile - - // stateMu guards pack cache and close state. - stateMu sync.RWMutex - // idxMu guards parsed index cache. - idxMu sync.RWMutex - // cacheMu guards delta cache operations. - cacheMu sync.RWMutex - // packs caches opened .pack handles by basename. - packs map[string]*packFile - // deltaCache caches resolved base objects by pack location. - deltaCache *deltaCache -} - -var _ objectstore.Reader = (*Store)(nil) diff --git a/object/store/packed/internal/reading/store_lookup.go b/object/store/packed/internal/reading/store_lookup.go deleted file mode 100644 index 9d863113..00000000 --- a/object/store/packed/internal/reading/store_lookup.go +++ /dev/null @@ -1,106 +0,0 @@ -package reading - -import ( - "errors" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// lookup resolves one object ID to its pack location. -func (store *Store) lookup(id objectid.ObjectID) (location, error) { - var zero location - if id.Algorithm() != store.algo { - return zero, errors.New("objectstore/packed: object id algorithm mismatch") - } - - snapshot, err := store.ensureCandidates() - if err != nil { - return zero, err - } - - loc, ok, err := store.lookupInCandidates(id, snapshot) - if err != nil { - return zero, err - } - - if ok { - return loc, nil - } - - if store.refreshPolicy == RefreshPolicyOnMissing { //nolint:nestif - err = store.Refresh() - if err != nil { - return zero, err - } - - refreshed := store.candidates.Load() - if refreshed != nil && refreshed != snapshot { - loc, ok, err = store.lookupInCandidates(id, refreshed) - if err != nil { - return zero, err - } - - if ok { - return loc, nil - } - } - } - - return zero, objectstore.ErrObjectNotFound -} - -func (store *Store) lookupInCandidates( - id objectid.ObjectID, - snapshot *candidateSnapshot, -) (location, bool, error) { - var zero location - - nextPackName := store.firstCandidatePackName(snapshot) - for nextPackName != "" { - candidate, ok := snapshot.candidateByPack[nextPackName] - if !ok { - nextPackName = store.firstCandidatePackName(snapshot) - - continue - } - - nextPackName = store.nextCandidatePackName(candidate.packName, snapshot) - - index, err := store.openIndex(candidate) - if err != nil { - return zero, false, err - } - - offset, ok, err := index.lookup(id) - if err != nil { - return zero, false, err - } - - if ok { - store.touchCandidate(candidate.packName) - - return location{packName: index.packName, offset: offset}, true, nil - } - } - - for _, candidate := range snapshot.candidates { - index, err := store.openIndex(candidate) - if err != nil { - return zero, false, err - } - - offset, ok, err := index.lookup(id) - if err != nil { - return zero, false, err - } - - if ok { - store.touchCandidate(candidate.packName) - - return location{packName: index.packName, offset: offset}, true, nil - } - } - - return zero, false, nil -} diff --git a/object/store/packed/internal/reading/store_open_pack.go b/object/store/packed/internal/reading/store_open_pack.go deleted file mode 100644 index 35cb960a..00000000 --- a/object/store/packed/internal/reading/store_open_pack.go +++ /dev/null @@ -1,57 +0,0 @@ -package reading - -// openPack returns one opened and validated pack handle. -func (store *Store) openPack(name string) (*packFile, error) { - store.stateMu.RLock() - - pack, ok := store.packs[name] - if ok { - store.stateMu.RUnlock() - - return pack, nil - } - - store.stateMu.RUnlock() - - file, err := store.root.Open(name) - if err != nil { - return nil, err - } - - info, err := file.Stat() - if err != nil { - _ = file.Close() - - return nil, err - } - - pack, err = openPackFile(name, file, info.Size()) - if err != nil { - _ = file.Close() - - return nil, err - } - - err = store.verifyPackMatchesIndexes(pack) - if err != nil { - _ = pack.close() - - return nil, err - } - - store.stateMu.Lock() - - existing, ok := store.packs[name] - if ok { - store.stateMu.Unlock() - - _ = pack.close() - - return existing, nil - } - - store.packs[name] = pack - store.stateMu.Unlock() - - return pack, nil -} diff --git a/object/store/packed/internal/reading/trailer_match.go b/object/store/packed/internal/reading/trailer_match.go deleted file mode 100644 index 8c7500b9..00000000 --- a/object/store/packed/internal/reading/trailer_match.go +++ /dev/null @@ -1,29 +0,0 @@ -package reading - -import "fmt" - -// verifyPackMatchesIndexes checks that one opened pack's trailer hash matches -// every loaded index that references the same pack name. -func (store *Store) verifyPackMatchesIndexes(pack *packFile) error { - snapshot, err := store.ensureCandidates() - if err != nil { - return err - } - - candidate, ok := snapshot.candidateByPack[pack.name] - if !ok { - return fmt.Errorf("objectstore/packed: missing index for pack %q", pack.name) - } - - index, err := store.openIndex(candidate) - if err != nil { - return err - } - - err = verifyMappedPackMatchesMappedIdx(pack.data, index.data, store.algo) - if err != nil { - return fmt.Errorf("objectstore/packed: pack %q does not match idx %q: %w", pack.name, index.idxName, err) - } - - return nil -} diff --git a/object/store/packed/new.go b/object/store/packed/new.go deleted file mode 100644 index cdc1b50f..00000000 --- a/object/store/packed/new.go +++ /dev/null @@ -1,25 +0,0 @@ -package packed - -import ( - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/packed/internal/reading" -) - -// New creates a packed-object store rooted at an objects/pack directory. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(root *os.Root, algo objectid.Algorithm, opts Options) (*Store, error) { - reader, err := reading.New(root, algo, opts.toReadingOptions()) - if err != nil { - return nil, err - } - - return &Store{ - root: root, - algo: algo, - opts: opts, - reader: reader, - }, nil -} diff --git a/object/store/packed/options.go b/object/store/packed/options.go deleted file mode 100644 index 718efc29..00000000 --- a/object/store/packed/options.go +++ /dev/null @@ -1,7 +0,0 @@ -package packed - -// Options configures a packed object store. -type Options struct { - RefreshPolicy RefreshPolicy - WriteRev bool -} diff --git a/object/store/packed/options_refresh.go b/object/store/packed/options_refresh.go deleted file mode 100644 index ee3d5f2e..00000000 --- a/object/store/packed/options_refresh.go +++ /dev/null @@ -1,11 +0,0 @@ -package packed - -// RefreshPolicy configures when candidate pack/index discovery refreshes. -type RefreshPolicy uint8 - -const ( - // RefreshPolicyOnMissing refreshes candidates once after a lookup miss. - RefreshPolicyOnMissing RefreshPolicy = iota - // RefreshPolicyNever disables automatic refresh after lookup misses. - RefreshPolicyNever -) diff --git a/object/store/packed/quarantine.go b/object/store/packed/quarantine.go deleted file mode 100644 index a8f6d08c..00000000 --- a/object/store/packed/quarantine.go +++ /dev/null @@ -1,19 +0,0 @@ -package packed - -import ( - "os" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -var _ objectstore.PackQuarantiner = (*Store)(nil) - -type packQuarantine struct { - *Store - - parent *Store - tempName string - tempRoot *os.Root -} - -var _ objectstore.PackQuarantine = (*packQuarantine)(nil) diff --git a/object/store/packed/quarantine_begin.go b/object/store/packed/quarantine_begin.go deleted file mode 100644 index 06b9a8a6..00000000 --- a/object/store/packed/quarantine_begin.go +++ /dev/null @@ -1,63 +0,0 @@ -package packed - -import ( - "crypto/rand" - "errors" - "fmt" - "io/fs" - "os" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// BeginPackQuarantine creates one quarantined packed store rooted privately -// beneath the destination pack root. -// -// Labels: Deps-Borrowed, Life-Parent, Close-No. -func (store *Store) BeginPackQuarantine(_ objectstore.PackQuarantineOptions) (objectstore.PackQuarantine, error) { - tempName, tempRoot, err := createPackQuarantineRoot(store.root) - if err != nil { - return nil, err - } - - quarantineStore, err := New(tempRoot, store.algo, store.opts) - if err != nil { - _ = tempRoot.Close() - _ = store.root.RemoveAll(tempName) - - return nil, err - } - - return &packQuarantine{ - Store: quarantineStore, - parent: store, - tempName: tempName, - tempRoot: tempRoot, - }, nil -} - -func createPackQuarantineRoot(parent *os.Root) (string, *os.Root, error) { - for range 32 { - name := "tmp_packq_" + rand.Text() - - err := parent.Mkdir(name, 0o700) - if err == nil { - root, err := parent.OpenRoot(name) - if err == nil { - return name, root, nil - } - - _ = parent.RemoveAll(name) - - return "", nil, err - } - - if errors.Is(err, fs.ErrExist) { - continue - } - - return "", nil, err - } - - return "", nil, fmt.Errorf("packed: unable to create quarantine directory") -} diff --git a/object/store/packed/quarantine_discard.go b/object/store/packed/quarantine_discard.go deleted file mode 100644 index a1dc7310..00000000 --- a/object/store/packed/quarantine_discard.go +++ /dev/null @@ -1,18 +0,0 @@ -package packed - -// Discard removes the quarantine and invalidates the receiver. -func (quarantine *packQuarantine) Discard() error { - closeErr := quarantine.Close() - tempRootErr := quarantine.tempRoot.Close() - removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) - - if closeErr != nil { - return closeErr - } - - if tempRootErr != nil { - return tempRootErr - } - - return removeErr -} diff --git a/object/store/packed/quarantine_promote.go b/object/store/packed/quarantine_promote.go deleted file mode 100644 index a4eb426d..00000000 --- a/object/store/packed/quarantine_promote.go +++ /dev/null @@ -1,89 +0,0 @@ -package packed - -import ( - "errors" - "fmt" - "io/fs" - "os" - "slices" - "strings" -) - -// Promote publishes all finalized pack artifacts in the quarantine into the -// parent packed store and invalidates the receiver. -func (quarantine *packQuarantine) Promote() error { - closeErr := quarantine.Close() - promoteErr := promotePackQuarantine(quarantine.parent.root, quarantine.tempName, quarantine.tempRoot) - tempRootErr := quarantine.tempRoot.Close() - removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) - - if closeErr != nil { - return closeErr - } - - if tempRootErr != nil { - return tempRootErr - } - - if promoteErr != nil { - return promoteErr - } - - return removeErr -} - -func promotePackQuarantine(parent *os.Root, tempName string, tempRoot *os.Root) error { - entries, err := fs.ReadDir(tempRoot.FS(), ".") - if err != nil && !errors.Is(err, fs.ErrNotExist) { - return err - } - - slices.SortFunc(entries, func(left, right fs.DirEntry) int { - return packPromotionPriority(left.Name()) - packPromotionPriority(right.Name()) - }) - - for _, entry := range entries { - if entry.IsDir() { - return fmt.Errorf("packed: quarantine contains unexpected directory %q", entry.Name()) - } - - err := promotePackQuarantineFile(parent, tempName, entry.Name()) - if err != nil { - return err - } - } - - return nil -} - -func promotePackQuarantineFile(parent *os.Root, tempName, name string) error { - src := tempName + "/" + name - - err := parent.Link(src, name) - if err == nil { - _ = parent.Remove(src) - - return nil - } - - if errors.Is(err, fs.ErrExist) { - _ = parent.Remove(src) - - return nil - } - - return fmt.Errorf("packed: promote quarantine %q -> %q: %w", src, name, err) -} - -func packPromotionPriority(name string) int { - switch { - case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".pack"): - return 1 - case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".rev"): - return 2 - case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".idx"): - return 3 - default: - return 0 - } -} diff --git a/object/store/packed/quarantine_test.go b/object/store/packed/quarantine_test.go deleted file mode 100644 index 036da535..00000000 --- a/object/store/packed/quarantine_test.go +++ /dev/null @@ -1,215 +0,0 @@ -package packed_test - -import ( - "bytes" - "os" - "path/filepath" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - "codeberg.org/lindenii/furgit/object/store/packed" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func fixturePath(t *testing.T, algo objectid.Algorithm, name string) string { - t.Helper() - - return filepath.Join("internal", "ingest", "testdata", "fixtures", algo.String(), name) -} - -func fixtureBytes(t *testing.T, algo objectid.Algorithm, name string) []byte { - t.Helper() - - path := fixturePath(t, algo, name) - dir := filepath.Dir(path) - base := filepath.Base(path) - - root, err := os.OpenRoot(dir) - if err != nil { - t.Fatalf("open fixture root %q: %v", dir, err) - } - - defer func() { - err := root.Close() - if err != nil { - t.Fatalf("close fixture root %q: %v", dir, err) - } - }() - - data, err := root.ReadFile(base) - if err != nil { - t.Fatalf("read fixture %q: %v", base, err) - } - - return data -} - -func fixtureMetadata(t *testing.T, algo objectid.Algorithm) map[string]string { - t.Helper() - - data := fixtureBytes(t, algo, "METADATA.txt") - out := make(map[string]string) - - for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - key, value, ok := strings.Cut(line, "=") - if !ok { - t.Fatalf("invalid fixture metadata line %q", line) - } - - out[strings.TrimSpace(key)] = strings.TrimSpace(value) - } - - return out -} - -func fixtureOID(t *testing.T, algo objectid.Algorithm, key string) objectid.ObjectID { - t.Helper() - - meta := fixtureMetadata(t, algo) - - hex, ok := meta[key] - if !ok { - t.Fatalf("missing fixture metadata key %q", key) - } - - id, err := objectid.ParseHex(algo, hex) - if err != nil { - t.Fatalf("parse fixture metadata oid %q: %v", hex, err) - } - - return id -} - -func TestPackQuarantinePromotePublishesWrittenObjects(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - packBytes := fixtureBytes(t, algo, "nonthin.pack") - - repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - packRoot := repo.OpenPackRoot(t) - - store, err := packed.New(packRoot, algo, packed.Options{WriteRev: true}) - if err != nil { - t.Fatalf("packed.New: %v", err) - } - - defer func() { - err := store.Close() - if err != nil { - t.Fatalf("store.Close: %v", err) - } - }() - - quarantiner, ok := any(store).(objectstore.PackQuarantiner) - if !ok { - t.Fatal("packed store does not implement PackQuarantiner") - } - - quarantine, err := quarantiner.BeginPackQuarantine(objectstore.PackQuarantineOptions{}) - if err != nil { - t.Fatalf("BeginPackQuarantine: %v", err) - } - - err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true}) - if err != nil { - t.Fatalf("quarantine.WritePack: %v", err) - } - - ty, _, err := quarantine.ReadHeader(head) - if err != nil { - t.Fatalf("quarantine.ReadHeader: %v", err) - } - - if ty != objecttype.TypeCommit { - t.Fatalf("quarantine.ReadHeader type = %v, want commit", ty) - } - - _, _, err = store.ReadHeader(head) - if err == nil { - t.Fatal("store.ReadHeader unexpectedly saw quarantined object before promote") - } - - err = quarantine.Promote() - if err != nil { - t.Fatalf("quarantine.Promote: %v", err) - } - - err = store.Refresh() - if err != nil { - t.Fatalf("store.Refresh: %v", err) - } - - ty, _, err = store.ReadHeader(head) - if err != nil { - t.Fatalf("store.ReadHeader after promote: %v", err) - } - - if ty != objecttype.TypeCommit { - t.Fatalf("store.ReadHeader type = %v, want commit", ty) - } - }) -} - -func TestPackQuarantineDiscardDropsWrittenObjects(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - packBytes := fixtureBytes(t, algo, "nonthin.pack") - - repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - packRoot := repo.OpenPackRoot(t) - - store, err := packed.New(packRoot, algo, packed.Options{WriteRev: true}) - if err != nil { - t.Fatalf("packed.New: %v", err) - } - - defer func() { - err := store.Close() - if err != nil { - t.Fatalf("store.Close: %v", err) - } - }() - - quarantiner, ok := any(store).(objectstore.PackQuarantiner) - if !ok { - t.Fatalf("expected objectstore.PackQuarantiner") - } - - quarantine, err := quarantiner.BeginPackQuarantine(objectstore.PackQuarantineOptions{}) - if err != nil { - t.Fatalf("BeginPackQuarantine: %v", err) - } - - err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true}) - if err != nil { - t.Fatalf("quarantine.WritePack: %v", err) - } - - err = quarantine.Discard() - if err != nil { - t.Fatalf("quarantine.Discard: %v", err) - } - - err = store.Refresh() - if err != nil { - t.Fatalf("store.Refresh: %v", err) - } - - _, _, err = store.ReadHeader(head) - if err == nil { - t.Fatal("store.ReadHeader unexpectedly saw discarded object") - } - }) -} diff --git a/object/store/packed/reader.go b/object/store/packed/reader.go deleted file mode 100644 index 45b9e8d9..00000000 --- a/object/store/packed/reader.go +++ /dev/null @@ -1,65 +0,0 @@ -package packed - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - "codeberg.org/lindenii/furgit/object/store/packed/internal/reading" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -var _ objectstore.Reader = (*Store)(nil) - -// ReadBytesFull reads a full serialized object as "type size\0content". -func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - return store.reader.ReadBytesFull(id) -} - -// ReadBytesContent reads an object's type and content bytes. -func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - return store.reader.ReadBytesContent(id) -} - -// ReadReaderFull reads a full serialized object stream as "type size\0content". -func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - return store.reader.ReadReaderFull(id) -} - -// ReadReaderContent reads an object's type, declared content length, and -// content stream. -func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - return store.reader.ReadReaderContent(id) -} - -// ReadSize reads an object's declared content length. -func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) { - return store.reader.ReadSize(id) -} - -// ReadHeader reads an object's type and declared content length. -func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - return store.reader.ReadHeader(id) -} - -// Refresh updates the packed-store view of on-disk pack/index candidates. -func (store *Store) Refresh() error { - return store.reader.Refresh() -} - -func (opts Options) toReadingOptions() reading.Options { - var refreshPolicy reading.RefreshPolicy - - switch opts.RefreshPolicy { - case RefreshPolicyOnMissing: - refreshPolicy = reading.RefreshPolicyOnMissing - case RefreshPolicyNever: - refreshPolicy = reading.RefreshPolicyNever - default: - refreshPolicy = reading.RefreshPolicy(opts.RefreshPolicy) - } - - return reading.Options{ - RefreshPolicy: refreshPolicy, - } -} diff --git a/object/store/packed/store.go b/object/store/packed/store.go deleted file mode 100644 index 2fe84c81..00000000 --- a/object/store/packed/store.go +++ /dev/null @@ -1,23 +0,0 @@ -package packed - -import ( - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/packed/internal/reading" -) - -// Store reads Git objects from pack/index files under an objects/pack root. -// -// Labels: Close-Caller. -type Store struct { - root *os.Root - algo objectid.Algorithm - opts Options - reader *reading.Store -} - -// Close releases mapped pack/index resources associated with the store. -func (store *Store) Close() error { - return store.reader.Close() -} diff --git a/object/store/packed/writer.go b/object/store/packed/writer.go deleted file mode 100644 index a96ea750..00000000 --- a/object/store/packed/writer.go +++ /dev/null @@ -1,22 +0,0 @@ -package packed - -import ( - "io" - - objectstore "codeberg.org/lindenii/furgit/object/store" - "codeberg.org/lindenii/furgit/object/store/packed/internal/ingest" -) - -var _ objectstore.PackWriter = (*Store)(nil) - -// WritePack ingests one pack stream into the packed store. -func (store *Store) WritePack(src io.Reader, opts objectstore.PackWriteOptions) error { - _, err := ingest.WritePack(store.root, store.algo, src, ingest.Options{ - WriteRev: store.opts.WriteRev, - ThinBase: opts.ThinBase, - Progress: opts.Progress, - RequireTrailingEOF: opts.RequireTrailingEOF, - }) - - return err -} diff --git a/object/store/quarantine.go b/object/store/quarantine.go deleted file mode 100644 index 5fa97ee7..00000000 --- a/object/store/quarantine.go +++ /dev/null @@ -1,20 +0,0 @@ -package objectstore - -// Quarantine represents one quarantined write that accepts both object- -// wise and pack-wise writes. -type Quarantine interface { - BaseQuarantine - Writer -} - -// QuarantineOptions controls the options for one coordinated quarantine creation. -type QuarantineOptions struct { - Object ObjectQuarantineOptions - Pack PackQuarantineOptions -} - -// Quarantiner creates coordinated quarantines that support both object- -// wise and pack-wise writes. -type Quarantiner interface { - BeginQuarantine(opts QuarantineOptions) (Quarantine, error) -} diff --git a/object/store/reader.go b/object/store/reader.go deleted file mode 100644 index 52a556bd..00000000 --- a/object/store/reader.go +++ /dev/null @@ -1,55 +0,0 @@ -package objectstore - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// Reader reads Git objects by object ID. -// -// Methods may perform implementation-defined integrity verification beyond -// successfully producing their documented result. -// -// Labels: MT-Safe. -type Reader interface { - // ReadBytesFull reads a full serialized object as "type size\0content". - // - // In a valid repository, hashing this payload with the same algorithm yields - // the requested object ID. Readers should treat this as a repository - // invariant and should not re-verify it on every read. - // - // Labels: Life-Parent. - ReadBytesFull(id objectid.ObjectID) ([]byte, error) - - // ReadBytesContent reads an object's type and content bytes. - // - // Labels: Life-Parent. - ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) - - // ReadReaderFull reads a full serialized object stream as "type size\0content". - // - // Labels: Life-Parent, Close-Caller. - ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) - - // ReadReaderContent reads an object's type, declared content length, - // and content stream. - // - // Labels: Life-Parent, Close-Caller. - ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) - - // ReadSize reads an object's declared content length. - // - // This is equivalent to ReadHeader(...).size and may be cheaper than - // ReadHeader when callers do not need object type. - ReadSize(id objectid.ObjectID) (int64, error) - - // ReadHeader reads an object's type and declared content length. - ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) - - // Refresh updates any backend-local discovery/cache view of on-disk objects. - // - // Backends without dynamic discovery should do nothing and return nil. - Refresh() error -} diff --git a/object/store/writer.go b/object/store/writer.go deleted file mode 100644 index 9fa05aba..00000000 --- a/object/store/writer.go +++ /dev/null @@ -1,8 +0,0 @@ -package objectstore - -// Writer represents a store that could perform both pack ingestions -// and individual object writes. -type Writer interface { - PackWriter - ObjectWriter -} diff --git a/object/store/writer_object.go b/object/store/writer_object.go deleted file mode 100644 index a18a5d84..00000000 --- a/object/store/writer_object.go +++ /dev/null @@ -1,37 +0,0 @@ -package objectstore - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ObjectWriter writes individual Git objects. -type ObjectWriter interface { - // WriteReaderContent writes one typed object content stream. - WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) - - // WriteReaderFull writes one full serialized object stream as "type size\0content". - WriteReaderFull(src io.Reader) (objectid.ObjectID, error) - - // WriteBytesContent writes one typed object content byte slice. - WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) - - // WriteBytesFull writes one full serialized object byte slice as "type size\0content". - WriteBytesFull(raw []byte) (objectid.ObjectID, error) -} - -// ObjectQuarantine represents one quarantined object-wise write. -type ObjectQuarantine interface { - BaseQuarantine - ObjectWriter -} - -// ObjectQuarantineOptions controls the options for one object quarantine creation. -type ObjectQuarantineOptions struct{} - -// ObjectQuarantiner creates quarantines for object-wise writes. -type ObjectQuarantiner interface { - BeginObjectQuarantine(opts ObjectQuarantineOptions) (ObjectQuarantine, error) -} diff --git a/object/store/writer_pack.go b/object/store/writer_pack.go deleted file mode 100644 index 0f78c429..00000000 --- a/object/store/writer_pack.go +++ /dev/null @@ -1,58 +0,0 @@ -package objectstore - -import ( - "io" - - "codeberg.org/lindenii/furgit/common/iowrap" -) - -// PackWriteOptions controls one pack write operation. -type PackWriteOptions struct { - // ThinBase supplies the wider object reader used to complete thin packs - // during ingestion. - // - // This is an option for the write operation rather than on a particular - // pack-backed store because any pack-accepting store is not generally - // expected to know the entire repository object universe around it. - // In a normal repository, thin bases usually come from a broader view - // such as mix(loose, packed), and should not be treated as a property of - // the destination pack-accepting store. Thus, in almost all pack-ingesting - // operations, a thin base reader would be required, and hence it is - // included here. - // - // When nil, external thin-base repair is disabled and unresolved thin deltas - // fail ingestion. - ThinBase Reader - - // Progress receives human-readable progress messages. - // - // When nil, no progress output is emitted. - Progress iowrap.WriteFlusher - - // RequireTrailingEOF requires the source to hit EOF after the pack trailer. - // - // This is suitable for exact pack-file readers, but should be disabled for - // full-duplex transport streams like receive-pack where the peer keeps the - // connection open to read the server response. - RequireTrailingEOF bool -} - -// PackWriter writes Git pack streams. -type PackWriter interface { - // WritePack ingests one pack stream. - WritePack(src io.Reader, opts PackWriteOptions) error -} - -// PackQuarantine represents one quarantined pack-wise write. -type PackQuarantine interface { - BaseQuarantine - PackWriter -} - -// PackQuarantineOptions controls the options for one pack quarantine creation. -type PackQuarantineOptions struct{} - -// PackQuarantiner creates quarantines for pack-wise writes. -type PackQuarantiner interface { - BeginPackQuarantine(opts PackQuarantineOptions) (PackQuarantine, error) -} diff --git a/object/stored/doc.go b/object/stored/doc.go deleted file mode 100644 index d57cbd55..00000000 --- a/object/stored/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package stored wraps parsed objects with the object IDs they were loaded -// under. -// -// Parsed Git object values do not carry storage identity on their own. This -// package provides a small generic wrapper for the common case where callers -// need both the parsed object value and the object ID it was read from. -package stored diff --git a/object/stored/id.go b/object/stored/id.go deleted file mode 100644 index 956d069e..00000000 --- a/object/stored/id.go +++ /dev/null @@ -1,8 +0,0 @@ -package stored - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// ID returns the object ID. -func (stored *Stored[T]) ID() objectid.ObjectID { - return stored.id -} diff --git a/object/stored/new.go b/object/stored/new.go deleted file mode 100644 index 8b0ef881..00000000 --- a/object/stored/new.go +++ /dev/null @@ -1,11 +0,0 @@ -package stored - -import ( - "codeberg.org/lindenii/furgit/object" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// New creates one stored object wrapper. -func New[T object.Object](id objectid.ObjectID, obj T) *Stored[T] { - return &Stored[T]{id: id, obj: obj} -} diff --git a/object/stored/object.go b/object/stored/object.go deleted file mode 100644 index ab22b9c8..00000000 --- a/object/stored/object.go +++ /dev/null @@ -1,6 +0,0 @@ -package stored - -// Object returns the wrapped object as itself. -func (stored *Stored[T]) Object() T { - return stored.obj -} diff --git a/object/stored/stored.go b/object/stored/stored.go deleted file mode 100644 index eb776f31..00000000 --- a/object/stored/stored.go +++ /dev/null @@ -1,13 +0,0 @@ -package stored - -import ( - "codeberg.org/lindenii/furgit/object" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Stored represents a stored object, -// i.e., an object along with its object ID. -type Stored[T object.Object] struct { - id objectid.ObjectID - obj T -} diff --git a/object/tag/parse.go b/object/tag/parse.go deleted file mode 100644 index 92fa0d8b..00000000 --- a/object/tag/parse.go +++ /dev/null @@ -1,89 +0,0 @@ -package tag - -import ( - "bytes" - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectsignature "codeberg.org/lindenii/furgit/object/signature" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// Parse decodes a tag object body. -func Parse(body []byte, algo objectid.Algorithm) (*Tag, error) { - t := new(Tag) - i := 0 - - var haveTarget, haveType bool - - for i < len(body) { - rel := bytes.IndexByte(body[i:], '\n') - if rel < 0 { - return nil, errors.New("object: tag: missing newline") - } - - line := body[i : i+rel] - i += rel + 1 - - if len(line) == 0 { - break - } - - key, value, found := bytes.Cut(line, []byte{' '}) - if !found { - return nil, errors.New("object: tag: malformed header") - } - - switch string(key) { - case "object": - id, err := objectid.ParseHex(algo, string(value)) - if err != nil { - return nil, fmt.Errorf("object: tag: object: %w", err) - } - - t.Target = id - haveTarget = true - case "type": - ty, ok := objecttype.Parse(string(value)) - if !ok { - return nil, errors.New("object: tag: unknown target type") - } - - t.TargetType = ty - haveType = true - case "tag": - t.Name = append([]byte(nil), value...) - case "tagger": - idt, err := objectsignature.Parse(value) - if err != nil { - return nil, fmt.Errorf("object: tag: tagger: %w", err) - } - - t.Tagger = idt - case "gpgsig", "gpgsig-sha256": - for i < len(body) { - nextRel := bytes.IndexByte(body[i:], '\n') - if nextRel < 0 { - return nil, errors.New("object: tag: unterminated gpgsig") - } - - if body[i] != ' ' { - break - } - - i += nextRel + 1 - } - default: - // Ignore unknown headers for now. - } - } - - if !haveTarget || !haveType { - return nil, errors.New("object: tag: missing required headers") - } - - t.Message = append([]byte(nil), body[i:]...) - - return t, nil -} diff --git a/object/tag/parse_test.go b/object/tag/parse_test.go deleted file mode 100644 index 293350ed..00000000 --- a/object/tag/parse_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package tag_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tag" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func TestTagParseFromGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, commitID := testRepo.MakeCommit(t, "subject\n\nbody") - tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message") - - rawBody := testRepo.CatFile(t, "tag", tagID) - - parsed, err := tag.Parse(rawBody, algo) - if err != nil { - t.Fatalf("ParseTag: %v", err) - } - - if parsed.Target != commitID { - t.Fatalf("tag target mismatch: got %s want %s", parsed.Target, commitID) - } - - if parsed.TargetType != objecttype.TypeCommit { - t.Fatalf("tag target type = %v, want %v", parsed.TargetType, objecttype.TypeCommit) - } - - if !bytes.Equal(parsed.Name, []byte("v1")) { - t.Fatalf("tag name = %q, want %q", parsed.Name, "v1") - } - - if parsed.Tagger == nil { - t.Fatalf("expected tagger") - } - - if !bytes.Contains(parsed.Message, []byte("tag message")) { - t.Fatalf("tag message mismatch: %q", parsed.Message) - } - }) -} diff --git a/object/tag/serialize.go b/object/tag/serialize.go deleted file mode 100644 index 4f9d6664..00000000 --- a/object/tag/serialize.go +++ /dev/null @@ -1,68 +0,0 @@ -package tag - -import ( - "bytes" - "errors" - "fmt" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// SerializeWithoutHeader renders the raw tag body bytes. -func (tag *Tag) SerializeWithoutHeader() ([]byte, error) { - if tag.Target.Algorithm().Size() == 0 { - return nil, errors.New("object: tag: missing target id") - } - - var buf bytes.Buffer - fmt.Fprintf(&buf, "object %s\n", tag.Target.String()) - - tyName, ok := tag.TargetType.Name() - if !ok { - return nil, fmt.Errorf("object: tag: invalid target type %d", tag.TargetType) - } - - buf.WriteString("type ") - buf.WriteString(tyName) - buf.WriteByte('\n') - - buf.WriteString("tag ") - buf.Write(tag.Name) - buf.WriteByte('\n') - - if tag.Tagger != nil { - taggerBytes, err := tag.Tagger.Serialize() - if err != nil { - return nil, err - } - - buf.WriteString("tagger ") - buf.Write(taggerBytes) - buf.WriteByte('\n') - } - - buf.WriteByte('\n') - buf.Write(tag.Message) - - return buf.Bytes(), nil -} - -// SerializeWithHeader renders the raw object (header + body). -func (tag *Tag) SerializeWithHeader() ([]byte, error) { - body, err := tag.SerializeWithoutHeader() - if err != nil { - return nil, err - } - - header, ok := objectheader.Encode(objecttype.TypeTag, int64(len(body))) - if !ok { - return nil, errors.New("object: tag: failed to encode object header") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return raw, nil -} diff --git a/object/tag/serialize_test.go b/object/tag/serialize_test.go deleted file mode 100644 index a1311c39..00000000 --- a/object/tag/serialize_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package tag_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tag" -) - -func TestTagSerialize(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, commitID := testRepo.MakeCommit(t, "subject\n\nbody") - tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message") - - rawBody := testRepo.CatFile(t, "tag", tagID) - - parsed, err := tag.Parse(rawBody, algo) - if err != nil { - t.Fatalf("ParseTag: %v", err) - } - - rawObj, err := parsed.SerializeWithHeader() - if err != nil { - t.Fatalf("SerializeWithHeader: %v", err) - } - - gotID := algo.Sum(rawObj) - if gotID != tagID { - t.Fatalf("tag id mismatch: got %s want %s", gotID, tagID) - } - }) -} diff --git a/object/tag/tag.go b/object/tag/tag.go deleted file mode 100644 index e01f8ac9..00000000 --- a/object/tag/tag.go +++ /dev/null @@ -1,24 +0,0 @@ -// Package tag provides parsed annotated tag objects and tag serialization. -// -// It parses annotated tags into ordinary Go values for reading and -// construction. It does not preserve the exact original byte layout needed for -// signature verification; callers that need signature-verification payload -// fidelity should use [codeberg.org/lindenii/furgit/object/signed/tag]. -package tag - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objectsignature "codeberg.org/lindenii/furgit/object/signature" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// Tag represents a fully materialized Git annotated tag object. -// -// Labels: MT-Unsafe. -type Tag struct { - Target objectid.ObjectID - TargetType objecttype.Type - Name []byte - Tagger *objectsignature.Signature - Message []byte -} diff --git a/object/tag/type.go b/object/tag/type.go deleted file mode 100644 index 215103ab..00000000 --- a/object/tag/type.go +++ /dev/null @@ -1,10 +0,0 @@ -package tag - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// ObjectType returns TypeTag. -func (tag *Tag) ObjectType() objecttype.Type { - _ = tag - - return objecttype.TypeTag -} diff --git a/object/tree/entry.go b/object/tree/entry.go deleted file mode 100644 index b3089b74..00000000 --- a/object/tree/entry.go +++ /dev/null @@ -1,57 +0,0 @@ -package tree - -import ( - "bytes" - "slices" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// TreeEntry represents a single entry in a tree. -type TreeEntry struct { - Mode FileMode - // Name is part of the tree ordering. Mutating it after insertion may break - // Tree ordering and lookup behavior. - Name []byte - ID objectid.ObjectID -} - -func (tree *Tree) entry(name []byte, searchIsTree bool) *TreeEntry { - index, ok := slices.BinarySearchFunc(tree.Entries, name, func(entry TreeEntry, name []byte) int { - return TreeEntryNameCompare(entry.Name, entry.Mode, name, searchIsTree) - }) - if !ok { - return nil - } - - entry := &tree.Entries[index] - if !bytes.Equal(entry.Name, name) { - return nil - } - - return entry -} - -func (tree *Tree) entryIndex(name []byte) (int, bool) { - index, ok := tree.entryIndexWithMode(name, true) - if ok { - return index, true - } - - return tree.entryIndexWithMode(name, false) -} - -func (tree *Tree) entryIndexWithMode(name []byte, searchIsTree bool) (int, bool) { - index, ok := slices.BinarySearchFunc(tree.Entries, name, func(entry TreeEntry, name []byte) int { - return TreeEntryNameCompare(entry.Name, entry.Mode, name, searchIsTree) - }) - if !ok { - return 0, false - } - - if !bytes.Equal(tree.Entries[index].Name, name) { - return 0, false - } - - return index, true -} diff --git a/object/tree/helpers_test.go b/object/tree/helpers_test.go deleted file mode 100644 index 3da92ce4..00000000 --- a/object/tree/helpers_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package tree_test - -import ( - "bytes" - "fmt" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/tree" -) - -func buildGitMktreeInput(entries []tree.TreeEntry) string { - var b strings.Builder - for _, e := range entries { - fmt.Fprintf(&b, "%o %s %s\t%s\n", e.Mode, mktreeTypeFromMode(e.Mode), e.ID.String(), e.Name) - } - - return b.String() -} - -func mktreeTypeFromMode(mode tree.FileMode) string { - switch mode { - case tree.FileModeDir: - return "tree" - case tree.FileModeRegular, tree.FileModeExecutable, tree.FileModeSymlink: - return "blob" - case tree.FileModeGitlink: - return "commit" - default: - return "" - } -} - -func gitLsTreeNames(out []byte) [][]byte { - if len(out) == 0 { - return nil - } - - parts := bytes.Split(out, []byte{0}) - if len(parts) > 0 && len(parts[len(parts)-1]) == 0 { - parts = parts[:len(parts)-1] - } - - names := make([][]byte, 0, len(parts)) - for _, name := range parts { - names = append(names, append([]byte(nil), name...)) - } - - return names -} - -func adversarialRootEntries(t *testing.T, testRepo *testgit.TestRepo) []tree.TreeEntry { - t.Helper() - - blobA := testRepo.HashObject(t, "blob", []byte("blob-A\n")) - blobB := testRepo.HashObject(t, "blob", []byte("blob-B\n")) - blobC := testRepo.HashObject(t, "blob", []byte("blob-C\n")) - - subDirA := testRepo.Mktree(t, - fmt.Sprintf("100644 blob %s\tnested-a.txt\n100755 blob %s\trun-a.sh\n", blobA.String(), blobB.String())) - subDirB := testRepo.Mktree(t, - fmt.Sprintf("100644 blob %s\tnested-b.txt\n100644 blob %s\tz-last\n", blobB.String(), blobC.String())) - subDirC := testRepo.Mktree(t, - fmt.Sprintf("120000 blob %s\tlink-c\n100644 blob %s\tchild\n", blobC.String(), blobA.String())) - subDirD := testRepo.Mktree(t, - fmt.Sprintf("100644 blob %s\tleaf\n", blobA.String())) - - return []tree.TreeEntry{ - {Mode: tree.FileModeRegular, Name: []byte("z"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("A"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("aa"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("a0"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("a-"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("a."), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("a_"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("a~"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("Z"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("0"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("9"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("00"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("这是一些非 ASCII 的字符"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("是新进入 Unicode 的字符"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("Emoji 👀"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("_"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("-dash"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("dot.file"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte(".hidden"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("CAPS"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("caps"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("mixCase"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("name with space"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("name-with-dash"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("name.with.dot"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("name_with_underscore"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("tilde~name"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("brace{name}"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("plus+name"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("equal=name"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("at@name"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("percent%name"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("caret^name"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("comma,name"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("semi;name"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("paren(name)"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("bracket[name]"), ID: blobA}, - {Mode: tree.FileModeExecutable, Name: []byte("exec.sh"), ID: blobB}, - {Mode: tree.FileModeSymlink, Name: []byte("sym.link"), ID: blobC}, - {Mode: tree.FileModeDir, Name: []byte("dir"), ID: subDirA}, - {Mode: tree.FileModeDir, Name: []byte("dir0"), ID: subDirB}, - {Mode: tree.FileModeDir, Name: []byte("dir.space"), ID: subDirC}, - {Mode: tree.FileModeDir, Name: []byte("x"), ID: subDirD}, - } -} diff --git a/object/tree/insert.go b/object/tree/insert.go deleted file mode 100644 index 22bda74f..00000000 --- a/object/tree/insert.go +++ /dev/null @@ -1,24 +0,0 @@ -package tree - -import ( - "fmt" - "slices" -) - -// InsertEntry inserts a tree entry while preserving Git ordering. -// -// InsertEntry copies newEntry.Name. -func (tree *Tree) InsertEntry(newEntry TreeEntry) error { - if tree.entry(newEntry.Name, true) != nil || tree.entry(newEntry.Name, false) != nil { - return fmt.Errorf("object: tree: entry %q already exists", newEntry.Name) - } - - newEntry.Name = append([]byte(nil), newEntry.Name...) - - insertAt, _ := slices.BinarySearchFunc(tree.Entries, newEntry.Name, func(entry TreeEntry, name []byte) int { - return TreeEntryNameCompare(entry.Name, entry.Mode, name, newEntry.Mode == FileModeDir) - }) - tree.Entries = slices.Insert(tree.Entries, insertAt, newEntry) - - return nil -} diff --git a/object/tree/lookup.go b/object/tree/lookup.go deleted file mode 100644 index 249efd0f..00000000 --- a/object/tree/lookup.go +++ /dev/null @@ -1,18 +0,0 @@ -package tree - -// Entry looks up a tree entry by name. -// -// The returned pointer refers to storage within tree.Entries and must not be -// retained across InsertEntry or RemoveEntry calls. -func (tree *Tree) Entry(name []byte) *TreeEntry { - if len(tree.Entries) == 0 { - return nil - } - - index, ok := tree.entryIndex(name) - if !ok { - return nil - } - - return &tree.Entries[index] -} diff --git a/object/tree/mode.go b/object/tree/mode.go deleted file mode 100644 index b1cbc6bc..00000000 --- a/object/tree/mode.go +++ /dev/null @@ -1,12 +0,0 @@ -package tree - -// FileMode represents the mode of a file in a Git tree. -type FileMode uint32 - -const ( - FileModeDir FileMode = 0o40000 - FileModeRegular FileMode = 0o100644 - FileModeExecutable FileMode = 0o100755 - FileModeSymlink FileMode = 0o120000 - FileModeGitlink FileMode = 0o160000 -) diff --git a/object/tree/mode_details.go b/object/tree/mode_details.go deleted file mode 100644 index 9c34fd7c..00000000 --- a/object/tree/mode_details.go +++ /dev/null @@ -1,10 +0,0 @@ -package tree - -type fileModeDetails struct { - isBlobLike bool - isRegularFile bool -} - -func (mode FileMode) details() fileModeDetails { - return fileModeTable[mode] -} diff --git a/object/tree/mode_has_same_type.go b/object/tree/mode_has_same_type.go deleted file mode 100644 index a058cb9c..00000000 --- a/object/tree/mode_has_same_type.go +++ /dev/null @@ -1,12 +0,0 @@ -package tree - -// HasSameType reports whether mode and other describe the same tree entry kind. -// -// Regular files and executable files have the same type for diff-status purposes. -func (mode FileMode) HasSameType(other FileMode) bool { - if mode == other { - return true - } - - return mode.details().isRegularFile && other.details().isRegularFile -} diff --git a/object/tree/mode_is_blob_like.go b/object/tree/mode_is_blob_like.go deleted file mode 100644 index 3ec3a308..00000000 --- a/object/tree/mode_is_blob_like.go +++ /dev/null @@ -1,8 +0,0 @@ -package tree - -// IsBlobLike reports whether mode names one blob-like tree entry kind. -// -// Blob-like entries store blob object IDs as their targets. -func (mode FileMode) IsBlobLike() bool { - return mode.details().isBlobLike -} diff --git a/object/tree/mode_is_regular_file.go b/object/tree/mode_is_regular_file.go deleted file mode 100644 index 115395c0..00000000 --- a/object/tree/mode_is_regular_file.go +++ /dev/null @@ -1,6 +0,0 @@ -package tree - -// IsRegularFile reports whether mode names one regular-file variant. -func (mode FileMode) IsRegularFile() bool { - return mode.details().isRegularFile -} diff --git a/object/tree/mode_table.go b/object/tree/mode_table.go deleted file mode 100644 index 1695f270..00000000 --- a/object/tree/mode_table.go +++ /dev/null @@ -1,24 +0,0 @@ -package tree - -var fileModeTable = map[FileMode]fileModeDetails{ //nolint:gochecknoglobals - FileModeDir: { - isBlobLike: false, - isRegularFile: false, - }, - FileModeRegular: { - isBlobLike: true, - isRegularFile: true, - }, - FileModeExecutable: { - isBlobLike: true, - isRegularFile: true, - }, - FileModeSymlink: { - isBlobLike: true, - isRegularFile: false, - }, - FileModeGitlink: { - isBlobLike: false, - isRegularFile: false, - }, -} diff --git a/object/tree/name.go b/object/tree/name.go deleted file mode 100644 index 02af3292..00000000 --- a/object/tree/name.go +++ /dev/null @@ -1,51 +0,0 @@ -package tree - -// TreeEntryNameCompare compares names using Git tree ordering rules. -func TreeEntryNameCompare(entryName []byte, entryMode FileMode, searchName []byte, searchIsTree bool) int { - isEntryTree := entryMode == FileModeDir - - entryLen := len(entryName) - if isEntryTree { - entryLen++ - } - - searchLen := len(searchName) - if searchIsTree { - searchLen++ - } - - n := min(searchLen, entryLen) - - for i := range n { - var ec, sc byte - if i < len(entryName) { - ec = entryName[i] - } else { - ec = '/' - } - - if i < len(searchName) { - sc = searchName[i] - } else { - sc = '/' - } - - if ec < sc { - return -1 - } - - if ec > sc { - return 1 - } - } - - if entryLen < searchLen { - return -1 - } - - if entryLen > searchLen { - return 1 - } - - return 0 -} diff --git a/object/tree/parse.go b/object/tree/parse.go deleted file mode 100644 index bb874828..00000000 --- a/object/tree/parse.go +++ /dev/null @@ -1,58 +0,0 @@ -package tree - -import ( - "bytes" - "fmt" - "strconv" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Parse decodes a tree object body into a fully materialized Tree. -func Parse(body []byte, algo objectid.Algorithm) (*Tree, error) { - var entries []TreeEntry - - i := 0 - for i < len(body) { - space := bytes.IndexByte(body[i:], ' ') - if space < 0 { - return nil, fmt.Errorf("object: tree: missing mode terminator") - } - - modeBytes := body[i : i+space] - i += space + 1 - - nul := bytes.IndexByte(body[i:], 0) - if nul < 0 { - return nil, fmt.Errorf("object: tree: missing name terminator") - } - - nameBytes := body[i : i+nul] - i += nul + 1 - - idEnd := i + algo.Size() - if idEnd > len(body) { - return nil, fmt.Errorf("object: tree: truncated child object id") - } - - id, err := objectid.FromBytes(algo, body[i:idEnd]) - if err != nil { - return nil, err - } - - i = idEnd - - mode, err := strconv.ParseUint(string(modeBytes), 8, 32) - if err != nil { - return nil, fmt.Errorf("object: tree: parse mode: %w", err) - } - - entries = append(entries, TreeEntry{ - Mode: FileMode(mode), - Name: append([]byte(nil), nameBytes...), - ID: id, - }) - } - - return &Tree{Entries: entries}, nil -} diff --git a/object/tree/parse_test.go b/object/tree/parse_test.go deleted file mode 100644 index 2b98ede7..00000000 --- a/object/tree/parse_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package tree_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -func TestTreeParseFromGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - entries := adversarialRootEntries(t, testRepo) - - inserted := &tree.Tree{} - for _, entry := range entries { - err := inserted.InsertEntry(entry) - if err != nil { - t.Fatalf("InsertEntry(%q): %v", entry.Name, err) - } - } - - treeID := testRepo.Mktree(t, buildGitMktreeInput(inserted.Entries)) - - rawBody := testRepo.CatFile(t, "tree", treeID) - - parsed, err := tree.Parse(rawBody, algo) - if err != nil { - t.Fatalf("ParseTree: %v", err) - } - - if len(parsed.Entries) != len(inserted.Entries) { - t.Fatalf("entry count = %d, want %d", len(parsed.Entries), len(inserted.Entries)) - } - - for i := range inserted.Entries { - got := parsed.Entries[i] - - want := inserted.Entries[i] - if got.Mode != want.Mode || got.ID != want.ID || !bytes.Equal(got.Name, want.Name) { - t.Fatalf("entry[%d] mismatch: got (%o,%q,%s) want (%o,%q,%s)", - i, got.Mode, got.Name, got.ID, want.Mode, want.Name, want.ID) - } - } - - lsNames := gitLsTreeNames(testRepo.RunBytes(t, "ls-tree", "--name-only", "-z", treeID.String())) - if len(lsNames) != len(parsed.Entries) { - t.Fatalf("ls-tree names = %d, want %d", len(lsNames), len(parsed.Entries)) - } - - for i := range lsNames { - if !bytes.Equal(lsNames[i], parsed.Entries[i].Name) { - t.Fatalf("ordering mismatch at %d: git=%q parsed=%q", i, lsNames[i], parsed.Entries[i].Name) - } - } - - for _, want := range inserted.Entries { - got := parsed.Entry(want.Name) - - if got == nil { - t.Fatalf("Entry(%q) returned nil", want.Name) - } - - if got.Mode != want.Mode || got.ID != want.ID { - t.Fatalf("Entry(%q) mismatch", want.Name) - } - } - - if parsed.Entry([]byte("does-not-exist")) != nil { - t.Fatalf("Entry on missing name should be nil") - } - }) -} - -func TestTreeInsertEntryCopiesName(t *testing.T) { - t.Parallel() - - var tr tree.Tree - - name := []byte("alpha") - entry := tree.TreeEntry{ - Mode: tree.FileModeRegular, - Name: name, - ID: objectid.ObjectID{}, - } - - err := tr.InsertEntry(entry) - if err != nil { - t.Fatalf("InsertEntry: %v", err) - } - - name[0] = 'b' - - got := tr.Entry([]byte("alpha")) - if got == nil { - t.Fatalf("Entry(alpha) returned nil") - } - - if !bytes.Equal(got.Name, []byte("alpha")) { - t.Fatalf("stored name = %q, want %q", got.Name, []byte("alpha")) - } - - if tr.Entry([]byte("blpha")) != nil { - t.Fatalf("mutating caller name should not affect stored entry") - } -} diff --git a/object/tree/path_append.go b/object/tree/path_append.go deleted file mode 100644 index 609d5279..00000000 --- a/object/tree/path_append.go +++ /dev/null @@ -1,14 +0,0 @@ -package tree - -// AppendPath appends path to dst as one slash-separated byte path. -func AppendPath(dst []byte, path [][]byte) []byte { - for i := range path { - if i > 0 { - dst = append(dst, '/') - } - - dst = append(dst, path[i]...) - } - - return dst -} diff --git a/object/tree/path_clone.go b/object/tree/path_clone.go deleted file mode 100644 index a4668add..00000000 --- a/object/tree/path_clone.go +++ /dev/null @@ -1,16 +0,0 @@ -package tree - -import ( - "bytes" - "slices" -) - -// ClonePath returns one deep copy of path. -func ClonePath(path [][]byte) [][]byte { - cloned := slices.Clone(path) - for i := range cloned { - cloned[i] = bytes.Clone(cloned[i]) - } - - return cloned -} diff --git a/object/tree/path_prefix.go b/object/tree/path_prefix.go deleted file mode 100644 index ed658cee..00000000 --- a/object/tree/path_prefix.go +++ /dev/null @@ -1,19 +0,0 @@ -package tree - -import ( - "bytes" - "slices" -) - -// HasPathPrefix reports whether path begins with prefix as whole components. -func HasPathPrefix(path, prefix [][]byte) bool { - if len(prefix) == 0 { - return true - } - - if len(path) < len(prefix) { - return false - } - - return slices.EqualFunc(path[:len(prefix)], prefix, bytes.Equal) -} diff --git a/object/tree/path_split.go b/object/tree/path_split.go deleted file mode 100644 index c147dd25..00000000 --- a/object/tree/path_split.go +++ /dev/null @@ -1,19 +0,0 @@ -package tree - -import ( - "bytes" -) - -// SplitPath splits one slash-separated tree path into components. -func SplitPath(path []byte) [][]byte { - if len(path) == 0 { - return nil - } - - parts := bytes.Split(path, []byte{'/'}) - for i := range parts { - parts[i] = bytes.Clone(parts[i]) - } - - return parts -} diff --git a/object/tree/remove.go b/object/tree/remove.go deleted file mode 100644 index 94de88da..00000000 --- a/object/tree/remove.go +++ /dev/null @@ -1,22 +0,0 @@ -package tree - -import ( - "fmt" - "slices" -) - -// RemoveEntry removes a tree entry by name. -func (tree *Tree) RemoveEntry(name []byte) error { - if len(tree.Entries) == 0 { - return fmt.Errorf("object: tree: entry %q not found", name) - } - - index, ok := tree.entryIndex(name) - if !ok { - return fmt.Errorf("object: tree: entry %q not found", name) - } - - tree.Entries = slices.Delete(tree.Entries, index, index+1) - - return nil -} diff --git a/object/tree/serialize.go b/object/tree/serialize.go deleted file mode 100644 index 69deacda..00000000 --- a/object/tree/serialize.go +++ /dev/null @@ -1,55 +0,0 @@ -package tree - -import ( - "errors" - "strconv" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// SerializeWithoutHeader renders the raw tree body bytes. -func (tree *Tree) SerializeWithoutHeader() ([]byte, error) { - var bodyLen int - - for _, entry := range tree.Entries { - mode := strconv.FormatUint(uint64(entry.Mode), 8) - bodyLen += len(mode) + 1 + len(entry.Name) + 1 + entry.ID.Algorithm().Size() - } - - body := make([]byte, bodyLen) - pos := 0 - - for _, entry := range tree.Entries { - mode := strconv.FormatUint(uint64(entry.Mode), 8) - pos += copy(body[pos:], mode) - body[pos] = ' ' - pos++ - pos += copy(body[pos:], entry.Name) - body[pos] = 0 - pos++ - id := entry.ID.Bytes() - pos += copy(body[pos:], id) - } - - return body, nil -} - -// SerializeWithHeader renders the raw object (header + body). -func (tree *Tree) SerializeWithHeader() ([]byte, error) { - body, err := tree.SerializeWithoutHeader() - if err != nil { - return nil, err - } - - header, ok := objectheader.Encode(objecttype.TypeTree, int64(len(body))) - if !ok { - return nil, errors.New("object: tree: failed to encode object header") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return raw, nil -} diff --git a/object/tree/serialize_test.go b/object/tree/serialize_test.go deleted file mode 100644 index 9c9a2f1c..00000000 --- a/object/tree/serialize_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package tree_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -func TestTreeSerialize(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - entries := adversarialRootEntries(t, testRepo) - obj := &tree.Tree{} - - for i := len(entries) - 1; i >= 0; i-- { - err := obj.InsertEntry(entries[i]) - if err != nil { - t.Fatalf("InsertEntry(%q): %v", entries[i].Name, err) - } - } - - if len(obj.Entries) < 32 { - t.Fatalf("expected at least 32 entries, got %d", len(obj.Entries)) - } - - dup := obj.Entries[0] - - err := obj.InsertEntry(dup) - if err == nil { - t.Fatalf("duplicate InsertEntry should fail") - } - - removed := obj.Entries[len(obj.Entries)/2] - - err = obj.RemoveEntry(removed.Name) - if err != nil { - t.Fatalf("RemoveEntry(%q): %v", removed.Name, err) - } - - if obj.Entry(removed.Name) != nil { - t.Fatalf("Entry(%q) should be nil after remove", removed.Name) - } - - err = obj.RemoveEntry([]byte("no-such-entry")) - if err == nil { - t.Fatalf("RemoveEntry missing entry should fail") - } - - err = obj.InsertEntry(removed) - if err != nil { - t.Fatalf("re-InsertEntry(%q): %v", removed.Name, err) - } - - if obj.Entry(removed.Name) == nil { - t.Fatalf("Entry(%q) should exist after reinsert", removed.Name) - } - - wantTreeID := testRepo.Mktree(t, buildGitMktreeInput(obj.Entries)) - - rawObj, err := obj.SerializeWithHeader() - if err != nil { - t.Fatalf("SerializeWithHeader: %v", err) - } - - gotTreeID := algo.Sum(rawObj) - if gotTreeID != wantTreeID { - t.Fatalf("tree id mismatch: got %s want %s", gotTreeID, wantTreeID) - } - }) -} diff --git a/object/tree/tree.go b/object/tree/tree.go deleted file mode 100644 index d0c7f4f0..00000000 --- a/object/tree/tree.go +++ /dev/null @@ -1,12 +0,0 @@ -// Package tree provides representations, parsers, and serializers for tree objects. -package tree - -// Tree represents a fully materialized Git tree object. -// -// Labels: MT-Unsafe. -type Tree struct { - // Entries must be sorted by TreeEntryNameCompare. - // Use the Tree methods to preserve ordering and copy semantics rather than - // modifying the slice directly. - Entries []TreeEntry -} diff --git a/object/tree/type.go b/object/tree/type.go deleted file mode 100644 index 416544af..00000000 --- a/object/tree/type.go +++ /dev/null @@ -1,10 +0,0 @@ -package tree - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// ObjectType returns TypeTree. -func (tree *Tree) ObjectType() objecttype.Type { - _ = tree - - return objecttype.TypeTree -} diff --git a/object/type/details.go b/object/type/details.go deleted file mode 100644 index 17bdcfd4..00000000 --- a/object/type/details.go +++ /dev/null @@ -1,10 +0,0 @@ -package objecttype - -type typeDetails struct { - name string - isBaseObject bool -} - -func (ty Type) details() typeDetails { - return typeTable[ty] -} diff --git a/object/type/is_base.go b/object/type/is_base.go deleted file mode 100644 index cdc11f5b..00000000 --- a/object/type/is_base.go +++ /dev/null @@ -1,7 +0,0 @@ -package objecttype - -// IsBaseObject reports whether ty is one of the four canonical Git object -// types encoded directly in pack entries. -func (ty Type) IsBaseObject() bool { - return ty.details().isBaseObject -} diff --git a/object/type/name.go b/object/type/name.go deleted file mode 100644 index c95fe90b..00000000 --- a/object/type/name.go +++ /dev/null @@ -1,11 +0,0 @@ -package objecttype - -// Name returns the canonical Git object type name. -func (ty Type) Name() (string, bool) { - details := ty.details() - if details.name == "" { - return "", false - } - - return details.name, true -} diff --git a/object/type/parse.go b/object/type/parse.go deleted file mode 100644 index bc5ca736..00000000 --- a/object/type/parse.go +++ /dev/null @@ -1,8 +0,0 @@ -package objecttype - -// Parse parses a canonical Git object type name. -func Parse(name string) (Type, bool) { - ty, ok := typeByName[name] - - return ty, ok -} diff --git a/object/type/table.go b/object/type/table.go deleted file mode 100644 index 19cc760d..00000000 --- a/object/type/table.go +++ /dev/null @@ -1,21 +0,0 @@ -package objecttype - -//nolint:gochecknoglobals -var typeTable = [...]typeDetails{ - TypeInvalid: {}, - TypeCommit: {name: "commit", isBaseObject: true}, - TypeTree: {name: "tree", isBaseObject: true}, - TypeBlob: {name: "blob", isBaseObject: true}, - TypeTag: {name: "tag", isBaseObject: true}, - TypeFuture: {}, - TypeOfsDelta: {}, - TypeRefDelta: {}, -} - -//nolint:gochecknoglobals -var typeByName = map[string]Type{ - typeTable[TypeCommit].name: TypeCommit, - typeTable[TypeTree].name: TypeTree, - typeTable[TypeBlob].name: TypeBlob, - typeTable[TypeTag].name: TypeTag, -} diff --git a/object/type/type.go b/object/type/type.go deleted file mode 100644 index 18e0ac35..00000000 --- a/object/type/type.go +++ /dev/null @@ -1,16 +0,0 @@ -// Package objecttype provides Git object type tags and names. -package objecttype - -// Type mirrors Git object type tags in packfiles. -type Type uint8 - -const ( - TypeInvalid Type = 0 - TypeCommit Type = 1 - TypeTree Type = 2 - TypeBlob Type = 3 - TypeTag Type = 4 - TypeFuture Type = 5 - TypeOfsDelta Type = 6 - TypeRefDelta Type = 7 -) |
