use crate::memdir::paths::{get_auto_mem_path, is_auto_memory_enabled};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct PathTraversalError {
message: String,
}
impl std::fmt::Display for PathTraversalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for PathTraversalError {}
impl PathTraversalError {
pub fn new(message: &str) -> Self {
Self {
message: message.to_string(),
}
}
}
fn sanitize_path_key(key: &str) -> Result<String, PathTraversalError> {
if key.contains('\0') {
return Err(PathTraversalError::new(&format!(
"Null byte in path key: \"{}\"",
key
)));
}
let mut has_url_encoded_traversal = false;
let key_lower = key.to_lowercase();
if key_lower.contains("%2e%2e") || key_lower.contains("%2e/") || key_lower.contains("/%2e%2e") {
has_url_encoded_traversal = true;
}
if has_url_encoded_traversal {
return Err(PathTraversalError::new(&format!(
"URL-encoded traversal in path key: \"{}\"",
key
)));
}
if key.contains('\\') {
return Err(PathTraversalError::new(&format!(
"Backslash in path key: \"{}\"",
key
)));
}
if key.starts_with('/') {
return Err(PathTraversalError::new(&format!(
"Absolute path key: \"{}\"",
key
)));
}
Ok(key.to_string())
}
pub fn is_team_memory_enabled() -> bool {
if !is_auto_memory_enabled() {
return false;
}
false
}
pub fn get_team_mem_path() -> PathBuf {
let auto_mem = get_auto_mem_path();
let team_path = auto_mem.join("team");
let path_str = team_path.to_string_lossy().to_string();
let sep = std::path::MAIN_SEPARATOR;
if !path_str.ends_with(sep) {
format!("{}{}", path_str, sep).into()
} else {
team_path
}
}
pub fn get_team_mem_entypoint() -> PathBuf {
get_team_mem_path().join("MEMORY.md")
}
pub fn is_team_mem_path(file_path: &Path) -> bool {
let resolved_path = std::fs::canonicalize(file_path).unwrap_or_else(|_| {
if file_path.is_absolute() {
file_path.to_path_buf()
} else {
std::env::current_dir()
.map(|c| c.join(file_path))
.unwrap_or_else(|_| file_path.to_path_buf())
}
});
let team_dir = get_team_mem_path();
let team_dir_str = team_dir.to_string_lossy();
resolved_path.to_string_lossy().starts_with(&*team_dir_str)
}
pub fn validate_team_mem_write_path(file_path: &Path) -> Result<PathBuf, PathTraversalError> {
let path_str = file_path.to_string_lossy();
if path_str.contains('\0') {
return Err(PathTraversalError::new(&format!(
"Null byte in path: \"{}\"",
file_path.display()
)));
}
let resolved_path = std::fs::canonicalize(file_path).unwrap_or_else(|_| {
if file_path.is_absolute() {
file_path.to_path_buf()
} else {
std::env::current_dir()
.map(|c| c.join(file_path))
.unwrap_or_else(|_| file_path.to_path_buf())
}
});
let team_dir = get_team_mem_path();
let team_dir_str = team_dir.to_string_lossy();
if !resolved_path.to_string_lossy().starts_with(&*team_dir_str) {
return Err(PathTraversalError::new(&format!(
"Path escapes team memory directory: \"{}\"",
file_path.display()
)));
}
Ok(resolved_path)
}
pub fn validate_team_mem_key(relative_key: &str) -> Result<PathBuf, PathTraversalError> {
sanitize_path_key(relative_key)?;
let team_dir = get_team_mem_path();
let full_path = team_dir.join(relative_key);
let resolved_path = std::fs::canonicalize(&full_path).unwrap_or_else(|_| full_path.clone());
let team_dir_str = team_dir.to_string_lossy();
if !resolved_path.to_string_lossy().starts_with(&*team_dir_str) {
return Err(PathTraversalError::new(&format!(
"Key escapes team memory directory: \"{}\"",
relative_key
)));
}
Ok(resolved_path)
}
pub fn is_team_mem_file(file_path: &Path) -> bool {
is_team_memory_enabled() && is_team_mem_path(file_path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_path_key_valid() {
assert!(sanitize_path_key("valid_name.md").is_ok());
assert!(sanitize_path_key("subdir/test.md").is_ok());
}
#[test]
fn test_sanitize_path_key_null_byte() {
let result = sanitize_path_key("test\0.md");
assert!(result.is_err());
}
#[test]
fn test_sanitize_path_key_absolute() {
let result = sanitize_path_key("/etc/passwd");
assert!(result.is_err());
}
#[test]
fn test_sanitize_path_key_backslash() {
let result = sanitize_path_key("test\\md");
assert!(result.is_err());
}
#[test]
fn test_is_team_memory_enabled_when_auto_disabled() {
let _ = is_team_memory_enabled();
}
}