blob: 98e81dcdd2f777140fc12d04af30e3edac714175 [file] [log] [blame]
use std::borrow::Cow;
use std::env;
use std::ffi::OsString;
use std::io;
use std::mem;
use std::os::windows::ffi::OsStrExt;
use std::os::windows::ffi::OsStringExt;
use std::path::Component;
use std::path::Path;
use std::path::Prefix;
use std::path::PrefixComponent;
use std::ptr;
use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
use crate::BasePath;
use crate::BasePathBuf;
macro_rules! static_assert {
( $condition:expr ) => {
const _: () = assert!($condition, "static assertion failed");
};
}
const SEPARATOR: u8 = b'\\';
pub(crate) fn is_base(path: &Path) -> bool {
matches!(path.components().next(), Some(Component::Prefix(_)))
}
pub(crate) fn to_base(path: &Path) -> io::Result<BasePathBuf> {
let base = env::current_dir()?;
debug_assert!(is_base(&base));
let mut base = BasePathBuf(base);
base.push(path);
Ok(base)
}
fn convert_separators(path: &Path, length: Option<usize>) -> Cow<'_, Path> {
let path_bytes = path.as_os_str().as_encoded_bytes();
let (prefix, suffix) = length
.map(|x| path_bytes.split_at(x))
.unwrap_or((path_bytes, &[]));
let mut parts = prefix.split(|&x| x == b'/');
let part = parts.next().expect("split iterator is empty");
if part.len() == prefix.len() {
debug_assert_eq!(prefix, part);
debug_assert_eq!(None, parts.next());
return Cow::Borrowed(path);
}
let mut path_bytes = Vec::with_capacity(path_bytes.len());
path_bytes.extend(part);
for part in parts {
path_bytes.push(SEPARATOR);
path_bytes.extend(part);
}
path_bytes.extend(suffix);
// SAFETY: Only UTF-8 substrings were replaced.
Cow::Owned(
unsafe { OsString::from_encoded_bytes_unchecked(path_bytes) }.into(),
)
}
fn normalize_verbatim(path: &Path) -> Cow<'_, BasePath> {
// Normalizing more of a verbatim path can change its meaning. The part
// changed here is required for the path to be verbatim.
let path = convert_separators(path, Some(4));
debug_assert!(matches!(
path.components().next(),
Some(Component::Prefix(prefix)) if prefix.kind().is_verbatim(),
));
match path {
Cow::Borrowed(path) => {
Cow::Borrowed(BasePath::from_inner(path.as_os_str()))
}
Cow::Owned(path) => Cow::Owned(BasePathBuf(path)),
}
}
pub(crate) fn normalize_virtually(
initial_path: &Path,
) -> io::Result<BasePathBuf> {
// [GetFullPathNameW] always converts separators.
let path = convert_separators(initial_path, None);
let mut wide_path: Vec<_> = path.as_os_str().encode_wide().collect();
if wide_path.contains(&0) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"strings passed to WinAPI cannot contains NULs",
));
}
wide_path.push(0);
match path.components().next() {
// Verbatim paths should not be modified.
Some(Component::Prefix(prefix)) if prefix.kind().is_verbatim() => {
return Ok(normalize_verbatim(initial_path).into_owned());
}
Some(Component::RootDir) if wide_path[1] == SEPARATOR.into() => {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"partial UNC prefixes are invalid",
));
}
_ => {}
}
let mut buffer = Vec::new();
let mut capacity = 0;
loop {
capacity = unsafe {
GetFullPathNameW(
wide_path.as_ptr(),
capacity,
buffer.as_mut_ptr(),
ptr::null_mut(),
)
};
if capacity == 0 {
break Err(io::Error::last_os_error());
}
let _: u32 = capacity;
// This assertion should never fail.
static_assert!(mem::size_of::<u32>() <= mem::size_of::<usize>());
let length = capacity as usize;
if let Some(mut additional_capacity) =
length.checked_sub(buffer.capacity())
{
assert_ne!(0, additional_capacity);
// WinAPI can recommend an insufficient capacity that causes it to
// return incorrect results, so extra space is reserved as a
// workaround.
macro_rules! extra_capacity {
() => {
2
};
}
capacity =
capacity.checked_add(extra_capacity!()).ok_or_else(|| {
io::Error::new(
io::ErrorKind::Other,
"required path length is too large for WinAPI",
)
})?;
additional_capacity += extra_capacity!();
buffer.reserve(additional_capacity);
continue;
}
// SAFETY: These characters were initialized by the syscall.
unsafe {
buffer.set_len(length);
}
break Ok(BasePathBuf(OsString::from_wide(&buffer).into()));
}
}
pub(crate) fn normalize(path: &Path) -> io::Result<BasePathBuf> {
path.metadata().and_then(|_| normalize_virtually(path))
}
fn get_prefix(base: &BasePath) -> PrefixComponent<'_> {
if let Some(Component::Prefix(prefix)) = base.components().next() {
prefix
} else {
// Base paths should always have a prefix.
panic!(
"base path is missing a prefix: \"{}\"",
base.as_path().display(),
);
}
}
fn push_separator(base: &mut BasePathBuf) {
// Add a separator if necessary.
base.0.push("");
}
pub(crate) fn push(base: &mut BasePathBuf, initial_path: &Path) {
// [GetFullPathNameW] always converts separators.
let path = convert_separators(initial_path, None);
let mut components = path.components();
let mut next_component = components.next();
match next_component {
Some(Component::Prefix(prefix)) => {
// Verbatim paths should not be modified.
if prefix.kind().is_verbatim() {
*base = normalize_verbatim(initial_path).into_owned();
return;
}
next_component = components.next();
// Other prefixes are absolute, except drive-relative prefixes.
if !matches!(prefix.kind(), Prefix::Disk(_))
|| prefix.kind() != get_prefix(base).kind()
// Equivalent to [path.has_root()] but more efficient.
|| next_component == Some(Component::RootDir)
{
*base = BasePathBuf(path.into_owned());
return;
}
}
Some(Component::RootDir) => {
let mut buffer = get_prefix(base).as_os_str().to_owned();
buffer.push(&*path);
*base = BasePathBuf(buffer.into());
return;
}
_ => {
while let Some(component) = next_component {
match component {
Component::CurDir => {}
Component::ParentDir if base.pop().is_ok() => {}
_ => break,
}
next_component = components.next();
}
}
}
if let Some(component) = next_component {
push_separator(base);
base.0.as_mut_os_string().push(component);
let components = components.as_path();
if !components.as_os_str().is_empty() {
push_separator(base);
base.0.as_mut_os_string().push(components);
}
}
let path_bytes = path.as_os_str().as_encoded_bytes();
// At least one separator should be kept.
if path_bytes.last() == Some(&SEPARATOR)
|| path_bytes.ends_with(&[SEPARATOR, b'.'])
{
push_separator(base);
}
}