// Package identity integrates the existing identicon-cli program without // copying its GUI or reimplementing its rendering algorithm. package identity import ( "bytes" "context" "encoding/base64" "errors" "fmt" "image" "image/png" "os" "os/exec" "path/filepath" "strings" "time" ) var cliCandidates = []string{ "/home/gabriel1/Projects/identicons/identicons-cli", "/home/gabriel1/Workspace/Projects/identicons/identicons", "/usr/local/bin/identicon-cli", "/usr/bin/identicon-cli", "/SDCard/identicon-cli", "/Shared/identicon-cli", } // FindCLI returns the configured or locally installed identicon-cli binary. func FindCLI() (string, error) { if configured := strings.TrimSpace(os.Getenv("AEGIS_IDENTICON_CLI")); configured != "" { if isExecutable(configured) { return configured, nil } return "", fmt.Errorf("AEGIS_IDENTICON_CLI is not executable: %s", configured) } for _, candidate := range cliCandidates { if isExecutable(candidate) { return candidate, nil } } return "", errors.New("identicon-cli executable not found") } func isExecutable(path string) bool { info, err := os.Stat(path) return err == nil && info.Mode().IsRegular() && info.Mode()&0o111 != 0 } // GenerateFace delegates to identicon-cli with its existing 48px rendering // and returns the base64 PNG payload used in a Face header. func GenerateFace(seed string) (string, error) { if strings.TrimSpace(seed) == "" { return "", errors.New("identicon seed is required") } return generateFace(seed, 48) } func generateFace(seed string, size int) (string, error) { if strings.TrimSpace(seed) == "" { return "", errors.New("identicon seed is required") } if size <= 0 { return "", errors.New("identicon size must be positive") } executable, err := FindCLI() if err != nil { return "", err } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() command := exec.CommandContext(ctx, executable, "-input", seed, "-format", "base64", "-size", fmt.Sprintf("%d", size), "-transparent") var stdout, stderr bytes.Buffer command.Stdout = &stdout command.Stderr = &stderr if err := command.Run(); err != nil { return "", fmt.Errorf("run identicon-cli: %w: %s", err, strings.TrimSpace(stderr.String())) } value := strings.TrimSpace(stdout.String()) if _, err := pngImageFromBase64(value, size); err != nil { return "", fmt.Errorf("identicon-cli returned invalid Face data: %w", err) } return value, nil } func decodeFacePNG(value string) ([]byte, error) { return pngBytes(value, 48) } func pngBytes(value string, size int) ([]byte, error) { data, err := base64.StdEncoding.DecodeString(value) if err != nil { return nil, fmt.Errorf("decode base64: %w", err) } if _, err := pngImage(data, size); err != nil { return nil, err } return data, nil } func pngImageFromBase64(value string, size int) (image.Image, error) { data, err := pngBytes(value, size) if err != nil { return nil, err } return pngImage(data, size) } func pngImage(data []byte, size int) (image.Image, error) { img, err := png.Decode(bytes.NewReader(data)) if err != nil { return nil, fmt.Errorf("decode PNG: %w", err) } if img.Bounds().Dx() != size || img.Bounds().Dy() != size { return nil, fmt.Errorf("PNG dimensions are %dx%d, want %dx%d", img.Bounds().Dx(), img.Bounds().Dy(), size, size) } return img, nil } // FormatFaceHeader folds the Face value in the same 70/75-byte pattern used // by the existing identicon-cli output files. func FormatFaceHeader(value string) string { if len(value) <= 70 { return "Face: " + value } var out strings.Builder out.WriteString("Face: ") out.WriteString(value[:70]) for position := 70; position < len(value); position += 75 { end := position + 75 if end > len(value) { end = len(value) } out.WriteString("\r\n ") out.WriteString(value[position:end]) } return out.String() } // CLIPathForDisplay returns a stable path label without exposing environment // secrets or invoking a shell. func CLIPathForDisplay(path string) string { if path == "" { return "" } return filepath.Clean(path) }