shithub: furgit

Download patch

ref: 6002485582541df9dff3e2c782a014564e22ed07
parent: 888f4af82ee72a47f50b71870ade0c788bcdfb4d
author: Runxi Yu <me@runxiyu.org>
date: Tue Jan 27 14:05:00 EST 2026

hash: Use a hashAlgorithmDetails struct for single source of truth

hashAlgorithm's are assumed to be valid; methods on invalid
hashAlgorithms will panic from out-of-bounds read when it's not found in
hashAlgorithmTable and that's expected and intended.

--- a/hash.go
+++ b/hash.go
@@ -6,31 +6,12 @@
 	"encoding/hex"
 )
 
-// maxHashSize MUST be equal to (or larger than) the size of the
-// largest hash supported in hashFuncs.
+// maxHashSize MUST be >= the largest supported algorithm size.
 const maxHashSize = sha256.Size
 
 // hashAlgorithm identifies the hash algorithm used for Git object IDs.
 type hashAlgorithm uint8
 
-// 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.algo = hashAlgoSHA1
-		return h
-	},
-	hashAlgoSHA256: func(data []byte) Hash {
-		sum := sha256.Sum256(data)
-		var h Hash
-		copy(h.data[:], sum[:])
-		h.algo = hashAlgoSHA256
-		return h
-	},
-}
-
 const (
 	hashAlgoUnknown hashAlgorithm = iota
 	hashAlgoSHA1
@@ -37,30 +18,64 @@
 	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
-	}
+type hashAlgorithmDetails struct {
+	name string
+	size int
+	sum  func([]byte) Hash
 }
 
+var hashAlgorithmTable = [...]hashAlgorithmDetails{
+	hashAlgoUnknown: {},
+	hashAlgoSHA1: {
+		name: "sha1",
+		size: sha1.Size,
+		sum: func(data []byte) Hash {
+			sum := sha1.Sum(data)
+			var h Hash
+			copy(h.data[:], sum[:])
+			h.algo = hashAlgoSHA1
+			return h
+		},
+	},
+	hashAlgoSHA256: {
+		name: "sha256",
+		size: sha256.Size,
+		sum: func(data []byte) Hash {
+			sum := sha256.Sum256(data)
+			var h Hash
+			copy(h.data[:], sum[:])
+			h.algo = hashAlgoSHA256
+			return h
+		},
+	},
+}
+
+func (algo hashAlgorithm) info() hashAlgorithmDetails {
+	return hashAlgorithmTable[algo]
+}
+
+// Size returns the hash size in bytes.
+func (algo hashAlgorithm) Size() int {
+	return algo.info().size
+}
+
 // 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:
+	inf := algo.info()
+	if inf.name == "" {
 		return "unknown"
 	}
+	return inf.name
 }
 
+func (algo hashAlgorithm) HexLen() int {
+	return algo.Size() * 2
+}
+
+func (algo hashAlgorithm) Sum(data []byte) Hash {
+	return algo.info().sum(data)
+}
+
 // Hash represents a Git object ID.
 type Hash struct {
 	algo hashAlgorithm
@@ -67,12 +82,9 @@
 	data [maxHashSize]byte
 }
 
