windows: Add support for SSH (#29145)

Closes #19892

This PR builds on top of #20587 and improves upon it.

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
张小白 2025-07-08 22:34:57 +08:00 committed by GitHub
parent 8bd739d869
commit 0ca0914cca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1435 additions and 354 deletions

View file

@ -27,3 +27,4 @@ prost-build.workspace = true
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }
typed-path = "0.11"

View file

@ -127,51 +127,46 @@ pub trait ToProto {
fn to_proto(self) -> String;
}
impl FromProto for PathBuf {
#[inline]
fn from_proto_path(proto: String) -> PathBuf {
#[cfg(target_os = "windows")]
fn from_proto(proto: String) -> Self {
proto.split("/").collect()
}
let proto = proto.replace('/', "\\");
PathBuf::from(proto)
}
#[inline]
fn to_proto_path(path: &Path) -> String {
#[cfg(target_os = "windows")]
let proto = path.to_string_lossy().replace('\\', "/");
#[cfg(not(target_os = "windows"))]
let proto = path.to_string_lossy().to_string();
proto
}
impl FromProto for PathBuf {
fn from_proto(proto: String) -> Self {
PathBuf::from(proto)
from_proto_path(proto)
}
}
impl FromProto for Arc<Path> {
fn from_proto(proto: String) -> Self {
PathBuf::from_proto(proto).into()
from_proto_path(proto).into()
}
}
impl ToProto for PathBuf {
#[cfg(target_os = "windows")]
fn to_proto(self) -> String {
self.components()
.map(|comp| comp.as_os_str().to_string_lossy().to_string())
.collect::<Vec<_>>()
.join("/")
}
#[cfg(not(target_os = "windows"))]
fn to_proto(self) -> String {
self.to_string_lossy().to_string()
to_proto_path(&self)
}
}
impl ToProto for &Path {
#[cfg(target_os = "windows")]
fn to_proto(self) -> String {
self.components()
.map(|comp| comp.as_os_str().to_string_lossy().to_string())
.collect::<Vec<_>>()
.join("/")
}
#[cfg(not(target_os = "windows"))]
fn to_proto(self) -> String {
self.to_string_lossy().to_string()
to_proto_path(self)
}
}
@ -214,3 +209,103 @@ impl<T: RequestMessage> TypedEnvelope<T> {
}
}
}
#[cfg(test)]
mod tests {
use typed_path::{UnixPath, UnixPathBuf, WindowsPath, WindowsPathBuf};
fn windows_path_from_proto(proto: String) -> WindowsPathBuf {
let proto = proto.replace('/', "\\");
WindowsPathBuf::from(proto)
}
fn unix_path_from_proto(proto: String) -> UnixPathBuf {
UnixPathBuf::from(proto)
}
fn windows_path_to_proto(path: &WindowsPath) -> String {
path.to_string_lossy().replace('\\', "/")
}
fn unix_path_to_proto(path: &UnixPath) -> String {
path.to_string_lossy().to_string()
}
#[test]
fn test_path_proto_interop() {
const WINDOWS_PATHS: &[&str] = &[
"C:\\Users\\User\\Documents\\file.txt",
"C:/Program Files/App/app.exe",
"projects\\zed\\crates\\proto\\src\\typed_envelope.rs",
"projects/my project/src/main.rs",
];
const UNIX_PATHS: &[&str] = &[
"/home/user/documents/file.txt",
"/usr/local/bin/my app/app",
"projects/zed/crates/proto/src/typed_envelope.rs",
"projects/my project/src/main.rs",
];
// Windows path to proto and back
for &windows_path_str in WINDOWS_PATHS {
let windows_path = WindowsPathBuf::from(windows_path_str);
let proto = windows_path_to_proto(&windows_path);
let recovered_path = windows_path_from_proto(proto);
assert_eq!(windows_path, recovered_path);
assert_eq!(
recovered_path.to_string_lossy(),
windows_path_str.replace('/', "\\")
);
}
// Unix path to proto and back
for &unix_path_str in UNIX_PATHS {
let unix_path = UnixPathBuf::from(unix_path_str);
let proto = unix_path_to_proto(&unix_path);
let recovered_path = unix_path_from_proto(proto);
assert_eq!(unix_path, recovered_path);
assert_eq!(recovered_path.to_string_lossy(), unix_path_str);
}
// Windows host, Unix client, host sends Windows path to client
for &windows_path_str in WINDOWS_PATHS {
let windows_host_path = WindowsPathBuf::from(windows_path_str);
let proto = windows_path_to_proto(&windows_host_path);
let unix_client_received_path = unix_path_from_proto(proto);
let proto = unix_path_to_proto(&unix_client_received_path);
let windows_host_recovered_path = windows_path_from_proto(proto);
assert_eq!(windows_host_path, windows_host_recovered_path);
assert_eq!(
windows_host_recovered_path.to_string_lossy(),
windows_path_str.replace('/', "\\")
);
}
// Unix host, Windows client, host sends Unix path to client
for &unix_path_str in UNIX_PATHS {
let unix_host_path = UnixPathBuf::from(unix_path_str);
let proto = unix_path_to_proto(&unix_host_path);
let windows_client_received_path = windows_path_from_proto(proto);
let proto = windows_path_to_proto(&windows_client_received_path);
let unix_host_recovered_path = unix_path_from_proto(proto);
assert_eq!(unix_host_path, unix_host_recovered_path);
assert_eq!(unix_host_recovered_path.to_string_lossy(), unix_path_str);
}
}
// todo(zjk)
#[test]
fn test_unsolved_case() {
// Unix host, Windows client
// The Windows client receives a Unix path with backslashes in it, then
// sends it back to the host.
// This currently fails.
let unix_path = UnixPathBuf::from("/home/user/projects/my\\project/src/main.rs");
let proto = unix_path_to_proto(&unix_path);
let windows_client_received_path = windows_path_from_proto(proto);
let proto = windows_path_to_proto(&windows_client_received_path);
let unix_host_recovered_path = unix_path_from_proto(proto);
assert_ne!(unix_path, unix_host_recovered_path);
assert_eq!(
unix_host_recovered_path.to_string_lossy(),
"/home/user/projects/my/project/src/main.rs"
);
}
}