···99mod image;
1010pub mod image_cache;
1111mod prefetch;
1212+mod sanitize_filename;
1213mod script;
1314mod style;
1415mod tailwind;
···413414414415fn make_filename(path: &Path, hash: &String, extension: Option<&str>) -> PathBuf {
415416 let file_stem = path.file_stem().unwrap();
417417+ let sanitized_stem = sanitize_filename::default_sanitize_file_name(file_stem.to_str().unwrap());
416418417419 let mut filename = PathBuf::new();
418418- filename.push(format!("{}.{}", file_stem.to_str().unwrap(), hash));
420420+ filename.push(format!("{}.{}", sanitized_stem, hash));
419421420422 if let Some(extension) = extension {
421423 filename.set_extension(format!("{}.{}", hash, extension));
+87
crates/maudit/src/assets/sanitize_filename.rs
···11+// MIT License
22+33+// Copyright (c) 2024-present VoidZero Inc. & Contributors
44+55+// Permission is hereby granted, free of charge, to any person obtaining a copy
66+// of this software and associated documentation files (the "Software"), to deal
77+// in the Software without restriction, including without limitation the rights
88+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+// copies of the Software, and to permit persons to whom the Software is
1010+// furnished to do so, subject to the following conditions:
1111+1212+// The above copyright notice and this permission notice shall be included in all
1313+// copies or substantial portions of the Software.
1414+1515+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+// SOFTWARE.
2222+2323+macro_rules! matches_invalid_chars {
2424+ ($chars:ident) => {
2525+ matches!($chars,
2626+ '\u{0000}'
2727+ ..='\u{001f}'
2828+ | '"'
2929+ | '#'
3030+ | '$'
3131+ | '%'
3232+ | '&'
3333+ | '*'
3434+ | '+'
3535+ | ','
3636+ | ':'
3737+ | ';'
3838+ | '<'
3939+ | '='
4040+ | '>'
4141+ | '?'
4242+ | '['
4343+ | ']'
4444+ | '^'
4545+ | '`'
4646+ | '{'
4747+ | '|'
4848+ | '}'
4949+ | '\u{007f}'
5050+ )
5151+ };
5252+}
5353+5454+// Follow from https://github.com/rollup/rollup/blob/master/src/utils/sanitizeFileName.ts
5555+pub fn default_sanitize_file_name(str: &str) -> String {
5656+ let mut sanitized = String::with_capacity(str.len());
5757+ let mut chars = str.chars();
5858+5959+ // A `:` is only allowed as part of a windows drive letter (ex: C:\foo)
6060+ // Otherwise, avoid them because they can refer to NTFS alternate data streams.
6161+ if starts_with_windows_drive(str) {
6262+ sanitized.push(chars.next().unwrap());
6363+ sanitized.push(chars.next().unwrap());
6464+ }
6565+6666+ for char in chars {
6767+ if matches_invalid_chars!(char) {
6868+ sanitized.push('_');
6969+ } else {
7070+ sanitized.push(char);
7171+ }
7272+ }
7373+ sanitized
7474+}
7575+7676+fn starts_with_windows_drive(str: &str) -> bool {
7777+ let mut chars = str.chars();
7878+ if !chars.next().is_some_and(|c| c.is_ascii_alphabetic()) {
7979+ return false;
8080+ }
8181+ chars.next().is_some_and(|c| c == ':')
8282+}
8383+8484+#[test]
8585+fn test_sanitize_file_name() {
8686+ assert_eq!(default_sanitize_file_name("\0+a=Z_0-"), "__a_Z_0-");
8787+}