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 }