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