1use crate::error::{Error, Result};
9
10pub const MAX_NAME_LEN: usize = 255;
12
13pub fn validate_new_name(name: &str) -> Result<()> {
18 if name.is_empty() || name.len() > MAX_NAME_LEN {
19 return Err(Error::invalid_name(name));
20 }
21 if name.contains('\0') {
22 return Err(Error::invalid_name(name));
23 }
24 let path = std::path::Path::new(name);
25 if path.is_absolute() {
26 return Err(Error::invalid_name(name));
27 }
28 for comp in path.components() {
29 match comp {
30 std::path::Component::Normal(_) => {}
31 _ => return Err(Error::invalid_name(name)),
32 }
33 }
34 if name.contains('\\') {
36 return Err(Error::invalid_name(name));
37 }
38 Ok(())
39}
40
41pub fn sanitize_stored_name(name: &str) -> Result<String> {
49 if name.is_empty() || name.len() > MAX_NAME_LEN {
50 return Err(Error::invalid_name(name));
51 }
52 validate_new_name(name)?;
53 Ok(name.to_string())
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn accepts_normal_names() {
62 assert!(validate_new_name("report.pdf").is_ok());
63 assert!(validate_new_name("photos").is_ok());
64 assert!(validate_new_name("photos/vacation/img.png").is_ok());
65 assert!(validate_new_name("ünïcødé.txt").is_ok());
66 }
67
68 #[test]
69 fn rejects_traversal_and_specials() {
70 assert!(validate_new_name("").is_err());
71 assert!(validate_new_name("../evil").is_err());
72 assert!(validate_new_name("a/../b").is_err());
73 assert!(validate_new_name("/abs").is_err());
74 assert!(validate_new_name("C:\\\\x").is_err());
75 assert!(validate_new_name("a\\b").is_err());
76 assert!(validate_new_name("nul\0").is_err());
77 assert!(validate_new_name(&"x".repeat(256)).is_err());
78 }
79
80 #[test]
81 fn sanitize_passthrough_valid() {
82 assert_eq!(sanitize_stored_name("ok.txt").unwrap(), "ok.txt");
83 }
84
85 #[test]
86 fn sanitize_rejects_traversal() {
87 assert!(sanitize_stored_name("../../etc/passwd").is_err());
88 }
89}