Skip to content

Commit 0057b13

Browse files
authored
Merge pull request #2 from zainfathoni/fix/circular-lookup-and-inline-proxy
fix: smart fizzy binary lookup to prevent circular resolution
2 parents 6012a1b + d267841 commit 0057b13

2 files changed

Lines changed: 224 additions & 41 deletions

File tree

main.go

Lines changed: 137 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package main
33
import (
44
"bytes"
55
"fmt"
6+
"io"
67
"os"
78
"os/exec"
89
"path/filepath"
10+
"runtime"
911
"strings"
1012

1113
"github.com/yuin/goldmark"
@@ -36,7 +38,7 @@ func convertMarkdownToHTML(markdown string) (string, error) {
3638
if err := md.Convert([]byte(markdown), &buf); err != nil {
3739
return "", fmt.Errorf("markdown conversion failed: %w", err)
3840
}
39-
41+
4042
// Trim trailing newline (goldmark adds it)
4143
html := strings.TrimSuffix(buf.String(), "\n")
4244
return html, nil
@@ -50,17 +52,17 @@ func readAndConvertFile(path string) (string, error) {
5052
}
5153

5254
ext := strings.ToLower(filepath.Ext(path))
53-
55+
5456
// If .html file, return as-is
5557
if ext == ".html" || ext == ".htm" {
5658
return string(content), nil
5759
}
58-
60+
5961
// If .md file or no extension, convert Markdown to HTML
6062
if ext == ".md" || ext == "" {
6163
return convertMarkdownToHTML(string(content))
6264
}
63-
65+
6466
// Unknown extension - assume Markdown for safety (agent-friendly default)
6567
return convertMarkdownToHTML(string(content))
6668
}
@@ -69,75 +71,182 @@ func readAndConvertFile(path string) (string, error) {
6971
func processArgs(args []string) ([]string, error) {
7072
result := make([]string, 0, len(args))
7173
i := 0
72-
74+
7375
for i < len(args) {
7476
arg := args[i]
75-
77+
7678
// Check for flags that need conversion
7779
if arg == "--description" || arg == "--body" {
7880
if i+1 >= len(args) {
7981
return nil, fmt.Errorf("flag %s requires a value", arg)
8082
}
81-
83+
8284
// Convert inline Markdown text to HTML
8385
markdown := args[i+1]
8486
html, err := convertMarkdownToHTML(markdown)
8587
if err != nil {
8688
return nil, fmt.Errorf("converting %s: %w", arg, err)
8789
}
88-
90+
8991
result = append(result, arg, html)
9092
i += 2
9193
continue
9294
}
93-
95+
9496
if arg == "--description_file" || arg == "--body_file" {
9597
if i+1 >= len(args) {
9698
return nil, fmt.Errorf("flag %s requires a file path", arg)
9799
}
98-
100+
99101
// Read file and convert to HTML
100102
filePath := args[i+1]
101103
html, err := readAndConvertFile(filePath)
102104
if err != nil {
103105
return nil, err
104106
}
105-
107+
106108
// Create temp file with HTML content
107109
tmpFile, err := os.CreateTemp("", "fizzy-md-*.html")
108110
if err != nil {
109111
return nil, fmt.Errorf("failed to create temp file: %w", err)
110112
}
111113
defer tmpFile.Close()
112-
114+
113115
if _, err := tmpFile.WriteString(html); err != nil {
114116
return nil, fmt.Errorf("failed to write temp file: %w", err)
115117
}
116-
118+
117119
// Replace flag with temp file path
118120
result = append(result, arg, tmpFile.Name())
119121
i += 2
120122
continue
121123
}
122-
124+
123125
// Pass through all other arguments unchanged
124126
result = append(result, arg)
125127
i++
126128
}
127-
129+
128130
return result, nil
129131
}
130132

133+
// findFizzy locates the real fizzy binary, avoiding circular resolution.
134+
// Priority: FIZZY_PATH env var → PATH lookup (skipping our own binary).
135+
func findFizzy() string {
136+
// 1. Explicit override via env var
137+
if p := os.Getenv("FIZZY_PATH"); p != "" {
138+
if _, err := os.Stat(p); err == nil {
139+
return p
140+
}
141+
}
142+
143+
// 2. Resolve our own executable path so we can skip it
144+
self, _ := os.Executable()
145+
if self != "" {
146+
self, _ = filepath.EvalSymlinks(self)
147+
}
148+
149+
// 3. Walk PATH entries looking for a "fizzy" that isn't us
150+
pathEnv := os.Getenv("PATH")
151+
for _, dir := range filepath.SplitList(pathEnv) {
152+
for _, candidate := range fizzyCandidates(dir) {
153+
info, err := os.Stat(candidate)
154+
if err != nil || info.IsDir() {
155+
continue
156+
}
157+
158+
real, _ := filepath.EvalSymlinks(candidate)
159+
160+
// Skip if it resolves to ourselves (fizzy-md)
161+
if self != "" && real == self {
162+
continue
163+
}
164+
165+
// Skip shell scripts that call fizzy-md (wrapper scripts)
166+
if isShellWrapperForFizzyMd(candidate) {
167+
continue
168+
}
169+
170+
return candidate
171+
}
172+
}
173+
174+
return ""
175+
}
176+
177+
// isShellWrapperForFizzyMd does a quick check if a file is a small shell script
178+
// that references fizzy-md (i.e. a wrapper that would cause a loop).
179+
func isShellWrapperForFizzyMd(path string) bool {
180+
info, err := os.Stat(path)
181+
if err != nil {
182+
return false
183+
}
184+
if info.IsDir() {
185+
return false
186+
}
187+
// Only check small files (real fizzy binary is >1MB)
188+
if info.Size() > 4096 {
189+
return false
190+
}
191+
192+
file, err := os.Open(path)
193+
if err != nil {
194+
return false
195+
}
196+
defer file.Close()
197+
198+
buf := make([]byte, 4096)
199+
n, err := file.Read(buf)
200+
if err != nil && err != io.EOF {
201+
return false
202+
}
203+
content := string(buf[:n])
204+
return strings.Contains(content, "fizzy-md")
205+
}
206+
207+
func fizzyCandidates(dir string) []string {
208+
base := "fizzy"
209+
if runtime.GOOS != "windows" {
210+
return []string{filepath.Join(dir, base)}
211+
}
212+
if filepath.Ext(base) != "" {
213+
return []string{filepath.Join(dir, base)}
214+
}
215+
216+
pathext := os.Getenv("PATHEXT")
217+
var exts []string
218+
if pathext == "" {
219+
exts = []string{".com", ".exe", ".bat", ".cmd"}
220+
} else {
221+
for _, ext := range strings.Split(pathext, ";") {
222+
clean := strings.TrimSpace(ext)
223+
if clean == "" {
224+
continue
225+
}
226+
if !strings.HasPrefix(clean, ".") {
227+
clean = "." + clean
228+
}
229+
exts = append(exts, clean)
230+
}
231+
}
232+
233+
paths := make([]string, 0, len(exts))
234+
for _, ext := range exts {
235+
paths = append(paths, filepath.Join(dir, base+ext))
236+
}
237+
return paths
238+
}
239+
131240
func main() {
132241
// Get original args (skip program name)
133242
args := os.Args[1:]
134-
243+
135244
// Handle --version flag
136245
if len(args) == 1 && (args[0] == "--version" || args[0] == "-v") {
137246
fmt.Printf("fizzy-md version %s\n", version)
138247
os.Exit(0)
139248
}
140-
249+
141250
// Handle stdin mode: if no args and stdin is piped, convert and output
142251
if len(args) == 0 {
143252
stat, _ := os.Stdin.Stat()
@@ -148,39 +257,39 @@ func main() {
148257
fmt.Fprintf(os.Stderr, "fizzy-md error: failed to read stdin: %v\n", err)
149258
os.Exit(1)
150259
}
151-
260+
152261
html, err := convertMarkdownToHTML(buf.String())
153262
if err != nil {
154263
fmt.Fprintf(os.Stderr, "fizzy-md error: %v\n", err)
155264
os.Exit(1)
156265
}
157-
266+
158267
fmt.Print(html)
159268
os.Exit(0)
160269
}
161270
}
162-
271+
163272
// Process args for Markdown conversion
164273
processedArgs, err := processArgs(args)
165274
if err != nil {
166275
fmt.Fprintf(os.Stderr, "fizzy-md error: %v\n", err)
167276
os.Exit(1)
168277
}
169-
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")
278+
279+
// Find fizzy executable, avoiding circular resolution back to ourselves
280+
fizzyPath := findFizzy()
281+
if fizzyPath == "" {
282+
fmt.Fprintf(os.Stderr, "fizzy-md error: fizzy command not found\n")
283+
fmt.Fprintf(os.Stderr, "Set FIZZY_PATH or install fizzy-cli: https://github.com/robzolkos/fizzy-cli\n")
175284
os.Exit(1)
176285
}
177-
286+
178287
// Execute real fizzy with processed args
179288
cmd := exec.Command(fizzyPath, processedArgs...)
180289
cmd.Stdin = os.Stdin
181290
cmd.Stdout = os.Stdout
182291
cmd.Stderr = os.Stderr
183-
292+
184293
if err := cmd.Run(); err != nil {
185294
if exitErr, ok := err.(*exec.ExitError); ok {
186295
os.Exit(exitErr.ExitCode())

0 commit comments

Comments
 (0)