Better absolute path handling (#19727)

Closes #19866

This PR supersedes #19228, as #19228 encountered too many merge
conflicts.

After some exploration, I found that for paths with the `\\?\` prefix,
we can safely remove it and consistently use the clean paths in all
cases. Previously, in #19228, I thought we would still need the `\\?\`
prefix for IO operations to handle long paths better. However, this
turns out to be unnecessary because Rust automatically manages this for
us when calling IO-related APIs. For details, refer to Rust's internal
function
[`get_long_path`](017ae1b21f/library/std/src/sys/path/windows.rs (L225-L233)).

Therefore, we can always store and use paths without the `\\?\` prefix.

This PR introduces a `SanitizedPath` structure, which represents a path
stripped of the `\\?\` prefix. To prevent untrimmed paths from being
mistakenly passed into `Worktree`, the type of `Worktree`’s `abs_path`
member variable has been changed to `SanitizedPath`.

Additionally, this PR reverts the changes of #15856 and #18726. After
testing, it appears that the issues those PRs addressed can be resolved
by this PR.

### Existing Issue
To keep the scope of modifications manageable, `Worktree::abs_path` has
retained its current signature as `fn abs_path(&self) -> Arc<Path>`,
rather than returning a `SanitizedPath`. Updating the method to return
`SanitizedPath`—which may better resolve path inconsistencies—would
likely introduce extensive changes similar to those in #19228.

Currently, the limitation is as follows:

```rust
let abs_path: &Arc<Path> = snapshot.abs_path();
let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project"); 
// The caller performs some actions here:
some_non_trimmed_path.strip_prefix(abs_path);  // This fails
some_non_trimmed_path.starts_with(abs_path);   // This fails too
```

The final two lines will fail because `snapshot.abs_path()` returns a
clean path without the `\\?\` prefix. I have identified two relevant
instances that may face this issue:
-
[lsp_store.rs#L3578](0173479d18/crates/project/src/lsp_store.rs (L3578))
-
[worktree.rs#L4338](0173479d18/crates/worktree/src/worktree.rs (L4338))

Switching `Worktree::abs_path` to return `SanitizedPath` would resolve
these issues but would also lead to many code changes.

Any suggestions or feedback on this approach are very welcome.

cc @SomeoneToIgnore 

Release Notes:

- N/A
This commit is contained in:
张小白 2024-11-28 02:22:58 +08:00 committed by GitHub
parent d0bafce86b
commit cff9ae0bbc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 189 additions and 113 deletions

View file

@ -1,5 +1,5 @@
use std::cmp;
use std::sync::OnceLock;
use std::sync::{Arc, OnceLock};
use std::{
ffi::OsStr,
path::{Path, PathBuf},
@ -95,6 +95,46 @@ impl<T: AsRef<Path>> PathExt for T {
}
}
/// Due to the issue of UNC paths on Windows, which can cause bugs in various parts of Zed, introducing this `SanitizedPath`
/// leverages Rust's type system to ensure that all paths entering Zed are always "sanitized" by removing the `\\\\?\\` prefix.
/// On non-Windows operating systems, this struct is effectively a no-op.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SanitizedPath(Arc<Path>);
impl SanitizedPath {
pub fn starts_with(&self, prefix: &SanitizedPath) -> bool {
self.0.starts_with(&prefix.0)
}
pub fn as_path(&self) -> &Arc<Path> {
&self.0
}
pub fn to_string(&self) -> String {
self.0.to_string_lossy().to_string()
}
}
impl From<SanitizedPath> for Arc<Path> {
fn from(sanitized_path: SanitizedPath) -> Self {
sanitized_path.0
}
}
impl<T: AsRef<Path>> From<T> for SanitizedPath {
#[cfg(not(target_os = "windows"))]
fn from(path: T) -> Self {
let path = path.as_ref();
SanitizedPath(path.into())
}
#[cfg(target_os = "windows")]
fn from(path: T) -> Self {
let path = path.as_ref();
SanitizedPath(dunce::simplified(path).into())
}
}
/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
@ -805,4 +845,22 @@ mod tests {
"Path matcher should match {path:?}"
);
}
#[test]
#[cfg(target_os = "windows")]
fn test_sanitized_path() {
let path = Path::new("C:\\Users\\someone\\test_file.rs");
let sanitized_path = SanitizedPath::from(path);
assert_eq!(
sanitized_path.to_string(),
"C:\\Users\\someone\\test_file.rs"
);
let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs");
let sanitized_path = SanitizedPath::from(path);
assert_eq!(
sanitized_path.to_string(),
"C:\\Users\\someone\\test_file.rs"
);
}
}