shithub: furgit

Download patch

ref: 7a0ab5f77917a36a87945f6a88b036b9b6ba88ee
parent: c1592fb8ba14a443ac7e575dbd41f3aa358f885d
author: Runxi Yu <runxiyu@umich.edu>
date: Sat Jan 17 17:56:53 EST 2026

hash: Key by algorithm, not size

--- a/hash.go
+++ b/hash.go
@@ -8,9 +8,42 @@
 
 const maxHashSize = 32
 
+// hashAlgorithm identifies the hash algorithm used for Git object IDs.
+type hashAlgorithm uint8
+
+const (
+	hashAlgoUnknown hashAlgorithm = iota
+	hashAlgoSHA1
+	hashAlgoSHA256
+)
+
+// size returns the hash size in bytes.
+func (algo hashAlgorithm) size() int {
+	switch algo {
+	case hashAlgoSHA1:
+		return sha1.Size
+	case hashAlgoSHA256:
+		return sha256.Size
+	default:
+		return 0
+	}
+}
+
+// String returns the canonical name of the hash algorithm.
+func (algo hashAlgorithm) String() string {
+	switch algo {
+	case hashAlgoSHA1:
+		return "sha1"
+	case hashAlgoSHA256:
+		return "sha256"
+	default:
+		return "unknown"
+	}
+}
+
 // Hash represents a Git object ID.
 type Hash struct {
-	size int
+	algo hashAlgorithm
 	data [maxHashSize]byte
 }
 
@@ -17,20 +50,20 @@
 // hashFunc is a function that computes a hash from input data.
 type hashFunc func([]byte) Hash
 
