# Code Review Guidelines **A review guide for Go + vanilla JS + Wails v2 desktop applications**, organized by priority and impact. This app spawns external processes, has no database, and targets Windows. --- ## Table of Contents ### Security — **CRITICAL** 1. [Command Injection Prevention](#command-injection-prevention) 2. [XSS Prevention in Wails](#xss-prevention-in-wails) 3. [Path Traversal Prevention](#path-traversal-prevention) ### Performance — **HIGH** 4. [Goroutine and Channel Safety](#goroutine-and-channel-safety) ### Correctness — **HIGH** 5. [Error Handling (Go + JS)](#error-handling) 6. [Race Conditions and Mutex Usage](#race-conditions-and-mutex-usage) ### Maintainability — **MEDIUM** 7. [Naming and Code Style](#naming-and-code-style) --- ## Security ### Command Injection Prevention **Impact: CRITICAL** | **Category: security** | **Tags:** exec, process, injection, yt-dlp, ffmpeg Never pass unsanitized user input as arguments to `exec.Command` or `exec.CommandContext`. This app spawns yt-dlp and FFmpeg processes with user-controlled parameters (URLs, file paths, extra args). #### Why This Matters Unlike web apps where SQL injection is the top risk, desktop apps that spawn processes face command injection. An attacker-crafted URL or filename can inject shell metacharacters or additional arguments. #### ❌ Incorrect ```go // Dangerous: user-controlled string interpolated into command func runYtDlp(url string) error { cmd := exec.Command("sh", "-c", "yt-dlp " + url) return cmd.Run() } // Input: "https://example.com; rm -rf /" // Executes both commands! ``` ```go // Dangerous: unsanitized extra args passed directly func buildArgs(extraArgs string) []string { return append(baseArgs, strings.Split(extraArgs, " ")...) } // Input: "--exec rm -rf /" // yt-dlp's --exec flag runs arbitrary commands! ``` #### ✅ Correct ```go // Safe: pass URL as a single argument, not through shell func runYtDlp(url string) error { cmd := exec.Command(ytDlpPath, "--no-exec", url) return cmd.Run() } // Safe: quote-aware split and validate args func buildArgs(extraArgs string) []string { args := splitQuotedArgs(extraArgs) // Validate no dangerous flags for _, arg := range args { if strings.HasPrefix(arg, "--exec") { continue // skip dangerous flags } } return append(baseArgs, args...) } ``` **Key principle:** Always use `exec.Command(binary, arg1, arg2)` with separate arguments. Never use `exec.Command("sh", "-c", concatenatedString)`. Go's exec passes args directly to the process without shell interpretation. [➡️ Full details: security-command-injection.md](rules/security-command-injection.md) --- ### XSS Prevention in Wails **Impact: CRITICAL** | **Category: security** | **Tags:** xss, wails, innerHTML, onclick, javascript Never insert unsanitized file paths, URLs, or user input into HTML via `innerHTML` or inline event handlers. This was a **real past bug** in this codebase. #### ❌ Incorrect ```javascript // Dangerous: file path in inline onclick const html = ``; container.innerHTML = html; // filePath: "file'); alert('xss" // Breaks out of the string and executes arbitrary JS ``` ```javascript // Dangerous: URL injected into innerHTML const html = `${title}`; listContainer.innerHTML += html; ``` #### ✅ Correct ```javascript // Safe: create elements programmatically const btn = document.createElement('button'); btn.textContent = 'Open'; btn.addEventListener('click', () => openFile(filePath)); container.appendChild(btn); // Safe: if innerHTML is unavoidable, escape the values function escapeJS(str) { return str.replace(/\\/g, '\\\\') .replace(/'/g, "\\'") .replace(/"/g, '\\"') .replace(/\n/g, '\\n'); } const html = ``; ``` **This app's specific risk:** File paths from yt-dlp output and download URLs are user-influenced and can contain quotes, backslashes, and special characters. Any inline `onclick` handler using these values must escape them properly. [➡️ Full details: security-xss-wails.md](rules/security-xss-wails.md) --- ### Path Traversal Prevention **Impact: CRITICAL** | **Category: security** | **Tags:** path, traversal, file, directory Validate that file operations stay within expected directories. User-controlled paths (save folder, output template, cookies file) can escape intended boundaries. #### ❌ Incorrect ```go // Dangerous: user controls the path, no validation func saveFile(userPath string, data []byte) error { return os.WriteFile(userPath, data, 0644) } // Input: "../../../etc/passwd" or "C:\Windows\System32\something" ``` #### ✅ Correct ```go // Safe: resolve and validate the path is within allowed directory func saveFile(baseDir, filename string, data []byte) error { absBase, _ := filepath.Abs(baseDir) fullPath := filepath.Join(absBase, filepath.Clean(filename)) // Verify it's still under the base directory if !strings.HasPrefix(fullPath, absBase) { return fmt.Errorf("path escapes base directory: %s", filename) } return os.WriteFile(fullPath, data, 0644) } ``` [➡️ Full details: security-path-traversal.md](rules/security-path-traversal.md) --- ## Performance ### Goroutine and Channel Safety **Impact: HIGH** | **Category: performance** | **Tags:** goroutine, channel, leak, process, context Goroutine leaks and zombie processes are the "N+1 queries" of desktop apps. Every spawned yt-dlp or FFmpeg process must be tracked and cleanable. #### ❌ Incorrect ```go // Leak: goroutine blocks forever if channel is never read func startDownload(url string) { ch := make(chan string) go func() { result := download(url) ch <- result // blocks forever if nobody reads ch }() // ch is never read on error paths } ``` ```go // Leak: process outlives its parent context func convert(path string) { cmd := exec.Command("ffmpeg", "-i", path, "out.mp4") cmd.Start() // If function returns early, ffmpeg is orphaned } ``` #### ✅ Correct ```go // Safe: use context for cancellation, buffered channel func startDownload(ctx context.Context, url string) (string, error) { ch := make(chan string, 1) // buffered so goroutine doesn't block go func() { ch <- download(url) }() select { case result := <-ch: return result, nil case <-ctx.Done(): return "", ctx.Err() } } ``` ```go // Safe: track process and kill on cancel func convert(ctx context.Context, path string) error { cmd := exec.CommandContext(ctx, "ffmpeg", "-i", path, "out.mp4") if err := cmd.Start(); err != nil { return err } return cmd.Wait() // CommandContext kills process when ctx cancels } ``` **This app's specific patterns:** - Download queue uses a channel semaphore for concurrency. New channels must be buffered to match `MaxConcurrentDownloads`. - `NtSuspendProcess`/`NtResumeProcess` must always be paired. A suspended process that's never resumed is a zombie. - FFmpeg scanner goroutines reading stdout/stderr must exit when the process exits. [➡️ Full details: performance-goroutine-safety.md](rules/performance-goroutine-safety.md) --- ## Correctness ### Error Handling **Impact: HIGH** | **Category: correctness** | **Tags:** errors, go, javascript, reliability #### Go: Wrap errors with context, never discard them ##### ❌ Incorrect ```go // Bad: error discarded silently data, _ := os.ReadFile(path) // Bad: error returned without context func loadSettings() (*Settings, error) { data, err := os.ReadFile(settingsPath) if err != nil { return nil, err // caller has no idea what file or why } } ``` ##### ✅ Correct ```go // Good: wrapped with context func loadSettings() (*Settings, error) { data, err := os.ReadFile(settingsPath) if err != nil { return nil, fmt.Errorf("loading settings from %s: %w", settingsPath, err) } var s Settings if err := json.Unmarshal(data, &s); err != nil { return nil, fmt.Errorf("parsing settings JSON: %w", err) } return &s, nil } ``` #### JavaScript: Handle async errors, don't swallow rejections ##### ❌ Incorrect ```javascript // Bad: unhandled promise rejection async function startDownload(url) { const result = await window.go.main.App.AddDownload(url); updateUI(result); // If AddDownload throws, the UI shows nothing } ``` ##### ✅ Correct ```javascript // Good: catch and handle async function startDownload(url) { try { const result = await window.go.main.App.AddDownload(url); updateUI(result); } catch (err) { showError(`Download failed: ${err.message}`); } } ``` [➡️ Full details: correctness-error-handling.md](rules/correctness-error-handling.md) --- ### Race Conditions and Mutex Usage **Impact: HIGH** | **Category: correctness** | **Tags:** mutex, sync, goroutine, race, settings This app has multiple goroutines accessing shared state: settings, download queue, history. All shared state access must use proper synchronization. #### ❌ Incorrect ```go // Race: reading settings field directly from another goroutine func (q *Queue) getMaxConcurrent() int { return q.settings.General.MaxConcurrentDownloads // data race! } ``` ```go // Race: map accessed from multiple goroutines without lock func (q *Queue) addItem(item *Item) { q.items[item.ID] = item // concurrent map write = panic } ``` #### ✅ Correct ```go // Safe: use the thread-safe accessor func (q *Queue) getMaxConcurrent() int { return q.settings.GetMaxConcurrentDownloads() } ``` ```go // Safe: protect map access with mutex func (q *Queue) addItem(item *Item) { q.mu.Lock() defer q.mu.Unlock() q.items[item.ID] = item } ``` **This app's rule:** Always use the `Get*()`/`Set*()` methods on `*config.Settings`. Never read or write struct fields directly across goroutines. [➡️ Full details: correctness-race-conditions.md](rules/correctness-race-conditions.md) --- ## Maintainability ### Naming and Code Style **Impact: MEDIUM** | **Category: maintainability** | **Tags:** naming, go, javascript, readability #### Go Conventions ```go // Exported: PascalCase func BuildArgs(s *Settings) []string { ... } type DownloadItem struct { ... } // Unexported: camelCase (no underscores) func parseOutput(line string) { ... } var maxRetries = 5 // Acronyms: all caps var httpClient *http.Client func (d *Downloader) GetID() string { ... } type URLParser struct { ... } // Errors: "Err" prefix for sentinels var ErrNotFound = errors.New("not found") ``` #### JavaScript Conventions ```javascript // Variables and functions: camelCase const downloadQueue = []; function updateProgressBar(item) { ... } // Constants: UPPER_SNAKE_CASE const MAX_CONCURRENT_DOWNLOADS = 3; // DOM IDs in HTML: kebab-case document.getElementById('download-progress'); // Wails event names: colon-separated, lowercase runtime.EventsOn('download:update', callback); ``` #### Comments ```go // ❌ What (redundant) // increment counter by 1 counter++ // ✅ Why (useful) // Retry rename up to 5 times because antivirus may hold a file lock // on the freshly downloaded binary. for i := 0; i < 5; i++ { err = os.Rename(tmpPath, finalPath) if err == nil { break } time.Sleep(200 * time.Millisecond) } ``` [➡️ Full details: maintainability-naming.md](rules/maintainability-naming.md) --- ## Quick Reference ### Review Checklist **Security (CRITICAL, review first)** - [ ] No command injection in exec.Command calls - [ ] File paths and URLs escaped in inline JS handlers - [ ] Path traversal checked on user-supplied file paths - [ ] Credentials (cookies, PO tokens) not logged or exposed - [ ] `SysProcAttr.CmdLine` usage reviewed for injection **Performance (HIGH)** - [ ] No goroutine leaks (channels closed, contexts cancelled) - [ ] No zombie processes (yt-dlp/FFmpeg cleaned up) - [ ] Queue persistence not triggered on every progress tick - [ ] Buffered channels used where appropriate **Correctness (HIGH)** - [ ] Go errors wrapped with `fmt.Errorf("context: %w", err)` - [ ] JS async errors caught and surfaced to UI - [ ] Shared state accessed through mutex or accessor methods - [ ] New settings fields have `mergeDefaults()` entries - [ ] Event names match the contract exactly **Maintainability (MEDIUM)** - [ ] Go: PascalCase exports, camelCase internal - [ ] JS: camelCase, UPPER_SNAKE for constants - [ ] Comments explain "why" not "what" - [ ] `frontend/wailsjs/` not manually edited --- ## Severity Levels | Level | Description | Examples | Action | |-------|-------------|----------|--------| | **CRITICAL** | Security vulnerabilities, data loss risks | Command injection, XSS, credential exposure | Block merge, fix immediately | | **HIGH** | Process leaks, correctness bugs | Goroutine leak, race condition, wrong event name | Fix before merge | | **MEDIUM** | Maintainability, code quality | Naming, comments, thread-safe accessor usage | Fix or accept with TODO | | **LOW** | Style preferences, minor improvements | Formatting, minor refactoring | Optional |