aboutsummaryrefslogtreecommitdiff
path: root/loose.go
diff options
context:
space:
mode:
authorGravatar Runxi Yu2025-11-14 00:00:00 +0000
committerGravatar Runxi Yu2025-11-14 00:00:00 +0000
commit9ef659a016d4ffeac931291984a4c71f9527a747 (patch)
tree957a76630fe248b638c0a9c84f7acef40a7ee9f5 /loose.go
parentInitial commit (diff)
Read types and sizes without inflating entire object
Diffstat (limited to 'loose.go')
-rw-r--r--loose.go111
1 files changed, 89 insertions, 22 deletions
diff --git a/loose.go b/loose.go
index 78c483c7..c32311f5 100644
--- a/loose.go
+++ b/loose.go
@@ -10,6 +10,8 @@ import (
"strconv"
)
+const looseHeaderLimit = 4096
+
func loosePath(id Hash) string {
hex := id.String()
return filepath.Join("objects", hex[:2], hex[2:])
@@ -53,30 +55,11 @@ func (repo *Repository) looseReadTyped(id Hash) (ObjType, []byte, error) {
header := raw[:nul]
body := raw[nul+1:]
- space := bytes.IndexByte(header, ' ')
- if space < 0 {
- return ObjInvalid, nil, ErrInvalidObject
- }
- tyStr := string(header[:space])
- var ty ObjType
- switch tyStr {
- case "blob":
- ty = ObjBlob
- case "tree":
- ty = ObjTree
- case "commit":
- ty = ObjCommit
- case "tag":
- ty = ObjTag
- default:
- return ObjInvalid, nil, ErrInvalidObject
- }
- expect := header[space+1:]
- size, err := strconv.Atoi(string(expect))
+ ty, declaredSize, err := parseLooseHeader(header)
if err != nil {
- return ObjInvalid, nil, fmt.Errorf("furgit: loose: size parse: %w", err)
+ return ObjInvalid, nil, err
}
- if size != len(body) {
+ if declaredSize != int64(len(body)) {
return ObjInvalid, nil, ErrInvalidObject
}
if !verifyRawObject(raw, id) {
@@ -86,3 +69,87 @@ func (repo *Repository) looseReadTyped(id Hash) (ObjType, []byte, error) {
out := append([]byte(nil), body...)
return ty, out, nil
}
+
+func (repo *Repository) looseTypeSize(id Hash) (ObjType, int64, error) {
+ path := repo.repoPath(loosePath(id))
+ // #nosec G304
+ f, err := os.Open(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return ObjInvalid, 0, ErrNotFound
+ }
+ return ObjInvalid, 0, err
+ }
+ defer func() { _ = f.Close() }()
+
+ zr, err := zlib.NewReader(f)
+ if err != nil {
+ return ObjInvalid, 0, err
+ }
+ defer func() { _ = zr.Close() }()
+
+ header := make([]byte, 0, 64)
+ chunk := make([]byte, 128)
+ for {
+ n, readErr := zr.Read(chunk)
+ if n > 0 {
+ data := chunk[:n]
+ if nul := bytes.IndexByte(data, 0); nul >= 0 {
+ header = append(header, data[:nul]...)
+ if len(header) > looseHeaderLimit {
+ return ObjInvalid, 0, ErrInvalidObject
+ }
+ break
+ }
+ header = append(header, data...)
+ if len(header) > looseHeaderLimit {
+ return ObjInvalid, 0, ErrInvalidObject
+ }
+ }
+ if readErr != nil {
+ if readErr == io.EOF {
+ return ObjInvalid, 0, ErrInvalidObject
+ }
+ return ObjInvalid, 0, readErr
+ }
+ }
+ return parseLooseHeader(header)
+}
+
+func parseLooseHeader(header []byte) (ObjType, int64, error) {
+ space := bytes.IndexByte(header, ' ')
+ if space < 0 {
+ return ObjInvalid, 0, ErrInvalidObject
+ }
+ ty, err := objTypeFromName(string(header[:space]))
+ if err != nil {
+ return ObjInvalid, 0, err
+ }
+ expect := header[space+1:]
+ if len(expect) == 0 {
+ return ObjInvalid, 0, ErrInvalidObject
+ }
+ size, err := strconv.ParseInt(string(expect), 10, 64)
+ if err != nil {
+ return ObjInvalid, 0, fmt.Errorf("furgit: loose: size parse: %w", err)
+ }
+ if size < 0 {
+ return ObjInvalid, 0, ErrInvalidObject
+ }
+ return ty, size, nil
+}
+
+func objTypeFromName(name string) (ObjType, error) {
+ switch name {
+ case objNameBlob:
+ return ObjBlob, nil
+ case objNameTree:
+ return ObjTree, nil
+ case objNameCommit:
+ return ObjCommit, nil
+ case objNameTag:
+ return ObjTag, nil
+ default:
+ return ObjInvalid, ErrInvalidObject
+ }
+}