// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::path::{Path, Component};

/**
 * Checks whether a directory traversal attack is possible with the given path.
 *
 * Returns `Ok(path)` if it's not possible, otherwise returning `Err(message)`.
 */
pub fn prevent(path: &Path) -> Result<&Path, String> {
	// NOTE: Will use Path::normalize_lexically once that becomes stable
	let mut depth = 0usize;
	for comp in path.components() {
		match comp {
			Component::Prefix(_) | Component::RootDir => return Err(format!("root dir or prefix found in path \"{}\"", path.display())),
			Component::Normal(_) => depth += 1,
			Component::CurDir => (),
			Component::ParentDir => depth = depth.checked_sub(1).ok_or_else(|| format!("directory traversal attack possible for path \"{}\"", path.display()))?,
		}
	}
	Ok(path)
}

#[cfg(test)]
mod tests {
	use super::*;

	fn valid(path: &str) {
		assert!(prevent(Path::new(path)).is_ok());
	}

	fn invalid(path: &str) {
		assert!(prevent(Path::new(path)).is_err());
	}

	#[test]
	fn normal() {
		valid("file");
		valid("./file");
		valid("./foo/bar");
		valid("foo/bar/baz");
		valid(" ");
	}

	#[test]
	fn empty() {
		valid("");
	}

	#[test]
	fn traversal() {
		valid("foo/..");
		valid("a/b/../..");
		valid("a/b/../.././.");
		valid("foo/../bar");
		valid("a/b/c/../d");
		valid("a/b/c/../../d");
		valid("a/b/c/../../../d");
		invalid("foo/../../bar");
		invalid("a/b/c/../../../../d");
		invalid("a/b/../../c/../../d");
		invalid("..");
		invalid("../a");
		invalid("../a/b/c/d");
		invalid("a/../..");
		invalid("a/././././././../../..");
	}
}
