diff --git a/src/cargo/util/mod.rs b/src/cargo/util/mod.rs index ccd6d59a4d1..56bfd38da74 100644 --- a/src/cargo/util/mod.rs +++ b/src/cargo/util/mod.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::path::{Path, PathBuf}; use std::time::Duration; pub use self::canonical_url::CanonicalUrl; @@ -133,6 +134,77 @@ pub fn truncate_with_ellipsis(s: &str, max_width: usize) -> String { prefix } +#[cfg(not(windows))] +#[inline] +pub fn try_canonicalize>(path: P) -> std::io::Result { + std::fs::canonicalize(&path) +} + +#[cfg(windows)] +#[inline] +pub fn try_canonicalize>(path: P) -> std::io::Result { + use std::ffi::OsString; + use std::io::Error; + use std::os::windows::ffi::{OsStrExt, OsStringExt}; + use std::{io::ErrorKind, ptr}; + use windows_sys::Win32::Foundation::{GetLastError, SetLastError}; + use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW; + + // On Windows `canonicalize` may fail, so we fall back to getting an absolute path. + std::fs::canonicalize(&path).or_else(|_| { + // Return an error if a file does not exist for better compatiblity with `canonicalize` + if !path.as_ref().try_exists()? { + return Err(Error::new(ErrorKind::NotFound, "the path was not found")); + } + + let path = path.as_ref().as_os_str(); + let mut path_u16 = Vec::with_capacity(path.len() + 1); + path_u16.extend(path.encode_wide()); + if path_u16.iter().find(|c| **c == 0).is_some() { + return Err(Error::new( + ErrorKind::InvalidInput, + "strings passed to WinAPI cannot contain NULs", + )); + } + path_u16.push(0); + + unsafe { + SetLastError(0); + let len = GetFullPathNameW(path_u16.as_ptr(), 0, &mut [] as *mut u16, ptr::null_mut()); + if len == 0 { + let error = GetLastError(); + if error != 0 { + return Err(Error::from_raw_os_error(error as i32)); + } + } + let mut result: Vec = std::iter::repeat(0).take(len as usize).collect(); + + let write_len = GetFullPathNameW( + path_u16.as_ptr(), + result.len().try_into().unwrap(), + result.as_mut_ptr().cast::(), + ptr::null_mut(), + ); + if write_len == 0 { + let error = GetLastError(); + if error != 0 { + return Err(Error::from_raw_os_error(error as i32)); + } + } + assert_eq!( + write_len + 1, + len, + "mismatching requested and written lengths for path {:?}", + path + ); + + Ok(PathBuf::from(OsString::from_wide( + &result[0..(write_len as usize)], + ))) + } + }) +} + #[cfg(test)] mod test { use super::*;