use crate::PathExt;
use std::{
ffi::{OsStr, OsString},
path::{Component, Path, PathBuf},
};
pub(crate) struct UncPath<'a> {
path: &'a Path,
}
impl<'a> TryFrom<&'a [u8]> for UncPath<'a> {
type Error = ();
fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> {
if bytes.starts_with(Self::UNC_PREFIX) {
let os_str = unsafe { OsStr::from_encoded_bytes_unchecked(bytes) };
let path = Path::new(os_str);
return Ok(Self { path });
}
Err(())
}
}
impl<'a> TryFrom<&'a Path> for UncPath<'a> {
type Error = ();
fn try_from(path: &'a Path) -> Result<Self, Self::Error> {
let bytes = path.as_os_str().as_encoded_bytes();
if bytes.starts_with(Self::UNC_PREFIX) {
return Ok(Self { path });
}
Err(())
}
}
#[cfg(test)]
impl<'a> TryFrom<&'a str> for UncPath<'a> {
type Error = ();
fn try_from(str: &'a str) -> Result<Self, Self::Error> {
Self::try_from(Path::new(str))
}
}
impl<'a> UncPath<'a> {
const UNC_PREFIX: &'static [u8] = br"\\";
const FILE_NAMESPACE_PREFIX: &'static [u8] = br"?\";
const UNC_KEYWORD: &'static [u8] = br"UNC";
#[inline]
fn path(&self) -> &Path {
self.path
}
fn as_encoded_bytes(&self) -> &[u8] {
self.path().as_os_str().as_encoded_bytes()
}
fn as_stripped_encoded_bytes(&self) -> &[u8] {
&self.as_encoded_bytes()[Self::UNC_PREFIX.len()..]
}
fn as_stripped_os_str(&self) -> &OsStr {
let bytes = self.as_stripped_encoded_bytes();
unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }
}
fn is_file_namespace(&self) -> bool {
self.as_stripped_encoded_bytes()
.starts_with(Self::FILE_NAMESPACE_PREFIX)
}
fn as_file_namespace_stripped_encoded_bytes(&self) -> Option<&[u8]> {
self.as_stripped_encoded_bytes()
.strip_prefix(Self::FILE_NAMESPACE_PREFIX)
}
fn as_file_namespace_stripped_os_str(&self) -> Option<&OsStr> {
self.as_file_namespace_stripped_encoded_bytes()
.map(|bytes| unsafe { OsStr::from_encoded_bytes_unchecked(bytes) })
}
fn as_file_namespace_stripped_path(&self) -> Option<&Path> {
self.as_file_namespace_stripped_os_str().map(Path::new)
}
pub(crate) fn is_file_namespace_unc(&self) -> bool {
if let Some(stripped) = self.as_file_namespace_stripped_encoded_bytes() {
return Self::starts_with_unc_keyword_and_separator(stripped);
}
false
}
fn as_file_namespace_unc_stripped_encoded_bytes(&self) -> Option<&[u8]> {
if let Some(stripped) = self.as_file_namespace_stripped_encoded_bytes()
&& Self::starts_with_unc_keyword_and_separator(stripped)
{
return Some(&stripped[Self::UNC_KEYWORD.len() + 1..]);
}
None
}
fn starts_with_unc_keyword_and_separator(str: &[u8]) -> bool {
str.len() > Self::UNC_KEYWORD.len()
&& str[..Self::UNC_KEYWORD.len()].eq_ignore_ascii_case(Self::UNC_KEYWORD)
&& str[Self::UNC_KEYWORD.len()] == b'\\'
}
pub(crate) fn has_invalid_chars(&self) -> bool {
if let Some(file_ns_stripped) = self.as_file_namespace_stripped_path() {
let mut components = file_ns_stripped.components();
return match components.next() {
None => false,
Some(Component::Prefix(_)) => components.as_path().has_win_invalid_chars(),
_ => file_ns_stripped.has_win_invalid_chars(),
};
}
self.path().has_win_invalid_chars()
}
pub(crate) fn to_short_unc(&self) -> Option<PathBuf> {
if let Some(stripped) = self.as_file_namespace_unc_stripped_encoded_bytes() {
let capacity = stripped.len() + Self::UNC_PREFIX.len();
let mut result_bytes = Vec::with_capacity(capacity);
result_bytes.extend_from_slice(Self::UNC_PREFIX);
result_bytes.extend_from_slice(stripped);
debug_assert_eq!(result_bytes.len(), capacity);
let os_str = unsafe { OsStr::from_encoded_bytes_unchecked(&result_bytes) };
return Some(PathBuf::from(os_str));
}
None
}
pub(crate) fn to_filename_space_unc(&self) -> Option<PathBuf> {
if !self.is_file_namespace() {
let mut path = OsString::from(r"\\?\UNC\");
path.push(self.as_stripped_os_str());
return Some(PathBuf::from(path));
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn try_from() {
assert!(UncPath::try_from(r"\\server\share\dir").is_ok());
assert!(UncPath::try_from(r"\\?").is_ok());
assert!(UncPath::try_from(r"\\?\server\share\dir").is_ok());
assert!(UncPath::try_from(r"\\?\C:\").is_ok());
assert!(UncPath::try_from(r"\\?\").is_ok());
assert!(UncPath::try_from(r"C:\a\b").is_err());
assert!(UncPath::try_from(r"\a\b").is_err());
assert!(UncPath::try_from(r"a\b").is_err());
}
fn from_str(str: &str) -> UncPath<'_> {
UncPath::try_from(str).unwrap()
}
#[test]
fn is_file_namespace() {
assert!(from_str(r"\\?\").is_file_namespace());
assert!(from_str(r"\\?\C:\server\share\dir").is_file_namespace());
assert!(from_str(r"\\?\UNC\server\share\dir").is_file_namespace());
assert!(!from_str(r"\\?").is_file_namespace());
assert!(!from_str(r"\\??\").is_file_namespace());
assert!(!from_str(r"\\server\share").is_file_namespace());
assert!(!from_str(r"\\.\device").is_file_namespace());
}
#[test]
fn is_file_namespace_unc() {
assert!(from_str(r"\\?\UNC\").is_file_namespace_unc());
assert!(from_str(r"\\?\UNC\server\share\dir").is_file_namespace_unc());
assert!(from_str(r"\\?\unc\server\share\dir").is_file_namespace_unc());
assert!(from_str(r"\\?\uNc\server\share\dir").is_file_namespace_unc());
assert!(from_str(r"\\?\UnC\server\share\dir").is_file_namespace_unc());
assert!(!from_str(r"\\?\UN").is_file_namespace_unc());
assert!(!from_str(r"\\?\UNC").is_file_namespace_unc());
assert!(!from_str(r"\\?\UNCD\").is_file_namespace_unc());
assert!(!from_str(r"\\?\server\share\dir").is_file_namespace_unc());
}
#[test]
fn has_invalid_chars() {
assert!(!from_str(r"\\?\UNC\foo").has_invalid_chars());
assert!(!from_str(r"\\?\C:\").has_invalid_chars());
assert!(!from_str(r"\\?\C:\foo").has_invalid_chars());
assert!(from_str(r"\\?\UNC\foo:").has_invalid_chars());
assert!(from_str(r"\\?\UNC\foo>").has_invalid_chars());
assert!(from_str(r"\\?\C:\foo:").has_invalid_chars());
assert!(from_str(r"\\?\:\foo").has_invalid_chars());
assert!(from_str(r"\\?\>\foo").has_invalid_chars());
}
#[test]
fn to_short_unc() {
assert_eq!(
from_str(r"\\?\UNC\server\share\dir").to_short_unc(),
Some(PathBuf::from(r"\\server\share\dir"))
);
assert_eq!(from_str(r"\\server\share\dir").to_short_unc(), None);
assert_eq!(from_str(r"\\.\device").to_short_unc(), None);
}
#[test]
fn to_filename_space_unc() {
assert_eq!(
from_str(r"\\server\share\dir").to_filename_space_unc(),
Some(PathBuf::from(r"\\?\UNC\server\share\dir"))
);
assert_eq!(
from_str(r"\\?\UNC\server\share\dir").to_filename_space_unc(),
None
);
assert_eq!(from_str(r"\\?\C:\dir").to_filename_space_unc(), None);
}
}