use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
use wtf_string::{Wtf16Str, Wtf16String};
use crate::error::{RequestError, RequestFailure, Win32Error};
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;
pub(crate) fn prepare(path: &Wtf16Str) -> Result<Wtf16String, RequestError> {
if path.is_empty() {
return Err(RequestError::new(RequestFailure::EmptyPath));
}
if path.has_interior_nul() {
return Err(RequestError::new(RequestFailure::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(RequestError::new(RequestFailure::PathTooLong));
}
resolve(path)
}
fn validate_verbatim(rest: &[u16]) -> Result<(), RequestError> {
let not_qualified = || RequestError::new(RequestFailure::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, RequestError> {
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 code = Win32Error::last();
unsafe { resolved.set_len_from_ffi(0) };
return Err(RequestError::with_code(
RequestFailure::PathResolution,
code,
));
}
let written = written as usize;
if written > MAX_PATH_CONTENT {
unsafe { resolved.set_len_from_ffi(0) };
return Err(RequestError::new(RequestFailure::PathTooLong));
}
unsafe { resolved.set_len_from_ffi(written) };
if resolved.is_empty() {
return Err(RequestError::new(RequestFailure::PathResolution));
}
Ok(resolved)
}
#[cfg(test)]
mod tests;