diff options
| -rw-r--r-- | hash.go | 53 | ||||
| -rw-r--r-- | hash_test.go | 20 | ||||
| -rw-r--r-- | loose.go | 42 | ||||
| -rw-r--r-- | obj.go | 28 | ||||
| -rw-r--r-- | obj_blob.go | 2 | ||||
| -rw-r--r-- | obj_commit.go | 16 | ||||
| -rw-r--r-- | obj_tag.go | 12 | ||||
| -rw-r--r-- | obj_tree.go | 19 | ||||
| -rw-r--r-- | objects_test.go | 45 | ||||
| -rw-r--r-- | pack_idx.go | 9 | ||||
| -rw-r--r-- | pack_midx.go | 8 | ||||
| -rw-r--r-- | pack_pack.go | 10 | ||||
| -rw-r--r-- | pack_test.go | 6 | ||||
| -rw-r--r-- | refs.go | 4 | ||||
| -rw-r--r-- | repo.go | 49 | ||||
| -rw-r--r-- | repo_test.go | 26 |
16 files changed, 211 insertions, 138 deletions
@@ -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") } } @@ -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 @@ -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 @@ -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) @@ -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 } @@ -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) |
