1#[allow(clippy::collapsible_str_replace)]
8pub fn sanitize_path_component(s: &str) -> String {
9 s.replace("..", "_")
10 .replace('/', "_")
11 .replace('\\', "_")
12 .chars()
13 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_' || *c == '.')
14 .collect()
15}
16
17#[cfg(test)]
18mod tests {
19 use super::*;
20
21 #[test]
22 fn test_sanitize_removes_path_traversal() {
23 assert_eq!(
24 sanitize_path_component("../../etc/passwd"),
25 "____etc_passwd"
26 );
27 }
28
29 #[test]
30 fn test_sanitize_removes_backslash() {
31 assert_eq!(sanitize_path_component("foo\\bar"), "foo_bar");
32 }
33
34 #[test]
35 fn test_sanitize_preserves_safe_chars() {
36 assert_eq!(
37 sanitize_path_component("my-dataset_2024.csv"),
38 "my-dataset_2024.csv"
39 );
40 }
41
42 #[test]
43 fn test_sanitize_strips_special_chars() {
44 assert_eq!(sanitize_path_component("hello world!@#"), "helloworld");
45 }
46
47 #[test]
48 fn test_sanitize_empty_string() {
49 assert_eq!(sanitize_path_component(""), "");
50 }
51
52 #[test]
53 fn test_sanitize_single_dot_preserved() {
54 assert_eq!(sanitize_path_component("."), ".");
55 }
56
57 #[test]
58 fn test_sanitize_hidden_file_prefix_preserved() {
59 assert_eq!(sanitize_path_component(".bashrc"), ".bashrc");
60 }
61
62 #[test]
63 fn test_sanitize_trailing_dot_preserved() {
64 assert_eq!(sanitize_path_component("file."), "file.");
65 }
66
67 #[test]
68 fn test_sanitize_three_dots_replaces_leading_pair() {
69 assert_eq!(sanitize_path_component("..."), "_.");
70 }
71
72 #[test]
73 fn test_sanitize_four_dots_replaces_both_pairs() {
74 assert_eq!(sanitize_path_component("...."), "__");
75 }
76
77 #[test]
78 fn test_sanitize_embedded_parent_traversal_replaced() {
79 assert_eq!(sanitize_path_component("foo..bar"), "foo_bar");
80 }
81
82 #[test]
83 fn test_sanitize_preserves_unicode_letters() {
84 assert_eq!(sanitize_path_component("résumé"), "résumé");
85 assert_eq!(sanitize_path_component("日本語"), "日本語");
86 }
87
88 #[test]
89 fn test_sanitize_only_special_chars_returns_empty() {
90 assert_eq!(sanitize_path_component("!@#$%^&*()"), "");
91 }
92
93 #[test]
94 fn test_sanitize_long_input_does_not_panic() {
95 let long = "a".repeat(10_000);
96 let result = sanitize_path_component(&long);
97 assert_eq!(result.len(), 10_000);
98 }
99
100 #[test]
101 fn test_sanitize_mixed_safe_and_traversal() {
102 assert_eq!(
103 sanitize_path_component("safe-name../evil"),
104 "safe-name__evil"
105 );
106 }
107}