use std::path::{Component, Path, PathBuf};
use crate::error::DocError;
pub(crate) fn guard_output_path(output_dir: &Path, relative: &Path) -> Result<PathBuf, DocError> {
for component in relative.components() {
match component {
Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
return Err(DocError::Escape(relative.display().to_string()));
}
Component::CurDir | Component::Normal(_) => {}
}
}
let joined = output_dir.join(relative);
if !joined.starts_with(output_dir) {
return Err(DocError::Escape(relative.display().to_string()));
}
Ok(joined)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_parent_dir_traversal() {
let output_dir = Path::new("/srv/mini-docs-output");
let malicious = Path::new("../../evil.html");
let err = guard_output_path(output_dir, malicious).expect_err("traversal must be rejected");
assert!(matches!(err, DocError::Escape(_)));
}
#[test]
fn rejects_traversal_in_middle_of_path() {
let output_dir = Path::new("/srv/mini-docs-output");
let malicious = Path::new("guide/../../evil.html");
let err = guard_output_path(output_dir, malicious)
.expect_err("mid-path traversal must be rejected");
assert!(matches!(err, DocError::Escape(_)));
}
#[test]
fn rejects_absolute_relative_path() {
let output_dir = Path::new("/srv/mini-docs-output");
let malicious = Path::new("/etc/evil.html");
let err =
guard_output_path(output_dir, malicious).expect_err("absolute path must be rejected");
assert!(matches!(err, DocError::Escape(_)));
}
#[test]
fn accepts_ordinary_nested_relative_path() {
let output_dir = Path::new("/srv/mini-docs-output");
let ok_path = Path::new("guide/setup.html");
let joined = guard_output_path(output_dir, ok_path).expect("ordinary path is fine");
assert_eq!(joined, output_dir.join("guide/setup.html"));
}
}