From 94bfb1fa147f80e6ec39009d41fc2f853925e0a5 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Sun, 16 Nov 2025 00:00:00 +0000 Subject: hash: Generic hash-algorithm API --- README.md | 3 +- hash.go | 98 ++++++++++++++++++++++++++++++++++------------------- hash_sha1_test.go | 12 +++++++ hash_sha256_test.go | 12 +++++++ hash_test.go | 21 ++++++------ loose.go | 52 ++++++++++++++-------------- obj.go | 29 +++++++--------- obj_blob.go | 13 ++++--- obj_commit.go | 28 +++++++-------- obj_tag.go | 22 ++++++------ obj_tree.go | 36 ++++++++++---------- objects_test.go | 43 +++++++++++------------ pack_idx.go | 41 +++++++++++----------- pack_midx.go | 44 ++++++++++++++++-------- pack_pack.go | 32 ++++++++--------- pack_test.go | 18 +++++----- refs.go | 33 +++++++++--------- repo.go | 35 +++++++++++-------- repo_test.go | 75 ++++++++++++++++++++-------------------- 19 files changed, 363 insertions(+), 284 deletions(-) diff --git a/README.md b/README.md index c0defc35..9f93c9fa 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,8 @@ optimizations thereof. ## Hash algorithm -Furgit supports both SHA-256 and SHA-1. +Furgit supports both SHA-256 and SHA-1 wit ha generics API. But it's broken and +being refactored. The default tests run with SHA-256. To run tests with SHA-1, use the `sha1` build tag. diff --git a/hash.go b/hash.go index 336d5322..53dff11b 100644 --- a/hash.go +++ b/hash.go @@ -5,52 +5,80 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "unsafe" ) -const maxHashSize = 32 +// HashType is a constraint that enumerates the supported hash sizes. +type HashType interface { + [sha1.Size]byte | [sha256.Size]byte +} -// Hash represents a Git object identifier. -type Hash [maxHashSize]byte +type ( + // SHA1Hash represents a SHA-1 hash. + SHA1Hash = [sha1.Size]byte -// hashFunc is a function that computes a hash from input data. -type hashFunc func([]byte) [maxHashSize]byte + // SHA256Hash represents a SHA-256 hash. + SHA256Hash = [sha256.Size]byte +) -// hashFuncs maps hash size to hash function. -var hashFuncs = map[int]hashFunc{ - sha1.Size: func(data []byte) [maxHashSize]byte { - var result [maxHashSize]byte - sum := sha1.Sum(data) - copy(result[:], sum[:]) - return result - }, - sha256.Size: func(data []byte) [maxHashSize]byte { - var result [maxHashSize]byte - sum := sha256.Sum256(data) - copy(result[:], sum[:]) - return result - }, +// Hash represents a Git object identifier with type-level hash size. +type Hash[T HashType] struct { + v T } -// 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)) +// hashLen returns the hash size for a given hash type. +func hashLen[T HashType]() int { + var zero T + return len(zero) +} + +// String returns the hash as a hex string. +func (h Hash[T]) String() string { + return hex.EncodeToString(unsafe.Slice((*byte)(unsafe.Pointer(&h.v)), hashLen[T]())) +} + +// Bytes returns a mutable copy of the underlying bytes. +func (h Hash[T]) Bytes() []byte { + s := unsafe.Slice((*byte)(unsafe.Pointer(&h.v)), hashLen[T]()) + return append([]byte(nil), s...) +} + +// Slice returns a read-only slice view of the underlying bytes. +func (h *Hash[T]) Slice() []byte { + return unsafe.Slice((*byte)(unsafe.Pointer(&h.v)), hashLen[T]()) +} + +// ParseHash converts a hex string into a Hash for the given hash type. +func ParseHash[T HashType](s string) (Hash[T], error) { + var out Hash[T] + wantHex := hashLen[T]() * 2 + + if len(s) != wantHex { + return out, fmt.Errorf("furgit: invalid hash length %d, want %d", len(s), wantHex) } - data, err := hex.DecodeString(s) + raw, err := hex.DecodeString(s) if err != nil { - return id, fmt.Errorf("furgit: decode hash: %w", err) + return out, fmt.Errorf("furgit: decode hash: %w", err) } - copy(id[:], data) - return id, nil + slice := unsafe.Slice((*byte)(unsafe.Pointer(&out.v)), hashLen[T]()) + copy(slice, raw) + return out, nil } -// StringWithSize returns the ID as hex for a given hash size. -func (id Hash) StringWithSize(hashSize int) string { - return hex.EncodeToString(id[:hashSize]) -} +// computeRawHash computes a hash from data. +func computeRawHash[T HashType](data []byte) Hash[T] { + var out Hash[T] + slice := unsafe.Slice((*byte)(unsafe.Pointer(&out.v)), hashLen[T]()) -// 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]...) + switch hashLen[T]() { + case sha1.Size: + sum := sha1.Sum(data) + copy(slice, sum[:]) + case sha256.Size: + sum := sha256.Sum256(data) + copy(slice, sum[:]) + default: + panic("furgit: unsupported hash length") + } + return out } diff --git a/hash_sha1_test.go b/hash_sha1_test.go index 9f3137b9..8d5b4e9c 100644 --- a/hash_sha1_test.go +++ b/hash_sha1_test.go @@ -7,3 +7,15 @@ import ( ) const testHashSize = sha1.Size + +type ( + testHashType = [sha1.Size]byte + TestHash = Hash[testHashType] + TestRepository = Repository[testHashType] + TestBlob = Blob[testHashType] + TestTree = Tree[testHashType] + TestTreeEntry = TreeEntry[testHashType] + TestCommit = Commit[testHashType] + TestTag = Tag[testHashType] + TestObject = Object[testHashType] +) diff --git a/hash_sha256_test.go b/hash_sha256_test.go index 0b735f0a..ce436ad7 100644 --- a/hash_sha256_test.go +++ b/hash_sha256_test.go @@ -7,3 +7,15 @@ import ( ) const testHashSize = sha256.Size + +type ( + testHashType = [sha256.Size]byte + TestHash = Hash[testHashType] + TestRepository = Repository[testHashType] + TestBlob = Blob[testHashType] + TestTree = Tree[testHashType] + TestTreeEntry = TreeEntry[testHashType] + TestCommit = Commit[testHashType] + TestTag = Tag[testHashType] + TestObject = Object[testHashType] +) diff --git a/hash_test.go b/hash_test.go index 4b359c4a..a6215a36 100644 --- a/hash_test.go +++ b/hash_test.go @@ -10,33 +10,34 @@ func TestParseHashValidAndInvalid(t *testing.T) { repeats := (testHashSize*2 + len(pattern) - 1) / len(pattern) hexStr := strings.Repeat(pattern, repeats)[:testHashSize*2] - id, err := ParseHashWithSize(hexStr, testHashSize) + id, err := ParseHash[testHashType](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 := ParseHash[testHashType]("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 := ParseHash[testHashType](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) +func TestHashTypeCopiesUnderlyingData(t *testing.T) { + var id TestHash + idSlice := id.Slice() + for i := range idSlice { + idSlice[i] = byte(i) } - orig := id.BytesWithSize(testHashSize) + orig := id.Bytes() orig[0] ^= 0xff - if id[0] == orig[0] { + if idSlice[0] == orig[0] { t.Fatal("Bytes should return a copy") } } diff --git a/loose.go b/loose.go index c1371991..8ed9d173 100644 --- a/loose.go +++ b/loose.go @@ -12,21 +12,21 @@ import ( const looseHeaderLimit = 4096 -func loosePath(id Hash, hashSize int) string { - hex := id.StringWithSize(hashSize) +func loosePath[T HashType](id Hash[T]) string { + hex := id.String() return filepath.Join("objects", hex[:2], hex[2:]) } -func (repo *Repository) looseRead(id Hash) (Object, error) { +func (repo *Repository[T]) looseRead(id Hash[T]) (Object[T], error) { ty, body, err := repo.looseReadTyped(id) if err != nil { return nil, err } - return parseObjectBody(ty, id, body, repo.HashSize) + return parseObjectBody[T](ty, id, body) } -func (repo *Repository) looseReadTyped(id Hash) (ObjType, []byte, error) { - path := repo.repoPath(loosePath(id, repo.HashSize)) +func (repo *Repository[T]) looseReadTyped(id Hash[T]) (ObjType, []byte, error) { + path := repo.repoPath(loosePath(id)) f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { @@ -62,7 +62,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 !verifyRawObject[T](raw, id) { return ObjInvalid, nil, ErrInvalidObject } @@ -70,8 +70,8 @@ func (repo *Repository) looseReadTyped(id Hash) (ObjType, []byte, error) { return ty, out, nil } -func (repo *Repository) looseTypeSize(id Hash) (ObjType, int64, error) { - path := repo.repoPath(loosePath(id, repo.HashSize)) +func (repo *Repository[T]) looseTypeSize(id Hash[T]) (ObjType, int64, error) { + path := repo.repoPath(loosePath(id)) // #nosec G304 f, err := os.Open(path) if err != nil { @@ -155,46 +155,46 @@ func objTypeFromName(name string) (ObjType, error) { } // WriteLooseObject writes an object to the repository as a loose object. -func (repo *Repository) WriteLooseObject(obj Object) (Hash, error) { +func (repo *Repository[T]) WriteLooseObject(obj Object[T]) (Hash[T], error) { var raw []byte var err error switch o := obj.(type) { - case *Blob: - raw, err = o.Serialize(repo.HashSize) - case *Tree: - raw, err = o.Serialize(repo.HashSize) - case *Commit: - raw, err = o.Serialize(repo.HashSize) - case *Tag: - raw, err = o.Serialize(repo.HashSize) + case *Blob[T]: + raw, err = o.Serialize() + case *Tree[T]: + raw, err = o.Serialize() + case *Commit[T]: + raw, err = o.Serialize() + case *Tag[T]: + raw, err = o.Serialize() default: - return Hash{}, fmt.Errorf("furgit: unsupported object type for writing: %T", obj) + return Hash[T]{}, fmt.Errorf("furgit: unsupported object type for writing: %T", obj) } // TODO: Consider adding serialize to the interface? if err != nil { - return Hash{}, err + return Hash[T]{}, err } - id := computeRawHash(raw, repo.HashSize) - path := repo.repoPath(loosePath(id, repo.HashSize)) + id := computeRawHash[T](raw) + path := repo.repoPath(loosePath(id)) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return Hash{}, err + return Hash[T]{}, err } var buf bytes.Buffer zw := zlib.NewWriter(&buf) if _, err := zw.Write(raw); err != nil { - return Hash{}, err + return Hash[T]{}, err } if err := zw.Close(); err != nil { - return Hash{}, err + return Hash[T]{}, err } if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil { - return Hash{}, err + return Hash[T]{}, err } return id, nil diff --git a/obj.go b/obj.go index ce3d0258..07b76c17 100644 --- a/obj.go +++ b/obj.go @@ -29,15 +29,10 @@ const ( ) // Object describes any Git object variant. -type Object interface { +type Object[T HashType] 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,11 +59,11 @@ 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 verifyRawObject[T HashType](buf []byte, want Hash[T]) bool { + return computeRawHash[T](buf) == want } -func verifyTypedObject(ty ObjType, body []byte, want Hash, hashSize int) bool { +func verifyTypedObject[T HashType](ty ObjType, body []byte, want Hash[T]) bool { header, err := headerForType(ty, body) if err != nil { return false @@ -76,19 +71,19 @@ func verifyTypedObject(ty ObjType, body []byte, want Hash, hashSize int) bool { raw := make([]byte, len(header)+len(body)) copy(raw, header) copy(raw[len(header):], body) - return computeRawHash(raw, hashSize) == want + return computeRawHash[T](raw) == want } -func parseObjectBody(ty ObjType, id Hash, body []byte, hashSize int) (Object, error) { +func parseObjectBody[T HashType](ty ObjType, id Hash[T], body []byte) (Object[T], error) { switch ty { case ObjBlob: - return parseBlob(id, body) + return parseBlob[T](id, body) case ObjTree: - return parseTree(id, body, hashSize) + return parseTree[T](id, body) case ObjCommit: - return parseCommit(id, body, hashSize) + return parseCommit[T](id, body) case ObjTag: - return parseTag(id, body, hashSize) + return parseTag[T](id, body) case ObjInvalid, ObjFuture, ObjOfsDelta, ObjRefDelta: return nil, fmt.Errorf("furgit: object: unsupported type %d", ty) default: @@ -97,7 +92,7 @@ func parseObjectBody(ty ObjType, id Hash, body []byte, hashSize int) (Object, er } // ReadObject resolves an ID by consulting loose then packed storage. -func (repo *Repository) ReadObject(id Hash) (Object, error) { +func (repo *Repository[T]) ReadObject(id Hash[T]) (Object[T], error) { obj, err := repo.looseRead(id) if err == nil { return obj, nil @@ -113,7 +108,7 @@ func (repo *Repository) ReadObject(id Hash) (Object, error) { } // ReadObjectTypeSize reports the object type and size without inflating the body. -func (repo *Repository) ReadObjectTypeSize(id Hash) (ObjType, int64, error) { +func (repo *Repository[T]) ReadObjectTypeSize(id Hash[T]) (ObjType, int64, error) { ty, size, err := repo.looseTypeSize(id) if err == nil { return ty, size, nil diff --git a/obj_blob.go b/obj_blob.go index 9edad0a9..1f74464e 100644 --- a/obj_blob.go +++ b/obj_blob.go @@ -1,27 +1,26 @@ package furgit // Blob represents the contents of a Git blob. -type Blob struct { - Hash Hash - +type Blob[T HashType] struct { + Hash Hash[T] Data []byte } // ObjType allows Blob to satisfy the Object interface. -func (*Blob) ObjType() ObjType { +func (*Blob[T]) ObjType() ObjType { return ObjBlob } -func parseBlob(id Hash, body []byte) (*Blob, error) { +func parseBlob[T HashType](id Hash[T], body []byte) (*Blob[T], error) { data := append([]byte(nil), body...) - return &Blob{ + return &Blob[T]{ Hash: id, Data: data, }, nil } // Serialize renders the full "blob size\\0body" representation. -func (b *Blob) Serialize(hashSize int) ([]byte, error) { +func (b *Blob[T]) 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..f733a56f 100644 --- a/obj_commit.go +++ b/obj_commit.go @@ -7,10 +7,10 @@ import ( ) // Commit mirrors the structure of a Git commit object. -type Commit struct { - Hash Hash - Tree Hash - Parents []Hash +type Commit[T HashType] struct { + Hash Hash[T] + Tree Hash[T] + Parents []Hash[T] Author Ident Committer Ident Message []byte @@ -18,12 +18,12 @@ type Commit struct { } // ObjType allows Commit to satisfy the Object interface. -func (*Commit) ObjType() ObjType { +func (*Commit[T]) ObjType() ObjType { return ObjCommit } -func parseCommit(id Hash, body []byte, hashSize int) (*Commit, error) { - c := new(Commit) +func parseCommit[T HashType](id Hash[T], body []byte) (*Commit[T], error) { + c := new(Commit[T]) c.Hash = id i := 0 for i < len(body) { @@ -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 := ParseHash[T](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 := ParseHash[T](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[T HashType](c *Commit[T]) []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[T]) 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..ce42e41b 100644 --- a/obj_tag.go +++ b/obj_tag.go @@ -7,9 +7,9 @@ import ( ) // Tag models an annotated Git tag object. -type Tag struct { - Hash Hash - Target Hash +type Tag[T HashType] struct { + Hash Hash[T] + Target Hash[T] TargetType ObjType Name []byte Tagger *Ident @@ -17,13 +17,13 @@ type Tag struct { } // ObjType allows Tag to satisfy the Object interface. -func (*Tag) ObjType() ObjType { +func (*Tag[T]) ObjType() ObjType { return ObjTag } // parseTag parses a tag object body. -func parseTag(id Hash, body []byte, hashSize int) (*Tag, error) { - t := new(Tag) +func parseTag[T HashType](id Hash[T], body []byte) (*Tag[T], error) { + t := new(Tag[T]) t.Hash = id i := 0 var haveTarget, haveType bool @@ -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 := ParseHash[T](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 HashType](t *Tag[T]) ([]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[T]) 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..c025dfa3 100644 --- a/obj_tree.go +++ b/obj_tree.go @@ -8,26 +8,27 @@ import ( ) // Tree represents a Git tree object. -type Tree struct { - Hash Hash - Entries []TreeEntry +type Tree[T HashType] struct { + Hash Hash[T] + Entries []TreeEntry[T] } // TreeEntry represents a single entry in a Git tree. -type TreeEntry struct { +type TreeEntry[T HashType] struct { Mode uint32 Name []byte - ID Hash + ID Hash[T] } // ObjType allows Tree to satisfy the Object interface. -func (*Tree) ObjType() ObjType { +func (*Tree[T]) ObjType() ObjType { return ObjTree } // parseTree decodes a tree body. -func parseTree(id Hash, body []byte, hashSize int) (*Tree, error) { - var entries []TreeEntry +func parseTree[T HashType](id Hash[T], body []byte) (*Tree[T], error) { + var entries []TreeEntry[T] + hashSize := hashLen[T]() i := 0 for i < len(body) { space := bytes.IndexByte(body[i:], ' ') @@ -47,8 +48,8 @@ func parseTree(id Hash, body []byte, hashSize int) (*Tree, error) { if i+hashSize > len(body) { return nil, errors.New("furgit: tree: truncated child hash") } - var child Hash - copy(child[:], body[i:i+hashSize]) + var child Hash[T] + copy(child.Slice(), body[i:i+hashSize]) i += hashSize mode, err := strconv.ParseUint(string(modeBytes), 8, 32) @@ -56,7 +57,7 @@ func parseTree(id Hash, body []byte, hashSize int) (*Tree, error) { return nil, fmt.Errorf("furgit: tree: parse mode: %w", err) } - entry := TreeEntry{ + entry := TreeEntry[T]{ Mode: uint32(mode), Name: append([]byte(nil), nameBytes...), ID: child, @@ -64,14 +65,15 @@ func parseTree(id Hash, body []byte, hashSize int) (*Tree, error) { entries = append(entries, entry) } - return &Tree{ + return &Tree[T]{ Hash: id, Entries: entries, }, nil } // treeBody builds the entry list for a tree without the Git header. -func treeBody(t *Tree, hashSize int) []byte { +func treeBody[T HashType](t *Tree[T]) []byte { + hashSize := hashLen[T]() var bodyLen int for _, e := range t.Entries { mode := strconv.FormatUint(uint64(e.Mode), 8) @@ -88,15 +90,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.Slice()[:hashSize]) } 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[T]) Serialize() ([]byte, error) { + body := treeBody(t) header, err := headerForType(ObjTree, body) if err != nil { return nil, err @@ -109,7 +111,7 @@ func (t *Tree) Serialize(hashSize int) ([]byte, error) { } // Entry looks up a tree entry by name. -func (t *Tree) Entry(name []byte) *TreeEntry { +func (t *Tree[T]) Entry(name []byte) *TreeEntry[T] { low, high := 0, len(t.Entries)-1 for low <= high { mid := (low + high) / 2 diff --git a/objects_test.go b/objects_test.go index 3a3e641f..cf9d3da7 100644 --- a/objects_test.go +++ b/objects_test.go @@ -8,18 +8,19 @@ import ( "testing" ) -func mustHash(t *testing.T, hex string) Hash { - id, err := ParseHashWithSize(hex, testHashSize) +func mustHash(t *testing.T, hex string) TestHash { + id, err := ParseHash[testHashType](hex) if err != nil { t.Fatalf("ParseHash failed: %v", err) } return id } -func hashWithByte(fill byte) Hash { - var h Hash +func hashWithByte(fill byte) TestHash { + var h TestHash + s := h.Slice() for i := 0; i < testHashSize; i++ { - h[i] = fill + s[i] = fill fill++ } return h @@ -31,7 +32,7 @@ func TestLoosePathUsesExpectedLayout(t *testing.T) { hexStr := strings.Repeat(pattern, repeats)[:testHashSize*2] id := mustHash(t, hexStr) expect := filepath.Join("objects", hexStr[:2], hexStr[2:]) - if got := loosePath(id, testHashSize); got != expect { + if got := loosePath(id); got != expect { t.Fatalf("unexpected loose path: %q", got) } } @@ -49,7 +50,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 +65,13 @@ func TestParseBlobAndSerialize(t *testing.T) { } func TestParseTreeAndSerialize(t *testing.T) { - entries := []TreeEntry{ + entries := []TestTreeEntry{ {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(&TestTree{Entries: entries}) id := hashWithByte(0x40) - tree, err := parseTree(id, body, testHashSize) + tree, err := parseTree(id, body) if err != nil { t.Fatalf("parseTree error: %v", err) } @@ -82,7 +83,7 @@ func TestParseTreeAndSerialize(t *testing.T) { t.Fatalf("entry %d mismatch", i) } } - serialized, err := (&Tree{Entries: entries}).Serialize(testHashSize) + serialized, err := (&TestTree{Entries: entries}).Serialize() if err != nil { t.Fatalf("Serialize error: %v", err) } @@ -103,8 +104,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 +113,7 @@ 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) + commit, err := parseCommit(hashWithByte(0x70), buf.Bytes()) if err != nil { t.Fatalf("parseCommit error: %v", err) } @@ -129,18 +130,18 @@ func TestParseCommitWithExtraHeader(t *testing.T) { t.Fatalf("extra headers mismatch: %+v", commit.ExtraHeaders) } - roundTrip := &Commit{ + roundTrip := &TestCommit{ Tree: treeID, - Parents: []Hash{parent}, + Parents: []TestHash{parent}, Author: ident, 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 +156,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 +164,7 @@ 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) + tag, err := parseTag(hashWithByte(0x90), body) if err != nil { t.Fatalf("parseTag error: %v", err) } @@ -179,7 +180,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..772c99cc 100644 --- a/pack_idx.go +++ b/pack_idx.go @@ -15,8 +15,8 @@ const ( idxVersion2 = 2 ) -type packIndex struct { - repo *Repository +type packIndex[T HashType] struct { + repo *Repository[T] idxRel string packPath string @@ -34,7 +34,7 @@ type packIndex struct { closeOnce sync.Once } -func (pi *packIndex) Close() error { +func (pi *packIndex[T]) Close() error { if pi == nil { return nil } @@ -56,14 +56,14 @@ func (pi *packIndex) Close() error { return closeErr } -func (pi *packIndex) ensureLoaded() error { +func (pi *packIndex[T]) ensureLoaded() error { pi.loadOnce.Do(func() { pi.loadErr = pi.load() }) return pi.loadErr } -func (pi *packIndex) load() error { +func (pi *packIndex[T]) load() error { if pi.repo == nil { return ErrInvalidObject } @@ -105,14 +105,14 @@ func (pi *packIndex) load() error { return nil } -func (r *Repository) packIndexes() ([]*packIndex, error) { +func (r *Repository[T]) packIndexes() ([]*packIndex[T], error) { r.packIdxOnce.Do(func() { r.packIdx, r.packIdxErr = r.loadPackIndexes() }) return r.packIdx, r.packIdxErr } -func (repo *Repository) loadPackIndexes() ([]*packIndex, error) { +func (repo *Repository[T]) loadPackIndexes() ([]*packIndex[T], error) { dir := filepath.Join(repo.rootPath, "objects", "pack") entries, err := os.ReadDir(dir) if err != nil { @@ -122,14 +122,14 @@ func (repo *Repository) loadPackIndexes() ([]*packIndex, error) { return nil, err } - idxs := make([]*packIndex, 0, len(entries)) + idxs := make([]*packIndex[T], 0, len(entries)) for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".idx") { continue } rel := filepath.Join("objects", "pack", entry.Name()) packRel := strings.TrimSuffix(rel, ".idx") + ".pack" - idxs = append(idxs, &packIndex{ + idxs = append(idxs, &packIndex[T]{ repo: repo, idxRel: rel, packPath: packRel, @@ -141,7 +141,7 @@ func (repo *Repository) loadPackIndexes() ([]*packIndex, error) { return idxs, nil } -func (pi *packIndex) parse(buf []byte) error { +func (pi *packIndex[T]) parse(buf []byte) error { if len(buf) < 8+256*4 { return ErrInvalidObject } @@ -161,8 +161,9 @@ func (pi *packIndex) parse(buf []byte) error { pi.fanout = buf[fanoutStart:fanoutEnd] nobj := int(readBE32(pi.fanout[len(pi.fanout)-4:])) + hashSize := pi.repo.hashSize() namesStart := fanoutEnd - namesEnd := namesStart + nobj*pi.repo.HashSize + namesEnd := namesStart + nobj*hashSize if namesEnd > len(buf) { return ErrInvalidObject } @@ -182,7 +183,7 @@ func (pi *packIndex) parse(buf []byte) error { pi.offset32 = buf[off32Start:off32End] off64Start := off32End - trailerStart := len(buf) - 2*pi.repo.HashSize + trailerStart := len(buf) - 2*hashSize if trailerStart < off64Start { return ErrInvalidObject } @@ -211,7 +212,7 @@ func readBE64(b []byte) uint64 { (uint64(b[6]) << 8) | uint64(b[7]) } -func (pi *packIndex) fanoutEntry(i int) uint32 { +func (pi *packIndex[T]) fanoutEntry(i int) uint32 { if len(pi.fanout) == 0 { return 0 } @@ -223,7 +224,7 @@ func (pi *packIndex) fanoutEntry(i int) uint32 { return readBE32(pi.fanout[start : start+4]) } -func (pi *packIndex) offset(idx int) (uint64, error) { +func (pi *packIndex[T]) offset(idx int) (uint64, error) { start := idx * 4 word := readBE32(pi.offset32[start : start+4]) if word&0x80000000 == 0 { @@ -238,18 +239,20 @@ func (pi *packIndex) offset(idx int) (uint64, error) { return readBE64(pi.offset64[base : base+8]), nil } -func (pi *packIndex) lookup(id Hash) (packlocation, error) { +func (pi *packIndex[T]) lookup(id Hash[T]) (packlocation, error) { err := pi.ensureLoaded() if err != nil { return packlocation{}, err } - first := int(id[0]) + idSlice := id.Slice() + first := int(idSlice[0]) var lo int if first > 0 { lo = int(pi.fanoutEntry(first - 1)) } hi := int(pi.fanoutEntry(first)) - idx, found := bsearchHash(pi.names, pi.repo.HashSize, lo, hi, id) + stride := hashLen[T]() + idx, found := bsearchHash(pi.names, stride, lo, hi, idSlice) if !found { return packlocation{}, ErrNotFound } @@ -263,10 +266,10 @@ func (pi *packIndex) lookup(id Hash) (packlocation, error) { }, nil } -func bsearchHash(names []byte, stride, lo, hi int, want Hash) (int, bool) { +func bsearchHash(names []byte, stride, lo, hi int, want []byte) (int, bool) { for lo < hi { mid := lo + (hi-lo)/2 - cmp := compareHash(names, stride, mid, want[:stride]) + cmp := compareHash(names, stride, mid, want) if cmp == 0 { return mid, true } diff --git a/pack_midx.go b/pack_midx.go index 7f31e565..e91d5d43 100644 --- a/pack_midx.go +++ b/pack_midx.go @@ -1,6 +1,8 @@ package furgit import ( + "crypto/sha1" + "crypto/sha256" "os" "path/filepath" "strings" @@ -22,8 +24,8 @@ const ( chunkLOFF = 0x4c4f4646 // LOFF ) -type multiPackIndex struct { - repo *Repository +type multiPackIndex[T HashType] struct { + repo *Repository[T] loadOnce sync.Once loadErr error @@ -40,7 +42,7 @@ type multiPackIndex struct { closeOnce sync.Once } -func (midx *multiPackIndex) Close() error { +func (midx *multiPackIndex[T]) Close() error { if midx == nil { return nil } @@ -63,14 +65,14 @@ func (midx *multiPackIndex) Close() error { return closeErr } -func (midx *multiPackIndex) ensureLoaded() error { +func (midx *multiPackIndex[T]) ensureLoaded() error { midx.loadOnce.Do(func() { midx.loadErr = midx.load() }) return midx.loadErr } -func (midx *multiPackIndex) load() error { +func (midx *multiPackIndex[T]) load() error { if midx.repo == nil { return ErrInvalidObject } @@ -113,7 +115,18 @@ func (midx *multiPackIndex) load() error { return nil } -func (midx *multiPackIndex) parse(buf []byte) error { +func oidVersionFor[T HashType]() byte { + switch hashLen[T]() { + case sha1.Size: + return midxOIDVersionSHA1 + case sha256.Size: + return midxOIDVersionSHA256 + default: + panic("furgit: unsupported hash len") + } +} + +func (midx *multiPackIndex[T]) parse(buf []byte) error { if len(buf) < 12 { return ErrInvalidObject } @@ -125,7 +138,7 @@ func (midx *multiPackIndex) parse(buf []byte) error { return ErrInvalidObject } oidVersion := buf[5] - if oidVersion != midxOIDVersionSHA1 && oidVersion != midxOIDVersionSHA256 { + if oidVersion != oidVersionFor[T]() { return ErrInvalidObject } numChunks := int(buf[6]) @@ -197,7 +210,8 @@ func (midx *multiPackIndex) parse(buf []byte) error { if !ok { return ErrInvalidObject } - oidlSize := int64(numObjects) * int64(midx.repo.HashSize) + hashSize := midx.repo.hashSize() + oidlSize := int64(numObjects) * int64(hashSize) if oidlOffset < 0 || oidlOffset+oidlSize > int64(len(buf)) { return ErrInvalidObject } @@ -237,7 +251,7 @@ func (midx *multiPackIndex) parse(buf []byte) error { return nil } -func (midx *multiPackIndex) lookup(id Hash) (packlocation, error) { +func (midx *multiPackIndex[T]) lookup(id Hash[T]) (packlocation, error) { if len(midx.data) == 0 { err := midx.ensureLoaded() if err != nil { @@ -245,14 +259,16 @@ func (midx *multiPackIndex) lookup(id Hash) (packlocation, error) { } } - first := int(id[0]) + idSlice := id.Slice() + first := int(idSlice[0]) var lo int if first > 0 { lo = int(readBE32(midx.fanout[(first-1)*4 : first*4])) } hi := int(readBE32(midx.fanout[first*4 : (first+1)*4])) - idx, found := bsearchHash(midx.oids, midx.repo.HashSize, lo, hi, id) + stride := hashLen[T]() + idx, found := bsearchHash(midx.oids, stride, lo, hi, idSlice) if !found { return packlocation{}, ErrNotFound } @@ -288,15 +304,15 @@ func (midx *multiPackIndex) lookup(id Hash) (packlocation, error) { }, nil } -func (repo *Repository) multiPackIndex() (*multiPackIndex, error) { +func (repo *Repository[T]) multiPackIndex() (*multiPackIndex[T], error) { repo.midxOnce.Do(func() { repo.midx, repo.midxErr = repo.loadMultiPackIndex() }) return repo.midx, repo.midxErr } -func (repo *Repository) loadMultiPackIndex() (*multiPackIndex, error) { - midx := &multiPackIndex{repo: repo} +func (repo *Repository[T]) loadMultiPackIndex() (*multiPackIndex[T], error) { + midx := &multiPackIndex[T]{repo: repo} err := midx.ensureLoaded() if err != nil { if os.IsNotExist(err) { diff --git a/pack_pack.go b/pack_pack.go index 757c5c02..9643ba99 100644 --- a/pack_pack.go +++ b/pack_pack.go @@ -24,7 +24,7 @@ type packlocation struct { Offset uint64 } -func (repo *Repository) packRead(id Hash) (Object, error) { +func (repo *Repository[T]) packRead(id Hash[T]) (Object[T], error) { loc, err := repo.packIndexFind(id) if err != nil { return nil, err @@ -32,7 +32,7 @@ func (repo *Repository) packRead(id Hash) (Object, error) { return repo.packReadAt(loc, id) } -func (repo *Repository) packIndexFind(id Hash) (packlocation, error) { +func (repo *Repository[T]) packIndexFind(id Hash[T]) (packlocation, error) { midx, err := repo.multiPackIndex() if err == nil { loc, err := midx.lookup(id) @@ -63,22 +63,22 @@ func (repo *Repository) packIndexFind(id Hash) (packlocation, error) { return packlocation{}, ErrNotFound } -func (repo *Repository) packReadAt(loc packlocation, want Hash) (Object, error) { +func (repo *Repository[T]) packReadAt(loc packlocation, want Hash[T]) (Object[T], error) { ty, body, err := repo.packBodyResolveAtLocation(loc) if err != nil { return nil, err } data := body.Bytes() - if !verifyTypedObject(ty, data, want, repo.HashSize) { + if !verifyTypedObject[T](ty, data, want) { body.Release() return nil, ErrInvalidObject } - obj, err := parseObjectBody(ty, want, data, repo.HashSize) + obj, err := parseObjectBody[T](ty, want, data) body.Release() return obj, err } -func (repo *Repository) packBodyResolveAtLocation(loc packlocation) (ObjType, borrowedBody, error) { +func (repo *Repository[T]) packBodyResolveAtLocation(loc packlocation) (ObjType, borrowedBody, error) { pf, err := repo.packFile(loc.PackPath) if err != nil { return ObjInvalid, borrowedBody{}, err @@ -86,7 +86,7 @@ func (repo *Repository) packBodyResolveAtLocation(loc packlocation) (ObjType, bo return repo.packBodyResolveWithin(pf, loc.Offset) } -func (repo *Repository) packTypeSizeAtLocation(loc packlocation, seen map[packKey]struct{}) (ObjType, int64, error) { +func (repo *Repository[T]) packTypeSizeAtLocation(loc packlocation, seen map[packKey]struct{}) (ObjType, int64, error) { pf, err := repo.packFile(loc.PackPath) if err != nil { return ObjInvalid, 0, err @@ -94,7 +94,7 @@ func (repo *Repository) packTypeSizeAtLocation(loc packlocation, seen map[packKe return repo.packTypeSizeWithin(pf, loc.Offset, seen) } -func (repo *Repository) packTypeSizeByID(id Hash, seen map[packKey]struct{}) (ObjType, int64, error) { +func (repo *Repository[T]) packTypeSizeByID(id Hash[T], seen map[packKey]struct{}) (ObjType, int64, error) { loc, err := repo.packIndexFind(id) if err == nil { return repo.packTypeSizeAtLocation(loc, seen) @@ -172,7 +172,7 @@ func packSectionInflate(r io.Reader, sizeHint int) (borrowedBody, error) { } } -func (repo *Repository) packDeltaResolveOfs(pf *packFile, deltaOffset uint64, r io.Reader) (ObjType, borrowedBody, error) { +func (repo *Repository[T]) packDeltaResolveOfs(pf *packFile, deltaOffset uint64, r io.Reader) (ObjType, borrowedBody, error) { dist, err := packDeltaReadOfsDistance(r) if err != nil { return ObjInvalid, borrowedBody{}, err @@ -220,7 +220,7 @@ func packDeltaReadOfsDistance(r io.Reader) (uint64, error) { return dist, nil } -func (repo *Repository) packBodyResolveByID(id Hash) (ObjType, borrowedBody, error) { +func (repo *Repository[T]) packBodyResolveByID(id Hash[T]) (ObjType, borrowedBody, error) { loc, err := repo.packIndexFind(id) if err == nil { return repo.packBodyResolveAtLocation(loc) @@ -240,7 +240,7 @@ type packKey struct { ofs uint64 } -func (repo *Repository) packTypeSizeWithin(pf *packFile, ofs uint64, seen map[packKey]struct{}) (ObjType, int64, error) { +func (repo *Repository[T]) packTypeSizeWithin(pf *packFile, ofs uint64, seen map[packKey]struct{}) (ObjType, int64, error) { if pf == nil { return ObjInvalid, 0, ErrInvalidObject } @@ -268,8 +268,8 @@ func (repo *Repository) packTypeSizeWithin(pf *packFile, ofs uint64, seen map[pa case ObjCommit, ObjTree, ObjBlob, ObjTag: return ty, declaredSize, nil case ObjRefDelta: - var base Hash - _, err := io.ReadFull(r, base[:]) + var base Hash[T] + _, err := io.ReadFull(r, base.Slice()) if err != nil { return ObjInvalid, 0, err } @@ -299,7 +299,7 @@ func (repo *Repository) packTypeSizeWithin(pf *packFile, ofs uint64, seen map[pa } } -func (repo *Repository) packBodyResolveWithin(pf *packFile, ofs uint64) (ObjType, borrowedBody, error) { +func (repo *Repository[T]) packBodyResolveWithin(pf *packFile, ofs uint64) (ObjType, borrowedBody, error) { r, err := pf.cursor(ofs) if err != nil { return ObjInvalid, borrowedBody{}, err @@ -314,8 +314,8 @@ func (repo *Repository) packBodyResolveWithin(pf *packFile, ofs uint64) (ObjType body, err := packSectionInflate(r, size) return ty, body, err case ObjRefDelta: - var base Hash - _, err := io.ReadFull(r, base[:]) + var base Hash[T] + _, err := io.ReadFull(r, base.Slice()) if err != nil { return ObjInvalid, borrowedBody{}, err } diff --git a/pack_test.go b/pack_test.go index 48f4d604..89b521fb 100644 --- a/pack_test.go +++ b/pack_test.go @@ -153,20 +153,22 @@ 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]...) - idx, found := bsearchHash(names, testHashSize, 0, 2, h2) + names := append(append([]byte(nil), h1.Slice()[:testHashSize]...), h2.Slice()[:testHashSize]...) + idx, found := bsearchHash(names, testHashSize, 0, 2, h2.Slice()) if !found || idx != 1 { t.Fatalf("expected to find second hash, idx=%d found=%v", idx, found) } - _, found = bsearchHash(names, testHashSize, 0, 2, hashWithByte(0x05)) + h3 := hashWithByte(0x05) + _, found = bsearchHash(names, testHashSize, 0, 2, h3.Slice()) if found { t.Fatalf("did not expect to find unknown hash") } } -func buildTestPackIndexBuffer(hash Hash, offset uint32) []byte { +func buildTestPackIndexBuffer(hash TestHash, offset uint32) []byte { fanout := make([]byte, 256*4) - first := int(hash[0]) + hashSlice := hash.Slice() + first := int(hashSlice[0]) for i := 0; i < 256; i++ { var val uint32 if i >= first { @@ -178,7 +180,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(hashSlice[:testHashSize]) buf.Write(make([]byte, 4)) off32 := make([]byte, 4) binary.BigEndian.PutUint32(off32, offset) @@ -190,7 +192,7 @@ func buildTestPackIndexBuffer(hash Hash, offset uint32) []byte { func TestPackIndexParse(t *testing.T) { h := hashWithByte(0x11) data := buildTestPackIndexBuffer(h, 0x12345678) - pi := &packIndex{repo: &Repository{HashSize: testHashSize}} + pi := &packIndex[testHashType]{repo: &TestRepository{}} if err := pi.parse(data); err != nil { t.Fatalf("parse error: %v", err) } @@ -203,7 +205,7 @@ func TestPackIndexParse(t *testing.T) { } func TestPackIndexOffset64(t *testing.T) { - pi := &packIndex{} + pi := &packIndex[testHashType]{} pi.offset32 = make([]byte, 4) binary.BigEndian.PutUint32(pi.offset32, 0x80000000) pi.offset64 = make([]byte, 8) diff --git a/refs.go b/refs.go index b543a842..92837454 100644 --- a/refs.go +++ b/refs.go @@ -9,44 +9,45 @@ import ( ) // ResolveRef resolves a fully qualified ref name to its object ID. -func (repo *Repository) ResolveRef(refname string) (Hash, error) { +func (repo *Repository[T]) ResolveRef(refname string) (Hash[T], error) { id, err := repo.resolveLooseRef(refname) if err == nil { return id, nil } else if !errors.Is(err, ErrNotFound) { - return Hash{}, err + return Hash[T]{}, err } return repo.resolvePackedRef(refname) } -func (repo *Repository) resolveLooseRef(refname string) (Hash, error) { +func (repo *Repository[T]) resolveLooseRef(refname string) (Hash[T], error) { data, err := os.ReadFile(repo.repoPath(refname)) if err != nil { if os.IsNotExist(err) { - return Hash{}, ErrNotFound + return Hash[T]{}, ErrNotFound } - return Hash{}, err + return Hash[T]{}, err } line := strings.TrimSpace(string(data)) - id, err := ParseHashWithSize(line, repo.HashSize) + id, err := ParseHash[T](line) if err != nil { - return Hash{}, err + return Hash[T]{}, err } return id, nil } -func (repo *Repository) resolvePackedRef(refname string) (Hash, error) { +func (repo *Repository[T]) resolvePackedRef(refname string) (Hash[T], error) { path := repo.repoPath("packed-refs") f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { - return Hash{}, ErrInvalidObject + return Hash[T]{}, ErrInvalidObject } - return Hash{}, err + return Hash[T]{}, err } defer func() { _ = f.Close() }() + hashSize := repo.hashSize() want := []byte(refname) scanner := bufio.NewScanner(f) for scanner.Scan() { @@ -55,28 +56,28 @@ func (repo *Repository) resolvePackedRef(refname string) (Hash, error) { continue } sp := bytes.IndexByte(line, ' ') - if sp != repo.HashSize*2 { + if sp != hashSize*2 { continue } name := line[sp+1:] if bytes.Equal(name, want) { hex := string(line[:sp]) - id, err := ParseHashWithSize(hex, repo.HashSize) + id, err := ParseHash[T](hex) if err != nil { - return Hash{}, err + return Hash[T]{}, err } return id, nil } } scanErr := scanner.Err() if scanErr != nil { - return Hash{}, scanErr + return Hash[T]{}, scanErr } - return Hash{}, ErrInvalidObject + return Hash[T]{}, ErrInvalidObject } // ResolveHEAD reads HEAD and returns the ref that HEAD points to. -func (repo *Repository) ResolveHEAD() (string, error) { +func (repo *Repository[T]) ResolveHEAD() (string, error) { data, err := os.ReadFile(repo.repoPath("HEAD")) if err != nil { return "", err diff --git a/repo.go b/repo.go index fb835edb..50bbf44c 100644 --- a/repo.go +++ b/repo.go @@ -1,33 +1,31 @@ package furgit import ( - "fmt" "os" "path/filepath" "sync" ) // Repository represents the root of a Git repository. -type Repository struct { +type Repository[T HashType] struct { rootPath string - HashSize int packIdxOnce sync.Once - packIdx []*packIndex + packIdx []*packIndex[T] packIdxErr error midxOnce sync.Once - midx *multiPackIndex + midx *multiPackIndex[T] midxErr error packFiles sync.Map // string, *packFile closeOnce sync.Once } -// OpenRepository opens the repository at the provided path with the specified hash size. +// OpenRepository opens the repository at the provided path with the specified hash type. // This will be replaced later with a function that auto-detects the hash size based // on the git configuration. -func OpenRepository(path string, hashSize int) (*Repository, error) { +func OpenRepository[T HashType](path string) (*Repository[T], error) { fi, err := os.Stat(path) if err != nil { return nil, err @@ -35,13 +33,20 @@ func OpenRepository(path string, hashSize int) (*Repository, error) { if !fi.IsDir() { return nil, ErrInvalidObject } - if _, ok := hashFuncs[hashSize]; !ok { - return nil, fmt.Errorf("furgit: unsupported hash size %d", hashSize) - } - return &Repository{rootPath: path, HashSize: hashSize}, nil + return &Repository[T]{rootPath: path}, nil +} + +// hashSize returns the hash size for this repository. +func (r *Repository[T]) hashSize() int { + return hashLen[T]() +} + +// ParseHash is a convenience method for parsing hashes in the context of this repository. +func (r *Repository[T]) ParseHash(s string) (Hash[T], error) { + return ParseHash[T](s) } -func (r *Repository) Close() error { +func (r *Repository[T]) Close() error { var closeErr error r.closeOnce.Do(func() { r.packFiles.Range(func(keya any, pfa any) bool { @@ -73,16 +78,16 @@ func (r *Repository) Close() error { } // Root returns the repository root path. -func (r *Repository) Root() string { +func (r *Repository[T]) Root() string { return r.rootPath } // repoPath joins the root with a relative path. -func (r *Repository) repoPath(rel string) string { +func (r *Repository[T]) repoPath(rel string) string { return filepath.Join(r.rootPath, rel) } -func (r *Repository) packFile(rel string) (*packFile, error) { +func (r *Repository[T]) packFile(rel string) (*packFile, error) { if pf, ok := r.packFiles.Load(rel); ok { return pf.(*packFile), nil } diff --git a/repo_test.go b/repo_test.go index d09d2642..f8e24926 100644 --- a/repo_test.go +++ b/repo_test.go @@ -12,8 +12,8 @@ import ( "testing" ) -func writeLooseBlob(t *testing.T, repo *Repository, data []byte) Hash { - blob := &Blob{Data: data} +func writeLooseBlob(t *testing.T, repo *TestRepository, data []byte) TestHash { + blob := &TestBlob{Data: data} id, err := repo.WriteLooseObject(blob) if err != nil { t.Fatalf("WriteLooseObject: %v", err) @@ -23,7 +23,7 @@ func writeLooseBlob(t *testing.T, repo *Repository, data []byte) Hash { func TestOpenRepositoryAndLooseRead(t *testing.T) { root := t.TempDir() - repo, err := OpenRepository(root, testHashSize) + repo, err := OpenRepository[testHashType](root) if err != nil { t.Fatalf("OpenRepository error: %v", err) } @@ -34,7 +34,7 @@ func TestOpenRepositoryAndLooseRead(t *testing.T) { if err != nil { t.Fatalf("looseRead error: %v", err) } - blob, ok := obj.(*Blob) + blob, ok := obj.(*TestBlob) if !ok { t.Fatalf("expected Blob, got %T", obj) } @@ -45,7 +45,7 @@ func TestOpenRepositoryAndLooseRead(t *testing.T) { func TestResolveRefLooseAndPacked(t *testing.T) { root := t.TempDir() - repo, err := OpenRepository(root, testHashSize) + repo, err := OpenRepository[testHashType](root) if err != nil { t.Fatalf("OpenRepository error: %v", err) } @@ -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) } @@ -85,7 +85,7 @@ func TestResolveRefLooseAndPacked(t *testing.T) { func TestResolveHEAD(t *testing.T) { root := t.TempDir() - repo, err := OpenRepository(root, testHashSize) + repo, err := OpenRepository[testHashType](root) if err != nil { t.Fatalf("OpenRepository error: %v", err) } @@ -110,7 +110,7 @@ func TestResolveHEAD(t *testing.T) { func TestReadObjectTypeSizeLoose(t *testing.T) { t.Parallel() root := t.TempDir() - repo, err := OpenRepository(root, testHashSize) + repo, err := OpenRepository[testHashType](root) if err != nil { t.Fatalf("OpenRepository error: %v", err) } @@ -142,7 +142,7 @@ func TestReadObjectTypeSizePackedObjects(t *testing.T) { } ids := writeTestPack(t, root, "pack-basic", objs) - repo, err := OpenRepository(root, testHashSize) + repo, err := OpenRepository[testHashType](root) if err != nil { t.Fatalf("OpenRepository error: %v", err) } @@ -169,7 +169,7 @@ func TestReadObjectTypeSizePackRefDeltaLooseBase(t *testing.T) { t.Parallel() root := t.TempDir() - repo, err := OpenRepository(root, testHashSize) + repo, err := OpenRepository[testHashType](root) if err != nil { t.Fatalf("OpenRepository error: %v", err) } @@ -200,14 +200,14 @@ func TestReadObjectTypeSizePackRefDeltaLooseBase(t *testing.T) { func TestWriteLooseObjectAllTypes(t *testing.T) { root := t.TempDir() - repo, err := OpenRepository(root, testHashSize) + repo, err := OpenRepository[testHashType](root) if err != nil { t.Fatalf("OpenRepository error: %v", err) } t.Cleanup(func() { _ = repo.Close() }) // Blob - blob := &Blob{Data: []byte("test blob data")} + blob := &TestBlob{Data: []byte("test blob data")} blobID, err := repo.WriteLooseObject(blob) if err != nil { t.Fatalf("WriteLooseObject Blob error: %v", err) @@ -216,15 +216,15 @@ func TestWriteLooseObjectAllTypes(t *testing.T) { if err != nil { t.Fatalf("ReadObject Blob error: %v", err) } - if rb, ok := readBlob.(*Blob); !ok { + if rb, ok := readBlob.(*TestBlob); !ok { t.Fatalf("expected Blob, got %T", readBlob) } else if string(rb.Data) != "test blob data" { t.Fatalf("blob data mismatch: %q", rb.Data) } // Tree - tree := &Tree{ - Entries: []TreeEntry{ + tree := &TestTree{ + Entries: []TestTreeEntry{ {Mode: 0100644, Name: []byte("file.txt"), ID: blobID}, }, } @@ -236,14 +236,14 @@ func TestWriteLooseObjectAllTypes(t *testing.T) { if err != nil { t.Fatalf("ReadObject Tree error: %v", err) } - if rt, ok := readTree.(*Tree); !ok { + if rt, ok := readTree.(*TestTree); !ok { t.Fatalf("expected Tree, got %T", readTree) } else if len(rt.Entries) != 1 { t.Fatalf("tree entries mismatch: %d", len(rt.Entries)) } // Commit - commit := &Commit{ + commit := &TestCommit{ Tree: treeID, Author: Ident{ Name: []byte("Test Author"), @@ -267,14 +267,14 @@ func TestWriteLooseObjectAllTypes(t *testing.T) { if err != nil { t.Fatalf("ReadObject Commit error: %v", err) } - if rc, ok := readCommit.(*Commit); !ok { + if rc, ok := readCommit.(*TestCommit); !ok { t.Fatalf("expected Commit, got %T", readCommit) } else if rc.Tree != treeID { t.Fatalf("commit tree mismatch") } // Tag - tag := &Tag{ + tag := &TestTag{ Target: commitID, TargetType: ObjCommit, Name: []byte("v1.0.0"), @@ -294,7 +294,7 @@ func TestWriteLooseObjectAllTypes(t *testing.T) { if err != nil { t.Fatalf("ReadObject Tag error: %v", err) } - if rtag, ok := readTag.(*Tag); !ok { + if rtag, ok := readTag.(*TestTag); !ok { t.Fatalf("expected Tag, got %T", readTag) } else if rtag.Target != commitID { t.Fatalf("tag target mismatch") @@ -314,11 +314,11 @@ type testPackObject struct { body []byte encoding packObjectEncoding baseIndex int - baseHash Hash + baseHash TestHash baseBody []byte } -func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Hash { +func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []TestHash { t.Helper() packDir := filepath.Join(root, "objects", "pack") err := os.MkdirAll(packDir, 0o750) @@ -343,7 +343,7 @@ func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Has } offsets := make([]uint64, len(objs)) - ids := make([]Hash, len(objs)) + ids := make([]TestHash, len(objs)) for i, obj := range objs { offset := buf.Len() @@ -358,7 +358,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] = computeRawHash[testHashType](raw) switch obj.encoding { case packEncodingFull: @@ -375,7 +375,7 @@ func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Has delta := buildInsertOnlyDelta(len(baseBody), obj.body) buf.Write(compressBytes(t, delta)) case packEncodingRefDelta: - if obj.baseHash == (Hash{}) { + if obj.baseHash == (TestHash{}) { t.Fatalf("ref delta %d missing base hash", i) } baseBody := obj.baseBody @@ -383,7 +383,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.Slice()) delta := buildInsertOnlyDelta(len(baseBody), obj.body) buf.Write(compressBytes(t, delta)) default: @@ -392,8 +392,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 := computeRawHash[testHashType](packContent) + buf.Write(packChecksum.Slice()[:testHashSize]) packBytes := buf.Bytes() packPath := filepath.Join(packDir, name+".pack") @@ -406,10 +406,10 @@ func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Has return ids } -func writeTestPackIndex(t *testing.T, packDir, name string, ids []Hash, offsets []uint64, packChecksum Hash) { +func writeTestPackIndex(t *testing.T, packDir, name string, ids []TestHash, offsets []uint64, packChecksum TestHash) { t.Helper() type idxEntry struct { - id Hash + id TestHash offset uint64 } entries := make([]idxEntry, len(ids)) @@ -417,7 +417,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.Slice(), entries[j].id.Slice()) < 0 }) var buf bytes.Buffer @@ -432,7 +432,8 @@ func writeTestPackIndex(t *testing.T, packDir, name string, ids []Hash, offsets var fanout [256]uint32 for _, entry := range entries { - first := int(entry.id[0]) + entrySlice := entry.id.Slice() + first := int(entrySlice[0]) for i := first; i < len(fanout); i++ { fanout[i]++ } @@ -445,7 +446,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.Slice()[:testHashSize]) } buf.Write(make([]byte, len(entries)*4)) @@ -460,9 +461,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 := computeRawHash[testHashType](idxData) + buf.Write(packChecksum.Slice()[:testHashSize]) + buf.Write(idxChecksum.Slice()[:testHashSize]) idxPath := filepath.Join(packDir, name+".idx") err = os.WriteFile(idxPath, buf.Bytes(), 0o600) -- cgit v1.3.1-10-gc9f91