-// hashFuncs maps hash size to hash function.
-var hashFuncs = map[int]hashFunc{
-	sha1.Size: func(data []byte) Hash {
+// hashFuncs maps hash algorithm to hash function.
+var hashFuncs = map[hashAlgorithm]hashFunc{
+	hashAlgoSHA1: func(data []byte) Hash {
 		sum := sha1.Sum(data)
 		var h Hash
 		copy(h.data[:], sum[:])
-		h.size = sha1.Size
+		h.algo = hashAlgoSHA1
 		return h
 	},
-	sha256.Size: func(data []byte) Hash {
+	hashAlgoSHA256: func(data []byte) Hash {
 		sum := sha256.Sum256(data)
 		var h Hash
 		copy(h.data[:], sum[:])
-		h.size = sha256.Size
+		h.algo = hashAlgoSHA256
 		return h
 	},
 }
@@ -37,15 +70,23 @@
 
 // String returns a hexadecimal string representation of the hash.
 func (hash Hash) String() string {
-	return hex.EncodeToString(hash.data[:hash.size])
+	size := hash.algo.size()
+	if size == 0 {
+		return ""
+	}
+	return hex.EncodeToString(hash.data[:size])
 }
 
 // Bytes returns a copy of the hash's bytes.
 func (hash Hash) Bytes() []byte {
-	return append([]byte(nil), hash.data[:hash.size]...)
+	size := hash.algo.size()
+	if size == 0 {
+		return nil
+	}
+	return append([]byte(nil), hash.data[:size]...)
 }
 
 // Size returns the hash size.
 func (hash Hash) Size() int {
-	return hash.size
+	return hash.algo.size()
 }
--- a/loose.go
+++ b/loose.go
@@ -17,8 +17,8 @@
 
 // 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)
+	if id.algo != repo.hashAlgo {
+		return "", fmt.Errorf("furgit: hash algorithm mismatch: got %s, expected %s", id.algo.String(), repo.hashAlgo.String())
 	}
 	hex := id.String()
 	return filepath.Join("objects", hex[:2], hex[2:]), nil
--- a/obj_tree.go
+++ b/obj_tree.go
@@ -83,7 +83,7 @@
 		}
 		var child Hash
 		copy(child.data[:], body[i:i+repo.hashSize])
-		child.size = repo.hashSize
+		child.algo = repo.hashAlgo
 		i += repo.hashSize
 
 		mode, err := strconv.ParseUint(string(modeBytes), 8, 32)
@@ -112,7 +112,7 @@
 	var bodyLen int
 	for _, e := range tree.Entries {
 		mode := strconv.FormatUint(uint64(e.Mode), 8)
-		bodyLen += len(mode) + 1 + len(e.Name) + 1 + e.ID.size
+		bodyLen += len(mode) + 1 + len(e.Name) + 1 + e.ID.Size()
 	}
 
 	body := make([]byte, bodyLen)
@@ -125,7 +125,8 @@
 		pos += copy(body[pos:], e.Name)
 		body[pos] = 0
 		pos++
-		pos += copy(body[pos:], e.ID.data[:e.ID.size])
+		size := e.ID.Size()
+		pos += copy(body[pos:], e.ID.data[:size])
 	}
 
 	return body
--- a/pack_idx.go
+++ b/pack_idx.go
@@ -244,8 +244,8 @@
 	if err != nil {
 		return packlocation{}, err
 	}
-	if id.size != pi.repo.hashSize {
-		return packlocation{}, fmt.Errorf("furgit: hash size mismatch: got %d, expected %d", id.size, pi.repo.hashSize)
+	if id.algo != pi.repo.hashAlgo {
+		return packlocation{}, fmt.Errorf("furgit: hash algorithm mismatch: got %s, expected %s", id.algo.String(), pi.repo.hashAlgo.String())
 	}
 	first := int(id.data[0])
 	var lo int
--- a/pack_pack.go
+++ b/pack_pack.go
@@ -182,7 +182,7 @@
 			}
 			var base Hash
 			copy(base.data[:], pf.data[dataStart:hashEnd])
-			base.size = repo.hashSize
+			base.algo = repo.hashAlgo
 			loc, err := repo.packIndexFind(base)
 			if err == nil {
 				pf, err = repo.packFile(loc.PackPath)
@@ -279,7 +279,7 @@
 			}
 			var base Hash
 			copy(base.data[:], pf.data[dataStart:hashEnd])
-			base.size = repo.hashSize
+			base.algo = repo.hashAlgo
 			delta, err := packSectionInflate(pf, hashEnd, 0)
 			if err != nil {
 				return fail(err)
--- a/repo.go
+++ b/repo.go
@@ -1,8 +1,6 @@
 package furgit
 
 import (
-	"crypto/sha1"
-	"crypto/sha256"
 	"encoding/hex"
 	"fmt"
 	"os"
@@ -21,6 +19,7 @@
 // has been closed.
 type Repository struct {
 	rootPath string
+	hashAlgo hashAlgorithm
 	hashSize int
 
 	packIdxOnce sync.Once
@@ -65,22 +64,27 @@
 		algo = "sha1"
 	}
 
-	var hashSize int
+	var hashAlgo hashAlgorithm
 	switch algo {
 	case "sha1":
-		hashSize = sha1.Size
+		hashAlgo = hashAlgoSHA1
 	case "sha256":
-		hashSize = sha256.Size
+		hashAlgo = hashAlgoSHA256
 	default:
 		return nil, fmt.Errorf("furgit: unsupported hash algorithm %q", algo)
 	}
 
-	if _, ok := hashFuncs[hashSize]; !ok {
+	hashSize := hashAlgo.size()
+	if hashSize == 0 {
+		return nil, fmt.Errorf("furgit: unsupported hash algorithm %q", algo)
+	}
+	if _, ok := hashFuncs[hashAlgo]; !ok {
 		return nil, fmt.Errorf("furgit: hash algorithm %q is not supported by the hash functions provided by this build", algo)
 	}
 
 	return &Repository{
 		rootPath:  path,
+		hashAlgo:  hashAlgo,
 		hashSize:  hashSize,
 		packFiles: make(map[string]*packFile),
 	}, nil
@@ -138,19 +142,19 @@
 		return id, fmt.Errorf("furgit: decode hash: %w", err)
 	}
 	copy(id.data[:], data)
-	id.size = len(s) / 2
+	id.algo = repo.hashAlgo
 	return id, nil
 }
 
 // computeRawHash computes a hash from raw data using the repository's hash algorithm.
 func (repo *Repository) computeRawHash(data []byte) Hash {
-	hashFunc := hashFuncs[repo.hashSize]
+	hashFunc := hashFuncs[repo.hashAlgo]
 	return hashFunc(data)
 }
 
 // verifyRawObject verifies a raw object against its expected hash.
 func (repo *Repository) verifyRawObject(buf []byte, want Hash) bool { //nolint:unused
-	if want.size != repo.hashSize {
+	if want.algo != repo.hashAlgo {
 		return false
 	}
 	return repo.computeRawHash(buf) == want
@@ -158,7 +162,7 @@
 
 // verifyTypedObject verifies a typed object against its expected hash.
 func (repo *Repository) verifyTypedObject(ty ObjectType, body []byte, want Hash) bool { //nolint:unused
-	if want.size != repo.hashSize {
+	if want.algo != repo.hashAlgo {
 		return false
 	}
 	header, err := headerForType(ty, body)
--