use crate::error::StrictPathError;
use crate::path::strict_path::StrictPath;
use crate::validator::path_history::*;
use crate::Result;
use std::io::{Error as IoError, ErrorKind};
use std::marker::PhantomData;
use std::path::Path;
use std::sync::Arc;
pub(crate) fn canonicalize_and_enforce_restriction_boundary<Marker>(
path: impl AsRef<Path>,
restriction: &PathBoundary<Marker>,
) -> Result<StrictPath<Marker>> {
let target_path = if path.as_ref().is_absolute() {
path.as_ref().to_path_buf()
} else {
restriction.path().join(path.as_ref())
};
let canonicalized = PathHistory::<Raw>::new(target_path).canonicalize()?;
let validated_path = canonicalized.boundary_check(&restriction.path)?;
Ok(StrictPath::new(
Arc::new(restriction.clone()),
validated_path,
))
}
#[must_use = "a PathBoundary is validated and ready to enforce path restrictions — call .strict_join() to validate untrusted input, .into_strictpath() to get the boundary path, or pass to functions that accept &PathBoundary<Marker>"]
#[doc(alias = "jail")]
#[doc(alias = "chroot")]
#[doc(alias = "sandbox")]
#[doc(alias = "sanitize")]
#[doc(alias = "boundary")]
pub struct PathBoundary<Marker = ()> {
path: Arc<PathHistory<((Raw, Canonicalized), Exists)>>,
_marker: PhantomData<Marker>,
}
impl<Marker> Clone for PathBoundary<Marker> {
fn clone(&self) -> Self {
Self {
path: self.path.clone(),
_marker: PhantomData,
}
}
}
impl<Marker> Eq for PathBoundary<Marker> {}
impl<M1, M2> PartialEq<PathBoundary<M2>> for PathBoundary<M1> {
#[inline]
fn eq(&self, other: &PathBoundary<M2>) -> bool {
self.path() == other.path()
}
}
impl<Marker> std::hash::Hash for PathBoundary<Marker> {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.path().hash(state);
}
}
impl<Marker> PartialOrd for PathBoundary<Marker> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<Marker> Ord for PathBoundary<Marker> {
#[inline]
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.path().cmp(other.path())
}
}
#[cfg(feature = "virtual-path")]
impl<M1, M2> PartialEq<crate::validator::virtual_root::VirtualRoot<M2>> for PathBoundary<M1> {
#[inline]
fn eq(&self, other: &crate::validator::virtual_root::VirtualRoot<M2>) -> bool {
self.path() == other.path()
}
}
impl<Marker> PartialEq<Path> for PathBoundary<Marker> {
#[inline]
fn eq(&self, other: &Path) -> bool {
self.path() == other
}
}
impl<Marker> PartialEq<std::path::PathBuf> for PathBoundary<Marker> {
#[inline]
fn eq(&self, other: &std::path::PathBuf) -> bool {
self.eq(other.as_path())
}
}
impl<Marker> PartialEq<&std::path::Path> for PathBoundary<Marker> {
#[inline]
fn eq(&self, other: &&std::path::Path) -> bool {
self.eq(*other)
}
}
impl<Marker> PathBoundary<Marker> {
#[must_use = "this returns a Result containing the validated PathBoundary — handle the Result to detect invalid boundary directories"]
#[inline]
pub fn try_new<P: AsRef<Path>>(restriction_path: P) -> Result<Self> {
let restriction_path = restriction_path.as_ref();
let raw = PathHistory::<Raw>::new(restriction_path);
let canonicalized = raw.canonicalize()?;
let verified_exists = match canonicalized.verify_exists() {
Some(path) => path,
None => {
let io = IoError::new(
ErrorKind::NotFound,
"The specified PathBoundary path does not exist.",
);
return Err(StrictPathError::invalid_restriction(
restriction_path.to_path_buf(),
io,
));
}
};
if !verified_exists.is_dir() {
let error = IoError::new(
ErrorKind::InvalidInput,
"The specified PathBoundary path exists but is not a directory.",
);
return Err(StrictPathError::invalid_restriction(
restriction_path.to_path_buf(),
error,
));
}
Ok(Self {
path: Arc::new(verified_exists),
_marker: PhantomData,
})
}
#[must_use = "this returns a Result containing the validated PathBoundary — handle the Result to detect invalid boundary directories"]
pub fn try_new_create<P: AsRef<Path>>(boundary_dir: P) -> Result<Self> {
let boundary_path = boundary_dir.as_ref();
if !boundary_path.exists() {
std::fs::create_dir_all(boundary_path).map_err(|e| {
StrictPathError::invalid_restriction(boundary_path.to_path_buf(), e)
})?;
}
Self::try_new(boundary_path)
}
#[must_use = "strict_join() validates untrusted input against the boundary — always handle the Result to detect path traversal attacks"]
#[inline]
pub fn strict_join(&self, candidate_path: impl AsRef<Path>) -> Result<StrictPath<Marker>> {
canonicalize_and_enforce_restriction_boundary(candidate_path, self)
}
#[must_use = "change_marker() consumes self — the original PathBoundary is moved; use the returned PathBoundary<NewMarker>"]
#[inline]
pub fn change_marker<NewMarker>(self) -> PathBoundary<NewMarker> {
PathBoundary {
path: self.path,
_marker: PhantomData,
}
}
#[must_use = "into_strictpath() consumes the PathBoundary — use the returned StrictPath for I/O operations"]
#[inline]
pub fn into_strictpath(self) -> Result<StrictPath<Marker>> {
let root_history = self.path.clone();
let validated = PathHistory::<Raw>::new(root_history.as_ref().to_path_buf())
.canonicalize()?
.boundary_check(root_history.as_ref())?;
Ok(StrictPath::new(Arc::new(self), validated))
}
#[inline]
pub(crate) fn path(&self) -> &Path {
self.path.as_ref()
}
#[cfg(feature = "virtual-path")]
#[inline]
pub(crate) fn stated_path(&self) -> &PathHistory<((Raw, Canonicalized), Exists)> {
&self.path
}
#[must_use]
#[inline]
pub fn exists(&self) -> bool {
self.path.exists()
}
#[must_use = "pass interop_path() directly to third-party APIs requiring AsRef<Path> — never wrap it in Path::new() or PathBuf::from() as that defeats boundary safety"]
#[inline]
pub fn interop_path(&self) -> &std::ffi::OsStr {
self.path.as_os_str()
}
#[must_use = "strictpath_display() shows the real system path (admin/debug use) — for user-facing output prefer VirtualPath::virtualpath_display() which hides internal paths"]
#[inline]
pub fn strictpath_display(&self) -> std::path::Display<'_> {
self.path().display()
}
#[inline]
pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
std::fs::metadata(self.path())
}
pub fn strict_symlink<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
let root = self
.clone()
.into_strictpath()
.map_err(std::io::Error::other)?;
root.strict_symlink(link_path)
}
pub fn strict_hard_link<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
let root = self
.clone()
.into_strictpath()
.map_err(std::io::Error::other)?;
root.strict_hard_link(link_path)
}
#[cfg(all(windows, feature = "junctions"))]
pub fn strict_junction<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
let root = self
.clone()
.into_strictpath()
.map_err(std::io::Error::other)?;
root.strict_junction(link_path)
}
#[inline]
pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
std::fs::read_dir(self.path())
}
#[inline]
pub fn strict_read_dir(&self) -> std::io::Result<BoundaryReadDir<'_, Marker>> {
Ok(BoundaryReadDir {
inner: std::fs::read_dir(self.path())?,
boundary: self,
})
}
#[inline]
pub fn remove_dir(&self) -> std::io::Result<()> {
std::fs::remove_dir(self.path())
}
#[inline]
pub fn remove_dir_all(&self) -> std::io::Result<()> {
std::fs::remove_dir_all(self.path())
}
#[must_use = "virtualize() consumes self — use the returned VirtualRoot for virtual path operations (.virtual_join(), .into_virtualpath())"]
#[cfg(feature = "virtual-path")]
#[inline]
pub fn virtualize(self) -> crate::VirtualRoot<Marker> {
crate::VirtualRoot {
root: self,
_marker: PhantomData,
}
}
}
impl<Marker> std::fmt::Debug for PathBoundary<Marker> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PathBoundary")
.field("path", &self.path.as_ref())
.field("marker", &std::any::type_name::<Marker>())
.finish()
}
}
impl<Marker> std::str::FromStr for PathBoundary<Marker> {
type Err = crate::StrictPathError;
#[inline]
fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
Self::try_new_create(path)
}
}
pub struct BoundaryReadDir<'a, Marker> {
inner: std::fs::ReadDir,
boundary: &'a PathBoundary<Marker>,
}
impl<Marker> std::fmt::Debug for BoundaryReadDir<'_, Marker> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BoundaryReadDir")
.field("boundary", &self.boundary.strictpath_display())
.finish_non_exhaustive()
}
}
impl<Marker: Clone> Iterator for BoundaryReadDir<'_, Marker> {
type Item = std::io::Result<crate::StrictPath<Marker>>;
fn next(&mut self) -> Option<Self::Item> {
match self.inner.next()? {
Ok(entry) => {
let file_name = entry.file_name();
match self.boundary.strict_join(file_name) {
Ok(strict_path) => Some(Ok(strict_path)),
Err(e) => Some(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e))),
}
}
Err(e) => Some(Err(e)),
}
}
}