use anyhow::{Result, ensure}; /// Sanitize the document's path to allow all clients to create the same path in /// their filesystem. If we didn't do this server-side, client's would need to /// deal with mapping invalid names to valid ones and then back. pub fn sanitize_path(path: &str) -> Result { let options = sanitize_filename::Options { truncate: true, windows: true, // Windows is the lowest common denominator replacement: "", }; let result = path .split('/') .map(|part| { let proposal = sanitize_filename::sanitize_with_options(part, options.clone()); if !part.is_empty() && proposal.is_empty() { "_".to_owned() } else { proposal } }) .collect::>() .join("/"); ensure!(!result.is_empty(), "Relative path is empty after sanitization"); Ok(result) } #[cfg(test)] mod test { use super::*; #[test] fn test_sanitize_path() { assert_eq!(sanitize_path("/my/path/what?").unwrap(), "/my/path/what"); assert_eq!(sanitize_path("file (1).md").unwrap(), "file (1).md"); assert_eq!(sanitize_path("/my/path/\\\\:?").unwrap(), "/my/path/_"); } #[test] fn test_sanitize_path_empty() { assert!(sanitize_path("").is_err()); } #[test] fn test_sanitize_path_idempotent_simple() { let mut result = sanitize_path("notes/my file.md").unwrap(); for _ in 0..5 { result = sanitize_path(&result).unwrap(); } assert_eq!(result, "notes/my file.md"); } #[test] fn test_sanitize_path_idempotent_special_chars() { let first = sanitize_path("/my/path/what?/file:name<>.md").unwrap(); let mut result = first.clone(); for _ in 0..5 { result = sanitize_path(&result).unwrap(); } assert_eq!(result, first); } }