diff options
| author | 2026-03-30 13:10:28 +0000 | |
|---|---|---|
| committer | 2026-03-30 13:10:28 +0000 | |
| commit | 0fe27421c7f37ba38445ead185580ca4e73b2664 (patch) | |
| tree | 0511effb432f51cc51e7a3f6eec032910aadabfb | |
| parent | object/store: Reorganize files (diff) | |
| signature | No signature | |
object/store/memory: I guess implement the ObjectWriter interface
| -rw-r--r-- | object/store/memory/add.go | 13 | ||||
| -rw-r--r-- | object/store/memory/write_bytes.go | 35 | ||||
| -rw-r--r-- | object/store/memory/write_reader.go | 55 | ||||
| -rw-r--r-- | object/store/memory/write_test.go | 170 |
4 files changed, 265 insertions, 8 deletions
diff --git a/object/store/memory/add.go b/object/store/memory/add.go index 3b27f52d..3946c89e 100644 --- a/object/store/memory/add.go +++ b/object/store/memory/add.go @@ -1,21 +1,18 @@ package memory import ( - objectheader "codeberg.org/lindenii/furgit/object/header" objectid "codeberg.org/lindenii/furgit/object/id" objecttype "codeberg.org/lindenii/furgit/object/type" ) // AddObject stores one object body and returns its object ID. +// +//go:fix inline func (store *Store) AddObject(ty objecttype.Type, body []byte) objectid.ObjectID { - header, ok := objectheader.Encode(ty, int64(len(body))) - if !ok { - panic("failed to encode object header") + id, err := store.WriteBytesContent(ty, body) + if err != nil { + panic(err) } - raw := append(append([]byte(nil), header...), body...) - id := store.algo.Sum(raw) - store.objects[id] = storedObject{ty: ty, content: append([]byte(nil), body...)} - return id } diff --git a/object/store/memory/write_bytes.go b/object/store/memory/write_bytes.go new file mode 100644 index 00000000..241169d9 --- /dev/null +++ b/object/store/memory/write_bytes.go @@ -0,0 +1,35 @@ +package memory + +import ( + "bytes" + + objectheader "codeberg.org/lindenii/furgit/object/header" + objectid "codeberg.org/lindenii/furgit/object/id" + objecttype "codeberg.org/lindenii/furgit/object/type" +) + +// WriteBytesContent writes one typed object content byte slice. +func (store *Store) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) { + id := store.algo.Sum(buildRawObject(ty, content)) + store.objects[id] = storedObject{ty: ty, content: append([]byte(nil), content...)} + + return id, nil +} + +// WriteBytesFull writes one full serialized object byte slice as "type size\0content". +func (store *Store) WriteBytesFull(raw []byte) (objectid.ObjectID, error) { + return store.WriteReaderFull(bytes.NewReader(raw)) +} + +func buildRawObject(ty objecttype.Type, body []byte) []byte { + header, ok := objectheader.Encode(ty, int64(len(body))) + if !ok { + panic("failed to encode object header") + } + + raw := make([]byte, len(header)+len(body)) + copy(raw, header) + copy(raw[len(header):], body) + + return raw +} diff --git a/object/store/memory/write_reader.go b/object/store/memory/write_reader.go new file mode 100644 index 00000000..0fa6a13f --- /dev/null +++ b/object/store/memory/write_reader.go @@ -0,0 +1,55 @@ +package memory + +import ( + "errors" + "fmt" + "io" + + objectheader "codeberg.org/lindenii/furgit/object/header" + objectid "codeberg.org/lindenii/furgit/object/id" + objecttype "codeberg.org/lindenii/furgit/object/type" +) + +// WriteReaderContent writes one typed object content stream. +func (store *Store) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) { + if size < 0 { + return objectid.ObjectID{}, fmt.Errorf("objectstore/memory: negative content size: %d", size) + } + + content, err := io.ReadAll(io.LimitReader(src, size+1)) + if err != nil { + return objectid.ObjectID{}, err + } + + switch { + case int64(len(content)) > size: + return objectid.ObjectID{}, errors.New("objectstore/memory: object content longer than declared size") + case int64(len(content)) < size: + return objectid.ObjectID{}, errors.New("objectstore/memory: object content shorter than declared size") + } + + return store.WriteBytesContent(ty, content) +} + +// WriteReaderFull writes one full serialized object stream as "type size\0content". +func (store *Store) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) { + raw, err := io.ReadAll(src) + if err != nil { + return objectid.ObjectID{}, err + } + + ty, size, headerLen, ok := objectheader.Parse(raw) + if !ok { + return objectid.ObjectID{}, errors.New("objectstore/memory: malformed object header") + } + + content := raw[headerLen:] + if int64(len(content)) != size { + return objectid.ObjectID{}, errors.New("objectstore/memory: object header size/content mismatch") + } + + id := store.algo.Sum(raw) + store.objects[id] = storedObject{ty: ty, content: append([]byte(nil), content...)} + + return id, nil +} diff --git a/object/store/memory/write_test.go b/object/store/memory/write_test.go new file mode 100644 index 00000000..5fe5f893 --- /dev/null +++ b/object/store/memory/write_test.go @@ -0,0 +1,170 @@ +package memory + +import ( + "bytes" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + objectheader "codeberg.org/lindenii/furgit/object/header" + objectid "codeberg.org/lindenii/furgit/object/id" + objecttype "codeberg.org/lindenii/furgit/object/type" +) + +func TestStoreWriteReaderContent(t *testing.T) { + t.Parallel() + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + store := New(algo) + content := []byte("memory-content\n") + + gotID, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content)) + if err != nil { + t.Fatalf("WriteReaderContent: %v", err) + } + + wantID := algo.Sum(buildRawObject(objecttype.TypeBlob, content)) + if gotID != wantID { + t.Fatalf("WriteReaderContent id = %s, want %s", gotID, wantID) + } + + gotType, gotContent, err := store.ReadBytesContent(gotID) + if err != nil { + t.Fatalf("ReadBytesContent: %v", err) + } + + if gotType != objecttype.TypeBlob { + t.Fatalf("ReadBytesContent type = %v, want %v", gotType, objecttype.TypeBlob) + } + + if !bytes.Equal(gotContent, content) { + t.Fatalf("ReadBytesContent content mismatch") + } + }) +} + +func TestStoreWriteReaderFull(t *testing.T) { + t.Parallel() + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + store := New(algo) + content := []byte("memory-full\n") + raw := buildRawObject(objecttype.TypeBlob, content) + + gotID, err := store.WriteReaderFull(bytes.NewReader(raw)) + if err != nil { + t.Fatalf("WriteReaderFull: %v", err) + } + + wantID := algo.Sum(raw) + if gotID != wantID { + t.Fatalf("WriteReaderFull id = %s, want %s", gotID, wantID) + } + + gotRaw, err := store.ReadBytesFull(gotID) + if err != nil { + t.Fatalf("ReadBytesFull: %v", err) + } + + if !bytes.Equal(gotRaw, raw) { + t.Fatalf("ReadBytesFull mismatch") + } + }) +} + +func TestStoreWriteBytes(t *testing.T) { + t.Parallel() + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + store := New(algo) + content := []byte("memory-bytes\n") + + gotID, err := store.WriteBytesContent(objecttype.TypeBlob, content) + if err != nil { + t.Fatalf("WriteBytesContent: %v", err) + } + + wantID := algo.Sum(buildRawObject(objecttype.TypeBlob, content)) + if gotID != wantID { + t.Fatalf("WriteBytesContent id = %s, want %s", gotID, wantID) + } + + raw := buildRawObject(objecttype.TypeBlob, content) + + gotID2, err := store.WriteBytesFull(raw) + if err != nil { + t.Fatalf("WriteBytesFull: %v", err) + } + + if gotID2 != wantID { + t.Fatalf("WriteBytesFull id = %s, want %s", gotID2, wantID) + } + }) +} + +func TestStoreWriteReaderValidationErrors(t *testing.T) { + t.Parallel() + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + t.Run("content overflow", func(t *testing.T) { + t.Parallel() + store := New(algo) + + _, err := store.WriteReaderContent(objecttype.TypeBlob, 1, bytes.NewReader([]byte("hello"))) + if err == nil { + t.Fatalf("expected error after overflow") + } + }) + + t.Run("content short", func(t *testing.T) { + t.Parallel() + store := New(algo) + + _, err := store.WriteReaderContent(objecttype.TypeBlob, 5, bytes.NewReader([]byte("x"))) + if err == nil { + t.Fatalf("expected error for short content") + } + }) + + t.Run("full malformed header", func(t *testing.T) { + t.Parallel() + store := New(algo) + + _, err := store.WriteReaderFull(bytes.NewReader([]byte("not-a-header"))) + if err == nil { + t.Fatalf("expected error for malformed header") + } + }) + + t.Run("full size mismatch", func(t *testing.T) { + t.Parallel() + store := New(algo) + + _, err := store.WriteReaderFull(bytes.NewReader([]byte("blob 1\x00hello"))) + if err == nil { + t.Fatalf("expected error after mismatch") + } + }) + + t.Run("bytes malformed header", func(t *testing.T) { + t.Parallel() + store := New(algo) + + _, err := store.WriteBytesFull([]byte("not-a-header")) + if err == nil { + t.Fatalf("expected error for malformed byte header") + } + }) + }) +} + +func TestBuildRawObjectMatchesObjectHeaderEncode(t *testing.T) { + t.Parallel() + + content := []byte("body") + raw := buildRawObject(objecttype.TypeBlob, content) + header, ok := objectheader.Encode(objecttype.TypeBlob, int64(len(content))) + if !ok { + t.Fatalf("objectheader.Encode failed") + } + + want := append(append([]byte(nil), header...), content...) + if !bytes.Equal(raw, want) { + t.Fatalf("buildRawObject mismatch") + } +} |
