diff options
Diffstat (limited to 'objectstore')
| -rw-r--r-- | objectstore/loose/write_bytes.go | 32 | ||||
| -rw-r--r-- | objectstore/loose/write_reader.go | 70 | ||||
| -rw-r--r-- | objectstore/loose/write_test.go | 111 | ||||
| -rw-r--r-- | objectstore/loose/write_writer.go | 45 |
4 files changed, 97 insertions, 161 deletions
diff --git a/objectstore/loose/write_bytes.go b/objectstore/loose/write_bytes.go index 1f7ab59f..247173fb 100644 --- a/objectstore/loose/write_bytes.go +++ b/objectstore/loose/write_bytes.go @@ -7,38 +7,12 @@ import ( "codeberg.org/lindenii/furgit/objecttype" ) -// WriteBytesFull writes a full serialized object as "type size\\x00content". +// WriteBytesFull writes a full serialized object as "type size\0content". func (store *Store) WriteBytesFull(raw []byte) (objectid.ObjectID, error) { - var zero objectid.ObjectID - - writer, finalize, err := store.WriteWriterFull() - if err != nil { - return zero, err - } - if _, err := bytes.NewReader(raw).WriteTo(writer); err != nil { - _ = writer.Close() - return zero, err - } - if err := writer.Close(); err != nil { - return zero, err - } - return finalize() + return store.WriteReaderFull(bytes.NewReader(raw)) } // WriteBytesContent writes typed content bytes as a loose object. func (store *Store) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) { - var zero objectid.ObjectID - - writer, finalize, err := store.WriteWriterContent(ty, int64(len(content))) - if err != nil { - return zero, err - } - if _, err := bytes.NewReader(content).WriteTo(writer); err != nil { - _ = writer.Close() - return zero, err - } - if err := writer.Close(); err != nil { - return zero, err - } - return finalize() + return store.WriteReaderContent(ty, int64(len(content)), bytes.NewReader(content)) } diff --git a/objectstore/loose/write_reader.go b/objectstore/loose/write_reader.go new file mode 100644 index 00000000..b2329f02 --- /dev/null +++ b/objectstore/loose/write_reader.go @@ -0,0 +1,70 @@ +package loose + +import ( + "fmt" + "io" + + "codeberg.org/lindenii/furgit/objectheader" + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/objecttype" +) + +// WriteReaderContent writes one loose object from typed content bytes read from src. +// src must provide exactly size bytes. +// size is required because loose object headers are "type size\0content", so the +// header must be emitted before streaming content without buffering. +func (store *Store) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) { + if size < 0 { + return objectid.ObjectID{}, fmt.Errorf("objectstore/loose: negative content size: %d", size) + } + + header, ok := objectheader.Encode(ty, size) + if !ok { + return objectid.ObjectID{}, fmt.Errorf("objectstore/loose: failed to encode object header for type %v", ty) + } + + writer, err := store.newStreamWriter(false) + if err != nil { + return objectid.ObjectID{}, err + } + writer.headerDone = true + writer.expectedContentLeft = size + + if err := writer.writeRawChunk(header); err != nil { + _ = writer.Close() + _ = store.root.Remove(writer.tmpRelPath) + return objectid.ObjectID{}, err + } + + return writeReaderIntoStreamWriter(writer, src) +} + +// WriteReaderFull writes one loose object from raw bytes "type size\0content" +// read from src. +func (store *Store) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) { + writer, err := store.newStreamWriter(true) + if err != nil { + return objectid.ObjectID{}, err + } + return writeReaderIntoStreamWriter(writer, src) +} + +// writeReaderIntoStreamWriter copies src into writer and publishes the object. +func writeReaderIntoStreamWriter(writer *streamWriter, src io.Reader) (objectid.ObjectID, error) { + if _, err := io.Copy(writer, src); err != nil { + _ = writer.Close() + _ = writer.store.root.Remove(writer.tmpRelPath) + return objectid.ObjectID{}, err + } + if err := writer.Close(); err != nil { + _ = writer.store.root.Remove(writer.tmpRelPath) + return objectid.ObjectID{}, err + } + + id, err := writer.finalize() + if err != nil { + _ = writer.store.root.Remove(writer.tmpRelPath) + return objectid.ObjectID{}, err + } + return id, nil +} diff --git a/objectstore/loose/write_test.go b/objectstore/loose/write_test.go index 411868a6..cceabe5a 100644 --- a/objectstore/loose/write_test.go +++ b/objectstore/loose/write_test.go @@ -2,7 +2,6 @@ package loose_test import ( "bytes" - "io" "testing" "codeberg.org/lindenii/furgit/internal/testgit" @@ -11,35 +10,25 @@ import ( "codeberg.org/lindenii/furgit/objecttype" ) -func TestLooseStoreWriteWriterContentAgainstGit(t *testing.T) { +func TestLooseStoreWriteReaderContentAgainstGit(t *testing.T) { t.Parallel() testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) store := openLooseStore(t, testRepo.Dir(), algo) - content := []byte("written-by-content-writer\n") + content := []byte("written-by-content-reader\n") expectedHex := testRepo.RunInput(t, content, "hash-object", "-t", "blob", "--stdin") expectedID, err := objectid.ParseHex(algo, expectedHex) if err != nil { t.Fatalf("ParseHex(expected): %v", err) } - writer, finalize, err := store.WriteWriterContent(objecttype.TypeBlob, int64(len(content))) + writtenID, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content)) if err != nil { - t.Fatalf("WriteWriterContent: %v", err) - } - if _, err := io.Copy(writer, bytes.NewReader(content)); err != nil { - t.Fatalf("WriteWriterContent write: %v", err) - } - if err := writer.Close(); err != nil { - t.Fatalf("WriteWriterContent close: %v", err) - } - writtenID, err := finalize() - if err != nil { - t.Fatalf("WriteWriterContent finalize: %v", err) + t.Fatalf("WriteReaderContent: %v", err) } if writtenID != expectedID { - t.Fatalf("WriteWriterContent id = %s, want %s", writtenID, expectedID) + t.Fatalf("WriteReaderContent id = %s, want %s", writtenID, expectedID) } gotBody := testRepo.CatFile(t, "blob", writtenID) @@ -48,33 +37,23 @@ func TestLooseStoreWriteWriterContentAgainstGit(t *testing.T) { } // Writing the same object again should succeed and return the same ID. - writer, finalize, err = store.WriteWriterContent(objecttype.TypeBlob, int64(len(content))) + writtenID2, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content)) if err != nil { - t.Fatalf("WriteWriterContent second: %v", err) - } - if _, err := io.Copy(writer, bytes.NewReader(content)); err != nil { - t.Fatalf("WriteWriterContent second write: %v", err) - } - if err := writer.Close(); err != nil { - t.Fatalf("WriteWriterContent second close: %v", err) - } - writtenID2, err := finalize() - if err != nil { - t.Fatalf("WriteWriterContent second finalize: %v", err) + t.Fatalf("WriteReaderContent second: %v", err) } if writtenID2 != expectedID { - t.Fatalf("WriteWriterContent second id = %s, want %s", writtenID2, expectedID) + t.Fatalf("WriteReaderContent second id = %s, want %s", writtenID2, expectedID) } }) } -func TestLooseStoreWriteWriterFullAgainstGit(t *testing.T) { +func TestLooseStoreWriteReaderFullAgainstGit(t *testing.T) { t.Parallel() testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) store := openLooseStore(t, testRepo.Dir(), algo) - body := []byte("full-writer-body\n") + body := []byte("full-reader-body\n") header, ok := objectheader.Encode(objecttype.TypeBlob, int64(len(body))) if !ok { t.Fatalf("objectheader.Encode failed") @@ -84,22 +63,12 @@ func TestLooseStoreWriteWriterFullAgainstGit(t *testing.T) { copy(raw[len(header):], body) wantID := algo.Sum(raw) - writer, finalize, err := store.WriteWriterFull() - if err != nil { - t.Fatalf("WriteWriterFull: %v", err) - } - if _, err := io.Copy(writer, bytes.NewReader(raw)); err != nil { - t.Fatalf("WriteWriterFull write: %v", err) - } - if err := writer.Close(); err != nil { - t.Fatalf("WriteWriterFull close: %v", err) - } - gotID, err := finalize() + gotID, err := store.WriteReaderFull(bytes.NewReader(raw)) if err != nil { - t.Fatalf("WriteWriterFull finalize: %v", err) + t.Fatalf("WriteReaderFull: %v", err) } if gotID != wantID { - t.Fatalf("WriteWriterFull id = %s, want %s", gotID, wantID) + t.Fatalf("WriteReaderFull id = %s, want %s", gotID, wantID) } gotBody := testRepo.CatFile(t, "blob", gotID) @@ -109,7 +78,7 @@ func TestLooseStoreWriteWriterFullAgainstGit(t *testing.T) { }) } -func TestLooseStoreWriterValidationErrors(t *testing.T) { +func TestLooseStoreReaderValidationErrors(t *testing.T) { t.Parallel() testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper t.Run("content overflow", func(t *testing.T) { @@ -117,16 +86,8 @@ func TestLooseStoreWriterValidationErrors(t *testing.T) { testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) store := openLooseStore(t, testRepo.Dir(), algo) - writer, finalize, err := store.WriteWriterContent(objecttype.TypeBlob, 1) - if err != nil { - t.Fatalf("WriteWriterContent: %v", err) - } - if _, err := writer.Write([]byte("hello")); err == nil { - t.Fatalf("expected overflow error") - } - _ = writer.Close() - if _, err := finalize(); err == nil { - t.Fatalf("expected finalize error after overflow") + if _, err := store.WriteReaderContent(objecttype.TypeBlob, 1, bytes.NewReader([]byte("hello"))); err == nil { + t.Fatalf("expected error after overflow") } }) @@ -135,18 +96,8 @@ func TestLooseStoreWriterValidationErrors(t *testing.T) { testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) store := openLooseStore(t, testRepo.Dir(), algo) - writer, finalize, err := store.WriteWriterContent(objecttype.TypeBlob, 5) - if err != nil { - t.Fatalf("WriteWriterContent: %v", err) - } - if _, err := writer.Write([]byte("x")); err != nil { - t.Fatalf("write short: %v", err) - } - if err := writer.Close(); err != nil { - t.Fatalf("close short: %v", err) - } - if _, err := finalize(); err == nil { - t.Fatalf("expected finalize error for short content") + if _, err := store.WriteReaderContent(objecttype.TypeBlob, 5, bytes.NewReader([]byte("x"))); err == nil { + t.Fatalf("expected error for short content") } }) @@ -155,18 +106,8 @@ func TestLooseStoreWriterValidationErrors(t *testing.T) { testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) store := openLooseStore(t, testRepo.Dir(), algo) - writer, finalize, err := store.WriteWriterFull() - if err != nil { - t.Fatalf("WriteWriterFull: %v", err) - } - if _, err := writer.Write([]byte("not-a-header")); err != nil { - t.Fatalf("write malformed header bytes unexpectedly failed: %v", err) - } - if err := writer.Close(); err != nil { - t.Fatalf("close malformed header: %v", err) - } - if _, err := finalize(); err == nil { - t.Fatalf("expected finalize error for malformed header") + if _, err := store.WriteReaderFull(bytes.NewReader([]byte("not-a-header"))); err == nil { + t.Fatalf("expected error for malformed header") } }) @@ -175,17 +116,9 @@ func TestLooseStoreWriterValidationErrors(t *testing.T) { testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) store := openLooseStore(t, testRepo.Dir(), algo) - writer, finalize, err := store.WriteWriterFull() - if err != nil { - t.Fatalf("WriteWriterFull: %v", err) - } raw := []byte("blob 1\x00hello") - if _, err := io.Copy(writer, bytes.NewReader(raw)); err == nil { - t.Fatalf("expected overflow error") - } - _ = writer.Close() - if _, err := finalize(); err == nil { - t.Fatalf("expected finalize error after mismatch") + if _, err := store.WriteReaderFull(bytes.NewReader(raw)); err == nil { + t.Fatalf("expected error after mismatch") } }) }) diff --git a/objectstore/loose/write_writer.go b/objectstore/loose/write_writer.go index e8f03f19..39834412 100644 --- a/objectstore/loose/write_writer.go +++ b/objectstore/loose/write_writer.go @@ -5,58 +5,17 @@ import ( "compress/zlib" "crypto/rand" "errors" - "fmt" "hash" - "io" "io/fs" "os" "path/filepath" "codeberg.org/lindenii/furgit/objectheader" "codeberg.org/lindenii/furgit/objectid" - "codeberg.org/lindenii/furgit/objecttype" ) const tempObjectFilePrefix = "tmp_obj_" -// WriteWriterContent returns a writer for object content bytes. -// The writer accepts exactly size bytes. After closing the writer, -// call finalize to atomically publish the loose object and get its ID. -func (store *Store) WriteWriterContent(ty objecttype.Type, size int64) (io.WriteCloser, func() (objectid.ObjectID, error), error) { - if size < 0 { - return nil, nil, errors.New("objectstore/loose: negative content size") - } - - header, ok := objectheader.Encode(ty, size) - if !ok { - return nil, nil, fmt.Errorf("objectstore/loose: failed to encode object header for type %d", ty) - } - - writer, err := store.newStreamWriter(false) - if err != nil { - return nil, nil, err - } - writer.headerDone = true - writer.expectedContentLeft = size - if err := writer.writeRawChunk(header); err != nil { - _ = writer.Close() - return nil, nil, err - } - - return writer, writer.Finalize, nil -} - -// WriteWriterFull returns a writer for full raw object bytes: -// "type size\0content". After closing the writer, call finalize -// to atomically publish the loose object and get its ID. -func (store *Store) WriteWriterFull() (io.WriteCloser, func() (objectid.ObjectID, error), error) { - writer, err := store.newStreamWriter(true) - if err != nil { - return nil, nil, err - } - return writer, writer.Finalize, nil -} - // streamWriter incrementally hashes and deflates an object into a temp file. // Finalize validates size accounting and atomically renames the temp file. type streamWriter struct { @@ -152,10 +111,10 @@ func (writer *streamWriter) Close() error { return errors.Join(errZlib, errSync, errFile) } -// Finalize validates write completeness and atomically publishes the object. +// finalize validates write completeness and atomically publishes the object. // Publication is no-clobber: it links tmpRelPath to the object path and treats // existing destination objects as success. -func (writer *streamWriter) Finalize() (objectid.ObjectID, error) { +func (writer *streamWriter) finalize() (objectid.ObjectID, error) { if writer.finalized { return writer.finalID, writer.finalErr } |
