mod display;
mod fs;
mod iter;
mod links;
mod traits;
pub use display::VirtualPathDisplay;
pub use iter::VirtualReadDir;
use crate::error::StrictPathError;
use crate::path::strict_path::StrictPath;
use crate::validator::path_history::{Canonicalized, PathHistory};
use crate::PathBoundary;
use crate::Result;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
#[derive(Clone)]
#[must_use = "a VirtualPath is boundary-validated and user-facing — use .virtualpath_display() for safe user output, .virtual_join() to compose paths, or .as_unvirtual() for system-facing I/O"]
#[doc(alias = "jailed_path")]
#[doc(alias = "sandboxed_path")]
#[doc(alias = "contained_path")]
pub struct VirtualPath<Marker = ()> {
pub(crate) inner: StrictPath<Marker>,
pub(crate) virtual_path: PathBuf,
}
fn sanitize_display_component(component: &str) -> String {
let mut out = String::with_capacity(component.len());
for ch in component.chars() {
let cp = ch as u32;
let needs_replace = ch == ';'
|| cp < 0x20 || cp == 0x7F || (0x80..=0x9F).contains(&cp) || cp == 0x2028 || cp == 0x2029 || cp == 0x200E || cp == 0x200F || (0x202A..=0x202E).contains(&cp) || (0x2066..=0x2069).contains(&cp); if needs_replace {
out.push('_');
} else {
out.push(ch);
}
}
out
}
#[inline]
fn clamp<Marker, H>(
restriction: &PathBoundary<Marker>,
anchored: PathHistory<(H, Canonicalized)>,
) -> crate::Result<crate::path::strict_path::StrictPath<Marker>> {
restriction.strict_join(anchored.into_inner())
}
impl<Marker> VirtualPath<Marker> {
#[must_use = "this returns a Result containing the validated VirtualPath — handle the Result to detect invalid roots"]
pub fn with_root<P: AsRef<Path>>(root: P) -> Result<Self> {
let vroot = crate::validator::virtual_root::VirtualRoot::try_new(root)?;
vroot.into_virtualpath()
}
#[must_use = "this returns a Result containing the validated VirtualPath — handle the Result to detect invalid roots"]
pub fn with_root_create<P: AsRef<Path>>(root: P) -> Result<Self> {
let vroot = crate::validator::virtual_root::VirtualRoot::try_new_create(root)?;
vroot.into_virtualpath()
}
#[inline]
pub(crate) fn new(strict_path: StrictPath<Marker>) -> Self {
fn compute_virtual<Marker>(
system_path: &std::path::Path,
restriction: &crate::PathBoundary<Marker>,
) -> std::path::PathBuf {
use std::path::Component;
#[cfg(windows)]
fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
dunce::simplified(p).to_path_buf()
}
#[cfg(not(windows))]
fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
p.to_path_buf()
}
let system_norm = strip_verbatim(system_path);
let jail_norm = strip_verbatim(restriction.path());
if let Ok(stripped) = system_norm.strip_prefix(&jail_norm) {
let mut cleaned = std::path::PathBuf::new();
for comp in stripped.components() {
if let Component::Normal(name) = comp {
cleaned.push(name);
}
}
return cleaned;
}
let mut strictpath_comps: Vec<_> = system_norm
.components()
.filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
.collect();
let mut boundary_comps: Vec<_> = jail_norm
.components()
.filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
.collect();
#[cfg(windows)]
fn comp_eq(a: &Component, b: &Component) -> bool {
match (a, b) {
(Component::Normal(x), Component::Normal(y)) => {
x.to_string_lossy().to_ascii_lowercase()
== y.to_string_lossy().to_ascii_lowercase()
}
_ => false,
}
}
#[cfg(not(windows))]
fn comp_eq(a: &Component, b: &Component) -> bool {
a == b
}
while !strictpath_comps.is_empty()
&& !boundary_comps.is_empty()
&& comp_eq(&strictpath_comps[0], &boundary_comps[0])
{
strictpath_comps.remove(0);
boundary_comps.remove(0);
}
let mut vb = std::path::PathBuf::new();
for c in strictpath_comps {
if let Component::Normal(name) = c {
vb.push(name);
}
}
vb
}
let virtual_path = compute_virtual(strict_path.path(), strict_path.boundary());
Self {
inner: strict_path,
virtual_path,
}
}
#[must_use = "unvirtual() consumes self — use the returned StrictPath for system-facing I/O, or prefer .as_unvirtual() to borrow without consuming"]
#[inline]
pub fn unvirtual(self) -> StrictPath<Marker> {
self.inner
}
#[must_use = "change_marker() consumes self — the original VirtualPath is moved; use the returned VirtualPath<NewMarker>"]
#[inline]
pub fn change_marker<NewMarker>(self) -> VirtualPath<NewMarker> {
let VirtualPath {
inner,
virtual_path,
} = self;
VirtualPath {
inner: inner.change_marker(),
virtual_path,
}
}
#[must_use = "try_into_root() consumes self — use the returned VirtualRoot to restrict future path operations"]
#[inline]
pub fn try_into_root(self) -> Result<crate::validator::virtual_root::VirtualRoot<Marker>> {
Ok(self.inner.try_into_boundary()?.virtualize())
}
#[must_use = "try_into_root_create() consumes self — use the returned VirtualRoot to restrict future path operations"]
#[inline]
pub fn try_into_root_create(
self,
) -> Result<crate::validator::virtual_root::VirtualRoot<Marker>> {
let strict_path = self.inner;
let validated_dir = strict_path.try_into_boundary_create()?;
Ok(validated_dir.virtualize())
}
#[must_use = "as_unvirtual() borrows the system-facing StrictPath — use it for system I/O or pass to functions accepting &StrictPath<Marker>"]
#[inline]
pub fn as_unvirtual(&self) -> &StrictPath<Marker> {
&self.inner
}
#[must_use = "pass interop_path() directly to third-party APIs requiring AsRef<Path> — never wrap it in Path::new() or PathBuf::from(); NEVER expose this in user-facing output (use .virtualpath_display() instead)"]
#[inline]
pub fn interop_path(&self) -> &std::ffi::OsStr {
self.inner.interop_path()
}
#[must_use = "virtual_join() validates untrusted input against the virtual root — always handle the Result to detect escape attempts"]
#[inline]
pub fn virtual_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
let candidate = self.virtual_path.join(path.as_ref());
let anchored = crate::validator::path_history::PathHistory::new(candidate)
.canonicalize_anchored(self.inner.boundary())?;
let boundary_path = clamp(self.inner.boundary(), anchored)?;
Ok(VirtualPath::new(boundary_path))
}
#[must_use = "returns a Result<Option> — handle both the error case and the None (at virtual root) case"]
pub fn virtualpath_parent(&self) -> Result<Option<Self>> {
match self.virtual_path.parent() {
Some(parent_virtual_path) => {
let anchored = crate::validator::path_history::PathHistory::new(
parent_virtual_path.to_path_buf(),
)
.canonicalize_anchored(self.inner.boundary())?;
let validated_path = clamp(self.inner.boundary(), anchored)?;
Ok(Some(VirtualPath::new(validated_path)))
}
None => Ok(None),
}
}
#[must_use = "returns a new validated VirtualPath with the file name replaced — the original is unchanged; handle the Result to detect boundary escapes"]
#[inline]
pub fn virtualpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
let candidate = self.virtual_path.with_file_name(file_name);
let anchored = crate::validator::path_history::PathHistory::new(candidate)
.canonicalize_anchored(self.inner.boundary())?;
let validated_path = clamp(self.inner.boundary(), anchored)?;
Ok(VirtualPath::new(validated_path))
}
#[must_use = "returns a new validated VirtualPath with the extension changed — the original is unchanged; handle the Result to detect boundary escapes"]
pub fn virtualpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
if self.virtual_path.file_name().is_none() {
return Err(StrictPathError::path_escapes_boundary(
self.virtual_path.clone(),
self.inner.boundary().path().to_path_buf(),
));
}
let candidate =
crate::path::with_validated_extension(&self.virtual_path, extension.as_ref())?;
let anchored = crate::validator::path_history::PathHistory::new(candidate)
.canonicalize_anchored(self.inner.boundary())?;
let validated_path = clamp(self.inner.boundary(), anchored)?;
Ok(VirtualPath::new(validated_path))
}
#[must_use]
#[inline]
pub fn virtualpath_file_name(&self) -> Option<&OsStr> {
self.virtual_path.file_name()
}
#[must_use]
#[inline]
pub fn virtualpath_file_stem(&self) -> Option<&OsStr> {
self.virtual_path.file_stem()
}
#[must_use]
#[inline]
pub fn virtualpath_extension(&self) -> Option<&OsStr> {
self.virtual_path.extension()
}
#[must_use]
#[inline]
pub fn virtualpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
self.virtual_path.starts_with(p)
}
#[must_use]
#[inline]
pub fn virtualpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
self.virtual_path.ends_with(p)
}
#[must_use = "virtualpath_display() returns a safe user-facing path representation — use this (not interop_path()) in API responses, logs, and UI"]
#[inline]
pub fn virtualpath_display(&self) -> VirtualPathDisplay<'_, Marker> {
VirtualPathDisplay(self)
}
#[must_use]
#[inline]
pub fn exists(&self) -> bool {
self.inner.exists()
}
#[must_use]
#[inline]
pub fn is_file(&self) -> bool {
self.inner.is_file()
}
#[must_use]
#[inline]
pub fn is_dir(&self) -> bool {
self.inner.is_dir()
}
#[inline]
pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
self.inner.metadata()
}
#[inline]
pub fn read_to_string(&self) -> std::io::Result<String> {
self.inner.read_to_string()
}
#[inline]
pub fn read(&self) -> std::io::Result<Vec<u8>> {
self.inner.read()
}
#[inline]
pub fn symlink_metadata(&self) -> std::io::Result<std::fs::Metadata> {
self.inner.symlink_metadata()
}
#[inline]
pub fn set_permissions(&self, perm: std::fs::Permissions) -> std::io::Result<()> {
self.inner.set_permissions(perm)
}
#[inline]
pub fn try_exists(&self) -> std::io::Result<bool> {
self.inner.try_exists()
}
pub fn touch(&self) -> std::io::Result<()> {
self.inner.touch()
}
pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
self.inner.read_dir()
}
pub fn virtual_read_dir(&self) -> std::io::Result<VirtualReadDir<'_, Marker>> {
let inner = std::fs::read_dir(self.inner.path())?;
Ok(VirtualReadDir {
inner,
parent: self,
})
}
}