-
Notifications
You must be signed in to change notification settings - Fork 9
fix(core): encode reserved path chars in file URIs #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -212,19 +212,46 @@ impl DocumentTracker { | |
| /// not occur for valid absolute paths. | ||
| #[must_use] | ||
| pub fn path_to_uri(path: &Path) -> Uri { | ||
| let uri_string = if cfg!(windows) { | ||
| let path_str = path.to_string_lossy(); | ||
| // canonicalize() on Windows adds a \\?\ extended-path prefix. | ||
| // Strip it before building the URI — file:////?\C:/ is not valid. | ||
| let stripped = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str); | ||
| format!("file:///{}", stripped.replace('\\', "/")) | ||
| } else { | ||
| format!("file://{}", path.display()) | ||
| }; | ||
| let uri_string = file_uri_string(path); | ||
| let uri_string = encode_rfc3986_path_chars(&uri_string); | ||
| #[allow(clippy::expect_used)] | ||
| uri_string.parse().expect("failed to create URI from path") | ||
| } | ||
|
|
||
| #[cfg(not(windows))] | ||
| fn file_uri_string(path: &Path) -> String { | ||
| #[allow(clippy::expect_used)] | ||
| let file_url = Url::from_file_path(path).expect("failed to create file URI from path"); | ||
| file_url.into() | ||
| } | ||
|
|
||
| #[cfg(windows)] | ||
| fn file_uri_string(path: &Path) -> String { | ||
| match Url::from_file_path(path) { | ||
| Ok(file_url) => file_url.into(), | ||
| Err(()) if path.has_root() => windows_rooted_path_to_file_uri(path), | ||
| Err(()) => panic!("failed to create file URI from path"), | ||
| } | ||
| } | ||
|
|
||
| #[cfg(windows)] | ||
| fn windows_rooted_path_to_file_uri(path: &Path) -> String { | ||
| let path_str = path.to_string_lossy(); | ||
| let stripped = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str); | ||
| format!("file:///{}", stripped.replace('\\', "/")) | ||
| } | ||
|
|
||
| fn encode_rfc3986_path_chars(uri: &str) -> String { | ||
| let path_start = url::Position::BeforePath as usize; | ||
| let (prefix, path) = uri.split_at(path_start); | ||
| let encoded_path = path | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [M2 — minor] |
||
| .replace('[', "%5B") | ||
| .replace(']', "%5D") | ||
| .replace('^', "%5E") | ||
| .replace('|', "%7C"); | ||
| format!("{prefix}{encoded_path}") | ||
| } | ||
|
|
||
| /// Convert an LSP `file://` URI to an absolute filesystem path. | ||
| /// | ||
| /// Returns `None` if the URI is not a valid `file://` URI, uses a non-file | ||
|
|
@@ -636,6 +663,28 @@ mod tests { | |
| assert!(uri.as_str().contains("project-test")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_path_to_uri_percent_encodes_reserved_chars() { | ||
| #[cfg(windows)] | ||
| let path = Path::new(r"C:\home\user\routes\api\[...]^|.ts"); | ||
| #[cfg(not(windows))] | ||
| let path = Path::new("/home/user/routes/api/[...]^|.ts"); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [M1 — minor] Only a long path is tested; the let path = Path::new("/[a].ts");
let uri = path_to_uri(path);
assert!(uri.as_str().ends_with("%5Ba%5D.ts")); |
||
|
|
||
| let uri = path_to_uri(path); | ||
|
|
||
| #[cfg(windows)] | ||
| let expected = "file:///C:/home/user/routes/api/%5B...%5D%5E%7C.ts"; | ||
| #[cfg(not(windows))] | ||
| let expected = "file:///home/user/routes/api/%5B...%5D%5E%7C.ts"; | ||
|
|
||
| assert_eq!(uri.as_str(), expected); | ||
| assert_eq!( | ||
| uri_to_path(&uri).as_deref(), | ||
| Some(path), | ||
| "encoded file URI should round-trip to the original path" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_document_tracker_concurrent_operations() { | ||
| let mut map = HashMap::new(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[C1 — critical]
url::Position::BeforePathis an enum discriminant (10), not a byte offset in the URI string. Forfile:///[a].tsthe[sits at byte 8 — it falls in the "prefix" slice and is never encoded. The stated motivation for this fix (Next.js[slug]files) silently fails.Use the
Indextrait that theurlcrate actually provides for this: