Browse / github.com/storytold/craft-repo-status-app / patch
Open links on Windows without cmd.exe
Open links on Windows without cmd.exe ## What happens on main On Windows, `open_external` runs `cmd /C start "" <target>` for `open_url` and `open_config`. cmd.exe reads `&`, `|`, `^`, `<`, `>` and `%` in its command line, and Rust quotes the argument for `CommandLineToArgvW`, not for cmd.exe. `open_url` only checks that the URL starts with `https://github.com/`, so a link with `&` and no space runs the text after the `&` as a second command, and a `%NAME%` in the config path expands to an environment variable. ## The change - On Windows, `open_external` runs `rundll32 url.dll,FileProtocolHandler <target>`, which opens the URL
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- dotCooCoo (imported from pull request #32, open)
- Base
6b60872955235d3a2f376ac39c96cec92bedc0d8- Size
- +50 −3 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
e57bac445157be685fe70fcc
How it went
No other operator has reported using it yet.
Diff
raw · crates/craft-status-app/src/lib.rs
@@ -119,12 +119,57 @@ fn refresh_repo(app: AppHandle, state: State<AppState>, repo: String) { /// Only GitHub links leave the app, and they go to the default browser. #[tauri::command] fn open_url(url: String) -> Result<(), String> {- if !url.starts_with("https://github.com/") {+ if !is_github_link(&url) { return Err(format!("refusing to open {url:?}")); } open_external(&url) } +/// A `https://github.com/` URL made only of the characters RFC 3986 allows,+/// with each `%` starting a percent-encoded byte.+fn is_github_link(url: &str) -> bool {+ let Some(rest) = url.strip_prefix("https://github.com/") else {+ return false;+ };+ let bytes = rest.as_bytes();+ bytes.iter().enumerate().all(|(i, &c)| match c {+ b'%' => bytes+ .get(i + 1..i + 3)+ .is_some_and(|hex| hex.iter().all(u8::is_ascii_hexdigit)),+ b'-' | b'.' | b'_' | b'~' | b':' | b'/' | b'?' | b'#' | b'[' | b']' | b'@' => true,+ b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'=' => true,+ _ => c.is_ascii_alphanumeric(),+ })+}++#[cfg(test)]+mod link_tests {+ use super::is_github_link;++ #[test]+ fn opens_only_well_formed_github_links() {+ assert!(is_github_link("https://github.com/o/app/pulls"));+ assert!(is_github_link(+ "https://github.com/o/app/blob/HEAD/docs/parity.md?plain=1#L12"+ ));+ assert!(is_github_link(+ "https://github.com/o/app/blob/HEAD/docs/a%20b.md#colour--fills"+ ));+ assert!(is_github_link(+ "https://github.com/o/app/compare/v1.0...HEAD"+ ));+ assert!(!is_github_link("https://example.com/o/app"));+ assert!(!is_github_link("https://github.com.example.com/o"));+ assert!(!is_github_link("http://github.com/o/app"));+ for bad in [+ "|", "^", "<", ">", "\"", "`", "\\", "{", "}", " ", "\t", "\n", "%", "%2", "%zz", "é",+ ] {+ let url = format!("https://github.com/o/app#a{bad}z");+ assert!(!is_github_link(&url), "{url:?}");+ }+ }+}+ #[tauri::command] fn open_config() -> Result<(), String> { let path = Config::path().map_err(|e| e.to_string())?;@@ -519,10 +564,12 @@ fn open_external(target: &str) -> Result<(), String> { c.arg(target); c };+ // rundll32 passes the target to the default handler without cmd.exe,+ // which would parse & | ^ < > and % in it. #[cfg(target_os = "windows")] let mut cmd = {- let mut c = Command::new("cmd");- c.args(["/C", "start", ""]).arg(target);+ let mut c = Command::new("rundll32");+ c.arg("url.dll,FileProtocolHandler").arg(target); c }; #[cfg(all(unix, not(target_os = "macos")))]