Skip to content

Commit

Permalink
Separate shortening and expansion methods
Browse files Browse the repository at this point in the history
  • Loading branch information
dylni committed Aug 1, 2024
1 parent efcaca9 commit 8164f5a
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 36 deletions.
23 changes: 23 additions & 0 deletions src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ use super::error::ParentError;
use super::imp;
use super::PathExt;

fn cow_path_into_base_path(path: Cow<'_, Path>) -> Cow<'_, BasePath> {
debug_assert!(imp::is_base(&path));

match path {
Cow::Borrowed(path) => {
Cow::Borrowed(BasePath::from_inner(path.as_os_str()))
}
Cow::Owned(path) => Cow::Owned(BasePathBuf(path)),
}
}

/// A borrowed path that has a [prefix] on Windows.
///
/// Note that comparison traits such as [`PartialEq`] will compare paths
Expand Down Expand Up @@ -170,6 +181,12 @@ impl BasePath {
self.as_path().exists()
}

/// Equivalent to [`PathExt::expand`].
#[inline]
pub fn expand(&self) -> io::Result<Cow<'_, Self>> {
self.as_path().expand().map(cow_path_into_base_path)
}

/// Equivalent to [`Path::extension`].
#[inline]
#[must_use]
Expand Down Expand Up @@ -390,6 +407,12 @@ impl BasePath {
self.as_path().read_link()
}

/// Equivalent to [`PathExt::shorten`].
#[inline]
pub fn shorten(&self) -> io::Result<Cow<'_, Self>> {
self.as_path().shorten().map(cow_path_into_base_path)
}

/// Equivalent to [`Path::starts_with`].
#[inline]
#[must_use]
Expand Down
9 changes: 9 additions & 0 deletions src/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::io;
use std::path::Path;

Expand All @@ -22,6 +23,14 @@ pub(crate) fn normalize(path: &Path) -> io::Result<BasePathBuf> {
path.canonicalize().and_then(BasePathBuf::new)
}

pub(crate) fn expand(path: &Path) -> io::Result<Cow<'_, Path>> {
path.metadata().map(|_| Cow::Borrowed(path))
}

pub(crate) fn shorten(path: &Path) -> io::Result<Cow<'_, Path>> {
expand(path)
}

