irox_tools/fs/
mod.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2025 IROX Contributors
3//
4
5//!
6//! Filesystem utilities
7
8use crate::cfg_feature_alloc;
9use core::fmt::{Display, Formatter};
10
11///
12/// A list of characters that are usually prohibited by common filesystems like VFAT and NTFS.
13/// See [Wikipedia:Filename](https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words)
14pub static USUALLY_PROHIBITED_FS_CHARS: &[char; 9] =
15    &['<', '>', ':', '"', '/', '\\', '|', '?', '*'];
16
17///
18/// A list of characters that are prohibited by FAT12, FAT16, FAT34
19/// See [Wikipedia:Filename](https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words)
20pub static FATXX_PROHIBITED_FS_CHARS: &[char; 18] = &[
21    '"', '*', '/', ':', '<', '>', '?', '\\', '|', '+', ',', '.', ';', '=', '[', ']', '!', '@',
22];
23
24///
25/// A list of filenames prohibited by windows.  
26/// See [Wikipedia:Filename](https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words)
27pub static WINDOWS_PROHIBITED_FILE_NAMES: &[&str; 45] = &[
28    "CON", "PRN", "AUX", "CLOCK$", "LST", "KEYBD$", "SCREEN$", "$IDLE$", "CONFIG$", "NUL", "COM0",
29    "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2",
30    "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "$Mft", "$MftMirr", "$LogFile",
31    "$Volume", "$AttrDef", "$Bitmap", "$Boot", "$BadClus", "$Secure", "$Upcase", "$Extend",
32    "$Quota", "$ObjId", "$Reparse", "$Extend",
33];
34
35///
36/// A list of characters that are usually safe for use in filesystem names.  This is essentially the
37/// printable characters minus [`USUALLY_PROHIBITED_FS_CHARS`] and [`;`,`$`] (to avoid shell issues)
38/// See [Wikipedia:Filename](https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words)
39pub static USUALLY_SAFE_FS_CHARS: &[char; 83] = &[
40    ' ', '!', '#', '%', '&', '(', ')', '+', ',', '-', '.', '0', '1', '2', '3', '4', '5', '6', '7',
41    '8', '9', '=', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
42    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', ']', '^', '_', '`', 'a', 'b', 'c',
43    'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
44    'w', 'x', 'y', 'z', '{', '}', '~',
45];
46
47///
48/// An error case returned from the filename checker
49#[derive(Debug, Copy, Clone, Eq, PartialEq)]
50pub enum FilenameError {
51    StartsWithWindowsProhibited(&'static str),
52    ContainsUsuallyInvalidChar(char),
53    EndsWithInvalidCharacter(char),
54}
55impl Display for FilenameError {
56    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
57        match self {
58            FilenameError::StartsWithWindowsProhibited(pro) => {
59                write!(f, "Filename starts with windows prohibited word: {pro}")
60            }
61            FilenameError::ContainsUsuallyInvalidChar(chr) => {
62                write!(
63                    f,
64                    "Filename contains a usually invalid character: {chr}(0x{:02X})",
65                    *chr as u16
66                )
67            }
68            FilenameError::EndsWithInvalidCharacter(chr) => {
69                write!(
70                    f,
71                    "Filename ends with a usually invalid character: {chr}(0x{:02X})",
72                    *chr as u16
73                )
74            }
75        }
76    }
77}
78impl core::error::Error for FilenameError {}
79
80cfg_feature_alloc! {
81    extern crate alloc;
82    ///
83    /// Removes any character in the input value that isn't in [`USUALLY_SAFE_FS_CHARS`]
84    pub fn clean_filename<T: AsRef<str>>(val: &T) -> alloc::string::String {
85        let input = val.as_ref();
86        let mut out = alloc::string::String::with_capacity(input.len());
87        for v in input.chars() {
88            if USUALLY_SAFE_FS_CHARS.binary_search(&v).is_ok() {
89                out.push(v);
90            }
91        }
92        out
93    }
94}
95
96///
97/// Checks the provided filename against the set of [`WINDOWS_PROHIBITED_FILE_NAMES`] and
98/// [`USUALLY_PROHIBITED_FS_CHARS`], returning an error if either set of checks are found
99pub fn is_filename_probably_valid<T: AsRef<str>>(val: &T) -> Result<(), FilenameError> {
100    let input = val.as_ref();
101    for invalid in WINDOWS_PROHIBITED_FILE_NAMES {
102        if input.starts_with(invalid) {
103            return Err(FilenameError::StartsWithWindowsProhibited(invalid));
104        }
105    }
106    for v in input.chars() {
107        let vi = v as u32;
108        if !(0x20..=0x7E).contains(&vi) || USUALLY_PROHIBITED_FS_CHARS.binary_search(&v).is_ok() {
109            return Err(FilenameError::ContainsUsuallyInvalidChar(v));
110        }
111    }
112    Ok(())
113}