Skip to content

Commit 0806444

Browse files
authored
Merge pull request #129 from ReagentX/feat/cs/sanitize-filenames
Feat/cs/sanitize filenames
2 parents e009027 + 9d5a038 commit 0806444

3 files changed

Lines changed: 75 additions & 6 deletions

File tree

src/extensions/parser.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use crate::{
2222
sum::Sum,
2323
},
2424
error::LogriaError,
25+
sanitizers::sanitize_filename,
2526
},
2627
};
2728

@@ -54,9 +55,10 @@ impl ExtensionMethods for Parser {
5455
/// Create parser file from a Parser struct
5556
fn save(self, file_name: &str) -> Result<(), LogriaError> {
5657
let parser_json = serde_json::to_string_pretty(&self).unwrap();
57-
let path = format!("{}/{}", patterns(), file_name);
58+
let sanitized_filename = sanitize_filename(file_name);
59+
let path = format!("{}/{}", patterns(), sanitized_filename);
5860

59-
match write(format!("{}/{}", patterns(), file_name), parser_json) {
61+
match write(&path, parser_json) {
6062
Ok(()) => Ok(()),
6163
Err(why) => Err(LogriaError::CannotWrite(path, <dyn Error>::to_string(&why))),
6264
}

src/extensions/session.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
1111
use crate::{
1212
constants::{cli::excludes::SESSION_FILE_EXCLUDES, directories::sessions},
1313
extensions::extension::ExtensionMethods,
14-
util::error::LogriaError,
14+
util::{error::LogriaError, sanitizers::sanitize_filename},
1515
};
1616

1717
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Debug)]
@@ -39,7 +39,8 @@ impl ExtensionMethods for Session {
3939
/// Create session file from a Session struct
4040
fn save(self, file_name: &str) -> Result<(), LogriaError> {
4141
let session_json = serde_json::to_string_pretty(&self).unwrap();
42-
let path = format!("{}/{}", sessions(), file_name);
42+
let sanitized_filename = sanitize_filename(file_name);
43+
let path = format!("{}/{}", sessions(), sanitized_filename);
4344
match write(&path, session_json) {
4445
Ok(()) => Ok(()),
4546
Err(why) => Err(LogriaError::CannotWrite(path, <dyn Error>::to_string(&why))),

src/util/sanitizers.rs

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,34 @@
1+
use std::{cmp::max, collections::HashSet, str::from_utf8, sync::LazyLock};
2+
13
use regex::bytes::Regex;
2-
use std::{cmp::max, str::from_utf8};
34

45
use crate::constants::cli::patterns::ANSI_COLOR_PATTERN;
56

7+
/// Characters disallowed in a filename
8+
static FILENAME_DISALLOWED_CHARS: LazyLock<HashSet<char>> =
9+
LazyLock::new(|| HashSet::from(['*', '"', '/', '\\', '<', '>', ':', '|', '?', '.']));
10+
/// The character to replace disallowed chars with
11+
const FILENAME_REPLACEMENT_CHAR: char = '_';
12+
13+
/// Remove unsafe chars in [this list](FILENAME_DISALLOWED_CHARS).
14+
///
15+
/// Does not need to use a `Cow` for optimization because the source is always generated based on chat data
16+
/// so there is no opportunity for the original input to be passed in from another borrow.
17+
pub fn sanitize_filename(filename: &str) -> String {
18+
filename
19+
.trim()
20+
.chars()
21+
.map(|letter| {
22+
if letter.is_control() || FILENAME_DISALLOWED_CHARS.contains(&letter) {
23+
FILENAME_REPLACEMENT_CHAR
24+
} else {
25+
letter
26+
}
27+
})
28+
.take(255)
29+
.collect()
30+
}
31+
632
pub struct LengthFinder {
733
color_pattern: Regex,
834
}
@@ -34,7 +60,7 @@ impl LengthFinder {
3460

3561
#[cfg(test)]
3662
mod tests {
37-
use crate::util::sanitizers::LengthFinder;
63+
use crate::util::sanitizers::{LengthFinder, sanitize_filename};
3864

3965
#[test]
4066
fn test_length_clean() {
@@ -81,4 +107,44 @@ mod tests {
81107
assert_eq!(rows, 1);
82108
assert_eq!(length, 6);
83109
}
110+
111+
#[test]
112+
fn test_sanitize_filename_clean() {
113+
assert_eq!(sanitize_filename("normal_filename"), "normal_filename");
114+
assert_eq!(sanitize_filename("file.txt"), "file_txt");
115+
assert_eq!(sanitize_filename("file_123"), "file_123");
116+
}
117+
118+
#[test]
119+
fn test_sanitize_filename_invalid_chars() {
120+
assert_eq!(sanitize_filename("file<>name"), "file__name");
121+
assert_eq!(sanitize_filename("file:name"), "file_name");
122+
assert_eq!(sanitize_filename("file\"name"), "file_name");
123+
assert_eq!(sanitize_filename("file|name"), "file_name");
124+
assert_eq!(sanitize_filename("file?name"), "file_name");
125+
assert_eq!(sanitize_filename("file*name"), "file_name");
126+
assert_eq!(sanitize_filename("file\\name"), "file_name");
127+
assert_eq!(sanitize_filename("file/name"), "file_name");
128+
}
129+
130+
#[test]
131+
fn test_sanitize_filename_control_chars() {
132+
assert_eq!(sanitize_filename("file\x00name"), "file_name");
133+
assert_eq!(sanitize_filename("file\x1fname"), "file_name");
134+
assert_eq!(sanitize_filename("file\x7fname"), "file_name");
135+
}
136+
137+
#[test]
138+
fn test_sanitize_filename_trim() {
139+
assert_eq!(sanitize_filename(" filename "), "filename");
140+
assert_eq!(sanitize_filename("..filename.."), "__filename__");
141+
assert_eq!(sanitize_filename("\tfilename\t"), "filename");
142+
}
143+
144+
#[test]
145+
fn test_sanitize_filename_long() {
146+
let long_name = "a".repeat(300);
147+
let sanitized = sanitize_filename(&long_name);
148+
assert_eq!(sanitized.len(), 255);
149+
}
84150
}

0 commit comments

Comments
 (0)