use std::path::{Component, Path, PathBuf};
use super::FsAccess;
pub(super) fn normalize_for_policy(path: &Path) -> PathBuf {
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
crate::stdlib::process::execution_root_path().join(path)
};
let absolute = normalize_lexically(&absolute);
if let Ok(canonical) = absolute.canonicalize() {
return normalize_os_path_alias(&canonical);
}
let mut existing = absolute.as_path();
let mut suffix = Vec::new();
while !existing.exists() {
let Some(parent) = existing.parent() else {
return normalize_os_path_alias(&normalize_lexically(&absolute));
};
if let Some(name) = existing.file_name() {
suffix.push(name.to_os_string());
}
existing = parent;
}
let mut normalized = existing
.canonicalize()
.unwrap_or_else(|_| normalize_lexically(existing));
for component in suffix.iter().rev() {
normalized.push(component);
}
normalize_os_path_alias(&normalize_lexically(&normalized))
}
#[cfg(target_os = "macos")]
fn normalize_os_path_alias(path: &Path) -> PathBuf {
for alias in [Path::new("/tmp"), Path::new("/var")] {
if let Ok(relative) = path.strip_prefix(alias) {
return Path::new("/private")
.join(alias.strip_prefix("/").unwrap())
.join(relative);
}
}
path.to_path_buf()
}
#[cfg(not(target_os = "macos"))]
fn normalize_os_path_alias(path: &Path) -> PathBuf {
path.to_path_buf()
}
fn normalize_lexically(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
normalized.pop();
}
other => normalized.push(other.as_os_str()),
}
}
normalized
}
pub(super) fn path_is_within(path: &Path, root: &Path) -> bool {
path == root || path.starts_with(root)
}
pub(super) fn normalize_io_device_path(path: &Path) -> PathBuf {
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
crate::stdlib::process::execution_root_path().join(path)
};
normalize_lexically(&absolute)
}
pub(super) fn is_standard_io_device_for_access(path: &Path, access: FsAccess) -> bool {
match access {
FsAccess::Read => {
matches!(
path.to_str(),
Some("/dev/stdin" | "/dev/stdout" | "/dev/stderr" | "/dev/null")
) || is_dev_fd_descriptor(path)
}
FsAccess::Write => {
matches!(
path.to_str(),
Some("/dev/stdout" | "/dev/stderr" | "/dev/null")
) || is_dev_fd_descriptor(path)
}
FsAccess::Delete => false,
}
}
fn is_dev_fd_descriptor(path: &Path) -> bool {
let Some(text) = path.to_str() else {
return false;
};
let Some(fd) = text.strip_prefix("/dev/fd/") else {
return false;
};
!fd.is_empty() && fd.bytes().all(|byte| byte.is_ascii_digit())
}
#[cfg(target_os = "macos")]
#[cfg(test)]
mod tests {
use super::normalize_for_policy;
use std::path::{Path, PathBuf};
#[test]
fn policy_paths_use_sandbox_exec_canonical_aliases() {
assert_eq!(
normalize_for_policy(Path::new("/tmp")),
PathBuf::from("/private/tmp")
);
assert_eq!(
normalize_for_policy(Path::new("/var/folders")),
PathBuf::from("/private/var/folders")
);
}
}