Fast implementation of Git in pure Go codeberg.org/lindenii/furgit
git go
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

object/store: Rename from object/storer

Runxi Yu d7e90798 80d62c00

+63 -4254
+2 -2
cmd/index-pack/main.go
··· 10 10 11 11 "codeberg.org/lindenii/furgit/format/packfile/ingest" 12 12 objectid "codeberg.org/lindenii/furgit/object/id" 13 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 13 + objectstore "codeberg.org/lindenii/furgit/object/store" 14 14 "codeberg.org/lindenii/furgit/repository" 15 15 ) 16 16 ··· 36 36 func run(repoPath, destinationPath, objectFormat string, fixThin, writeRev bool) error { 37 37 var ( 38 38 algo objectid.Algorithm 39 - base objectstorer.Store 39 + base objectstore.Store 40 40 repo *repository.Repository 41 41 ) 42 42
+1 -1
commitquery/ancestor_unit_test.go
··· 8 8 giterrors "codeberg.org/lindenii/furgit/errors" 9 9 "codeberg.org/lindenii/furgit/internal/testgit" 10 10 objectid "codeberg.org/lindenii/furgit/object/id" 11 - "codeberg.org/lindenii/furgit/object/storer/memory" 11 + "codeberg.org/lindenii/furgit/object/store/memory" 12 12 objecttree "codeberg.org/lindenii/furgit/object/tree" 13 13 objecttype "codeberg.org/lindenii/furgit/object/type" 14 14
+3 -3
commitquery/context.go
··· 4 4 import ( 5 5 commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 6 6 objectid "codeberg.org/lindenii/furgit/object/id" 7 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 7 + objectstore "codeberg.org/lindenii/furgit/object/store" 8 8 ) 9 9 10 10 // Query owns the mutable node arena for commit-domain queries over one object 11 11 // store. 12 12 type Query struct { 13 - store objectstorer.Store 13 + store objectstore.Store 14 14 graph *commitgraphread.Reader 15 15 16 16 nodes []node ··· 24 24 25 25 // New builds one reusable commit query arena over one object store and optional 26 26 // commit-graph reader. 27 - func New(store objectstorer.Store, graph *commitgraphread.Reader) *Query { 27 + func New(store objectstore.Store, graph *commitgraphread.Reader) *Query { 28 28 return &Query{ 29 29 store: store, 30 30 graph: graph,
+1 -1
commitquery/mergebase_unit_test.go
··· 11 11 giterrors "codeberg.org/lindenii/furgit/errors" 12 12 "codeberg.org/lindenii/furgit/internal/testgit" 13 13 objectid "codeberg.org/lindenii/furgit/object/id" 14 - "codeberg.org/lindenii/furgit/object/storer/memory" 14 + "codeberg.org/lindenii/furgit/object/store/memory" 15 15 "codeberg.org/lindenii/furgit/object/tree" 16 16 objecttype "codeberg.org/lindenii/furgit/object/type" 17 17 )
+2 -2
commitquery/oid.go
··· 8 8 "codeberg.org/lindenii/furgit/internal/peel" 9 9 objectcommit "codeberg.org/lindenii/furgit/object/commit" 10 10 objectid "codeberg.org/lindenii/furgit/object/id" 11 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 11 + objectstore "codeberg.org/lindenii/furgit/object/store" 12 12 objecttype "codeberg.org/lindenii/furgit/object/type" 13 13 ) 14 14 ··· 70 70 71 71 ty, content, err := query.store.ReadBytesContent(id) 72 72 if err != nil { 73 - if stderrors.Is(err, objectstorer.ErrObjectNotFound) { 73 + if stderrors.Is(err, objectstore.ErrObjectNotFound) { 74 74 return &giterrors.ObjectMissingError{OID: id} 75 75 } 76 76
+1 -1
diff/trees/diff_test.go
··· 7 7 "codeberg.org/lindenii/furgit/diff/trees" 8 8 "codeberg.org/lindenii/furgit/internal/testgit" 9 9 objectid "codeberg.org/lindenii/furgit/object/id" 10 - "codeberg.org/lindenii/furgit/object/storer/loose" 10 + "codeberg.org/lindenii/furgit/object/store/loose" 11 11 "codeberg.org/lindenii/furgit/object/tree" 12 12 objecttype "codeberg.org/lindenii/furgit/object/type" 13 13 )
+2 -2
format/packfile/ingest/api.go
··· 8 8 "os" 9 9 10 10 objectid "codeberg.org/lindenii/furgit/object/id" 11 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 11 + objectstore "codeberg.org/lindenii/furgit/object/store" 12 12 ) 13 13 14 14 // Options controls one pack ingest operation. ··· 18 18 // WriteRev writes a .rev alongside the .pack and .idx. 19 19 WriteRev bool 20 20 // Base supplies existing objects for thin-pack fixup. 21 - Base objectstorer.Store 21 + Base objectstore.Store 22 22 // Progress receives human-readable progress messages. 23 23 // 24 24 // When nil, no progress output is emitted.
+2 -2
format/packfile/ingest/thin_fix.go
··· 6 6 7 7 "codeberg.org/lindenii/furgit/internal/intconv" 8 8 "codeberg.org/lindenii/furgit/internal/progress" 9 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 9 + objectstore "codeberg.org/lindenii/furgit/object/store" 10 10 ) 11 11 12 12 // maybeFixThin appends missing bases and rewrites pack header/trailer when needed. ··· 71 71 for _, id := range baseIDs { 72 72 ty, content, err := state.opts.Base.ReadBytesContent(id) 73 73 if err != nil { 74 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 74 + if errors.Is(err, objectstore.ErrObjectNotFound) { 75 75 continue 76 76 } 77 77
+4 -4
internal/peel/peel.go
··· 6 6 7 7 giterrors "codeberg.org/lindenii/furgit/errors" 8 8 objectid "codeberg.org/lindenii/furgit/object/id" 9 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 9 + objectstore "codeberg.org/lindenii/furgit/object/store" 10 10 "codeberg.org/lindenii/furgit/object/tag" 11 11 objecttype "codeberg.org/lindenii/furgit/object/type" 12 12 ) 13 13 14 14 // ToCommit peels annotated tags transitively until a commit is reached. 15 - func ToCommit(store objectstorer.Store, id objectid.ObjectID) (objectid.ObjectID, error) { 15 + func ToCommit(store objectstore.Store, id objectid.ObjectID) (objectid.ObjectID, error) { 16 16 for { 17 17 ty, _, err := store.ReadHeader(id) 18 18 if err != nil { 19 - if stderrors.Is(err, objectstorer.ErrObjectNotFound) { 19 + if stderrors.Is(err, objectstore.ErrObjectNotFound) { 20 20 return objectid.ObjectID{}, &giterrors.ObjectMissingError{OID: id} 21 21 } 22 22 ··· 33 33 34 34 _, content, err := store.ReadBytesContent(id) 35 35 if err != nil { 36 - if stderrors.Is(err, objectstorer.ErrObjectNotFound) { 36 + if stderrors.Is(err, objectstore.ErrObjectNotFound) { 37 37 return objectid.ObjectID{}, &giterrors.ObjectMissingError{OID: id} 38 38 } 39 39
+2 -2
internal/testgit/repo_open_object_store.go
··· 3 3 import ( 4 4 "testing" 5 5 6 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 6 + objectstore "codeberg.org/lindenii/furgit/object/store" 7 7 "codeberg.org/lindenii/furgit/repository" 8 8 ) 9 9 ··· 11 11 // the caller. 12 12 // 13 13 //nolint:ireturn 14 - func (testRepo *TestRepo) OpenObjectStore(tb testing.TB) objectstorer.Store { 14 + func (testRepo *TestRepo) OpenObjectStore(tb testing.TB) objectstore.Store { 15 15 tb.Helper() 16 16 17 17 root := testRepo.OpenGitRoot(tb)
+3 -3
network/receivepack/hook.go
··· 6 6 7 7 "codeberg.org/lindenii/furgit/network/receivepack/service" 8 8 objectid "codeberg.org/lindenii/furgit/object/id" 9 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 9 + objectstore "codeberg.org/lindenii/furgit/object/store" 10 10 refstore "codeberg.org/lindenii/furgit/ref/store" 11 11 ) 12 12 ··· 35 35 // valid for the duration of the hook call. 36 36 type HookRequest struct { 37 37 Refs refstore.ReadingStore 38 - ExistingObjects objectstorer.Store 39 - QuarantinedObjects objectstorer.Store 38 + ExistingObjects objectstore.Store 39 + QuarantinedObjects objectstore.Store 40 40 Updates []RefUpdate 41 41 PushOptions []string 42 42 IO HookIO
+1 -1
network/receivepack/hooks/reject_force_push.go
··· 8 8 "codeberg.org/lindenii/furgit/commitquery" 9 9 receivepack "codeberg.org/lindenii/furgit/network/receivepack" 10 10 objectid "codeberg.org/lindenii/furgit/object/id" 11 - objectmix "codeberg.org/lindenii/furgit/object/storer/mix" 11 + objectmix "codeberg.org/lindenii/furgit/object/store/mix" 12 12 refstore "codeberg.org/lindenii/furgit/ref/store" 13 13 ) 14 14
+2 -2
network/receivepack/options.go
··· 4 4 "os" 5 5 6 6 objectid "codeberg.org/lindenii/furgit/object/id" 7 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 7 + objectstore "codeberg.org/lindenii/furgit/object/store" 8 8 refstore "codeberg.org/lindenii/furgit/ref/store" 9 9 ) 10 10 ··· 25 25 Refs refstore.ReadWriteStore 26 26 // ExistingObjects is the object store visible to the push before any newly 27 27 // uploaded quarantined objects are promoted. 28 - ExistingObjects objectstorer.Store 28 + ExistingObjects objectstore.Store 29 29 // ObjectsRoot is the permanent object storage root beneath which per-push 30 30 // quarantine directories are derived. 31 31 ObjectsRoot *os.Root
+3 -3
network/receivepack/service/hook.go
··· 5 5 "io" 6 6 7 7 objectid "codeberg.org/lindenii/furgit/object/id" 8 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 8 + objectstore "codeberg.org/lindenii/furgit/object/store" 9 9 refstore "codeberg.org/lindenii/furgit/ref/store" 10 10 ) 11 11 ··· 31 31 // valid for the duration of the hook call. 32 32 type HookRequest struct { 33 33 Refs refstore.ReadingStore 34 - ExistingObjects objectstorer.Store 35 - QuarantinedObjects objectstorer.Store 34 + ExistingObjects objectstore.Store 35 + QuarantinedObjects objectstore.Store 36 36 Updates []RefUpdate 37 37 PushOptions []string 38 38 IO HookIO
+2 -2
network/receivepack/service/options.go
··· 6 6 "os" 7 7 8 8 objectid "codeberg.org/lindenii/furgit/object/id" 9 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 9 + objectstore "codeberg.org/lindenii/furgit/object/store" 10 10 refstore "codeberg.org/lindenii/furgit/ref/store" 11 11 ) 12 12 ··· 26 26 type Options struct { 27 27 Algorithm objectid.Algorithm 28 28 Refs refstore.ReadWriteStore 29 - ExistingObjects objectstorer.Store 29 + ExistingObjects objectstore.Store 30 30 ObjectsRoot *os.Root 31 31 Progress io.Writer 32 32 ProgressFlush func() error
+1 -1
network/receivepack/service/quarantine_test.go
··· 8 8 "testing" 9 9 10 10 objectid "codeberg.org/lindenii/furgit/object/id" 11 - "codeberg.org/lindenii/furgit/object/storer/memory" 11 + "codeberg.org/lindenii/furgit/object/store/memory" 12 12 ) 13 13 14 14 type quarantineFixture struct {
+5 -5
network/receivepack/service/run_hook.go
··· 5 5 "os" 6 6 7 7 "codeberg.org/lindenii/furgit/internal/utils" 8 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 9 - "codeberg.org/lindenii/furgit/object/storer/loose" 10 - objectmix "codeberg.org/lindenii/furgit/object/storer/mix" 11 - "codeberg.org/lindenii/furgit/object/storer/packed" 8 + objectstore "codeberg.org/lindenii/furgit/object/store" 9 + "codeberg.org/lindenii/furgit/object/store/loose" 10 + objectmix "codeberg.org/lindenii/furgit/object/store/mix" 11 + "codeberg.org/lindenii/furgit/object/store/packed" 12 12 ) 13 13 14 14 func (service *Service) runHook( ··· 40 40 quarantinedObjects := service.opts.ExistingObjects 41 41 42 42 var ( 43 - quarantineObjectsStore objectstorer.Store 43 + quarantineObjectsStore objectstore.Store 44 44 quarantineLooseStore *loose.Store 45 45 quarantinePackedStore *packed.Store 46 46 quarantineLooseRoot *os.Root
+1 -1
network/receivepack/service/service_test.go
··· 10 10 "codeberg.org/lindenii/furgit/internal/testgit" 11 11 "codeberg.org/lindenii/furgit/network/receivepack/service" 12 12 objectid "codeberg.org/lindenii/furgit/object/id" 13 - "codeberg.org/lindenii/furgit/object/storer/memory" 13 + "codeberg.org/lindenii/furgit/object/store/memory" 14 14 ) 15 15 16 16 func TestExecutePackExpectedWithoutObjectsRoot(t *testing.T) {
+1 -1
object/blob/blob.go
··· 4 4 // Blob represents a Git blob object. 5 5 // 6 6 // This Blob object is fully materialized in memory. 7 - // Consider using objectstorer/Store.ReadReaderContent, 7 + // Consider using objectstore/Store.ReadReaderContent, 8 8 // or appropriate streaming write APIs. 9 9 type Blob struct { 10 10 Data []byte
+3 -3
object/fetch/fetcher.go
··· 1 1 package fetch 2 2 3 - import objectstorer "codeberg.org/lindenii/furgit/object/storer" 3 + import objectstore "codeberg.org/lindenii/furgit/object/store" 4 4 5 5 // Fetcher resolves parsed and streamed objects from an object store. 6 6 // 7 7 // A Fetcher does not take ownership of the store and does not close it. 8 8 type Fetcher struct { 9 - store objectstorer.Store 9 + store objectstore.Store 10 10 } 11 11 12 12 // New returns a Fetcher that reads objects from store. 13 13 // 14 14 // The returned Fetcher does not take ownership of store. 15 - func New(store objectstorer.Store) *Fetcher { 15 + func New(store objectstore.Store) *Fetcher { 16 16 return &Fetcher{store: store} 17 17 }
-46
object/storer/chain/bytes.go
··· 1 - package chain 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - 7 - objectid "codeberg.org/lindenii/furgit/object/id" 8 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 9 - objecttype "codeberg.org/lindenii/furgit/object/type" 10 - ) 11 - 12 - // ReadBytesFull reads a full serialized object from the first backend that has it. 13 - func (chain *Chain) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { 14 - for i, backend := range chain.backends { 15 - full, err := backend.ReadBytesFull(id) 16 - if err == nil { 17 - return full, nil 18 - } 19 - 20 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 21 - continue 22 - } 23 - 24 - return nil, fmt.Errorf("objectstorer: backend %d read bytes full: %w", i, err) 25 - } 26 - 27 - return nil, objectstorer.ErrObjectNotFound 28 - } 29 - 30 - // ReadBytesContent reads an object's type and content bytes from the first backend that has it. 31 - func (chain *Chain) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { 32 - for i, backend := range chain.backends { 33 - ty, content, err := backend.ReadBytesContent(id) 34 - if err == nil { 35 - return ty, content, nil 36 - } 37 - 38 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 39 - continue 40 - } 41 - 42 - return objecttype.TypeInvalid, nil, fmt.Errorf("objectstorer: backend %d read bytes content: %w", i, err) 43 - } 44 - 45 - return objecttype.TypeInvalid, nil, objectstorer.ErrObjectNotFound 46 - }
-12
object/storer/chain/chain.go
··· 1 - // Package chain provides a wrapper object storage backend to query a chain of 2 - // backends. 3 - package chain 4 - 5 - import objectstorer "codeberg.org/lindenii/furgit/object/storer" 6 - 7 - // Chain queries multiple object databases in order. 8 - // 9 - // Chain borrows its backend stores. 10 - type Chain struct { 11 - backends []objectstorer.Store 12 - }
-8
object/storer/chain/close.go
··· 1 - package chain 2 - 3 - // Close releases wrapper-local resources. 4 - // 5 - // Chain borrows its backends, so Close does not close them. 6 - // 7 - // Repeated calls to Close are undefined behavior. 8 - func (chain *Chain) Close() error { return nil }
-28
object/storer/chain/header.go
··· 1 - package chain 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - 7 - objectid "codeberg.org/lindenii/furgit/object/id" 8 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 9 - objecttype "codeberg.org/lindenii/furgit/object/type" 10 - ) 11 - 12 - // ReadHeader reads object header data from the first backend that has it. 13 - func (chain *Chain) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { 14 - for i, backend := range chain.backends { 15 - ty, size, err := backend.ReadHeader(id) 16 - if err == nil { 17 - return ty, size, nil 18 - } 19 - 20 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 21 - continue 22 - } 23 - 24 - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstorer: backend %d read header: %w", i, err) 25 - } 26 - 27 - return objecttype.TypeInvalid, 0, objectstorer.ErrObjectNotFound 28 - }
-13
object/storer/chain/new.go
··· 1 - package chain 2 - 3 - import objectstorer "codeberg.org/lindenii/furgit/object/storer" 4 - 5 - // New creates an ordered object database chain. 6 - // 7 - // The provided backends must be non-nil and distinct. 8 - // Chain borrows the provided backends and does not close them in Close. 9 - func New(backends ...objectstorer.Store) *Chain { 10 - return &Chain{ 11 - backends: append([]objectstorer.Store(nil), backends...), 12 - } 13 - }
-47
object/storer/chain/reader.go
··· 1 - package chain 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - "io" 7 - 8 - objectid "codeberg.org/lindenii/furgit/object/id" 9 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 10 - objecttype "codeberg.org/lindenii/furgit/object/type" 11 - ) 12 - 13 - // ReadReaderFull reads a full serialized object stream from the first backend that has it. 14 - func (chain *Chain) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { 15 - for i, backend := range chain.backends { 16 - reader, err := backend.ReadReaderFull(id) 17 - if err == nil { 18 - return reader, nil 19 - } 20 - 21 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 22 - continue 23 - } 24 - 25 - return nil, fmt.Errorf("objectstorer: backend %d read reader full: %w", i, err) 26 - } 27 - 28 - return nil, objectstorer.ErrObjectNotFound 29 - } 30 - 31 - // ReadReaderContent reads an object's type, declared content length, and content stream from the first backend that has it. 32 - func (chain *Chain) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { 33 - for i, backend := range chain.backends { 34 - ty, size, reader, err := backend.ReadReaderContent(id) 35 - if err == nil { 36 - return ty, size, reader, nil 37 - } 38 - 39 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 40 - continue 41 - } 42 - 43 - return objecttype.TypeInvalid, 0, nil, fmt.Errorf("objectstorer: backend %d read reader content: %w", i, err) 44 - } 45 - 46 - return objecttype.TypeInvalid, 0, nil, objectstorer.ErrObjectNotFound 47 - }
-17
object/storer/chain/refresh.go
··· 1 - package chain 2 - 3 - import "errors" 4 - 5 - // Refresh forwards refresh calls to all backends. 6 - func (chain *Chain) Refresh() error { 7 - var errs []error 8 - 9 - for _, backend := range chain.backends { 10 - err := backend.Refresh() 11 - if err != nil { 12 - errs = append(errs, err) 13 - } 14 - } 15 - 16 - return errors.Join(errs...) 17 - }
-27
object/storer/chain/size.go
··· 1 - package chain 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - 7 - objectid "codeberg.org/lindenii/furgit/object/id" 8 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 9 - ) 10 - 11 - // ReadSize reads object content length from the first backend that has it. 12 - func (chain *Chain) ReadSize(id objectid.ObjectID) (int64, error) { 13 - for i, backend := range chain.backends { 14 - size, err := backend.ReadSize(id) 15 - if err == nil { 16 - return size, nil 17 - } 18 - 19 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 20 - continue 21 - } 22 - 23 - return 0, fmt.Errorf("objectstorer: backend %d read size: %w", i, err) 24 - } 25 - 26 - return 0, objectstorer.ErrObjectNotFound 27 - }
-107
object/storer/loose/helpers_test.go
··· 1 - package loose_test 2 - 3 - import ( 4 - "io" 5 - "os" 6 - "testing" 7 - 8 - "codeberg.org/lindenii/furgit/internal/testgit" 9 - objectheader "codeberg.org/lindenii/furgit/object/header" 10 - objectid "codeberg.org/lindenii/furgit/object/id" 11 - "codeberg.org/lindenii/furgit/object/storer/loose" 12 - objecttype "codeberg.org/lindenii/furgit/object/type" 13 - ) 14 - 15 - func openLooseStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *loose.Store { 16 - t.Helper() 17 - 18 - root := testRepo.OpenObjectsRoot(t) 19 - 20 - store, err := loose.New(root, algo) 21 - if err != nil { 22 - t.Fatalf("loose.New: %v", err) 23 - } 24 - 25 - return store 26 - } 27 - 28 - func mustReadAllAndClose(t *testing.T, reader io.ReadCloser) []byte { 29 - t.Helper() 30 - 31 - data, err := io.ReadAll(reader) 32 - if err != nil { 33 - _ = reader.Close() 34 - 35 - t.Fatalf("ReadAll: %v", err) 36 - } 37 - 38 - err = reader.Close() 39 - if err != nil { 40 - t.Fatalf("Close: %v", err) 41 - } 42 - 43 - return data 44 - } 45 - 46 - func expectedRawObject(t *testing.T, testRepo *testgit.TestRepo, id objectid.ObjectID) (objecttype.Type, []byte, []byte) { 47 - t.Helper() 48 - 49 - typeName := testRepo.Run(t, "cat-file", "-t", id.String()) 50 - 51 - ty, ok := objecttype.ParseName(typeName) 52 - if !ok { 53 - t.Fatalf("ParseName(%q) failed", typeName) 54 - } 55 - 56 - body := testRepo.CatFile(t, typeName, id) 57 - 58 - header, ok := objectheader.Encode(ty, int64(len(body))) 59 - if !ok { 60 - t.Fatalf("objectheader.Encode failed") 61 - } 62 - 63 - raw := make([]byte, len(header)+len(body)) 64 - copy(raw, header) 65 - copy(raw[len(header):], body) 66 - 67 - return ty, body, raw 68 - } 69 - 70 - func corruptLooseObjectTrailer(t *testing.T, testRepo *testgit.TestRepo, id objectid.ObjectID) { 71 - t.Helper() 72 - 73 - root := testRepo.OpenObjectsRoot(t) 74 - 75 - hex := id.String() 76 - relPath := hex[:2] + "/" + hex[2:] 77 - 78 - file, err := root.OpenFile(relPath, os.O_RDWR, 0) 79 - if err != nil { 80 - t.Fatalf("OpenFile(%q): %v", relPath, err) 81 - } 82 - 83 - defer func() { _ = file.Close() }() 84 - 85 - info, err := file.Stat() 86 - if err != nil { 87 - t.Fatalf("Stat(%q): %v", relPath, err) 88 - } 89 - 90 - if info.Size() == 0 { 91 - t.Fatalf("corrupt trailer on empty file %q", relPath) 92 - } 93 - 94 - last := make([]byte, 1) 95 - 96 - _, err = file.ReadAt(last, info.Size()-1) 97 - if err != nil { 98 - t.Fatalf("ReadAt(%q): %v", relPath, err) 99 - } 100 - 101 - last[0] ^= 0xff 102 - 103 - _, err = file.WriteAt(last, info.Size()-1) 104 - if err != nil { 105 - t.Fatalf("WriteAt(%q): %v", relPath, err) 106 - } 107 - }
-55
object/storer/loose/parse.go
··· 1 - package loose 2 - 3 - import ( 4 - "bufio" 5 - "errors" 6 - "io" 7 - "os" 8 - 9 - "codeberg.org/lindenii/furgit/internal/compress/zlib" 10 - objectheader "codeberg.org/lindenii/furgit/object/header" 11 - objecttype "codeberg.org/lindenii/furgit/object/type" 12 - ) 13 - 14 - // decodeAll inflates the full loose object payload from file. 15 - func decodeAll(file *os.File) ([]byte, error) { 16 - zr, err := zlib.NewReader(file) 17 - if err != nil { 18 - return nil, err 19 - } 20 - 21 - defer func() { _ = zr.Close() }() 22 - 23 - return io.ReadAll(zr) 24 - } 25 - 26 - // parseRaw parses a loose object payload in "type size\0content" format. 27 - func parseRaw(raw []byte) (objecttype.Type, []byte, error) { 28 - ty, size, headerLen, ok := objectheader.Parse(raw) 29 - if !ok { 30 - return objecttype.TypeInvalid, nil, errors.New("objectstorer/loose: malformed object header") 31 - } 32 - 33 - content := raw[headerLen:] 34 - if int64(len(content)) != size { 35 - return objecttype.TypeInvalid, nil, errors.New("objectstorer/loose: object header size/content mismatch") 36 - } 37 - 38 - return ty, content, nil 39 - } 40 - 41 - // readHeader reads and parses a loose object header from br, and returns 42 - // the raw header bytes including the trailing NUL. 43 - func readHeader(br *bufio.Reader) ([]byte, objecttype.Type, int64, error) { 44 - header, err := br.ReadSlice(0) 45 - if err != nil { 46 - return nil, objecttype.TypeInvalid, 0, err 47 - } 48 - 49 - ty, size, _, ok := objectheader.Parse(header) 50 - if !ok { 51 - return nil, objecttype.TypeInvalid, 0, errors.New("objectstorer/loose: malformed object header") 52 - } 53 - 54 - return header, ty, size, nil 55 - }
-43
object/storer/loose/paths.go
··· 1 - package loose 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - "io/fs" 7 - "os" 8 - "path/filepath" 9 - 10 - objectid "codeberg.org/lindenii/furgit/object/id" 11 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 12 - ) 13 - 14 - // objectPath returns the loose object path for id relative to the objects root. 15 - func (store *Store) objectPath(id objectid.ObjectID) (string, error) { 16 - if id.Algorithm() != store.algo { 17 - return "", fmt.Errorf("objectstorer/loose: object id algorithm mismatch: got %s want %s", id.Algorithm(), store.algo) 18 - } 19 - 20 - hex := id.String() 21 - 22 - return filepath.Join(hex[:2], hex[2:]), nil 23 - } 24 - 25 - // openObject opens the loose object file for id. 26 - // Missing files cause objectstorer.ErrObjectNotFound. 27 - func (store *Store) openObject(id objectid.ObjectID) (*os.File, error) { 28 - relPath, err := store.objectPath(id) 29 - if err != nil { 30 - return nil, err 31 - } 32 - 33 - file, err := store.root.Open(relPath) 34 - if err != nil { 35 - if errors.Is(err, fs.ErrNotExist) { 36 - return nil, objectstorer.ErrObjectNotFound 37 - } 38 - 39 - return nil, err 40 - } 41 - 42 - return file, nil 43 - }
-49
object/storer/loose/read_bytes.go
··· 1 - package loose 2 - 3 - import ( 4 - objectid "codeberg.org/lindenii/furgit/object/id" 5 - objecttype "codeberg.org/lindenii/furgit/object/type" 6 - ) 7 - 8 - // readBytesParsed reads, inflates, and parses a loose object in one pass. 9 - // It returns the full raw payload and its parsed type and content. 10 - func (store *Store) readBytesParsed(id objectid.ObjectID) ([]byte, objecttype.Type, []byte, error) { 11 - file, err := store.openObject(id) 12 - if err != nil { 13 - return nil, objecttype.TypeInvalid, nil, err 14 - } 15 - 16 - defer func() { _ = file.Close() }() 17 - 18 - raw, err := decodeAll(file) 19 - if err != nil { 20 - return nil, objecttype.TypeInvalid, nil, err 21 - } 22 - 23 - ty, content, err := parseRaw(raw) 24 - if err != nil { 25 - return nil, objecttype.TypeInvalid, nil, err 26 - } 27 - 28 - return raw, ty, content, nil 29 - } 30 - 31 - // ReadBytesFull reads a full serialized object as "type size\0content". 32 - func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { 33 - raw, _, _, err := store.readBytesParsed(id) 34 - if err != nil { 35 - return nil, err 36 - } 37 - 38 - return raw, nil 39 - } 40 - 41 - // ReadBytesContent reads an object's type and content bytes. 42 - func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { 43 - _, ty, content, err := store.readBytesParsed(id) 44 - if err != nil { 45 - return objecttype.TypeInvalid, nil, err 46 - } 47 - 48 - return ty, content, nil 49 - }
-37
object/storer/loose/read_header.go
··· 1 - package loose 2 - 3 - import ( 4 - "bufio" 5 - 6 - "codeberg.org/lindenii/furgit/internal/compress/zlib" 7 - objectid "codeberg.org/lindenii/furgit/object/id" 8 - objecttype "codeberg.org/lindenii/furgit/object/type" 9 - ) 10 - 11 - // ReadHeader reads an object's type and declared content length. 12 - // 13 - // It parses only enough of the zlib-decoded object to recover the object 14 - // header. It does not verify that the remaining object content is readable and 15 - // does not verify the zlib Adler-32 trailer. 16 - func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { 17 - file, err := store.openObject(id) 18 - if err != nil { 19 - return objecttype.TypeInvalid, 0, err 20 - } 21 - 22 - defer func() { _ = file.Close() }() 23 - 24 - zr, err := zlib.NewReader(file) 25 - if err != nil { 26 - return objecttype.TypeInvalid, 0, err 27 - } 28 - 29 - defer func() { _ = zr.Close() }() 30 - 31 - _, ty, size, err := readHeader(bufio.NewReader(zr)) 32 - if err != nil { 33 - return objecttype.TypeInvalid, 0, err 34 - } 35 - 36 - return ty, size, nil 37 - }
-118
object/storer/loose/read_reader.go
··· 1 - package loose 2 - 3 - import ( 4 - "bufio" 5 - "bytes" 6 - "errors" 7 - "io" 8 - "os" 9 - 10 - "codeberg.org/lindenii/furgit/internal/compress/zlib" 11 - "codeberg.org/lindenii/furgit/internal/iolimit" 12 - objectid "codeberg.org/lindenii/furgit/object/id" 13 - objecttype "codeberg.org/lindenii/furgit/object/type" 14 - ) 15 - 16 - type objectReader struct { 17 - // reader is the stream exposed by Read. 18 - reader io.Reader 19 - // file is the underlying loose object file and is closed by Close. 20 - file *os.File 21 - // zr is the zlib decoder and is closed by Close. 22 - zr io.ReadCloser 23 - } 24 - 25 - func (reader *objectReader) Read(dst []byte) (int, error) { 26 - return reader.reader.Read(dst) 27 - } 28 - 29 - func (reader *objectReader) Close() error { 30 - errZlib := reader.zr.Close() 31 - errFile := reader.file.Close() 32 - 33 - return errors.Join(errZlib, errFile) 34 - } 35 - 36 - // openInflated opens and zlib-decodes a loose object file. 37 - // The caller owns both returned closers and must close them. 38 - func (store *Store) openInflated(id objectid.ObjectID) (*os.File, io.ReadCloser, error) { 39 - file, err := store.openObject(id) 40 - if err != nil { 41 - return nil, nil, err 42 - } 43 - 44 - zr, err := zlib.NewReader(file) 45 - if err != nil { 46 - _ = file.Close() 47 - 48 - return nil, nil, err 49 - } 50 - 51 - return file, zr, nil 52 - } 53 - 54 - // ReadReaderFull reads a full serialized object stream as "type size\0content". 55 - // 56 - // The caller must close the returned reader. 57 - // 58 - // Close releases resources only. It does not drain unread data for additional 59 - // validation. In particular, malformed trailing compressed data, trailing bytes 60 - // past the declared object size, and the zlib Adler-32 trailer may go 61 - // unverified unless the caller reads to io.EOF. 62 - func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { 63 - file, zr, err := store.openInflated(id) 64 - if err != nil { 65 - return nil, err 66 - } 67 - 68 - br := bufio.NewReader(zr) 69 - 70 - header, _, size, err := readHeader(br) 71 - if err != nil { 72 - _ = zr.Close() 73 - _ = file.Close() 74 - 75 - return nil, err 76 - } 77 - 78 - return &objectReader{ 79 - reader: io.MultiReader( 80 - bytes.NewReader(header), 81 - iolimit.ExpectLengthReader(br, size), 82 - ), 83 - file: file, 84 - zr: zr, 85 - }, nil 86 - } 87 - 88 - // ReadReaderContent reads an object's type, declared content length, and 89 - // content stream. 90 - // 91 - // The caller must close the returned reader. 92 - // 93 - // Close releases resources only. It does not drain unread data for additional 94 - // validation. In particular, malformed trailing compressed data, trailing bytes 95 - // past the declared object size, and the zlib Adler-32 trailer may go 96 - // unverified unless the caller reads to io.EOF. 97 - func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { 98 - file, zr, err := store.openInflated(id) 99 - if err != nil { 100 - return objecttype.TypeInvalid, 0, nil, err 101 - } 102 - 103 - br := bufio.NewReader(zr) 104 - 105 - _, ty, size, err := readHeader(br) 106 - if err != nil { 107 - _ = zr.Close() 108 - _ = file.Close() 109 - 110 - return objecttype.TypeInvalid, 0, nil, err 111 - } 112 - 113 - return ty, size, &objectReader{ 114 - reader: iolimit.ExpectLengthReader(br, size), 115 - file: file, 116 - zr: zr, 117 - }, nil 118 - }
-13
object/storer/loose/read_size.go
··· 1 - package loose 2 - 3 - import objectid "codeberg.org/lindenii/furgit/object/id" 4 - 5 - // ReadSize reads an object's declared content length. 6 - // 7 - // Like ReadHeader, it parses only enough of the zlib-decoded object to recover 8 - // the header and does not verify the zlib Adler-32 trailer. 9 - func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) { 10 - _, size, err := store.ReadHeader(id) 11 - 12 - return size, err 13 - }
-212
object/storer/loose/read_test.go
··· 1 - package loose_test 2 - 3 - import ( 4 - "bytes" 5 - "errors" 6 - "os" 7 - "strings" 8 - "testing" 9 - 10 - "codeberg.org/lindenii/furgit/internal/testgit" 11 - objectid "codeberg.org/lindenii/furgit/object/id" 12 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 13 - "codeberg.org/lindenii/furgit/object/storer/loose" 14 - objecttype "codeberg.org/lindenii/furgit/object/type" 15 - ) 16 - 17 - func TestLooseStoreReadAgainstGit(t *testing.T) { 18 - t.Parallel() 19 - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 20 - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 21 - blobID := testRepo.HashObject(t, "blob", []byte("blob body\n")) 22 - _, treeID, commitID := testRepo.MakeCommit(t, "subject\n\nbody") 23 - tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message") 24 - 25 - store := openLooseStore(t, testRepo, algo) 26 - 27 - tests := []struct { 28 - name string 29 - id objectid.ObjectID 30 - }{ 31 - {name: "blob", id: blobID}, 32 - {name: "tree", id: treeID}, 33 - {name: "commit", id: commitID}, 34 - {name: "tag", id: tagID}, 35 - } 36 - 37 - for _, tt := range tests { 38 - t.Run(tt.name, func(t *testing.T) { 39 - wantType, wantBody, wantRaw := expectedRawObject(t, testRepo, tt.id) 40 - 41 - gotRaw, err := store.ReadBytesFull(tt.id) 42 - if err != nil { 43 - t.Fatalf("ReadBytesFull: %v", err) 44 - } 45 - 46 - if !bytes.Equal(gotRaw, wantRaw) { 47 - t.Fatalf("ReadBytesFull mismatch") 48 - } 49 - 50 - gotType, gotBody, err := store.ReadBytesContent(tt.id) 51 - if err != nil { 52 - t.Fatalf("ReadBytesContent: %v", err) 53 - } 54 - 55 - if gotType != wantType { 56 - t.Fatalf("ReadBytesContent type = %v, want %v", gotType, wantType) 57 - } 58 - 59 - if !bytes.Equal(gotBody, wantBody) { 60 - t.Fatalf("ReadBytesContent body mismatch") 61 - } 62 - 63 - headType, headSize, err := store.ReadHeader(tt.id) 64 - if err != nil { 65 - t.Fatalf("ReadHeader: %v", err) 66 - } 67 - 68 - if headType != wantType { 69 - t.Fatalf("ReadHeader type = %v, want %v", headType, wantType) 70 - } 71 - 72 - if headSize != int64(len(wantBody)) { 73 - t.Fatalf("ReadHeader size = %d, want %d", headSize, len(wantBody)) 74 - } 75 - 76 - fullReader, err := store.ReadReaderFull(tt.id) 77 - if err != nil { 78 - t.Fatalf("ReadReaderFull: %v", err) 79 - } 80 - 81 - got := mustReadAllAndClose(t, fullReader) 82 - if !bytes.Equal(got, wantRaw) { 83 - t.Fatalf("ReadReaderFull stream mismatch") 84 - } 85 - 86 - contentType, contentSize, contentReader, err := store.ReadReaderContent(tt.id) 87 - if err != nil { 88 - t.Fatalf("ReadReaderContent: %v", err) 89 - } 90 - 91 - if contentType != wantType { 92 - t.Fatalf("ReadReaderContent type = %v, want %v", contentType, wantType) 93 - } 94 - 95 - if contentSize != int64(len(wantBody)) { 96 - t.Fatalf("ReadReaderContent size = %d, want %d", contentSize, len(wantBody)) 97 - } 98 - 99 - got = mustReadAllAndClose(t, contentReader) 100 - if !bytes.Equal(got, wantBody) { 101 - t.Fatalf("ReadReaderContent stream mismatch") 102 - } 103 - }) 104 - } 105 - }) 106 - } 107 - 108 - func TestLooseStoreErrors(t *testing.T) { 109 - t.Parallel() 110 - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 111 - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 112 - store := openLooseStore(t, testRepo, algo) 113 - 114 - notFoundID, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen())) 115 - if err != nil { 116 - t.Fatalf("ParseHex(notFoundID): %v", err) 117 - } 118 - 119 - _, err = store.ReadBytesFull(notFoundID) 120 - if !errors.Is(err, objectstorer.ErrObjectNotFound) { 121 - t.Fatalf("ReadBytesFull not-found error = %v", err) 122 - } 123 - 124 - _, _, err = store.ReadBytesContent(notFoundID) 125 - if !errors.Is(err, objectstorer.ErrObjectNotFound) { 126 - t.Fatalf("ReadBytesContent not-found error = %v", err) 127 - } 128 - 129 - _, err = store.ReadReaderFull(notFoundID) 130 - if !errors.Is(err, objectstorer.ErrObjectNotFound) { 131 - t.Fatalf("ReadReaderFull not-found error = %v", err) 132 - } 133 - 134 - _, _, _, err = store.ReadReaderContent(notFoundID) 135 - if !errors.Is(err, objectstorer.ErrObjectNotFound) { 136 - t.Fatalf("ReadReaderContent not-found error = %v", err) 137 - } 138 - 139 - _, _, err = store.ReadHeader(notFoundID) 140 - if !errors.Is(err, objectstorer.ErrObjectNotFound) { 141 - t.Fatalf("ReadHeader not-found error = %v", err) 142 - } 143 - 144 - var otherAlgo objectid.Algorithm 145 - if algo == objectid.AlgorithmSHA1 { 146 - otherAlgo = objectid.AlgorithmSHA256 147 - } else { 148 - otherAlgo = objectid.AlgorithmSHA1 149 - } 150 - 151 - otherID, err := objectid.ParseHex(otherAlgo, strings.Repeat("1", otherAlgo.HexLen())) 152 - if err != nil { 153 - t.Fatalf("ParseHex(otherID): %v", err) 154 - } 155 - 156 - _, err = store.ReadBytesFull(otherID) 157 - if err == nil || !strings.Contains(err.Error(), "algorithm mismatch") { 158 - t.Fatalf("ReadBytesFull algorithm-mismatch error = %v", err) 159 - } 160 - }) 161 - } 162 - 163 - func TestLooseStoreNewValidation(t *testing.T) { 164 - t.Parallel() 165 - 166 - root, err := os.OpenRoot(t.TempDir()) 167 - if err != nil { 168 - t.Fatalf("OpenRoot: %v", err) 169 - } 170 - 171 - defer func() { _ = root.Close() }() 172 - 173 - _, err = loose.New(root, objectid.AlgorithmUnknown) 174 - if err == nil { 175 - t.Fatalf("loose.New(root, unknown) expected error") 176 - } 177 - } 178 - 179 - func TestLooseStoreReadHeaderDoesNotVerifyAdler32(t *testing.T) { 180 - t.Parallel() 181 - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 182 - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 183 - store := openLooseStore(t, testRepo, algo) 184 - 185 - content := []byte("header-only-check\n") 186 - 187 - id, err := store.WriteBytesContent(objecttype.TypeBlob, content) 188 - if err != nil { 189 - t.Fatalf("WriteBytesContent: %v", err) 190 - } 191 - 192 - corruptLooseObjectTrailer(t, testRepo, id) 193 - 194 - ty, size, err := store.ReadHeader(id) 195 - if err != nil { 196 - t.Fatalf("ReadHeader: %v", err) 197 - } 198 - 199 - if ty != objecttype.TypeBlob { 200 - t.Fatalf("ReadHeader type = %v, want %v", ty, objecttype.TypeBlob) 201 - } 202 - 203 - if size != int64(len(content)) { 204 - t.Fatalf("ReadHeader size = %d, want %d", size, len(content)) 205 - } 206 - 207 - _, err = store.ReadBytesFull(id) 208 - if err == nil { 209 - t.Fatalf("ReadBytesFull on corrupted trailer succeeded") 210 - } 211 - }) 212 - }
-6
object/storer/loose/refresh.go
··· 1 - package loose 2 - 3 - // Refresh is a no-op for loose object stores. 4 - func (store *Store) Refresh() error { 5 - return nil 6 - }
-41
object/storer/loose/store.go
··· 1 - // Package loose provides a loose object backend (objects/XX/YYYYY..). 2 - package loose 3 - 4 - import ( 5 - "os" 6 - 7 - objectid "codeberg.org/lindenii/furgit/object/id" 8 - ) 9 - 10 - // Store reads loose Git objects from an objects directory root. 11 - // 12 - // Loose objects are zlib streams whose trailer uses Adler-32. Which reads 13 - // consume enough of the stream to reach and verify that trailer is documented 14 - // on the individual methods. 15 - type Store struct { 16 - // root is the objects directory capability used for all object file access. 17 - // Object files are opened by relative paths like "<first2>/<rest>". 18 - // Store borrows this root. 19 - root *os.Root 20 - // algo is the expected object ID algorithm for lookups. 21 - algo objectid.Algorithm 22 - } 23 - 24 - // New creates a loose-object store rooted at an objects directory for algo. 25 - func New(root *os.Root, algo objectid.Algorithm) (*Store, error) { 26 - if algo.Size() == 0 { 27 - return nil, objectid.ErrInvalidAlgorithm 28 - } 29 - 30 - return &Store{ 31 - root: root, 32 - algo: algo, 33 - }, nil 34 - } 35 - 36 - // Close releases resources associated with the backend. 37 - // 38 - // Store borrows its root, so Close does not close it. 39 - // 40 - // Repeated calls to Close are undefined behavior. 41 - func (store *Store) Close() error { return nil }
-18
object/storer/loose/write_bytes.go
··· 1 - package loose 2 - 3 - import ( 4 - "bytes" 5 - 6 - objectid "codeberg.org/lindenii/furgit/object/id" 7 - objecttype "codeberg.org/lindenii/furgit/object/type" 8 - ) 9 - 10 - // WriteBytesFull writes a full serialized object as "type size\0content". 11 - func (store *Store) WriteBytesFull(raw []byte) (objectid.ObjectID, error) { 12 - return store.WriteReaderFull(bytes.NewReader(raw)) 13 - } 14 - 15 - // WriteBytesContent writes typed content bytes as a loose object. 16 - func (store *Store) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) { 17 - return store.WriteReaderContent(ty, int64(len(content)), bytes.NewReader(content)) 18 - }
-81
object/storer/loose/write_reader.go
··· 1 - package loose 2 - 3 - import ( 4 - "fmt" 5 - "io" 6 - 7 - objectheader "codeberg.org/lindenii/furgit/object/header" 8 - objectid "codeberg.org/lindenii/furgit/object/id" 9 - objecttype "codeberg.org/lindenii/furgit/object/type" 10 - ) 11 - 12 - // WriteReaderContent writes one loose object from typed content bytes read from src. 13 - // src must provide exactly size bytes. 14 - // size is required because loose object headers are "type size\0content", so the 15 - // header must be emitted before streaming content without buffering. 16 - func (store *Store) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) { 17 - if size < 0 { 18 - return objectid.ObjectID{}, fmt.Errorf("objectstorer/loose: negative content size: %d", size) 19 - } 20 - 21 - header, ok := objectheader.Encode(ty, size) 22 - if !ok { 23 - return objectid.ObjectID{}, fmt.Errorf("objectstorer/loose: failed to encode object header for type %v", ty) 24 - } 25 - 26 - writer, err := store.newStreamWriter(false) 27 - if err != nil { 28 - return objectid.ObjectID{}, err 29 - } 30 - 31 - writer.headerDone = true 32 - writer.expectedContentLeft = size 33 - 34 - err = writer.writeRawChunk(header) 35 - if err != nil { 36 - _ = writer.Close() 37 - _ = store.root.Remove(writer.tmpRelPath) 38 - 39 - return objectid.ObjectID{}, err 40 - } 41 - 42 - return writeReaderIntoStreamWriter(writer, src) 43 - } 44 - 45 - // WriteReaderFull writes one loose object from raw bytes "type size\0content" 46 - // read from src. 47 - func (store *Store) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) { 48 - writer, err := store.newStreamWriter(true) 49 - if err != nil { 50 - return objectid.ObjectID{}, err 51 - } 52 - 53 - return writeReaderIntoStreamWriter(writer, src) 54 - } 55 - 56 - // writeReaderIntoStreamWriter copies src into writer and publishes the object. 57 - func writeReaderIntoStreamWriter(writer *streamWriter, src io.Reader) (objectid.ObjectID, error) { 58 - _, err := io.Copy(writer, src) 59 - if err != nil { 60 - _ = writer.Close() 61 - _ = writer.store.root.Remove(writer.tmpRelPath) 62 - 63 - return objectid.ObjectID{}, err 64 - } 65 - 66 - err = writer.Close() 67 - if err != nil { 68 - _ = writer.store.root.Remove(writer.tmpRelPath) 69 - 70 - return objectid.ObjectID{}, err 71 - } 72 - 73 - id, err := writer.finalize() 74 - if err != nil { 75 - _ = writer.store.root.Remove(writer.tmpRelPath) 76 - 77 - return objectid.ObjectID{}, err 78 - } 79 - 80 - return id, nil 81 - }
-30
object/storer/loose/write_temp_object_file.go
··· 1 - package loose 2 - 3 - import ( 4 - "crypto/rand" 5 - "errors" 6 - "io/fs" 7 - "os" 8 - "path/filepath" 9 - ) 10 - 11 - // createTempObjectFile creates a unique temporary object file within dir. 12 - // The returned path is relative to the objects root. 13 - func (store *Store) createTempObjectFile(dir string) (string, *os.File, error) { 14 - for range 16 { 15 - relPath := filepath.Join(dir, tempObjectFilePrefix+rand.Text()) 16 - 17 - file, err := store.root.OpenFile(relPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) 18 - if err == nil { 19 - return relPath, file, nil 20 - } 21 - 22 - if errors.Is(err, fs.ErrExist) { 23 - continue 24 - } 25 - 26 - return "", nil, err 27 - } 28 - 29 - return "", nil, errors.New("objectstorer/loose: failed to create temporary object file") 30 - }
-137
object/storer/loose/write_test.go
··· 1 - package loose_test 2 - 3 - import ( 4 - "bytes" 5 - "testing" 6 - 7 - "codeberg.org/lindenii/furgit/internal/testgit" 8 - objectheader "codeberg.org/lindenii/furgit/object/header" 9 - objectid "codeberg.org/lindenii/furgit/object/id" 10 - objecttype "codeberg.org/lindenii/furgit/object/type" 11 - ) 12 - 13 - func TestLooseStoreWriteReaderContentAgainstGit(t *testing.T) { 14 - t.Parallel() 15 - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 16 - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 17 - store := openLooseStore(t, testRepo, algo) 18 - 19 - content := []byte("written-by-content-reader\n") 20 - expectedHex := testRepo.RunInput(t, content, "hash-object", "-t", "blob", "--stdin") 21 - 22 - expectedID, err := objectid.ParseHex(algo, expectedHex) 23 - if err != nil { 24 - t.Fatalf("ParseHex(expected): %v", err) 25 - } 26 - 27 - writtenID, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content)) 28 - if err != nil { 29 - t.Fatalf("WriteReaderContent: %v", err) 30 - } 31 - 32 - if writtenID != expectedID { 33 - t.Fatalf("WriteReaderContent id = %s, want %s", writtenID, expectedID) 34 - } 35 - 36 - gotBody := testRepo.CatFile(t, "blob", writtenID) 37 - if !bytes.Equal(gotBody, content) { 38 - t.Fatalf("git cat-file body mismatch") 39 - } 40 - 41 - // Writing the same object again should succeed and return the same ID. 42 - writtenID2, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content)) 43 - if err != nil { 44 - t.Fatalf("WriteReaderContent second: %v", err) 45 - } 46 - 47 - if writtenID2 != expectedID { 48 - t.Fatalf("WriteReaderContent second id = %s, want %s", writtenID2, expectedID) 49 - } 50 - }) 51 - } 52 - 53 - func TestLooseStoreWriteReaderFullAgainstGit(t *testing.T) { 54 - t.Parallel() 55 - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 56 - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 57 - store := openLooseStore(t, testRepo, algo) 58 - 59 - body := []byte("full-reader-body\n") 60 - 61 - header, ok := objectheader.Encode(objecttype.TypeBlob, int64(len(body))) 62 - if !ok { 63 - t.Fatalf("objectheader.Encode failed") 64 - } 65 - 66 - raw := make([]byte, len(header)+len(body)) 67 - copy(raw, header) 68 - copy(raw[len(header):], body) 69 - 70 - wantID := algo.Sum(raw) 71 - 72 - gotID, err := store.WriteReaderFull(bytes.NewReader(raw)) 73 - if err != nil { 74 - t.Fatalf("WriteReaderFull: %v", err) 75 - } 76 - 77 - if gotID != wantID { 78 - t.Fatalf("WriteReaderFull id = %s, want %s", gotID, wantID) 79 - } 80 - 81 - gotBody := testRepo.CatFile(t, "blob", gotID) 82 - if !bytes.Equal(gotBody, body) { 83 - t.Fatalf("git cat-file body mismatch") 84 - } 85 - }) 86 - } 87 - 88 - func TestLooseStoreReaderValidationErrors(t *testing.T) { 89 - t.Parallel() 90 - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 91 - t.Run("content overflow", func(t *testing.T) { 92 - t.Parallel() 93 - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 94 - store := openLooseStore(t, testRepo, algo) 95 - 96 - _, err := store.WriteReaderContent(objecttype.TypeBlob, 1, bytes.NewReader([]byte("hello"))) 97 - if err == nil { 98 - t.Fatalf("expected error after overflow") 99 - } 100 - }) 101 - 102 - t.Run("content short", func(t *testing.T) { 103 - t.Parallel() 104 - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 105 - store := openLooseStore(t, testRepo, algo) 106 - 107 - _, err := store.WriteReaderContent(objecttype.TypeBlob, 5, bytes.NewReader([]byte("x"))) 108 - if err == nil { 109 - t.Fatalf("expected error for short content") 110 - } 111 - }) 112 - 113 - t.Run("full malformed header", func(t *testing.T) { 114 - t.Parallel() 115 - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 116 - store := openLooseStore(t, testRepo, algo) 117 - 118 - _, err := store.WriteReaderFull(bytes.NewReader([]byte("not-a-header"))) 119 - if err == nil { 120 - t.Fatalf("expected error for malformed header") 121 - } 122 - }) 123 - 124 - t.Run("full size mismatch", func(t *testing.T) { 125 - t.Parallel() 126 - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 127 - store := openLooseStore(t, testRepo, algo) 128 - 129 - raw := []byte("blob 1\x00hello") 130 - 131 - _, err := store.WriteReaderFull(bytes.NewReader(raw)) 132 - if err == nil { 133 - t.Fatalf("expected error after mismatch") 134 - } 135 - }) 136 - }) 137 - }
-94
object/storer/loose/write_writer.go
··· 1 - package loose 2 - 3 - import ( 4 - "errors" 5 - "hash" 6 - "os" 7 - 8 - "codeberg.org/lindenii/furgit/internal/compress/zlib" 9 - ) 10 - 11 - const tempObjectFilePrefix = "tmp_obj_" 12 - 13 - // streamWriter incrementally hashes and deflates an object into a temp file. 14 - // Finalize validates size accounting and atomically renames the temp file. 15 - type streamWriter struct { 16 - // store owns path and root operations used by this write session. 17 - store *Store 18 - // file is the temporary destination file under objects/. 19 - file *os.File 20 - // zw compresses raw object bytes into file. 21 - zw *zlib.Writer 22 - // hash receives the same raw bytes used to compute the resulting object ID. 23 - hash hash.Hash 24 - 25 - // tmpRelPath is the relative path of file under the objects root. 26 - tmpRelPath string 27 - 28 - // fullMode selects full-object input ("type size\0content") as opposed to content-only input. 29 - fullMode bool 30 - 31 - // headerBuf accumulates header bytes while fullMode parses up to the first NUL. 32 - headerBuf []byte 33 - // headerDone reports whether the full-object header has been parsed. 34 - headerDone bool 35 - // expectedContentLeft tracks remaining declared content bytes. 36 - expectedContentLeft int64 37 - 38 - closed bool 39 - finalized bool 40 - } 41 - 42 - // newStreamWriter creates a stream writer with a temp file rooted in objects/. 43 - func (store *Store) newStreamWriter(fullMode bool) (*streamWriter, error) { 44 - hashFn, err := store.algo.New() 45 - if err != nil { 46 - return nil, err 47 - } 48 - 49 - tmpRelPath, file, err := store.createTempObjectFile(".") 50 - if err != nil { 51 - return nil, err 52 - } 53 - 54 - return &streamWriter{ 55 - store: store, 56 - file: file, 57 - zw: zlib.NewWriter(file), 58 - hash: hashFn, 59 - tmpRelPath: tmpRelPath, 60 - fullMode: fullMode, 61 - headerBuf: make([]byte, 0, 64), 62 - }, nil 63 - } 64 - 65 - // Write validates and writes raw bytes into the stream. 66 - // In full mode, it parses and enforces the streamed header-declared content size. 67 - func (writer *streamWriter) Write(src []byte) (int, error) { 68 - if writer.finalized { 69 - return 0, errors.New("objectstorer/loose: write after finalize") 70 - } 71 - 72 - if writer.closed { 73 - return 0, errors.New("objectstorer/loose: write after close") 74 - } 75 - 76 - if writer.fullMode { 77 - err := writer.acceptFull(src) 78 - if err != nil { 79 - return 0, err 80 - } 81 - } else { 82 - err := writer.acceptContent(int64(len(src))) 83 - if err != nil { 84 - return 0, err 85 - } 86 - } 87 - 88 - err := writer.writeRawChunk(src) 89 - if err != nil { 90 - return 0, err 91 - } 92 - 93 - return len(src), nil 94 - }
-61
object/storer/loose/write_writer_accept.go
··· 1 - package loose 2 - 3 - import ( 4 - "bytes" 5 - "errors" 6 - 7 - objectheader "codeberg.org/lindenii/furgit/object/header" 8 - ) 9 - 10 - // acceptFull validates and accounts raw full-object input. 11 - func (writer *streamWriter) acceptFull(src []byte) error { 12 - if !writer.headerDone { 13 - nul := bytes.IndexByte(src, 0) 14 - if nul >= 0 { 15 - headerChunkLen := nul + 1 16 - writer.headerBuf = append(writer.headerBuf, src[:headerChunkLen]...) 17 - 18 - _, size, _, ok := objectheader.Parse(writer.headerBuf) 19 - if !ok { 20 - return errors.New("objectstorer/loose: malformed object header") 21 - } 22 - 23 - writer.headerDone = true 24 - writer.expectedContentLeft = size 25 - 26 - return writer.acceptContent(int64(len(src) - headerChunkLen)) 27 - } 28 - 29 - writer.headerBuf = append(writer.headerBuf, src...) 30 - 31 - return nil 32 - } 33 - 34 - return writer.acceptContent(int64(len(src))) 35 - } 36 - 37 - // acceptContent validates and accounts content byte counts. 38 - func (writer *streamWriter) acceptContent(n int64) error { 39 - if n > writer.expectedContentLeft { 40 - return errors.New("objectstorer/loose: object content exceeds declared size") 41 - } 42 - 43 - writer.expectedContentLeft -= n 44 - 45 - return nil 46 - } 47 - 48 - // writeRawChunk forwards raw bytes to the hash and deflate pipeline. 49 - func (writer *streamWriter) writeRawChunk(src []byte) error { 50 - _, err := writer.hash.Write(src) 51 - if err != nil { 52 - return err 53 - } 54 - 55 - _, err = writer.zw.Write(src) 56 - if err != nil { 57 - return err 58 - } 59 - 60 - return nil 61 - }
-90
object/storer/loose/write_writer_finalize.go
··· 1 - package loose 2 - 3 - import ( 4 - "errors" 5 - "io/fs" 6 - "path/filepath" 7 - 8 - objectid "codeberg.org/lindenii/furgit/object/id" 9 - ) 10 - 11 - // Close flushes and closes the underlying zlib stream and temp file. 12 - // 13 - // Repeated calls to Close are undefined behavior. 14 - func (writer *streamWriter) Close() error { 15 - errZlib := writer.zw.Close() 16 - errSync := writer.file.Sync() 17 - errFile := writer.file.Close() 18 - 19 - writer.closed = true 20 - writer.file = nil 21 - 22 - return errors.Join(errZlib, errSync, errFile) 23 - } 24 - 25 - // finalize validates write completeness and atomically publishes the object. 26 - // Publication is no-clobber: it links tmpRelPath to the object path and treats 27 - // existing destination objects as success. 28 - func (writer *streamWriter) finalize() (objectid.ObjectID, error) { 29 - writer.finalized = true 30 - 31 - var zero objectid.ObjectID 32 - 33 - if !writer.closed { 34 - err := writer.Close() 35 - if err != nil { 36 - return zero, err 37 - } 38 - } 39 - 40 - if writer.fullMode && !writer.headerDone { 41 - return zero, errors.New("objectstorer/loose: missing full object header") 42 - } 43 - 44 - if writer.expectedContentLeft != 0 { 45 - return zero, errors.New("objectstorer/loose: object content shorter than declared size") 46 - } 47 - 48 - idBytes := writer.hash.Sum(nil) 49 - 50 - id, err := objectid.FromBytes(writer.store.algo, idBytes) 51 - if err != nil { 52 - return zero, err 53 - } 54 - 55 - relPath, err := writer.store.objectPath(id) 56 - if err != nil { 57 - return zero, err 58 - } 59 - 60 - dir := filepath.Dir(relPath) 61 - 62 - err = writer.store.root.MkdirAll(dir, 0o755) 63 - if err != nil { 64 - return zero, err 65 - } 66 - 67 - cleanup := true 68 - 69 - defer func() { 70 - if cleanup { 71 - _ = writer.store.root.Remove(writer.tmpRelPath) 72 - } 73 - }() 74 - 75 - err = writer.store.root.Link(writer.tmpRelPath, relPath) 76 - if err != nil { 77 - if errors.Is(err, fs.ErrExist) { 78 - cleanup = false 79 - _ = writer.store.root.Remove(writer.tmpRelPath) 80 - 81 - return id, nil 82 - } 83 - 84 - return zero, err 85 - } 86 - 87 - cleanup = false 88 - 89 - return id, nil 90 - }
-21
object/storer/memory/add.go
··· 1 - package memory 2 - 3 - import ( 4 - objectheader "codeberg.org/lindenii/furgit/object/header" 5 - objectid "codeberg.org/lindenii/furgit/object/id" 6 - objecttype "codeberg.org/lindenii/furgit/object/type" 7 - ) 8 - 9 - // AddObject stores one object body and returns its object ID. 10 - func (store *Store) AddObject(ty objecttype.Type, body []byte) objectid.ObjectID { 11 - header, ok := objectheader.Encode(ty, int64(len(body))) 12 - if !ok { 13 - panic("failed to encode object header") 14 - } 15 - 16 - raw := append(append([]byte(nil), header...), body...) 17 - id := store.algo.Sum(raw) 18 - store.objects[id] = storedObject{ty: ty, content: append([]byte(nil), body...)} 19 - 20 - return id 21 - }
-8
object/storer/memory/algorithm.go
··· 1 - package memory 2 - 3 - import objectid "codeberg.org/lindenii/furgit/object/id" 4 - 5 - // Algorithm returns the object ID algorithm used by the store. 6 - func (store *Store) Algorithm() objectid.Algorithm { 7 - return store.algo 8 - }
-2
object/storer/memory/doc.go
··· 1 - // Package memory provides one in-memory object store. 2 - package memory
-9
object/storer/memory/object.go
··· 1 - package memory 2 - 3 - import objecttype "codeberg.org/lindenii/furgit/object/type" 4 - 5 - // storedObject is one in-memory object entry. 6 - type storedObject struct { 7 - ty objecttype.Type 8 - content []byte 9 - }
-37
object/storer/memory/read_bytes.go
··· 1 - package memory 2 - 3 - import ( 4 - objectheader "codeberg.org/lindenii/furgit/object/header" 5 - objectid "codeberg.org/lindenii/furgit/object/id" 6 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 7 - objecttype "codeberg.org/lindenii/furgit/object/type" 8 - ) 9 - 10 - // ReadBytesFull reads one full object, including the object header. 11 - func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { 12 - obj, ok := store.objects[id] 13 - if !ok { 14 - return nil, objectstorer.ErrObjectNotFound 15 - } 16 - 17 - header, ok := objectheader.Encode(obj.ty, int64(len(obj.content))) 18 - if !ok { 19 - panic("failed to encode object header") 20 - } 21 - 22 - raw := make([]byte, len(header)+len(obj.content)) 23 - copy(raw, header) 24 - copy(raw[len(header):], obj.content) 25 - 26 - return raw, nil 27 - } 28 - 29 - // ReadBytesContent reads one object body. 30 - func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { 31 - obj, ok := store.objects[id] 32 - if !ok { 33 - return objecttype.TypeInvalid, nil, objectstorer.ErrObjectNotFound 34 - } 35 - 36 - return obj.ty, append([]byte(nil), obj.content...), nil 37 - }
-17
object/storer/memory/read_header.go
··· 1 - package memory 2 - 3 - import ( 4 - objectid "codeberg.org/lindenii/furgit/object/id" 5 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 6 - objecttype "codeberg.org/lindenii/furgit/object/type" 7 - ) 8 - 9 - // ReadHeader reads one object header. 10 - func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { 11 - obj, ok := store.objects[id] 12 - if !ok { 13 - return objecttype.TypeInvalid, 0, objectstorer.ErrObjectNotFound 14 - } 15 - 16 - return obj.ty, int64(len(obj.content)), nil 17 - }
-29
object/storer/memory/read_reader.go
··· 1 - package memory 2 - 3 - import ( 4 - "bytes" 5 - "io" 6 - 7 - objectid "codeberg.org/lindenii/furgit/object/id" 8 - objecttype "codeberg.org/lindenii/furgit/object/type" 9 - ) 10 - 11 - // ReadReaderFull reads one full object through a reader. 12 - func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { 13 - raw, err := store.ReadBytesFull(id) 14 - if err != nil { 15 - return nil, err 16 - } 17 - 18 - return io.NopCloser(bytes.NewReader(raw)), nil 19 - } 20 - 21 - // ReadReaderContent reads one object body through a reader. 22 - func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { 23 - ty, content, err := store.ReadBytesContent(id) 24 - if err != nil { 25 - return objecttype.TypeInvalid, 0, nil, err 26 - } 27 - 28 - return ty, int64(len(content)), io.NopCloser(bytes.NewReader(content)), nil 29 - }
-13
object/storer/memory/read_size.go
··· 1 - package memory 2 - 3 - import objectid "codeberg.org/lindenii/furgit/object/id" 4 - 5 - // ReadSize reads one object size. 6 - func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) { 7 - _, size, err := store.ReadHeader(id) 8 - if err != nil { 9 - return 0, err 10 - } 11 - 12 - return size, nil 13 - }
-6
object/storer/memory/refresh.go
··· 1 - package memory 2 - 3 - // Refresh is a no-op for in-memory object stores. 4 - func (store *Store) Refresh() error { 5 - return nil 6 - }
-24
object/storer/memory/store.go
··· 1 - package memory 2 - 3 - import ( 4 - objectid "codeberg.org/lindenii/furgit/object/id" 5 - ) 6 - 7 - // Store is one in-memory object store. 8 - type Store struct { 9 - algo objectid.Algorithm 10 - objects map[objectid.ObjectID]storedObject 11 - } 12 - 13 - // New builds one empty in-memory store for one object format. 14 - func New(algo objectid.Algorithm) *Store { 15 - return &Store{ 16 - algo: algo, 17 - objects: make(map[objectid.ObjectID]storedObject), 18 - } 19 - } 20 - 21 - // Close closes the in-memory store. 22 - func (store *Store) Close() error { 23 - return nil 24 - }
-51
object/storer/mix/bytes.go
··· 1 - package mix 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - 7 - objectid "codeberg.org/lindenii/furgit/object/id" 8 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 9 - objecttype "codeberg.org/lindenii/furgit/object/type" 10 - ) 11 - 12 - // ReadBytesFull reads a full serialized object from one backend that has it. 13 - func (mix *Mix) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { 14 - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { 15 - full, err := backend.ReadBytesFull(id) 16 - if err == nil { 17 - mix.touchBackend(backend) 18 - 19 - return full, nil 20 - } 21 - 22 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 23 - continue 24 - } 25 - 26 - return nil, fmt.Errorf("objectstorer: backend %d read bytes full: %w", i, err) 27 - } 28 - 29 - return nil, objectstorer.ErrObjectNotFound 30 - } 31 - 32 - // ReadBytesContent reads an object's type and content bytes from one backend 33 - // that has it. 34 - func (mix *Mix) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { 35 - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { 36 - ty, content, err := backend.ReadBytesContent(id) 37 - if err == nil { 38 - mix.touchBackend(backend) 39 - 40 - return ty, content, nil 41 - } 42 - 43 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 44 - continue 45 - } 46 - 47 - return objecttype.TypeInvalid, nil, fmt.Errorf("objectstorer: backend %d read bytes content: %w", i, err) 48 - } 49 - 50 - return objecttype.TypeInvalid, nil, objectstorer.ErrObjectNotFound 51 - }
-8
object/storer/mix/close.go
··· 1 - package mix 2 - 3 - // Close releases wrapper-local resources. 4 - // 5 - // Mix borrows its backends, so Close does not close them. 6 - // 7 - // Repeated calls to Close are undefined behavior. 8 - func (mix *Mix) Close() error { return nil }
-30
object/storer/mix/header.go
··· 1 - package mix 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - 7 - objectid "codeberg.org/lindenii/furgit/object/id" 8 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 9 - objecttype "codeberg.org/lindenii/furgit/object/type" 10 - ) 11 - 12 - // ReadHeader reads object header data from one backend that has it. 13 - func (mix *Mix) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { 14 - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { 15 - ty, size, err := backend.ReadHeader(id) 16 - if err == nil { 17 - mix.touchBackend(backend) 18 - 19 - return ty, size, nil 20 - } 21 - 22 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 23 - continue 24 - } 25 - 26 - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstorer: backend %d read header: %w", i, err) 27 - } 28 - 29 - return objecttype.TypeInvalid, 0, objectstorer.ErrObjectNotFound 30 - }
-20
object/storer/mix/mix.go
··· 1 - // Package mix provides an adaptive wrapper over multiple object storage 2 - // backends. 3 - package mix 4 - 5 - import ( 6 - "sync" 7 - 8 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 9 - ) 10 - 11 - // Mix queries multiple object databases with an MRU backend preference. 12 - // 13 - // Mix borrows its backend stores. 14 - type Mix struct { 15 - mu sync.RWMutex 16 - 17 - backendHead *backendNode 18 - backendTail *backendNode 19 - backendNodeByStore map[objectstorer.Store]*backendNode 20 - }
-74
object/storer/mix/mru.go
··· 1 - package mix 2 - 3 - import objectstorer "codeberg.org/lindenii/furgit/object/storer" 4 - 5 - type backendNode struct { 6 - backend objectstorer.Store 7 - prev *backendNode 8 - next *backendNode 9 - } 10 - 11 - //nolint:ireturn 12 - func (mix *Mix) firstBackend() objectstorer.Store { 13 - mix.mu.RLock() 14 - defer mix.mu.RUnlock() 15 - 16 - if mix.backendHead == nil { 17 - return nil 18 - } 19 - 20 - return mix.backendHead.backend 21 - } 22 - 23 - //nolint:ireturn 24 - func (mix *Mix) nextBackend(current objectstorer.Store) objectstorer.Store { 25 - mix.mu.RLock() 26 - defer mix.mu.RUnlock() 27 - 28 - node := mix.backendNodeByStore[current] 29 - if node == nil || node.next == nil { 30 - return nil 31 - } 32 - 33 - return node.next.backend 34 - } 35 - 36 - func (mix *Mix) touchBackend(backend objectstorer.Store) { 37 - if backend == nil { 38 - return 39 - } 40 - 41 - if !mix.mu.TryLock() { 42 - return 43 - } 44 - defer mix.mu.Unlock() 45 - 46 - node := mix.backendNodeByStore[backend] 47 - if node == nil || node == mix.backendHead { 48 - return 49 - } 50 - 51 - if node.prev != nil { 52 - node.prev.next = node.next 53 - } 54 - 55 - if node.next != nil { 56 - node.next.prev = node.prev 57 - } 58 - 59 - if mix.backendTail == node { 60 - mix.backendTail = node.prev 61 - } 62 - 63 - node.prev = nil 64 - 65 - node.next = mix.backendHead 66 - if mix.backendHead != nil { 67 - mix.backendHead.prev = node 68 - } 69 - 70 - mix.backendHead = node 71 - if mix.backendTail == nil { 72 - mix.backendTail = node 73 - } 74 - }
-39
object/storer/mix/new.go
··· 1 - package mix 2 - 3 - import objectstorer "codeberg.org/lindenii/furgit/object/storer" 4 - 5 - // New creates a Mix from backends. 6 - // 7 - // The provided backends must be non-nil and distinct. 8 - // Mix borrows the provided backends and does not close them in Close. 9 - func New(backends ...objectstorer.Store) *Mix { 10 - nodeByStore := make(map[objectstorer.Store]*backendNode, len(backends)) 11 - 12 - var ( 13 - head *backendNode 14 - tail *backendNode 15 - ) 16 - 17 - for _, backend := range backends { 18 - node := &backendNode{ 19 - backend: backend, 20 - prev: tail, 21 - } 22 - if tail != nil { 23 - tail.next = node 24 - } 25 - 26 - if head == nil { 27 - head = node 28 - } 29 - 30 - tail = node 31 - nodeByStore[backend] = node 32 - } 33 - 34 - return &Mix{ 35 - backendHead: head, 36 - backendTail: tail, 37 - backendNodeByStore: nodeByStore, 38 - } 39 - }
-53
object/storer/mix/reader.go
··· 1 - package mix 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - "io" 7 - 8 - objectid "codeberg.org/lindenii/furgit/object/id" 9 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 10 - objecttype "codeberg.org/lindenii/furgit/object/type" 11 - ) 12 - 13 - // ReadReaderFull reads a full serialized object stream from one backend that 14 - // has it. 15 - func (mix *Mix) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { 16 - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { 17 - reader, err := backend.ReadReaderFull(id) 18 - if err == nil { 19 - mix.touchBackend(backend) 20 - 21 - return reader, nil 22 - } 23 - 24 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 25 - continue 26 - } 27 - 28 - return nil, fmt.Errorf("objectstorer: backend %d read reader full: %w", i, err) 29 - } 30 - 31 - return nil, objectstorer.ErrObjectNotFound 32 - } 33 - 34 - // ReadReaderContent reads an object's type, declared content length, and 35 - // content stream from one backend that has it. 36 - func (mix *Mix) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { 37 - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { 38 - ty, size, reader, err := backend.ReadReaderContent(id) 39 - if err == nil { 40 - mix.touchBackend(backend) 41 - 42 - return ty, size, reader, nil 43 - } 44 - 45 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 46 - continue 47 - } 48 - 49 - return objecttype.TypeInvalid, 0, nil, fmt.Errorf("objectstorer: backend %d read reader content: %w", i, err) 50 - } 51 - 52 - return objecttype.TypeInvalid, 0, nil, objectstorer.ErrObjectNotFound 53 - }
-30
object/storer/mix/refresh.go
··· 1 - package mix 2 - 3 - import ( 4 - "errors" 5 - 6 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 7 - ) 8 - 9 - // Refresh forwards refresh calls to refresh-capable backends. 10 - func (mix *Mix) Refresh() error { 11 - mix.mu.RLock() 12 - 13 - backends := make([]objectstorer.Store, 0, len(mix.backendNodeByStore)) 14 - for node := mix.backendHead; node != nil; node = node.next { 15 - backends = append(backends, node.backend) 16 - } 17 - 18 - mix.mu.RUnlock() 19 - 20 - var errs []error 21 - 22 - for _, backend := range backends { 23 - err := backend.Refresh() 24 - if err != nil { 25 - errs = append(errs, err) 26 - } 27 - } 28 - 29 - return errors.Join(errs...) 30 - }
-29
object/storer/mix/size.go
··· 1 - package mix 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - 7 - objectid "codeberg.org/lindenii/furgit/object/id" 8 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 9 - ) 10 - 11 - // ReadSize reads object content length from one backend that has it. 12 - func (mix *Mix) ReadSize(id objectid.ObjectID) (int64, error) { 13 - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { 14 - size, err := backend.ReadSize(id) 15 - if err == nil { 16 - mix.touchBackend(backend) 17 - 18 - return size, nil 19 - } 20 - 21 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 22 - continue 23 - } 24 - 25 - return 0, fmt.Errorf("objectstorer: backend %d read size: %w", i, err) 26 - } 27 - 28 - return 0, objectstorer.ErrObjectNotFound 29 - }
-92
object/storer/objectstore.go
··· 1 - // Package objectstorer provides interfaces for object storage backends. 2 - package objectstorer 3 - 4 - import ( 5 - "errors" 6 - "io" 7 - 8 - objectid "codeberg.org/lindenii/furgit/object/id" 9 - objecttype "codeberg.org/lindenii/furgit/object/type" 10 - ) 11 - 12 - // ErrObjectNotFound indicates that an object does not exist in a backend. 13 - // This error MUST only be used in situations where the object store has 14 - // no specified object ID, but no other unexpected conditions were 15 - // encountered. In particular, it is not suitable for situations where one 16 - // object references another (such as a tree referencing a blob) but 17 - // the latter does not exist; these situations should use a separate 18 - // error (TODO). 19 - var ErrObjectNotFound = errors.New("objectstorer: object not found") 20 - 21 - // Store reads Git objects by object ID. 22 - // 23 - // Unless an implementation explicitly documents otherwise, values returned by 24 - // Store methods are only valid until the store is closed. 25 - type Store interface { 26 - // ReadBytesFull reads a full serialized object as "type size\0content". 27 - // 28 - // In a valid repository, hashing this payload with the same algorithm yields 29 - // the requested object ID. Readers should treat this as a repository 30 - // invariant and should not re-verify it on every read. 31 - // 32 - // Any read-time integrity verification beyond producing this payload is 33 - // implementation-defined. 34 - ReadBytesFull(id objectid.ObjectID) ([]byte, error) 35 - 36 - // ReadBytesContent reads an object's type and content bytes. 37 - // 38 - // Any read-time integrity verification beyond producing this payload is 39 - // implementation-defined. 40 - ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) 41 - 42 - // ReadReaderFull reads a full serialized object stream as "type size\0content". 43 - // 44 - // Caller must close the returned reader. 45 - // The returned reader is only valid until the store is closed. 46 - // 47 - // Any read-time integrity verification performed while producing the stream 48 - // is implementation-defined. 49 - ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) 50 - 51 - // ReadReaderContent reads an object's type, declared content length, 52 - // and content stream. 53 - // 54 - // Caller must close the returned reader. 55 - // The returned reader is only valid until the store is closed. 56 - // 57 - // Any read-time integrity verification performed while producing the stream 58 - // is implementation-defined. 59 - ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) 60 - 61 - // ReadSize reads an object's declared content length. 62 - // 63 - // This is equivalent to ReadHeader(...).size and may be cheaper than 64 - // ReadHeader when callers do not need object type. 65 - // 66 - // Any read-time integrity verification performed to produce the size is 67 - // implementation-defined. 68 - ReadSize(id objectid.ObjectID) (int64, error) 69 - 70 - // ReadHeader reads an object's type and declared content length. 71 - // 72 - // Any read-time integrity verification performed to produce the header is 73 - // implementation-defined. 74 - ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) 75 - 76 - // Refresh updates any backend-local discovery/cache view of on-disk objects. 77 - // 78 - // Backends without dynamic discovery should return nil. 79 - Refresh() error 80 - 81 - // Close releases resources associated with the backend. 82 - // 83 - // Repeated calls to Close are undefined behavior unless the implementation 84 - // explicitly documents otherwise. 85 - Close() error 86 - } 87 - 88 - // type Cursor any 89 - // 90 - // Then make all read functions accept and provide a Cursor 91 - // nil must always be accepted and would exhibit the same behavior as right now 92 - // Non-nil behavior is implementation-defined: e.g., pack selection
-3
object/storer/packed/TODO
··· 1 - * Per delta-plan memo map 2 - * Internal handle/request context (might expose it externally later and add to global interface) 3 - * Audit on mutex
-38
object/storer/packed/close.go
··· 1 - package packed 2 - 3 - // Close releases mapped pack/index resources associated with the store. 4 - // 5 - // Store borrows its root, so Close does not close it. 6 - // Close releases cached pack/index mappings retained by the store. 7 - // 8 - // Repeated calls to Close are undefined behavior. 9 - func (store *Store) Close() error { 10 - store.stateMu.Lock() 11 - packs := store.packs 12 - store.stateMu.Unlock() 13 - store.idxMu.RLock() 14 - indexes := store.idxByPack 15 - store.idxMu.RUnlock() 16 - 17 - var closeErr error 18 - 19 - for _, pack := range packs { 20 - err := pack.close() 21 - if err != nil && closeErr == nil { 22 - closeErr = err 23 - } 24 - } 25 - 26 - for _, index := range indexes { 27 - err := index.close() 28 - if err != nil && closeErr == nil { 29 - closeErr = err 30 - } 31 - } 32 - 33 - store.cacheMu.Lock() 34 - store.deltaCache.clear() 35 - store.cacheMu.Unlock() 36 - 37 - return closeErr 38 - }
-66
object/storer/packed/delta_build_chain.go
··· 1 - package packed 2 - 3 - import ( 4 - "fmt" 5 - 6 - packfmt "codeberg.org/lindenii/furgit/format/packfile" 7 - objecttype "codeberg.org/lindenii/furgit/object/type" 8 - ) 9 - 10 - // deltaBuildChain walks one object's chain and builds a reconstruction chain. 11 - func (store *Store) deltaBuildChain(start location) (deltaChain, error) { 12 - visited := make(map[location]struct{}) 13 - current := start 14 - 15 - var chain deltaChain 16 - 17 - for { 18 - if _, ok := visited[current]; ok { 19 - return deltaChain{}, fmt.Errorf("objectstorer/packed: delta cycle while resolving object") 20 - } 21 - 22 - visited[current] = struct{}{} 23 - 24 - _, meta, err := store.entryMetaAt(current) 25 - if err != nil { 26 - return deltaChain{}, err 27 - } 28 - 29 - if packfmt.IsBaseObjectType(meta.ty) { 30 - chain.baseLoc = current 31 - chain.baseType = meta.ty 32 - 33 - return chain, nil 34 - } 35 - 36 - switch meta.ty { 37 - case objecttype.TypeRefDelta: 38 - chain.deltas = append(chain.deltas, deltaNode{ 39 - loc: current, 40 - dataOffset: meta.dataOffset, 41 - }) 42 - 43 - next, err := store.lookup(meta.baseRefID) 44 - if err != nil { 45 - return deltaChain{}, err 46 - } 47 - 48 - current = next 49 - case objecttype.TypeOfsDelta: 50 - chain.deltas = append(chain.deltas, deltaNode{ 51 - loc: current, 52 - dataOffset: meta.dataOffset, 53 - }) 54 - current = location{ 55 - packName: current.packName, 56 - offset: meta.baseOfs, 57 - } 58 - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: 59 - return deltaChain{}, fmt.Errorf("objectstorer/packed: internal invariant violation for base type %d", meta.ty) 60 - case objecttype.TypeInvalid, objecttype.TypeFuture: 61 - return deltaChain{}, fmt.Errorf("objectstorer/packed: unsupported pack type %d", meta.ty) 62 - default: 63 - return deltaChain{}, fmt.Errorf("objectstorer/packed: unsupported pack type %d", meta.ty) 64 - } 65 - } 66 - }
-61
object/storer/packed/delta_cache.go
··· 1 - package packed 2 - 3 - import ( 4 - "codeberg.org/lindenii/furgit/internal/lru" 5 - objecttype "codeberg.org/lindenii/furgit/object/type" 6 - ) 7 - 8 - const defaultDeltaCacheMaxBytes = 32 << 20 9 - 10 - // deltaBaseKey identifies one base object by pack location. 11 - type deltaBaseKey struct { 12 - packName string 13 - offset uint64 14 - } 15 - 16 - // deltaBaseValue stores one cached base object body. 17 - type deltaBaseValue struct { 18 - ty objecttype.Type 19 - content []byte 20 - } 21 - 22 - // deltaCache wraps a weighted LRU for resolved delta bases. 23 - type deltaCache struct { 24 - lru *lru.Cache[deltaBaseKey, deltaBaseValue] 25 - } 26 - 27 - // newDeltaCache creates a delta base cache with a byte budget. 28 - func newDeltaCache(maxBytes int64) *deltaCache { 29 - return &deltaCache{ 30 - lru: lru.New( 31 - maxBytes, 32 - func(_ deltaBaseKey, value deltaBaseValue) int64 { 33 - return int64(len(value.content)) 34 - }, 35 - nil, 36 - ), 37 - } 38 - } 39 - 40 - // get returns a cloned cached base object value. 41 - func (cache *deltaCache) get(key deltaBaseKey) (objecttype.Type, []byte, bool) { 42 - value, ok := cache.lru.Get(key) 43 - if !ok { 44 - return objecttype.TypeInvalid, nil, false 45 - } 46 - 47 - return value.ty, append([]byte(nil), value.content...), true 48 - } 49 - 50 - // add stores a cloned base object value. 51 - func (cache *deltaCache) add(key deltaBaseKey, ty objecttype.Type, content []byte) { 52 - cache.lru.Add(key, deltaBaseValue{ 53 - ty: ty, 54 - content: append([]byte(nil), content...), 55 - }) 56 - } 57 - 58 - // clear removes all cached entries. 59 - func (cache *deltaCache) clear() { 60 - cache.lru.Clear() 61 - }
-13
object/storer/packed/delta_chain.go
··· 1 - package packed 2 - 3 - import objecttype "codeberg.org/lindenii/furgit/object/type" 4 - 5 - // deltaChain describes how to reconstruct one requested object. 6 - type deltaChain struct { 7 - // baseLoc points to the innermost base object. 8 - baseLoc location 9 - // baseType is the canonical object type resolved from baseLoc. 10 - baseType objecttype.Type 11 - // deltas contains delta objects from target down toward base. 12 - deltas []deltaNode 13 - }
-9
object/storer/packed/delta_node.go
··· 1 - package packed 2 - 3 - // deltaNode describes one delta object in a reconstruction chain. 4 - type deltaNode struct { 5 - // loc identifies the delta object's pack location. 6 - loc location 7 - // dataOffset points to the start of the delta zlib payload in pack. 8 - dataOffset int 9 - }
-61
object/storer/packed/delta_resolve_chain.go
··· 1 - package packed 2 - 3 - import ( 4 - "fmt" 5 - 6 - deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply" 7 - objecttype "codeberg.org/lindenii/furgit/object/type" 8 - ) 9 - 10 - // deltaResolveChain resolves one object chain into content bytes. 11 - func (store *Store) deltaResolveChain(chain deltaChain, declaredSize int64) (objecttype.Type, []byte, error) { 12 - ty, out, nextDelta, err := store.deltaResolveChainStart(chain) 13 - if err != nil { 14 - return objecttype.TypeInvalid, nil, err 15 - } 16 - 17 - for i := nextDelta; i >= 0; i-- { 18 - node := chain.deltas[i] 19 - 20 - pack, err := store.openPack(node.loc.packName) 21 - if err != nil { 22 - return objecttype.TypeInvalid, nil, err 23 - } 24 - 25 - delta, err := inflateAt(pack, node.dataOffset, -1) 26 - if err != nil { 27 - return objecttype.TypeInvalid, nil, err 28 - } 29 - 30 - out, err = deltaapply.Apply(out, delta) 31 - if err != nil { 32 - return objecttype.TypeInvalid, nil, err 33 - } 34 - 35 - store.cacheMu.Lock() 36 - store.deltaCache.add( 37 - deltaBaseKey{packName: node.loc.packName, offset: node.loc.offset}, 38 - ty, 39 - out, 40 - ) 41 - store.cacheMu.Unlock() 42 - } 43 - 44 - if int64(len(out)) != declaredSize { 45 - return objecttype.TypeInvalid, nil, fmt.Errorf( 46 - "objectstorer/packed: resolved content size mismatch: got %d want %d", 47 - len(out), 48 - declaredSize, 49 - ) 50 - } 51 - 52 - if ty != chain.baseType { 53 - return objecttype.TypeInvalid, nil, fmt.Errorf( 54 - "objectstorer/packed: resolved content type mismatch: got %d want %d", 55 - ty, 56 - chain.baseType, 57 - ) 58 - } 59 - 60 - return ty, out, nil 61 - }
-59
object/storer/packed/delta_resolve_chain_start.go
··· 1 - package packed 2 - 3 - import ( 4 - "fmt" 5 - 6 - packfmt "codeberg.org/lindenii/furgit/format/packfile" 7 - objecttype "codeberg.org/lindenii/furgit/object/type" 8 - ) 9 - 10 - // deltaResolveChainStart finds the nearest cached chain node or inflates the 11 - // innermost base object. It returns the starting bytes and the next delta index 12 - // to apply in reverse order. 13 - func (store *Store) deltaResolveChainStart(chain deltaChain) (objecttype.Type, []byte, int, error) { 14 - for i, node := range chain.deltas { 15 - store.cacheMu.RLock() 16 - ty, out, ok := store.deltaCache.get( 17 - deltaBaseKey{packName: node.loc.packName, offset: node.loc.offset}, 18 - ) 19 - store.cacheMu.RUnlock() 20 - 21 - if ok { 22 - return ty, out, i - 1, nil 23 - } 24 - } 25 - 26 - store.cacheMu.RLock() 27 - ty, out, ok := store.deltaCache.get( 28 - deltaBaseKey{packName: chain.baseLoc.packName, offset: chain.baseLoc.offset}, 29 - ) 30 - store.cacheMu.RUnlock() 31 - 32 - if ok { 33 - return ty, out, len(chain.deltas) - 1, nil 34 - } 35 - 36 - pack, meta, err := store.entryMetaAt(chain.baseLoc) 37 - if err != nil { 38 - return objecttype.TypeInvalid, nil, 0, err 39 - } 40 - 41 - if !packfmt.IsBaseObjectType(meta.ty) { 42 - return objecttype.TypeInvalid, nil, 0, fmt.Errorf("objectstorer/packed: delta chain base is not a base object") 43 - } 44 - 45 - base, err := inflateAt(pack, meta.dataOffset, meta.size) 46 - if err != nil { 47 - return objecttype.TypeInvalid, nil, 0, err 48 - } 49 - 50 - store.cacheMu.Lock() 51 - store.deltaCache.add( 52 - deltaBaseKey{packName: chain.baseLoc.packName, offset: chain.baseLoc.offset}, 53 - meta.ty, 54 - base, 55 - ) 56 - store.cacheMu.Unlock() 57 - 58 - return meta.ty, base, len(chain.deltas) - 1, nil 59 - }
-29
object/storer/packed/delta_resolve_content.go
··· 1 - package packed 2 - 3 - import ( 4 - packfmt "codeberg.org/lindenii/furgit/format/packfile" 5 - objecttype "codeberg.org/lindenii/furgit/object/type" 6 - ) 7 - 8 - // deltaResolveContent resolves one object's content bytes from its pack location. 9 - func (store *Store) deltaResolveContent(start location) (objecttype.Type, []byte, error) { 10 - chain, err := store.deltaBuildChain(start) 11 - if err != nil { 12 - return objecttype.TypeInvalid, nil, err 13 - } 14 - 15 - pack, meta, err := store.entryMetaAt(start) 16 - if err != nil { 17 - return objecttype.TypeInvalid, nil, err 18 - } 19 - 20 - declaredSize := meta.size 21 - if !packfmt.IsBaseObjectType(meta.ty) { 22 - declaredSize, err = deltaDeclaredSizeAt(pack, meta.dataOffset) 23 - if err != nil { 24 - return objecttype.TypeInvalid, nil, err 25 - } 26 - } 27 - 28 - return store.deltaResolveChain(chain, declaredSize) 29 - }
-27
object/storer/packed/delta_size.go
··· 1 - package packed 2 - 3 - import ( 4 - "bufio" 5 - 6 - deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply" 7 - ) 8 - 9 - // deltaDeclaredSizeAt returns the resolved object size declared by one delta 10 - // stream header at dataOffset. 11 - func deltaDeclaredSizeAt(pack *packFile, dataOffset int) (int64, error) { 12 - reader, err := zlibReaderAt(pack, dataOffset) 13 - if err != nil { 14 - return 0, err 15 - } 16 - 17 - defer func() { _ = reader.Close() }() 18 - 19 - br := bufio.NewReaderSize(reader, 32) 20 - 21 - _, size, err := deltaapply.ReadHeaderSizes(br) 22 - if err != nil { 23 - return 0, err 24 - } 25 - 26 - return int64(size), nil 27 - }
-55
object/storer/packed/entry_inflate.go
··· 1 - package packed 2 - 3 - import ( 4 - "bytes" 5 - "fmt" 6 - "io" 7 - "math" 8 - 9 - "codeberg.org/lindenii/furgit/internal/compress/zlib" 10 - ) 11 - 12 - // zlibReaderAt opens a zlib reader starting at data offset within pack. 13 - func zlibReaderAt(pack *packFile, offset int) (io.ReadCloser, error) { 14 - if offset < 0 || offset > len(pack.data) { 15 - return nil, fmt.Errorf("objectstorer/packed: pack %q zlib offset out of bounds", pack.name) 16 - } 17 - 18 - return zlib.NewReader(bytes.NewReader(pack.data[offset:])) 19 - } 20 - 21 - // inflateAt inflates one entry payload from data offset. 22 - func inflateAt(pack *packFile, offset int, expectedSize int64) ([]byte, error) { 23 - reader, err := zlibReaderAt(pack, offset) 24 - if err != nil { 25 - return nil, err 26 - } 27 - 28 - defer func() { _ = reader.Close() }() 29 - 30 - if expectedSize >= 0 { 31 - if expectedSize > int64(math.MaxInt) { 32 - return nil, fmt.Errorf( 33 - "objectstorer/packed: pack %q expected inflated size overflows int: %d", 34 - pack.name, 35 - expectedSize, 36 - ) 37 - } 38 - 39 - body := make([]byte, int(expectedSize)) 40 - 41 - _, err := io.ReadFull(reader, body) 42 - if err != nil { 43 - return nil, err 44 - } 45 - 46 - return body, nil 47 - } 48 - 49 - body, err := io.ReadAll(reader) 50 - if err != nil { 51 - return nil, err 52 - } 53 - 54 - return body, nil 55 - }
-16
object/storer/packed/entry_meta.go
··· 1 - package packed 2 - 3 - // entryMetaAt parses one pack entry header at location. 4 - func (store *Store) entryMetaAt(loc location) (*packFile, entryMeta, error) { 5 - pack, err := store.openPack(loc.packName) 6 - if err != nil { 7 - return nil, entryMeta{}, err 8 - } 9 - 10 - meta, err := parseEntryMeta(pack, store.algo, loc.offset) 11 - if err != nil { 12 - return nil, entryMeta{}, err 13 - } 14 - 15 - return pack, meta, nil 16 - }
-71
object/storer/packed/entry_parse.go
··· 1 - package packed 2 - 3 - import ( 4 - "fmt" 5 - 6 - packfmt "codeberg.org/lindenii/furgit/format/packfile" 7 - "codeberg.org/lindenii/furgit/internal/intconv" 8 - objectid "codeberg.org/lindenii/furgit/object/id" 9 - objecttype "codeberg.org/lindenii/furgit/object/type" 10 - ) 11 - 12 - // entryMeta describes one parsed pack entry header. 13 - type entryMeta struct { 14 - // ty is the pack entry type tag. 15 - ty objecttype.Type 16 - // size is the declared resulting content size. 17 - size int64 18 - // dataOffset points to the zlib payload start. 19 - dataOffset int 20 - // baseRefID is set for ref-delta entries. 21 - baseRefID objectid.ObjectID 22 - // baseOfs is set for ofs-delta entries. 23 - baseOfs uint64 24 - } 25 - 26 - // parseEntryMeta parses one pack entry header at offset. 27 - func parseEntryMeta(pack *packFile, algo objectid.Algorithm, offset uint64) (entryMeta, error) { 28 - var zero entryMeta 29 - if offset >= uint64(len(pack.data)) { 30 - return zero, fmt.Errorf("objectstorer/packed: pack %q offset %d out of bounds", pack.name, offset) 31 - } 32 - 33 - pos, err := intconv.Uint64ToInt(offset) 34 - if err != nil { 35 - return zero, fmt.Errorf("objectstorer/packed: pack %q offset conversion: %w", pack.name, err) 36 - } 37 - 38 - entry, err := packfmt.ParseEntry(pack.data[pos:], algo.Size()) 39 - if err != nil { 40 - return zero, fmt.Errorf("objectstorer/packed: pack %q: %w", pack.name, err) 41 - } 42 - 43 - meta := entryMeta{ 44 - ty: entry.Type, 45 - size: entry.Size, 46 - dataOffset: pos + entry.DataOffset, 47 - } 48 - switch meta.ty { 49 - case objecttype.TypeRefDelta: 50 - baseID, err := objectid.FromBytes(algo, entry.RefBaseID) 51 - if err != nil { 52 - return zero, fmt.Errorf("objectstorer/packed: pack %q invalid ref-delta base id: %w", pack.name, err) 53 - } 54 - 55 - meta.baseRefID = baseID 56 - case objecttype.TypeOfsDelta: 57 - if offset <= entry.OfsBaseDistance { 58 - return zero, fmt.Errorf("objectstorer/packed: pack %q has invalid ofs-delta base", pack.name) 59 - } 60 - 61 - meta.baseOfs = offset - entry.OfsBaseDistance 62 - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: 63 - // Base object types do not have delta base metadata. 64 - case objecttype.TypeInvalid, objecttype.TypeFuture: 65 - return zero, fmt.Errorf("objectstorer/packed: pack %q has unsupported entry type %d", pack.name, meta.ty) 66 - default: 67 - return zero, fmt.Errorf("objectstorer/packed: pack %q has unsupported entry type %d", pack.name, meta.ty) 68 - } 69 - 70 - return meta, nil 71 - }
-102
object/storer/packed/helpers_test.go
··· 1 - package packed_test 2 - 3 - import ( 4 - "fmt" 5 - "io" 6 - "strconv" 7 - "strings" 8 - "testing" 9 - 10 - "codeberg.org/lindenii/furgit/internal/testgit" 11 - objectheader "codeberg.org/lindenii/furgit/object/header" 12 - objectid "codeberg.org/lindenii/furgit/object/id" 13 - "codeberg.org/lindenii/furgit/object/storer/packed" 14 - objecttype "codeberg.org/lindenii/furgit/object/type" 15 - ) 16 - 17 - func openPackedStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *packed.Store { 18 - t.Helper() 19 - 20 - root := testRepo.OpenPackRoot(t) 21 - 22 - store, err := packed.New(root, algo, packed.Options{}) 23 - if err != nil { 24 - t.Fatalf("packed.New: %v", err) 25 - } 26 - 27 - return store 28 - } 29 - 30 - func mustReadAllAndClose(t *testing.T, reader io.ReadCloser) []byte { 31 - t.Helper() 32 - 33 - data, err := io.ReadAll(reader) 34 - if err != nil { 35 - _ = reader.Close() 36 - 37 - t.Fatalf("ReadAll: %v", err) 38 - } 39 - 40 - err = reader.Close() 41 - if err != nil { 42 - t.Fatalf("Close: %v", err) 43 - } 44 - 45 - return data 46 - } 47 - 48 - func expectedRawObject(t *testing.T, testRepo *testgit.TestRepo, id objectid.ObjectID) (objecttype.Type, []byte, []byte) { 49 - t.Helper() 50 - 51 - typeName := testRepo.Run(t, "cat-file", "-t", id.String()) 52 - 53 - ty, ok := objecttype.ParseName(typeName) 54 - if !ok { 55 - t.Fatalf("ParseName(%q) failed", typeName) 56 - } 57 - 58 - body := testRepo.CatFile(t, typeName, id) 59 - 60 - header, ok := objectheader.Encode(ty, int64(len(body))) 61 - if !ok { 62 - t.Fatalf("objectheader.Encode failed") 63 - } 64 - 65 - raw := make([]byte, len(header)+len(body)) 66 - copy(raw, header) 67 - copy(raw[len(header):], body) 68 - 69 - return ty, body, raw 70 - } 71 - 72 - func createPackedFixtureRepo(t *testing.T, algo objectid.Algorithm) (*testgit.TestRepo, []objectid.ObjectID) { 73 - t.Helper() 74 - 75 - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 76 - blobID, treeID, commitID := testRepo.MakeCommit(t, "packed store base commit") 77 - testRepo.Run(t, "update-ref", "refs/heads/main", commitID.String()) 78 - tagID := testRepo.TagAnnotated(t, "v1.0.0", commitID, "packed-store-tag") 79 - 80 - parent := commitID 81 - 82 - for i := range 24 { 83 - content := "common-prefix\n" + strings.Repeat("line-"+strconv.Itoa(i%3)+"\n", 256) + fmt.Sprintf("tail-%d\n", i) 84 - nextBlob, nextTree := testRepo.MakeSingleFileTree(t, fmt.Sprintf("file-%02d.txt", i), []byte(content)) 85 - nextCommit := testRepo.CommitTree(t, nextTree, fmt.Sprintf("commit-%02d", i), parent) 86 - testRepo.Run(t, "update-ref", "refs/heads/main", nextCommit.String()) 87 - parent = nextCommit 88 - 89 - _ = nextBlob 90 - _ = nextTree 91 - } 92 - 93 - testRepo.Repack(t, "-a", "-d", "-f", "--window=64", "--depth=64") 94 - 95 - return testRepo, []objectid.ObjectID{ 96 - blobID, 97 - treeID, 98 - commitID, 99 - tagID, 100 - parent, 101 - } 102 - }
-36
object/storer/packed/idx.go
··· 1 - package packed 2 - 3 - import ( 4 - "os" 5 - 6 - objectid "codeberg.org/lindenii/furgit/object/id" 7 - ) 8 - 9 - // idxFile stores one mapped and validated idx v2 file. 10 - type idxFile struct { 11 - // idxName is the basename of this .idx file. 12 - idxName string 13 - // packName is the matching .pack basename. 14 - packName string 15 - // algo is the hash algorithm encoded by the index. 16 - algo objectid.Algorithm 17 - 18 - // file is the opened index file descriptor. 19 - file *os.File 20 - // data is the mapped index bytes. 21 - data []byte 22 - 23 - // fanout stores fanout table values. 24 - fanout [256]uint32 25 - // numObjects equals fanout[255]. 26 - numObjects int 27 - 28 - // namesOffset starts the sorted object-id table. 29 - namesOffset int 30 - // offset32Offset starts the 32-bit offset table. 31 - offset32Offset int 32 - // offset64Offset starts the 64-bit offset table. 33 - offset64Offset int 34 - // offset64Count is the number of 64-bit offset entries. 35 - offset64Count int 36 - }
-136
object/storer/packed/idx_candidates_mru.go
··· 1 - package packed 2 - 3 - // packCandidateNode is one node in the candidate MRU order list. 4 - type packCandidateNode struct { 5 - packName string 6 - prev *packCandidateNode 7 - next *packCandidateNode 8 - } 9 - 10 - func (store *Store) reconcileMRU(candidates []packCandidate) { 11 - store.mruMu.Lock() 12 - defer store.mruMu.Unlock() 13 - 14 - if store.mruNodeByPack == nil { 15 - store.mruNodeByPack = make(map[string]*packCandidateNode, len(candidates)) 16 - } 17 - 18 - present := make(map[string]struct{}, len(candidates)) 19 - for _, candidate := range candidates { 20 - present[candidate.packName] = struct{}{} 21 - } 22 - 23 - ordered := make([]string, 0, len(candidates)) 24 - 25 - for node := store.mruHead; node != nil; node = node.next { 26 - if _, ok := present[node.packName]; !ok { 27 - continue 28 - } 29 - 30 - ordered = append(ordered, node.packName) 31 - delete(present, node.packName) 32 - } 33 - 34 - for _, candidate := range candidates { 35 - if _, ok := present[candidate.packName]; !ok { 36 - continue 37 - } 38 - 39 - ordered = append(ordered, candidate.packName) 40 - delete(present, candidate.packName) 41 - } 42 - 43 - store.mruHead = nil 44 - store.mruTail = nil 45 - store.mruNodeByPack = make(map[string]*packCandidateNode, len(ordered)) 46 - 47 - for _, packName := range ordered { 48 - node := &packCandidateNode{ 49 - packName: packName, 50 - prev: store.mruTail, 51 - } 52 - if store.mruTail != nil { 53 - store.mruTail.next = node 54 - } 55 - 56 - if store.mruHead == nil { 57 - store.mruHead = node 58 - } 59 - 60 - store.mruTail = node 61 - store.mruNodeByPack[packName] = node 62 - } 63 - } 64 - 65 - // touchCandidate moves one candidate to the front of the lookup order. 66 - // This is done on a best-effort basis. 67 - func (store *Store) touchCandidate(packName string) { 68 - if !store.mruMu.TryLock() { 69 - return 70 - } 71 - defer store.mruMu.Unlock() 72 - 73 - node := store.mruNodeByPack[packName] 74 - if node == nil || node == store.mruHead { 75 - return 76 - } 77 - 78 - if node.prev != nil { 79 - node.prev.next = node.next 80 - } 81 - 82 - if node.next != nil { 83 - node.next.prev = node.prev 84 - } 85 - 86 - if store.mruTail == node { 87 - store.mruTail = node.prev 88 - } 89 - 90 - node.prev = nil 91 - 92 - node.next = store.mruHead 93 - if store.mruHead != nil { 94 - store.mruHead.prev = node 95 - } 96 - 97 - store.mruHead = node 98 - if store.mruTail == nil { 99 - store.mruTail = node 100 - } 101 - } 102 - 103 - // firstCandidatePackName returns the current head pack name, or "" when none 104 - // are available. 105 - func (store *Store) firstCandidatePackName(snapshot *candidateSnapshot) string { 106 - store.mruMu.RLock() 107 - defer store.mruMu.RUnlock() 108 - 109 - for node := store.mruHead; node != nil; node = node.next { 110 - if _, ok := snapshot.candidateByPack[node.packName]; ok { 111 - return node.packName 112 - } 113 - } 114 - 115 - return "" 116 - } 117 - 118 - // nextCandidatePackName returns the pack name after currentPack in current MRU 119 - // order, or "" at end / when currentPack is not present. 120 - func (store *Store) nextCandidatePackName(currentPack string, snapshot *candidateSnapshot) string { 121 - store.mruMu.RLock() 122 - defer store.mruMu.RUnlock() 123 - 124 - node := store.mruNodeByPack[currentPack] 125 - if node == nil { 126 - return "" 127 - } 128 - 129 - for node = node.next; node != nil; node = node.next { 130 - if _, ok := snapshot.candidateByPack[node.packName]; ok { 131 - return node.packName 132 - } 133 - } 134 - 135 - return "" 136 - }
-28
object/storer/packed/idx_close.go
··· 1 - package packed 2 - 3 - import "syscall" 4 - 5 - // close unmaps and closes one idx handle. 6 - func (index *idxFile) close() error { 7 - var closeErr error 8 - 9 - if index.data != nil { 10 - err := syscall.Munmap(index.data) 11 - if err != nil && closeErr == nil { 12 - closeErr = err 13 - } 14 - 15 - index.data = nil 16 - } 17 - 18 - if index.file != nil { 19 - err := index.file.Close() 20 - if err != nil && closeErr == nil { 21 - closeErr = err 22 - } 23 - 24 - index.file = nil 25 - } 26 - 27 - return closeErr 28 - }
-91
object/storer/packed/idx_lookup.go
··· 1 - package packed 2 - 3 - import ( 4 - "bytes" 5 - "encoding/binary" 6 - "fmt" 7 - 8 - objectid "codeberg.org/lindenii/furgit/object/id" 9 - ) 10 - 11 - // lookup resolves one object ID to its pack offset within this index. 12 - func (index *idxFile) lookup(id objectid.ObjectID) (uint64, bool, error) { 13 - if id.Algorithm() != index.algo { 14 - return 0, false, fmt.Errorf("objectstorer/packed: object id algorithm mismatch") 15 - } 16 - 17 - idBytes := (&id).RawBytes() 18 - 19 - hashSize := len(idBytes) 20 - if hashSize != index.algo.Size() { 21 - return 0, false, fmt.Errorf("objectstorer/packed: unexpected object id length") 22 - } 23 - 24 - first := int(idBytes[0]) 25 - 26 - lo := 0 27 - if first > 0 { 28 - lo = int(index.fanout[first-1]) 29 - } 30 - 31 - hi := int(index.fanout[first]) 32 - if lo < 0 || hi < 0 || lo > hi || hi > index.numObjects { 33 - return 0, false, fmt.Errorf("objectstorer/packed: idx %q has invalid fanout bounds", index.idxName) 34 - } 35 - 36 - for lo < hi { 37 - mid := lo + (hi-lo)/2 38 - 39 - nameOffset := index.namesOffset + mid*hashSize 40 - if nameOffset < 0 || nameOffset+hashSize > len(index.data) { 41 - return 0, false, fmt.Errorf("objectstorer/packed: idx %q truncated name table", index.idxName) 42 - } 43 - 44 - cmp := bytes.Compare(index.data[nameOffset:nameOffset+hashSize], idBytes) 45 - if cmp == 0 { 46 - offset, err := index.offsetAt(mid) 47 - if err != nil { 48 - return 0, false, err 49 - } 50 - 51 - return offset, true, nil 52 - } 53 - 54 - if cmp < 0 { 55 - lo = mid + 1 56 - } else { 57 - hi = mid 58 - } 59 - } 60 - 61 - return 0, false, nil 62 - } 63 - 64 - // offsetAt resolves the pack offset for one object index entry. 65 - func (index *idxFile) offsetAt(objectIndex int) (uint64, error) { 66 - if objectIndex < 0 || objectIndex >= index.numObjects { 67 - return 0, fmt.Errorf("objectstorer/packed: idx %q offset index out of bounds", index.idxName) 68 - } 69 - 70 - wordOffset := index.offset32Offset + objectIndex*4 71 - if wordOffset < 0 || wordOffset+4 > len(index.data) { 72 - return 0, fmt.Errorf("objectstorer/packed: idx %q truncated 32-bit offset table", index.idxName) 73 - } 74 - 75 - word := binary.BigEndian.Uint32(index.data[wordOffset : wordOffset+4]) 76 - if word&0x80000000 == 0 { 77 - return uint64(word), nil 78 - } 79 - 80 - pos := int(word & 0x7fffffff) 81 - if pos < 0 || pos >= index.offset64Count { 82 - return 0, fmt.Errorf("objectstorer/packed: idx %q invalid 64-bit offset position", index.idxName) 83 - } 84 - 85 - offOffset := index.offset64Offset + pos*8 86 - if offOffset < 0 || offOffset+8 > len(index.data)-2*index.algo.Size() { 87 - return 0, fmt.Errorf("objectstorer/packed: idx %q truncated 64-bit offset table", index.idxName) 88 - } 89 - 90 - return binary.BigEndian.Uint64(index.data[offOffset : offOffset+8]), nil 91 - }
-126
object/storer/packed/idx_lookup_candidates.go
··· 1 - package packed 2 - 3 - import ( 4 - "fmt" 5 - "os" 6 - "slices" 7 - "strings" 8 - ) 9 - 10 - // packCandidate describes one discovered pack/index pair. 11 - type packCandidate struct { 12 - // packName is the .pack basename. 13 - packName string 14 - // idxName is the .idx basename. 15 - idxName string 16 - // mtime is the pack file modification time for initial ordering. 17 - mtime int64 18 - } 19 - 20 - type candidateSnapshot struct { 21 - candidates []packCandidate 22 - candidateByPack map[string]packCandidate 23 - } 24 - 25 - // Refresh rescans objects/pack and atomically installs a fresh candidate list 26 - // for future lookups. 27 - // 28 - // Refresh does not invalidate existing readers. Cached pack/index mappings, 29 - // including ones for previously visible candidates, may be retained until 30 - // Close. 31 - func (store *Store) Refresh() error { 32 - store.refreshMu.Lock() 33 - defer store.refreshMu.Unlock() 34 - 35 - candidates, err := store.discoverCandidates() 36 - if err != nil { 37 - return err 38 - } 39 - 40 - candidateByPack := make(map[string]packCandidate, len(candidates)) 41 - for _, candidate := range candidates { 42 - candidateByPack[candidate.packName] = candidate 43 - } 44 - 45 - store.reconcileMRU(candidates) 46 - 47 - store.candidates.Store(&candidateSnapshot{ 48 - candidates: candidates, 49 - candidateByPack: candidateByPack, 50 - }) 51 - 52 - return nil 53 - } 54 - 55 - func (store *Store) ensureCandidates() (*candidateSnapshot, error) { 56 - snapshot := store.candidates.Load() 57 - if snapshot != nil { 58 - return snapshot, nil 59 - } 60 - 61 - err := store.Refresh() 62 - if err != nil { 63 - return nil, err 64 - } 65 - 66 - return store.candidates.Load(), nil 67 - } 68 - 69 - // discoverCandidates scans the objects/pack root and returns sorted pack/index 70 - // pairs. 71 - func (store *Store) discoverCandidates() ([]packCandidate, error) { 72 - dir, err := store.root.Open(".") 73 - if err != nil { 74 - if os.IsNotExist(err) { 75 - return nil, nil 76 - } 77 - 78 - return nil, err 79 - } 80 - 81 - defer func() { _ = dir.Close() }() 82 - 83 - entries, err := dir.ReadDir(-1) 84 - if err != nil { 85 - return nil, err 86 - } 87 - 88 - candidates := make([]packCandidate, 0, len(entries)) 89 - for _, entry := range entries { 90 - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".idx") { 91 - continue 92 - } 93 - 94 - idxName := entry.Name() 95 - packName := strings.TrimSuffix(idxName, ".idx") + ".pack" 96 - 97 - packInfo, err := store.root.Stat(packName) 98 - if err != nil { 99 - if os.IsNotExist(err) { 100 - return nil, fmt.Errorf("objectstorer/packed: missing pack file for index %q", idxName) 101 - } 102 - 103 - return nil, err 104 - } 105 - 106 - candidates = append(candidates, packCandidate{ 107 - packName: packName, 108 - idxName: idxName, 109 - mtime: packInfo.ModTime().UnixNano(), 110 - }) 111 - } 112 - 113 - slices.SortFunc(candidates, func(a, b packCandidate) int { 114 - if a.mtime != b.mtime { 115 - if a.mtime > b.mtime { 116 - return -1 117 - } 118 - 119 - return 1 120 - } 121 - 122 - return strings.Compare(a.packName, b.packName) 123 - }) 124 - 125 - return candidates, nil 126 - }
-98
object/storer/packed/idx_open.go
··· 1 - package packed 2 - 3 - import ( 4 - "fmt" 5 - "os" 6 - "syscall" 7 - 8 - "codeberg.org/lindenii/furgit/internal/intconv" 9 - objectid "codeberg.org/lindenii/furgit/object/id" 10 - ) 11 - 12 - // openIndex returns one opened and parsed index, caching it by pack basename. 13 - func (store *Store) openIndex(candidate packCandidate) (*idxFile, error) { 14 - store.idxMu.RLock() 15 - 16 - index, ok := store.idxByPack[candidate.packName] 17 - if ok { 18 - store.idxMu.RUnlock() 19 - 20 - return index, nil 21 - } 22 - 23 - store.idxMu.RUnlock() 24 - 25 - index, err := openIdxFile(store.root, candidate.idxName, candidate.packName, store.algo) 26 - if err != nil { 27 - return nil, err 28 - } 29 - 30 - store.idxMu.Lock() 31 - 32 - existing, ok := store.idxByPack[candidate.packName] 33 - if ok { 34 - store.idxMu.Unlock() 35 - 36 - _ = index.close() 37 - 38 - return existing, nil 39 - } 40 - 41 - store.idxByPack[candidate.packName] = index 42 - store.idxMu.Unlock() 43 - 44 - return index, nil 45 - } 46 - 47 - // openIdxFile maps and validates one idx v2 file. 48 - func openIdxFile(root *os.Root, idxName, packName string, algo objectid.Algorithm) (*idxFile, error) { 49 - file, err := root.Open(idxName) 50 - if err != nil { 51 - return nil, err 52 - } 53 - 54 - info, err := file.Stat() 55 - if err != nil { 56 - _ = file.Close() 57 - 58 - return nil, err 59 - } 60 - 61 - size := info.Size() 62 - if size < 0 || size > int64(int(^uint(0)>>1)) { 63 - _ = file.Close() 64 - 65 - return nil, fmt.Errorf("objectstorer/packed: idx %q has unsupported size", idxName) 66 - } 67 - 68 - fd, err := intconv.UintptrToInt(file.Fd()) 69 - if err != nil { 70 - _ = file.Close() 71 - 72 - return nil, err 73 - } 74 - 75 - data, err := syscall.Mmap(fd, 0, int(size), syscall.PROT_READ, syscall.MAP_PRIVATE) 76 - if err != nil { 77 - _ = file.Close() 78 - 79 - return nil, err 80 - } 81 - 82 - index := &idxFile{ 83 - idxName: idxName, 84 - packName: packName, 85 - algo: algo, 86 - file: file, 87 - data: data, 88 - } 89 - 90 - err = index.parse() 91 - if err != nil { 92 - _ = index.close() 93 - 94 - return nil, err 95 - } 96 - 97 - return index, nil 98 - }
-78
object/storer/packed/idx_parse.go
··· 1 - package packed 2 - 3 - import ( 4 - "encoding/binary" 5 - "fmt" 6 - ) 7 - 8 - const ( 9 - idxMagicV2 = 0xff744f63 10 - idxVersionV2 = 2 11 - ) 12 - 13 - // parse validates mapped idx v2 structure and stores table boundaries. 14 - func (index *idxFile) parse() error { 15 - hashSize := index.algo.Size() 16 - if hashSize <= 0 { 17 - return fmt.Errorf("objectstorer/packed: idx %q has invalid hash algorithm", index.idxName) 18 - } 19 - 20 - minLen := 8 + 256*4 + 2*hashSize 21 - if len(index.data) < minLen { 22 - return fmt.Errorf("objectstorer/packed: idx %q too short", index.idxName) 23 - } 24 - 25 - if binary.BigEndian.Uint32(index.data[:4]) != idxMagicV2 { 26 - return fmt.Errorf("objectstorer/packed: idx %q invalid magic", index.idxName) 27 - } 28 - 29 - if binary.BigEndian.Uint32(index.data[4:8]) != idxVersionV2 { 30 - return fmt.Errorf("objectstorer/packed: idx %q unsupported version", index.idxName) 31 - } 32 - 33 - prev := uint32(0) 34 - 35 - for i := range 256 { 36 - base := 8 + i*4 37 - 38 - cur := binary.BigEndian.Uint32(index.data[base : base+4]) 39 - if cur < prev { 40 - return fmt.Errorf("objectstorer/packed: idx %q has non-monotonic fanout table", index.idxName) 41 - } 42 - 43 - index.fanout[i] = cur 44 - prev = cur 45 - } 46 - 47 - index.numObjects = int(index.fanout[255]) 48 - if index.numObjects < 0 { 49 - return fmt.Errorf("objectstorer/packed: idx %q has invalid object count", index.idxName) 50 - } 51 - 52 - namesBytes := index.numObjects * hashSize 53 - crcBytes := index.numObjects * 4 54 - offset32Bytes := index.numObjects * 4 55 - 56 - minSize := 8 + 256*4 + namesBytes + crcBytes + offset32Bytes + 2*hashSize 57 - if minSize < 0 || len(index.data) < minSize { 58 - return fmt.Errorf("objectstorer/packed: idx %q has truncated tables", index.idxName) 59 - } 60 - 61 - index.namesOffset = 8 + 256*4 62 - index.offset32Offset = index.namesOffset + namesBytes + crcBytes 63 - index.offset64Offset = index.offset32Offset + offset32Bytes 64 - 65 - offset64Bytes := len(index.data) - index.offset64Offset - 2*hashSize 66 - if offset64Bytes < 0 || offset64Bytes%8 != 0 { 67 - return fmt.Errorf("objectstorer/packed: idx %q has malformed 64-bit offset table", index.idxName) 68 - } 69 - 70 - index.offset64Count = offset64Bytes / 8 71 - 72 - maxOffset64Count := max(index.numObjects-1, 0) 73 - if index.offset64Count > maxOffset64Count { 74 - return fmt.Errorf("objectstorer/packed: idx %q has oversized 64-bit offset table", index.idxName) 75 - } 76 - 77 - return nil 78 - }
-7
object/storer/packed/location.go
··· 1 - package packed 2 - 3 - // location identifies one object entry in a specific pack file. 4 - type location struct { 5 - packName string 6 - offset uint64 7 - }
-31
object/storer/packed/new.go
··· 1 - package packed 2 - 3 - import ( 4 - "fmt" 5 - "os" 6 - 7 - objectid "codeberg.org/lindenii/furgit/object/id" 8 - ) 9 - 10 - // New creates a packed-object store rooted at an objects/pack directory. 11 - func New(root *os.Root, algo objectid.Algorithm, opts Options) (*Store, error) { 12 - if algo.Size() == 0 { 13 - return nil, objectid.ErrInvalidAlgorithm 14 - } 15 - 16 - switch opts.RefreshPolicy { 17 - case RefreshPolicyOnMissing, RefreshPolicyNever: 18 - default: 19 - return nil, fmt.Errorf("objectstorer/packed: invalid refresh policy %d", opts.RefreshPolicy) 20 - } 21 - 22 - return &Store{ 23 - root: root, 24 - algo: algo, 25 - refreshPolicy: opts.RefreshPolicy, 26 - mruNodeByPack: make(map[string]*packCandidateNode), 27 - idxByPack: make(map[string]*idxFile), 28 - packs: make(map[string]*packFile), 29 - deltaCache: newDeltaCache(defaultDeltaCacheMaxBytes), 30 - }, nil 31 - }
-16
object/storer/packed/options.go
··· 1 - package packed 2 - 3 - // RefreshPolicy configures when candidate pack/index discovery refreshes. 4 - type RefreshPolicy uint8 5 - 6 - const ( 7 - // RefreshPolicyOnMissing refreshes candidates once after a lookup miss. 8 - RefreshPolicyOnMissing RefreshPolicy = iota 9 - // RefreshPolicyNever disables automatic refresh after lookup misses. 10 - RefreshPolicyNever 11 - ) 12 - 13 - // Options configures a packed object store. 14 - type Options struct { 15 - RefreshPolicy RefreshPolicy 16 - }
-82
object/storer/packed/pack.go
··· 1 - package packed 2 - 3 - import ( 4 - "encoding/binary" 5 - "fmt" 6 - "os" 7 - "syscall" 8 - 9 - packfmt "codeberg.org/lindenii/furgit/format/packfile" 10 - "codeberg.org/lindenii/furgit/internal/intconv" 11 - ) 12 - 13 - // packFile stores one mapped and validated .pack file. 14 - type packFile struct { 15 - // name is the .pack basename. 16 - name string 17 - // file is the opened pack file descriptor. 18 - file *os.File 19 - // data is the mapped pack bytes. 20 - data []byte 21 - } 22 - 23 - // openPackFile maps and validates one pack file. 24 - func openPackFile(name string, file *os.File, size int64) (*packFile, error) { 25 - if size < 12 { 26 - return nil, fmt.Errorf("objectstorer/packed: pack %q too short", name) 27 - } 28 - 29 - if size > int64(int(^uint(0)>>1)) { 30 - return nil, fmt.Errorf("objectstorer/packed: pack %q has unsupported size", name) 31 - } 32 - 33 - fd, err := intconv.UintptrToInt(file.Fd()) 34 - if err != nil { 35 - return nil, err 36 - } 37 - 38 - data, err := syscall.Mmap(fd, 0, int(size), syscall.PROT_READ, syscall.MAP_PRIVATE) 39 - if err != nil { 40 - return nil, err 41 - } 42 - 43 - if binary.BigEndian.Uint32(data[:4]) != packfmt.Signature { 44 - _ = syscall.Munmap(data) 45 - 46 - return nil, fmt.Errorf("objectstorer/packed: pack %q invalid signature", name) 47 - } 48 - 49 - version := binary.BigEndian.Uint32(data[4:8]) 50 - if !packfmt.VersionSupported(version) { 51 - _ = syscall.Munmap(data) 52 - 53 - return nil, fmt.Errorf("objectstorer/packed: pack %q unsupported version %d", name, version) 54 - } 55 - 56 - return &packFile{name: name, file: file, data: data}, nil 57 - } 58 - 59 - // close unmaps and closes one pack handle. 60 - func (pack *packFile) close() error { 61 - var closeErr error 62 - 63 - if pack.data != nil { 64 - err := syscall.Munmap(pack.data) 65 - if err != nil && closeErr == nil { 66 - closeErr = err 67 - } 68 - 69 - pack.data = nil 70 - } 71 - 72 - if pack.file != nil { 73 - err := pack.file.Close() 74 - if err != nil && closeErr == nil { 75 - closeErr = err 76 - } 77 - 78 - pack.file = nil 79 - } 80 - 81 - return closeErr 82 - }
-34
object/storer/packed/pack_idx_checksum.go
··· 1 - package packed 2 - 3 - import ( 4 - "bytes" 5 - "fmt" 6 - 7 - objectid "codeberg.org/lindenii/furgit/object/id" 8 - ) 9 - 10 - // verifyMappedPackMatchesMappedIdx compares one mapped pack trailer hash with 11 - // the pack hash recorded in one mapped idx trailer. 12 - func verifyMappedPackMatchesMappedIdx(packData, idxData []byte, algo objectid.Algorithm) error { 13 - hashSize := algo.Size() 14 - if hashSize <= 0 { 15 - return objectid.ErrInvalidAlgorithm 16 - } 17 - 18 - if len(packData) < hashSize { 19 - return fmt.Errorf("objectstorer/packed: pack too short for trailer hash") 20 - } 21 - 22 - if len(idxData) < hashSize*2 { 23 - return fmt.Errorf("objectstorer/packed: idx too short for trailer hashes") 24 - } 25 - 26 - packTrailerHash := packData[len(packData)-hashSize:] 27 - 28 - idxPackHash := idxData[len(idxData)-hashSize*2 : len(idxData)-hashSize] 29 - if !bytes.Equal(packTrailerHash, idxPackHash) { 30 - return fmt.Errorf("objectstorer/packed: pack hash does not match idx") 31 - } 32 - 33 - return nil 34 - }
-38
object/storer/packed/read_bytes.go
··· 1 - package packed 2 - 3 - import ( 4 - "fmt" 5 - 6 - objectheader "codeberg.org/lindenii/furgit/object/header" 7 - objectid "codeberg.org/lindenii/furgit/object/id" 8 - objecttype "codeberg.org/lindenii/furgit/object/type" 9 - ) 10 - 11 - // ReadBytesContent reads an object's type and content bytes. 12 - func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { 13 - loc, err := store.lookup(id) 14 - if err != nil { 15 - return objecttype.TypeInvalid, nil, err 16 - } 17 - 18 - return store.deltaResolveContent(loc) 19 - } 20 - 21 - // ReadBytesFull reads a full serialized object as "type size\0content". 22 - func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { 23 - ty, content, err := store.ReadBytesContent(id) 24 - if err != nil { 25 - return nil, err 26 - } 27 - 28 - header, ok := objectheader.Encode(ty, int64(len(content))) 29 - if !ok { 30 - return nil, fmt.Errorf("objectstorer/packed: failed to encode object header for type %d", ty) 31 - } 32 - 33 - out := make([]byte, len(header)+len(content)) 34 - copy(out, header) 35 - copy(out[len(header):], content) 36 - 37 - return out, nil 38 - }
-19
object/storer/packed/read_closer.go
··· 1 - package packed 2 - 3 - import "io" 4 - 5 - // readCloser proxies reads and closes one underlying closer. 6 - type readCloser struct { 7 - reader io.Reader 8 - closer io.Closer 9 - } 10 - 11 - // Read proxies reads to the underlying reader. 12 - func (reader *readCloser) Read(dst []byte) (int, error) { 13 - return reader.reader.Read(dst) 14 - } 15 - 16 - // Close closes the underlying closer. 17 - func (reader *readCloser) Close() error { 18 - return reader.closer.Close() 19 - }
-20
object/storer/packed/read_header.go
··· 1 - package packed 2 - 3 - import ( 4 - objectid "codeberg.org/lindenii/furgit/object/id" 5 - objecttype "codeberg.org/lindenii/furgit/object/type" 6 - ) 7 - 8 - // ReadHeader reads an object's type and declared content size. 9 - // 10 - // It resolves header metadata only. It does not verify that the full pack entry 11 - // payload is readable and does not verify any zlib Adler-32 trailer for 12 - // compressed entry data. 13 - func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { 14 - loc, err := store.lookup(id) 15 - if err != nil { 16 - return objecttype.TypeInvalid, 0, err 17 - } 18 - 19 - return store.resolveHeaderAt(loc) 20 - }
-66
object/storer/packed/read_header_resolve.go
··· 1 - package packed 2 - 3 - import ( 4 - "fmt" 5 - 6 - packfmt "codeberg.org/lindenii/furgit/format/packfile" 7 - objecttype "codeberg.org/lindenii/furgit/object/type" 8 - ) 9 - 10 - // resolveHeaderAt resolves one object's canonical type and declared content size. 11 - func (store *Store) resolveHeaderAt(start location) (objecttype.Type, int64, error) { 12 - visited := make(map[location]struct{}) 13 - current := start 14 - declaredSize := int64(-1) 15 - 16 - for { 17 - if _, ok := visited[current]; ok { 18 - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstorer/packed: delta cycle while resolving object header") 19 - } 20 - 21 - visited[current] = struct{}{} 22 - 23 - pack, meta, err := store.entryMetaAt(current) 24 - if err != nil { 25 - return objecttype.TypeInvalid, 0, err 26 - } 27 - 28 - if declaredSize < 0 { 29 - if packfmt.IsBaseObjectType(meta.ty) { 30 - declaredSize = meta.size 31 - } else { 32 - size, err := deltaDeclaredSizeAt(pack, meta.dataOffset) 33 - if err != nil { 34 - return objecttype.TypeInvalid, 0, err 35 - } 36 - 37 - declaredSize = size 38 - } 39 - } 40 - 41 - if packfmt.IsBaseObjectType(meta.ty) { 42 - return meta.ty, declaredSize, nil 43 - } 44 - 45 - switch meta.ty { 46 - case objecttype.TypeRefDelta: 47 - next, err := store.lookup(meta.baseRefID) 48 - if err != nil { 49 - return objecttype.TypeInvalid, 0, err 50 - } 51 - 52 - current = next 53 - case objecttype.TypeOfsDelta: 54 - current = location{ 55 - packName: current.packName, 56 - offset: meta.baseOfs, 57 - } 58 - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: 59 - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstorer/packed: internal invariant violation for base type %d", meta.ty) 60 - case objecttype.TypeInvalid, objecttype.TypeFuture: 61 - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstorer/packed: unsupported pack type %d", meta.ty) 62 - default: 63 - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstorer/packed: unsupported pack type %d", meta.ty) 64 - } 65 - } 66 - }
-103
object/storer/packed/read_reader.go
··· 1 - package packed 2 - 3 - import ( 4 - "bytes" 5 - "fmt" 6 - "io" 7 - 8 - packfmt "codeberg.org/lindenii/furgit/format/packfile" 9 - "codeberg.org/lindenii/furgit/internal/iolimit" 10 - objectheader "codeberg.org/lindenii/furgit/object/header" 11 - objectid "codeberg.org/lindenii/furgit/object/id" 12 - objecttype "codeberg.org/lindenii/furgit/object/type" 13 - ) 14 - 15 - // ReadReaderContent reads an object's type, declared content size, and content 16 - // stream. 17 - // 18 - // The caller must close the returned reader. 19 - // 20 - // For base pack entries, the returned reader borrows store-owned mapped pack 21 - // data and is only valid until the store is closed. 22 - // 23 - // Close releases reader-local resources only. It does not drain unread data for 24 - // additional validation. In particular, malformed trailing compressed data, 25 - // trailing bytes past the declared object size, and the zlib Adler-32 trailer 26 - // may go unverified unless the caller reads to io.EOF. 27 - func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { 28 - loc, err := store.lookup(id) 29 - if err != nil { 30 - return objecttype.TypeInvalid, 0, nil, err 31 - } 32 - 33 - pack, meta, err := store.entryMetaAt(loc) 34 - if err != nil { 35 - return objecttype.TypeInvalid, 0, nil, err 36 - } 37 - 38 - if packfmt.IsBaseObjectType(meta.ty) { 39 - zr, err := zlibReaderAt(pack, meta.dataOffset) 40 - if err != nil { 41 - return objecttype.TypeInvalid, 0, nil, err 42 - } 43 - 44 - return meta.ty, meta.size, &readCloser{ 45 - reader: iolimit.ExpectLengthReader(zr, meta.size), 46 - closer: zr, 47 - }, nil 48 - } 49 - 50 - ty, content, err := store.deltaResolveContent(loc) 51 - if err != nil { 52 - return objecttype.TypeInvalid, 0, nil, err 53 - } 54 - 55 - return ty, int64(len(content)), io.NopCloser(bytes.NewReader(content)), nil 56 - } 57 - 58 - // ReadReaderFull reads a full serialized object stream as "type size\0content". 59 - // 60 - // The caller must close the returned reader. 61 - // 62 - // For base pack entries, the returned reader borrows store-owned mapped pack 63 - // data and is only valid until the store is closed. 64 - // 65 - // Close releases reader-local resources only. It does not drain unread data for 66 - // additional validation. In particular, malformed trailing compressed data, 67 - // trailing bytes past the declared object size, and the zlib Adler-32 trailer 68 - // may go unverified unless the caller reads to io.EOF. 69 - func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { 70 - loc, err := store.lookup(id) 71 - if err != nil { 72 - return nil, err 73 - } 74 - 75 - pack, meta, err := store.entryMetaAt(loc) 76 - if err != nil { 77 - return nil, err 78 - } 79 - 80 - if packfmt.IsBaseObjectType(meta.ty) { 81 - header, ok := objectheader.Encode(meta.ty, meta.size) 82 - if !ok { 83 - return nil, fmt.Errorf("objectstorer/packed: failed to encode object header for type %d", meta.ty) 84 - } 85 - 86 - zr, err := zlibReaderAt(pack, meta.dataOffset) 87 - if err != nil { 88 - return nil, err 89 - } 90 - 91 - return &readCloser{ 92 - reader: io.MultiReader(bytes.NewReader(header), iolimit.ExpectLengthReader(zr, meta.size)), 93 - closer: zr, 94 - }, nil 95 - } 96 - 97 - raw, err := store.ReadBytesFull(id) 98 - if err != nil { 99 - return nil, err 100 - } 101 - 102 - return io.NopCloser(bytes.NewReader(raw)), nil 103 - }
-46
object/storer/packed/read_size.go
··· 1 - package packed 2 - 3 - import ( 4 - "fmt" 5 - 6 - packfmt "codeberg.org/lindenii/furgit/format/packfile" 7 - objectid "codeberg.org/lindenii/furgit/object/id" 8 - objecttype "codeberg.org/lindenii/furgit/object/type" 9 - ) 10 - 11 - // ReadSize reads an object's declared content size. 12 - // 13 - // Like ReadHeader, it resolves header metadata only. It does not verify that 14 - // the full pack entry payload is readable and does not verify any zlib 15 - // Adler-32 trailer for compressed entry data. 16 - func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) { 17 - loc, err := store.lookup(id) 18 - if err != nil { 19 - return 0, err 20 - } 21 - 22 - return store.resolveSizeAt(loc) 23 - } 24 - 25 - // resolveSizeAt resolves one object's declared content size from location. 26 - func (store *Store) resolveSizeAt(start location) (int64, error) { 27 - pack, meta, err := store.entryMetaAt(start) 28 - if err != nil { 29 - return 0, err 30 - } 31 - 32 - if packfmt.IsBaseObjectType(meta.ty) { 33 - return meta.size, nil 34 - } 35 - 36 - switch meta.ty { 37 - case objecttype.TypeRefDelta, objecttype.TypeOfsDelta: 38 - return deltaDeclaredSizeAt(pack, meta.dataOffset) 39 - case objecttype.TypeInvalid, objecttype.TypeFuture: 40 - return 0, fmt.Errorf("objectstorer/packed: unsupported pack type %d", meta.ty) 41 - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: 42 - return 0, fmt.Errorf("objectstorer/packed: internal invariant violation for base type %d", meta.ty) 43 - default: 44 - return 0, fmt.Errorf("objectstorer/packed: unsupported pack type %d", meta.ty) 45 - } 46 - }
-301
object/storer/packed/read_test.go
··· 1 - package packed_test 2 - 3 - import ( 4 - "bytes" 5 - "errors" 6 - "fmt" 7 - "io/fs" 8 - "strconv" 9 - "strings" 10 - "testing" 11 - 12 - "codeberg.org/lindenii/furgit/internal/testgit" 13 - objectid "codeberg.org/lindenii/furgit/object/id" 14 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 15 - "codeberg.org/lindenii/furgit/object/storer/packed" 16 - ) 17 - 18 - func TestPackedStoreReadAgainstGit(t *testing.T) { 19 - t.Parallel() 20 - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 21 - testRepo, ids := createPackedFixtureRepo(t, algo) 22 - store := openPackedStore(t, testRepo, algo) 23 - 24 - for _, id := range ids { 25 - t.Run(id.String(), func(t *testing.T) { 26 - wantType, wantBody, wantRaw := expectedRawObject(t, testRepo, id) 27 - 28 - gotHeaderType, gotHeaderSize, err := store.ReadHeader(id) 29 - if err != nil { 30 - t.Fatalf("ReadHeader: %v", err) 31 - } 32 - 33 - if gotHeaderType != wantType { 34 - t.Fatalf("ReadHeader type = %v, want %v", gotHeaderType, wantType) 35 - } 36 - 37 - if gotHeaderSize != int64(len(wantBody)) { 38 - t.Fatalf("ReadHeader size = %d, want %d", gotHeaderSize, len(wantBody)) 39 - } 40 - 41 - gotSize, err := store.ReadSize(id) 42 - if err != nil { 43 - t.Fatalf("ReadSize: %v", err) 44 - } 45 - 46 - if gotSize != int64(len(wantBody)) { 47 - t.Fatalf("ReadSize = %d, want %d", gotSize, len(wantBody)) 48 - } 49 - 50 - gotRaw, err := store.ReadBytesFull(id) 51 - if err != nil { 52 - t.Fatalf("ReadBytesFull: %v", err) 53 - } 54 - 55 - if !bytes.Equal(gotRaw, wantRaw) { 56 - t.Fatalf("ReadBytesFull mismatch") 57 - } 58 - 59 - gotType, gotBody, err := store.ReadBytesContent(id) 60 - if err != nil { 61 - t.Fatalf("ReadBytesContent: %v", err) 62 - } 63 - 64 - if gotType != wantType { 65 - t.Fatalf("ReadBytesContent type = %v, want %v", gotType, wantType) 66 - } 67 - 68 - if !bytes.Equal(gotBody, wantBody) { 69 - t.Fatalf("ReadBytesContent mismatch") 70 - } 71 - 72 - fullReader, err := store.ReadReaderFull(id) 73 - if err != nil { 74 - t.Fatalf("ReadReaderFull: %v", err) 75 - } 76 - 77 - got := mustReadAllAndClose(t, fullReader) 78 - if !bytes.Equal(got, wantRaw) { 79 - t.Fatalf("ReadReaderFull mismatch") 80 - } 81 - 82 - contentType, contentSize, contentReader, err := store.ReadReaderContent(id) 83 - if err != nil { 84 - t.Fatalf("ReadReaderContent: %v", err) 85 - } 86 - 87 - if contentType != wantType { 88 - t.Fatalf("ReadReaderContent type = %v, want %v", contentType, wantType) 89 - } 90 - 91 - if contentSize != int64(len(wantBody)) { 92 - t.Fatalf("ReadReaderContent size = %d, want %d", contentSize, len(wantBody)) 93 - } 94 - 95 - got = mustReadAllAndClose(t, contentReader) 96 - if !bytes.Equal(got, wantBody) { 97 - t.Fatalf("ReadReaderContent mismatch") 98 - } 99 - }) 100 - } 101 - }) 102 - } 103 - 104 - func TestPackedStoreErrors(t *testing.T) { 105 - t.Parallel() 106 - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 107 - testRepo, _ := createPackedFixtureRepo(t, algo) 108 - store := openPackedStore(t, testRepo, algo) 109 - 110 - notFoundID, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen())) 111 - if err != nil { 112 - t.Fatalf("ParseHex(notFound): %v", err) 113 - } 114 - 115 - _, err = store.ReadBytesFull(notFoundID) 116 - if !errors.Is(err, objectstorer.ErrObjectNotFound) { 117 - t.Fatalf("ReadBytesFull not-found error = %v", err) 118 - } 119 - 120 - _, _, err = store.ReadBytesContent(notFoundID) 121 - if !errors.Is(err, objectstorer.ErrObjectNotFound) { 122 - t.Fatalf("ReadBytesContent not-found error = %v", err) 123 - } 124 - 125 - _, err = store.ReadReaderFull(notFoundID) 126 - if !errors.Is(err, objectstorer.ErrObjectNotFound) { 127 - t.Fatalf("ReadReaderFull not-found error = %v", err) 128 - } 129 - 130 - _, _, _, err = store.ReadReaderContent(notFoundID) 131 - if !errors.Is(err, objectstorer.ErrObjectNotFound) { 132 - t.Fatalf("ReadReaderContent not-found error = %v", err) 133 - } 134 - 135 - _, _, err = store.ReadHeader(notFoundID) 136 - if !errors.Is(err, objectstorer.ErrObjectNotFound) { 137 - t.Fatalf("ReadHeader not-found error = %v", err) 138 - } 139 - 140 - _, err = store.ReadSize(notFoundID) 141 - if !errors.Is(err, objectstorer.ErrObjectNotFound) { 142 - t.Fatalf("ReadSize not-found error = %v", err) 143 - } 144 - 145 - var otherAlgo objectid.Algorithm 146 - 147 - for _, candidate := range objectid.SupportedAlgorithms() { 148 - if candidate != algo { 149 - otherAlgo = candidate 150 - 151 - break 152 - } 153 - } 154 - 155 - if otherAlgo != objectid.AlgorithmUnknown { 156 - mismatchID, err := objectid.ParseHex(otherAlgo, strings.Repeat("0", otherAlgo.HexLen())) 157 - if err != nil { 158 - t.Fatalf("ParseHex(mismatch): %v", err) 159 - } 160 - 161 - _, err = store.ReadBytesFull(mismatchID) 162 - if err == nil || !strings.Contains(err.Error(), "algorithm mismatch") { 163 - t.Fatalf("ReadBytesFull algorithm-mismatch error = %v", err) 164 - } 165 - } 166 - }) 167 - } 168 - 169 - func TestPackedStoreNewValidation(t *testing.T) { 170 - t.Parallel() 171 - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 172 - testRepo, _ := createPackedFixtureRepo(t, algo) 173 - 174 - store := openPackedStore(t, testRepo, algo) 175 - 176 - err := store.Close() 177 - if err != nil { 178 - t.Fatalf("Close: %v", err) 179 - } 180 - }) 181 - } 182 - 183 - func TestPackedStoreInvalidAlgorithm(t *testing.T) { 184 - t.Parallel() 185 - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectid.AlgorithmSHA1, Bare: true}) 186 - 187 - root := testRepo.OpenPackRoot(t) 188 - 189 - _, err := packed.New(root, objectid.AlgorithmUnknown, packed.Options{}) 190 - if !errors.Is(err, objectid.ErrInvalidAlgorithm) { 191 - t.Fatalf("packed.New invalid algorithm error = %v", err) 192 - } 193 - } 194 - 195 - func TestPackedStoreReadHeaderUsesResolvedObjectSizeForDelta(t *testing.T) { 196 - t.Parallel() 197 - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 198 - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 199 - 200 - var parent objectid.ObjectID 201 - 202 - for i := range 96 { 203 - content := strings.Repeat("common-line-"+strconv.Itoa(i%7)+"\n", 384) + fmt.Sprintf("tail-%03d\n", i) 204 - 205 - _, treeID := testRepo.MakeSingleFileTree(t, "file.txt", []byte(content)) 206 - if i == 0 { 207 - parent = testRepo.CommitTree(t, treeID, "delta-header-size-0") 208 - 209 - continue 210 - } 211 - 212 - parent = testRepo.CommitTree(t, treeID, fmt.Sprintf("delta-header-size-%03d", i), parent) 213 - } 214 - 215 - testRepo.UpdateRef(t, "refs/heads/main", parent) 216 - testRepo.Repack(t, "-a", "-d", "-f", "--window=128", "--depth=128") 217 - 218 - deltaID, wantResolvedSize := findDeltaObjectWithResolvedSizeMismatch(t, testRepo, algo) 219 - store := openPackedStore(t, testRepo, algo) 220 - 221 - _, gotSize, err := store.ReadHeader(deltaID) 222 - if err != nil { 223 - t.Fatalf("ReadHeader(%s): %v", deltaID, err) 224 - } 225 - 226 - if gotSize != wantResolvedSize { 227 - t.Fatalf("ReadHeader(%s) size = %d, want resolved size %d", deltaID, gotSize, wantResolvedSize) 228 - } 229 - 230 - gotReadSize, err := store.ReadSize(deltaID) 231 - if err != nil { 232 - t.Fatalf("ReadSize(%s): %v", deltaID, err) 233 - } 234 - 235 - if gotReadSize != wantResolvedSize { 236 - t.Fatalf("ReadSize(%s) = %d, want resolved size %d", deltaID, gotReadSize, wantResolvedSize) 237 - } 238 - }) 239 - } 240 - 241 - func findDeltaObjectWithResolvedSizeMismatch(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) (objectid.ObjectID, int64) { 242 - t.Helper() 243 - 244 - packRoot := testRepo.OpenPackRoot(t) 245 - 246 - entries, err := fs.ReadDir(packRoot.FS(), ".") 247 - if err != nil { 248 - t.Fatalf("ReadDir(pack): %v", err) 249 - } 250 - 251 - var idxName string 252 - 253 - for _, entry := range entries { 254 - if strings.HasSuffix(entry.Name(), ".idx") { 255 - idxName = entry.Name() 256 - 257 - break 258 - } 259 - } 260 - 261 - if idxName == "" { 262 - t.Fatalf("no idx files found") 263 - } 264 - 265 - verifyOut := testRepo.Run(t, "verify-pack", "-v", "objects/pack/"+idxName) 266 - for line := range strings.SplitSeq(strings.TrimSpace(verifyOut), "\n") { 267 - fields := strings.Fields(line) 268 - if len(fields) < 7 { 269 - continue 270 - } 271 - 272 - idHex := fields[0] 273 - 274 - deltaStreamSize, err := strconv.ParseInt(fields[2], 10, 64) 275 - if err != nil { 276 - continue 277 - } 278 - 279 - resolvedSizeStr := testRepo.Run(t, "cat-file", "-s", idHex) 280 - 281 - resolvedSize, err := strconv.ParseInt(strings.TrimSpace(resolvedSizeStr), 10, 64) 282 - if err != nil { 283 - t.Fatalf("parse cat-file size for %s: %v", idHex, err) 284 - } 285 - 286 - if deltaStreamSize == resolvedSize { 287 - continue 288 - } 289 - 290 - id, err := objectid.ParseHex(algo, idHex) 291 - if err != nil { 292 - t.Fatalf("ParseHex(%s): %v", idHex, err) 293 - } 294 - 295 - return id, resolvedSize 296 - } 297 - 298 - t.Fatalf("did not find a delta object with mismatched stream/resolved size") 299 - 300 - return objectid.ObjectID{}, 0 301 - }
-51
object/storer/packed/store.go
··· 1 - // Package packed provides packfile reading and associated indexes. 2 - package packed 3 - 4 - import ( 5 - "os" 6 - "sync" 7 - "sync/atomic" 8 - 9 - objectid "codeberg.org/lindenii/furgit/object/id" 10 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 11 - ) 12 - 13 - // Store reads Git objects from pack/index files under an objects/pack root. 14 - // 15 - // Store borrows its root. Cached pack/index mappings are retained until Close. 16 - type Store struct { 17 - // root is the borrowed objects/pack capability used for all file access. 18 - root *os.Root 19 - // algo is the expected object ID algorithm for lookups. 20 - algo objectid.Algorithm 21 - // refreshPolicy controls automatic candidate refresh on lookup misses. 22 - refreshPolicy RefreshPolicy 23 - 24 - // candidates stores the latest immutable candidate snapshot. 25 - candidates atomic.Pointer[candidateSnapshot] 26 - // refreshMu serializes candidate refresh. 27 - refreshMu sync.Mutex 28 - // mruMu guards candidate MRU linked-list state. 29 - mruMu sync.RWMutex 30 - // mruHead is the first pack in MRU order. 31 - mruHead *packCandidateNode 32 - // mruTail is the last pack in MRU order. 33 - mruTail *packCandidateNode 34 - // mruNodeByPack maps pack basename to MRU node. 35 - mruNodeByPack map[string]*packCandidateNode 36 - // idxByPack caches opened and parsed indexes by pack basename. 37 - idxByPack map[string]*idxFile 38 - 39 - // stateMu guards pack cache and close state. 40 - stateMu sync.RWMutex 41 - // idxMu guards parsed index cache. 42 - idxMu sync.RWMutex 43 - // cacheMu guards delta cache operations. 44 - cacheMu sync.RWMutex 45 - // packs caches opened .pack handles by basename. 46 - packs map[string]*packFile 47 - // deltaCache caches resolved base objects by pack location. 48 - deltaCache *deltaCache 49 - } 50 - 51 - var _ objectstorer.Store = (*Store)(nil)
-106
object/storer/packed/store_lookup.go
··· 1 - package packed 2 - 3 - import ( 4 - "errors" 5 - 6 - objectid "codeberg.org/lindenii/furgit/object/id" 7 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 8 - ) 9 - 10 - // lookup resolves one object ID to its pack location. 11 - func (store *Store) lookup(id objectid.ObjectID) (location, error) { 12 - var zero location 13 - if id.Algorithm() != store.algo { 14 - return zero, errors.New("objectstorer/packed: object id algorithm mismatch") 15 - } 16 - 17 - snapshot, err := store.ensureCandidates() 18 - if err != nil { 19 - return zero, err 20 - } 21 - 22 - loc, ok, err := store.lookupInCandidates(id, snapshot) 23 - if err != nil { 24 - return zero, err 25 - } 26 - 27 - if ok { 28 - return loc, nil 29 - } 30 - 31 - if store.refreshPolicy == RefreshPolicyOnMissing { //nolint:nestif 32 - err = store.Refresh() 33 - if err != nil { 34 - return zero, err 35 - } 36 - 37 - refreshed := store.candidates.Load() 38 - if refreshed != nil && refreshed != snapshot { 39 - loc, ok, err = store.lookupInCandidates(id, refreshed) 40 - if err != nil { 41 - return zero, err 42 - } 43 - 44 - if ok { 45 - return loc, nil 46 - } 47 - } 48 - } 49 - 50 - return zero, objectstorer.ErrObjectNotFound 51 - } 52 - 53 - func (store *Store) lookupInCandidates( 54 - id objectid.ObjectID, 55 - snapshot *candidateSnapshot, 56 - ) (location, bool, error) { 57 - var zero location 58 - 59 - nextPackName := store.firstCandidatePackName(snapshot) 60 - for nextPackName != "" { 61 - candidate, ok := snapshot.candidateByPack[nextPackName] 62 - if !ok { 63 - nextPackName = store.firstCandidatePackName(snapshot) 64 - 65 - continue 66 - } 67 - 68 - nextPackName = store.nextCandidatePackName(candidate.packName, snapshot) 69 - 70 - index, err := store.openIndex(candidate) 71 - if err != nil { 72 - return zero, false, err 73 - } 74 - 75 - offset, ok, err := index.lookup(id) 76 - if err != nil { 77 - return zero, false, err 78 - } 79 - 80 - if ok { 81 - store.touchCandidate(candidate.packName) 82 - 83 - return location{packName: index.packName, offset: offset}, true, nil 84 - } 85 - } 86 - 87 - for _, candidate := range snapshot.candidates { 88 - index, err := store.openIndex(candidate) 89 - if err != nil { 90 - return zero, false, err 91 - } 92 - 93 - offset, ok, err := index.lookup(id) 94 - if err != nil { 95 - return zero, false, err 96 - } 97 - 98 - if ok { 99 - store.touchCandidate(candidate.packName) 100 - 101 - return location{packName: index.packName, offset: offset}, true, nil 102 - } 103 - } 104 - 105 - return zero, false, nil 106 - }
-57
object/storer/packed/store_open_pack.go
··· 1 - package packed 2 - 3 - // openPack returns one opened and validated pack handle. 4 - func (store *Store) openPack(name string) (*packFile, error) { 5 - store.stateMu.RLock() 6 - 7 - pack, ok := store.packs[name] 8 - if ok { 9 - store.stateMu.RUnlock() 10 - 11 - return pack, nil 12 - } 13 - 14 - store.stateMu.RUnlock() 15 - 16 - file, err := store.root.Open(name) 17 - if err != nil { 18 - return nil, err 19 - } 20 - 21 - info, err := file.Stat() 22 - if err != nil { 23 - _ = file.Close() 24 - 25 - return nil, err 26 - } 27 - 28 - pack, err = openPackFile(name, file, info.Size()) 29 - if err != nil { 30 - _ = file.Close() 31 - 32 - return nil, err 33 - } 34 - 35 - err = store.verifyPackMatchesIndexes(pack) 36 - if err != nil { 37 - _ = pack.close() 38 - 39 - return nil, err 40 - } 41 - 42 - store.stateMu.Lock() 43 - 44 - existing, ok := store.packs[name] 45 - if ok { 46 - store.stateMu.Unlock() 47 - 48 - _ = pack.close() 49 - 50 - return existing, nil 51 - } 52 - 53 - store.packs[name] = pack 54 - store.stateMu.Unlock() 55 - 56 - return pack, nil 57 - }
-29
object/storer/packed/trailer_match.go
··· 1 - package packed 2 - 3 - import "fmt" 4 - 5 - // verifyPackMatchesIndexes checks that one opened pack's trailer hash matches 6 - // every loaded index that references the same pack name. 7 - func (store *Store) verifyPackMatchesIndexes(pack *packFile) error { 8 - snapshot, err := store.ensureCandidates() 9 - if err != nil { 10 - return err 11 - } 12 - 13 - candidate, ok := snapshot.candidateByPack[pack.name] 14 - if !ok { 15 - return fmt.Errorf("objectstorer/packed: missing index for pack %q", pack.name) 16 - } 17 - 18 - index, err := store.openIndex(candidate) 19 - if err != nil { 20 - return err 21 - } 22 - 23 - err = verifyMappedPackMatchesMappedIdx(pack.data, index.data, store.algo) 24 - if err != nil { 25 - return fmt.Errorf("objectstorer/packed: pack %q does not match idx %q: %w", pack.name, index.idxName, err) 26 - } 27 - 28 - return nil 29 - }
+3 -3
reachability/helpers.go
··· 6 6 7 7 giterrors "codeberg.org/lindenii/furgit/errors" 8 8 objectid "codeberg.org/lindenii/furgit/object/id" 9 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 9 + objectstore "codeberg.org/lindenii/furgit/object/store" 10 10 objecttype "codeberg.org/lindenii/furgit/object/type" 11 11 ) 12 12 ··· 39 39 func (r *Reachability) readHeaderType(id objectid.ObjectID) (objecttype.Type, error) { 40 40 ty, _, err := r.store.ReadHeader(id) 41 41 if err != nil { 42 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 42 + if errors.Is(err, objectstore.ErrObjectNotFound) { 43 43 return objecttype.TypeInvalid, &giterrors.ObjectMissingError{OID: id} 44 44 } 45 45 ··· 61 61 func (r *Reachability) readBytesContent(id objectid.ObjectID) ([]byte, error) { 62 62 _, content, err := r.store.ReadBytesContent(id) 63 63 if err != nil { 64 - if errors.Is(err, objectstorer.ErrObjectNotFound) { 64 + if errors.Is(err, objectstore.ErrObjectNotFound) { 65 65 return nil, &giterrors.ObjectMissingError{OID: id} 66 66 } 67 67
+4 -4
reachability/reachability.go
··· 3 3 4 4 import ( 5 5 commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 6 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 6 + objectstore "codeberg.org/lindenii/furgit/object/store" 7 7 ) 8 8 9 9 // Reachability provides graph traversal over objects in one object store. 10 10 // 11 11 // It is not safe for concurrent use. 12 12 type Reachability struct { 13 - store objectstorer.Store 13 + store objectstore.Store 14 14 graph *commitgraphread.Reader 15 15 } 16 16 17 17 // New builds a Reachability over one object store. 18 - func New(store objectstorer.Store) *Reachability { 18 + func New(store objectstore.Store) *Reachability { 19 19 return &Reachability{store: store} 20 20 } 21 21 22 22 // NewWithCommitGraph builds a Reachability over one object store with an 23 23 // optional commit-graph reader for faster commit-domain traversal. 24 - func NewWithCommitGraph(store objectstorer.Store, graph *commitgraphread.Reader) *Reachability { 24 + func NewWithCommitGraph(store objectstore.Store, graph *commitgraphread.Reader) *Reachability { 25 25 return &Reachability{store: store, graph: graph} 26 26 }
+1 -1
reachability/unit_test.go
··· 10 10 giterrors "codeberg.org/lindenii/furgit/errors" 11 11 "codeberg.org/lindenii/furgit/internal/testgit" 12 12 objectid "codeberg.org/lindenii/furgit/object/id" 13 - "codeberg.org/lindenii/furgit/object/storer/memory" 13 + "codeberg.org/lindenii/furgit/object/store/memory" 14 14 "codeberg.org/lindenii/furgit/object/tree" 15 15 objecttype "codeberg.org/lindenii/furgit/object/type" 16 16 "codeberg.org/lindenii/furgit/reachability"
+1 -1
ref/store/errors.go
··· 3 3 import "errors" 4 4 5 5 // ErrReferenceNotFound indicates that a reference does not exist in a backend. 6 - // TODO: Interface error? Just like object not found in objectstorer. 6 + // TODO: Interface error? Just like object not found in objectstore. 7 7 var ErrReferenceNotFound = errors.New("refstore: reference not found")
+7 -7
repository/objects.go
··· 6 6 "os" 7 7 8 8 objectid "codeberg.org/lindenii/furgit/object/id" 9 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 10 - objectloose "codeberg.org/lindenii/furgit/object/storer/loose" 11 - objectmix "codeberg.org/lindenii/furgit/object/storer/mix" 12 - objectpacked "codeberg.org/lindenii/furgit/object/storer/packed" 9 + objectstore "codeberg.org/lindenii/furgit/object/store" 10 + objectloose "codeberg.org/lindenii/furgit/object/store/loose" 11 + objectmix "codeberg.org/lindenii/furgit/object/store/mix" 12 + objectpacked "codeberg.org/lindenii/furgit/object/store/packed" 13 13 ) 14 14 15 15 //nolint:ireturn ··· 17 17 root *os.Root, 18 18 algo objectid.Algorithm, 19 19 ) ( 20 - objects objectstorer.Store, 20 + objects objectstore.Store, 21 21 objectsRoot *os.Root, 22 22 objectsPackRoot *os.Root, 23 23 objectsLoose *objectloose.Store, ··· 36 36 return nil, nil, nil, nil, nil, err 37 37 } 38 38 39 - backends := []objectstorer.Store{objectsLoose} 39 + backends := []objectstore.Store{objectsLoose} 40 40 41 41 objectsPackRoot, err = objectsRoot.OpenRoot("pack") 42 42 if err == nil { ··· 73 73 // Close. 74 74 // 75 75 //nolint:ireturn 76 - func (repo *Repository) Objects() objectstorer.Store { 76 + func (repo *Repository) Objects() objectstore.Store { 77 77 return repo.objects 78 78 }
+4 -4
repository/repository.go
··· 6 6 7 7 "codeberg.org/lindenii/furgit/config" 8 8 objectid "codeberg.org/lindenii/furgit/object/id" 9 - objectstorer "codeberg.org/lindenii/furgit/object/storer" 10 - objectloose "codeberg.org/lindenii/furgit/object/storer/loose" 11 - objectpacked "codeberg.org/lindenii/furgit/object/storer/packed" 9 + objectstore "codeberg.org/lindenii/furgit/object/store" 10 + objectloose "codeberg.org/lindenii/furgit/object/store/loose" 11 + objectpacked "codeberg.org/lindenii/furgit/object/store/packed" 12 12 refstore "codeberg.org/lindenii/furgit/ref/store" 13 13 ) 14 14 ··· 25 25 config *config.Config 26 26 algo objectid.Algorithm 27 27 28 - objects objectstorer.Store 28 + objects objectstore.Store 29 29 objectsRoot *os.Root 30 30 objectsPackRoot *os.Root 31 31 objectsLoose *objectloose.Store
+1 -1
repository/write_loose.go
··· 1 1 package repository 2 2 3 3 import ( 4 - objectloose "codeberg.org/lindenii/furgit/object/storer/loose" 4 + objectloose "codeberg.org/lindenii/furgit/object/store/loose" 5 5 ) 6 6 7 7 // LooseStoreForWriting returns the repository's loose-object writer.