1use std::fs;
4use std::path::{Component, Path, PathBuf};
5
6pub fn abspath(p: &Path) -> PathBuf {
9 let abs = std::path::absolute(p).unwrap_or_else(|_| p.to_path_buf());
10 let mut out = PathBuf::new();
11 for c in abs.components() {
12 match c {
13 Component::CurDir => {}
14 Component::ParentDir => {
15 if !out.pop() {
16 out.push("..");
17 }
18 }
19 other => out.push(other.as_os_str()),
20 }
21 }
22 out
23}
24
25pub fn relpath(path: &Path, start: &Path) -> PathBuf {
28 let a: Vec<_> = abspath(path)
29 .components()
30 .map(|c| c.as_os_str().to_owned())
31 .collect();
32 let b: Vec<_> = abspath(start)
33 .components()
34 .map(|c| c.as_os_str().to_owned())
35 .collect();
36 let common = a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count();
37 let mut out = PathBuf::new();
38 for _ in common..b.len() {
39 out.push("..");
40 }
41 for seg in &a[common..] {
42 out.push(seg);
43 }
44 if out.as_os_str().is_empty() {
45 PathBuf::from(".")
46 } else {
47 out
48 }
49}
50
51pub fn glob_children(dir: &Path, prefix: &str, suffix: &str) -> Vec<PathBuf> {
54 let mut out: Vec<PathBuf> = fs::read_dir(dir)
55 .map(|rd| {
56 rd.filter_map(Result::ok)
57 .map(|e| e.path())
58 .filter(|p| name_matches(p, prefix, suffix))
59 .collect()
60 })
61 .unwrap_or_default();
62 out.sort();
63 out
64}
65
66pub fn glob_recursive(root: &Path, prefix: &str, suffix: &str) -> Vec<PathBuf> {
69 let mut out = Vec::new();
70 walk(root, prefix, suffix, &mut out);
71 out.sort();
72 out
73}
74
75fn walk(dir: &Path, prefix: &str, suffix: &str, out: &mut Vec<PathBuf>) {
76 let Ok(rd) = fs::read_dir(dir) else { return };
77 for e in rd.filter_map(Result::ok) {
78 let p = e.path();
79 let hidden = p
80 .file_name()
81 .map(|n| n.to_string_lossy().starts_with('.'))
82 .unwrap_or(false);
83 if hidden {
84 continue;
85 }
86 if p.is_dir() {
87 walk(&p, prefix, suffix, out);
88 } else if name_matches(&p, prefix, suffix) {
89 out.push(p);
90 }
91 }
92}
93
94fn name_matches(p: &Path, prefix: &str, suffix: &str) -> bool {
95 let Some(name) = p.file_name().and_then(|n| n.to_str()) else {
96 return false;
97 };
98 !name.starts_with('.')
99 && name.starts_with(prefix)
100 && name.ends_with(suffix)
101 && name.len() >= prefix.len() + suffix.len()
102}
103
104pub fn list_dir_raw(dir: &Path) -> Vec<PathBuf> {
107 fs::read_dir(dir)
108 .map(|rd| rd.filter_map(Result::ok).map(|e| e.path()).collect())
109 .unwrap_or_default()
110}
111
112pub fn sort_py(paths: &mut [PathBuf]) {
116 paths.sort_by(|a, b| {
117 a.as_os_str()
118 .as_encoded_bytes()
119 .cmp(b.as_os_str().as_encoded_bytes())
120 });
121}
122
123pub fn pathlib_glob(dir: &Path, prefix: &str, suffix: &str) -> Vec<PathBuf> {
126 let mut out: Vec<PathBuf> = list_dir_raw(dir)
127 .into_iter()
128 .filter(|p| {
129 p.file_name()
130 .and_then(|n| n.to_str())
131 .map(|n| {
132 n.starts_with(prefix)
133 && n.ends_with(suffix)
134 && n.len() >= prefix.len() + suffix.len()
135 })
136 .unwrap_or(false)
137 })
138 .collect();
139 sort_py(&mut out);
140 out
141}
142
143pub fn pathlib_rglob_contains(dir: &Path, infix: &str, suffix: &str) -> Vec<PathBuf> {
147 let mut out = Vec::new();
148 let mut stack = vec![dir.to_path_buf()];
149 while let Some(d) = stack.pop() {
150 for p in list_dir_raw(&d) {
151 if p.is_dir() {
152 stack.push(p);
153 } else if let Some(n) = p.file_name().and_then(|n| n.to_str()) {
154 if let Some(stem) = n.strip_suffix(suffix) {
155 if stem.contains(infix) {
156 out.push(p);
157 }
158 }
159 }
160 }
161 }
162 sort_py(&mut out);
163 out
164}
165
166pub fn basename(p: &Path) -> String {
168 p.file_name()
169 .map(|n| n.to_string_lossy().into_owned())
170 .unwrap_or_default()
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
181 #[cfg(unix)]
182 fn abspath_normalises() {
183 assert_eq!(abspath(Path::new("/a/b/../c/./d/")), Path::new("/a/c/d"));
184 assert_eq!(abspath(Path::new("/a/b/")), Path::new("/a/b"));
185 }
186
187 #[test]
188 fn relpath_cases() {
189 assert_eq!(
190 relpath(Path::new("/a/b/c.mp3"), Path::new("/a/b")),
191 Path::new("c.mp3")
192 );
193 assert_eq!(
194 relpath(Path::new("/a/b"), Path::new("/a/b")),
195 Path::new(".")
196 );
197 assert_eq!(
198 relpath(Path::new("/a/x/y"), Path::new("/a/b/c")),
199 Path::new("../../x/y")
200 );
201 }
202
203 #[test]
204 fn globs_sort_and_filter() {
205 let tmp = tempfile::tempdir().unwrap();
206 let r = tmp.path();
207 fs::create_dir_all(r.join("sub/deeper")).unwrap();
208 fs::create_dir_all(r.join(".hidden")).unwrap();
209 for f in [
210 "parsed_b.json",
211 "parsed_a.json",
212 "orig_parsed_a.json",
213 "sub/parsed_c.json",
214 "sub/deeper/parsed_d.json",
215 ".hidden/parsed_z.json",
216 "parsed_.json",
217 ] {
218 fs::write(r.join(f), "{}").unwrap();
219 }
220 let names = |v: Vec<PathBuf>| {
221 v.iter()
222 .map(|p| relpath(p, r).display().to_string().replace('\\', "/"))
223 .collect::<Vec<_>>()
224 };
225 assert_eq!(
226 names(glob_children(r, "parsed_", ".json")),
227 vec!["parsed_.json", "parsed_a.json", "parsed_b.json"]
228 );
229 assert_eq!(
230 names(glob_recursive(r, "parsed_", ".json")),
231 vec![
232 "parsed_.json",
233 "parsed_a.json",
234 "parsed_b.json",
235 "sub/deeper/parsed_d.json",
236 "sub/parsed_c.json"
237 ]
238 );
239 }
240}