aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--hash.go53
-rw-r--r--hash_test.go20
-rw-r--r--loose.go42
-rw-r--r--obj.go28
-rw-r--r--obj_blob.go2
-rw-r--r--obj_commit.go16
-rw-r--r--obj_tag.go12
-rw-r--r--obj_tree.go19
-rw-r--r--objects_test.go45
-rw-r--r--pack_idx.go9
-rw-r--r--pack_midx.go8
-rw-r--r--pack_pack.go10
-rw-r--r--pack_test.go6
-rw-r--r--refs.go4
-rw-r--r--repo.go49
-rw-r--r--repo_test.go26
16 files changed, 211 insertions, 138 deletions
diff --git a/hash.go b/hash.go
index 336d5322..1ca26391 100644
--- a/hash.go
+++ b/hash.go
@@ -4,53 +4,48 @@ import (
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
- "fmt"
)
const maxHashSize = 32
// Hash represents a Git object identifier.
-type Hash [maxHashSize]byte
+type Hash struct {
+ data [maxHashSize]byte
+ size int
+}
// hashFunc is a function that computes a hash from input data.
-type hashFunc func([]byte) [maxHashSize]byte
+type hashFunc func([]byte) Hash
// hashFuncs maps hash size to hash function.
var hashFuncs = map[int]hashFunc{
- sha1.Size: func(data []byte) [maxHashSize]byte {
- var result [maxHashSize]byte
+ sha1.Size: func(data []byte) Hash {
sum := sha1.Sum(data)
- copy(result[:], sum[:])
- return result
+ var h Hash
+ copy(h.data[:], sum[:])
+ h.size = sha1.Size
+ return h
},
- sha256.Size: func(data []byte) [maxHashSize]byte {
- var result [maxHashSize]byte
+ sha256.Size: func(data []byte) Hash {
sum := sha256.Sum256(data)
- copy(result[:], sum[:])
- return result
+ var h Hash
+ copy(h.data[:], sum[:])
+ h.size = sha256.Size
+ return h
},
}
-// ParseHashWithSize converts a hex string into a Hash for a given hash size.
-func ParseHashWithSize(s string, hashSize int) (Hash, error) {
- var id Hash
- if len(s) != hashSize*2 {
- return id, fmt.Errorf("furgit: invalid hash length %d", len(s))
- }
- data, err := hex.DecodeString(s)
- if err != nil {
- return id, fmt.Errorf("furgit: decode hash: %w", err)
- }
- copy(id[:], data)
- return id, nil
+// String returns the ID as hex using its internal size.
+func (id Hash) String() string {
+ return hex.EncodeToString(id.data[:id.size])
}
-// StringWithSize returns the ID as hex for a given hash size.
-func (id Hash) StringWithSize(hashSize int) string {
- return hex.EncodeToString(id[:hashSize])
+// Bytes returns a mutable copy of the underlying bytes using its internal size.
+func (id Hash) Bytes() []byte {
+ return append([]byte(nil), id.data[:id.size]...)
}
-// BytesWithSize returns a mutable copy of the underlying bytes for a given hash size.
-func (id Hash) BytesWithSize(hashSize int) []byte {
- return append([]byte(nil), id[:hashSize]...)
+// Size returns the hash size.
+func (id Hash) Size() int {
+ return id.size
}
diff --git a/hash_test.go b/hash_test.go
index 4b359c4a..89e66fd1 100644
--- a/hash_test.go
+++ b/hash_test.go
@@ -9,34 +9,36 @@ func TestParseHashValidAndInvalid(t *testing.T) {
pattern := "0123456789abcdef"
repeats := (testHashSize*2 + len(pattern) - 1) / len(pattern)
hexStr := strings.Repeat(pattern, repeats)[:testHashSize*2]
-
- id, err := ParseHashWithSize(hexStr, testHashSize)
+
+ repo := &Repository{HashSize: testHashSize}
+ id, err := repo.ParseHash(hexStr)
if err != nil {
t.Fatalf("ParseHash returned error: %v", err)
}
- if got := id.StringWithSize(testHashSize); got != hexStr {
+ if got := id.String(); got != hexStr {
t.Fatalf("unexpected String result: %q", got)
}
- if _, err := ParseHashWithSize("abcd", testHashSize); err == nil {
+ if _, err := repo.ParseHash("abcd"); err == nil {
t.Fatal("expected error for short hash")
}
badHex := strings.Repeat("z", testHashSize*2)
- if _, err := ParseHashWithSize(badHex, testHashSize); err == nil {
+ if _, err := repo.ParseHash(badHex); err == nil {
t.Fatal("expected error for non-hex input")
}
}
func TestHashBytesCopiesUnderlyingData(t *testing.T) {
var id Hash
- for i := range id {
- id[i] = byte(i)
+ for i := range id.data {
+ id.data[i] = byte(i)
}
- orig := id.BytesWithSize(testHashSize)
+ id.size = testHashSize
+ orig := id.Bytes()
orig[0] ^= 0xff
- if id[0] == orig[0] {
+ if id.data[0] == orig[0] {
t.Fatal("Bytes should return a copy")
}
}
diff --git a/loose.go b/loose.go
index c1371991..d79b2f48 100644
--- a/loose.go
+++ b/loose.go
@@ -12,9 +12,13 @@ import (
const looseHeaderLimit = 4096
-func loosePath(id Hash, hashSize int) string {
- hex := id.StringWithSize(hashSize)
- return filepath.Join("objects", hex[:2], hex[2:])
+// loosePath returns the path for a loose object, validating hash size.
+func (repo *Repository) loosePath(id Hash) (string, error) {
+ if id.size != repo.HashSize {
+ return "", fmt.Errorf("furgit: hash size mismatch: got %d, expected %d", id.size, repo.HashSize)
+ }
+ hex := id.String()
+ return filepath.Join("objects", hex[:2], hex[2:]), nil
}
func (repo *Repository) looseRead(id Hash) (Object, error) {
@@ -22,11 +26,15 @@ func (repo *Repository) looseRead(id Hash) (Object, error) {
if err != nil {
return nil, err
}
- return parseObjectBody(ty, id, body, repo.HashSize)
+ return parseObjectBody(ty, id, body, repo)
}
func (repo *Repository) looseReadTyped(id Hash) (ObjType, []byte, error) {
- path := repo.repoPath(loosePath(id, repo.HashSize))
+ path, err := repo.loosePath(id)
+ if err != nil {
+ return ObjInvalid, nil, err
+ }
+ path = repo.repoPath(path)
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
@@ -62,7 +70,7 @@ func (repo *Repository) looseReadTyped(id Hash) (ObjType, []byte, error) {
if declaredSize != int64(len(body)) {
return ObjInvalid, nil, ErrInvalidObject
}
- if !verifyRawObject(raw, id, repo.HashSize) {
+ if !repo.verifyRawObject(raw, id) {
return ObjInvalid, nil, ErrInvalidObject
}
@@ -71,7 +79,11 @@ func (repo *Repository) looseReadTyped(id Hash) (ObjType, []byte, error) {
}
func (repo *Repository) looseTypeSize(id Hash) (ObjType, int64, error) {
- path := repo.repoPath(loosePath(id, repo.HashSize))
+ path, err := repo.loosePath(id)
+ if err != nil {
+ return ObjInvalid, 0, err
+ }
+ path = repo.repoPath(path)
// #nosec G304
f, err := os.Open(path)
if err != nil {
@@ -161,13 +173,13 @@ func (repo *Repository) WriteLooseObject(obj Object) (Hash, error) {
switch o := obj.(type) {
case *Blob:
- raw, err = o.Serialize(repo.HashSize)
+ raw, err = o.Serialize()
case *Tree:
- raw, err = o.Serialize(repo.HashSize)
+ raw, err = o.Serialize()
case *Commit:
- raw, err = o.Serialize(repo.HashSize)
+ raw, err = o.Serialize()
case *Tag:
- raw, err = o.Serialize(repo.HashSize)
+ raw, err = o.Serialize()
default:
return Hash{}, fmt.Errorf("furgit: unsupported object type for writing: %T", obj)
}
@@ -177,8 +189,12 @@ func (repo *Repository) WriteLooseObject(obj Object) (Hash, error) {
return Hash{}, err
}
- id := computeRawHash(raw, repo.HashSize)
- path := repo.repoPath(loosePath(id, repo.HashSize))
+ id := repo.computeRawHash(raw)
+ path, err := repo.loosePath(id)
+ if err != nil {
+ return Hash{}, err
+ }
+ path = repo.repoPath(path)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return Hash{}, err
diff --git a/obj.go b/obj.go
index ce3d0258..983c7557 100644
--- a/obj.go
+++ b/obj.go
@@ -33,11 +33,6 @@ type Object interface {
ObjType() ObjType
}
-func computeRawHash(data []byte, hashSize int) Hash {
- hashFunc := hashFuncs[hashSize]
- return hashFunc(data)
-}
-
func headerForType(ty ObjType, body []byte) ([]byte, error) {
var tyStr string
switch ty {
@@ -64,31 +59,16 @@ func headerForType(ty ObjType, body []byte) ([]byte, error) {
return buf.Bytes(), nil
}
-func verifyRawObject(buf []byte, want Hash, hashSize int) bool {
- return computeRawHash(buf, hashSize) == want
-}
-
-func verifyTypedObject(ty ObjType, body []byte, want Hash, hashSize int) bool {
- header, err := headerForType(ty, body)
- if err != nil {
- return false
- }
- raw := make([]byte, len(header)+len(body))
- copy(raw, header)
- copy(raw[len(header):], body)
- return computeRawHash(raw, hashSize) == want
-}
-
-func parseObjectBody(ty ObjType, id Hash, body []byte, hashSize int) (Object, error) {
+func parseObjectBody(ty ObjType, id Hash, body []byte, repo *Repository) (Object, error) {
switch ty {
case ObjBlob:
return parseBlob(id, body)
case ObjTree:
- return parseTree(id, body, hashSize)
+ return parseTree(id, body, repo)
case ObjCommit:
- return parseCommit(id, body, hashSize)
+ return parseCommit(id, body, repo)
case ObjTag:
- return parseTag(id, body, hashSize)
+ return parseTag(id, body, repo)
case ObjInvalid, ObjFuture, ObjOfsDelta, ObjRefDelta:
return nil, fmt.Errorf("furgit: object: unsupported type %d", ty)
default:
diff --git a/obj_blob.go b/obj_blob.go
index 9edad0a9..5ae0c40e 100644
--- a/obj_blob.go
+++ b/obj_blob.go
@@ -21,7 +21,7 @@ func parseBlob(id Hash, body []byte) (*Blob, error) {
}
// Serialize renders the full "blob size\\0body" representation.
-func (b *Blob) Serialize(hashSize int) ([]byte, error) {
+func (b *Blob) Serialize() ([]byte, error) {
header, err := headerForType(ObjBlob, b.Data)
if err != nil {
return nil, err
diff --git a/obj_commit.go b/obj_commit.go
index 84de2c41..9ce52530 100644
--- a/obj_commit.go
+++ b/obj_commit.go
@@ -22,7 +22,7 @@ func (*Commit) ObjType() ObjType {
return ObjCommit
}
-func parseCommit(id Hash, body []byte, hashSize int) (*Commit, error) {
+func parseCommit(id Hash, body []byte, repo *Repository) (*Commit, error) {
c := new(Commit)
c.Hash = id
i := 0
@@ -39,13 +39,13 @@ func parseCommit(id Hash, body []byte, hashSize int) (*Commit, error) {
switch {
case bytes.HasPrefix(line, []byte("tree ")):
- treeID, err := ParseHashWithSize(string(line[5:]), hashSize)
+ treeID, err := repo.ParseHash(string(line[5:]))
if err != nil {
return nil, fmt.Errorf("furgit: commit: tree: %w", err)
}
c.Tree = treeID
case bytes.HasPrefix(line, []byte("parent ")):
- parent, err := ParseHashWithSize(string(line[7:]), hashSize)
+ parent, err := repo.ParseHash(string(line[7:]))
if err != nil {
return nil, fmt.Errorf("furgit: commit: parent: %w", err)
}
@@ -91,11 +91,11 @@ func parseCommit(id Hash, body []byte, hashSize int) (*Commit, error) {
return c, nil
}
-func commitBody(c *Commit, hashSize int) []byte {
+func commitBody(c *Commit) []byte {
var buf bytes.Buffer
- fmt.Fprintf(&buf, "tree %s\n", c.Tree.StringWithSize(hashSize))
+ fmt.Fprintf(&buf, "tree %s\n", c.Tree.String())
for _, p := range c.Parents {
- fmt.Fprintf(&buf, "parent %s\n", p.StringWithSize(hashSize))
+ fmt.Fprintf(&buf, "parent %s\n", p.String())
}
buf.WriteString("author ")
buf.Write(c.Author.Serialize())
@@ -110,8 +110,8 @@ func commitBody(c *Commit, hashSize int) []byte {
}
// Serialize renders a Commit into canonical Git format.
-func (c *Commit) Serialize(hashSize int) ([]byte, error) {
- body := commitBody(c, hashSize)
+func (c *Commit) Serialize() ([]byte, error) {
+ body := commitBody(c)
header, err := headerForType(ObjCommit, body)
if err != nil {
return nil, err
diff --git a/obj_tag.go b/obj_tag.go
index bf9ee97d..dd0dcad0 100644
--- a/obj_tag.go
+++ b/obj_tag.go
@@ -22,7 +22,7 @@ func (*Tag) ObjType() ObjType {
}
// parseTag parses a tag object body.
-func parseTag(id Hash, body []byte, hashSize int) (*Tag, error) {
+func parseTag(id Hash, body []byte, repo *Repository) (*Tag, error) {
t := new(Tag)
t.Hash = id
i := 0
@@ -41,7 +41,7 @@ func parseTag(id Hash, body []byte, hashSize int) (*Tag, error) {
switch {
case bytes.HasPrefix(line, []byte("object ")):
- hash, err := ParseHashWithSize(string(line[7:]), hashSize)
+ hash, err := repo.ParseHash(string(line[7:]))
if err != nil {
return nil, fmt.Errorf("furgit: tag: object: %w", err)
}
@@ -94,9 +94,9 @@ func parseTag(id Hash, body []byte, hashSize int) (*Tag, error) {
return t, nil
}
-func tagBody(t *Tag, hashSize int) ([]byte, error) {
+func tagBody(t *Tag) ([]byte, error) {
var buf bytes.Buffer
- fmt.Fprintf(&buf, "object %s\n", t.Target.StringWithSize(hashSize))
+ fmt.Fprintf(&buf, "object %s\n", t.Target.String())
buf.WriteString("type ")
switch t.TargetType {
case ObjCommit:
@@ -128,8 +128,8 @@ func tagBody(t *Tag, hashSize int) ([]byte, error) {
}
// Serialize renders a Tag into canonical Git format.
-func (t *Tag) Serialize(hashSize int) ([]byte, error) {
- body, err := tagBody(t, hashSize)
+func (t *Tag) Serialize() ([]byte, error) {
+ body, err := tagBody(t)
if err != nil {
return nil, err
}
diff --git a/obj_tree.go b/obj_tree.go
index 55a27a08..2bc3262f 100644
--- a/obj_tree.go
+++ b/obj_tree.go
@@ -26,7 +26,7 @@ func (*Tree) ObjType() ObjType {
}
// parseTree decodes a tree body.
-func parseTree(id Hash, body []byte, hashSize int) (*Tree, error) {
+func parseTree(id Hash, body []byte, repo *Repository) (*Tree, error) {
var entries []TreeEntry
i := 0
for i < len(body) {
@@ -44,12 +44,13 @@ func parseTree(id Hash, body []byte, hashSize int) (*Tree, error) {
nameBytes := body[i : i+nul]
i += nul + 1
- if i+hashSize > len(body) {
+ if i+repo.HashSize > len(body) {
return nil, errors.New("furgit: tree: truncated child hash")
}
var child Hash
- copy(child[:], body[i:i+hashSize])
- i += hashSize
+ copy(child.data[:], body[i:i+repo.HashSize])
+ child.size = repo.HashSize
+ i += repo.HashSize
mode, err := strconv.ParseUint(string(modeBytes), 8, 32)
if err != nil {
@@ -71,11 +72,11 @@ func parseTree(id Hash, body []byte, hashSize int) (*Tree, error) {
}
// treeBody builds the entry list for a tree without the Git header.
-func treeBody(t *Tree, hashSize int) []byte {
+func treeBody(t *Tree) []byte {
var bodyLen int
for _, e := range t.Entries {
mode := strconv.FormatUint(uint64(e.Mode), 8)
- bodyLen += len(mode) + 1 + len(e.Name) + 1 + hashSize
+ bodyLen += len(mode) + 1 + len(e.Name) + 1 + e.ID.size
}
body := make([]byte, bodyLen)
@@ -88,15 +89,15 @@ func treeBody(t *Tree, hashSize int) []byte {
pos += copy(body[pos:], e.Name)
body[pos] = 0
pos++
- pos += copy(body[pos:], e.ID[:hashSize])
+ pos += copy(body[pos:], e.ID.data[:e.ID.size])
}
return body
}
// Serialize renders a Tree into canonical Git format.
-func (t *Tree) Serialize(hashSize int) ([]byte, error) {
- body := treeBody(t, hashSize)
+func (t *Tree) Serialize() ([]byte, error) {
+ body := treeBody(t)
header, err := headerForType(ObjTree, body)
if err != nil {
return nil, err
diff --git a/objects_test.go b/objects_test.go
index 3a3e641f..3fb48c53 100644
--- a/objects_test.go
+++ b/objects_test.go
@@ -8,8 +8,14 @@ import (
"testing"
)
+func testRepo(t *testing.T) *Repository {
+ t.Helper()
+ return &Repository{HashSize: testHashSize}
+}
+
func mustHash(t *testing.T, hex string) Hash {
- id, err := ParseHashWithSize(hex, testHashSize)
+ repo := testRepo(t)
+ id, err := repo.ParseHash(hex)
if err != nil {
t.Fatalf("ParseHash failed: %v", err)
}
@@ -19,9 +25,10 @@ func mustHash(t *testing.T, hex string) Hash {
func hashWithByte(fill byte) Hash {
var h Hash
for i := 0; i < testHashSize; i++ {
- h[i] = fill
+ h.data[i] = fill
fill++
}
+ h.size = testHashSize
return h
}
@@ -30,8 +37,13 @@ func TestLoosePathUsesExpectedLayout(t *testing.T) {
repeats := (testHashSize*2 + len(pattern) - 1) / len(pattern)
hexStr := strings.Repeat(pattern, repeats)[:testHashSize*2]
id := mustHash(t, hexStr)
+ repo := testRepo(t)
expect := filepath.Join("objects", hexStr[:2], hexStr[2:])
- if got := loosePath(id, testHashSize); got != expect {
+ got, err := repo.loosePath(id)
+ if err != nil {
+ t.Fatalf("loosePath error: %v", err)
+ }
+ if got != expect {
t.Fatalf("unexpected loose path: %q", got)
}
}
@@ -49,7 +61,7 @@ func TestParseBlobAndSerialize(t *testing.T) {
if blob.Hash != id {
t.Fatalf("blob hash mismatch: %v", blob.Hash)
}
- raw, err := blob.Serialize(testHashSize)
+ raw, err := blob.Serialize()
if err != nil {
t.Fatalf("Serialize error: %v", err)
}
@@ -64,13 +76,14 @@ func TestParseBlobAndSerialize(t *testing.T) {
}
func TestParseTreeAndSerialize(t *testing.T) {
+ repo := testRepo(t)
entries := []TreeEntry{
{Mode: 0100644, Name: []byte("file.txt"), ID: hashWithByte(0x20)},
{Mode: 040000, Name: []byte("subdir"), ID: hashWithByte(0x30)},
}
- body := treeBody(&Tree{Entries: entries}, testHashSize)
+ body := treeBody(&Tree{Entries: entries})
id := hashWithByte(0x40)
- tree, err := parseTree(id, body, testHashSize)
+ tree, err := parseTree(id, body, repo)
if err != nil {
t.Fatalf("parseTree error: %v", err)
}
@@ -82,7 +95,7 @@ func TestParseTreeAndSerialize(t *testing.T) {
t.Fatalf("entry %d mismatch", i)
}
}
- serialized, err := (&Tree{Entries: entries}).Serialize(testHashSize)
+ serialized, err := (&Tree{Entries: entries}).Serialize()
if err != nil {
t.Fatalf("Serialize error: %v", err)
}
@@ -103,8 +116,8 @@ func TestParseCommitWithExtraHeader(t *testing.T) {
OffsetMinutes: -420,
}
var buf bytes.Buffer
- fmt.Fprintf(&buf, "tree %s\n", treeID.StringWithSize(testHashSize))
- fmt.Fprintf(&buf, "parent %s\n", parent.StringWithSize(testHashSize))
+ fmt.Fprintf(&buf, "tree %s\n", treeID.String())
+ fmt.Fprintf(&buf, "parent %s\n", parent.String())
buf.WriteString("author ")
buf.Write(ident.Serialize())
buf.WriteByte('\n')
@@ -112,7 +125,8 @@ func TestParseCommitWithExtraHeader(t *testing.T) {
buf.Write(ident.Serialize())
buf.WriteByte('\n')
buf.WriteString("extra data\n\nMessage body\n")
- commit, err := parseCommit(hashWithByte(0x70), buf.Bytes(), testHashSize)
+ repo := testRepo(t)
+ commit, err := parseCommit(hashWithByte(0x70), buf.Bytes(), repo)
if err != nil {
t.Fatalf("parseCommit error: %v", err)
}
@@ -136,11 +150,11 @@ func TestParseCommitWithExtraHeader(t *testing.T) {
Committer: ident,
Message: []byte("Message body\n"),
}
- raw, err := roundTrip.Serialize(testHashSize)
+ raw, err := roundTrip.Serialize()
if err != nil {
t.Fatalf("Serialize error: %v", err)
}
- if !strings.Contains(string(raw), "tree "+treeID.StringWithSize(testHashSize)) {
+ if !strings.Contains(string(raw), "tree "+treeID.String()) {
t.Fatalf("serialized commit missing tree header")
}
}
@@ -155,7 +169,7 @@ func TestParseTagAndSerialize(t *testing.T) {
}
var buf bytes.Buffer
buf.WriteString("object ")
- buf.WriteString(target.StringWithSize(testHashSize))
+ buf.WriteString(target.String())
buf.WriteByte('\n')
buf.WriteString("type commit\n")
buf.WriteString("tag v1.0\n")
@@ -163,7 +177,8 @@ func TestParseTagAndSerialize(t *testing.T) {
buf.Write(tagger.Serialize())
buf.WriteString("\n\nannotated tag\n")
body := append([]byte(nil), buf.Bytes()...)
- tag, err := parseTag(hashWithByte(0x90), body, testHashSize)
+ repo := testRepo(t)
+ tag, err := parseTag(hashWithByte(0x90), body, repo)
if err != nil {
t.Fatalf("parseTag error: %v", err)
}
@@ -179,7 +194,7 @@ func TestParseTagAndSerialize(t *testing.T) {
if string(tag.Name) != "v1.0" {
t.Fatalf("tag name mismatch: %q", tag.Name)
}
- serialized, err := tag.Serialize(testHashSize)
+ serialized, err := tag.Serialize()
if err != nil {
t.Fatalf("Serialize error: %v", err)
}
diff --git a/pack_idx.go b/pack_idx.go
index 9ca28193..d313825f 100644
--- a/pack_idx.go
+++ b/pack_idx.go
@@ -3,6 +3,7 @@ package furgit
import (
"bytes"
"errors"
+ "fmt"
"os"
"path/filepath"
"strings"
@@ -243,7 +244,11 @@ func (pi *packIndex) lookup(id Hash) (packlocation, error) {
if err != nil {
return packlocation{}, err
}
- first := int(id[0])
+ // Verify hash size matches repository hash size
+ if id.size != pi.repo.HashSize {
+ return packlocation{}, fmt.Errorf("furgit: hash size mismatch: got %d, expected %d", id.size, pi.repo.HashSize)
+ }
+ first := int(id.data[0])
var lo int
if first > 0 {
lo = int(pi.fanoutEntry(first - 1))
@@ -266,7 +271,7 @@ func (pi *packIndex) lookup(id Hash) (packlocation, error) {
func bsearchHash(names []byte, stride, lo, hi int, want Hash) (int, bool) {
for lo < hi {
mid := lo + (hi-lo)/2
- cmp := compareHash(names, stride, mid, want[:stride])
+ cmp := compareHash(names, stride, mid, want.data[:stride])
if cmp == 0 {
return mid, true
}
diff --git a/pack_midx.go b/pack_midx.go
index 7f31e565..0c4f3213 100644
--- a/pack_midx.go
+++ b/pack_midx.go
@@ -1,6 +1,7 @@
package furgit
import (
+ "fmt"
"os"
"path/filepath"
"strings"
@@ -245,7 +246,12 @@ func (midx *multiPackIndex) lookup(id Hash) (packlocation, error) {
}
}
- first := int(id[0])
+ // Verify hash size matches repository hash size
+ if id.size != midx.repo.HashSize {
+ return packlocation{}, fmt.Errorf("furgit: hash size mismatch: got %d, expected %d", id.size, midx.repo.HashSize)
+ }
+
+ first := int(id.data[0])
var lo int
if first > 0 {
lo = int(readBE32(midx.fanout[(first-1)*4 : first*4]))
diff --git a/pack_pack.go b/pack_pack.go
index 757c5c02..4a75b1ad 100644
--- a/pack_pack.go
+++ b/pack_pack.go
@@ -69,11 +69,11 @@ func (repo *Repository) packReadAt(loc packlocation, want Hash) (Object, error)
return nil, err
}
data := body.Bytes()
- if !verifyTypedObject(ty, data, want, repo.HashSize) {
+ if !repo.verifyTypedObject(ty, data, want) {
body.Release()
return nil, ErrInvalidObject
}
- obj, err := parseObjectBody(ty, want, data, repo.HashSize)
+ obj, err := parseObjectBody(ty, want, data, repo)
body.Release()
return obj, err
}
@@ -269,10 +269,11 @@ func (repo *Repository) packTypeSizeWithin(pf *packFile, ofs uint64, seen map[pa
return ty, declaredSize, nil
case ObjRefDelta:
var base Hash
- _, err := io.ReadFull(r, base[:])
+ _, err := io.ReadFull(r, base.data[:repo.HashSize])
if err != nil {
return ObjInvalid, 0, err
}
+ base.size = repo.HashSize
baseTy, _, err := repo.packTypeSizeByID(base, seen)
if err != nil {
return ObjInvalid, 0, err
@@ -315,10 +316,11 @@ func (repo *Repository) packBodyResolveWithin(pf *packFile, ofs uint64) (ObjType
return ty, body, err
case ObjRefDelta:
var base Hash
- _, err := io.ReadFull(r, base[:])
+ _, err := io.ReadFull(r, base.data[:repo.HashSize])
if err != nil {
return ObjInvalid, borrowedBody{}, err
}
+ base.size = repo.HashSize
delta, err := packSectionInflate(r, 0)
if err != nil {
return ObjInvalid, borrowedBody{}, err
diff --git a/pack_test.go b/pack_test.go
index 48f4d604..6ca7e115 100644
--- a/pack_test.go
+++ b/pack_test.go
@@ -153,7 +153,7 @@ func TestPackDeltaReadOfsDistance(t *testing.T) {
func TestBsearchHash(t *testing.T) {
h1 := hashWithByte(0x01)
h2 := hashWithByte(0x03)
- names := append(append([]byte(nil), h1[:testHashSize]...), h2[:testHashSize]...)
+ names := append(append([]byte(nil), h1.data[:testHashSize]...), h2.data[:testHashSize]...)
idx, found := bsearchHash(names, testHashSize, 0, 2, h2)
if !found || idx != 1 {
t.Fatalf("expected to find second hash, idx=%d found=%v", idx, found)
@@ -166,7 +166,7 @@ func TestBsearchHash(t *testing.T) {
func buildTestPackIndexBuffer(hash Hash, offset uint32) []byte {
fanout := make([]byte, 256*4)
- first := int(hash[0])
+ first := int(hash.data[0])
for i := 0; i < 256; i++ {
var val uint32
if i >= first {
@@ -178,7 +178,7 @@ func buildTestPackIndexBuffer(hash Hash, offset uint32) []byte {
_ = binary.Write(&buf, binary.BigEndian, uint32(idxMagic))
_ = binary.Write(&buf, binary.BigEndian, uint32(idxVersion2))
buf.Write(fanout)
- buf.Write(hash[:testHashSize])
+ buf.Write(hash.data[:testHashSize])
buf.Write(make([]byte, 4))
off32 := make([]byte, 4)
binary.BigEndian.PutUint32(off32, offset)
diff --git a/refs.go b/refs.go
index b543a842..36f7a962 100644
--- a/refs.go
+++ b/refs.go
@@ -29,7 +29,7 @@ func (repo *Repository) resolveLooseRef(refname string) (Hash, error) {
return Hash{}, err
}
line := strings.TrimSpace(string(data))
- id, err := ParseHashWithSize(line, repo.HashSize)
+ id, err := repo.ParseHash(line)
if err != nil {
return Hash{}, err
}
@@ -61,7 +61,7 @@ func (repo *Repository) resolvePackedRef(refname string) (Hash, error) {
name := line[sp+1:]
if bytes.Equal(name, want) {
hex := string(line[:sp])
- id, err := ParseHashWithSize(hex, repo.HashSize)
+ id, err := repo.ParseHash(hex)
if err != nil {
return Hash{}, err
}
diff --git a/repo.go b/repo.go
index fb835edb..8376d8af 100644
--- a/repo.go
+++ b/repo.go
@@ -1,6 +1,7 @@
package furgit
import (
+ "encoding/hex"
"fmt"
"os"
"path/filepath"
@@ -97,3 +98,51 @@ func (r *Repository) packFile(rel string) (*packFile, error) {
}
return pf, nil
}
+
+// ParseHash converts a hex string into a Hash, validating it matches the repository's hash size.
+func (r *Repository) ParseHash(s string) (Hash, error) {
+ var id Hash
+ if len(s)%2 != 0 {
+ return id, fmt.Errorf("furgit: invalid hash length %d, it has to be even at the very least", len(s))
+ }
+ expectedLen := r.HashSize * 2
+ if len(s) != expectedLen {
+ return id, fmt.Errorf("furgit: hash length mismatch: got %d chars, expected %d for hash size %d", len(s), expectedLen, r.HashSize)
+ }
+ data, err := hex.DecodeString(s)
+ if err != nil {
+ return id, fmt.Errorf("furgit: decode hash: %w", err)
+ }
+ copy(id.data[:], data)
+ id.size = len(s) / 2
+ return id, nil
+}
+
+// computeRawHash computes a hash from raw data using the repository's hash algorithm.
+func (r *Repository) computeRawHash(data []byte) Hash {
+ hashFunc := hashFuncs[r.HashSize]
+ return hashFunc(data)
+}
+
+// verifyRawObject verifies a raw object against its expected hash.
+func (r *Repository) verifyRawObject(buf []byte, want Hash) bool {
+ if want.size != r.HashSize {
+ return false
+ }
+ return r.computeRawHash(buf) == want
+}
+
+// verifyTypedObject verifies a typed object against its expected hash.
+func (r *Repository) verifyTypedObject(ty ObjType, body []byte, want Hash) bool {
+ if want.size != r.HashSize {
+ return false
+ }
+ header, err := headerForType(ty, body)
+ if err != nil {
+ return false
+ }
+ raw := make([]byte, len(header)+len(body))
+ copy(raw, header)
+ copy(raw[len(header):], body)
+ return r.computeRawHash(raw) == want
+}
diff --git a/repo_test.go b/repo_test.go
index d09d2642..409919cf 100644
--- a/repo_test.go
+++ b/repo_test.go
@@ -56,7 +56,7 @@ func TestResolveRefLooseAndPacked(t *testing.T) {
if err := os.MkdirAll(loosePath, 0o755); err != nil {
t.Fatalf("mkdir refs: %v", err)
}
- if err := os.WriteFile(filepath.Join(loosePath, "master"), []byte(looseID.StringWithSize(testHashSize)+"\n"), 0o644); err != nil {
+ if err := os.WriteFile(filepath.Join(loosePath, "master"), []byte(looseID.String()+"\n"), 0o644); err != nil {
t.Fatalf("write ref: %v", err)
}
id, err := repo.ResolveRef("refs/heads/master")
@@ -65,7 +65,7 @@ func TestResolveRefLooseAndPacked(t *testing.T) {
}
packedID := hashWithByte(0xb0)
- packed := fmt.Sprintf("%s refs/tags/v1\n", packedID.StringWithSize(testHashSize))
+ packed := fmt.Sprintf("%s refs/tags/v1\n", packedID.String())
if err := os.WriteFile(filepath.Join(root, "packed-refs"), []byte(packed), 0o644); err != nil {
t.Fatalf("write packed refs: %v", err)
}
@@ -320,6 +320,7 @@ type testPackObject struct {
func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Hash {
t.Helper()
+ repo := &Repository{HashSize: testHashSize}
packDir := filepath.Join(root, "objects", "pack")
err := os.MkdirAll(packDir, 0o750)
if err != nil {
@@ -358,7 +359,7 @@ func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Has
raw := make([]byte, len(header)+len(obj.body))
copy(raw, header)
copy(raw[len(header):], obj.body)
- ids[i] = computeRawHash(raw, testHashSize)
+ ids[i] = repo.computeRawHash(raw)
switch obj.encoding {
case packEncodingFull:
@@ -383,7 +384,7 @@ func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Has
t.Fatalf("ref delta %d missing base body", i)
}
buf.Write(encodePackHeader(ObjRefDelta, len(obj.body)))
- buf.Write(obj.baseHash[:])
+ buf.Write(obj.baseHash.data[:testHashSize])
delta := buildInsertOnlyDelta(len(baseBody), obj.body)
buf.Write(compressBytes(t, delta))
default:
@@ -392,8 +393,8 @@ func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Has
}
packContent := append([]byte(nil), buf.Bytes()...)
- packChecksum := computeRawHash(packContent, testHashSize)
- buf.Write(packChecksum[:testHashSize])
+ packChecksum := repo.computeRawHash(packContent)
+ buf.Write(packChecksum.data[:testHashSize])
packBytes := buf.Bytes()
packPath := filepath.Join(packDir, name+".pack")
@@ -408,6 +409,7 @@ func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Has
func writeTestPackIndex(t *testing.T, packDir, name string, ids []Hash, offsets []uint64, packChecksum Hash) {
t.Helper()
+ repo := &Repository{HashSize: testHashSize}
type idxEntry struct {
id Hash
offset uint64
@@ -417,7 +419,7 @@ func writeTestPackIndex(t *testing.T, packDir, name string, ids []Hash, offsets
entries[i] = idxEntry{id: ids[i], offset: offsets[i]}
}
sort.Slice(entries, func(i, j int) bool {
- return bytes.Compare(entries[i].id[:], entries[j].id[:]) < 0
+ return bytes.Compare(entries[i].id.data[:testHashSize], entries[j].id.data[:testHashSize]) < 0
})
var buf bytes.Buffer
@@ -432,7 +434,7 @@ func writeTestPackIndex(t *testing.T, packDir, name string, ids []Hash, offsets
var fanout [256]uint32
for _, entry := range entries {
- first := int(entry.id[0])
+ first := int(entry.id.data[0])
for i := first; i < len(fanout); i++ {
fanout[i]++
}
@@ -445,7 +447,7 @@ func writeTestPackIndex(t *testing.T, packDir, name string, ids []Hash, offsets
}
for _, entry := range entries {
- buf.Write(entry.id[:testHashSize])
+ buf.Write(entry.id.data[:testHashSize])
}
buf.Write(make([]byte, len(entries)*4))
@@ -460,9 +462,9 @@ func writeTestPackIndex(t *testing.T, packDir, name string, ids []Hash, offsets
}
idxData := append([]byte(nil), buf.Bytes()...)
- idxChecksum := computeRawHash(idxData, testHashSize)
- buf.Write(packChecksum[:testHashSize])
- buf.Write(idxChecksum[:testHashSize])
+ idxChecksum := repo.computeRawHash(idxData)
+ buf.Write(packChecksum.data[:testHashSize])
+ buf.Write(idxChecksum.data[:testHashSize])
idxPath := filepath.Join(packDir, name+".idx")
err = os.WriteFile(idxPath, buf.Bytes(), 0o600)