use crate::error::StrictPathError;
use crate::path::strict_path::StrictPath;
use crate::validator::path_history::*;
use crate::Result;
#[cfg(windows)]
use std::ffi::OsStr;
use std::io::{Error as IoError, ErrorKind};
use std::marker::PhantomData;
use std::path::Path;
use std::sync::Arc;
#[cfg(feature = "tempfile")]
use tempfile::TempDir;
#[cfg(windows)]
use std::path::Component;
#[cfg(windows)]
fn is_potential_83_short_name(os: &OsStr) -> bool {
let s = os.to_string_lossy();
if let Some(pos) = s.find('~') {
s[pos + 1..]
.chars()
.next()
.is_some_and(|ch| ch.is_ascii_digit())
} else {
false
}
}
pub(crate) fn canonicalize_and_enforce_restriction_boundary<Marker>(
path: impl AsRef<Path>,
restriction: &PathBoundary<Marker>,
) -> Result<StrictPath<Marker>> {
#[cfg(windows)]
{
let original_user_path = path.as_ref().to_path_buf();
if !path.as_ref().is_absolute() {
let mut probe = restriction.path().to_path_buf();
for comp in path.as_ref().components() {
match comp {
Component::CurDir | Component::ParentDir => continue,
Component::RootDir | Component::Prefix(_) => continue,
Component::Normal(name) => {
if is_potential_83_short_name(name) {
return Err(StrictPathError::windows_short_name(
name.to_os_string(),
original_user_path,
probe.clone(),
));
}
probe.push(name);
}
}
}
}
}
let target_path = if path.as_ref().is_absolute() {
path.as_ref().to_path_buf()
} else {
restriction.path().join(path.as_ref())
};
let validated_path = PathHistory::<Raw>::new(target_path)
.canonicalize()?
.boundary_check(&restriction.path)?;
Ok(StrictPath::new(
Arc::new(restriction.clone()),
validated_path,
))
}
pub struct PathBoundary<Marker = ()> {
path: Arc<PathHistory<((Raw, Canonicalized), Exists)>>,
#[cfg(feature = "tempfile")]
_temp_dir: Option<Arc<TempDir>>,
_marker: PhantomData<Marker>,
}
impl<Marker> Clone for PathBoundary<Marker> {
fn clone(&self) -> Self {
Self {
path: self.path.clone(),
#[cfg(feature = "tempfile")]
_temp_dir: self._temp_dir.clone(),
_marker: PhantomData,
}
}
}
impl<Marker> PartialEq for PathBoundary<Marker> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.path() == other.path()
}
}
impl<Marker> Eq for PathBoundary<Marker> {}
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())
}
}
impl<Marker> PartialEq<crate::validator::virtual_root::VirtualRoot<Marker>>
for PathBoundary<Marker>
{
#[inline]
fn eq(&self, other: &crate::validator::virtual_root::VirtualRoot<Marker>) -> 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> {
#[cfg(feature = "tempfile")]
fn new_with_temp_dir(
path: Arc<PathHistory<((Raw, Canonicalized), Exists)>>,
temp_dir: Option<Arc<TempDir>>,
) -> Self {
Self {
path,
_temp_dir: temp_dir,
_marker: PhantomData,
}
}
#[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,
));
}
#[cfg(feature = "tempfile")]
{
Ok(Self::new_with_temp_dir(Arc::new(verified_exists), None))
}
#[cfg(not(feature = "tempfile"))]
{
Ok(Self {
path: Arc::new(verified_exists),
_marker: PhantomData,
})
}
}
pub fn try_new_create<P: AsRef<Path>>(root: P) -> Result<Self> {
let root_path = root.as_ref();
if !root_path.exists() {
std::fs::create_dir_all(root_path)
.map_err(|e| StrictPathError::invalid_restriction(root_path.to_path_buf(), e))?;
}
Self::try_new(root_path)
}
#[inline]
pub fn strict_join(&self, candidate_path: impl AsRef<Path>) -> Result<StrictPath<Marker>> {
canonicalize_and_enforce_restriction_boundary(candidate_path, self)
}
#[inline]
pub(crate) fn path(&self) -> &Path {
self.path.as_ref()
}
#[inline]
pub(crate) fn stated_path(&self) -> &PathHistory<((Raw, Canonicalized), Exists)> {
&self.path
}
#[inline]
pub fn exists(&self) -> bool {
self.path.exists()
}
#[inline]
pub fn interop_path(&self) -> &std::ffi::OsStr {
self.path.as_os_str()
}
#[inline]
pub fn strictpath_display(&self) -> std::path::Display<'_> {
self.path().display()
}
#[cfg(feature = "tempfile")]
#[inline]
pub(crate) fn temp_dir_arc(&self) -> Option<Arc<TempDir>> {
self._temp_dir.clone()
}
#[inline]
pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
std::fs::metadata(self.path())
}
pub fn strict_symlink(
&self,
link_path: &crate::path::strict_path::StrictPath<Marker>,
) -> std::io::Result<()> {
let root = self
.strict_join("")
.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
root.strict_symlink(link_path)
}
pub fn strict_hard_link(
&self,
link_path: &crate::path::strict_path::StrictPath<Marker>,
) -> std::io::Result<()> {
let root = self
.strict_join("")
.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
root.strict_hard_link(link_path)
}
#[inline]
pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
std::fs::read_dir(self.path())
}
#[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())
}
#[inline]
pub fn virtualize(self) -> crate::VirtualRoot<Marker> {
crate::VirtualRoot {
root: self,
#[cfg(feature = "tempfile")]
_temp_dir: None,
_marker: PhantomData,
}
}
#[cfg(feature = "dirs")]
pub fn try_new_os_config(app_name: &str) -> Result<Self> {
let config_dir = dirs::config_dir()
.ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-config".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS config directory not available",
),
})?
.join(app_name);
Self::try_new_create(config_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_data(app_name: &str) -> Result<Self> {
let data_dir = dirs::data_dir()
.ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-data".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS data directory not available",
),
})?
.join(app_name);
Self::try_new_create(data_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_cache(app_name: &str) -> Result<Self> {
let cache_dir = dirs::cache_dir()
.ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-cache".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS cache directory not available",
),
})?
.join(app_name);
Self::try_new_create(cache_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_config_local(app_name: &str) -> Result<Self> {
let config_dir = dirs::config_local_dir()
.ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-config-local".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS local config directory not available",
),
})?
.join(app_name);
Self::try_new_create(config_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_data_local(app_name: &str) -> Result<Self> {
let data_dir = dirs::data_local_dir()
.ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-data-local".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS local data directory not available",
),
})?
.join(app_name);
Self::try_new_create(data_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_home() -> Result<Self> {
let home_dir =
dirs::home_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-home".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS home directory not available",
),
})?;
Self::try_new(home_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_desktop() -> Result<Self> {
let desktop_dir =
dirs::desktop_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-desktop".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS desktop directory not available",
),
})?;
Self::try_new(desktop_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_documents() -> Result<Self> {
let docs_dir =
dirs::document_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-documents".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS documents directory not available",
),
})?;
Self::try_new(docs_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_downloads() -> Result<Self> {
let downloads_dir =
dirs::download_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-downloads".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS downloads directory not available",
),
})?;
Self::try_new(downloads_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_pictures() -> Result<Self> {
let pictures_dir =
dirs::picture_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-pictures".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS pictures directory not available",
),
})?;
Self::try_new(pictures_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_audio() -> Result<Self> {
let audio_dir =
dirs::audio_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-audio".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS audio directory not available",
),
})?;
Self::try_new(audio_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_videos() -> Result<Self> {
let videos_dir =
dirs::video_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-videos".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS videos directory not available",
),
})?;
Self::try_new(videos_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_executables() -> Result<Self> {
let exec_dir =
dirs::executable_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-executables".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS executables directory not available on this platform",
),
})?;
Self::try_new(exec_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_runtime() -> Result<Self> {
let runtime_dir =
dirs::runtime_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-runtime".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS runtime directory not available on this platform",
),
})?;
Self::try_new(runtime_dir)
}
#[cfg(feature = "dirs")]
pub fn try_new_os_state(app_name: &str) -> Result<Self> {
let state_dir = dirs::state_dir()
.ok_or_else(|| crate::StrictPathError::InvalidRestriction {
restriction: "os-state".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"OS state directory not available on this platform",
),
})?
.join(app_name);
Self::try_new_create(state_dir)
}
#[cfg(feature = "tempfile")]
pub fn try_new_temp() -> Result<Self> {
let temp_dir =
tempfile::tempdir().map_err(|e| crate::StrictPathError::InvalidRestriction {
restriction: "temp".into(),
source: e,
})?;
let temp_path = temp_dir.path();
let raw = PathHistory::<Raw>::new(temp_path);
let canonicalized = raw.canonicalize()?;
let verified_exists = canonicalized.verify_exists().ok_or_else(|| {
crate::StrictPathError::InvalidRestriction {
restriction: "temp".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"Temp directory verification failed",
),
}
})?;
Ok(Self::new_with_temp_dir(
Arc::new(verified_exists),
Some(Arc::new(temp_dir)),
))
}
#[cfg(feature = "tempfile")]
pub fn try_new_temp_with_prefix(prefix: &str) -> Result<Self> {
let temp_dir = tempfile::Builder::new()
.prefix(prefix)
.tempdir()
.map_err(|e| crate::StrictPathError::InvalidRestriction {
restriction: "temp".into(),
source: e,
})?;
let temp_path = temp_dir.path();
let raw = PathHistory::<Raw>::new(temp_path);
let canonicalized = raw.canonicalize()?;
let verified_exists = canonicalized.verify_exists().ok_or_else(|| {
crate::StrictPathError::InvalidRestriction {
restriction: "temp".into(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"Temp directory verification failed",
),
}
})?;
Ok(Self::new_with_temp_dir(
Arc::new(verified_exists),
Some(Arc::new(temp_dir)),
))
}
#[cfg(feature = "app-path")]
pub fn try_new_app_path<P: AsRef<std::path::Path>>(
subdir: P,
env_override: Option<&str>,
) -> Result<Self> {
let subdir_path = subdir.as_ref();
let override_value: Option<String> = env_override.and_then(|key| std::env::var(key).ok());
let app_path = app_path::AppPath::try_with_override(subdir_path, override_value.as_deref())
.map_err(|e| crate::StrictPathError::InvalidRestriction {
restriction: format!("app-path: {}", subdir_path.display()).into(),
source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e),
})?;
Self::try_new_create(app_path)
}
#[cfg(feature = "app-path")]
pub fn try_new_app_path_with_env<P: AsRef<std::path::Path>>(
subdir: P,
env_override: &str,
) -> Result<Self> {
let subdir_path = subdir.as_ref();
Self::try_new_app_path(subdir_path, Some(env_override))
}
}
impl<Marker> AsRef<Path> for PathBoundary<Marker> {
#[inline]
fn as_ref(&self) -> &Path {
self.path.as_ref()
}
}
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: Default> 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)
}
}