#[cfg(not(unix))]
use super::path::require_under_root;
use super::{config::FilesystemConfig, error::FileReadError};
use std::{
fs::{File, Metadata},
io,
path::{Component, Path},
};
#[cfg(unix)]
use rustix::fs::{Mode, OFlags, openat};
pub(super) fn open_existing_file(
path: &Path,
config: &FilesystemConfig,
) -> Result<(File, Metadata), FileReadError> {
#[cfg(unix)]
{
open_existing_file_at_workspace_root(path, config).map_err(|source| FileReadError::Open {
path: path.to_owned(),
source,
})
}
#[cfg(not(unix))]
{
let file = File::open(path).map_err(|source| FileReadError::Open {
path: path.to_owned(),
source,
})?;
let metadata = file.metadata().map_err(|source| FileReadError::Metadata {
path: path.to_owned(),
source,
})?;
require_current_path_identity(path, &metadata, config).map_err(|source| {
FileReadError::Metadata {
path: path.to_owned(),
source,
}
})?;
Ok((file, metadata))
}
}
#[cfg(not(unix))]
pub(super) fn require_current_path_identity(
path: &Path,
opened: &Metadata,
config: &FilesystemConfig,
) -> Result<(), io::Error> {
let canonical = path.canonicalize()?;
require_under_root(&canonical, config)
.map_err(|_error| io::Error::other("opened file resolved outside workspace root"))?;
let current = std::fs::symlink_metadata(path)?;
if same_file_identity(opened, ¤t) {
Ok(())
} else {
Err(io::Error::other("opened file changed during policy check"))
}
}
#[cfg(not(unix))]
fn same_file_identity(left: &Metadata, right: &Metadata) -> bool {
let Some(left) = FileIdentity::from_metadata(left) else {
return false;
};
let Some(right) = FileIdentity::from_metadata(right) else {
return false;
};
left == right
}
#[cfg(not(unix))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct FileIdentity {
volume: u64,
file: u128,
}
#[cfg(not(unix))]
impl FileIdentity {
fn from_metadata(metadata: &Metadata) -> Option<Self> {
file_identity_from_metadata(metadata)
}
}
#[cfg(windows)]
fn file_identity_from_metadata(metadata: &Metadata) -> Option<FileIdentity> {
use std::os::windows::fs::MetadataExt;
Some(FileIdentity {
volume: u64::from(metadata.volume_serial_number()?),
file: (u128::from(metadata.file_index_high()) << 64)
| u128::from(metadata.file_index_low()),
})
}
#[cfg(all(not(unix), not(windows)))]
fn file_identity_from_metadata(_metadata: &Metadata) -> Option<FileIdentity> {
None
}
#[cfg(unix)]
fn open_existing_file_at_workspace_root(
path: &Path,
config: &FilesystemConfig,
) -> Result<(File, Metadata), io::Error> {
let relative_path = path
.strip_prefix(&config.workspace_root)
.map_err(|_error| {
io::Error::new(
io::ErrorKind::PermissionDenied,
"path is outside workspace root",
)
})?;
if relative_path.as_os_str().is_empty() {
let file = open_workspace_directory(Path::new(""), config)?;
let metadata = file.metadata()?;
return Ok((file, metadata));
}
let (parent, file_name) = split_parent_and_file_name(relative_path)?;
let parent_dir = open_workspace_directory(parent, config)?;
let file_fd = openat(
&parent_dir,
file_name,
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::empty(),
)?;
let file = File::from(file_fd);
let metadata = file.metadata()?;
Ok((file, metadata))
}
#[cfg(unix)]
pub(super) fn open_workspace_directory(
relative_dir: &Path,
config: &FilesystemConfig,
) -> Result<File, io::Error> {
let mut current = File::from(openat(
rustix::fs::CWD,
&config.workspace_root,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::empty(),
)?);
for component in normal_components(relative_dir)? {
let next = File::from(openat(
¤t,
component,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::empty(),
)?);
current = next;
}
Ok(current)
}
#[cfg(unix)]
fn split_parent_and_file_name(path: &Path) -> Result<(&Path, &Path), io::Error> {
let file_name = path
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path has no file name"))?;
let parent = path.parent().unwrap_or_else(|| Path::new(""));
Ok((parent, Path::new(file_name)))
}
#[cfg(unix)]
fn normal_components(path: &Path) -> Result<Vec<&Path>, io::Error> {
let mut components = Vec::new();
for component in path.components() {
match component {
Component::Normal(part) => components.push(Path::new(part)),
Component::CurDir => {}
Component::Prefix(_) | Component::RootDir | Component::ParentDir => {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"path contains non-normal component",
));
}
}
}
Ok(components)
}