wildpath/
lib.rs

1use std::{ffi::OsStr, fs, path::{Path, PathBuf}};
2use regex::Regex;
3
4pub fn resolve(path: &Path) -> Option<Vec<PathBuf>> {
5    let mut fin: Vec<PathBuf> = Vec::new();
6
7    match path.iter().nth(0) {
8        Some(e) => {
9            let mut t = PathBuf::new();
10            t.push(e);
11            _ = t.canonicalize();
12            fin.push(t)
13        },
14        None => { return Some(fin); }
15    }
16
17    for pe in path.iter().skip(1) {
18        if fin.is_empty() { return Some(fin); }
19
20        fin = get_next_file_layer(fin, pe);
21    }
22    
23    Some(fin)
24}
25
26fn get_next_file_layer(current_layer: Vec<PathBuf>, next_element: &OsStr) -> Vec<PathBuf> {
27    let mut new_layer: Vec<PathBuf> = Vec::new();
28
29    for p in &current_layer {
30        if p.is_file() { continue; }
31        
32        let mut candidates = match next_element.to_str().unwrap().contains("*") {
33            false => {
34                if p.join(next_element).try_exists().expect(format!("Failed to determine if {:?} exists in {:?}", next_element, p).as_str()) {
35                    vec![PathBuf::from(next_element)]
36                } else {
37                    vec![]
38                }
39            },
40            true => {
41                let re = Regex::new(
42                    &format!("^{}$", &next_element.to_str().unwrap().replace(".", "[.]").replace("*", ".*"))
43                ).expect(format!("Failed to create regex from {:?}", next_element).as_str());
44
45                let regex_filter = |x: PathBuf| -> Option<PathBuf> {
46                    if re.is_match(x.iter().last().expect(format!("Failed to parse with regex {:?}", x).as_str()).to_str().unwrap()) {
47                        return Some(p.join(x))
48                    } else {
49                        return None
50                    };
51                };
52
53                fs::read_dir(p).expect(format!("Failed to read directory: {:?}", p).as_str())
54                    .map(|x| PathBuf::from(x.unwrap().file_name()))
55                    .filter_map(regex_filter)
56                    .collect()
57            }
58        };
59
60        candidates = candidates.into_iter()
61            .filter_map(|x|
62                if x.is_symlink() {
63                    Some(x.read_link().expect(format!("Failed to follow symlink from {:?}", x).as_str()))
64                }
65                else {
66                    Some(x)
67                }
68            )
69            .map(|x| p.join(x))
70            .collect();
71
72        new_layer.append(&mut candidates);
73    }
74    
75    new_layer
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use rand::distributions::{Alphanumeric, DistString};
82    use std::fs;
83
84    fn test_setup() -> PathBuf {
85        let test_dir = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
86
87        let mut tdr = std::env::temp_dir();
88        tdr.push(test_dir);
89        _ = fs::create_dir(&tdr);
90
91        tdr
92    }
93
94    fn validate(mut output: Vec<PathBuf>, mut solution: Vec<PathBuf>, test_dir: Option<PathBuf>) {
95        match test_dir {
96            Some(p) => { _ = fs::remove_dir_all(p); }
97            None => ()
98        }
99
100        output.sort_by_key( |k| k.as_os_str().to_owned());
101        solution.sort_by_key( |k| k.as_os_str().to_owned());
102
103        dbg!(&output);
104        dbg!(&solution);
105
106        assert_eq!(output, solution);
107    }
108
109    #[test]
110    fn ending_asterisk() {
111        let tdr = test_setup();
112        let test_path = tdr.clone().join("*");
113
114        let solution: Vec<PathBuf> = vec![
115            tdr.join("A"),
116            tdr.join("B"),
117        ];
118
119        for p in &solution {
120            _ = fs::create_dir(p);
121        }
122        
123        validate(
124            resolve(&test_path).unwrap(),
125            solution,
126            Some(tdr)
127        );
128    }
129
130    #[test]
131    fn dot_path() {
132        let tdr = test_setup();
133        let test_path = tdr.clone().join("*.*").join("*");
134
135        _ = fs::create_dir(tdr.join("A.B"));
136        _ = fs::create_dir(tdr.join("A.B").join("C"));
137
138        let solution: Vec<PathBuf> = vec![
139            tdr.join("A.B").join("C")
140        ];
141
142        validate(
143            resolve(&test_path).unwrap(),
144            solution,
145            Some(tdr)
146        )
147    }
148
149    #[test]
150    fn double_asterisk() {
151        let tdr = test_setup();
152        let test_input = tdr.clone().join("*").join("*");
153
154        _ = fs::create_dir(tdr.join("A"));
155        _ = fs::create_dir(tdr.join("B"));
156
157        let solution: Vec<PathBuf> = vec![
158            tdr.join("A").join("C"),
159            tdr.join("B").join("D"),
160        ];
161
162        for p in &solution {
163            _ = fs::create_dir(p);
164        }
165        
166        validate(
167            resolve(&test_input).unwrap(),
168            solution,
169            Some(tdr)
170        );
171    }
172
173    #[test]
174    fn not_all_items() {
175        let tdr = test_setup();
176        let test_input = tdr.clone().join("X*");
177
178        let mut paths: Vec<PathBuf> = vec![
179            tdr.join("XA"),
180            tdr.join("X")
181        ];
182
183        let solution = paths.clone();
184
185        paths.push(tdr.join("YB"));
186        paths.push(tdr.join("YX"));
187        for p in paths {
188            _ = fs::create_dir(p);
189        }
190
191        validate(
192            resolve(&test_input).unwrap(),
193            solution,
194            Some(tdr)
195        );
196    }
197
198    #[test]
199    fn double_compound() {
200        let tdr = test_setup();
201        let test_input = tdr.clone().join("X*").join("Y").join("*Z");
202
203        let first_layer: Vec<PathBuf> = vec![
204            tdr.join("X"),
205            tdr.join("XA"),
206            tdr.join("YB"),
207            tdr.join("YX"),
208        ];
209        for p in &first_layer { _ = fs::create_dir(p); }
210
211        let second_layer: Vec<PathBuf> = vec![
212            tdr.join("X").join("Y"),
213            tdr.join("X").join("TY"),
214            tdr.join("XA").join("Y"),
215            tdr.join("YB").join("Y")
216        ];
217        for p in &second_layer { _ = fs::create_dir(p); }
218
219        let mut third_layer: Vec<PathBuf> = vec![
220            tdr.join("X").join("Y").join("Z"),
221            tdr.join("XA").join("Y").join("Z"),
222            tdr.join("XA").join("Y").join("TZ"),
223            tdr.join("XA").join("Y").join("ZAZ"),
224            tdr.join("XA").join("Y").join("ZZZ"),
225        ];
226
227        let solution = third_layer.clone();
228
229        third_layer.push(tdr.join("X").join("Y").join("z"));
230        third_layer.push(tdr.join("X").join("TY").join("Z"));
231        third_layer.push(tdr.join("XA").join("Y").join("ZA"));
232
233        for p in &third_layer { _ = fs::create_dir(p); }
234
235        validate(
236            resolve(&test_input).unwrap(),
237            solution,
238            Some(tdr)
239        );
240    }
241
242    #[test]
243    fn files() {
244        let tdr = test_setup();
245        let test_path = tdr.clone().join("A").join("*.jp*g");
246
247        _ = fs::create_dir(tdr.join("A"));
248        _ = fs::File::create(tdr.join("A").join("a.jpg"));
249        _ = fs::File::create(tdr.join("A").join("b.jpeg"));
250        _ = fs::File::create(tdr.join("A").join("c.jg"));
251
252        let solution: Vec<PathBuf> = vec![
253            tdr.join("A").join("a.jpg"),
254            tdr.join("A").join("b.jpeg")
255        ];
256
257        validate(
258            resolve(&test_path).unwrap(),
259            solution,
260            Some(tdr)
261        );
262    }
263
264    #[cfg(target_family = "unix")]
265    #[test]
266    fn symlinks_unix() {
267        let tdr = test_setup();
268        let test_path = tdr.clone().join("A").join("*").join("*");
269
270        _ = fs::create_dir(tdr.join("A"));
271        _ = fs::create_dir(tdr.join("B"));
272        _ = fs::create_dir(tdr.join("B").join("C"));
273
274        _ = std::os::unix::fs::symlink(
275            tdr.join("B"),
276            tdr.join("A").join("X")
277        );
278
279        let solution: Vec<PathBuf> = vec![tdr.join("B").join("C")];
280
281        validate(
282            resolve(&test_path).unwrap(),
283            solution,
284            Some(tdr)
285        );
286    }
287
288    #[test]
289    fn doc_example() {
290        let tdr = test_setup();
291
292        _ = fs::create_dir(tdr.join("blogs"));
293
294        _ = fs::create_dir(tdr.join("blogs").join("blog_1"));
295        _ = fs::create_dir(tdr.join("blogs").join("blog_1").join("assets"));
296        _ = fs::File::create(tdr.join("blogs").join("blog_1").join("post.txt"));
297        _ = fs::File::create(tdr.join("blogs").join("blog_1").join("assets").join("logo.jpeg"));
298
299        _ = fs::create_dir(tdr.join("blogs").join("blog_2"));
300        _ = fs::create_dir(tdr.join("blogs").join("blog_2").join("assets"));
301        _ = fs::File::create(tdr.join("blogs").join("blog_2").join("post.txt"));
302        _ = fs::File::create(tdr.join("blogs").join("blog_2").join("assets").join("research_notes.txt"));
303        
304        _ = fs::create_dir(tdr.join("videos"));
305        
306        _ = fs::create_dir(tdr.join("videos").join("video_1"));
307        _ = fs::create_dir(tdr.join("videos").join("video_1").join("assets"));
308        _ = fs::File::create(tdr.join("videos").join("video_1").join("script.txt"));
309        _ = fs::File::create(tdr.join("videos").join("video_1").join("assets").join("logo.jpeg"));
310
311        _ = fs::create_dir(tdr.join("videos").join("video_2"));
312        _ = fs::create_dir(tdr.join("videos").join("video_2").join("assets"));
313        _ = fs::File::create(tdr.join("videos").join("video_2").join("script.txt"));
314        _ = fs::File::create(tdr.join("videos").join("video_2").join("assets").join("sound_effect.wav"));
315
316        _ = fs::create_dir(tdr.join("videos").join("video_3"));
317        _ = fs::create_dir(tdr.join("videos").join("video_3").join("assets"));
318        _ = fs::File::create(tdr.join("videos").join("video_3").join("script.txt"));
319        _ = fs::File::create(tdr.join("videos").join("video_3").join("assets").join("new_logo.png"));
320
321
322        let mut test_path = tdr.clone().join("*").join("*").join("*.txt");
323        let mut solution: Vec<PathBuf> = vec![
324            tdr.join("blogs").join("blog_1").join("post.txt"),
325            tdr.join("blogs").join("blog_2").join("post.txt"),
326            tdr.join("videos").join("video_1").join("script.txt"),
327            tdr.join("videos").join("video_2").join("script.txt"),
328            tdr.join("videos").join("video_3").join("script.txt")
329        ];
330
331        validate(
332            resolve(&test_path).unwrap(),
333            solution,
334            None
335        );
336
337        test_path = tdr.clone().join("*").join("*").join("assets").join("*logo*");
338        solution = vec![
339            tdr.join("blogs").join("blog_1").join("assets").join("logo.jpeg"),
340            tdr.join("videos").join("video_1").join("assets").join("logo.jpeg"),
341            tdr.join("videos").join("video_3").join("assets").join("new_logo.png")
342        ];
343
344        validate(
345            resolve(&test_path).unwrap(),
346            solution,
347            None
348        );
349
350        test_path = tdr.clone().join("*").join("*_1").join("assets").join("*logo*");
351        solution = vec![
352            tdr.join("blogs").join("blog_1").join("assets").join("logo.jpeg"),
353            tdr.join("videos").join("video_1").join("assets").join("logo.jpeg")
354        ];
355
356        validate(
357            resolve(&test_path).unwrap(),
358            solution,
359            None
360        );
361
362        _ = fs::create_dir(tdr.join("presentations"));
363        _ = fs::create_dir(tdr.join("presentations").join("presentation_1"));
364        _ = fs::create_dir(tdr.join("presentations").join("presentation_1").join("assets"));
365        _ = fs::File::create(tdr.join("presentations").join("presentation_1").join("assets").join("logo.jpeg"));
366
367        solution = vec![
368            tdr.join("blogs").join("blog_1").join("assets").join("logo.jpeg"),
369            tdr.join("videos").join("video_1").join("assets").join("logo.jpeg"),
370            tdr.join("presentations").join("presentation_1").join("assets").join("logo.jpeg")
371        ];
372
373        validate(
374            resolve(&test_path).unwrap(),
375            solution,
376            Some(tdr)
377        );
378    }
379}