package platform import ( "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" ) // SaveDownload writes a received file's bytes into dir (empty → system Downloads), // returning the absolute path written. The name comes from a transfer peer (U2, // untrusted), so it is sanitized to a bare filename before use — no path // components survive, so a crafted name like "../../x" cannot escape dir. On a // name collision a " (n)" suffix is appended rather than overwriting. After a // successful write the file is best-effort revealed in the OS file manager. // // If the configured dir is unwritable (deleted, no permission, read-only), the // write is retried into the system Downloads dir before giving up — the WebView // has no usable download fallback (WKWebView ignores blob a[download]), so a // received file must never be silently lost to a bad setting. The returned path // reflects where the file actually landed, so the UI can show it. func SaveDownload(dir, name string, data []byte) (string, error) { if dir == "" { dir = DefaultDownloadDir() } path, err := writeInto(dir, name, data) if err == nil { return path, nil } if def := DefaultDownloadDir(); def != dir { if p, ferr := writeInto(def, name, data); ferr == nil { return p, nil } } return "", err // report the original (configured-dir) failure } // writeInto sanitizes the name, resolves a non-colliding path under dir, writes // the bytes, and reveals the result. Returns the absolute path written. func writeInto(dir, name string, data []byte) (string, error) { if err := os.MkdirAll(dir, 0o755); err != nil { return "", err } safe := sanitizeFileName(name) if safe == "" { safe = "cdrop-download" } target := uniquePath(dir, safe) if err := os.WriteFile(target, data, 0o644); err != nil { return "", err } revealInFileManager(target) return target, nil } // sanitizeFileName reduces a peer-supplied name to a single safe filename: // directory components are dropped (path-traversal defense), control chars are // removed, and characters reserved by common filesystems are replaced with "_". func sanitizeFileName(name string) string { name = filepath.Base(filepath.FromSlash(name)) // strip any directory parts name = strings.TrimSpace(name) if name == "." || name == ".." { return "" } var b strings.Builder for _, r := range name { switch { case r < 0x20: continue // control characters case strings.ContainsRune(`/\:*?"<>|`, r): b.WriteRune('_') // filesystem-reserved default: b.WriteRune(r) } } out := strings.TrimSpace(b.String()) if len(out) > 200 { out = strings.TrimSpace(out[:200]) } return out } // uniquePath returns dir/name, or dir/"name (n).ext" with the lowest n that does // not yet exist, so a repeated transfer never silently overwrites a prior file. func uniquePath(dir, name string) string { target := filepath.Join(dir, name) if _, err := os.Stat(target); os.IsNotExist(err) { return target } ext := filepath.Ext(name) stem := strings.TrimSuffix(name, ext) for i := 1; ; i += 1 { cand := filepath.Join(dir, fmt.Sprintf("%s (%d)%s", stem, i, ext)) if _, err := os.Stat(cand); os.IsNotExist(err) { return cand } } } // revealInFileManager opens the OS file manager with the saved file selected. // Best-effort: the save already succeeded, so any error here is ignored. func revealInFileManager(path string) { switch runtime.GOOS { case "darwin": _ = exec.Command("open", "-R", path).Start() case "windows": _ = exec.Command("explorer", "/select,"+path).Start() } }