Files
go-jdenticon/jdenticon/hash.go
Kevin McIntyre f84b511895 init
2025-06-18 01:00:00 -04:00

226 lines
7.0 KiB
Go

package jdenticon
import (
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"github.com/kevin/go-jdenticon/internal/util"
)
// ComputeHash computes a SHA-1 hash for any value and returns it as a hexadecimal string.
// This function mimics the JavaScript version's behavior for compatibility.
func ComputeHash(value interface{}) string {
var input string
// Handle different input types, converting to string like JavaScript version
switch v := value.(type) {
case nil:
input = ""
case string:
input = v
case []byte:
input = string(v)
case int:
input = strconv.Itoa(v)
case int64:
input = strconv.FormatInt(v, 10)
case float64:
input = fmt.Sprintf("%g", v)
default:
// Convert to string using fmt.Sprintf for other types
input = fmt.Sprintf("%v", v)
}
// Compute SHA-1 hash using Go's crypto/sha1 package
h := sha1.New()
h.Write([]byte(input))
hash := h.Sum(nil)
// Convert to hexadecimal string (lowercase to match JavaScript)
return fmt.Sprintf("%x", hash)
}
// HashValue is a convenience function that wraps ComputeHash for string inputs.
// Kept for backward compatibility.
func HashValue(value string) string {
return ComputeHash(value)
}
// IsValidHash checks if a string is a valid hash for Jdenticon.
// It must be a hexadecimal string with at least 11 characters.
func IsValidHash(hashCandidate string) bool {
return util.IsValidHash(hashCandidate)
}
// isValidHash is a private wrapper for backward compatibility with existing tests
func isValidHash(hashCandidate string) bool {
return IsValidHash(hashCandidate)
}
// parseHex extracts a value from a hex string at a specific position.
// This function is used to deterministically extract shape and color information
// from the hash string, matching the JavaScript implementation.
// When octets is 0 or negative, it reads from startPosition to the end of the string.
func parseHex(hash string, startPosition int, octets int) (int, error) {
return util.ParseHex(hash, startPosition, octets)
}
// ParseHex provides a public API that matches the JavaScript parseHex function exactly.
// It extracts a hexadecimal value from the hash string at the specified position.
// If octets is not provided or is <= 0, it reads from the position to the end of the string.
// Returns 0 on error to maintain compatibility with the JavaScript implementation.
func ParseHex(hash string, startPosition int, octets ...int) int {
octetCount := 0
if len(octets) > 0 {
octetCount = octets[0]
}
result, err := parseHex(hash, startPosition, octetCount)
if err != nil {
return 0 // Maintain JavaScript compatibility: return 0 on error
}
return result
}
// ParseHash converts a hexadecimal hash string into a byte array for further processing.
// It validates the input hash string and handles common prefixes like "0x".
// Returns an error if the hash contains invalid hexadecimal characters.
func ParseHash(hash string) ([]byte, error) {
if hash == "" {
return nil, errors.New("hash string cannot be empty")
}
// Remove "0x" prefix if present
cleanHash := strings.TrimPrefix(hash, "0x")
cleanHash = strings.TrimPrefix(cleanHash, "0X")
// Validate hash length (must be even for proper byte conversion)
if len(cleanHash)%2 != 0 {
return nil, errors.New("hash string must have even length")
}
// Decode hex string to bytes
bytes, err := hex.DecodeString(cleanHash)
if err != nil {
return nil, fmt.Errorf("invalid hexadecimal string: %w", err)
}
return bytes, nil
}
// ExtractInt extracts a specific number of bits from a hash byte array and converts them to an integer.
// The index parameter specifies the starting position (negative values count from the end).
// The bits parameter specifies how many bits to extract.
func ExtractInt(hash []byte, index, bits int) int {
if len(hash) == 0 || bits <= 0 {
return 0
}
// Handle negative indices (count from end)
if index < 0 {
index = len(hash) + index
}
// Ensure index is within bounds
if index < 0 || index >= len(hash) {
return 0
}
// Calculate how many bytes we need to read
bytesNeeded := (bits + 7) / 8 // Round up to nearest byte
// Ensure we don't read past the end of the array
if index+bytesNeeded > len(hash) {
bytesNeeded = len(hash) - index
}
if bytesNeeded <= 0 {
return 0
}
// Extract bytes and convert to integer
var result int
for i := 0; i < bytesNeeded; i++ {
if index+i < len(hash) {
result = (result << 8) | int(hash[index+i])
}
}
// Mask to only include the requested number of bits
if bits < 64 {
mask := (1 << bits) - 1
result &= mask
}
return result
}
// ExtractFloat extracts a specific number of bits from a hash byte array and converts them to a float64 value between 0 and 1.
// The value is normalized by dividing by the maximum possible value for the given number of bits.
func ExtractFloat(hash []byte, index, bits int) float64 {
if bits <= 0 {
return 0.0
}
// Extract integer value
intValue := ExtractInt(hash, index, bits)
// Calculate maximum possible value for the given number of bits
maxValue := (1 << bits) - 1
if maxValue == 0 {
return 0.0
}
// Normalize to [0,1] range
return float64(intValue) / float64(maxValue)
}
// ExtractHue extracts the hue value from a hash string using the same algorithm as the JavaScript version.
// This is a convenience function that extracts the last 7 characters and normalizes to [0,1] range.
// Returns 0.0 on error to maintain compatibility with the JavaScript implementation.
func ExtractHue(hash string) float64 {
hueValue, err := parseHex(hash, -7, 0) // Read from -7 to end
if err != nil {
return 0.0 // Maintain JavaScript compatibility: return 0.0 on error
}
return float64(hueValue) / 0xfffffff
}
// ExtractShapeIndex extracts a shape index from the hash at the specified position.
// This is a convenience function that matches the JavaScript shape selection logic.
// Returns 0 on error to maintain compatibility with the JavaScript implementation.
func ExtractShapeIndex(hash string, position int) int {
result, err := parseHex(hash, position, 1)
if err != nil {
return 0 // Maintain JavaScript compatibility: return 0 on error
}
return result
}
// ExtractRotation extracts a rotation value from the hash at the specified position.
// This is a convenience function that matches the JavaScript rotation logic.
// Returns 0 on error to maintain compatibility with the JavaScript implementation.
func ExtractRotation(hash string, position int) int {
result, err := parseHex(hash, position, 1)
if err != nil {
return 0 // Maintain JavaScript compatibility: return 0 on error
}
return result
}
// ExtractColorIndex extracts a color index from the hash at the specified position.
// This is a convenience function that matches the JavaScript color selection logic.
// Returns 0 on error to maintain compatibility with the JavaScript implementation.
func ExtractColorIndex(hash string, position int, availableColors int) int {
value, err := parseHex(hash, position, 1)
if err != nil {
return 0 // Maintain JavaScript compatibility: return 0 on error
}
if availableColors > 0 {
return value % availableColors
}
return value
}