Skip to content

Commit

Permalink
Implement push_checked for Pathbuf and Utf8PathBuf; add join_checked …
Browse files Browse the repository at this point in the history
…to Path and Utf8Path
  • Loading branch information
chipsenkbeil committed Feb 24, 2024
1 parent 0288e69 commit b2db69f
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 5 deletions.
33 changes: 32 additions & 1 deletion src/common/non_utf8/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ use core::{cmp, fmt};
pub use display::Display;

use crate::no_std_compat::*;
use crate::{Ancestors, Component, Components, Encoding, Iter, PathBuf, StripPrefixError};
use crate::{
Ancestors, CheckedPathError, Component, Components, Encoding, Iter, PathBuf, StripPrefixError,
};

/// A slice of a path (akin to [`str`]).
///
Expand Down Expand Up @@ -666,6 +668,35 @@ where
buf
}

/// Creates an owned [`PathBuf`] with `path` adjoined to `self`, checking the `path` to ensure
/// it is safe to join. _When dealing with user-provided paths, this is the preferred method._
///
/// See [`PathBuf::push_checked`] for more details on what it means to adjoin a path safely.
///
/// # Examples
///
/// ```
/// use typed_path::{CheckedPathError, Path, PathBuf, UnixEncoding};
///
/// // NOTE: A path cannot be created on its own without a defined encoding
/// let path = Path::<UnixEncoding>::new("/etc");
///
/// // A valid path can be joined onto the existing one
/// assert_eq!(path.join_checked("passwd"), Ok(PathBuf::from("/etc/passwd")));
///
/// // An invalid path will result in an error
/// assert_eq!(path.join_checked("/sneaky/replacement"), Err(CheckedPathError::UnexpectedRoot));
/// ```
pub fn join_checked<P: AsRef<Path<T>>>(&self, path: P) -> Result<PathBuf<T>, CheckedPathError> {
self._join_checked(path.as_ref())
}

fn _join_checked(&self, path: &Path<T>) -> Result<PathBuf<T>, CheckedPathError> {
let mut buf = self.to_path_buf();
buf.push_checked(path)?;
Ok(buf)
}

/// Creates an owned [`PathBuf`] like `self` but with the given file name.
///
/// See [`PathBuf::set_file_name`] for more details.
Expand Down
59 changes: 58 additions & 1 deletion src/common/non_utf8/pathbuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use core::str::FromStr;
use core::{cmp, fmt};

use crate::no_std_compat::*;
use crate::{Encoding, Iter, Path};
use crate::{CheckedPathError, Encoding, Iter, Path};

/// An owned, mutable path that mirrors [`std::path::PathBuf`], but operatings using an
/// [`Encoding`] to determine how to parse the underlying bytes.
Expand Down Expand Up @@ -175,6 +175,63 @@ where
T::push(&mut self.inner, path.as_ref().as_bytes());
}

/// Like [`PathBuf::push`], extends `self` with `path`, but also checks to ensure that `path`
/// abides by a set of rules.
///
/// # Rules
///
/// 1. `path` cannot contain a prefix component.
/// 2. `path` cannot contain a root component.
/// 3. `path` cannot contain invalid filename bytes.
/// 4. `path` cannot contain parent components such that the current path would be escaped.
///
/// # Examples
///
/// Pushing a relative path extends the existing path:
///
/// ```
/// use typed_path::{PathBuf, UnixEncoding};
///
/// // NOTE: A pathbuf cannot be created on its own without a defined encoding
/// let mut path = PathBuf::<UnixEncoding>::from("/tmp");
///
/// // Pushing a relative path works like normal
/// assert!(path.push_checked("file.bk").is_ok());
/// assert_eq!(path, PathBuf::from("/tmp/file.bk"));
/// ```
///
/// Pushing a relative path that contains unresolved parent directory references fails
/// with an error:
///
/// ```
/// use typed_path::{CheckedPathError, PathBuf, UnixEncoding};
///
/// // NOTE: A pathbuf cannot be created on its own without a defined encoding
/// let mut path = PathBuf::<UnixEncoding>::from("/tmp");
///
/// // Pushing a relative path that contains parent directory references that cannot be
/// // resolved within the path is considered an error as this is considered a path
/// // traversal attack!
/// assert_eq!(path.push_checked(".."), Err(CheckedPathError::PathTraversalAttack));
/// assert_eq!(path, PathBuf::from("/tmp"));
/// ```
///
/// Pushing an absolute path fails with an error:
///
/// ```
/// use typed_path::{CheckedPathError, PathBuf, UnixEncoding};
///
/// // NOTE: A pathbuf cannot be created on its own without a defined encoding
/// let mut path = PathBuf::<UnixEncoding>::from("/tmp");
///
/// // Pushing an absolute path will fail with an error
/// assert_eq!(path.push_checked("/etc"), Err(CheckedPathError::UnexpectedRoot));
/// assert_eq!(path, PathBuf::from("/tmp"));
/// ```
pub fn push_checked<P: AsRef<Path<T>>>(&mut self, path: P) -> Result<(), CheckedPathError> {
T::push_checked(&mut self.inner, path.as_ref().as_bytes())
}

/// Truncates `self` to [`self.parent`].
///
/// Returns `false` and does nothing if [`self.parent`] is [`None`].
Expand Down
38 changes: 36 additions & 2 deletions src/common/utf8/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use core::{cmp, fmt};

