simple-path 0.3.3

Simplify Windows UNC path for `fs::canonicalize`
Documentation
use crate::{OsStrExt, PathExt};
use std::{
    ffi::OsStr,
    path::{Component, Path, PathBuf},
};
use windows::Win32::Foundation::MAX_PATH;

/// Represents a path prefixed by `\\?\`.
/// Also called [Win32 File Namespace],
/// or Extended-Length Path.
///
/// [Win32 File Namespace]: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
pub(crate) struct LongUnc<'a> {
    /// The path with the `\\?\` prefix stripped.
    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,
            // Skip `Prefix`; e.g., `\\?\C:`.
            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);
        }
    }
}