//! Open a URL in the default browser (AGENTS.md §13 / §38). //! //! Strict scheme check: only `http://` and `https://` pass through //! (the docstring on §13 spells out "拒绝 file:// / javascript: / //! 自定义 scheme"). `url::Url::parse` does the structural validation //! (RFC 3986); we still gate on the scheme afterward so a parseable //! but dangerous scheme (`data:`, `javascript:`, custom apps) is //! refused even if the URL came from a trusted source. use url::Url; use windows_sys::Win32::UI::Shell::ShellExecuteW; use windows_sys::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL; /// `true` if the URL was accepted and handed to ShellExecuteW. `false` /// if the scheme was not http/https, the URL failed to parse, or /// ShellExecuteW returned a <=32 error code (per the shell API /// convention: "If the function succeeds, it returns a value greater /// than 32"). pub fn open(url_str: &str) -> bool { let trimmed = url_str.trim(); if trimmed.is_empty() { return false; } let parsed = match Url::parse(trimmed) { Ok(u) => u, Err(e) => { tracing::warn!(url = %url_str, error = %e, "url: parse failed"); return false; } }; match parsed.scheme() { "http" | "https" => {} // ok other => { tracing::warn!(scheme = %other, "url: refusing non-http(s) scheme"); return false; } } // ShellExecuteW takes a wide, null-terminated string for lpFile. let wide: Vec = trimmed.encode_utf16().chain(std::iter::once(0)).collect(); // SAFETY: ShellExecuteW spawns the default handler; passing NULL for // lpOperation / lpParameters / lpDirectory uses the file's verb // association. The wide string is null-terminated and outlives // the call. let result = unsafe { ShellExecuteW( std::ptr::null_mut(), std::ptr::null(), wide.as_ptr(), std::ptr::null(), std::ptr::null(), SW_SHOWNORMAL, ) }; if result as isize > 32 { true } else { tracing::warn!(code = result as isize, "url: ShellExecuteW failed"); false } } #[cfg(test)] mod tests { use super::*; #[test] fn empty_string_rejected() { assert!(!open("")); assert!(!open(" ")); } #[test] fn unparseable_rejected() { assert!(!open("not a url")); assert!(!open("http://[invalid")); } #[test] fn disallowed_schemes_rejected() { assert!(!open("file:///etc/passwd")); assert!(!open("javascript:alert(1)")); assert!(!open("data:text/plain,hi")); assert!(!open("ftp://example.com/")); assert!(!open("custom://thing")); } // We do not exercise the http/https success path here — it would // spawn the user's default browser. The CI environment can't // assert on that. The dispatch logic is covered by the rejection // tests above. }