shithub: furgit

ref: 2a7a1e8c95025da4fbd6c398a5e51672fb1a8bd2
dir: /objectid/objectid.go/

View raw version
// Package objectid provides utilities around object IDs and hash algorithms.
package objectid

import (
	"crypto/sha1" //#nosec G505
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"hash"
)

var (
	// ErrInvalidAlgorithm indicates an unsupported object ID algorithm.
	ErrInvalidAlgorithm = errors.New("objectid: invalid algorithm")
	// ErrInvalidObjectID indicates malformed object ID data.
	ErrInvalidObjectID = errors.New("objectid: invalid object id")
)

// 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
	sum  func([]byte) ObjectID
	new  func() hash.Hash
}

var algorithmTable = [...]algorithmDetails{
	AlgorithmUnknown: {},
	AlgorithmSHA1: {
		name: "sha1",
		size: sha1.Size,
		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,
		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 algorithmByName = map[string]Algorithm{}
var supportedAlgorithms []Algorithm

func init() {
	for algo := Algorithm(0); int(algo) < len(algorithmTable); algo++ {
		info := algorithmTable[algo]
		if info.name == "" {
			continue
		}
		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
}

// 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
}

func (algo Algorithm) info() algorithmDetails {
	return algorithmTable[algo]
}

// ObjectID represents a Git object ID.
//
//nolint:recvcheck
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]
}

// 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
}