diff options
29 files changed, 290 insertions, 264 deletions
diff --git a/format/commitgraph/read/layer_lookup.go b/format/commitgraph/read/layer_lookup.go index 84095788..fafc594b 100644 --- a/format/commitgraph/read/layer_lookup.go +++ b/format/commitgraph/read/layer_lookup.go @@ -9,7 +9,7 @@ import ( ) func layerLookup(layer *layer, oid objectid.ObjectID) (uint32, bool) { - hashSize := oid.Size() + hashSize := oid.Algorithm().Size() first := int(oid.RawBytes()[0]) var lo uint32 diff --git a/object/commit/serialize.go b/object/commit/serialize.go index 721cacf6..3f141550 100644 --- a/object/commit/serialize.go +++ b/object/commit/serialize.go @@ -13,7 +13,7 @@ import ( func (commit *Commit) SerializeWithoutHeader() ([]byte, error) { var buf bytes.Buffer - if commit.Tree.Size() == 0 { + if commit.Tree.Algorithm().Size() == 0 { return nil, errors.New("object: commit: missing tree id") } diff --git a/object/id/algorithm.go b/object/id/algorithm.go new file mode 100644 index 00000000..a695889c --- /dev/null +++ b/object/id/algorithm.go @@ -0,0 +1,12 @@ +package objectid + +//#nosec gosec + +// Algorithm identifies the hash algorithm used for Git object IDs. +type Algorithm uint8 + +const ( + AlgorithmUnknown Algorithm = iota + AlgorithmSHA1 + AlgorithmSHA256 +) diff --git a/object/id/algorithm_details.go b/object/id/algorithm_details.go new file mode 100644 index 00000000..fbdc3c78 --- /dev/null +++ b/object/id/algorithm_details.go @@ -0,0 +1,16 @@ +package objectid + +import "hash" + +type algorithmDetails struct { + name string + size int + packHashID uint32 + sum func([]byte) ObjectID + new func() hash.Hash + emptyTree ObjectID +} + +func (algo Algorithm) info() algorithmDetails { + return algorithmTable[algo] +} diff --git a/object/id/algorithm_emptytree.go b/object/id/algorithm_emptytree.go new file mode 100644 index 00000000..32f57385 --- /dev/null +++ b/object/id/algorithm_emptytree.go @@ -0,0 +1,7 @@ +package objectid + +// EmptyTree returns the object ID of an empty tree ("tree 0\x00") for this +// algorithm. +func (algo Algorithm) EmptyTree() ObjectID { + return algo.info().emptyTree +} diff --git a/object/id/algorithm_hexlen.go b/object/id/algorithm_hexlen.go new file mode 100644 index 00000000..2b7fa0fa --- /dev/null +++ b/object/id/algorithm_hexlen.go @@ -0,0 +1,6 @@ +package objectid + +// HexLen returns the encoded hexadecimal length. +func (algo Algorithm) HexLen() int { + return algo.Size() * 2 +} diff --git a/object/id/algorithm_new.go b/object/id/algorithm_new.go new file mode 100644 index 00000000..8abbaeda --- /dev/null +++ b/object/id/algorithm_new.go @@ -0,0 +1,13 @@ +package objectid + +import "hash" + +// New returns a new hash.Hash for this algorithm. +func (algo Algorithm) New() (hash.Hash, error) { + newFn := algo.info().new + if newFn == nil { + return nil, ErrInvalidAlgorithm + } + + return newFn(), nil +} diff --git a/object/id/algorithm_packhashid.go b/object/id/algorithm_packhashid.go new file mode 100644 index 00000000..93c0f61b --- /dev/null +++ b/object/id/algorithm_packhashid.go @@ -0,0 +1,8 @@ +package objectid + +// PackHashID returns the Git pack/rev hash-id encoding for this algorithm. +// +// Unknown algorithms return 0. +func (algo Algorithm) PackHashID() uint32 { + return algo.info().packHashID +} diff --git a/object/id/algorithm_parse.go b/object/id/algorithm_parse.go new file mode 100644 index 00000000..d5fb0c64 --- /dev/null +++ b/object/id/algorithm_parse.go @@ -0,0 +1,8 @@ +package objectid + +// ParseAlgorithm parses a canonical algorithm name (e.g. "sha1", "sha256"). +func ParseAlgorithm(s string) (Algorithm, bool) { + algo, ok := algorithmByName[s] + + return algo, ok +} diff --git a/object/id/algorithm_size.go b/object/id/algorithm_size.go new file mode 100644 index 00000000..104bfeb2 --- /dev/null +++ b/object/id/algorithm_size.go @@ -0,0 +1,6 @@ +package objectid + +// Size returns the hash size in bytes. +func (algo Algorithm) Size() int { + return algo.info().size +} diff --git a/object/id/algorithm_string.go b/object/id/algorithm_string.go new file mode 100644 index 00000000..410ee8a3 --- /dev/null +++ b/object/id/algorithm_string.go @@ -0,0 +1,11 @@ +package objectid + +// String returns the canonical algorithm name. +func (algo Algorithm) String() string { + inf := algo.info() + if inf.name == "" { + return "unknown" + } + + return inf.name +} diff --git a/object/id/algorithm_sum.go b/object/id/algorithm_sum.go new file mode 100644 index 00000000..26ad2ff6 --- /dev/null +++ b/object/id/algorithm_sum.go @@ -0,0 +1,6 @@ +package objectid + +// Sum computes an object ID from raw data using the selected algorithm. +func (algo Algorithm) Sum(data []byte) ObjectID { + return algo.info().sum(data) +} diff --git a/object/id/algorithm_supported.go b/object/id/algorithm_supported.go new file mode 100644 index 00000000..1f61e771 --- /dev/null +++ b/object/id/algorithm_supported.go @@ -0,0 +1,7 @@ +package objectid + +// SupportedAlgorithms returns all object ID algorithms supported by furgit. +// Do not mutate. +func SupportedAlgorithms() []Algorithm { + return supportedAlgorithms +} diff --git a/object/id/algorithm_tables.go b/object/id/algorithm_tables.go new file mode 100644 index 00000000..86e1341e --- /dev/null +++ b/object/id/algorithm_tables.go @@ -0,0 +1,63 @@ +package objectid + +import ( + "crypto/sha1" + "crypto/sha256" +) + +//nolint:gochecknoglobals +var algorithmTable = [...]algorithmDetails{ + AlgorithmUnknown: {}, + AlgorithmSHA1: { + name: "sha1", + size: sha1.Size, + packHashID: 1, + sum: func(data []byte) ObjectID { + sum := sha1.Sum(data) //#nosec G401 + + var id ObjectID + copy(id.data[:], sum[:]) + id.algo = AlgorithmSHA1 + + return id + }, + new: sha1.New, + }, + AlgorithmSHA256: { + name: "sha256", + size: sha256.Size, + packHashID: 2, + sum: func(data []byte) ObjectID { + sum := sha256.Sum256(data) + + var id ObjectID + copy(id.data[:], sum[:]) + id.algo = AlgorithmSHA256 + + return id + }, + new: sha256.New, + }, +} + +var ( + //nolint:gochecknoglobals + algorithmByName = map[string]Algorithm{} + //nolint:gochecknoglobals + supportedAlgorithms []Algorithm +) + +func init() { //nolint:gochecknoinits + emptyTreeInput := []byte("tree 0\x00") + + for algo := Algorithm(0); int(algo) < len(algorithmTable); algo++ { + info := &algorithmTable[algo] + if info.name == "" { + continue + } + + info.emptyTree = info.sum(emptyTreeInput) + algorithmByName[info.name] = algo + supportedAlgorithms = append(supportedAlgorithms, algo) + } +} diff --git a/object/id/algorithms.go b/object/id/algorithms.go deleted file mode 100644 index f3540c42..00000000 --- a/object/id/algorithms.go +++ /dev/null @@ -1,150 +0,0 @@ -package objectid - -import ( - "crypto/sha1" //#nosec gosec - "crypto/sha256" - "hash" -) - -// maxObjectIDSize MUST be >= the largest supported algorithm size. -const maxObjectIDSize = sha256.Size - -// Algorithm identifies the hash algorithm used for Git object IDs. -type Algorithm uint8 - -const ( - AlgorithmUnknown Algorithm = iota - AlgorithmSHA1 - AlgorithmSHA256 -) - -type algorithmDetails struct { - name string - size int - packHashID uint32 - sum func([]byte) ObjectID - new func() hash.Hash - emptyTree ObjectID -} - -//nolint:gochecknoglobals -var algorithmTable = [...]algorithmDetails{ - AlgorithmUnknown: {}, - AlgorithmSHA1: { - name: "sha1", - size: sha1.Size, - packHashID: 1, - sum: func(data []byte) ObjectID { - sum := sha1.Sum(data) //#nosec G401 - - var id ObjectID - copy(id.data[:], sum[:]) - id.algo = AlgorithmSHA1 - - return id - }, - new: sha1.New, - }, - AlgorithmSHA256: { - name: "sha256", - size: sha256.Size, - packHashID: 2, - sum: func(data []byte) ObjectID { - sum := sha256.Sum256(data) - - var id ObjectID - copy(id.data[:], sum[:]) - id.algo = AlgorithmSHA256 - - return id - }, - new: sha256.New, - }, -} - -var ( - //nolint:gochecknoglobals - algorithmByName = map[string]Algorithm{} - //nolint:gochecknoglobals - supportedAlgorithms []Algorithm -) - -func init() { //nolint:gochecknoinits - emptyTreeInput := []byte("tree 0\x00") - - for algo := Algorithm(0); int(algo) < len(algorithmTable); algo++ { - info := &algorithmTable[algo] - if info.name == "" { - continue - } - - info.emptyTree = info.sum(emptyTreeInput) - algorithmByName[info.name] = algo - supportedAlgorithms = append(supportedAlgorithms, algo) - } -} - -// SupportedAlgorithms returns all object ID algorithms supported by furgit. -// Do not mutate. -func SupportedAlgorithms() []Algorithm { - return supportedAlgorithms -} - -// ParseAlgorithm parses a canonical algorithm name (e.g. "sha1", "sha256"). -func ParseAlgorithm(s string) (Algorithm, bool) { - algo, ok := algorithmByName[s] - - return algo, ok -} - -// Size returns the hash size in bytes. -func (algo Algorithm) Size() int { - return algo.info().size -} - -// String returns the canonical algorithm name. -func (algo Algorithm) String() string { - inf := algo.info() - if inf.name == "" { - return "unknown" - } - - return inf.name -} - -// HexLen returns the encoded hexadecimal length. -func (algo Algorithm) HexLen() int { - return algo.Size() * 2 -} - -// PackHashID returns the Git pack/rev hash-id encoding for this algorithm. -// -// Unknown algorithms return 0. -func (algo Algorithm) PackHashID() uint32 { - return algo.info().packHashID -} - -// Sum computes an object ID from raw data using the selected algorithm. -func (algo Algorithm) Sum(data []byte) ObjectID { - return algo.info().sum(data) -} - -// New returns a new hash.Hash for this algorithm. -func (algo Algorithm) New() (hash.Hash, error) { - newFn := algo.info().new - if newFn == nil { - return nil, ErrInvalidAlgorithm - } - - return newFn(), nil -} - -// EmptyTree returns the object ID of an empty tree ("tree 0\x00") for this -// algorithm. -func (algo Algorithm) EmptyTree() ObjectID { - return algo.info().emptyTree -} - -func (algo Algorithm) info() algorithmDetails { - return algorithmTable[algo] -} diff --git a/object/id/max_size.go b/object/id/max_size.go new file mode 100644 index 00000000..d2a64a10 --- /dev/null +++ b/object/id/max_size.go @@ -0,0 +1,6 @@ +package objectid + +import "crypto/sha256" + +// maxObjectIDSize MUST be >= the largest supported algorithm size. +const maxObjectIDSize = sha256.Size diff --git a/object/id/objectid.go b/object/id/objectid.go index 301eb8e6..33a54225 100644 --- a/object/id/objectid.go +++ b/object/id/objectid.go @@ -1,12 +1,6 @@ package objectid -import ( - //#nosec G505 - - "bytes" - "encoding/hex" - "fmt" -) +//#nosec G505 // ObjectID represents a Git object ID. // @@ -15,98 +9,3 @@ type ObjectID struct { algo Algorithm data [maxObjectIDSize]byte } - -// Algorithm returns the object ID's hash algorithm. -func (id ObjectID) Algorithm() Algorithm { - return id.algo -} - -// Size returns the object ID size in bytes. -func (id ObjectID) Size() int { - return id.algo.Size() -} - -// String returns the canonical hex representation. -func (id ObjectID) String() string { - size := id.Size() - - return hex.EncodeToString(id.data[:size]) -} - -// Bytes returns a copy of the object ID bytes. -func (id ObjectID) Bytes() []byte { - size := id.Size() - - return append([]byte(nil), id.data[:size]...) -} - -// RawBytes returns a direct byte slice view of the object ID bytes. -// -// The returned slice aliases the object ID's internal storage. Callers MUST -// treat it as read-only and MUST NOT modify its contents. -// -// Use Bytes when an independent copy is required. -func (id *ObjectID) RawBytes() []byte { - size := id.Size() - - return id.data[:size:size] -} - -// Compare lexicographically compares two object IDs by their canonical byte -// representation. -func Compare(left, right ObjectID) int { - return bytes.Compare(left.RawBytes(), right.RawBytes()) -} - -// Zero returns the all-zero object ID for the specified algorithm. -func Zero(algo Algorithm) ObjectID { - id, err := FromBytes(algo, make([]byte, algo.Size())) - if err != nil { - panic(err) - } - - return id -} - -// ParseHex parses an object ID from hex for the specified algorithm. -func ParseHex(algo Algorithm, s string) (ObjectID, error) { - var id ObjectID - if algo.Size() == 0 { - return id, ErrInvalidAlgorithm - } - - if len(s)%2 != 0 { - return id, fmt.Errorf("%w: odd hex length %d", ErrInvalidObjectID, len(s)) - } - - if len(s) != algo.HexLen() { - return id, fmt.Errorf("%w: got %d chars, expected %d", ErrInvalidObjectID, len(s), algo.HexLen()) - } - - decoded, err := hex.DecodeString(s) - if err != nil { - return id, fmt.Errorf("%w: decode: %w", ErrInvalidObjectID, err) - } - - copy(id.data[:], decoded) - id.algo = algo - - return id, nil -} - -// FromBytes builds an object ID from raw bytes for the specified algorithm. -func FromBytes(algo Algorithm, b []byte) (ObjectID, error) { - var id ObjectID - if algo.Size() == 0 { - return id, ErrInvalidAlgorithm - } - - if len(b) != algo.Size() { - return id, fmt.Errorf("%w: got %d bytes, expected %d", ErrInvalidObjectID, len(b), algo.Size()) - } - - copy(id.data[:], b) - id.algo = algo - - return id, nil -} diff --git a/object/id/objectid_algorithm.go b/object/id/objectid_algorithm.go new file mode 100644 index 00000000..cb694b7c --- /dev/null +++ b/object/id/objectid_algorithm.go @@ -0,0 +1,6 @@ +package objectid + +// Algorithm returns the object ID's hash algorithm. +func (id ObjectID) Algorithm() Algorithm { + return id.algo +} diff --git a/object/id/objectid_byte.go b/object/id/objectid_byte.go new file mode 100644 index 00000000..8384ff45 --- /dev/null +++ b/object/id/objectid_byte.go @@ -0,0 +1,20 @@ +package objectid + +// Bytes returns a copy of the object ID bytes. +func (id ObjectID) Bytes() []byte { + size := id.Algorithm().Size() + + return append([]byte(nil), id.data[:size]...) +} + +// RawBytes returns a direct byte slice view of the object ID bytes. +// +// The returned slice aliases the object ID's internal storage. Callers MUST +// treat it as read-only and MUST NOT modify its contents. +// +// Use Bytes when an independent copy is required. +func (id *ObjectID) RawBytes() []byte { + size := id.Algorithm().Size() + + return id.data[:size:size] +} diff --git a/object/id/objectid_compare.go b/object/id/objectid_compare.go new file mode 100644 index 00000000..a40bcc89 --- /dev/null +++ b/object/id/objectid_compare.go @@ -0,0 +1,9 @@ +package objectid + +import "bytes" + +// Compare lexicographically compares two object IDs by their canonical byte +// representation. +func Compare(left, right ObjectID) int { + return bytes.Compare(left.RawBytes(), right.RawBytes()) +} diff --git a/object/id/objectid_frombytes.go b/object/id/objectid_frombytes.go new file mode 100644 index 00000000..ea8dacfe --- /dev/null +++ b/object/id/objectid_frombytes.go @@ -0,0 +1,20 @@ +package objectid + +import "fmt" + +// FromBytes builds an object ID from raw bytes for the specified algorithm. +func FromBytes(algo Algorithm, b []byte) (ObjectID, error) { + var id ObjectID + if algo.Size() == 0 { + return id, ErrInvalidAlgorithm + } + + if len(b) != algo.Size() { + return id, fmt.Errorf("%w: got %d bytes, expected %d", ErrInvalidObjectID, len(b), algo.Size()) + } + + copy(id.data[:], b) + id.algo = algo + + return id, nil +} diff --git a/object/id/objectid_parse.go b/object/id/objectid_parse.go new file mode 100644 index 00000000..e6cbb641 --- /dev/null +++ b/object/id/objectid_parse.go @@ -0,0 +1,32 @@ +package objectid + +import ( + "encoding/hex" + "fmt" +) + +// ParseHex parses an object ID from hex for the specified algorithm. +func ParseHex(algo Algorithm, s string) (ObjectID, error) { + var id ObjectID + if algo.Size() == 0 { + return id, ErrInvalidAlgorithm + } + + if len(s)%2 != 0 { + return id, fmt.Errorf("%w: odd hex length %d", ErrInvalidObjectID, len(s)) + } + + if len(s) != algo.HexLen() { + return id, fmt.Errorf("%w: got %d chars, expected %d", ErrInvalidObjectID, len(s), algo.HexLen()) + } + + decoded, err := hex.DecodeString(s) + if err != nil { + return id, fmt.Errorf("%w: decode: %w", ErrInvalidObjectID, err) + } + + copy(id.data[:], decoded) + id.algo = algo + + return id, nil +} diff --git a/object/id/objectid_string.go b/object/id/objectid_string.go new file mode 100644 index 00000000..36a7177d --- /dev/null +++ b/object/id/objectid_string.go @@ -0,0 +1,10 @@ +package objectid + +import "encoding/hex" + +// String returns the canonical hex representation. +func (id ObjectID) String() string { + size := id.Algorithm().Size() + + return hex.EncodeToString(id.data[:size]) +} diff --git a/object/id/objectid_test.go b/object/id/objectid_test.go index dc25832b..9d179fb5 100644 --- a/object/id/objectid_test.go +++ b/object/id/objectid_test.go @@ -44,7 +44,7 @@ func TestParseHexRoundtrip(t *testing.T) { t.Fatalf("String() = %q, want %q", got, hex) } - if got := id.Size(); got != algo.Size() { + if got := id.Algorithm().Size(); got != algo.Size() { t.Fatalf("Size() = %d, want %d", got, algo.Size()) } @@ -148,8 +148,8 @@ func TestRawBytesAliasesStorage(t *testing.T) { } b := id.RawBytes() - if len(b) != id.Size() { - t.Fatalf("RawBytes len = %d, want %d", len(b), id.Size()) + if len(b) != id.Algorithm().Size() { + t.Fatalf("RawBytes len = %d, want %d", len(b), id.Algorithm().Size()) } if cap(b) != len(b) { @@ -169,12 +169,12 @@ func TestAlgorithmSum(t *testing.T) { t.Parallel() id1 := objectid.AlgorithmSHA1.Sum([]byte("hello")) - if id1.Algorithm() != objectid.AlgorithmSHA1 || id1.Size() != objectid.AlgorithmSHA1.Size() { + if id1.Algorithm() != objectid.AlgorithmSHA1 || id1.Algorithm().Size() != objectid.AlgorithmSHA1.Size() { t.Fatalf("sha1 sum produced invalid object id") } id2 := objectid.AlgorithmSHA256.Sum([]byte("hello")) - if id2.Algorithm() != objectid.AlgorithmSHA256 || id2.Size() != objectid.AlgorithmSHA256.Size() { + if id2.Algorithm() != objectid.AlgorithmSHA256 || id2.Algorithm().Size() != objectid.AlgorithmSHA256.Size() { t.Fatalf("sha256 sum produced invalid object id") } diff --git a/object/id/objectid_zero.go b/object/id/objectid_zero.go new file mode 100644 index 00000000..9cc3d205 --- /dev/null +++ b/object/id/objectid_zero.go @@ -0,0 +1,11 @@ +package objectid + +// Zero returns the all-zero object ID for the specified algorithm. +func Zero(algo Algorithm) ObjectID { + id, err := FromBytes(algo, make([]byte, algo.Size())) + if err != nil { + panic(err) + } + + return id +} diff --git a/object/tag/serialize.go b/object/tag/serialize.go index 5f712950..758cee39 100644 --- a/object/tag/serialize.go +++ b/object/tag/serialize.go @@ -11,7 +11,7 @@ import ( // SerializeWithoutHeader renders the raw tag body bytes. func (tag *Tag) SerializeWithoutHeader() ([]byte, error) { - if tag.Target.Size() == 0 { + if tag.Target.Algorithm().Size() == 0 { return nil, errors.New("object: tag: missing target id") } diff --git a/object/tree/serialize.go b/object/tree/serialize.go index be31297b..69deacda 100644 --- a/object/tree/serialize.go +++ b/object/tree/serialize.go @@ -14,7 +14,7 @@ func (tree *Tree) SerializeWithoutHeader() ([]byte, error) { for _, entry := range tree.Entries { mode := strconv.FormatUint(uint64(entry.Mode), 8) - bodyLen += len(mode) + 1 + len(entry.Name) + 1 + entry.ID.Size() + bodyLen += len(mode) + 1 + len(entry.Name) + 1 + entry.ID.Algorithm().Size() } body := make([]byte, bodyLen) diff --git a/ref/store/files/resolve_list_test.go b/ref/store/files/resolve_list_test.go index e25a53f4..d52c5aa2 100644 --- a/ref/store/files/resolve_list_test.go +++ b/ref/store/files/resolve_list_test.go @@ -236,7 +236,7 @@ func TestFilesPackedRefsReadSemanticsMatchGit(t *testing.T) { t.Fatalf("Resolve(tag) type = %T, want ref.Detached", tagRef) } - if tagDet.ID.Size() == 0 { + if tagDet.ID.Algorithm().Size() == 0 { t.Fatal("Resolve(tag) returned zero object id") } }) diff --git a/ref/store/files/update_validate.go b/ref/store/files/update_validate.go index cfbaca1d..ac3429aa 100644 --- a/ref/store/files/update_validate.go +++ b/ref/store/files/update_validate.go @@ -21,7 +21,7 @@ func (executor *refUpdateExecutor) validateQueuedUpdate(op queuedUpdate) error { return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) } - if op.newID.Size() == 0 { + if op.newID.Algorithm().Size() == 0 { return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: objectid.ErrInvalidAlgorithm}) } case updateDelete, updateVerify: @@ -30,7 +30,7 @@ func (executor *refUpdateExecutor) validateQueuedUpdate(op queuedUpdate) error { return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) } - if op.oldID.Size() == 0 { + if op.oldID.Algorithm().Size() == 0 { return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: objectid.ErrInvalidAlgorithm}) } case updateCreateSymbolic, updateReplaceSymbolic: |
