collective

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

diff --git a/crates/craft-status-app/src/lib.rs b/crates/craft-status-app/src/lib.rs--- a/crates/craft-status-app/src/lib.rs+++ b/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")))]