diff options
Diffstat (limited to 'internal/identity/cli.go')
| -rw-r--r-- | internal/identity/cli.go | 147 |
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) +} |
