summaryrefslogtreecommitdiffstats
path: root/internal/identity/cli.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/identity/cli.go')
-rw-r--r--internal/identity/cli.go147
1 files changed, 147 insertions, 0 deletions
diff --git a/internal/identity/cli.go b/internal/identity/cli.go
new file mode 100644
index 0000000..85408c8
--- /dev/null
+++ b/internal/identity/cli.go
@@ -0,0 +1,147 @@
+// 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)
+}