aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--objectstore/loose/parse.go45
-rw-r--r--objectstore/loose/paths.go41
-rw-r--r--objectstore/loose/read_bytes.go29
-rw-r--r--objectstore/loose/read_header.go30
-rw-r--r--objectstore/loose/read_reader.go84
-rw-r--r--objectstore/loose/store.go40
6 files changed, 269 insertions, 0 deletions
diff --git a/objectstore/loose/parse.go b/objectstore/loose/parse.go
new file mode 100644
index 00000000..30c7e251
--- /dev/null
+++ b/objectstore/loose/parse.go
@@ -0,0 +1,45 @@
+package loose
+
+import (
+ "bufio"
+ "compress/zlib"
+ "errors"
+ "io"
+ "os"
+
+ "codeberg.org/lindenii/furgit/objectheader"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+// decodeAll inflates the full loose object payload from file.
+func decodeAll(file *os.File) ([]byte, error) {
+ zr, err := zlib.NewReader(file)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = zr.Close() }()
+ return io.ReadAll(zr)
+}
+
+// parseRaw parses a loose object payload in "type size\0content" format.
+func parseRaw(raw []byte) (objecttype.Type, []byte, error) {
+ ty, _, headerLen, ok := objectheader.Parse(raw)
+ if !ok {
+ return objecttype.TypeInvalid, nil, errors.New("objectstore/loose: malformed object header")
+ }
+ return ty, raw[headerLen:], nil
+}
+
+// readHeader reads and parses a loose object header from br.
+// br must be positioned at the start of decoded loose object bytes.
+func readHeader(br *bufio.Reader) (objecttype.Type, int64, error) {
+ header, err := br.ReadSlice(0)
+ if err != nil {
+ return objecttype.TypeInvalid, 0, err
+ }
+ ty, size, _, ok := objectheader.Parse(header)
+ if !ok {
+ return objecttype.TypeInvalid, 0, errors.New("objectstore/loose: malformed object header")
+ }
+ return ty, size, nil
+}
diff --git a/objectstore/loose/paths.go b/objectstore/loose/paths.go
new file mode 100644
index 00000000..ee761f39
--- /dev/null
+++ b/objectstore/loose/paths.go
@@ -0,0 +1,41 @@
+package loose
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objectstore"
+)
+
+// objectPath returns the loose object path for id.
+func (store *Store) objectPath(id objectid.ObjectID) (string, error) {
+ if id.Algorithm() != store.algo {
+ return "", fmt.Errorf("objectstore/loose: object id algorithm mismatch: got %s want %s", id.Algorithm(), store.algo)
+ }
+ hex := id.String()
+ if len(hex) != store.algo.HexLen() {
+ return "", fmt.Errorf("objectstore/loose: malformed object id %q", hex)
+ }
+ return path.Join("objects", hex[:2], hex[2:]), nil
+}
+
+// openObject opens the loose object file for id.
+// Missing files cause objectstore.ErrObjectNotFound.
+func (store *Store) openObject(id objectid.ObjectID) (*os.File, error) {
+ relPath, err := store.objectPath(id)
+ if err != nil {
+ return nil, err
+ }
+ file, err := store.root.Open(relPath)
+ if err != nil {
+ if errors.Is(err, fs.ErrNotExist) {
+ return nil, objectstore.ErrObjectNotFound
+ }
+ return nil, err
+ }
+ return file, nil
+}
diff --git a/objectstore/loose/read_bytes.go b/objectstore/loose/read_bytes.go
new file mode 100644
index 00000000..4d1cb439
--- /dev/null
+++ b/objectstore/loose/read_bytes.go
@@ -0,0 +1,29 @@
+package loose
+
+import (
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+// ReadBytesFull reads a full serialized object as "type size\0content".
+func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) {
+ file, err := store.openObject(id)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = file.Close() }()
+ return decodeAll(file)
+}
+
+// ReadBytesContent reads an object's type and content bytes.
+func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) {
+ raw, err := store.ReadBytesFull(id)
+ if err != nil {
+ return objecttype.TypeInvalid, nil, err
+ }
+ ty, content, err := parseRaw(raw)
+ if err != nil {
+ return objecttype.TypeInvalid, nil, err
+ }
+ return ty, content, nil
+}
diff --git a/objectstore/loose/read_header.go b/objectstore/loose/read_header.go
new file mode 100644
index 00000000..0fa587fc
--- /dev/null
+++ b/objectstore/loose/read_header.go
@@ -0,0 +1,30 @@
+package loose
+
+import (
+ "bufio"
+ "compress/zlib"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+// ReadHeader reads an object's type and declared content length.
+func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) {
+ file, err := store.openObject(id)
+ if err != nil {
+ return objecttype.TypeInvalid, 0, err
+ }
+ defer func() { _ = file.Close() }()
+
+ zr, err := zlib.NewReader(file)
+ if err != nil {
+ return objecttype.TypeInvalid, 0, err
+ }
+ defer func() { _ = zr.Close() }()
+
+ ty, size, err := readHeader(bufio.NewReader(zr))
+ if err != nil {
+ return objecttype.TypeInvalid, 0, err
+ }
+ return ty, size, nil
+}
diff --git a/objectstore/loose/read_reader.go b/objectstore/loose/read_reader.go
new file mode 100644
index 00000000..dde0de07
--- /dev/null
+++ b/objectstore/loose/read_reader.go
@@ -0,0 +1,84 @@
+package loose
+
+import (
+ "bufio"
+ "compress/zlib"
+ "errors"
+ "io"
+ "os"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+type objectReader struct {
+ // reader is the stream exposed by Read. It may be the raw zlib reader
+ // (full object) or a buffered reader positioned at content bytes only.
+ reader io.Reader
+ // file is the underlying loose object file and is closed by Close.
+ file *os.File
+ // zr is the zlib decoder and is closed by Close.
+ zr io.ReadCloser
+}
+
+func (reader *objectReader) Read(dst []byte) (int, error) {
+ return reader.reader.Read(dst)
+}
+
+func (reader *objectReader) Close() error {
+ errZlib := reader.zr.Close()
+ errFile := reader.file.Close()
+ return errors.Join(errZlib, errFile)
+}
+
+// openInflated opens and zlib-decodes a loose object file.
+// The caller owns both returned closers and must close them.
+func (store *Store) openInflated(id objectid.ObjectID) (*os.File, io.ReadCloser, error) {
+ file, err := store.openObject(id)
+ if err != nil {
+ return nil, nil, err
+ }
+ zr, err := zlib.NewReader(file)
+ if err != nil {
+ _ = file.Close()
+ return nil, nil, err
+ }
+ return file, zr, nil
+}
+
+// ReadReaderFull reads a full serialized object stream as "type size\\x00content".
+// The caller must close the returned reader.
+func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) {
+ file, zr, err := store.openInflated(id)
+ if err != nil {
+ return nil, err
+ }
+ return &objectReader{
+ reader: zr,
+ file: file,
+ zr: zr,
+ }, nil
+}
+
+// ReadReaderContent reads an object's type, declared content length, and content stream.
+// The caller must close the returned reader.
+func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) {
+ file, zr, err := store.openInflated(id)
+ if err != nil {
+ return objecttype.TypeInvalid, 0, nil, err
+ }
+
+ br := bufio.NewReader(zr)
+ ty, size, err := readHeader(br)
+ if err != nil {
+ _ = zr.Close()
+ _ = file.Close()
+ return objecttype.TypeInvalid, 0, nil, err
+ }
+
+ return ty, size, &objectReader{
+ reader: br,
+ file: file,
+ zr: zr,
+ }, nil
+}
diff --git a/objectstore/loose/store.go b/objectstore/loose/store.go
new file mode 100644
index 00000000..1b1b1d0b
--- /dev/null
+++ b/objectstore/loose/store.go
@@ -0,0 +1,40 @@
+// Package loose provides loose-object reads from a repository root.
+package loose
+
+import (
+ "errors"
+ "os"
+
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// Store reads loose Git objects from a repository root.
+//
+// Store does not own root. Callers are responsible for closing root.
+type Store struct {
+ // root is the repository root capability used for all object file access.
+ // Store does not own this root.
+ root *os.Root
+ // algo is the expected object ID algorithm for lookups.
+ algo objectid.Algorithm
+}
+
+// New creates a loose-object store rooted at root for algo.
+func New(root *os.Root, algo objectid.Algorithm) (*Store, error) {
+ if root == nil {
+ return nil, errors.New("objectstore/loose: nil root")
+ }
+ if algo.Size() == 0 {
+ return nil, objectid.ErrInvalidAlgorithm
+ }
+ return &Store{
+ root: root,
+ algo: algo,
+ }, nil
+}
+
+// Close releases resources associated with the backend.
+func (store *Store) Close() error {
+ _ = store
+ return nil
+}