refactor: deduplicate filename sanitization logic
storage.rs and google_tasks.rs had near-identical sanitize_filename implementations. Extract the shared logic to a crate-level function so both modules reuse it. The google_tasks version also gains Windows reserved device name handling it previously lacked. https://claude.ai/code/session_013ooJht2HrZUTXgNJFU79cV
This commit is contained in:
parent
5c04e50956
commit
e470e79e78
|
|
@ -452,15 +452,7 @@ fn render_task_markdown(task: &Task) -> String {
|
|||
|
||||
/// Sanitize a string for use as a filesystem path component.
|
||||
fn sanitize_name(name: &str) -> String {
|
||||
let s: String = name.chars()
|
||||
.map(|c| match c {
|
||||
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
||||
'\0'..='\x1f' => '_',
|
||||
_ => c,
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim_matches(|c: char| c == '.' || c == ' ')
|
||||
.to_string();
|
||||
let s = crate::sanitize_filename(name);
|
||||
if s.is_empty() { "Untitled".to_string() } else { s }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,3 +11,30 @@ pub use models::{Task, TaskStatus, TaskList};
|
|||
pub use repository::TaskRepository;
|
||||
pub use config::{AppConfig, WorkspaceConfig};
|
||||
pub use error::{Error, Result};
|
||||
|
||||
/// Sanitize a string for use as a filesystem path component.
|
||||
/// Replaces filesystem-unsafe characters with underscores, trims leading/trailing
|
||||
/// dots and spaces, and prefixes Windows reserved device names.
|
||||
pub(crate) fn sanitize_filename(name: &str) -> String {
|
||||
let sanitized: String = name.chars()
|
||||
.map(|c| match c {
|
||||
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
||||
'\0'..='\x1f' => '_',
|
||||
_ => c,
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim_matches(|c: char| c == '.' || c == ' ')
|
||||
.to_string();
|
||||
// Reject Windows reserved device names (CON, PRN, AUX, NUL, COM0-9, LPT0-9)
|
||||
let stem = sanitized.split('.').next().unwrap_or("").to_uppercase();
|
||||
let is_reserved = matches!(stem.as_str(),
|
||||
"CON" | "PRN" | "AUX" | "NUL"
|
||||
| "COM0" | "COM1" | "COM2" | "COM3" | "COM4" | "COM5" | "COM6" | "COM7" | "COM8" | "COM9"
|
||||
| "LPT0" | "LPT1" | "LPT2" | "LPT3" | "LPT4" | "LPT5" | "LPT6" | "LPT7" | "LPT8" | "LPT9"
|
||||
);
|
||||
if is_reserved {
|
||||
format!("_{}", sanitized)
|
||||
} else {
|
||||
sanitized
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,27 +237,7 @@ impl FileSystemStorage {
|
|||
}
|
||||
|
||||
fn sanitize_filename(name: &str) -> String {
|
||||
let sanitized: String = name.chars()
|
||||
.map(|c| match c {
|
||||
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
||||
'\0'..='\x1f' => '_',
|
||||
_ => c,
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim_matches(|c: char| c == '.' || c == ' ')
|
||||
.to_string();
|
||||
// Reject Windows reserved device names (CON, PRN, AUX, NUL, COM0-9, LPT0-9)
|
||||
let stem = sanitized.split('.').next().unwrap_or("").to_uppercase();
|
||||
let is_reserved = matches!(stem.as_str(),
|
||||
"CON" | "PRN" | "AUX" | "NUL"
|
||||
| "COM0" | "COM1" | "COM2" | "COM3" | "COM4" | "COM5" | "COM6" | "COM7" | "COM8" | "COM9"
|
||||
| "LPT0" | "LPT1" | "LPT2" | "LPT3" | "LPT4" | "LPT5" | "LPT6" | "LPT7" | "LPT8" | "LPT9"
|
||||
);
|
||||
if is_reserved {
|
||||
format!("_{}", sanitized)
|
||||
} else {
|
||||
sanitized
|
||||
}
|
||||
crate::sanitize_filename(name)
|
||||
}
|
||||
|
||||
fn task_file_path(&self, list_dir: &Path, task: &Task) -> PathBuf {
|
||||
|
|
|
|||
Loading…
Reference in a new issue