use crate::containment::is_lexically_inside;
use crate::error::PathError;
use crate::internal::components::is_drive_relative_path;
use crate::internal::validation::reject_nul_path;
use crate::normalize::normalize;
use std::path::{Path, PathBuf};
pub fn absolute(path: impl AsRef<Path>) -> Result<PathBuf, PathError> {
let path = path.as_ref();
reject_nul_path(path)?;
if is_drive_relative_path(path) {
return Err(PathError::drive_relative(path));
}
if path.is_absolute() {
return normalize(path);
}
let cwd = std::env::current_dir()
.map_err(|e| PathError::CurrentDirectoryUnavailable { source: e })?;
normalize(cwd.join(path))
}
pub fn resolve_against(
base: impl AsRef<Path>,
input: impl AsRef<Path>,
) -> Result<PathBuf, PathError> {
let base = base.as_ref();
let input = input.as_ref();
reject_nul_path(base)?;
reject_nul_path(input)?;
if is_drive_relative_path(input) {
return Err(PathError::drive_relative(input));
}
if is_drive_relative_path(base) {
return Err(PathError::drive_relative(base));
}
if input.is_absolute() {
return normalize(input);
}
normalize(base.join(input))
}
pub fn join_relative(
base: impl AsRef<Path>,
child: impl AsRef<Path>,
) -> Result<PathBuf, PathError> {
let base = base.as_ref();
let child = child.as_ref();
reject_nul_path(base)?;
reject_nul_path(child)?;
if is_drive_relative_path(child) {
return Err(PathError::drive_relative(child));
}
if child.is_absolute() {
return Err(PathError::absolute_child(child));
}
if child
.components()
.next()
.is_some_and(|c| matches!(c, std::path::Component::Prefix(_)))
{
return Err(PathError::absolute_child(child));
}
normalize(base.join(child))
}
pub fn resolve_inside(
root: impl AsRef<Path>,
child: impl AsRef<Path>,
) -> Result<PathBuf, PathError> {
let root = root.as_ref();
let child = child.as_ref();
reject_nul_path(root)?;
reject_nul_path(child)?;
if is_drive_relative_path(root) {
return Err(PathError::drive_relative(root));
}
if is_drive_relative_path(child) {
return Err(PathError::drive_relative(child));
}
let joined = join_relative(root, child)?;
let root_norm = normalize(root)?;
if !is_lexically_inside(&joined, &root_norm) {
return Err(PathError::root_escape(&joined));
}
Ok(joined)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn join_rejects_absolute() {
#[cfg(unix)]
{
assert!(matches!(
join_relative("/repo", "/etc/passwd"),
Err(PathError::AbsoluteChildPath { .. })
));
}
}
#[test]
fn resolve_inside_blocks_escape() {
let err = resolve_inside("/repo", "../../etc/passwd").unwrap_err();
assert!(matches!(err, PathError::RootEscape { .. }));
}
#[test]
fn resolve_inside_allows_nested() {
let p = resolve_inside("/repo", "src/main.rs").unwrap();
assert_eq!(p, PathBuf::from("/repo/src/main.rs"));
}
}