use super::*;use crate::PathUri;use codex_utils_absolute_path::AbsolutePathBuf;use pretty_assertions::assert_eq;#[derive(Clone, Copy, Debug)]struct RenderCase { uri: &'static str, convention: PathConvention, expected: RenderExpectation,}impl RenderCase { const fn round_trips( uri: &'static str, convention: PathConvention, rendered: &'static str, ) -> Self { Self { uri, convention, expected: RenderExpectation::RoundTrip(rendered), } } const fn rejects(uri: &'static str, convention: PathConvention, error: ExpectedError) -> Self { Self { uri, convention, expected: RenderExpectation::Error(error), } } const fn renders_lossily( uri: &'static str, convention: PathConvention, rendered: &'static str, ) -> Self { Self { uri, convention, expected: RenderExpectation::RenderOnly(rendered), } }}#[derive(Clone, Copy, Debug)]enum RenderExpectation { RoundTrip(&'static str), RenderOnly(&'static str), Error(ExpectedError),}#[derive(Clone, Copy, Debug)]enum ExpectedError { OpaqueFallback, IncompatibleConvention,}const RENDER_CASES: &[RenderCase] = &[ // POSIX paths. RenderCase::round_trips("file:///", PathConvention::Posix, "/"), RenderCase::round_trips( "file:///home/alice/src/main.rs", PathConvention::Posix, "/home/alice/src/main.rs", ), RenderCase::round_trips( "file:///home/alice/a%20file.rs", PathConvention::Posix, "/home/alice/a file.rs", ), RenderCase::round_trips( "file:///workspace/src/lib.rs", PathConvention::Posix, "/workspace/src/lib.rs", ), RenderCase::round_trips( "file:///workspace/tests/test.rs", PathConvention::Posix, "/workspace/tests/test.rs", ), RenderCase::round_trips("file:///etc", PathConvention::Posix, "/etc"), RenderCase::round_trips("file:///tmp/", PathConvention::Posix, "/tmp/"), RenderCase::round_trips("file:///C:/Project", PathConvention::Posix, "/C:/Project"), RenderCase::round_trips("file:///C:", PathConvention::Posix, "/C:"), RenderCase::round_trips("file:///tmp/%E2%98%83", PathConvention::Posix, "/tmp/☃"), RenderCase::round_trips("file:///tmp/a%5Cb", PathConvention::Posix, "/tmp/a\\b"), RenderCase::round_trips( "file:///tmp/100%25/file", PathConvention::Posix, "/tmp/100%/file", ), RenderCase::round_trips( "file:///tmp/a%3Fb%23c%25d", PathConvention::Posix, "/tmp/a?b#c%d", ), RenderCase::round_trips("file:///tmp/a%252Fb", PathConvention::Posix, "/tmp/a%2Fb"), RenderCase::round_trips( "file:///bad/path/L3RtcC9udWxsLQAt_y1ieXRl", PathConvention::Posix, "/bad/path/L3RtcC9udWxsLQAt_y1ieXRl", ), RenderCase::round_trips( "FILE:///workspace/src", PathConvention::Posix, "/workspace/src", ), RenderCase::round_trips( "file:/workspace/src", PathConvention::Posix, "/workspace/src", ), RenderCase::round_trips( "file://localhost/workspace/src", PathConvention::Posix, "/workspace/src", ), RenderCase::round_trips( "file://LOCALHOST/workspace/src", PathConvention::Posix, "/workspace/src", ), // Windows drive paths. RenderCase::round_trips( "file:///C:/Users/Alice%20Smith/src/main.rs", PathConvention::Windows, r"C:\Users\Alice Smith\src\main.rs", ), RenderCase::round_trips("file:///C:/", PathConvention::Windows, "C:\\"), RenderCase::renders_lossily("file:///C:", PathConvention::Windows, "C:\\"), RenderCase::round_trips("file:///C:/Users", PathConvention::Windows, r"C:\Users"), RenderCase::round_trips("file:///C:/Windows", PathConvention::Windows, r"C:\Windows"), RenderCase::round_trips( "file:///d:/snowman/%E2%98%83", PathConvention::Windows, r"d:\snowman\☃", ), RenderCase::round_trips("file:///C:/tmp/", PathConvention::Windows, "C:\\tmp\\"), RenderCase::round_trips( "file:///C:/test%20with%20%25/path", PathConvention::Windows, r"C:\test with %\path", ), RenderCase::round_trips( "file:///C:/test%20with%20%2525/c%23code", PathConvention::Windows, r"C:\test with %25\c#code", ), RenderCase::round_trips( "file:///C:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins/c%23/plugin.json", PathConvention::Windows, r"C:\Source\Zürich or Zurich (ˈzjʊərɪk,\Code\resources\app\plugins\c#\plugin.json", ), RenderCase::round_trips( "file:///C:/project/owner's_file/database.sqlite", PathConvention::Windows, r"C:\project\owner's_file\database.sqlite", ), RenderCase::round_trips( "file:///C:/project/%25A0.txt", PathConvention::Windows, r"C:\project\%A0.txt", ), RenderCase::round_trips( "file:///C:/project/%252e.txt", PathConvention::Windows, r"C:\project\%2e.txt", ), // Windows UNC paths. RenderCase::round_trips( "file://server/share/src/main.rs", PathConvention::Windows, r"\\server\share\src\main.rs", ), RenderCase::round_trips( "file://server/share", PathConvention::Windows, r"\\server\share", ), RenderCase::round_trips( "file://server/share/", PathConvention::Windows, "\\\\server\\share\\", ), RenderCase::round_trips( "file://shares/files/c%23/p.cs", PathConvention::Windows, r"\\shares\files\c#\p.cs", ), RenderCase::round_trips( "file://monacotools1/certificates/SSL/", PathConvention::Windows, "\\\\monacotools1\\certificates\\SSL\\", ), // Opaque fallbacks rendered according to their source convention. RenderCase::renders_lossily( "file:///%00/bad/path/L3RtcC9udWxsLQAt_y1ieXRl", PathConvention::Posix, "/tmp/null-\0-U+FFFD-byte", ), RenderCase::round_trips( "file:///%00/bad/path/XABcAC4AXABDAE8ATQAxAFwA", PathConvention::Windows, r"\\.\COM1\", ), RenderCase::round_trips( "file:///%00/bad/path/XABcAD8AXABWAG8AbAB1AG0AZQB7ADAAMAAwADAAMAAwADAAMAAtADAAMAAwADAALQAwADAAMAAwAC0AMAAwADAAMAAtADAAMAAwADAAMAAwADAAMAAwADAAMAAwAH0AXABmAGkAbABlAC4AcgBzAA", PathConvention::Windows, r"\\?\Volume{00000000-0000-0000-0000-000000000000}\file.rs", ), // Windows rendering preserves path text without filesystem validation. RenderCase::round_trips("file:///C:/a%3Fb", PathConvention::Windows, "C:\\a?b"), RenderCase::round_trips("file:///C:/a*b", PathConvention::Windows, "C:\\a*b"), RenderCase::round_trips( "file:///C:/trailing.", PathConvention::Windows, "C:\\trailing.", ), RenderCase::round_trips( "file:///C:/trailing%20", PathConvention::Windows, "C:\\trailing ", ), RenderCase::round_trips( "file:///C:/control-%01", PathConvention::Windows, "C:\\control-\u{1}", ), RenderCase::round_trips( "file:///C:/file.txt:stream", PathConvention::Windows, "C:\\file.txt:stream", ), RenderCase::round_trips( "file://server/sh%3Fare/file.rs", PathConvention::Windows, "\\\\server\\sh?are\\file.rs", ), // These renderings intentionally lose URI byte or segment boundaries. RenderCase::renders_lossily( "file:///tmp/non-utf8-%FF", PathConvention::Posix, "/tmp/non-utf8-U+FFFD", ), RenderCase::renders_lossily( "file:///tmp/non-utf8-%A0", PathConvention::Posix, "/tmp/non-utf8-U+FFFD", ), RenderCase::renders_lossily("file:///tmp/a%2Fb", PathConvention::Posix, "/tmp/a/b"), RenderCase::renders_lossily("file:///C:/a%2Fb", PathConvention::Windows, "C:\\a/b"), RenderCase::renders_lossily("file:///C:/a%5Cb", PathConvention::Windows, "C:\\a\\b"), // URI shapes that do not match the requested convention. RenderCase::rejects( "file://server/share/file.txt", PathConvention::Posix, ExpectedError::IncompatibleConvention, ), RenderCase::rejects( "file://server/share/file.rs", PathConvention::Posix, ExpectedError::IncompatibleConvention, ), RenderCase::rejects( "file:///usr/local/file.txt", PathConvention::Windows, ExpectedError::IncompatibleConvention, ), RenderCase::rejects( "file:///home/alice/file.rs", PathConvention::Windows, ExpectedError::IncompatibleConvention, ), RenderCase::rejects( "file://server/", PathConvention::Windows, ExpectedError::IncompatibleConvention, ), RenderCase::rejects( "file:///_:/path", PathConvention::Windows, ExpectedError::IncompatibleConvention, ), // Invalid opaque fallback payloads. RenderCase::rejects( "file:///%00/bad/path/YQ", PathConvention::Posix, ExpectedError::OpaqueFallback, ), RenderCase::rejects( "file:///%00/bad/path/L3RtcC9udWxsLQAt_y1ieXRl", PathConvention::Windows, ExpectedError::OpaqueFallback, ),];#[test]fn renders_native_paths_from_shared_cases() { for case in RENDER_CASES { let path = PathUri::parse(case.uri).expect("valid file URI"); let expected = match case.expected { RenderExpectation::RoundTrip(rendered) => Ok(LegacyAppPathString(rendered.to_string())), RenderExpectation::RenderOnly(rendered) => { Ok(LegacyAppPathString(rendered.to_string())) } RenderExpectation::Error(ExpectedError::OpaqueFallback) => { Err(LegacyAppPathStringError::OpaqueFallback { path: path.to_string(), }) } RenderExpectation::Error(ExpectedError::IncompatibleConvention) => { Err(LegacyAppPathStringError::IncompatibleConvention { path: path.to_string(), convention: case.convention, }) } }; let actual = LegacyAppPathString::from_path_uri(&path, case.convention); assert_eq!(actual, expected, "rendering {case:?}"); if let Ok(rendered) = &actual { assert_eq!( rendered.infer_absolute_path_convention(), Some(case.convention), "inferring {case:?}" ); } if let RenderExpectation::RoundTrip(rendered) = case.expected { let api_path = serde_json::from_value::<LegacyAppPathString>(serde_json::json!(rendered)) .expect("native path should deserialize from API text"); let reparsed = api_path .to_path_uri(case.convention) .expect("native path should parse using its convention"); assert_eq!(reparsed, path, "parsing {case:?}"); assert_eq!( LegacyAppPathString::from_path_uri(&reparsed, case.convention), Ok(api_path), "round-tripping {case:?}" ); } }}#[test]fn relative_api_path_serializes_and_deserializes_unchanged() { for raw_path in [".", "subdir", "subdir/file.rs"] { let path = serde_json::from_value::<LegacyAppPathString>(serde_json::json!(raw_path)) .expect("relative API path should deserialize"); assert_eq!( serde_json::to_value(path).expect("relative API path should serialize"), serde_json::json!(raw_path) ); }}#[test]fn relative_api_path_is_invalid_when_converted_to_a_path_uri() { let raw_path = "subdir"; let path = serde_json::from_value::<LegacyAppPathString>(serde_json::json!(raw_path)) .expect("relative API path should deserialize"); assert_eq!(path.infer_absolute_path_convention(), None); assert_eq!( path.to_path_uri(PathConvention::Posix), Err(LegacyAppPathStringError::InvalidNativePath { path: raw_path.to_string(), convention: PathConvention::Posix, }) );}#[test]fn other_non_absolute_api_paths_cannot_be_converted_to_path_uris() { for (raw_path, convention) in [ (r"workspace\file.rs", PathConvention::Windows), (r"C:file.rs", PathConvention::Windows), ] { let path = serde_json::from_value::<LegacyAppPathString>(serde_json::json!(raw_path)) .expect("API path should deserialize without validation"); assert_eq!(path.infer_absolute_path_convention(), None); assert_eq!( path.to_path_uri(convention), Err(LegacyAppPathStringError::InvalidNativePath { path: raw_path.to_string(), convention, }) ); }}#[test]fn infers_absolute_path_conventions_from_api_text() { for (raw_path, expected) in [ (r"C:\workspace\file.rs", Some(PathConvention::Windows)), ("c:/workspace/file.rs", Some(PathConvention::Windows)), (r"\\server\share\file.rs", Some(PathConvention::Windows)), (r"\\?\C:\workspace\file.rs", Some(PathConvention::Windows)), (r"\\.\COM1", Some(PathConvention::Windows)), ("/workspace/file.rs", Some(PathConvention::Posix)), ("/C:/workspace/file.rs", Some(PathConvention::Posix)), ("//server/share/file.rs", Some(PathConvention::Posix)), ("", None), (".", None), ("subdir/file.rs", None), (r"subdir\file.rs", None), (r"C:file.rs", None), (r"\rooted-without-drive", None), ] { let path = serde_json::from_value::<LegacyAppPathString>(serde_json::json!(raw_path)) .expect("API path should deserialize without validation"); assert_eq!( path.infer_absolute_path_convention(), expected, "inferring {raw_path:?}" ); }}#[test]fn foreign_absolute_syntax_deserializes_without_host_interpretation() { for (raw_path, convention) in [ (r"C:\workspace\file.rs", PathConvention::Windows), ("/workspace/file.rs", PathConvention::Posix), ] { let path = serde_json::from_value::<LegacyAppPathString>(serde_json::json!(raw_path)) .expect("foreign API path should deserialize"); assert_eq!(path.as_str(), raw_path); assert_eq!(path.infer_absolute_path_convention(), Some(convention)); }}#[test]fn renders_an_absolute_path_using_the_host_convention() { #[cfg(unix)] let native_path = "/workspace/a file.rs"; #[cfg(windows)] let native_path = r"C:\workspace\a file.rs"; let path = AbsolutePathBuf::from_absolute_path_checked(native_path) .expect("native path should be absolute"); assert_eq!( LegacyAppPathString::from(path), LegacyAppPathString(native_path.to_string()) );}#[cfg(windows)]#[test]fn renders_native_non_unicode_windows_fallback_lossily() { use std::os::windows::ffi::OsStringExt; let native_path = std::path::PathBuf::from(std::ffi::OsString::from_wide( &r"C:\bad\" .encode_utf16() .chain([0xd800]) .collect::<Vec<_>>(), )); let native_path = AbsolutePathBuf::from_absolute_path_checked(native_path).expect("absolute native path"); assert_eq!( LegacyAppPathString::from_abs_path(&native_path), LegacyAppPathString(r"C:\bad\U+FFFD".to_string()) ); let path = PathUri::from_abs_path(&native_path); assert_eq!( LegacyAppPathString::from_path_uri(&path, PathConvention::Windows), Ok(LegacyAppPathString(r"C:\bad\U+FFFD".to_string())) ); assert_eq!( LegacyAppPathString::from_path_uri(&path, PathConvention::Posix), Err(LegacyAppPathStringError::OpaqueFallback { path: path.to_string(), }) );}#[test]fn serializes_and_deserializes_as_a_string() { let path = PathUri::parse("file:///workspace/src/lib.rs").expect("valid file URI"); let rendered = LegacyAppPathString::from_path_uri(&path, PathConvention::Posix) .expect("POSIX URI should render"); let json = serde_json::to_string(&rendered).expect("rendered path should serialize"); assert_eq!(json, r#""/workspace/src/lib.rs""#); assert_eq!( serde_json::from_str::<LegacyAppPathString>(&json) .expect("rendered path should deserialize from a string"), rendered );}