1use crate::StringError;
4use std::path::{Component, Path, PathBuf};
5
6pub fn split_ext(p: &str) -> [&str; 2] {
8 match p.rfind('.') {
9 Some(0) | None => [p, ""],
10 Some(idx) if idx + 1 < p.len() => [&p[..idx], &p[idx + 1..]],
11 Some(idx) => [&p[..idx], ""], }
13}
14
15pub fn root_dir() -> PathBuf {
16 PathBuf::from(std::path::MAIN_SEPARATOR_STR)
17}
18
19#[easy_ext::ext(PathExt)]
20pub impl<T: AsRef<Path>> T {
21 fn basename(&self) -> String {
24 let path = self.as_ref();
25 match path.file_name() {
26 Some(s) => s.to_string_lossy(),
27 None => path.to_string_lossy(),
28 }
29 .to_string()
30 }
31
32 fn filename(&self) -> Cow<'_, str> {
35 let path = self.as_ref();
36
37 match path.file_name() {
38 Some(s) => s.to_string_lossy(),
39 None => {
40 log::warn!("Failed to determine basename of {path:?}");
41 "".into()
42 }
43 }
44 }
45
46 fn _filename(&self) -> Result<&str, StringError> {
48 let path = self.as_ref();
49
50 let err_prefix = format!("Failed to determine basename of {path:?}");
51 path.file_name()
52 .ok_or(StringError(err_prefix.clone()))?
53 .to_str()
54 .ok_or(StringError(err_prefix))
55 }
56
57 fn display_short(&self, home_dir: &Path) -> String {
58 let path = self.as_ref();
59 if let Ok(stripped) = path.strip_prefix(home_dir) {
60 PathBuf::from("~").join(stripped).to_string_lossy().into()
61 } else {
62 path.to_string_lossy().into()
63 }
64 }
65
66 fn len(&self) -> usize {
67 self.as_ref().normalize().iter().count()
68 }
69
70 fn is_hidden(&self) -> bool {
72 let mut skip = 0;
73
74 for c in self.as_ref().components().rev() {
75 match c {
76 Component::ParentDir => {
77 skip += 1;
78 }
79 Component::CurDir => {}
80 Component::Normal(name) => {
81 if skip > 0 {
82 skip -= 1;
83 continue;
84 }
85 return name.as_encoded_bytes().first() == Some(&b'.');
86 }
87 _ => {}
88 }
89 }
90
91 false
92 }
93
94 fn abs(&self, base: impl AsRef<Path>) -> PathBuf {
105 let path = self.as_ref();
106 let base = base.as_ref();
107
108 base.join(path).normalize()
109 }
110
111 fn is_empty(&self) -> bool {
112 let path = self.as_ref();
113 path.components().next().is_none()
114 }
115
116 fn normalize(&self) -> PathBuf {
118 let path = self.as_ref();
119 let mut components = path.components().peekable();
120 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
122 components.next();
123 PathBuf::from(c.as_os_str())
124 } else {
125 PathBuf::new()
126 };
127
128 for component in components {
129 match component {
130 Component::Prefix(..) => unreachable!(),
131 Component::RootDir => {
132 ret.push(component.as_os_str());
133 }
134 Component::CurDir => {}
135 Component::ParentDir => {
136 ret.pop();
137 }
138 Component::Normal(c) => {
139 ret.push(c);
140 }
141 }
142 }
143
144 ret
145 }
146
147 fn shell_quote(&self) -> Option<String> {
150 let Some(s) = self.as_ref().to_str() else {
151 return None;
152 };
153
154 if cfg!(windows) {
155 let escaped = s.replace('"', "\"\"");
158 Some(format!("\"{}\"", escaped))
159 } else if cfg!(unix) {
160 let escaped = s.replace('\'', r"'\''");
163 Some(format!("'{}'", escaped))
164 } else {
165 None
166 }
167 }
168}
169
170pub fn shell_quote_impl(s: &str) -> String {
171 if cfg!(windows) {
172 let escaped = s.replace('"', "\"\"");
175 format!("\"{}\"", escaped)
176 } else if cfg!(unix) {
177 let escaped = s.replace('\'', r"'\''");
180 format!("'{}'", escaped)
181 } else {
182 s.to_string()
183 }
184}
185
186#[macro_export]
190macro_rules! expr_as_path_fn {
191 ($fn_name:ident, $expr:expr) => {
192 pub fn $fn_name() -> &'static std::path::Path {
193 static VALUE: std::sync::LazyLock<std::path::PathBuf> =
194 std::sync::LazyLock::new(|| $expr.into());
195 &VALUE
196 }
197 };
198}
199
200use std::borrow::Cow;
203use std::ffi::{OsStr, OsString};
204
205#[cfg(unix)]
207pub fn os_str_to_bytes(string: &OsStr) -> Cow<'_, [u8]> {
208 use std::os::unix::ffi::OsStrExt;
209 Cow::Borrowed(string.as_bytes())
210}
211
212#[cfg(windows)]
213pub fn os_str_to_bytes(string: &OsStr) -> Cow<'_, [u8]> {
214 use std::os::windows::ffi::OsStrExt;
215 let bytes = string.encode_wide().flat_map(u16::to_le_bytes).collect();
216 Cow::Owned(bytes)
217}
218
219#[cfg(unix)]
220pub fn bytes_to_os_string(bytes: Vec<u8>) -> OsString {
221 use std::os::unix::ffi::OsStringExt;
222 OsString::from_vec(bytes)
223}
224
225#[cfg(windows)]
226pub fn bytes_to_os_string(bytes: Vec<u8>) -> OsString {
227 use std::os::windows::ffi::OsStringExt;
228
229 debug_assert!(bytes.len() % 2 == 0, "invalid UTF-16 byte length");
230
231 let wide: Vec<u16> = bytes
232 .chunks_exact(2)
233 .map(|c| u16::from_le_bytes([c[0], c[1]]))
234 .collect();
235
236 OsString::from_wide(&wide)
237}
238
239pub fn to_string_lossy(s: &impl AsRef<std::ffi::OsStr>) -> std::borrow::Cow<'_, str> {
242 s.as_ref().to_string_lossy()
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
247#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
248pub enum RenamePolicy {
249 WrappedInc(String, String),
250 Replace, }
254
255impl Default for RenamePolicy {
260 fn default() -> Self {
261 Self::WrappedInc("_".into(), "".into())
262 }
263}
264
265pub fn auto_dest_for_src(
268 src: impl AsRef<Path>,
269 dest: impl AsRef<OsStr>,
270 method: &RenamePolicy,
271) -> PathBuf {
272 let src = src.as_ref();
273 let dest = dest.as_ref();
274
275 let put_into_dest =
276 dest.is_empty() || dest.to_string_lossy().ends_with(std::path::MAIN_SEPARATOR);
277 let dest_path = Path::new(dest).normalize();
278
279 let initial_dest = if put_into_dest || dest_path.file_name().is_none() {
280 let name = src
281 .file_name()
282 .expect("Could not determine a valid destination: missing file_name.");
283 dest_path.join(name)
284 } else {
285 dest_path
286 };
287
288 match method {
289 RenamePolicy::Replace => {
290 return initial_dest;
291 }
292 RenamePolicy::WrappedInc(prefix, suffix) => {
293 if !initial_dest.exists() {
294 return initial_dest;
295 }
296
297 let parent = initial_dest.parent().unwrap_or(&initial_dest);
298 let s = initial_dest.filename();
299 let [stem, ext] = split_ext(&s);
300
301 for i in 1usize.. {
302 let candidate: PathBuf = parent.join(if ext.is_empty() {
303 format!("{stem}{prefix}{i}{suffix}")
304 } else {
305 format!("{stem}{prefix}{i}{suffix}.{ext}")
306 });
307
308 if !candidate.exists() {
309 return candidate;
310 }
311 }
312 unreachable!()
313 }
314 }
315}