use crate::env::{EnvProvider, RealEnv};
use std::path::Path;
const MAX_COMPONENT_LENGTH: usize = 255;
const MAX_PATH_LENGTH: usize = 4096;
#[must_use]
pub fn validate_path_with_env(path: &Path, env: &dyn EnvProvider) -> bool {
let Some(path_str) = path.to_str() else {
return false;
};
if path_str.contains('\0') {
return false;
}
let max_path = env
.var("LUFF_MAX_PATH_LENGTH")
.and_then(|s| s.parse().ok())
.unwrap_or(MAX_PATH_LENGTH);
if path_str.len() > max_path {
return false;
}
let max_component = env
.var("LUFF_MAX_COMPONENT_LENGTH")
.and_then(|s| s.parse().ok())
.unwrap_or(MAX_COMPONENT_LENGTH);
for component in path.components() {
let Some(comp_str) = component.as_os_str().to_str() else {
return false;
};
let encoded_len = comp_str.len();
if encoded_len > max_component {
return false;
}
if comp_str.bytes().any(|b| b < 32 || b == 127) {
return false;
}
if super::platform::is_reserved_name(comp_str) {
return false;
}
}
true
}
#[must_use]
pub fn validate_path(path: &Path) -> bool {
validate_path_with_env(path, &RealEnv)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::env::MockEnv;
use std::path::Path;
#[test]
fn test_validate_path_valid() {
let env = MockEnv::new();
let path = Path::new("src/main.rs");
assert!(validate_path_with_env(path, &env));
}
#[test]
fn test_validate_path_component_length() {
let env = MockEnv::new();
let valid_comp = "a".repeat(255);
let path_str = format!("/tmp/{valid_comp}");
let path = Path::new(&path_str);
assert!(validate_path_with_env(path, &env));
let invalid_comp = "a".repeat(256);
let path_str = format!("/tmp/{invalid_comp}");
let path = Path::new(&path_str);
assert!(!validate_path_with_env(path, &env));
}
#[test]
fn test_validate_path_windows_reserved_names() {
let env = MockEnv::new();
assert!(!validate_path_with_env(Path::new("/tmp/CON"), &env));
assert!(!validate_path_with_env(Path::new("/tmp/PRN"), &env));
assert!(!validate_path_with_env(Path::new("/tmp/AUX"), &env));
assert!(!validate_path_with_env(Path::new("/tmp/NUL"), &env));
assert!(!validate_path_with_env(Path::new("/tmp/CON.txt"), &env));
assert!(!validate_path_with_env(Path::new("/tmp/COM1.log"), &env));
assert!(!validate_path_with_env(Path::new("/tmp/con"), &env));
assert!(!validate_path_with_env(Path::new("/tmp/CoM1"), &env));
assert!(validate_path_with_env(Path::new("/tmp/config.txt"), &env));
assert!(validate_path_with_env(Path::new("/tmp/LPT10"), &env));
}
#[test]
fn test_validate_path_control_characters() {
let env = MockEnv::new();
assert!(!validate_path_with_env(
Path::new("/tmp/test\x01file.txt"),
&env
));
assert!(!validate_path_with_env(
Path::new("/tmp/test\x1Ffile.txt"),
&env
));
assert!(!validate_path_with_env(
Path::new("/tmp/test\x7Ffile.txt"),
&env
));
assert!(validate_path_with_env(
Path::new("/tmp/test_file.txt"),
&env
));
assert!(validate_path_with_env(
Path::new("/tmp/test-file.txt"),
&env
));
}
#[test]
fn test_validate_path_max_length() {
use std::iter;
let mut env = MockEnv::new();
let comp_255 = "a".repeat(255);
let components: Vec<String> = iter::repeat_n(comp_255, 16).collect();
let valid_path = components.join("/");
assert_eq!(valid_path.len(), 4095); assert!(validate_path_with_env(Path::new(&valid_path), &env));
let invalid_path = valid_path + "/a";
assert!(!validate_path_with_env(Path::new(&invalid_path), &env));
env = env.with_var("LUFF_MAX_PATH_LENGTH", "100");
let long_path = "a".repeat(101);
assert!(!validate_path_with_env(Path::new(&long_path), &env));
}
#[test]
fn test_validate_path_null_bytes() {
let env = MockEnv::new();
let path_str = "test\0file.txt";
let path = Path::new(path_str);
assert!(!validate_path_with_env(path, &env));
}
#[test]
fn test_empty_path_validation() {
let env = MockEnv::new();
let path = Path::new("");
assert!(validate_path_with_env(path, &env));
}
#[test]
fn test_mock_env_functionality() {
let env = MockEnv::new()
.with_var("TEST_VAR", "value")
.without_var("MISSING_VAR");
assert_eq!(env.var("TEST_VAR"), Some("value".to_string()));
assert_eq!(env.var("MISSING_VAR"), None);
}
}