Leverage typed-path to determine if path is windows

pull/137/head
Chip Senkbeil 2 years ago
parent 4011671a77
commit 486e5399ff
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131

7
Cargo.lock generated

@ -833,6 +833,7 @@ dependencies = [
"shell-words",
"smol",
"tokio",
"typed-path",
"wezterm-ssh",
"which",
"whoami",
@ -3042,6 +3043,12 @@ dependencies = [
"once_cell",
]
[[package]]
name = "typed-path"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0303bfe6ef379273be7ce99d8a6a2e54261fd110a01cf70986ec0fe855ff075e"
[[package]]
name = "typenum"
version = "1.15.0"

@ -29,6 +29,7 @@ rpassword = "7.0.0"
shell-words = "1.1.0"
smol = "1.2.5"
tokio = { version = "1.20.1", features = ["full"] }
typed-path = "0.1.0"
wezterm-ssh = { version = "0.4.0", default-features = false }
winsplit = "0.1.0"

@ -1,9 +1,10 @@
use async_compat::CompatExt;
use std::{
fmt, io,
path::{Component, Path, PathBuf, Prefix},
path::{Path, PathBuf},
time::Duration,
};
use typed_path::{windows::WindowsComponent, WindowsEncoding, WindowsPathBuf};
use wezterm_ssh::{ExecResult, Session, Sftp};
#[allow(dead_code)]
@ -87,182 +88,11 @@ pub async fn execute_output(session: &Session, cmd: &str) -> io::Result<ExecOutp
})
}
/// Performs canonicalization of the given path using SFTP with various handling of Windows paths
pub async fn canonicalize(sftp: &Sftp, path: impl AsRef<Path>) -> io::Result<PathBuf> {
// Determine if we are supplying a Windows path
let mut is_windows_path = path
.as_ref()
.components()
.any(|c| matches!(c, Component::Prefix(_)));
// Try to canonicalize original path first
let result = sftp
.canonicalize(path.as_ref().to_path_buf())
.compat()
.await;
// If we don't see the path initially as a Windows path, but we can find a drive letter after
// canonicalization, still treat it as a windows path
//
// NOTE: This is for situations where we are given a relative path like '.' where we cannot
// infer the path is for Windows out of the box
if !is_windows_path {
if let Ok(path) = result.as_ref() {
is_windows_path = drive_letter(path.as_std_path()).is_some();
}
}
// If result is a failure, we want to try again with a unix path in case we were using
// a windows path and sshd had a problem with canonicalizing it
let unix_path = if result.is_err() && is_windows_path {
Some(to_unix_path(path.as_ref()))
} else {
None
};
// 1. If we succeeded on first try, return that path
// a. If the canonicalized path was for a Windows path, sftp may return something odd
// like C:\Users\example -> /c:/Users/example and we need to transform it back
// b. Otherwise, if the input path was a unix path, we return canonicalized as is
// 2. If we failed on first try and have a clear Windows path, try the unix version
// and then convert result back to windows version, return our original error if we fail
// 3. If we failed and there is no valid unix path for a Windows path, return the
// original error
match (result, unix_path) {
(Ok(path), _) if is_windows_path => Ok(to_windows_path(path.as_std_path())),
(Ok(path), _) => Ok(path.into_std_path_buf()),
(Err(x), Some(path)) => Ok(to_windows_path(
&sftp
.canonicalize(path.to_path_buf())
.compat()
.await
.map_err(|_| to_other_error(x))?
.into_std_path_buf(),
)),
(Err(x), None) => Err(to_other_error(x)),
}
}
/// Convert a path into unix-oriented path
///
/// E.g. C:\Users\example\Documents\file.txt -> /c/Users/example/Documents/file.txt
pub fn to_unix_path(path: &Path) -> PathBuf {
let is_windows_path = path.components().any(|c| matches!(c, Component::Prefix(_)));
if !is_windows_path {
return path.to_path_buf();
}
let mut p = PathBuf::new();
for component in path.components() {
match component {
Component::Prefix(x) => match x.kind() {
Prefix::Verbatim(path) => p.push(path),
Prefix::VerbatimUNC(hostname, share) => {
p.push(hostname);
p.push(share);
}
Prefix::VerbatimDisk(letter) => {
p.push(format!("/{}", letter as char));
}
Prefix::DeviceNS(device_name) => p.push(device_name),
Prefix::UNC(hostname, share) => {
p.push(hostname);
p.push(share);
}
Prefix::Disk(letter) => {
p.push(format!("/{}", letter as char));
}
},
// If we have a prefix, then we are dropping it and converting into
// a root and normal component, so we will now skip this root
Component::RootDir => continue,
x => p.push(x),
}
}
p
}
/// Convert a path into windows-oriented path
///
/// E.g. /c/Users/example/Documents/file.txt -> C:\Users\example\Documents\file.txt
pub fn to_windows_path(path: &Path) -> PathBuf {
let is_windows_path = path.components().any(|c| matches!(c, Component::Prefix(_)));
if is_windows_path {
return path.to_path_buf();
}
// See if we have a drive letter at the beginning, otherwise default to C:\
let drive_letter = drive_letter(path);
let mut p = PathBuf::new();
// Start with a drive prefix
p.push(format!("{}:", drive_letter.unwrap_or('C')));
let mut components = path.components();
// If we start with a root portion of the regular path, we want to drop
// it and the drive letter since we've added that separately
if path.has_root() {
p.push(Component::RootDir);
components.next();
if drive_letter.is_some() {
components.next();
}
}
for component in components {
p.push(component);
}
p
}
/// Looks for a drive letter in the given path
pub fn drive_letter(path: &Path) -> Option<char> {
// See if we are a windows path, and if so grab the letter from the components
let maybe_letter = path.components().find_map(|c| match c {
Component::Prefix(x) => match x.kind() {
Prefix::Disk(letter) | Prefix::VerbatimDisk(letter) => Some(letter as char),
_ => None,
},
_ => None,
});
if let Some(letter) = maybe_letter {
return Some(letter);
}
// If there was no drive letter and we are not a root, there is nothing left to find
if !path.has_root() {
return None;
}
// Otherwise, scan just after root for a drive letter
path.components().nth(1).and_then(|c| match c {
Component::Normal(s) => s.to_str().and_then(|s| {
let mut chars = s.chars();
let first = chars.next();
let second = chars.next();
let has_more = chars.next().is_some();
if has_more {
return None;
}
match (first, second) {
(letter, Some(':') | None) => letter,
_ => None,
}
}),
_ => None,
})
pub fn to_other_error<E>(err: E) -> io::Error
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
io::Error::new(io::ErrorKind::Other, err)
}
/// Determines if using windows by checking the canonicalized path of '.'
@ -280,14 +110,24 @@ pub async fn is_windows(sftp: &Sftp) -> io::Result<bool> {
// However, the above is not working for whatever reason (always has success == false); so,
// we're purely using a check if we have a drive letter on the canonicalized path to
// determine if on windows for now. Some sort of failure with SIGPIPE
Ok(current_dir
.components()
.any(|c| matches!(c, Component::Prefix(_))))
let windows_path = WindowsPathBuf::from(current_dir.to_string_lossy().to_string());
let mut components = windows_path.components();
if let Some(WindowsComponent::Prefix(_)) = components.next() {
Ok(true)
} else if let Some(WindowsComponent::Prefix(_)) =
components.as_path::<WindowsEncoding>().components().next()
{
Ok(true)
} else {
Ok(false)
}
}
pub fn to_other_error<E>(err: E) -> io::Error
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
io::Error::new(io::ErrorKind::Other, err)
/// Performs canonicalization of the given path using SFTP
pub async fn canonicalize(sftp: &Sftp, path: impl AsRef<Path>) -> io::Result<PathBuf> {
sftp.canonicalize(path.as_ref().to_path_buf())
.compat()
.await
.map(|p| p.into_std_path_buf())
.map_err(to_other_error)
}

Loading…
Cancel
Save