use super::{config::FilesystemConfig, error::PathPolicyError};
use std::{
ffi::OsStr,
fs, io,
path::{Component, Path, PathBuf},
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct ResolvedBufferPath {
pub(super) path: PathBuf,
pub(super) exists: bool,
}
pub(super) fn resolve_buffer_path(
requested_path: &Path,
config: &FilesystemConfig,
) -> Result<ResolvedBufferPath, PathPolicyError> {
let current_dir =
std::env::current_dir().map_err(|source| PathPolicyError::CurrentDir { source })?;
resolve_buffer_path_from(requested_path, config, ¤t_dir)
}
pub(super) fn resolve_buffer_path_from(
requested_path: &Path,
config: &FilesystemConfig,
base_dir: &Path,
) -> Result<ResolvedBufferPath, PathPolicyError> {
let absolute_path = if requested_path.is_absolute() {
requested_path.to_owned()
} else {
base_dir.join(requested_path)
};
match absolute_path.canonicalize() {
Ok(canonical) => {
require_under_root(&canonical, config)?;
Ok(ResolvedBufferPath {
path: canonical,
exists: true,
})
}
Err(error) => {
reject_existing_unresolvable_path(&absolute_path, error)?;
resolve_new_buffer_path(&absolute_path, config)
}
}
}
fn reject_existing_unresolvable_path(
absolute_path: &Path,
canonicalize_error: io::Error,
) -> Result<(), PathPolicyError> {
match fs::symlink_metadata(absolute_path) {
Ok(_metadata) => Err(PathPolicyError::Unresolvable {
path: absolute_path.to_owned(),
source: canonicalize_error,
}),
Err(metadata_error) if metadata_error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(metadata_error) => Err(PathPolicyError::Unresolvable {
path: absolute_path.to_owned(),
source: metadata_error,
}),
}
}
fn resolve_new_buffer_path(
absolute_path: &Path,
config: &FilesystemConfig,
) -> Result<ResolvedBufferPath, PathPolicyError> {
let parent = absolute_path
.parent()
.ok_or_else(|| PathPolicyError::MissingParent {
path: absolute_path.to_owned(),
})?;
let canonical_parent = parent
.canonicalize()
.map_err(|source| PathPolicyError::Parent {
path: absolute_path.to_owned(),
source,
})?;
require_under_root(&canonical_parent, config)?;
let file_name = absolute_path
.file_name()
.ok_or_else(|| PathPolicyError::MissingParent {
path: absolute_path.to_owned(),
})?;
require_normal_file_name(file_name, absolute_path)?;
Ok(ResolvedBufferPath {
path: canonical_parent.join(file_name),
exists: false,
})
}
fn require_normal_file_name(
file_name: &OsStr,
absolute_path: &Path,
) -> Result<(), PathPolicyError> {
let mut components = Path::new(file_name).components();
if matches!(components.next(), Some(Component::Normal(_))) && components.next().is_none() {
Ok(())
} else {
Err(PathPolicyError::MissingParent {
path: absolute_path.to_owned(),
})
}
}
pub(super) fn require_under_root(
path: &Path,
config: &FilesystemConfig,
) -> Result<(), PathPolicyError> {
if path.starts_with(&config.workspace_root) {
Ok(())
} else {
Err(PathPolicyError::OutsideWorkspace {
path: path.to_owned(),
workspace_root: config.workspace_root.clone(),
})
}
}