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)}
--
⑨