From 486e5399ff30e2fe046a9920b326468b1b85a783 Mon Sep 17 00:00:00 2001 From: Chip Senkbeil Date: Thu, 25 Aug 2022 00:32:26 -0500 Subject: [PATCH] Leverage typed-path to determine if path is windows --- Cargo.lock | 7 ++ distant-ssh2/Cargo.toml | 1 + distant-ssh2/src/utils.rs | 210 +++++--------------------------------- 3 files changed, 33 insertions(+), 185 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f81b00..38d17a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/distant-ssh2/Cargo.toml b/distant-ssh2/Cargo.toml index 3c0e217..a856ef9 100644 --- a/distant-ssh2/Cargo.toml +++ b/distant-ssh2/Cargo.toml @@ -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" diff --git a/distant-ssh2/src/utils.rs b/distant-ssh2/src/utils.rs index b6ea836..7c23866 100644 --- a/distant-ssh2/src/utils.rs +++ b/distant-ssh2/src/utils.rs @@ -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) -> io::Result { - // 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 { - // 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(err: E) -> io::Error +where + E: Into>, +{ + 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 { // 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::().components().next() + { + Ok(true) + } else { + Ok(false) + } } -pub fn to_other_error(err: E) -> io::Error -where - E: Into>, -{ - io::Error::new(io::ErrorKind::Other, err) +/// Performs canonicalization of the given path using SFTP +pub async fn canonicalize(sftp: &Sftp, path: impl AsRef) -> io::Result { + sftp.canonicalize(path.as_ref().to_path_buf()) + .compat() + .await + .map(|p| p.into_std_path_buf()) + .map_err(to_other_error) }