diff --git a/tests/invalid_encoding.rs b/tests/invalid_encoding.rs index 791c504..020d19e 100644 --- a/tests/invalid_encoding.rs +++ b/tests/invalid_encoding.rs @@ -86,13 +86,14 @@ mod unix { } #[test] - fn slash_conversion_reports_and_replaces_invalid_encoding() { - let input = path(b"dir/\x80/file"); + fn non_utf8_slash_policies_preserve_non_normalized_spelling() { + let input = path(b"./dir//invalid-\x80/../tail/"); + let expected = "./dir//invalid-\u{fffd}/../tail/"; assert!(input.try_to_slash().is_none()); assert!(std::panic::catch_unwind(|| input.to_slash()).is_err()); let lossy = input.to_slash_lossy(); - assert_eq!(lossy, "dir/\u{fffd}/file"); + assert_eq!(lossy, expected); assert!(matches!(&lossy, Cow::Owned(_))); } @@ -319,13 +320,19 @@ mod windows { } #[test] - fn slash_conversion_reports_and_replaces_invalid_encoding() { - let input = invalid_path(r"dir\", r"\file"); + fn invalid_unicode_slash_policies_preserve_non_normalized_spelling() { + let input = invalid_path(r".\dir\\invalid-", r"\..\tail\"); + let expected = "./dir//invalid-\u{fffd}/../tail/"; assert!(input.try_to_slash().is_none()); assert!(std::panic::catch_unwind(|| input.to_slash()).is_err()); let lossy = input.to_slash_lossy(); - assert_eq!(lossy, "dir/\u{fffd}/file"); + assert_eq!(lossy, expected); + assert!(matches!(&lossy, Cow::Owned(_))); + + let no_native_separator = invalid_path("invalid-", "/tail"); + let lossy = no_native_separator.to_slash_lossy(); + assert_eq!(lossy, "invalid-\u{fffd}/tail"); assert!(matches!(&lossy, Cow::Owned(_))); } diff --git a/tests/owned_api.rs b/tests/owned_api.rs index f63cdd3..81f1a81 100644 --- a/tests/owned_api.rs +++ b/tests/owned_api.rs @@ -69,28 +69,42 @@ fn into_normalized_matches_borrowed_api_for_dirty_paths() { #[test] fn owned_slash_apis_reuse_valid_unicode_buffers() { #[cfg(target_family = "unix")] - let (input, expected) = ( - "/workspace/rolldown/crates/rolldown/src/module_loader/module_task.rs", - "/workspace/rolldown/crates/rolldown/src/module_loader/module_task.rs", - ); + let cases = [ + ( + "/workspace/rolldown/crates/rolldown/src/module_loader/module_task.rs", + "/workspace/rolldown/crates/rolldown/src/module_loader/module_task.rs", + ), + (r"/root/./β/../tail//literal\name//", r"/root/./β/../tail//literal\name//"), + ]; #[cfg(target_family = "windows")] - let (input, expected) = ( - r"C:\workspace\rolldown\crates\rolldown\src\module_loader\module_task.rs", - "C:/workspace/rolldown/crates/rolldown/src/module_loader/module_task.rs", - ); + let cases = [ + ( + r"C:\workspace\rolldown\crates\rolldown\src\module_loader\module_task.rs", + "C:/workspace/rolldown/crates/rolldown/src/module_loader/module_task.rs", + ), + (r"\\server\share\.\β\..\tail\\foreign/name\\", "//server/share/./β/../tail//foreign/name//"), + ("//server/share/./β/../tail//foreign/name//", "//server/share/./β/../tail//foreign/name//"), + ]; - let path = owned_path_with_capacity(input); - let identity = buffer_identity(&path); - let slash = path.into_slash(); + for (input, expected) in cases { + let path = owned_path_with_capacity(input); + let identity = buffer_identity(&path); + let slash = path.into_slash(); + assert_eq!(slash, expected, "strict: input {input:?}"); + assert_eq!((slash.as_ptr(), slash.capacity()), identity, "strict: input {input:?}"); - assert_eq!(slash, expected); - assert_eq!((slash.as_ptr(), slash.capacity()), identity); + let path = owned_path_with_capacity(input); + let identity = buffer_identity(&path); + let slash = path.try_into_slash().expect("the fixture is valid Unicode"); + assert_eq!(slash, expected, "try: input {input:?}"); + assert_eq!((slash.as_ptr(), slash.capacity()), identity, "try: input {input:?}"); - let path = owned_path_with_capacity(input); - let identity = buffer_identity(&path); - let slash = path.try_into_slash().expect("the fixture is valid Unicode"); - assert_eq!(slash, expected); - assert_eq!((slash.as_ptr(), slash.capacity()), identity); + let path = owned_path_with_capacity(input); + let identity = buffer_identity(&path); + let slash = path.into_slash_lossy(); + assert_eq!(slash, expected, "lossy: input {input:?}"); + assert_eq!((slash.as_ptr(), slash.capacity()), identity, "lossy: input {input:?}"); + } } #[cfg(target_family = "unix")] @@ -101,13 +115,20 @@ fn owned_apis_preserve_and_replace_invalid_unix_encoding() { os::unix::ffi::{OsStrExt, OsStringExt}, }; - let input = PathBuf::from(OsString::from_vec(b"dir/invalid-\x80/./file".to_vec())); - let normalized = input.clone().into_normalized(); + let normalize_input = PathBuf::from(OsString::from_vec(b"dir/invalid-\x80/./file".to_vec())); + let normalized = normalize_input.into_normalized(); assert_eq!(normalized.as_os_str().as_bytes(), b"dir/invalid-\x80/file"); + + let input = PathBuf::from(OsString::from_vec(b"./dir//invalid-\x80/../tail/".to_vec())); + + let strict = input.clone(); + assert!(std::panic::catch_unwind(move || strict.into_slash()).is_err()); + + let recoverable = input.clone(); let returned = - input.clone().try_into_slash().expect_err("invalid Unix encoding must be returned unchanged"); + recoverable.try_into_slash().expect_err("invalid Unix encoding must be returned unchanged"); assert_eq!(returned.as_os_str().as_bytes(), input.as_os_str().as_bytes()); - assert_eq!(input.into_slash_lossy(), "dir/invalid-\u{fffd}/./file"); + assert_eq!(input.into_slash_lossy(), "./dir//invalid-\u{fffd}/../tail/"); } #[cfg(target_family = "windows")] @@ -118,22 +139,36 @@ fn owned_apis_preserve_and_replace_invalid_windows_encoding() { os::windows::ffi::{OsStrExt, OsStringExt}, }; - let mut input_wide: Vec = r"C:\workspace\invalid-".encode_utf16().collect(); - input_wide.push(0xd800); - input_wide.extend(r"\.\file".encode_utf16()); - let input = PathBuf::from(OsString::from_wide(&input_wide)); + let mut normalize_input_wide: Vec = r"C:\workspace\invalid-".encode_utf16().collect(); + normalize_input_wide.push(0xd800); + normalize_input_wide.extend(r"\.\file".encode_utf16()); + let normalize_input = PathBuf::from(OsString::from_wide(&normalize_input_wide)); - let mut expected_wide: Vec = r"C:\workspace\invalid-".encode_utf16().collect(); - expected_wide.push(0xd800); - expected_wide.extend(r"\file".encode_utf16()); + let mut normalized_wide: Vec = r"C:\workspace\invalid-".encode_utf16().collect(); + normalized_wide.push(0xd800); + normalized_wide.extend(r"\file".encode_utf16()); assert_eq!( - input.clone().into_normalized().as_os_str().encode_wide().collect::>(), - expected_wide, + normalize_input.into_normalized().as_os_str().encode_wide().collect::>(), + normalized_wide, ); - let returned = input - .clone() - .try_into_slash() - .expect_err("invalid Windows encoding must be returned unchanged"); + + let mut input_wide: Vec = r".\dir\\invalid-".encode_utf16().collect(); + input_wide.push(0xd800); + input_wide.extend(r"\..\tail\".encode_utf16()); + let input = PathBuf::from(OsString::from_wide(&input_wide)); + + let strict = input.clone(); + assert!(std::panic::catch_unwind(move || strict.into_slash()).is_err()); + + let recoverable = input.clone(); + let returned = + recoverable.try_into_slash().expect_err("invalid Windows encoding must be returned unchanged"); assert_eq!(returned.as_os_str().encode_wide().collect::>(), input_wide); - assert_eq!(input.into_slash_lossy(), "C:/workspace/invalid-\u{fffd}/./file"); + assert_eq!(input.into_slash_lossy(), "./dir//invalid-\u{fffd}/../tail/"); + + let mut no_native_separator_wide: Vec = "invalid-".encode_utf16().collect(); + no_native_separator_wide.push(0xd800); + no_native_separator_wide.extend("/tail".encode_utf16()); + let no_native_separator = PathBuf::from(OsString::from_wide(&no_native_separator_wide)); + assert_eq!(no_native_separator.into_slash_lossy(), "invalid-\u{fffd}/tail"); } diff --git a/tests/to_slash.rs b/tests/to_slash.rs index 12e1477..0715246 100644 --- a/tests/to_slash.rs +++ b/tests/to_slash.rs @@ -1,53 +1,132 @@ +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; + +use sugar_path::SugarPath; + +fn assert_cow_result( + result: Cow<'_, str>, + source: &str, + expected: &str, + expect_borrowed: bool, + receiver: &str, + policy: &str, +) { + assert_eq!(result, expected, "{receiver}/{policy}: input {source:?}"); + + match result { + Cow::Borrowed(actual) => { + assert!(expect_borrowed, "{receiver}/{policy} unexpectedly borrowed: input {source:?}"); + assert_eq!(actual.as_ptr(), source.as_ptr(), "{receiver}/{policy}: input {source:?}"); + assert_eq!(actual.len(), source.len(), "{receiver}/{policy}: input {source:?}"); + } + Cow::Owned(_) => { + assert!(!expect_borrowed, "{receiver}/{policy} unexpectedly allocated: input {source:?}"); + } + } +} + +fn assert_policies<'a>( + receiver: &str, + source: &'a str, + strict: Cow<'a, str>, + fallible: Option>, + lossy: Cow<'a, str>, + expected: &str, + expect_borrowed: bool, +) { + assert_cow_result(strict, source, expected, expect_borrowed, receiver, "strict"); + assert_cow_result( + fallible.expect("the fixture is valid UTF-8"), + source, + expected, + expect_borrowed, + receiver, + "try", + ); + assert_cow_result(lossy, source, expected, expect_borrowed, receiver, "lossy"); +} + +fn assert_valid_case(input: &str, expected: &str, expect_borrowed: bool) { + assert_policies( + "str", + input, + input.to_slash(), + input.try_to_slash(), + input.to_slash_lossy(), + expected, + expect_borrowed, + ); + + let string = input.to_owned(); + assert_policies( + "String", + &string, + string.to_slash(), + string.try_to_slash(), + string.to_slash_lossy(), + expected, + expect_borrowed, + ); + + let path = Path::new(input); + let source = path.to_str().expect("the fixture is valid UTF-8"); + assert_policies( + "Path", + source, + path.to_slash(), + path.try_to_slash(), + path.to_slash_lossy(), + expected, + expect_borrowed, + ); + + let path_buf = PathBuf::from(input); + let source = path_buf.to_str().expect("the fixture is valid UTF-8"); + assert_policies( + "PathBuf", + source, + path_buf.to_slash(), + path_buf.try_to_slash(), + path_buf.to_slash_lossy(), + expected, + expect_borrowed, + ); +} + #[cfg(target_family = "unix")] #[test] -fn unix() { - use std::borrow::Cow; - use sugar_path::SugarPath; - +fn unix_valid_receiver_and_policy_matrix_is_exact_and_borrowed() { let cases = [ ("hello/world", "hello/world"), - ("hello/world/", "hello/world/"), - ("/hello/world", "/hello/world"), - ("/hello/world/", "/hello/world/"), - ("/hello\\world", "/hello\\world"), + (r"/root/./β/../tail//literal\name//", r"/root/./β/../tail//literal\name//"), ]; - for (input, right) in cases { - let strict = input.to_slash(); - let lossy = input.to_slash_lossy(); - assert!(matches!(strict, Cow::Borrowed(_)), "strict result should borrow: {input:#?}"); - assert!(matches!(lossy, Cow::Borrowed(_)), "lossy result should borrow: {input:#?}"); - assert_eq!(strict, right, "case: {input:#?}"); - assert_eq!(lossy, right, "case: {input:#?}"); + for (input, expected) in cases { + assert_valid_case(input, expected, true); } } #[cfg(target_family = "windows")] #[test] -fn windows() { - use std::borrow::Cow; - use sugar_path::SugarPath; - +fn windows_valid_receiver_and_policy_matrix_preserves_spelling() { let cases = [ - ("hello\\world", "hello/world"), - ("hello\\world\\", "hello/world/"), - ("c:hello\\world", "c:hello/world"), - ("c:hello\\world\\", "c:hello/world/"), - ("c:\\hello\\world", "c:/hello/world"), - ("c:\\hello\\world/", "c:/hello/world/"), + (r"c:hello\world\", "c:hello/world/", false), + (r"C:\hello\world\", "C:/hello/world/", false), + ( + r"\\server\share\.\β\..\tail\\foreign/name\\", + "//server/share/./β/../tail//foreign/name//", + false, + ), + ( + "//server/share/./β/../tail//foreign/name//", + "//server/share/./β/../tail//foreign/name//", + true, + ), ]; - for (input, right) in cases { - let strict = input.to_slash(); - let lossy = input.to_slash_lossy(); - assert!(matches!(strict, Cow::Owned(_)), "strict result should own: {input:#?}"); - assert!(matches!(lossy, Cow::Owned(_)), "lossy result should own: {input:#?}"); - assert_eq!(strict, right, "case: {input:#?}"); - assert_eq!(lossy, right, "case: {input:#?}"); + for (input, expected, expect_borrowed) in cases { + assert_valid_case(input, expected, expect_borrowed); } - - let slash_only = "C:/hello/world"; - assert!(matches!(slash_only.to_slash(), Cow::Borrowed(_))); - assert!(matches!(slash_only.try_to_slash(), Some(Cow::Borrowed(_)))); - assert!(matches!(slash_only.to_slash_lossy(), Cow::Borrowed(_))); }