use std::fmt;
use std::io;
use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
use wtf_string::{Wtf16Str, Wtf16String};
const MAX_PATH: usize = 260;
const MAX_PATH_CONTENT: usize = MAX_PATH - 1;
const VERBATIM_PREFIX: [u16; 4] = [b'\\' as u16, b'\\' as u16, b'?' as u16, b'\\' as u16];
const VERBATIM_UNC: [u16; 4] = [b'U' as u16, b'N' as u16, b'C' as u16, b'\\' as u16];
const BACKSLASH: u16 = b'\\' as u16;
const COLON: u16 = b':' as u16;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PathFailure {
EmptyPath,
InteriorNul,
PathTooLong,
NotFullyQualified,
PathResolution,
}
impl PathFailure {
#[must_use]
pub fn description(self) -> &'static str {
match self {
Self::EmptyPath => "the path was empty",
Self::InteriorNul => "the path contained an interior NUL",
Self::PathTooLong => "the path exceeded MAX_PATH",
Self::NotFullyQualified => "the verbatim path was not fully qualified",
Self::PathResolution => "the path could not be resolved",
}
}
}
#[derive(Debug)]
pub struct PathError {
failure: PathFailure,
source: Option<io::Error>,
}
impl PathError {
fn new(failure: PathFailure) -> Self {
Self {
failure,
source: None,
}
}
fn with_last_os(failure: PathFailure) -> Self {
Self {
failure,
source: Some(io::Error::last_os_error()),
}
}
#[must_use]
pub fn failure(&self) -> PathFailure {
self.failure
}
#[must_use]
pub fn raw_os_error(&self) -> Option<i32> {
self.source.as_ref().and_then(io::Error::raw_os_error)
}
}
impl fmt::Display for PathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.source {
Some(source) => write!(f, "{}: {source}", self.failure.description()),
None => f.write_str(self.failure.description()),
}
}
}
impl std::error::Error for PathError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|source| source as &(dyn std::error::Error + 'static))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PreparedPath {
units: Wtf16String,
}
impl PreparedPath {
#[must_use]
pub fn as_wtf16(&self) -> &Wtf16Str {
&self.units
}
#[must_use]
pub fn into_wtf16(self) -> Wtf16String {
self.units
}
pub(crate) fn as_wtf16_terminated(&self) -> *const u16 {
self.units.as_terminated_ptr()
}
}
pub fn prepare(path: &Wtf16Str) -> Result<PreparedPath, PathError> {
prepare_units(path).map(|units| PreparedPath { units })
}
fn prepare_units(path: &Wtf16Str) -> Result<Wtf16String, PathError> {
if path.is_empty() {
return Err(PathError::new(PathFailure::EmptyPath));
}
if path.has_interior_nul() {
return Err(PathError::new(PathFailure::InteriorNul));
}
let units = path.as_units();
if units.starts_with(&VERBATIM_PREFIX) {
validate_verbatim(&units[VERBATIM_PREFIX.len()..])?;
return Ok(Wtf16String::from_units(units));
}
if units.len() > MAX_PATH_CONTENT {
return Err(PathError::new(PathFailure::PathTooLong));
}
resolve(path)
}
fn validate_verbatim(rest: &[u16]) -> Result<(), PathError> {
let not_qualified = || PathError::new(PathFailure::NotFullyQualified);
if rest.starts_with(&VERBATIM_UNC) {
let after_unc = &rest[VERBATIM_UNC.len()..];
let Some(separator) = after_unc.iter().position(|unit| *unit == BACKSLASH) else {
return Err(not_qualified());
};
let server = &after_unc[..separator];
let share = &after_unc[separator + 1..];
let share_len = share
.iter()
.position(|unit| *unit == BACKSLASH)
.unwrap_or(share.len());
if server.is_empty() || share_len == 0 {
return Err(not_qualified());
}
return Ok(());
}
let Some(separator) = rest.iter().position(|unit| *unit == BACKSLASH) else {
return Err(not_qualified());
};
let root = &rest[..separator];
if root.is_empty() {
return Err(not_qualified());
}
if root.contains(&COLON) && !is_drive_designator(root) {
return Err(not_qualified());
}
Ok(())
}
fn is_drive_designator(root: &[u16]) -> bool {
let [letter, colon] = root else {
return false;
};
*colon == COLON && u8::try_from(*letter).is_ok_and(|byte| byte.is_ascii_alphabetic())
}
fn resolve(path: &Wtf16Str) -> Result<Wtf16String, PathError> {
let input = Wtf16String::from_units(path.as_units());
let mut resolved = Wtf16String::with_capacity(MAX_PATH);
let written = unsafe {
GetFullPathNameW(
input.as_terminated_ptr(),
MAX_PATH as u32,
resolved.as_mut_ptr(),
core::ptr::null_mut(),
)
};
if written == 0 {
let failure = PathError::with_last_os(PathFailure::PathResolution);
unsafe { resolved.set_len_from_ffi(0) };
return Err(failure);
}
let written = written as usize;
if written > MAX_PATH_CONTENT {
unsafe { resolved.set_len_from_ffi(0) };
return Err(PathError::new(PathFailure::PathTooLong));
}
unsafe { resolved.set_len_from_ffi(written) };
if resolved.is_empty() {
return Err(PathError::new(PathFailure::PathResolution));
}
Ok(resolved)
}
#[cfg(test)]
mod tests;