use std::path::{Component, Path, PathBuf};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ContextDir {
current_dir: PathBuf,
file_root: PathBuf,
}
impl Default for ContextDir {
fn default() -> Self {
ContextDir {
current_dir: PathBuf::new(),
file_root: PathBuf::new(),
}
}
}
impl ContextDir {
pub fn new(current_dir: &Path, file_root: &Path) -> ContextDir {
ContextDir {
current_dir: PathBuf::from(current_dir),
file_root: PathBuf::from(file_root),
}
}
pub fn resolved_path(&self, filename: &Path) -> PathBuf {
self.file_root.join(filename)
}
pub fn is_access_allowed(&self, filename: &Path) -> bool {
let file = self.resolved_path(filename);
let absolute_file = self.current_dir.join(file);
let absolute_file_root = self.current_dir.join(&self.file_root);
is_descendant(absolute_file.as_path(), absolute_file_root.as_path())
}
}
fn is_descendant(path: &Path, ancestor: &Path) -> bool {
let path = normalize_path(path);
let ancestor = normalize_path(ancestor);
for a in path.ancestors() {
if ancestor == a {
return true;
}
}
false
}
fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
pub fn create_dir_all(filename: &Path) -> Result<(), std::io::Error> {
if let Some(parent) = filename.parent() {
return std::fs::create_dir_all(parent);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_filename_allowed_access_without_user_file_root() {
let current_dir = Path::new("/tmp");
let file_root = Path::new("");
let ctx = ContextDir::new(current_dir, file_root);
assert!(ctx.is_access_allowed(Path::new("foo.bin")));
assert!(ctx.is_access_allowed(Path::new("/tmp/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("a/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("a/b/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("../tmp/a/b/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("../../../tmp/a/b/foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("/file/foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../../foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../../file/foo.bin")));
}
#[test]
fn check_filename_allowed_access_with_explicit_absolute_user_file_root() {
let current_dir = Path::new("/tmp");
let file_root = Path::new("/file");
let ctx = ContextDir::new(current_dir, file_root);
assert!(ctx.is_access_allowed(Path::new("foo.bin"))); assert!(ctx.is_access_allowed(Path::new("/file/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("a/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("a/b/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("../../file/foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("/tmp/foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../tmp/a/b/foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../../foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../../../tmp/a/b/foo.bin")));
let current_dir = Path::new("/tmp");
let file_root = Path::new("../file");
let ctx = ContextDir::new(current_dir, file_root);
assert!(ctx.is_access_allowed(Path::new("foo.bin")));
assert!(ctx.is_access_allowed(Path::new("/file/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("a/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("a/b/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("../../file/foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("/tmp/foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../tmp/a/b/foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../../foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../../../tmp/a/b/foo.bin")));
}
#[test]
fn check_filename_allowed_access_with_implicit_relative_user_file_root() {
let current_dir = Path::new("/tmp");
let file_root = Path::new("a/b");
let ctx = ContextDir::new(current_dir, file_root);
assert!(ctx.is_access_allowed(Path::new("foo.bin")));
assert!(ctx.is_access_allowed(Path::new("c/foo.bin"))); assert!(ctx.is_access_allowed(Path::new("/tmp/a/b/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("/tmp/a/b/c/d/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("../../../tmp/a/b/foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("/tmp/foo.bin")));
}
#[test]
fn check_filename_allowed_access_with_explicit_relative_user_file_root() {
let current_dir = Path::new("/tmp");
let file_root = Path::new("../tmp");
let ctx = ContextDir::new(current_dir, file_root);
assert!(ctx.is_access_allowed(Path::new("foo.bin")));
assert!(ctx.is_access_allowed(Path::new("/tmp/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("a/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("a/b/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("../tmp/a/b/foo.bin")));
assert!(ctx.is_access_allowed(Path::new("../../../tmp/a/b/foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("/file/foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../../foo.bin")));
assert!(!ctx.is_access_allowed(Path::new("../../file/foo.bin")));
}
#[test]
fn is_descendant_true() {
let child = Path::new("/tmp/foo/bar.txt");
let parent = Path::new("/tmp");
assert!(is_descendant(child, parent));
let child = Path::new("/tmp/foo/../bar.txt");
let parent = Path::new("/tmp");
assert!(is_descendant(child, parent));
let child = Path::new("bar.txt");
let parent = Path::new("");
assert!(is_descendant(child, parent));
}
#[test]
fn is_descendant_false() {
let child = Path::new("/tmp/foo/../../bar.txt");
let parent = Path::new("/tmp");
assert!(!is_descendant(child, parent));
let child = Path::new("/a/bar.txt");
let parent = Path::new("/b");
assert!(!is_descendant(child, parent));
let child = Path::new("/bar.txt");
let parent = Path::new("");
assert!(!is_descendant(child, parent));
}
}