use crate::no_std_compat::*;
use crate::{
Encoding, Path, StripPrefixError, Utf8Ancestors, Utf8Component, Utf8Components, Utf8Encoding,
Utf8Iter, Utf8PathBuf,
CheckedPathError, Encoding, Path, StripPrefixError, Utf8Ancestors, Utf8Component,
Utf8Components, Utf8Encoding, Utf8Iter, Utf8PathBuf,
};

/// A slice of a path (akin to [`str`]).
Expand Down Expand Up @@ -618,6 +618,40 @@ where
buf
}

/// Creates an owned [`Utf8PathBuf`] with `path` adjoined to `self`, checking the `path` to
/// ensure it is safe to join. _When dealing with user-provided paths, this is the preferred
/// method._
///
/// See [`Utf8PathBuf::push_checked`] for more details on what it means to adjoin a path
/// safely.
///
/// # Examples
///
/// ```
/// use typed_path::{CheckedPathError, Utf8Path, Utf8PathBuf, Utf8UnixEncoding};
///
/// // NOTE: A path cannot be created on its own without a defined encoding
/// let path = Utf8Path::<Utf8UnixEncoding>::new("/etc");
///
/// // A valid path can be joined onto the existing one
/// assert_eq!(path.join_checked("passwd"), Ok(Utf8PathBuf::from("/etc/passwd")));
///
/// // An invalid path will result in an error
/// assert_eq!(path.join_checked("/sneaky/replacement"), Err(CheckedPathError::UnexpectedRoot));
/// ```
pub fn join_checked<P: AsRef<Utf8Path<T>>>(
&self,
path: P,
) -> Result<Utf8PathBuf<T>, CheckedPathError> {
self._join_checked(path.as_ref())
}

fn _join_checked(&self, path: &Utf8Path<T>) -> Result<Utf8PathBuf<T>, CheckedPathError> {
let mut buf = self.to_path_buf();
buf.push_checked(path)?;
Ok(buf)
}

/// Creates an owned [`Utf8PathBuf`] like `self` but with the given file name.
///
/// See [`Utf8PathBuf::set_file_name`] for more details.
Expand Down
59 changes: 58 additions & 1 deletion src/common/utf8/pathbuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use core::str::FromStr;
use core::{cmp, fmt};

use crate::no_std_compat::*;
use crate::{Encoding, PathBuf, Utf8Encoding, Utf8Iter, Utf8Path};
use crate::{CheckedPathError, Encoding, PathBuf, Utf8Encoding, Utf8Iter, Utf8Path};

/// An owned, mutable path that mirrors [`std::path::PathBuf`], but operatings using a
/// [`Utf8Encoding`] to determine how to parse the underlying str.
Expand Down Expand Up @@ -180,6 +180,63 @@ where
T::push(&mut self.inner, path.as_ref().as_str());
}

/// Like [`Utf8PathBuf::push`], extends `self` with `path`, but also checks to ensure that
/// `path` abides by a set of rules.
///
/// # Rules
///
/// 1. `path` cannot contain a prefix component.
/// 2. `path` cannot contain a root component.
/// 3. `path` cannot contain invalid filename bytes.
/// 4. `path` cannot contain parent components such that the current path would be escaped.
///
/// # Examples
///
/// Pushing a relative path extends the existing path:
///
/// ```
/// use typed_path::{Utf8PathBuf, Utf8UnixEncoding};
///
/// // NOTE: A pathbuf cannot be created on its own without a defined encoding
/// let mut path = Utf8PathBuf::<Utf8UnixEncoding>::from("/tmp");
///
/// // Pushing a relative path works like normal
/// assert!(path.push_checked("file.bk").is_ok());
/// assert_eq!(path, Utf8PathBuf::from("/tmp/file.bk"));
/// ```
///
/// Pushing a relative path that contains unresolved parent directory references fails
/// with an error:
///
/// ```
/// use typed_path::{CheckedPathError, Utf8PathBuf, Utf8UnixEncoding};
///
/// // NOTE: A pathbuf cannot be created on its own without a defined encoding
/// let mut path = Utf8PathBuf::<Utf8UnixEncoding>::from("/tmp");
///
/// // Pushing a relative path that contains parent directory references that cannot be
/// // resolved within the path is considered an error as this is considered a path
/// // traversal attack!
/// assert_eq!(path.push_checked(".."), Err(CheckedPathError::PathTraversalAttack));
/// assert_eq!(path, Utf8PathBuf::from("/tmp"));
/// ```
///
/// Pushing an absolute path fails with an error:
///
/// ```
/// use typed_path::{CheckedPathError, Utf8PathBuf, Utf8UnixEncoding};
///
/// // NOTE: A pathbuf cannot be created on its own without a defined encoding
/// let mut path = Utf8PathBuf::<Utf8UnixEncoding>::from("/tmp");
///
/// // Pushing an absolute path will fail with an error
/// assert_eq!(path.push_checked("/etc"), Err(CheckedPathError::UnexpectedRoot));
/// assert_eq!(path, Utf8PathBuf::from("/tmp"));
/// ```
pub fn push_checked<P: AsRef<Utf8Path<T>>>(&mut self, path: P) -> Result<(), CheckedPathError> {
T::push_checked(&mut self.inner, path.as_ref().as_str())
}

/// Truncates `self` to [`self.parent`].
///
/// Returns `false` and does nothing if [`self.parent`] is [`None`].
Expand Down

0 comments on commit b2db69f

Please sign in to comment.