diff --git a/src/core.rs b/src/core.rs index bc29105..2423f86 100644 --- a/src/core.rs +++ b/src/core.rs @@ -16,7 +16,7 @@ use crate::html::{ has_favicon, html_to_dom, serialize_document, set_base_url, set_charset, set_robots, walk, }; use crate::session::Session; -use crate::url::{create_data_url, resolve_url}; +use crate::url::{create_data_url, is_windows_drive_path, resolve_url}; #[derive(Debug)] pub struct MonolithError { @@ -167,7 +167,7 @@ pub fn create_monolithic_document_from_data( } else { // Custom base URL provided match Url::parse(&custom_base_url) { - Ok(parsed_url) => { + Ok(parsed_url) if !is_windows_drive_path(&custom_base_url) => { if parsed_url.scheme() == "file" { // File base URLs can only work with documents saved from filesystem if base_url.scheme() == "file" { @@ -177,7 +177,7 @@ pub fn create_monolithic_document_from_data( base_url = parsed_url; } } - Err(_) => { + _ => { // Failed to parse given base URL, perhaps it's a filesystem path? if base_url.scheme() == "file" { // Relative paths could work for documents saved from filesystem @@ -358,7 +358,7 @@ pub fn create_monolithic_document( let mut target_url = match target.as_str() { target_str => match Url::parse(target_str) { - Ok(target_url) => match target_url.scheme() { + Ok(target_url) if !is_windows_drive_path(target_str) => match target_url.scheme() { "data" | "file" | "http" | "https" => target_url, unsupported_scheme => { return Err(MonolithError::new(&format!( @@ -367,7 +367,7 @@ pub fn create_monolithic_document( ))); } }, - Err(_) => { + _ => { // Failed to parse given base URL (perhaps it's a filesystem path?) let path: &Path = Path::new(&target_str); diff --git a/src/url.rs b/src/url.rs index 9265633..653a3d4 100644 --- a/src/url.rs +++ b/src/url.rs @@ -147,6 +147,17 @@ pub fn domain_is_within_domain(domain: &str, domain_to_match_against: &str) -> b ok } +/// Checks if the input is an absolute Windows path (e.g. `C:\dir\page.html` or `C:/dir/page.html`), +/// which the URL parser would otherwise take for a URL with a one-letter scheme +pub fn is_windows_drive_path(input: &str) -> bool { + let bytes = input.as_bytes(); + + bytes.len() >= 3 + && bytes[0].is_ascii_alphabetic() + && bytes[1] == b':' + && (bytes[2] == b'\\' || bytes[2] == b'/') +} + pub fn is_url_and_has_protocol(input: &str) -> bool { match Url::parse(input) { Ok(parsed_url) => !parsed_url.scheme().is_empty(), diff --git a/tests/cli/local_files.rs b/tests/cli/local_files.rs index 665a136..24d03e8 100644 --- a/tests/cli/local_files.rs +++ b/tests/cli/local_files.rs @@ -127,6 +127,40 @@ document.body.style.color = "red"; out.assert().code(0); } + #[test] + fn local_file_target_input_full_target_path() { + let mut cmd = cargo_bin_cmd!(env!("CARGO_PKG_NAME")); + // On Windows this is a path that starts with a drive letter, e.g. "C:\...\local-file.html" + let path_html = env::current_dir() + .unwrap() + .join("tests") + .join("_data_") + .join("basic") + .join("local-file.html"); + + let out = cmd + .arg("-M") + .arg("-Ijci") + .arg(path_html.as_os_str()) + .output() + .unwrap(); + + // STDERR should contain only the target file + assert_eq!( + String::from_utf8_lossy(&out.stderr), + format!( + "{file_url_html}\n", + file_url_html = Url::from_file_path(fs::canonicalize(&path_html).unwrap()).unwrap(), + ) + ); + + // STDOUT should contain HTML from the local file + assert!(String::from_utf8_lossy(&out.stdout).contains("Local HTML file")); + + // Exit code should be 0 + out.assert().code(0); + } + #[test] fn local_file_url_target_input() { let mut cmd = cargo_bin_cmd!(env!("CARGO_PKG_NAME")); diff --git a/tests/url/is_windows_drive_path.rs b/tests/url/is_windows_drive_path.rs new file mode 100644 index 0000000..cd8ec63 --- /dev/null +++ b/tests/url/is_windows_drive_path.rs @@ -0,0 +1,69 @@ +// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ +// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ +// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ +// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ +// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ +// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ + +#[cfg(test)] +mod passing { + use monolith::url; + + #[test] + fn backslashes() { + assert!(url::is_windows_drive_path( + r"C:\Users\Me\Downloads\page.html" + )); + } + + #[test] + fn forward_slashes() { + assert!(url::is_windows_drive_path( + "C:/Users/Me/Downloads/page.html" + )); + } + + #[test] + fn lowercase_drive_letter() { + assert!(url::is_windows_drive_path(r"d:\page.html")); + } +} + +// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗ +// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝ +// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗ +// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║ +// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝ +// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ + +#[cfg(test)] +mod failing { + use monolith::url; + + #[test] + fn file_url() { + assert!(!url::is_windows_drive_path( + "file:///C:/Users/Me/Downloads/page.html" + )); + } + + #[test] + fn http_url() { + assert!(!url::is_windows_drive_path("https://example.com/")); + } + + #[test] + fn unix_path() { + assert!(!url::is_windows_drive_path("/home/me/page.html")); + } + + #[test] + fn relative_path() { + assert!(!url::is_windows_drive_path("page.html")); + } + + #[test] + fn one_letter_scheme() { + assert!(!url::is_windows_drive_path("c:page.html")); + } +} diff --git a/tests/url/mod.rs b/tests/url/mod.rs index 001364b..a848915 100644 --- a/tests/url/mod.rs +++ b/tests/url/mod.rs @@ -3,5 +3,6 @@ mod create_data_url; mod domain_is_within_domain; mod get_referer_url; mod is_url_and_has_protocol; +mod is_windows_drive_path; mod parse_data_url; mod resolve_url;