use std::fmt;
use windows_sys::Win32::System::Environment::GetCurrentDirectoryW;
use wtf_string::{Wtf16Str, Wtf16String};
use crate::outcome::{Win32Error, perform_nonzero};
const MAX_ATTEMPTS: usize = 8;
const FIRST_ATTEMPT_CHARS: usize = 260;
const BACKSLASH: u16 = b'\\' as u16;
const SLASH: u16 = b'/' as u16;
const COLON: u16 = b':' as u16;
const DOT: u16 = b'.' as u16;
const QUESTION: u16 = b'?' as u16;
const SPACE: u16 = b' ' as u16;
const DIGIT_ZERO: u16 = b'0' as u16;
const DIGIT_NINE: u16 = b'9' as u16;
const SUPERSCRIPT_DIGITS: [u16; 3] = [0x00B9, 0x00B2, 0x00B3];
const RESERVED_NAMES: [&[u8]; 6] = [b"CON", b"PRN", b"AUX", b"NUL", b"CONIN$", b"CONOUT$"];
const RESERVED_NAMES_WITH_DIGIT: [&[u8]; 2] = [b"COM", b"LPT"];
#[derive(Debug)]
#[non_exhaustive]
pub enum PathCaptureError {
InteriorNul,
CurrentDirectory(Win32Error),
CurrentDirectoryUnstable {
attempts: usize,
},
}
impl fmt::Display for PathCaptureError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InteriorNul => f.write_str("the path contains an interior NUL"),
Self::CurrentDirectory(error) => write!(f, "GetCurrentDirectoryW: {error}"),
Self::CurrentDirectoryUnstable { attempts } => write!(
f,
"GetCurrentDirectoryW: the required size changed on each of {attempts} attempts"
),
}
}
}
impl std::error::Error for PathCaptureError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::CurrentDirectory(error) => Some(error),
Self::InteriorNul | Self::CurrentDirectoryUnstable { .. } => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct AnchoredPath {
given: Wtf16String,
grafted: Option<Wtf16String>,
}
impl AnchoredPath {
pub fn new(given: Wtf16String) -> Result<Self, PathCaptureError> {
if given.has_interior_nul() {
return Err(PathCaptureError::InteriorNul);
}
if !has_graftable_shape(given.as_units()) {
return Ok(Self {
given,
grafted: None,
});
}
let root = capture_current_directory()?;
Ok(Self::anchored_at(given, &root))
}
pub(crate) fn anchored_at(given: Wtf16String, root: &Wtf16Str) -> Self {
let grafted =
graft(given.as_units(), root.as_units()).map(|units| Wtf16String::from_units(&units));
Self { given, grafted }
}
#[must_use]
pub fn given(&self) -> &Wtf16Str {
&self.given
}
#[must_use]
pub fn win32(&self) -> &Wtf16Str {
self.grafted.as_ref().unwrap_or(&self.given)
}
#[cfg(test)]
pub(crate) fn is_grafted(&self) -> bool {
self.grafted.is_some()
}
pub(crate) fn win32_terminated(&self) -> *const u16 {
self.grafted
.as_ref()
.unwrap_or(&self.given)
.as_terminated_ptr()
}
}
fn graft(child: &[u16], root: &[u16]) -> Option<Vec<u16>> {
if root.is_empty() || !has_graftable_shape(child) {
return None;
}
if is_verbatim_or_device(root) {
return None;
}
if is_unc(root) && has_parent_component(child) {
return None;
}
let needs_separator = root.last() != Some(&BACKSLASH);
let mut joined = Vec::with_capacity(root.len() + 1 + child.len());
joined.extend_from_slice(root);
if needs_separator {
joined.push(BACKSLASH);
}
joined.extend_from_slice(child);
Some(joined)
}
fn has_graftable_shape(child: &[u16]) -> bool {
let Some(first) = child.first() else {
return false;
};
if is_separator(*first) || child.contains(&COLON) {
return false;
}
!components(child).any(is_reserved_device_name)
}
fn is_verbatim_or_device(root: &[u16]) -> bool {
matches!(root, [BACKSLASH, BACKSLASH, prefix, BACKSLASH, ..] if *prefix == QUESTION || *prefix == DOT)
}
fn is_unc(root: &[u16]) -> bool {
matches!(root, [first, second, ..] if is_separator(*first) && is_separator(*second))
}
fn has_parent_component(child: &[u16]) -> bool {
components(child).any(|component| component == [DOT, DOT])
}
fn is_separator(unit: u16) -> bool {
unit == BACKSLASH || unit == SLASH
}
fn components(path: &[u16]) -> impl Iterator<Item = &[u16]> {
path.split(|unit| is_separator(*unit))
}
fn is_reserved_device_name(component: &[u16]) -> bool {
let stem = component.split(|unit| *unit == DOT).next().unwrap_or(&[]);
let end = stem
.iter()
.rposition(|unit| *unit != SPACE)
.map_or(0, |last| last + 1);
let stem = &stem[..end];
if RESERVED_NAMES.iter().any(|name| equals_ascii(stem, name)) {
return true;
}
let Some((digit, name)) = stem.split_last() else {
return false;
};
let is_digit = (DIGIT_ZERO..=DIGIT_NINE).contains(digit) || SUPERSCRIPT_DIGITS.contains(digit);
is_digit
&& RESERVED_NAMES_WITH_DIGIT
.iter()
.any(|reserved| equals_ascii(name, reserved))
}
fn equals_ascii(units: &[u16], name: &[u8]) -> bool {
units.len() == name.len()
&& units.iter().zip(name).all(|(unit, expected)| {
u8::try_from(*unit).is_ok_and(|unit| unit.eq_ignore_ascii_case(expected))
})
}
fn capture_current_directory() -> Result<Wtf16String, PathCaptureError> {
read_current_directory(|buffer| {
let capacity = u32::try_from(buffer.len()).unwrap_or(u32::MAX);
perform_nonzero(|| {
unsafe { GetCurrentDirectoryW(capacity, buffer.as_mut_ptr()) }
})
})
}
fn read_current_directory(
mut call: impl FnMut(&mut [u16]) -> Result<u32, Win32Error>,
) -> Result<Wtf16String, PathCaptureError> {
let mut buffer = vec![0_u16; FIRST_ATTEMPT_CHARS];
for _ in 0..MAX_ATTEMPTS {
let written = call(&mut buffer).map_err(PathCaptureError::CurrentDirectory)? as usize;
if written < buffer.len() {
return Ok(Wtf16String::from_units(&buffer[..written]));
}
buffer.resize(written, 0);
}
Err(PathCaptureError::CurrentDirectoryUnstable {
attempts: MAX_ATTEMPTS,
})
}
#[cfg(test)]
mod tests;