1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
|
use std::path::{Path, PathBuf};
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct PathFormatConfig {
/// Whether to strip ANSI escape sequences (e.g., `\x1b[31m`, `\x1b[0m`).
/// When the path string may contain terminal color codes, enabling this option will clean them up.
#[cfg(feature = "strip-ansi")]
pub strip_ansi: bool,
/// Whether to strip characters disallowed in Windows filenames (`*`, `?`, `"`, `<`, `>`, `|`).
/// These characters typically have special meaning or are not allowed in filesystems.
pub strip_unfriendly_chars: bool,
/// Whether to resolve parent directory references (`..`).
/// When enabled, attempts to navigate upward in the path, e.g., `/a/b/../c` becomes `/a/c`.
/// Note: This operation is based solely on the path string itself, without accessing the actual filesystem.
pub resolve_parent_dirs: bool,
/// Whether to collapse consecutive forward slashes (`/`).
/// For example, `/home//user` becomes `/home/user`.
pub collapse_consecutive_slashes: bool,
/// Whether to escape backslashes (`\`) to forward slashes (`/`).
/// This helps unify Windows‑style paths to Unix style, facilitating cross‑platform handling.
pub escape_backslashes: bool,
}
impl Default for PathFormatConfig {
fn default() -> Self {
Self {
#[cfg(feature = "strip-ansi")]
strip_ansi: true,
strip_unfriendly_chars: true,
resolve_parent_dirs: true,
collapse_consecutive_slashes: true,
escape_backslashes: true,
}
}
}
/// Normalize an input path string into a canonical, platform‑agnostic form.
///
/// This function removes ANSI escape sequences (requires `strip-ansi` feature),
/// unifies separators to `/`,
/// collapses duplicate slashes, strips unfriendly characters (`*`, `?`, `"`, `<`, `>`, `|`),
/// resolves simple `..` components, and preserves a trailing slash when present.
///
/// See examples below for the exact normalization behavior.
///
/// # Examples
///
/// ```
/// # use just_fmt::fmt_path::fmt_path_str;
/// # use just_fmt::fmt_path::FormatPathError;
///
/// # fn main() -> Result<(), FormatPathError> {
/// assert_eq!(fmt_path_str("C:\\Users\\\\test")?, "C:/Users/test");
/// assert_eq!(
/// fmt_path_str("/path/with/*unfriendly?chars")?,
/// "/path/with/unfriendlychars"
/// );
/// assert_eq!(fmt_path_str("\x1b[31m/path\x1b[0m")?, "/path");
/// assert_eq!(fmt_path_str("/home/user/dir/")?, "/home/user/dir/");
/// assert_eq!(
/// fmt_path_str("/home/user/file.txt")?,
/// "/home/user/file.txt"
/// );
/// assert_eq!(
/// fmt_path_str("/home/my_user/DOCS/JVCS_TEST/Workspace/../Vault/")?,
/// "/home/my_user/DOCS/JVCS_TEST/Vault/"
/// );
/// assert_eq!(fmt_path_str("./home/file.txt")?, "home/file.txt");
/// assert_eq!(fmt_path_str("./home/path/")?, "home/path/");
/// assert_eq!(fmt_path_str("./")?, "");
/// # Ok(())
/// # }
/// ```
pub fn fmt_path_str(path: impl Into<String>) -> Result<String, PathFormatError> {
fmt_path_str_custom(path, &PathFormatConfig::default())
}
/// Normalize an input path string into a canonical, platform‑agnostic form.
///
/// This function removes ANSI escape sequences (requires `strip-ansi` feature),
/// unifies separators to `/`,
/// collapses duplicate slashes, strips unfriendly characters (`*`, `?`, `"`, `<`, `>`, `|`),
/// resolves simple `..` components, and preserves a trailing slash when present.
///
/// Unlike `fmt_path_str`,
/// this method uses `PathFormatConfig` to precisely control
/// what should be processed
pub fn fmt_path_str_custom(
path: impl Into<String>,
config: &PathFormatConfig,
) -> Result<String, PathFormatError> {
let path_result = path.into();
let ends_with_slash = path_result.ends_with('/');
// ANSI Strip
#[cfg(feature = "strip-ansi")]
let path_result = if config.strip_ansi {
let cleaned = strip_ansi_escapes::strip(&path_result);
String::from_utf8(cleaned).map_err(PathFormatError::InvalidUtf8)?
} else {
path_result
};
let path_result = if config.escape_backslashes {
path_result.replace('\\', "/")
} else {
path_result
};
let mut result = String::new();
let mut prev_char = '\0';
for c in path_result.chars() {
if config.collapse_consecutive_slashes && c == '/' && prev_char == '/' {
continue;
}
result.push(c);
prev_char = c;
}
if config.strip_unfriendly_chars {
let unfriendly_chars = ['*', '?', '"', '<', '>', '|'];
result = result
.chars()
.filter(|c| !unfriendly_chars.contains(c))
.collect();
}
// Handle ".." path components
let path_buf = PathBuf::from(&result);
let normalized_path = if config.resolve_parent_dirs {
normalize_path(&path_buf)
} else {
path_buf
};
result = normalized_path.to_string_lossy().replace('\\', "/");
// Restore trailing slash if original path had one
if ends_with_slash && !result.ends_with('/') {
result.push('/');
}
// Special case: when result is only "./", return ""
if result == "./" {
return Ok(String::new());
}
Ok(result)
}
/// Normalize path by resolving ".." components without requiring file system access
fn normalize_path(path: &Path) -> PathBuf {
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
if !components.is_empty() {
components.pop();
}
}
std::path::Component::CurDir => {
// Skip current directory components
}
_ => {
components.push(component);
}
}
}
if components.is_empty() {
PathBuf::from(".")
} else {
components.iter().collect()
}
}
/// Format a [`PathBuf`] into its canonical string form and convert it back.
///
/// This is a convenience wrapper around [`fmt_path_str`], preserving
/// the semantics of [`PathBuf`] while applying the same normalization rules:
/// - normalize separators to `/`
/// - remove duplicated separators
/// - strip ANSI escape sequences
/// - remove unfriendly characters (`*`, `?`, etc.)
/// - resolve simple `..` segments
pub fn fmt_path(path: impl Into<PathBuf>) -> Result<PathBuf, PathFormatError> {
let path_str = fmt_path_str(path.into().display().to_string())?;
Ok(PathBuf::from(path_str))
}
/// Format a [`PathBuf`] into its canonical string form and convert it back.
///
/// Unlike `fmt_path`,
/// this method uses `PathFormatConfig` to precisely control
/// what should be processed
pub fn fmt_path_custom(
path: impl Into<PathBuf>,
config: &PathFormatConfig,
) -> Result<PathBuf, PathFormatError> {
let path_str = fmt_path_str_custom(path.into().display().to_string(), config)?;
Ok(PathBuf::from(path_str))
}
/// Error type for path formatting operations.
#[derive(Debug)]
pub enum PathFormatError {
/// The input string contained invalid UTF-8 after stripping ANSI escape sequences.
InvalidUtf8(std::string::FromUtf8Error),
}
impl std::fmt::Display for PathFormatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PathFormatError::InvalidUtf8(e) => {
write!(f, "Invalid UTF-8 after ANSI stripping: {}", e)
}
}
}
}
impl std::error::Error for PathFormatError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
PathFormatError::InvalidUtf8(e) => Some(e),
}
}
}
impl From<std::string::FromUtf8Error> for PathFormatError {
fn from(e: std::string::FromUtf8Error) -> Self {
PathFormatError::InvalidUtf8(e)
}
}
|