pub mod text;
#[must_use]
pub fn counted(n: usize, noun: &str) -> String {
if n == 1 { format!("1 {noun}") } else { format!("{n} {noun}s") }
}
const ILLEGAL: &[char] = &['<', '>', ':', '"', '/', '\\', '|', '?', '*'];
#[must_use]
pub fn file_name(label: &str) -> String {
let stem: String = label
.chars()
.map(|c| if c.is_control() || ILLEGAL.contains(&c) { '_' } else { c })
.collect();
format!("BDINFO.{stem}.txt")
}
#[cfg(test)]
mod tests {
use super::{counted, file_name};
#[test]
fn counted_agrees_the_noun_with_the_count() {
assert_eq!(counted(0, "error"), "0 errors");
assert_eq!(counted(1, "error"), "1 error");
assert_eq!(counted(2, "stream file"), "2 stream files");
}
#[test]
fn a_clean_label_passes_through() {
assert_eq!(file_name("MY_DISC"), "BDINFO.MY_DISC.txt");
assert_eq!(file_name("BigBuckBunny"), "BDINFO.BigBuckBunny.txt");
assert_eq!(file_name("Blu-Ray"), "BDINFO.Blu-Ray.txt");
assert_eq!(file_name(""), "BDINFO..txt");
}
#[test]
fn illegal_and_control_characters_become_underscores() {
assert_eq!(file_name(r"J:\"), "BDINFO.J__.txt");
assert_eq!(file_name("a/b"), "BDINFO.a_b.txt");
assert_eq!(file_name("x<y>z|?*\""), "BDINFO.x_y_z____.txt");
assert_eq!(file_name("tab\there"), "BDINFO.tab_here.txt");
assert_eq!(file_name(r"..\..\x"), "BDINFO..._.._x.txt");
}
mod prop {
use proptest::prelude::{any, prop_assert, prop_assert_eq, proptest};
use super::super::{ILLEGAL, file_name};
proptest! {
#[test]
fn the_name_is_always_one_safe_component(label in any::<String>()) {
let name = file_name(&label);
let stem = name.strip_prefix("BDINFO.").and_then(|rest| rest.strip_suffix(".txt"));
prop_assert!(stem.is_some(), "the name keeps its fixed shape: {name}");
prop_assert!(!name.chars().any(char::is_control));
prop_assert!(!name.chars().any(|c| ILLEGAL.contains(&c)));
prop_assert_eq!(std::path::Path::new(&name).components().count(), 1);
}
}
}
}