Initial release: Go Jdenticon library v0.1.0
- Core library with SVG and PNG generation - CLI tool with generate and batch commands - Cross-platform path handling for Windows compatibility - Comprehensive test suite with integration tests
This commit is contained in:
@@ -1,23 +1,35 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/sha1" // #nosec G505 -- SHA1 used for visual identity hashing (jdenticon compatibility), not cryptographic security
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ParseHex parses a hexadecimal value from the hash string
|
||||
// ComputeHash generates a SHA1 hash from the input string.
|
||||
// This matches the hash generation used by the JavaScript jdenticon library.
|
||||
// Note: SHA1 is used here for visual identity generation (deterministic icon creation),
|
||||
// not for cryptographic security purposes.
|
||||
func ComputeHash(input string) string {
|
||||
hasher := sha1.New() // #nosec G401 -- SHA1 used for visual identity hashing, not cryptographic security
|
||||
hasher.Write([]byte(input))
|
||||
hash := hasher.Sum(nil)
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
||||
|
||||
// ParseHex parses a hexadecimal value from the hash string with smart byte/character detection
|
||||
// This implementation is shared between engine and jdenticon packages for consistency
|
||||
func ParseHex(hash string, startPosition, octets int) (int, error) {
|
||||
// Handle negative indices (count from end like JavaScript)
|
||||
if startPosition < 0 {
|
||||
startPosition = len(hash) + startPosition
|
||||
}
|
||||
|
||||
|
||||
// Ensure we don't go out of bounds
|
||||
if startPosition < 0 || startPosition >= len(hash) {
|
||||
return 0, fmt.Errorf("parseHex: position %d out of bounds for hash length %d", startPosition, len(hash))
|
||||
return 0, fmt.Errorf("jdenticon: hash: parsing failed: position out of bounds: position %d out of bounds for hash length %d", startPosition, len(hash))
|
||||
}
|
||||
|
||||
|
||||
// If octets is 0 or negative, read from startPosition to end (like JavaScript default)
|
||||
end := len(hash)
|
||||
if octets > 0 {
|
||||
@@ -26,34 +38,49 @@ func ParseHex(hash string, startPosition, octets int) (int, error) {
|
||||
end = len(hash)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Extract substring and parse as hexadecimal
|
||||
substr := hash[startPosition:end]
|
||||
if len(substr) == 0 {
|
||||
return 0, fmt.Errorf("parseHex: empty substring at position %d", startPosition)
|
||||
return 0, fmt.Errorf("jdenticon: hash: parsing failed: empty substring: empty substring at position %d", startPosition)
|
||||
}
|
||||
|
||||
result, err := strconv.ParseInt(substr, 16, 64)
|
||||
|
||||
result, err := strconv.ParseInt(substr, 16, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parseHex: failed to parse hex '%s' at position %d: %w", substr, startPosition, err)
|
||||
return 0, fmt.Errorf("jdenticon: hash: parsing failed: invalid hex format: failed to parse hex '%s' at position %d: %w", substr, startPosition, err)
|
||||
}
|
||||
|
||||
|
||||
return int(result), nil
|
||||
}
|
||||
|
||||
// ValidateHash validates a hash string and returns detailed error information
|
||||
func ValidateHash(hash string) error {
|
||||
if len(hash) < 11 {
|
||||
return fmt.Errorf("jdenticon: hash: validation failed: insufficient length: hash too short: %d characters (minimum 11 required)", len(hash))
|
||||
}
|
||||
|
||||
// Check if all characters are valid hexadecimal
|
||||
for i, r := range hash {
|
||||
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
|
||||
return fmt.Errorf("jdenticon: hash: validation failed: invalid character: invalid hexadecimal character '%c' at position %d", r, i)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidHash checks if a hash string is valid for jdenticon generation
|
||||
// This implementation is shared between engine and jdenticon packages for consistency
|
||||
func IsValidHash(hash string) bool {
|
||||
if len(hash) < 11 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if all characters are valid hexadecimal
|
||||
for _, r := range hash {
|
||||
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
|
||||
return false
|
||||
return ValidateHash(hash) == nil
|
||||
}
|
||||
|
||||
// ContainsInt checks if an integer slice contains a specific value
|
||||
func ContainsInt(slice []int, value int) bool {
|
||||
for _, item := range slice {
|
||||
if item == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
360
internal/util/hash_test.go
Normal file
360
internal/util/hash_test.go
Normal file
@@ -0,0 +1,360 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContainsInt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
slice []int
|
||||
value int
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "value exists in slice",
|
||||
slice: []int{1, 2, 3, 4, 5},
|
||||
value: 3,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "value does not exist in slice",
|
||||
slice: []int{1, 2, 3, 4, 5},
|
||||
value: 6,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty slice",
|
||||
slice: []int{},
|
||||
value: 1,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "single element slice - match",
|
||||
slice: []int{42},
|
||||
value: 42,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "single element slice - no match",
|
||||
slice: []int{42},
|
||||
value: 43,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "duplicate values in slice",
|
||||
slice: []int{1, 2, 2, 3, 2},
|
||||
value: 2,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "negative values",
|
||||
slice: []int{-5, -3, -1, 0, 1},
|
||||
value: -3,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "zero value",
|
||||
slice: []int{-1, 0, 1},
|
||||
value: 0,
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ContainsInt(tt.slice, tt.value)
|
||||
if result != tt.expected {
|
||||
t.Errorf("ContainsInt(%v, %d) = %v, expected %v",
|
||||
tt.slice, tt.value, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHex(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hash string
|
||||
startPosition int
|
||||
octets int
|
||||
expected int
|
||||
expectError bool
|
||||
errorType string
|
||||
}{
|
||||
// Valid cases
|
||||
{
|
||||
name: "simple hex parsing",
|
||||
hash: "abc123def456",
|
||||
startPosition: 0,
|
||||
octets: 2,
|
||||
expected: 0xab,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "parse single character",
|
||||
hash: "a1b2c3d4e5f6",
|
||||
startPosition: 1,
|
||||
octets: 1,
|
||||
expected: 0x1,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "parse from middle",
|
||||
hash: "123456789abc",
|
||||
startPosition: 6,
|
||||
octets: 3,
|
||||
expected: 0x789,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "negative position (from end)",
|
||||
hash: "abcdef123456",
|
||||
startPosition: -2,
|
||||
octets: 2,
|
||||
expected: 0x56,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "octets 0 reads to end",
|
||||
hash: "abc123",
|
||||
startPosition: 3,
|
||||
octets: 0,
|
||||
expected: 0x123,
|
||||
expectError: false,
|
||||
},
|
||||
// Error cases
|
||||
{
|
||||
name: "position out of bounds",
|
||||
hash: "abc123",
|
||||
startPosition: 10,
|
||||
octets: 1,
|
||||
expectError: true,
|
||||
errorType: "position out of bounds",
|
||||
},
|
||||
{
|
||||
name: "negative position too large",
|
||||
hash: "abc123",
|
||||
startPosition: -10,
|
||||
octets: 1,
|
||||
expectError: true,
|
||||
errorType: "position out of bounds",
|
||||
},
|
||||
{
|
||||
name: "invalid hex character",
|
||||
hash: "abcghi",
|
||||
startPosition: 3,
|
||||
octets: 3,
|
||||
expectError: true,
|
||||
errorType: "invalid hex format",
|
||||
},
|
||||
// Platform-specific overflow tests
|
||||
{
|
||||
name: "value at 32-bit int boundary (safe)",
|
||||
hash: "7fffffff",
|
||||
startPosition: 0,
|
||||
octets: 8,
|
||||
expected: math.MaxInt32,
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Add platform-specific overflow test that should fail on 32-bit systems
|
||||
if strconv.IntSize == 32 {
|
||||
tests = append(tests, struct {
|
||||
name string
|
||||
hash string
|
||||
startPosition int
|
||||
octets int
|
||||
expected int
|
||||
expectError bool
|
||||
errorType string
|
||||
}{
|
||||
name: "overflow on 32-bit systems",
|
||||
hash: "80000000", // This exceeds math.MaxInt32
|
||||
startPosition: 0,
|
||||
octets: 8,
|
||||
expectError: true,
|
||||
errorType: "value out of range",
|
||||
})
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := ParseHex(tt.hash, tt.startPosition, tt.octets)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("ParseHex(%q, %d, %d) expected error but got none",
|
||||
tt.hash, tt.startPosition, tt.octets)
|
||||
return
|
||||
}
|
||||
if tt.errorType != "" && !containsString(err.Error(), tt.errorType) {
|
||||
t.Errorf("ParseHex(%q, %d, %d) error %q does not contain expected type %q",
|
||||
tt.hash, tt.startPosition, tt.octets, err.Error(), tt.errorType)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("ParseHex(%q, %d, %d) unexpected error: %v",
|
||||
tt.hash, tt.startPosition, tt.octets, err)
|
||||
return
|
||||
}
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("ParseHex(%q, %d, %d) = %d, expected %d",
|
||||
tt.hash, tt.startPosition, tt.octets, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateHash(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hash string
|
||||
expectError bool
|
||||
errorType string
|
||||
}{
|
||||
{
|
||||
name: "valid hash",
|
||||
hash: "abc123def456789",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "minimum valid length",
|
||||
hash: "abc123def45", // exactly 11 chars
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "hash too short",
|
||||
hash: "abc123def4", // 10 chars
|
||||
expectError: true,
|
||||
errorType: "insufficient length",
|
||||
},
|
||||
{
|
||||
name: "invalid character",
|
||||
hash: "abc123gef456789",
|
||||
expectError: true,
|
||||
errorType: "invalid character",
|
||||
},
|
||||
{
|
||||
name: "uppercase hex is valid",
|
||||
hash: "ABC123DEF456789",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "mixed case is valid",
|
||||
hash: "AbC123dEf456789",
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateHash(tt.hash)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("ValidateHash(%q) expected error but got none", tt.hash)
|
||||
return
|
||||
}
|
||||
if tt.errorType != "" && !containsString(err.Error(), tt.errorType) {
|
||||
t.Errorf("ValidateHash(%q) error %q does not contain expected type %q",
|
||||
tt.hash, err.Error(), tt.errorType)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("ValidateHash(%q) unexpected error: %v", tt.hash, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidHash(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hash string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "valid hash returns true",
|
||||
hash: "abc123def456789",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "invalid hash returns false",
|
||||
hash: "abc123g", // too short and invalid char
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty hash returns false",
|
||||
hash: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := IsValidHash(tt.hash)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsValidHash(%q) = %v, expected %v", tt.hash, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseHexDeterministic verifies that ParseHex produces consistent results
|
||||
func TestParseHexDeterministic(t *testing.T) {
|
||||
testCases := []struct {
|
||||
hash string
|
||||
pos int
|
||||
oct int
|
||||
}{
|
||||
{"abc123def456", 0, 2},
|
||||
{"fedcba987654", 3, 4},
|
||||
{"123456789abc", 6, 3},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run("deterministic_"+tc.hash, func(t *testing.T) {
|
||||
// Parse the same input multiple times
|
||||
results := make([]int, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
result, err := ParseHex(tc.hash, tc.pos, tc.oct)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseHex failed on iteration %d: %v", i, err)
|
||||
}
|
||||
results[i] = result
|
||||
}
|
||||
|
||||
// Verify all results are identical
|
||||
first := results[0]
|
||||
for i, result := range results[1:] {
|
||||
if result != first {
|
||||
t.Errorf("ParseHex result not deterministic: iteration %d gave %d, expected %d",
|
||||
i+1, result, first)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if a string contains a substring
|
||||
func containsString(s, substr string) bool {
|
||||
return len(s) >= len(substr) &&
|
||||
(len(substr) == 0 ||
|
||||
func() bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}())
|
||||
}
|
||||
Reference in New Issue
Block a user