Skip to content

Commit 483708d

Browse files
committed
fix: smart fizzy binary lookup to prevent circular resolution
- Replace exec.LookPath("fizzy") with findFizzy() that: - Checks FIZZY_PATH env var first (explicit override) - Walks PATH but skips entries that resolve to fizzy-md itself - Detects shell wrapper scripts that reference fizzy-md - Enables fizzy-md to sit in PATH as 'fizzy' transparently - Agents call 'fizzy' naturally, markdown→HTML happens invisibly Closes #1 (if applicable) Related: Fizzy card #225, #208
1 parent 6012a1b commit 483708d

2 files changed

Lines changed: 113 additions & 5 deletions

File tree

main.go

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,64 @@ func processArgs(args []string) ([]string, error) {
128128
return result, nil
129129
}
130130

131+
// findFizzy locates the real fizzy binary, avoiding circular resolution.
132+
// Priority: FIZZY_PATH env var → PATH lookup (skipping our own binary).
133+
func findFizzy() string {
134+
// 1. Explicit override via env var
135+
if p := os.Getenv("FIZZY_PATH"); p != "" {
136+
if _, err := os.Stat(p); err == nil {
137+
return p
138+
}
139+
}
140+
141+
// 2. Resolve our own executable path so we can skip it
142+
self, _ := os.Executable()
143+
if self != "" {
144+
self, _ = filepath.EvalSymlinks(self)
145+
}
146+
147+
// 3. Walk PATH entries looking for a "fizzy" that isn't us
148+
pathEnv := os.Getenv("PATH")
149+
for _, dir := range filepath.SplitList(pathEnv) {
150+
candidate := filepath.Join(dir, "fizzy")
151+
info, err := os.Stat(candidate)
152+
if err != nil || info.IsDir() {
153+
continue
154+
}
155+
156+
real, _ := filepath.EvalSymlinks(candidate)
157+
158+
// Skip if it resolves to ourselves (fizzy-md)
159+
if self != "" && real == self {
160+
continue
161+
}
162+
163+
// Skip shell scripts that call fizzy-md (wrapper scripts)
164+
if isShellWrapperForFizzyMd(candidate) {
165+
continue
166+
}
167+
168+
return candidate
169+
}
170+
171+
return ""
172+
}
173+
174+
// isShellWrapperForFizzyMd does a quick check if a file is a small shell script
175+
// that references fizzy-md (i.e. a wrapper that would cause a loop).
176+
func isShellWrapperForFizzyMd(path string) bool {
177+
data, err := os.ReadFile(path)
178+
if err != nil {
179+
return false
180+
}
181+
// Only check small files (real fizzy binary is >1MB)
182+
if len(data) > 4096 {
183+
return false
184+
}
185+
content := string(data)
186+
return strings.Contains(content, "fizzy-md")
187+
}
188+
131189
func main() {
132190
// Get original args (skip program name)
133191
args := os.Args[1:]
@@ -167,11 +225,11 @@ func main() {
167225
os.Exit(1)
168226
}
169227

170-
// Find fizzy executable
171-
fizzyPath, err := exec.LookPath("fizzy")
172-
if err != nil {
173-
fmt.Fprintf(os.Stderr, "fizzy-md error: fizzy command not found in PATH\n")
174-
fmt.Fprintf(os.Stderr, "Please install fizzy-cli: https://github.com/robzolkos/fizzy-cli\n")
228+
// Find fizzy executable, avoiding circular resolution back to ourselves
229+
fizzyPath := findFizzy()
230+
if fizzyPath == "" {
231+
fmt.Fprintf(os.Stderr, "fizzy-md error: fizzy command not found\n")
232+
fmt.Fprintf(os.Stderr, "Set FIZZY_PATH or install fizzy-cli: https://github.com/robzolkos/fizzy-cli\n")
175233
os.Exit(1)
176234
}
177235

main_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,56 @@ func TestProcessArgsWithFiles(t *testing.T) {
456456
})
457457
}
458458

459+
func TestFindFizzySkipsSelf(t *testing.T) {
460+
// findFizzy should not return a path that resolves to the fizzy-md binary itself
461+
result := findFizzy()
462+
if result == "" {
463+
t.Skip("no fizzy binary found in PATH (expected in CI)")
464+
}
465+
466+
self, _ := os.Executable()
467+
if self != "" {
468+
selfReal, _ := filepath.EvalSymlinks(self)
469+
resultReal, _ := filepath.EvalSymlinks(result)
470+
if selfReal == resultReal {
471+
t.Errorf("findFizzy returned self: %s", result)
472+
}
473+
}
474+
}
475+
476+
func TestFindFizzyRespectsEnvVar(t *testing.T) {
477+
// Create a fake fizzy binary
478+
tmpDir := t.TempDir()
479+
fakeFizzy := filepath.Join(tmpDir, "fizzy")
480+
if err := os.WriteFile(fakeFizzy, []byte("#!/bin/sh\necho fake"), 0755); err != nil {
481+
t.Fatalf("failed to create fake fizzy: %v", err)
482+
}
483+
484+
t.Setenv("FIZZY_PATH", fakeFizzy)
485+
result := findFizzy()
486+
if result != fakeFizzy {
487+
t.Errorf("expected %s, got %s", fakeFizzy, result)
488+
}
489+
}
490+
491+
func TestIsShellWrapperForFizzyMd(t *testing.T) {
492+
tmpDir := t.TempDir()
493+
494+
// A wrapper script that references fizzy-md
495+
wrapper := filepath.Join(tmpDir, "wrapper")
496+
os.WriteFile(wrapper, []byte("#!/bin/sh\nexec /opt/homebrew/bin/fizzy-md \"$@\"\n"), 0755)
497+
if !isShellWrapperForFizzyMd(wrapper) {
498+
t.Error("expected wrapper to be detected")
499+
}
500+
501+
// A real binary (large file, not a wrapper)
502+
notWrapper := filepath.Join(tmpDir, "real")
503+
os.WriteFile(notWrapper, []byte("#!/bin/sh\nexec /opt/homebrew/bin/fizzy \"$@\"\n"), 0755)
504+
if isShellWrapperForFizzyMd(notWrapper) {
505+
t.Error("expected non-wrapper to not be detected")
506+
}
507+
}
508+
459509
// Benchmark for performance requirement (<100ms)
460510
func BenchmarkConvertMarkdownToHTML(b *testing.B) {
461511
input := `## Overview

0 commit comments

Comments
 (0)