use crate::constants::DEFAULT_MAX_PATH_LEN;
use crate::error::{PathError, PathResult, SafeNameError};
use crate::name::{FileSanitizationOptions, sanitize_file_with_options};
#[derive(Debug, Clone)]
pub struct PathSanitizationOptions<'a> {
pub file_options: FileSanitizationOptions<'a>,
pub max_path_len: usize,
}
impl Default for PathSanitizationOptions<'_> {
fn default() -> Self {
Self {
file_options: FileSanitizationOptions::default(),
max_path_len: DEFAULT_MAX_PATH_LEN,
}
}
}
pub fn sanitize_path(path: impl AsRef<[u8]>) -> PathResult<Vec<u8>> {
sanitize_path_with_options(path, &PathSanitizationOptions::default())
}
pub fn sanitize_path_with_options(
path: impl AsRef<[u8]>,
options: &PathSanitizationOptions<'_>,
) -> PathResult<Vec<u8>> {
let path = path.as_ref();
if path.is_empty() {
return Ok(options.file_options.replacement.to_vec());
}
let mut result = Vec::with_capacity(path.len());
let mut component_idx = 0;
let mut start = 0;
for (idx, &byte) in path.iter().enumerate() {
if byte == b'/' {
if idx > start {
let component = &path[start..idx];
let sanitized = sanitize_file_with_options(component, &options.file_options)
.map_err(|err| PathError::new(component_idx, err))?;
result.extend_from_slice(&sanitized);
component_idx += 1;
}
result.push(b'/');
start = idx + 1;
}
}
if start < path.len() {
let component = &path[start..];
let sanitized = sanitize_file_with_options(component, &options.file_options)
.map_err(|err| PathError::new(component_idx, err))?;
result.extend_from_slice(&sanitized);
}
if result.len() > options.max_path_len {
return Err(PathError::new(
0,
SafeNameError::InvalidLength {
len: result.len(),
max: options.max_path_len,
},
));
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::path::validation::validate_path;
use proptest::prelude::*;
#[test]
fn test_sanitize_path() {
assert_eq!(sanitize_path(b"/home/user").unwrap(), b"/home/user");
assert_eq!(sanitize_path("/home/user").unwrap(), b"/home/user");
let result = sanitize_path(b"/home/-rf").unwrap();
assert!(validate_path(&result).is_ok());
}
#[test]
fn test_sanitize_empty_path() {
assert_eq!(sanitize_path(b"").unwrap(), b"_");
}
#[test]
fn test_sanitize_relative_paths() {
assert_eq!(sanitize_path(b"../a/b/c").unwrap(), b"../a/b/c");
assert_eq!(sanitize_path(b"a/b/c/").unwrap(), b"a/b/c/");
assert_eq!(sanitize_path(b"a").unwrap(), b"a");
assert_eq!(sanitize_path(b"..").unwrap(), b"..");
let result = sanitize_path(b"../-rf/file").unwrap();
assert_eq!(result, b"../_rf/file");
assert!(validate_path(&result).is_ok());
}
#[test]
fn test_sanitize_path_too_long() {
let long_path = vec![b'a'; 5000];
let result = sanitize_path(&long_path);
assert!(result.is_err());
}
#[test]
fn test_sanitize_path_exceeds_max_after_replacement() {
use crate::REPLACEMENT_CHAR;
use crate::name::FileSanitizationOptions;
let file_opts = FileSanitizationOptions {
replacement: REPLACEMENT_CHAR,
..Default::default()
};
let opts = PathSanitizationOptions {
file_options: file_opts,
max_path_len: 20, };
let path = b"/a\x00\x00\x00\x00\x00\x00\x00";
let result = sanitize_path_with_options(path, &opts);
assert!(result.is_err());
if let Err(err) = result {
assert!(matches!(err.error, SafeNameError::InvalidLength { .. }));
}
}
proptest! {
#[test]
fn prop_sanitized_path_always_valid(input in prop::collection::vec(any::<u8>(), 0..256)) {
if let Ok(sanitized) = sanitize_path(&input) {
prop_assert!(
validate_path(&sanitized).is_ok(),
"sanitize_path({:?}) = {:?} failed validation",
input, sanitized
);
}
}
#[test]
fn prop_valid_paths_unchanged(
components in prop::collection::vec("[a-zA-Z0-9._]{1,10}", 1..5)
) {
let path = components.join("/");
let bytes = path.as_bytes();
if validate_path(bytes).is_ok() {
let sanitized = sanitize_path(bytes).unwrap();
prop_assert_eq!(sanitized.as_slice(), bytes, "Path modified by sanitization");
}
}
}
}