thoughts_tool/utils/
validation.rs1use anyhow::Result;
2use anyhow::bail;
3use std::path::Component;
4use std::path::Path;
5
6pub fn validate_simple_filename(filename: &str) -> Result<()> {
9 if filename.trim().is_empty() {
10 bail!("Filename cannot be empty");
11 }
12
13 let p = Path::new(filename);
15 let mut comps = p.components();
16
17 if matches!(
19 comps.next(),
20 Some(Component::RootDir | Component::Prefix(_))
21 ) {
22 bail!("Absolute paths are not allowed");
23 }
24
25 if p.components().count() != 1 {
27 bail!("Filename must not contain directories");
28 }
29
30 if filename == "." || filename == ".." {
32 bail!("Invalid filename");
33 }
34
35 let ok = filename
37 .chars()
38 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'));
39 if !ok {
40 bail!("Filename contains invalid characters (allowed: A-Z a-z 0-9 . _ -)");
41 }
42
43 Ok(())
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 #[test]
51 fn test_validate_simple_filename_ok() {
52 for f in ["a.md", "plan-01.md", "notes_v2.md", "R1.TOC"] {
53 assert!(validate_simple_filename(f).is_ok(), "{f}");
54 }
55 }
56
57 #[test]
58 fn test_validate_simple_filename_bad() {
59 for f in [
60 "../x.md",
61 "/abs.md",
62 "a/b.md",
63 " ",
64 "",
65 ".",
66 "..",
67 "name with space.md",
68 ] {
69 assert!(validate_simple_filename(f).is_err(), "{f}");
70 }
71 }
72}