diff options
| author | 2026-02-21 03:46:50 +0800 | |
|---|---|---|
| committer | 2026-02-21 03:46:50 +0800 | |
| commit | 27ee98e7a9a8693db59e755739e10c1c1ba852b4 (patch) | |
| tree | 2b69d6912851d36b06c0a795391120d204605ffe | |
| parent | objectstore: ReadReaderContent should have an advisory declared length (diff) | |
| signature | No signature | |
objectstore/loose: Add loose backend
| -rw-r--r-- | objectstore/loose/parse.go | 45 | ||||
| -rw-r--r-- | objectstore/loose/paths.go | 41 | ||||
| -rw-r--r-- | objectstore/loose/read_bytes.go | 29 | ||||
| -rw-r--r-- | objectstore/loose/read_header.go | 30 | ||||
| -rw-r--r-- | objectstore/loose/read_reader.go | 84 | ||||
| -rw-r--r-- | objectstore/loose/store.go | 40 |
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 +} |
