226 lines
7.0 KiB
Go
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
|
|
} |