use std::fs::File;
use std::io::Read;
use std::path::{Component, Path, PathBuf};
use nix::fcntl::{OFlag, OpenHow, ResolveFlag, openat2};
use nix::sys::stat::Mode;
fn resolve_against(base: &Path, path: &str) -> PathBuf {
let wanted = Path::new(path);
let absolute = if wanted.is_absolute() {
wanted.to_path_buf()
} else {
base.join(wanted)
};
let mut parts: Vec<Component> = Vec::new();
for component in absolute.components() {
match component {
Component::CurDir => {}
Component::ParentDir => match parts.last() {
Some(Component::RootDir) | None => {}
Some(Component::Normal(_)) => {
parts.pop();
}
Some(_) => parts.push(component),
},
other => parts.push(other),
}
}
let mut resolved = PathBuf::new();
for part in parts {
resolved.push(part.as_os_str());
}
resolved
}
fn normalize(path: &str) -> String {
let mut parts: Vec<Component> = Vec::new();
for component in Path::new(path).components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if matches!(parts.last(), Some(Component::Normal(_))) {
parts.pop();
} else if !parts.is_empty() || component != Component::RootDir {
parts.push(component);
}
}
other => parts.push(other),
}
}
let mut normalized = PathBuf::new();
for part in parts {
normalized.push(part.as_os_str());
}
normalized.to_string_lossy().into_owned()
}
pub fn within(root: &str, wanted: &str) -> Option<String> {
let base = super::resolve_root(root);
let target = resolve_against(Path::new(&base), &normalize(wanted));
target
.starts_with(&base)
.then(|| target.to_string_lossy().into_owned())
}
pub fn host_path_under(workspace: &str, project_path: &str, requested: &str) -> Option<String> {
let trimmed = requested.trim();
if trimmed.is_empty() {
return None;
}
let stripped = trimmed.strip_prefix(workspace).unwrap_or(trimmed);
let relative = stripped.trim_start_matches('/');
let relative = if relative.is_empty() { "." } else { relative };
within(project_path, relative)
}
pub fn open_beneath(root: &str, relative: &str, options: &OpenOptions) -> std::io::Result<File> {
let root_dir = File::open(root)?;
let wanted = normalize(relative);
let wanted = wanted.trim_start_matches('/');
let wanted = if wanted.is_empty() { "." } else { wanted };
let mut how = OpenHow::new()
.flags(OFlag::from_bits_truncate(options.flags))
.resolve(ResolveFlag::RESOLVE_BENEATH | ResolveFlag::RESOLVE_NO_SYMLINKS);
if let Some(mode) = options.mode {
how = how.mode(mode);
}
match openat2(&root_dir, wanted, how) {
Ok(opened) => Ok(File::from(opened)),
Err(errno) => Err(std::io::Error::from_raw_os_error(errno as i32)),
}
}
pub fn containment_is_enforced() -> bool {
!matches!(
open_beneath("/", ".", &OpenOptions::read()),
Err(error) if error.raw_os_error() == Some(nix::errno::Errno::ENOSYS as i32)
)
}
pub fn pinned_path(file: &File) -> String {
use std::os::fd::AsRawFd;
format!("/proc/self/fd/{}", file.as_raw_fd())
}
#[derive(Debug, Clone, Copy)]
pub struct OpenOptions {
flags: i32,
mode: Option<Mode>,
}
impl OpenOptions {
pub fn read() -> Self {
Self {
flags: OFlag::O_RDONLY.bits(),
mode: None,
}
}
pub fn truncate() -> Self {
Self {
flags: (OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_TRUNC).bits(),
mode: Some(Mode::from_bits_truncate(0o600)),
}
}
pub fn truncate_mode(mode: u32) -> Self {
Self {
flags: (OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_TRUNC).bits(),
mode: Some(Mode::from_bits_truncate(mode)),
}
}
pub fn create_new() -> Self {
Self {
flags: (OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_EXCL).bits(),
mode: Some(Mode::from_bits_truncate(0o600)),
}
}
}
pub fn read_beneath(root: &str, relative: &str) -> std::io::Result<String> {
let mut file = open_beneath(root, relative, &OpenOptions::read())?;
let mut text = String::new();
file.read_to_string(&mut text)?;
Ok(text)
}
pub fn truncate_beneath(root: &str, relative: &str) -> std::io::Result<()> {
open_beneath(root, relative, &OpenOptions::truncate()).map(|_| ())
}
pub fn write_beneath(root: &str, relative: &str, bytes: &[u8]) -> std::io::Result<()> {
use std::io::Write;
let mut file = open_beneath(root, relative, &OpenOptions::truncate())?;
file.write_all(bytes)
}
#[cfg(test)]
mod tests;