mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
## Why Filesystem policies can describe paths using a convention that differs from the host running Codex. Native path comparisons can therefore mis-handle cases such as case-variant Windows paths or ambiguous encoded components. ## What changed - Resolve policy entries and special roots as `PathUri` values using the executor's path convention. - Use validated URI components for containment, overlap, and precedence, and fail closed when component boundaries are ambiguous. - Restrict relative joins to descendants and apply the same matching rules to protected metadata paths and permission-profile intersections. ## Testing Added coverage for Windows case variants, encoded and opaque paths, repeated separators, descendant joins, special roots, metadata protection, and preserved deny entries. GitOrigin-RevId: fb09d44d11df25faaac806fe00457e6f6b0d8596
1291 lines
41 KiB
Rust
1291 lines
41 KiB
Rust
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::<Vec<_>>(), 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::<Vec<_>>();
|
|
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::<PathUri>(&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::<PathUri>(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::<PathUri>(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::<Vec<_>>();
|
|
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")
|
|
);
|
|
}
|