Handle Windows reserved file names

There are a collection of case-insensitive file base names that are reserved on Windows for reasons of ancient history: per https://docs.microsoft.com/en-au/windows/win32/fileio/naming-a-file: "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9". Windows software file name sanitisation needs to take this list into account, and there’s minimal harm in protecting against them everywhere.

Here’s a quick implementation of checking (unbenchmarked; I can think of various clever optimisations that it might or might not already do):

/// Checks if the file name is reserved and invalid on Windows.
///
/// The list of reserved base names: CON, PRN, AUX, NUL, COM1–COM9, LPT1–LPT9.
/// These names are case-insensitive, and are reserved with no extension or one extension.
///
/// Returns 3 or 4 if the file name is reserved, depending on the name length
/// (3 for CON, PRN, AUX or NUL; 4 for the others). Examples:
///
/// ```
/// # use sanitize_filename_reader_friendly::check_reserved_windows_file_name;
/// assert_eq!(check_reserved_windows_file_name("CON"), 3);
/// assert_eq!(check_reserved_windows_file_name("aux.h"), 3);
/// assert_eq!(check_reserved_windows_file_name("Lpt1.exe"), 4);
/// ```
///
/// Returns 0 if the file name is not reserved. Examples:
///
/// ```
/// # use sanitize_filename_reader_friendly::check_reserved_windows_file_name;
/// assert_eq!(check_reserved_windows_file_name("xyz"), 0);
/// assert_eq!(check_reserved_windows_file_name(""), 0);
/// assert_eq!(check_reserved_windows_file_name("nül"), 0);
/// assert_eq!(check_reserved_windows_file_name("COM1.jpg.png"), 0);
/// ```
pub fn check_reserved_windows_file_name(name: &str) -> usize {
    let name = name.as_bytes();
    let name = name
        .iter()
        .enumerate()
        .rev()
        .find(|(_, c)| **c == b'.')
        .map(|(i, _)| &name[..i])
        .unwrap_or(name);
    match name {
        [b'C' | b'c', b'O' | b'o', b'N' | b'n'] => 3,
        [b'P' | b'p', b'R' | b'r', b'N' | b'n'] => 3,
        [b'A' | b'a', b'U' | b'u', b'X' | b'x'] => 3,
        [b'N' | b'n', b'U' | b'u', b'L' | b'l'] => 3,
        [b'C' | b'c', b'O' | b'o', b'M' | b'm', b'1'..=b'9'] => 4,
        [b'L' | b'l', b'P' | b'p', b'T' | b't', b'1'..=b'9'] => 4,
        _ => 0,
    }
}

You could just provide a function like this and leave action to the users, but I think it’d be nice to take it into account automatically with something like appending an underscore (though this would break the existing guarantee that the output string is not longer than the input string), so CON → CON_, aux.h → aux_.h, which I think is nice and friendly:

pub fn sanitize(s: &str) -> String {
    let mut string = ;
    let reservation_len = check_reserved_windows_file_name(string);
    if reservation_len > 0 {
        string.insert(reservation_len, '_');
    }
    string
}

Near the end of writing all this, I noticed src/lib.rs:170–172 mentioning these reserved names, so I gather this was already on the radar. Well, here’s a most of an implementation and a suggestion on handling it automatically. 🙂