1use crate::{cfg_feature_alloc, cfg_feature_std};
9use core::fmt::{Display, Formatter};
10
11cfg_feature_std! {
12 mod temp;
13 pub use temp::*;
14}
15
16pub static USUALLY_PROHIBITED_FS_CHARS: &[char; 9] =
20 &['<', '>', ':', '"', '/', '\\', '|', '?', '*'];
21
22pub static FATXX_PROHIBITED_FS_CHARS: &[char; 18] = &[
26 '"', '*', '/', ':', '<', '>', '?', '\\', '|', '+', ',', '.', ';', '=', '[', ']', '!', '@',
27];
28
29pub static WINDOWS_PROHIBITED_FILE_NAMES: &[&str; 45] = &[
33 "CON", "PRN", "AUX", "CLOCK$", "LST", "KEYBD$", "SCREEN$", "$IDLE$", "CONFIG$", "NUL", "COM0",
34 "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2",
35 "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "$Mft", "$MftMirr", "$LogFile",
36 "$Volume", "$AttrDef", "$Bitmap", "$Boot", "$BadClus", "$Secure", "$Upcase", "$Extend",
37 "$Quota", "$ObjId", "$Reparse", "$Extend",
38];
39
40pub static USUALLY_SAFE_FS_CHARS: &[char; 83] = &[
45 ' ', '!', '#', '%', '&', '(', ')', '+', ',', '-', '.', '0', '1', '2', '3', '4', '5', '6', '7',
46 '8', '9', '=', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
47 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', ']', '^', '_', '`', 'a', 'b', 'c',
48 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
49 'w', 'x', 'y', 'z', '{', '}', '~',
50];
51
52#[derive(Debug, Copy, Clone, Eq, PartialEq)]
55pub enum FilenameError {
56 StartsWithWindowsProhibited(&'static str),
57 ContainsUsuallyInvalidChar(char),
58 EndsWithInvalidCharacter(char),
59}
60impl Display for FilenameError {
61 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
62 match self {
63 FilenameError::StartsWithWindowsProhibited(pro) => {
64 write!(f, "Filename starts with windows prohibited word: {pro}")
65 }
66 FilenameError::ContainsUsuallyInvalidChar(chr) => {
67 write!(
68 f,
69 "Filename contains a usually invalid character: {chr}(0x{:02X})",
70 *chr as u16
71 )
72 }
73 FilenameError::EndsWithInvalidCharacter(chr) => {
74 write!(
75 f,
76 "Filename ends with a usually invalid character: {chr}(0x{:02X})",
77 *chr as u16
78 )
79 }
80 }
81 }
82}
83impl core::error::Error for FilenameError {}
84
85cfg_feature_alloc! {
86 extern crate alloc;
87 pub fn clean_filename<T: AsRef<str>>(val: &T) -> alloc::string::String {
90 let input = val.as_ref();
91 let mut out = alloc::string::String::with_capacity(input.len());
92 for v in input.chars() {
93 if USUALLY_SAFE_FS_CHARS.binary_search(&v).is_ok() {
94 out.push(v);
95 }
96 }
97 out
98 }
99}
100
101pub fn is_filename_probably_valid<T: AsRef<str>>(val: &T) -> Result<(), FilenameError> {
105 let input = val.as_ref();
106 for invalid in WINDOWS_PROHIBITED_FILE_NAMES {
107 if input.starts_with(invalid) {
108 return Err(FilenameError::StartsWithWindowsProhibited(invalid));
109 }
110 }
111 for v in input.chars() {
112 let vi = v as u32;
113 if !(0x20..=0x7E).contains(&vi) || USUALLY_PROHIBITED_FS_CHARS.binary_search(&v).is_ok() {
114 return Err(FilenameError::ContainsUsuallyInvalidChar(v));
115 }
116 }
117 Ok(())
118}
119
120cfg_feature_std! {
121 use std::path::{Path, PathBuf};
122 use std::fs::{DirEntry};
123 use irox_bits::{Error, ErrorKind};
124
125 pub fn find_all_files_with_extension<T: AsRef<Path>>(start: T, ext: &str) -> Result<Vec<PathBuf>, Error> {
128 let mut out = Vec::new();
129 let path = start.as_ref();
130 let ent = std::fs::read_dir(path)?;
131 for dirent in ent {
132 let dirent = dirent?;
133 collect_fileswithext_in_dirent(&dirent, &mut out, ext)?;
134 }
135 Ok(out)
136 }
137
138 fn collect_fileswithext_in_dirent(
139 dirent: &DirEntry,
140 out: &mut Vec<PathBuf>,
141 _ext: &str,
142 ) -> Result<(), Error> {
143 let path = dirent.path();
144 if path.is_dir() {
145 let ent = std::fs::read_dir(path)?;
146 for dirent in ent {
147 let dirent = dirent?;
148 collect_fileswithext_in_dirent(&dirent, out, _ext)?;
149 }
150 } else if let Some(ext) = path.extension() {
151 if ext.to_string_lossy().to_lowercase().as_str() == ext {
152 out.push(path);
153 }
154 }
155
156 Ok(())
157 }
158
159 pub fn find_associated_file<T: AsRef<Path>>(file: T, newext: &str) -> Result<PathBuf, Error> {
162 let path = file.as_ref();
163 let dir = path.parent().unwrap_or_else(|| Path::new("."));
164 let Some(filename) = path.file_name() else {
166 return Error::err(ErrorKind::NotFound, "Provided file doesn't have a name");
167 };
168 let filename = format!("{}.{newext}", filename.to_string_lossy());
169 let mut appended = path.to_path_buf();
170 appended.set_file_name(filename);
171
172 if appended.exists() && appended.is_file() {
173 return Ok(appended);
174 }
175 let Some(stem) = path.file_stem() else {
177 return Error::err(ErrorKind::NotFound, "Provided file doesn't have a stem");
178 };
179 Ok(dir.join(format!("{}.{}", stem.to_string_lossy(), newext)))
180 }
181}