-// hashFunc is a function that computes a hash from input data.
-type hashFunc func([]byte) Hash
-
 // String returns a hexadecimal string representation of the hash.
 func (hash Hash) String() string {
-	size := hash.algo.size()
+	size := hash.algo.Size()
 	if size == 0 {
 		return ""
 	}
@@ -81,7 +93,7 @@
 
 // Bytes returns a copy of the hash's bytes.
 func (hash Hash) Bytes() []byte {
-	size := hash.algo.size()
+	size := hash.algo.Size()
 	if size == 0 {
 		return nil
 	}
@@ -90,5 +102,21 @@
 
 // Size returns the hash size.
 func (hash Hash) Size() int {
-	return hash.algo.size()
+	return hash.algo.Size()
+}
+
+var algoByName = map[string]hashAlgorithm{}
+
+func init() {
+	for algo, info := range hashAlgorithmTable {
+		if info.name == "" {
+			continue
+		}
+		algoByName[info.name] = hashAlgorithm(algo)
+	}
+}
+
+func parseHashAlgorithm(s string) (hashAlgorithm, bool) {
+	algo, ok := algoByName[s]
+	return algo, ok
 }
--- a/hash_test.go
+++ b/hash_test.go
@@ -18,7 +18,7 @@
 
 	var validHash string
 	var expectedSize int
-	if repo.hashAlgo.size() == 32 {
+	if repo.hashAlgo.Size() == 32 {
 		validHash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
 		expectedSize = 32
 	} else {
--- a/obj_tree.go
+++ b/obj_tree.go
@@ -78,13 +78,13 @@
 		nameBytes := body[i : i+nul]
 		i += nul + 1
 
-		if i+repo.hashAlgo.size() > len(body) {
+		if i+repo.hashAlgo.Size() > len(body) {
 			return nil, errors.New("furgit: tree: truncated child hash")
 		}
 		var child Hash
-		copy(child.data[:], body[i:i+repo.hashAlgo.size()])
+		copy(child.data[:], body[i:i+repo.hashAlgo.Size()])
 		child.algo = repo.hashAlgo
-		i += repo.hashAlgo.size()
+		i += repo.hashAlgo.Size()
 
 		mode, err := strconv.ParseUint(string(modeBytes), 8, 32)
 		if err != nil {
--- a/pack_idx.go
+++ b/pack_idx.go
@@ -163,7 +163,7 @@
 	nobj := int(readBE32(pi.fanout[len(pi.fanout)-4:]))
 
 	namesStart := fanoutEnd
-	namesEnd := namesStart + nobj*pi.repo.hashAlgo.size()
+	namesEnd := namesStart + nobj*pi.repo.hashAlgo.Size()
 	if namesEnd > len(buf) {
 		return ErrInvalidObject
 	}
@@ -183,7 +183,7 @@
 	pi.offset32 = buf[off32Start:off32End]
 
 	off64Start := off32End
-	trailerStart := len(buf) - 2*pi.repo.hashAlgo.size()
+	trailerStart := len(buf) - 2*pi.repo.hashAlgo.Size()
 	if trailerStart < off64Start {
 		return ErrInvalidObject
 	}
@@ -253,7 +253,7 @@
 		lo = int(pi.fanoutEntry(first - 1))
 	}
 	hi := int(pi.fanoutEntry(first))
-	idx, found := bsearchHash(pi.names, pi.repo.hashAlgo.size(), lo, hi, id)
+	idx, found := bsearchHash(pi.names, pi.repo.hashAlgo.Size(), lo, hi, id)
 	if !found {
 		return packlocation{}, ErrNotFound
 	}
--- a/pack_pack.go
+++ b/pack_pack.go
@@ -176,7 +176,7 @@
 		case ObjectTypeCommit, ObjectTypeTree, ObjectTypeBlob, ObjectTypeTag:
 			return ty, declaredSize, nil
 		case ObjectTypeRefDelta:
-			hashEnd := dataStart + uint64(repo.hashAlgo.size())
+			hashEnd := dataStart + uint64(repo.hashAlgo.Size())
 			if hashEnd > uint64(len(pf.data)) {
 				return ObjectTypeInvalid, 0, io.ErrUnexpectedEOF
 			}
@@ -273,7 +273,7 @@
 			resultTy = ty
 			resolved = true
 		case ObjectTypeRefDelta:
-			hashEnd := dataStart + uint64(repo.hashAlgo.size())
+			hashEnd := dataStart + uint64(repo.hashAlgo.Size())
 			if hashEnd > uint64(len(pf.data)) {
 				return fail(io.ErrUnexpectedEOF)
 			}
--- a/refs.go
+++ b/refs.go
@@ -70,7 +70,7 @@
 		}
 
 		sp := bytes.IndexByte(line, ' ')
-		if sp != repo.hashAlgo.size()*2 {
+		if sp != repo.hashAlgo.Size()*2 {
 			continue
 		}
 
@@ -428,7 +428,7 @@
 		}
 
 		sp := bytes.IndexByte(line, ' ')
-		if sp != repo.hashAlgo.size()*2 {
+		if sp != repo.hashAlgo.Size()*2 {
 			lastIdx = -1
 			continue
 		}
--- a/repo.go
+++ b/repo.go
@@ -63,23 +63,11 @@
 		algo = "sha1"
 	}
 
-	var hashAlgo hashAlgorithm
-	switch algo {
-	case "sha1":
-		hashAlgo = hashAlgoSHA1
-	case "sha256":
-		hashAlgo = hashAlgoSHA256
-	default:
+	hashAlgo, ok := parseHashAlgorithm(algo)
+	if !ok {
 		return nil, fmt.Errorf("furgit: unsupported hash algorithm %q", algo)
 	}
 
-	if hashAlgo.size() == 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,
@@ -130,9 +118,9 @@
 	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 := repo.hashAlgo.size() * 2
+	expectedLen := repo.hashAlgo.Size() * 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, repo.hashAlgo.size())
+		return id, fmt.Errorf("furgit: hash length mismatch: got %d chars, expected %d for hash size %d", len(s), expectedLen, repo.hashAlgo.Size())
 	}
 	data, err := hex.DecodeString(s)
 	if err != nil {
@@ -145,8 +133,7 @@
 
 // computeRawHash computes a hash from raw data using the repository's hash algorithm.
 func (repo *Repository) computeRawHash(data []byte) Hash {
-	hashFunc := hashFuncs[repo.hashAlgo]
-	return hashFunc(data)
+	return repo.hashAlgo.Sum(data)
 }
 
 // verifyRawObject verifies a raw object against its expected hash.
--- a/repo_test.go
+++ b/repo_test.go
@@ -17,7 +17,7 @@
 	if repo.rootPath != repoPath {
 		t.Errorf("rootPath: got %q, want %q", repo.rootPath, repoPath)
 	}
-	hashSize := repo.hashAlgo.size()
+	hashSize := repo.hashAlgo.Size()
 	if hashSize != 32 && hashSize != 20 {
 		t.Errorf("hashSize: got %d, want 32 (SHA-256) or 20 (SHA-1)", hashSize)
 	}
--