use crate::{OsStrExt, PathExt};
use std::{
ffi::OsStr,
path::{Component, Path, PathBuf},
};
use windows::Win32::Foundation::MAX_PATH;
pub(crate) struct LongUnc<'a> {
stripped: &'a [u8],
}
impl<'a> TryFrom<&'a [u8]> for LongUnc<'a> {
type Error = ();
fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> {
if let Some(suffix) = bytes.strip_prefix(Self::PREFIX) {
return Ok(Self { stripped: suffix });
}
Err(())
}
}
impl<'a, const N: usize> TryFrom<&'a [u8; N]> for LongUnc<'a> {
type Error = ();
fn try_from(bytes: &'a [u8; N]) -> Result<Self, Self::Error> {
Self::try_from(bytes.as_slice())
}
}
impl<'a> TryFrom<&'a str> for LongUnc<'a> {
type Error = ();
fn try_from(str: &'a str) -> Result<Self, Self::Error> {
Self::try_from(str.as_bytes())
}
}
impl<'a> TryFrom<&'a Path> for LongUnc<'a> {
type Error = ();
fn try_from(path: &'a Path) -> Result<Self, Self::Error> {
Self::try_from(path.as_os_str().as_encoded_bytes())
}
}
impl<'a> LongUnc<'a> {
const PREFIX: &'static [u8] = br"\\?\";
const UNC_SUB_PREFIX: &'static [u8] = br"UNC\";
const SHORT_UNC_PREFIX: &'static [u8] = br"\\";
const SHORT_UNC_LEN_SUB: usize = Self::UNC_SUB_PREFIX.len() - Self::SHORT_UNC_PREFIX.len();
fn as_stripped_osstr(&self) -> &OsStr {
unsafe { OsStr::from_encoded_bytes_unchecked(self.stripped) }
}
fn as_stripped_path(&self) -> &Path {
Path::new(self.as_stripped_osstr())
}
pub(crate) fn is_sub_prefix_unc(&self) -> bool {
self.stripped.len() >= Self::UNC_SUB_PREFIX.len()
&& self.stripped[..Self::UNC_SUB_PREFIX.len()]
.eq_ignore_ascii_case(Self::UNC_SUB_PREFIX)
}
fn is_stripped_longer_than_wide(&self, max: u32) -> bool {
self.as_stripped_osstr().is_longer_than_wide(max)
}
pub(crate) fn is_short_unc_longer_than_max_path(&self) -> bool {
self.is_stripped_longer_than_wide(MAX_PATH + Self::SHORT_UNC_LEN_SUB as u32)
}
pub(crate) fn has_invalid_chars(&self) -> bool {
let mut path = self.as_stripped_path();
let mut components = path.components();
match components.next() {
None => return false,
Some(Component::Prefix(_)) => path = components.as_path(),
_ => {}
};
path.has_win_invalid_chars()
}
pub(crate) fn to_short_unc(&self) -> PathBuf {
assert!(self.is_sub_prefix_unc());
let capacity = self.stripped.len() - Self::SHORT_UNC_LEN_SUB;
let mut result_bytes = Vec::with_capacity(capacity);
result_bytes.extend_from_slice(Self::SHORT_UNC_PREFIX);
result_bytes.extend_from_slice(&self.stripped[Self::UNC_SUB_PREFIX.len()..]);
debug_assert_eq!(result_bytes.len(), capacity);
let os_str = unsafe { OsStr::from_encoded_bytes_unchecked(&result_bytes) };
PathBuf::from(os_str)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::windows::ffi::OsStrExt;
#[test]
fn try_from() {
assert!(LongUnc::try_from(br"\\?\server\share\dir").is_ok());
assert!(LongUnc::try_from(br"\\?\C:\").is_ok());
assert!(LongUnc::try_from(br"\\?\").is_ok());
assert!(LongUnc::try_from(br"\\?").is_err());
assert!(LongUnc::try_from(br"\\server\share\dir").is_err());
assert!(LongUnc::try_from(br"\a\b").is_err());
}
fn from_bytes(bytes: &[u8]) -> LongUnc<'_> {
LongUnc::try_from(bytes).unwrap()
}
fn from_str(str: &str) -> LongUnc<'_> {
LongUnc::try_from(str).unwrap()
}
#[test]
fn is_sub_prefix_unc() {
assert!(from_bytes(br"\\?\UNC\").is_sub_prefix_unc());
assert!(from_bytes(br"\\?\UNC\server\share\dir").is_sub_prefix_unc());
assert!(from_bytes(br"\\?\unc\server\share\dir").is_sub_prefix_unc());
assert!(from_bytes(br"\\?\uNc\server\share\dir").is_sub_prefix_unc());
assert!(from_bytes(br"\\?\UnC\server\share\dir").is_sub_prefix_unc());
assert!(!from_bytes(br"\\?\UN").is_sub_prefix_unc());
assert!(!from_bytes(br"\\?\UNC").is_sub_prefix_unc());
assert!(!from_bytes(br"\\?\UNCD\").is_sub_prefix_unc());
assert!(!from_bytes(br"\\?\server\share\dir").is_sub_prefix_unc());
}
#[test]
fn has_invalid_chars() {
assert!(!from_bytes(br"\\?\UNC\foo").has_invalid_chars());
assert!(!from_bytes(br"\\?\C:\").has_invalid_chars());
assert!(!from_bytes(br"\\?\C:\foo").has_invalid_chars());
assert!(from_bytes(br"\\?\UNC\foo:").has_invalid_chars());
assert!(from_bytes(br"\\?\UNC\foo>").has_invalid_chars());
assert!(from_bytes(br"\\?\C:\foo:").has_invalid_chars());
assert!(from_bytes(br"\\?\:\foo").has_invalid_chars());
assert!(from_bytes(br"\\?\>\foo").has_invalid_chars());
}
#[test]
fn to_short_unc() {
assert_eq!(
from_bytes(br"\\?\UNC\server\share\dir").to_short_unc(),
PathBuf::from(r"\\server\share\dir")
);
}
#[test]
fn is_short_unc_longer_than_max_path() {
const PREFIX: &str = r"\\?\UNC\";
const SHORT_PREFIX: &str = r"\\";
const SERVER_SHARE: &str = r"server\share\";
const PATH_MAX: usize = MAX_PATH as usize - SHORT_PREFIX.len() - SERVER_SHARE.len();
for (suffix, len) in [
("1".repeat(PATH_MAX), MAX_PATH),
("1".repeat(PATH_MAX + 1), MAX_PATH + 1),
("\u{3042}".repeat(PATH_MAX), MAX_PATH),
("\u{3042}".repeat(PATH_MAX + 1), MAX_PATH + 1),
] {
let is_too_long = len > MAX_PATH;
let suffix = SERVER_SHARE.to_string() + &suffix;
let path_str = PREFIX.to_string() + &suffix;
let long = from_str(&path_str);
assert_eq!(long.is_short_unc_longer_than_max_path(), is_too_long);
let short = long.to_short_unc();
assert_eq!(short, PathBuf::from(&(SHORT_PREFIX.to_string() + &suffix)));
assert_eq!(short.as_os_str().encode_wide().count(), len as usize);
}
}
}