diff --git a/src/libraries.rs b/src/libraries.rs index 02d02ee..59b614a 100644 --- a/src/libraries.rs +++ b/src/libraries.rs @@ -80,16 +80,21 @@ impl Library { } } -/// Parse a comma-separated excluded_dirs column into a Vec, dropping -/// empty entries (mirrors `AppState::parse_excluded_dirs` for the env -/// var). NULL → empty Vec. Duplicates are preserved — `PathExcluder` -/// accepts repeats, and the storage-side normaliser is where dedup -/// happens. +/// Parse an excluded_dirs string into a Vec, dropping empty entries. +/// NULL → empty Vec. Duplicates are preserved — `PathExcluder` accepts +/// repeats, and the storage-side normaliser is where dedup happens. +/// +/// Accepts both `,` and newline (`\n` / `\r\n`) as separators so the +/// UI's textarea can submit one-entry-per-line input without forcing +/// the operator to remember commas. The DB stores the canonical +/// comma-joined form (see `normalize_excluded_dirs_input`); the +/// newline path matters mostly for the frontend submit, but mirroring +/// it here keeps the parse direction round-trip safe. pub fn parse_excluded_dirs_column(raw: Option<&str>) -> Vec { match raw { None => Vec::new(), Some(s) => s - .split(',') + .split(|c: char| matches!(c, ',' | '\n' | '\r')) .map(str::trim) .filter(|s| !s.is_empty()) .map(String::from) @@ -739,6 +744,40 @@ mod tests { ); } + #[test] + fn parse_excluded_dirs_column_splits_on_newlines_too() { + // Newline-separated input from a textarea submit. One-per-line + // is the recommended UX because "I forgot the comma" was a + // recurring footgun (.thumbnails .thumbnails2 silently + // becomes a single never-matching pattern). + assert_eq!( + parse_excluded_dirs_column(Some("@eaDir\n.thumbnails\n/private")), + vec![ + "@eaDir".to_string(), + ".thumbnails".to_string(), + "/private".to_string() + ] + ); + // Windows line endings (CRLF) — the carriage return is its own + // separator so the trailing empty token between \r and \n gets + // trimmed + dropped. + assert_eq!( + parse_excluded_dirs_column(Some("a\r\nb\r\nc")), + vec!["a".to_string(), "b".to_string(), "c".to_string()] + ); + // Mixed comma + newline — the user pastes from one source, + // adds a few entries inline. Both work, in any combination. + assert_eq!( + parse_excluded_dirs_column(Some("a, b\nc,d")), + vec![ + "a".to_string(), + "b".to_string(), + "c".to_string(), + "d".to_string() + ] + ); + } + #[test] fn effective_excluded_dirs_unions_global_and_per_library() { let lib_no_extras = Library {