use super::*; use pretty_assertions::assert_eq; #[cfg(windows)] use std::ffi::OsString; #[cfg(unix)] use std::os::unix::ffi::OsStringExt; #[cfg(windows)] use std::os::windows::ffi::OsStringExt; use std::path::PathBuf; #[test] fn native_byte_joins_preserve_foreign_posix_filenames() { let base = PathUri::parse("file:///root/%FE/admin").unwrap(); for (path, expected) in [ ( b"../\xff/%2e?#\\".as_slice(), "file:///root/%FE/%FF/%252e%3F%23%5C", ), ( b"//other/./x/../\xff/.git".as_slice(), "file:///other/%FF/.git", ), (b"../../../../\xff".as_slice(), "file:///%FF"), (b"../plain".as_slice(), "file:///root/%FE/plain"), ] { assert_eq!(base.join_native_bytes(path).unwrap().to_string(), expected); } assert!(base.join_native_bytes(b"bad\0\xff").is_err()); assert!( PathUri::parse("file:///C:/repo") .unwrap() .join_native_bytes(b"\xff") .is_err() ); } #[test] fn file_uri_round_trips_an_absolute_path() { let path = AbsolutePathBuf::current_dir() .expect("current directory") .join("a path/file.rs"); let uri = PathUri::from_abs_path(&path); let uri_string = uri.to_string(); assert!(uri_string.starts_with("file:")); assert!(uri_string.ends_with("/a%20path/file.rs")); assert_eq!( PathUri::parse(&uri_string).expect("serialized URI should parse"), uri ); assert_eq!( uri.to_abs_path() .expect("local file URI should convert to a native path"), path ); } #[test] fn non_native_uri_io_conversion_is_invalid_input() { #[cfg(unix)] let uris = ["file://server/share/file.txt", "file:///C:/workspace"]; #[cfg(windows)] let uris = ["file:///usr/local/file.txt"]; for uri in uris { let uri = PathUri::parse(uri).expect("valid file URI"); let error = uri .to_abs_path() .expect_err("URI should not be host-native"); assert_eq!( (error.kind(), error.to_string()), ( io::ErrorKind::InvalidInput, format!("'{uri}' is invalid on '{}'", std::env::consts::OS), ) ); } } #[test] fn windows_uri_native_conversion_rejects_encoded_separators() { for uri in [ "file:///C%3A/plugins/demo/..%5Coutside.json", "file:///C%3a/plugins/demo/..%5coutside.json", "file:///C:/plugins/demo/..%2Foutside.json", "file://server/share/plugins/demo/..%5Coutside.json", ] { let uri = PathUri::parse(uri).expect("valid Windows file URI"); assert_eq!(uri.infer_path_convention(), Some(PathConvention::Windows)); assert!(containment_path_segments(&uri.0, PathConvention::Windows).is_none()); #[cfg(windows)] assert_eq!( uri.to_abs_path() .expect_err("encoded Windows separators must not reach native conversion") .kind(), io::ErrorKind::InvalidInput ); } } #[test] fn file_uri_parses_a_windows_path_on_any_host() { let uri = PathUri::parse("file:///C:/Users/Alice%20Smith/src/main.rs") .expect("Windows file URI should parse on every host"); assert_eq!(uri.encoded_path(), "/C:/Users/Alice%20Smith/src/main.rs"); assert_eq!(uri.basename(), Some("main.rs".to_string())); assert_eq!( uri.to_string(), "file:///C:/Users/Alice%20Smith/src/main.rs" ); } #[test] fn file_uri_normalizes_windows_drive_letter_case() { let lowercase = PathUri::parse("file:///c:/Users/Alice%20Smith/src/main.rs") .expect("Windows file URI should parse"); let uppercase = PathUri::parse("file:///C:/Users/Alice%20Smith/src/main.rs") .expect("Windows file URI should parse"); assert_eq!(lowercase, uppercase); assert_eq!( lowercase.to_string(), "file:///C:/Users/Alice%20Smith/src/main.rs" ); } #[test] fn path_uri_equality_and_hashing_follow_path_convention() { for (left, right, expected) in [ ("file:///C:/Users/Alice", "file:///c:/users/ALICE", true), ( "file://SERVER/SHARE/Project", "file://server/share/project", true, ), ("file:///home/Alice", "file:///home/alice", false), ("file:///C:/plugins/Lj", "file:///C:/plugins/LJ", false), ("file:///C:/plugins/%41", "file:///C:/plugins/a", true), ("file:///C:/plugins/a%2Fb", "file:///C:/plugins/a/b", false), ("file:///%00/bad/path/YQ", "file:///%00/bad/path/yQ", false), ] { let left = PathUri::parse(left).expect("valid left URI"); let right = PathUri::parse(right).expect("valid right URI"); assert_eq!( ( left == right, std::collections::HashSet::from([left]).contains(&right), ), (expected, expected), "comparing {right}" ); } } #[test] fn infers_path_conventions_from_uri_shape() { for (uri, expected) in [ ("file:///", Some(PathConvention::Posix)), ("file:///home/alice/src", Some(PathConvention::Posix)), ("file:///C:/Users/Alice/src", Some(PathConvention::Windows)), ("file:///d:", Some(PathConvention::Windows)), ( "file:///c%3A/Users/Alice/src", Some(PathConvention::Windows), ), ( "file:///D%3a/Users/Alice/src", Some(PathConvention::Windows), ), ("file://server/share/src", Some(PathConvention::Windows)), // Opaque fallback for POSIX bytes `/tmp/null-\0-\xff-byte`. ( "file:///%00/bad/path/L3RtcC9udWxsLQAt_y1ieXRl", Some(PathConvention::Posix), ), // Opaque fallback for Windows UTF-16LE `\\.\COM1\`. ( "file:///%00/bad/path/XABcAC4AXABDAE8ATQAxAFwA", Some(PathConvention::Windows), ), ("file:///%00/bad/path/YQ", None), ] { let path = PathUri::parse(uri).expect("valid path URI"); assert_eq!(path.infer_path_convention(), expected, "inferring {uri}"); } } #[test] fn path_convention_splits_absolute_relative_and_bare_path_text() { for (convention, path, expected) in [ ( PathConvention::Posix, "/usr/local/bin/bash", vec!["", "usr", "local", "bin", "bash"], ), ( PathConvention::Posix, r"tools\pwsh.exe", vec![r"tools\pwsh.exe"], ), ( PathConvention::Windows, r"C:\Program Files\PowerShell\7\pwsh.exe", vec!["C:", "Program Files", "PowerShell", "7", "pwsh.exe"], ), ( PathConvention::Windows, "tools/pwsh.exe", vec!["tools", "pwsh.exe"], ), (PathConvention::Windows, "cmd.exe", vec!["cmd.exe"]), ] { assert_eq!(convention.path_segments(path).collect::>(), expected); } } #[test] fn drive_shaped_posix_uri_is_intentionally_inferred_as_windows() { let path = PathUri::parse("file:///C:/actually/a/posix/path").expect("valid path URI"); // `/C:/...` is valid on POSIX, but treating this uncommon spelling as a // Windows drive lets callers render the overwhelmingly more common foreign // Windows URI without separately carrying its source convention. assert_eq!(path.infer_path_convention(), Some(PathConvention::Windows)); } #[test] fn inferred_native_path_string_uses_the_inferred_convention() { for (uri, expected) in [ ("file:///home/alice/a%20file.rs", "/home/alice/a file.rs"), ( "file:///C:/Users/Alice%20Smith/main.rs", r"C:\Users\Alice Smith\main.rs", ), ( "file:///c%3A/Users/Alice/src/main.rs", r"C:\Users\Alice\src\main.rs", ), ("file://server/share/main.rs", r"\\server\share\main.rs"), ("file://server/", "file://server/"), ("file:///%00/bad/path/YQ", "file:///%00/bad/path/YQ"), ] { let path = PathUri::parse(uri).expect("valid path URI"); assert_eq!( path.inferred_native_path_string(), expected, "rendering {uri}" ); assert_eq!( LegacyAppPathString::from(path).as_str(), expected, "rendering typed API path {uri}" ); } } #[test] fn relative_path_from_is_host_independent() { // `file://abc/...` has an authority and is inferred as Windows UNC, while // `file:///abc/...` is hostless and inferred as POSIX. for (path, base, expected) in [ ( "file:///home/alice/project/src/a%20file.rs", "file:///home/alice/project", Some("src/a file.rs"), ), ( "file:///c:/Users/Alice/project/src/main.rs", "file:///C:/Users/Alice/project", Some(r"src\main.rs"), ), ( "file:///C:/USERS/%C3%84/PROJECT/src/main.rs", "file:///c:/users/%C3%A4/project", None, ), ( "file://server/share/project/src/main.rs", "file://server/share/project", Some(r"src\main.rs"), ), ( "file://SERVER/SHARE/PROJECT/src/main.rs", "file://server/share/project", Some(r"src\main.rs"), ), ( "file:///home/alice/project", "file:///home/alice/project/", Some(""), ), ( "file:///home/alice/project-two/main.rs", "file:///home/alice/project", None, ), ("file:///HOME/alice/project", "file:///home", None), ( "file://other/share/project/main.rs", "file://server/share/project", None, ), ( "file:///home/alice/project/src%2Fmain.rs", "file:///home/alice/project", None, ), ( "file:///C:/project/src%5Cmain.rs", "file:///C:/project", None, ), ("file:///C:/project/main.rs", "file:///", None), ] { let path = PathUri::parse(path).expect("valid path URI"); let base = PathUri::parse(base).expect("valid base URI"); assert_eq!( path.relative_path_from(&base).as_deref(), expected, "finding {path} relative to {base}" ); } } #[test] fn relative_path_from_treats_fallback_uris_as_opaque() { let path = PathUri::parse("file:///%00/bad/path/YQ").expect("valid fallback URI"); let other = PathUri::parse("file:///%00/bad/path/Yg").expect("valid fallback URI"); let root = PathUri::parse("file:///").expect("valid root URI"); assert_eq!(path.relative_path_from(&path), Some(String::new())); assert_eq!(path.relative_path_from(&other), None); assert_eq!(path.relative_path_from(&root), None); } #[cfg(windows)] #[test] fn file_uri_falls_back_for_windows_prefixes_without_a_uri_representation() { for (native_path, expected_uri) in [ (r"\\.\COM1", "file:///%00/bad/path/XABcAC4AXABDAE8ATQAxAFwA"), ( r"\\?\Volume{00000000-0000-0000-0000-000000000000}\file.rs", "file:///%00/bad/path/XABcAD8AXABWAG8AbAB1AG0AZQB7ADAAMAAwADAAMAAwADAAMAAtADAAMAAwADAALQAwADAAMAAwAC0AMAAwADAAMAAtADAAMAAwADAAMAAwADAAMAAwADAAMAAwAH0AXABmAGkAbABlAC4AcgBzAA", ), ] { let path = AbsolutePathBuf::from_absolute_path_checked(native_path) .expect("Windows namespace path should be absolute"); let uri = PathUri::from_abs_path(&path); assert_eq!(uri.to_string(), expected_uri, "converting {native_path}"); assert_eq!( PathUri::parse(&uri.to_string()) .expect("fallback URI should parse") .to_abs_path() .expect("fallback URI should decode"), path, "round-tripping {native_path}" ); } } #[cfg(windows)] #[test] fn file_uri_fallback_round_trips_non_unicode_windows_paths() { let path_wide = r"C:\bad\" .encode_utf16() .chain([0xd800]) .collect::>(); let path = PathBuf::from(OsString::from_wide(&path_wide)); let path = AbsolutePathBuf::from_absolute_path_checked(path).expect("absolute Windows path"); let uri = PathUri::from_abs_path(&path); let reparsed = PathUri::parse(&uri.to_string()).expect("fallback URI should parse"); assert!(uri.to_string().starts_with(BAD_PATH_URI_PREFIX)); assert_eq!( reparsed.to_abs_path().expect("fallback URI should decode"), path ); } #[cfg(unix)] #[test] fn file_uri_falls_back_for_posix_paths_with_null_bytes() { let path = PathBuf::from(std::ffi::OsString::from_vec( b"/tmp/null-\0-\xff-byte".to_vec(), )); let path = AbsolutePathBuf::from_absolute_path_checked(path).expect("absolute POSIX path"); let uri = PathUri::from_abs_path(&path); assert_eq!( uri, PathUri::parse("file:///%00/bad/path/L3RtcC9udWxsLQAt_y1ieXRl") .expect("valid fallback URI") ); let json = serde_json::to_string(&uri).expect("fallback URI should serialize"); let reparsed: PathUri = serde_json::from_str(&json).expect("serialized fallback URI should parse"); assert_eq!(json, r#""file:///%00/bad/path/L3RtcC9udWxsLQAt_y1ieXRl""#); assert_eq!(reparsed, uri); assert_eq!( reparsed.to_abs_path().expect("fallback URI should decode"), path ); } #[cfg(unix)] #[test] fn ordinary_bad_path_uri_is_not_decoded_as_a_fallback() { let path = AbsolutePathBuf::from_absolute_path_checked("/bad/path/L3RtcC9udWxsLQAt_y1ieXRl") .expect("absolute POSIX path"); let uri = PathUri::from_abs_path(&path); assert_eq!(uri.to_string(), "file:///bad/path/L3RtcC9udWxsLQAt_y1ieXRl"); assert_eq!( uri.to_abs_path().expect("URI should convert literally"), path ); } #[test] fn malformed_bad_path_uris_are_rejected() { for uri in [ "file:///%00/bad/path/", "file:///%00/bad/path/not*base64", "file:///%00/bad/path/YQ==", "file:///%00/bad/path/YR", "file:///%00/bad/path/YQ/extra", "file:///%00/other/YQ", ] { assert_eq!( PathUri::parse(uri), Err(PathUriParseError::InvalidFileUriPath { path: uri.to_string(), }), "parsing {uri}" ); } } #[test] fn structurally_valid_bad_path_uri_with_invalid_native_payload_fails_conversion() { let uri = PathUri::parse("file:///%00/bad/path/YQ") .expect("canonical base64 fallback URI should parse"); assert_eq!( uri.to_abs_path() .expect_err("relative fallback payload should not convert") .kind(), io::ErrorKind::InvalidInput ); } #[test] fn bad_path_uris_are_opaque_to_lexical_operations() { let uri = PathUri::parse("file:///%00/bad/path/YQ") .expect("canonical base64 fallback URI should parse"); let other = PathUri::parse("file:///%00/bad/path/Yg") .expect("canonical base64 fallback URI should parse"); let root = PathUri::parse("file:///").expect("valid root URI"); assert_eq!(uri.basename(), None); assert_eq!(uri.parent(), None); assert!(uri.starts_with(&uri)); assert!(!uri.starts_with(&root)); assert!(!uri.starts_with(&other)); assert!(!other.starts_with(&uri)); assert_eq!(uri.join(""), Ok(uri.clone())); assert_eq!( uri.join("child"), Err(PathUriParseError::InvalidFileUriPath { path: uri.to_string(), }) ); } #[test] fn file_uri_parses_a_posix_path_on_any_host() { let uri = PathUri::parse("file:///home/alice/src/main.rs") .expect("POSIX file URI should parse on every host"); assert_eq!(uri.encoded_path(), "/home/alice/src/main.rs"); assert_eq!(uri.basename(), Some("main.rs".to_string())); assert_eq!(uri.to_string(), "file:///home/alice/src/main.rs"); } #[test] fn file_uri_preserves_paths_that_resemble_windows_paths() { for (input, expected_path) in [("file:///C:/Project", "/C:/Project"), ("file:///C:", "/C:")] { let uri = PathUri::parse(input).expect("file URI should parse"); let reparsed = PathUri::parse(&uri.to_string()).expect("file URI should reparse"); assert_eq!(uri.encoded_path(), expected_path); assert_eq!(reparsed, uri); } } #[test] #[cfg(unix)] fn file_uri_accepts_non_utf8_posix_paths() { let path = PathBuf::from(std::ffi::OsString::from_vec(b"/tmp/non-utf8-\xff".to_vec())); let path = AbsolutePathBuf::from_absolute_path_checked(path).expect("absolute POSIX path"); let uri = PathUri::from_abs_path(&path); assert_eq!( uri.to_abs_path() .expect("URI should convert to native path"), path ); assert_eq!( PathUri::parse(&uri.to_string()).expect("non-UTF-8 URI should reparse"), uri ); } #[test] fn file_uri_round_trips_literal_percent_characters() { let uri = PathUri::parse("file:///tmp/100%25/file").expect("file URI should parse"); assert_eq!(uri.to_string(), "file:///tmp/100%25/file"); assert_eq!(uri.encoded_path(), "/tmp/100%25/file"); assert_eq!(uri.basename(), Some("file".to_string())); } #[test] #[cfg(windows)] fn file_uri_round_trips_windows_unc_paths() { let path = AbsolutePathBuf::from_absolute_path_checked(r"\\server\share\src\main.rs") .expect("absolute UNC path"); let uri = PathUri::from_abs_path(&path); assert_eq!(uri.encoded_path(), "/share/src/main.rs"); assert_eq!(uri.to_abs_path().expect("UNC URI should convert"), path); let localhost = AbsolutePathBuf::from_absolute_path_checked(r"\\localhost\share\src") .expect("absolute localhost UNC path"); let uri = PathUri::from_abs_path(&localhost); assert!(uri.to_string().starts_with(BAD_PATH_URI_PREFIX)); assert_eq!( uri.to_abs_path().expect("opaque URI should convert"), localhost ); } #[test] fn file_uri_retains_unc_authority() { let uri = PathUri::parse("file://server/share/src/main.rs").expect("valid file URI"); assert_eq!(uri.encoded_path(), "/share/src/main.rs"); assert_eq!(uri.to_string(), "file://server/share/src/main.rs"); } #[test] fn file_uri_spelling_aliases_have_one_canonical_form() { for input in [ "FILE:///workspace/src", "file:/workspace/src", "file://localhost/workspace/src", "file://LOCALHOST/workspace/src", ] { let uri = PathUri::parse(input).expect("file URI alias should parse"); assert_eq!(uri.to_string(), "file:///workspace/src", "parsing {input}"); } } #[test] fn unsupported_schemes_are_rejected_at_construction() { for (input, expected_scheme) in [ ("codex-env:///devbox/workspace", "codex-env"), ("artifact://store/object-1", "artifact"), ("http://example.com/file", "http"), ("https://example.com/file", "https"), ("ssh://host/workspace", "ssh"), ("vscode-remote://ssh-remote+host/workspace", "vscode-remote"), ("untitled:Untitled-1", "untitled"), ] { let error = PathUri::parse(input).expect_err("unsupported schemes should be rejected"); assert!( matches!( error, PathUriParseError::UnsupportedScheme(scheme) if scheme == expected_scheme ), "parsing {input}" ); } } #[test] fn path_uri_serializes_as_a_string() { let uri: PathUri = "file:///workspace/src/lib.rs" .parse() .expect("valid file URI"); let json = serde_json::to_string(&uri).expect("URI should serialize"); let deserialized: PathUri = serde_json::from_str(&json).expect("URI should deserialize"); assert_eq!(json, r#""file:///workspace/src/lib.rs""#); assert_eq!(deserialized, uri); } #[test] fn path_uri_rejects_native_absolute_paths_during_deserialization() { let path = AbsolutePathBuf::current_dir() .expect("current directory") .join("workspace/src"); let json = serde_json::to_string(&path).expect("absolute path should serialize"); serde_json::from_str::(&json) .expect_err("native absolute path should not deserialize as a URI"); } #[test] fn path_uri_rejects_relative_native_paths() { let error = PathUri::from_host_native_path("src/lib.rs").expect_err("relative path should be rejected"); assert_eq!(error.kind(), io::ErrorKind::InvalidInput); } #[test] fn path_uri_rejects_relative_strings_during_deserialization() { let error = serde_json::from_str::(r#""src/lib.rs""#) .expect_err("relative path should be rejected"); assert!(error.to_string().contains("relative URL without a base")); } #[test] fn unsupported_scheme_is_rejected_during_deserialization() { let error = serde_json::from_str::(r#""artifact://store/object-1""#) .expect_err("unsupported scheme should fail deserialization"); assert!( error .to_string() .contains("unsupported path URI scheme `artifact`") ); } #[test] fn known_path_uris_reject_queries_and_fragments() { let query_error = PathUri::parse("file:///tmp/file.rs?version=1").expect_err("query should be rejected"); let fragment_error = PathUri::parse("file:///tmp/file.rs#L1").expect_err("fragment should be rejected"); assert!(matches!(query_error, PathUriParseError::QueryNotAllowed)); assert!(matches!( fragment_error, PathUriParseError::FragmentNotAllowed )); } #[test] fn path_uris_reject_encoded_null_bytes() { assert!(PathUri::parse("file:///tmp/%00").is_err()); } #[test] fn encoded_filename_characters_round_trip_without_becoming_uri_metadata() { let uri = PathUri::parse("file:///tmp/a%3Fb%23c%25d") .expect("encoded filename characters should parse"); assert_eq!(uri.to_string(), "file:///tmp/a%3Fb%23c%25d"); assert_eq!(uri.encoded_path(), "/tmp/a%3Fb%23c%25d"); assert_eq!(uri.basename(), Some("a?b#c%d".to_string())); } #[test] fn double_encoded_separator_remains_filename_text() { let uri = PathUri::parse("file:///tmp/a%252Fb") .expect("double-encoded separator should parse as filename text"); assert_eq!(uri.to_string(), "file:///tmp/a%252Fb"); assert_eq!(uri.encoded_path(), "/tmp/a%252Fb"); assert_eq!(uri.basename(), Some("a%2Fb".to_string())); } #[test] fn basename_uses_decoded_uri_segments() { for (input, expected) in [ ("file:///", None), ("file:///workspace/src/lib.rs", Some("lib.rs")), ("file:///workspace/a%20file.rs", Some("a file.rs")), ("file:///C:/", Some("C:")), ("file://server/share", Some("share")), ] { let uri = PathUri::parse(input).expect("valid file URI"); assert_eq!( uri.basename(), expected.map(str::to_string), "basename for {input}" ); } } #[test] fn path_buf_uses_the_inferred_native_spelling() { let windows = PathUri::parse("file:///C:/Program%20Files/pwsh.exe").expect("Windows URI"); let posix = PathUri::parse("file:///usr/local/bin/bash").expect("POSIX URI"); assert_eq!( (windows.to_path_buf(), posix.to_path_buf()), ( PathBuf::from(r"C:\Program Files\pwsh.exe"), PathBuf::from("/usr/local/bin/bash"), ) ); } #[test] fn parent_stops_at_posix_drive_and_unc_roots() { for (input, expected) in [ ( "file:///workspace/src/lib.rs", Some("file:///workspace/src"), ), ("file:///workspace", Some("file:///")), ("file:///", None), ("file:///C:/Users", Some("file:///C:")), ("file:///C:/", None), ("file:///C:", None), ( "file://server/share/src/main.rs", Some("file://server/share/src"), ), ("file://server/share", None), ] { let uri = PathUri::parse(input).expect("valid file URI"); let expected = expected.map(|value| PathUri::parse(value).expect("valid expected URI")); assert_eq!(uri.parent(), expected, "parent for {input}"); } } #[test] fn ancestors_include_self_and_stop_at_native_path_roots() { for (input, expected) in [ ( "file:///workspace/src", vec!["file:///workspace/src", "file:///workspace", "file:///"], ), ( "file:///C:/workspace/src", vec![ "file:///C:/workspace/src", "file:///C:/workspace", "file:///C:", ], ), ( "file://server/share/project", vec!["file://server/share/project", "file://server/share"], ), ] { let uri = PathUri::parse(input).expect("valid file URI"); let ancestors = uri .ancestors() .map(|path| path.to_string()) .collect::>(); assert_eq!(ancestors, expected, "ancestors for {input}"); } } #[test] fn join_normalizes_relative_uri_segments() { for (base, relative, expected) in [ ( "file:///workspace/src", "../tests/test.rs", "file:///workspace/tests/test.rs", ), ("file:///", "../../etc", "file:///etc"), ("file:///C:/Users", "../Windows", "file:///C:/Windows"), ( "file://server/share/src", "../tests", "file://server/share/tests", ), ( "file:///workspace", "a?b#c%d", "file:///workspace/a%3Fb%23c%25d", ), ("file:///workspace/", "", "file:///workspace/"), ] { let base = PathUri::parse(base).expect("valid base URI"); let expected = PathUri::parse(expected).expect("valid expected URI"); assert_eq!(base.join(relative), Ok(expected), "joining {relative}"); } } #[test] fn join_descendant_uses_the_base_path_convention() { for (base, relative, expected) in [ ( "file:///workspace", "docs/../public", "file:///workspace/public", ), ( "file:///C:/workspace", r"docs\..\public", "file:///C:/workspace/public", ), ( "file://server/share/workspace", r"docs\..\public", "file://server/share/workspace/public", ), ] { let base = PathUri::parse(base).expect("valid base URI"); let expected = PathUri::parse(expected).expect("valid expected URI"); assert_eq!( base.join_descendant(relative), Ok(expected), "joining {relative}" ); } } #[test] fn join_descendant_rejects_non_descendant_paths() { for (base, path) in [ ("file:///workspace", "/workspace/docs"), ("file:///workspace", "../outside"), ("file:///C:/workspace", r"\workspace\docs"), ("file:///C:/workspace", r"C:\workspace\docs"), ("file:///C:/workspace", r"C:docs"), ("file:///C:/workspace", r"docs\file:stream"), ("file://server/share/workspace", r"..\outside"), ] { let base = PathUri::parse(base).expect("valid base URI"); assert_eq!( base.join_descendant(path), Err(PathUriParseError::JoinPathMustBeDescendant( path.to_string() )), "joining {path}" ); } } #[test] fn join_replaces_posix_absolute_path() { let base = PathUri::parse("file:///workspace").expect("valid base URI"); assert_eq!( base.join("/src"), Ok(PathUri::parse("file:///src").expect("valid absolute URI")) ); } #[test] fn join_keeps_canonicalized_posix_double_slash_paths_hierarchical() { let base = PathUri::parse("file:///workspace").expect("valid base URI"); let cwd = base .join("//server/share/project") .expect("valid absolute path"); assert_eq!( cwd, PathUri::parse("file:///server/share/project").expect("valid canonical URI") ); assert_eq!( cwd.parent(), Some(PathUri::parse("file:///server/share").expect("valid parent URI")) ); assert_eq!( cwd.join("AGENTS.md"), Ok(PathUri::parse("file:///server/share/project/AGENTS.md").expect("valid child URI")) ); #[cfg(unix)] assert_eq!( cwd.to_abs_path() .expect("cwd should convert to a native path"), AbsolutePathBuf::try_from("/server/share/project") .expect("expected native path should be absolute") ); } #[test] fn join_normalizes_absolute_parent_segments() { for (base, path, expected) in [ ("file:///workspace", "/tmp/a/../b", "file:///tmp/b"), ("file:///C:/workspace", r"D:\tmp\a\..\b", "file:///D:/tmp/b"), ( "file:///C:/workspace", r"\\server\share\a\..\b", "file://server/share/b", ), ( "file:///C:/workspace", r"\\?\D:\reports\report.pdf", "file:///D:/reports/report.pdf", ), ( "file:///C:/workspace", r"\\.\D:\reports\report.pdf", "file:///D:/reports/report.pdf", ), ( "file:///C:/workspace", r"\\?\UNC\server\share\reports\report.pdf", "file://server/share/reports/report.pdf", ), ( "file:///C:/workspace", r"\\.\UNC\server\share\reports\report.pdf", "file://server/share/reports/report.pdf", ), ("file:///workspace", "/tmp//a/../b", "file:///tmp/b"), ("file:///workspace", "/tmp/a/..//b", "file:///tmp/b"), ("file:///workspace", "/tmp/a///../b", "file:///tmp/b"), ( "file:///C:/workspace", r"D:\tmp\a\\\..\b", "file:///D:/tmp/b", ), ( "file:///C:/workspace", r"\\server\share\a\\\..\b", "file://server/share/b", ), ("file:///workspace", "/tmp/a///b/../..", "file:///tmp"), ( "file:///C:/workspace", r"D:\tmp\a\\\b\..\..", "file:///D:/tmp", ), ( "file:///C:/workspace", r"\\server\share\a\\\b\..\..", "file://server/share", ), ] { let base = PathUri::parse(base).expect("valid base URI"); let expected = PathUri::parse(expected).expect("valid expected URI"); if normalize_windows_device_path(path).is_some() { assert_eq!( LegacyAppPathString::from_string(path).to_path_uri(PathConvention::Windows), Ok(expected.clone()), "converting {path}" ); } assert_eq!(base.join(path), Ok(expected), "joining {path}"); } } #[test] fn windows_namespace_normalization_preserves_opaque_paths() { let base = PathUri::parse("file:///C:/workspace").expect("valid Windows base URI"); for path in [ r"\\?\UNC\server", r"\\.\UNC\server", r"\\?\UNC\localhost\share\report.pdf", r"\\.\UNC\LOCALHOST\share\report.pdf", r"\\?\UNC\.\share\report.pdf", r"\\.\UNC\..\share\report.pdf", r"\\?\UNC\server\.\report.pdf", r"\\.\UNC\server\..\report.pdf", r"\\?\UNC\?\UNC\?\C:\report.pdf", r"\\.\UNC\?\UNC\?\C:\report.pdf", r"\\.\COM1", r"\\?\Volume{00000000-0000-0000-0000-000000000000}\report.pdf", ] { let expected = windows_opaque_path_uri(path); assert_eq!( PathUri::from_absolute_native_path(path, PathConvention::Windows), Some(expected.clone()), "parsing {path}" ); assert_eq!( LegacyAppPathString::from_string(path).to_path_uri(PathConvention::Windows), Ok(expected.clone()), "converting {path}" ); assert_eq!(base.join(path), Ok(expected), "joining {path}"); } } #[test] fn join_absolute_parent_segments_stop_at_native_path_roots() { for (base, path, expected) in [ ("file:///workspace", "/a/..", "file:///"), ("file:///C:/workspace", r"D:\a\..", "file:///D:/"), ( "file:///C:/workspace", r"\\server\share\a\..", "file://server/share", ), ("file:///workspace", "/../../b", "file:///b"), ("file:///C:/workspace", r"D:\..\..\b", "file:///D:/b"), ( "file:///C:/workspace", r"\\server\share\..\..\b", "file://server/share/b", ), ( "file:///C:/workspace", r"\\server\share\\\..\b", "file://server/share/b", ), ] { let base = PathUri::parse(base).expect("valid base URI"); let expected = PathUri::parse(expected).expect("valid expected URI"); assert_eq!(base.join(path), Ok(expected), "joining {path}"); } } #[test] fn join_collapses_redundant_absolute_separators() { for (base, path, expected) in [ ("file:///workspace", "/tmp///", "file:///tmp/"), ("file:///workspace", "///", "file:///"), ( "file:///workspace", "///server/share///", "file:///server/share/", ), ("file:///C:/workspace", r"D:\tmp\\\", "file:///D:/tmp/"), ("file:///C:/workspace", r"D:\\\", "file:///D:/"), ( "file:///C:/workspace", r"\\server\share\tmp\\\", "file://server/share/tmp/", ), ( "file:///C:/workspace", r"\\server\share\\\", "file://server/share/", ), ] { let base = PathUri::parse(base).expect("valid base URI"); let expected = PathUri::parse(expected).expect("valid expected URI"); assert_eq!(base.join(path), Ok(expected), "joining {path}"); } } #[test] fn join_replaces_windows_absolute_path() { let base = PathUri::parse("file:///C:/workspace/src").expect("valid base URI"); assert_eq!( base.join(r"D:\tmp\test.rs"), Ok(PathUri::parse("file:///D:/tmp/test.rs").expect("valid absolute URI")) ); } #[test] fn join_windows_root_relative_path_preserves_drive_or_share() { for (base, path, expected) in [ ("file:///C:/base/dir", r"\Windows", "file:///C:/Windows"), ( "file://server/share/base/dir", r"\Windows", "file://server/share/Windows", ), ] { let base = PathUri::parse(base).expect("valid base URI"); let expected = PathUri::parse(expected).expect("valid expected URI"); assert_eq!(base.join(path), Ok(expected), "joining {path}"); } } #[test] fn join_resolves_windows_same_drive_relative_path() { for (base, path, expected) in [ ("file:///C:/base", r"C:tmp", "file:///C:/base/tmp"), ("file:///C:/base", r"c:tmp", "file:///C:/base/tmp"), ("file:///C%3A/base", r"C:tmp", "file:///C%3A/base/tmp"), ("file:///C%3a/base", r"c:tmp", "file:///C%3a/base/tmp"), ("file:///C:/base/dir", r"C:..\tmp", "file:///C:/base/tmp"), ("file:///C:/base", "C:", "file:///C:/base"), ] { let base = PathUri::parse(base).expect("valid base URI"); let expected = PathUri::parse(expected).expect("valid expected URI"); assert_eq!(base.join(path), Ok(expected), "joining {path}"); } } #[test] fn join_rejects_windows_other_drive_relative_path() { let base = PathUri::parse("file:///C:/base").expect("valid base URI"); assert_eq!( base.join(r"D:tmp"), Err(PathUriParseError::InvalidFileUriPath { path: r"D:tmp".to_string(), }) ); } #[test] fn join_parent_segments_preserve_windows_drive_or_share_anchor() { for (base, expected) in [ ("file:///C:/base/dir", "file:///C:/Windows"), ( "file://server/share/base/dir", "file://server/share/Windows", ), ] { let base = PathUri::parse(base).expect("valid base URI"); let expected = PathUri::parse(expected).expect("valid expected URI"); assert_eq!(base.join(r"..\..\..\Windows"), Ok(expected)); } } #[test] fn join_rejects_null_paths() { let base = PathUri::parse("file:///workspace").expect("valid base URI"); assert_eq!( base.join("src\0file"), Err(PathUriParseError::InvalidFileUriPath { path: "src\0file".to_string(), }) ); } #[test] fn join_uses_the_base_uri_path_convention() { for (base, path, expected) in [ ( "file:///workspace/src", "../tests/test.rs", "file:///workspace/tests/test.rs", ), ( "file:///C:/workspace/src", r"..\tests\test.rs", "file:///C:/workspace/tests/test.rs", ), ] { let base = PathUri::parse(base).expect("valid base URI"); let expected = PathUri::parse(expected).expect("valid expected URI"); assert_eq!(base.join(path), Ok(expected), "joining {path}"); } } #[test] fn starts_with_uses_uri_segment_boundaries() { for (path, base, expected) in [ ("file:///workspace/plugin", "file:///", true), ("file:///workspace/plugin", "file:///workspace/plugin", true), ( "file:///workspace/plugin/assets/icon.svg", "file:///workspace/plugin", true, ), ( "file:///workspace/plugin-other/icon.svg", "file:///workspace/plugin", false, ), ( "file:///C:/plugins/foo/assets/icon.svg", "file:///C:/plugins/foo", true, ), ("file:///C:/project/secret", "file:///%63%3A/project", false), ( "file:///C:/PLUGINS/%C3%84/assets/icon.svg", "file:///c:/plugins/%C3%A4", false, ), ( "file:///C:/plugins/Lj/assets/icon.svg", "file:///C:/plugins/LJ", false, ), ( "file:///C:/plugins/foo2/assets/icon.svg", "file:///C:/plugins/foo", false, ), ( "file://server/share/plugins/foo/icon.svg", "file://server/share/plugins/foo", true, ), ( "file://SERVER/SHARE/PLUGINS/FOO/icon.svg", "file://server/share/plugins/foo", true, ), ("file:///WORKSPACE/plugin", "file:///workspace", false), ( "file://other/share/plugins/foo/icon.svg", "file://server/share/plugins/foo", false, ), ( "file:///workspace/plugin/%2F..%2Foutside", "file:///workspace/plugin", false, ), ( "file:///workspace/pri%76ate/file", "file:///workspace/%70rivate", true, ), ("file:///workspace/%ff/file", "file:///workspace/%FF", true), ( "file:///workspace/plugin/%5C..%5Coutside", "file:///workspace/plugin", true, ), ( "file:///C:/plugins/foo/%5C..%5Coutside", "file:///C:/plugins/foo", false, ), ] { let path = PathUri::parse(path).expect("valid path URI"); let base = PathUri::parse(base).expect("valid base URI"); assert_eq!(path.starts_with(&base), expected); } } #[test] fn overlaps_uses_lexical_containment() { for (left, right, expected) in [ ("file:///workspace", "file:///workspace/src", Some(true)), ( "file:///C:/WORKSPACE", "file:///c:/workspace/src", Some(true), ), ( "file:///workspace/src", "file:///workspace/tests", Some(false), ), ("file:///WORKSPACE", "file:///workspace/src", Some(false)), ] { let left = PathUri::parse(left).expect("valid left URI"); let right = PathUri::parse(right).expect("valid right URI"); assert_eq!(left.overlaps(&right), expected, "{left} and {right}"); assert_eq!(right.overlaps(&left), expected, "{right} and {left}"); } let opaque = PathUri::from_opaque_path_bytes(b"/workspace/private"); let lexical = PathUri::parse("file:///workspace").expect("valid lexical URI"); assert_eq!(opaque.overlaps(&opaque), Some(true)); assert_eq!(opaque.overlaps(&lexical), None); assert_eq!(lexical.overlaps(&opaque), None); } #[test] fn lexical_depth_counts_validated_nonempty_segments() { for (path, expected) in [ ("file:///", Some(0)), ("file:///workspace////", Some(1)), ("file:///workspace/%70rivate", Some(2)), ("file:///workspace/private%2Fsecret", None), ] { let path = PathUri::parse(path).expect("valid path URI"); assert_eq!(path.lexical_depth(), expected, "lexical depth for {path}"); } assert_eq!( PathUri::from_opaque_path_bytes(b"/workspace").lexical_depth(), None ); } #[test] fn to_url_returns_the_validated_url() { let uri = PathUri::parse("file://localhost/workspace/a%20file.rs").expect("valid file URI"); assert_eq!( uri.to_url(), Url::parse("file:///workspace/a%20file.rs").expect("valid URL") ); }