pub(crate) fn push(base: &mut BasePathBuf, path: &Path) {
if !path.as_os_str().is_empty() {
base.0.push(path);
Expand Down
117 changes: 103 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
//!
//! # Implementation
//!
//! Some methods return [`Cow`] to account for platform differences. However,
//! no guarantee is made that the same variant of that enum will always be
//! returned for the same platform. Whichever can be constructed most
//! efficiently will be returned.
//!
//! All traits are [sealed], meaning that they can only be implemented by this
//! crate. Otherwise, backward compatibility would be more difficult to
//! maintain for new features.
Expand Down Expand Up @@ -95,7 +100,6 @@
#![cfg_attr(normpath_docs_rs, feature(doc_cfg))]
#![warn(unused_results)]

#[cfg(feature = "localization")]
use std::borrow::Cow;
#[cfg(feature = "localization")]
use std::ffi::OsStr;
Expand All @@ -120,6 +124,49 @@ use imp::localize;

/// Additional methods added to [`Path`].
pub trait PathExt: private::Sealed {
/// Expands `self` from its short form, if the convention exists for the
/// platform.
///
/// This method reverses [`shorten`] but may not return the original path.
/// Additional components may be shortened that were not before calling
/// [`shorten`].
///
/// # Implementation
///
/// Currently, this method calls:
/// - [`GetLongPathNameW`] on Windows.
///
/// However, the implementation is subject to change. This section is only
/// informative.
///
/// # Errors
///
/// Returns an error if `self` does not exist, even on Unix.
///
/// # Examples
///
/// ```
/// # use std::io;
/// use std::path::Path;
///
/// use normpath::PathExt;
///
/// if cfg!(windows) {
/// assert_eq!(
/// Path::new(r"C:\Documents and Settings"),
/// Path::new(r"C:\DOCUME~1").expand()?,
/// );
/// }
/// #
/// # Ok::<_, io::Error>(())
/// ```
///
/// [`GetLongPathNameW`]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getlongpathnamew
/// [`shorten`]: Self::shorten
fn expand(&self) -> io::Result<Cow<'_, Self>>
where
Self: ToOwned;

/// Returns the localized simple name for this path.
///
/// If the path does not exist or localization is not possible, the last
Expand Down Expand Up @@ -220,7 +267,7 @@ pub trait PathExt: private::Sealed {
///
/// Currently, this method calls:
/// - [`fs::canonicalize`] on Unix.
/// - [`GetFullPathNameW`] and [`GetLongPathNameW`] on Windows.
/// - [`GetFullPathNameW`] on Windows.
///
/// However, the implementation is subject to change. This section is only
/// informative.
Expand Down Expand Up @@ -254,7 +301,6 @@ pub trait PathExt: private::Sealed {
///
/// [`fs::canonicalize`]: ::std::fs::canonicalize
/// [`GetFullPathNameW`]: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfullpathnamew
/// [`GetLongPathNameW`]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getlongpathnamew
/// [`normalize_virtually`]: Self::normalize_virtually
/// [rust-lang/rust#42869]: https://github.com/rust-lang/rust/issues/42869
/// [rust-lang/rust#49342]: https://github.com/rust-lang/rust/issues/49342
Expand All @@ -263,16 +309,7 @@ pub trait PathExt: private::Sealed {
/// [verbatim]: ::std::path::Prefix::is_verbatim
fn normalize(&self) -> io::Result<BasePathBuf>;

/// Normalizes `self` similarly to [`normalize`] but does not perform any
/// operations that require accessing the filesystem.
///
/// # Implementation
///
/// Currently, this method is equivalent to [`normalize`], except that it
/// does not call [`GetLongPathNameW`] on Windows.
///
/// However, the implementation is subject to change. This section is only
/// informative.
/// Equivalent to [`normalize`] but does not access the file system.
///
/// # Errors
///
Expand All @@ -296,14 +333,61 @@ pub trait PathExt: private::Sealed {
/// # Ok::<_, io::Error>(())
/// ```
///
/// [`GetLongPathNameW`]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getlongpathnamew
/// [`normalize`]: Self::normalize
#[cfg(any(doc, windows))]
#[cfg_attr(normpath_docs_rs, doc(cfg(windows)))]
fn normalize_virtually(&self) -> io::Result<BasePathBuf>;

/// Shortens `self` from its expanded form, if the convention exists for
/// the platform.
///
/// This method reverses [`expand`] but may not return the original path.
/// Additional components may be shortened that were not before calling
/// [`expand`].
///
/// # Implementation
///
/// Currently, this method calls:
/// - [`GetShortPathNameW`] on Windows.
///
/// However, the implementation is subject to change. This section is only
/// informative.
///
/// # Errors
///
/// Returns an error if `self` does not exist, even on Unix.
///
/// # Examples
///
/// ```
/// # use std::io;
/// use std::path::Path;
///
/// use normpath::PathExt;
///
/// if cfg!(windows) {
/// assert_eq!(
/// Path::new(r"C:\DOCUME~1"),
/// Path::new(r"C:\Documents and Settings").shorten()?,
/// );
/// }
/// #
/// # Ok::<_, io::Error>(())
/// ```
///
/// [`expand`]: Self::expand
/// [`GetShortPathNameW`]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getshortpathnamew
fn shorten(&self) -> io::Result<Cow<'_, Self>>
where
Self: ToOwned;
}

impl PathExt for Path {
#[inline]
fn expand(&self) -> io::Result<Cow<'_, Self>> {
imp::expand(self)
}

#[cfg(feature = "localization")]
#[inline]
fn localize_name(&self) -> Cow<'_, OsStr> {
Expand Down Expand Up @@ -332,6 +416,11 @@ impl PathExt for Path {
fn normalize_virtually(&self) -> io::Result<BasePathBuf> {
imp::normalize_virtually(self)
}

#[inline]
fn shorten(&self) -> io::Result<Cow<'_, Self>> {
imp::shorten(self)
}
}

mod private {
Expand Down
47 changes: 26 additions & 21 deletions src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use std::ptr;

use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
use windows_sys::Win32::Storage::FileSystem::GetLongPathNameW;
use windows_sys::Win32::Storage::FileSystem::GetShortPathNameW;

use crate::BasePath;
use crate::BasePathBuf;
Expand Down Expand Up @@ -95,7 +96,10 @@ where
}
}

fn normalize_with(path: &Path, existing: bool) -> io::Result<BasePathBuf> {
fn winapi_path(
path: &Path,
call_fn: fn(*const u16, *mut u16, u32) -> u32,
) -> io::Result<Cow<'_, Path>> {
if path.as_os_str().as_encoded_bytes().contains(&0) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
Expand All @@ -106,7 +110,7 @@ fn normalize_with(path: &Path, existing: bool) -> io::Result<BasePathBuf> {
match path.components().next() {
// Verbatim paths should not be modified.
Some(Component::Prefix(prefix)) if prefix.kind().is_verbatim() => {
return Ok(BasePathBuf(path.to_owned()));
return Ok(Cow::Borrowed(path));
}
Some(Component::RootDir)
if path
Expand All @@ -127,33 +131,34 @@ fn normalize_with(path: &Path, existing: bool) -> io::Result<BasePathBuf> {
debug_assert!(!path.contains(&0));
path.push(0);

path = winapi_buffered(|buffer, capacity| unsafe {
GetFullPathNameW(path.as_ptr(), capacity, buffer, ptr::null_mut())
path = winapi_buffered(|buffer, capacity| {
call_fn(path.as_ptr(), buffer, capacity)
})?;

if existing {
if path.contains(&0) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"existing paths should not contains NULs",
));
}
path.push(0);

path = winapi_buffered(|buffer, capacity| unsafe {
GetLongPathNameW(path.as_ptr(), buffer, capacity)
})?;
}

Ok(BasePathBuf(OsString::from_wide(&path).into()))
Ok(Cow::Owned(OsString::from_wide(&path).into()))
}

pub(crate) fn normalize_virtually(path: &Path) -> io::Result<BasePathBuf> {
normalize_with(path, false)
winapi_path(path, |path, buffer, capacity| unsafe {
GetFullPathNameW(path, capacity, buffer, ptr::null_mut())
})
.map(|x| BasePathBuf(x.into_owned()))
}

pub(crate) fn normalize(path: &Path) -> io::Result<BasePathBuf> {
path.metadata().and_then(|_| normalize_with(path, true))
path.metadata().and_then(|_| normalize_virtually(path))
}

pub(crate) fn expand(path: &Path) -> io::Result<Cow<'_, Path>> {
winapi_path(path, |path, buffer, capacity| unsafe {
GetLongPathNameW(path, buffer, capacity)
})
}

pub(crate) fn shorten(path: &Path) -> io::Result<Cow<'_, Path>> {
winapi_path(path, |path, buffer, capacity| unsafe {
GetShortPathNameW(path, buffer, capacity)
})
}

fn get_prefix(base: &BasePath) -> PrefixComponent<'_> {
Expand Down
4 changes: 3 additions & 1 deletion tests/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ fn test_short() {
fn test(short_path: &str, long_path: &str) {
let short_path = Path::new(short_path);
let long_path = Path::new(long_path);
common::assert_eq(long_path, short_path.normalize());
common::assert_eq(long_path, short_path.expand());
common::assert_eq(short_path, long_path.shorten());
assert_ne!(
long_path,
short_path
Expand All @@ -158,6 +159,7 @@ fn test_existing() {
let path = Path::new(path);
assert!(path.metadata().is_ok());
common::assert_eq(path, path.normalize());
common::assert_eq(path, path.expand());
}

test(r"C:\Documents and Settings");
Expand Down

0 comments on commit 8164f5a

Please sign in to comment.