filestructure_rs/
lib.rs

1
2use std::fmt::Display;
3use std::io::Write;
4use std::path::Path;
5use std::fs::{File, create_dir};
6
7#[cfg(tokenstream2)]
8use proc_macro2::TokenStream;
9
10/// A struct which represents a (sub)-filestructure.
11#[derive(Debug, Eq, PartialEq)]
12pub struct FileStructure {
13    path: String,
14    kind: FileType,
15}
16
17/// The types of file this filestructure can represent.
18///
19/// Because [`TokenStream`]s are kinda funky and they just get converted to strings
20/// for writing to disk anyway, they are not supported. Just convert them to string instead.
21#[derive(Debug, Eq, PartialEq)]
22enum FileType {
23    /// Write a specific string to a file.
24    String(String),
25    /// Write binary data to a file.
26    Blob(Box<[u8]>),
27    /// This is a directory which holds multiple other files.
28    Dir(Vec<FileStructure>)
29}
30
31impl Default for FileStructure {
32    fn default() -> Self {
33        Self::new(String::new(), FileType::Dir(vec![]))
34    }
35}
36
37impl FileStructure {
38    /// Basic constructor.
39    #[must_use] 
40    const fn new(path: String, kind: FileType) -> Self {
41        Self { path, kind }
42    }
43
44    /// Constructs a dir.
45    #[must_use]
46    const fn new_dir(name: String) -> Self {
47        Self { path: name, kind: FileType::Dir(Vec::new()) }
48    }
49
50    /// Given a unix path, splits it into it's separate components.
51    ///
52    /// Will also check whether the path contains a starting `/`, returning `true` if so and `false` otherwise.
53    ///
54    /// # Example
55    /// ```ignore // Has to be ignored because private
56    /// use filestructure::FileStructure;
57    /// let s1 = "/some/path";
58    /// let s2 = "some/path";
59    /// 
60    /// assert_eq!(FileStructure::split_path(s1), (vec!["some".to_string(), "path".to_string()], true));
61    /// assert_eq!(FileStructure::split_path(s2), (vec!["some".to_string(), "path".to_string()], false));
62    /// ```
63    fn split_path(path: &str) -> (Vec<String>, bool) {
64        let mut iter = path.split('/');
65        let mut ret = Vec::new();
66        let is_root = iter.next().map_or(true, |s| 
67            if s.is_empty() {
68                true
69            } else {
70                ret.push(s.to_string());
71                false
72            });
73        for sub in iter {
74            if !sub.is_empty() {
75                ret.push(sub.to_string());
76            }
77        }
78        (ret, is_root)
79    }
80
81    fn _from_path(mut components: Vec<String>, is_root: bool) -> Self {
82        components.pop().map_or_else(|| Self::new(String::new(), FileType::Dir(vec![])), |leaf_path| {
83            let mut curr = Self::new(leaf_path, FileType::Dir(vec![]));
84            while let Some(sub_dir) = components.pop() {
85                let new = Self::new(sub_dir, FileType::Dir(vec![curr]));
86                curr = new;
87            }
88            if is_root {
89                Self::new(String::new(), FileType::Dir(vec![curr]))
90            } else {
91                curr
92            }
93        })
94    }
95
96    /// Constructs a `FileStructure` completely from a unix path.
97    ///
98    /// If the path contains a starting `/`, will return a structure with a root node. If not, the root will be the first name of the path.
99    ///
100    /// # Example
101    /// ```
102    /// use filestructure::{FileStructure, FileType};
103    /// let s = "/some/path";
104    /// let fs = FileStructure::from_path(s);
105    /// let expected = FileStructure::new("".to_string(), 
106    ///     FileType::Dir(vec![
107    ///         FileStructure::new("some".to_string(),
108    ///             FileType::Dir(vec![
109    ///                 FileStructure::new("path".to_string(),
110    ///                     FileType::Dir(vec![])
111    ///                 )
112    ///             ])
113    ///         )
114    ///     ])
115    /// );
116    /// assert_eq!(fs, expected);
117    #[must_use]
118    pub fn from_path(path: &str) -> Self {
119        let (components, is_root) = Self::split_path(path);
120        Self::_from_path(components, is_root)
121    }
122
123    /// Given a full `FileStructure` and a starting path, attempts to write it all to disk.
124    ///
125    /// Directories are handled recursively. Everything else is converted to bytes and
126    /// written to a specific file.
127    ///
128    /// # Errors
129    /// Errors whenever we fail to either write to a file or create a dir. See the
130    /// documentation for [`File::create`], [`Write::write_all`] and [`create_dir`] for more
131    /// info.
132    pub fn write_to_disk(&self, root: &Path) -> std::io::Result<()> {
133        let path = root.join(&self.path);
134        match &self.kind {
135            FileType::String(s) => File::create(path)?.write_all(s.as_bytes())?,
136            FileType::Blob(b) => File::create(path)?.write_all(b)?,
137            FileType::Dir(fs) => {
138                if !path.exists() {
139                    create_dir(path.clone())?;
140                }
141                for f in fs {
142                    f.write_to_disk(&path)?;
143                }
144            },
145        }
146        Ok(())
147    }
148
149    /// Tries finding a file in the structure by its full path.
150    ///
151    /// Path should be written unix style. 
152    #[must_use] 
153    pub fn get(&self, path: &str) -> Option<&Self> {
154        let (components, is_root) = Self::split_path(path);
155        let stop = components.len() - 1;
156        let mut iter = components.iter().enumerate();
157        if let Some((_, component)) = iter.next() { // Handle the first bit
158            if &self.path != component {
159                return None
160            }
161        } else if self.path.is_empty() && is_root {
162            return Some(self)
163        } else {
164            return None
165        };
166        let mut curr = self;
167        for (i, component) in iter {
168            if let FileType::Dir(files) = &curr.kind {
169                if let Some(next) = files.iter().find(|file| &file.path == component) {
170                    curr = next;
171                } else {
172                    return None;
173                }
174            } else if i < stop { // We've reached the end of the tree but not the end of the path
175                return None;
176            }
177        }
178        Some(curr)
179    }
180
181    /// Tries finding a file in the structure by its full path, mutably
182    ///
183    /// Path should be written unix style. 
184    #[must_use] 
185    pub fn get_mut(&mut self, path: &str) -> Option<&mut Self> {
186        let (components, is_root) = Self::split_path(path);
187        let stop = components.len() - 1;
188        let mut iter = components.iter().enumerate();
189        if let Some((_, component)) = iter.next() { // Handle the first bit
190            if &self.path != component {
191                return None
192            }
193        } else if self.path.is_empty() && is_root {
194            return Some(self)
195        } else {
196            return None
197        };
198        let mut curr = self;
199        for (i, component) in iter {
200            if let FileType::Dir(ref mut files) = curr.kind {
201                if let Some(next) = files.iter_mut().find(|file| &file.path == component) {
202                    curr = next;
203                } else {
204                    return None;
205                }
206            } else if i < stop { // We've reached the end of the tree but not the end of the path
207                return None;
208            } else {
209                break; // This isn't needed for logic, but it is needed for the borrow checker.
210            }
211        }
212        Some(curr)
213    }
214
215    fn _insert_dir(&mut self, components: Vec<String>) -> Option<&mut Self> {
216        let mut iter = components.into_iter();
217        let mut curr = if let Some(component) = iter.next() {
218            if self.path.is_empty() {
219                if let FileType::Dir(ref mut files) = self.kind {
220                    if let Some(index) = files.iter().position(|file| file.path == component) { // Use the index to avoid borrow checker
221                        &mut files[index]
222                    } else {
223                        let subdir = Self::new_dir(component);
224                        files.push(subdir);
225                        files.last_mut()?
226                    }
227                } else { // We've reached the end of the tree but not the end of the path and it's not a dir.
228                    return None;
229                }
230            } else if self.path != component {
231                if let FileType::Dir(ref mut files) = self.kind { // Duplicate check because borrow checker complains.
232                    let subdir = Self::new_dir(component);
233                    files.push(subdir);
234                    files.last_mut()?
235                } else {
236                    return None;
237                }
238            } else {
239                self
240            }
241        } else {
242            return Some(self)
243        };
244        for component in iter {
245            if let FileType::Dir(ref mut files) = curr.kind {
246                if let Some(index) = files.iter().position(|file| file.path == component) { // Use the index to avoid borrow checker
247                    curr = &mut files[index];
248                } else {
249                    let subdir = Self::new_dir(component); // Need to keep making dirs until we find the end.
250                    files.push(subdir);
251                    curr = files.last_mut()?;
252                }
253            } else { // We've reached the end of the tree but not the end of the path and it's not a dir.
254                return None;
255            }
256        }
257        Some(curr)
258    }
259
260    fn insert_data(&mut self, path: &str, data: FileType) -> Option<&mut Self> {
261        let (mut components, _) = Self::split_path(path);
262        let filename = components.pop()?;
263        let dir = self._insert_dir(components)?;
264        dir.insert(Self::new(filename, data))
265    }
266
267    /// Inserts a directory into the `FileStructure`. Returns a `None` if we fail, returns a mutable reference to the bottom directory if we succeed.
268    ///
269    /// Functions like `mkdir -p`, meaning that it will automatically create directories as needed until the full path has been added.
270    pub fn insert_dir(&mut self, path: &str) -> Option<&mut Self> {
271        let (components, _) = Self::split_path(path);
272        self._insert_dir(components)
273    }
274
275    /// Insert binary blob data at some path relative to the root `FileStructure`.
276    ///
277    /// Will automatically create directories if needed.
278    pub fn insert_blob(&mut self, path: &str, blob: Box<[u8]>) -> Option<&mut Self> {
279        self.insert_data(path, FileType::Blob(blob))
280    }
281
282    /// Insert a [`String`] at some path relative to the root `FileStructure`.
283    ///
284    /// Will automatically create directories if needed.
285    pub fn insert_string(&mut self, path: &str, data: String) -> Option<&mut Self> {
286        self.insert_data(path, FileType::String(data))
287    }
288
289    /// Insert a [`TokenStream`] at some path relative to the root `FileStructure`.
290    /// 
291    /// The [`TokenStream`] will be converted to a [`String`]. If the `pretty` flag is set, the `TokenStream` will be prettified first.
292    ///
293    /// Will automatically create directories if needed.
294    ///
295    /// # Errors
296    /// Returns a [`syn::parse::Error`] if the `pretty` flag is set and we fail to parse the [`TokenStream`].
297    #[cfg(tokenstream2)]
298    pub fn insert_tokenstream(&mut self, path: &str, data: TokenStream, pretty: bool) -> syn::parse::Result<Option<&mut Self>> {
299        let data = if pretty {
300            let ast: syn::File = syn::parse2(data)?;
301            FileType::String(prettyplease::unparse(&ast))
302        } else {
303            FileType::String(data.to_string())
304        };
305        self.insert_data(path, data).map_or(Ok(None), |dir| Ok(Some(dir)))
306    }
307
308    /// Insert a `FileStructure` as a child to this `FileStructure`.
309    ///
310    /// Returns a `None` if this `FileStructure` is not a directory. Returns a mutable reference to the child otherwise.
311    pub fn insert(&mut self, child: Self) -> Option<&mut Self> {
312        if let FileType::Dir(ref mut files) = self.kind {
313            files.push(child);
314            Some(files.last_mut()?)
315        } else {
316            None
317        }
318    }
319
320    /// Get the path for this node of the structure.
321    #[must_use] 
322    pub fn get_path(&self) -> &str {
323        &self.path
324    }
325
326    /// Get the "len" (file count) of the structure.
327    ///
328    /// TODO: Precalculate this instead of exploring the whole tree.
329    #[must_use]
330    pub fn len(&self) -> usize {
331        match &self.kind {
332            FileType::Dir(files) => {
333                let mut sum = 0;
334                for file in files {
335                    sum += file.len();
336                }
337                sum
338            },
339            _ => 1
340        }
341    }
342
343    /// A filestructure is deemed empty if there are 0 and 0 subdirs in it.
344    #[must_use]
345    pub fn is_empty(&self) -> bool {
346        self.len() == 0
347    }
348
349    /// Convert the `FileStructure` into a usable iterator.
350    ///
351    /// This will iterate over all the nodes in a DFS style.
352    #[must_use]
353    pub fn iter(&self) -> FileIterator {
354        FileIterator { stack: vec![self] }
355    }
356
357}
358
359pub struct FileIterator<'a> {
360    stack: Vec<&'a FileStructure>
361}
362
363pub struct OwnedFileIterator {
364    stack: Vec<FileStructure>
365}
366
367impl<'a> Iterator for FileIterator<'a> {
368    type Item = &'a FileStructure;
369
370    fn next(&mut self) -> Option<Self::Item> {
371        let cont = self.stack.pop()?;
372        if let FileType::Dir(subdirs) = &cont.kind {
373            self.stack.extend(subdirs.iter());
374        }
375        Some(cont)
376    }
377}
378
379impl Iterator for OwnedFileIterator {
380    type Item = FileStructure;
381
382    fn next(&mut self) -> Option<Self::Item> {
383        let mut cont = self.stack.pop()?;
384        if let FileType::Dir(ref mut subdirs) = cont.kind {
385            self.stack.extend(std::mem::take(subdirs));
386        }
387        Some(cont)
388    }
389}
390
391impl IntoIterator for FileStructure {
392    type Item = Self;
393
394    type IntoIter = OwnedFileIterator;
395
396    fn into_iter(self) -> Self::IntoIter {
397        OwnedFileIterator { stack: vec![self] }
398    }
399}
400
401impl<'a> IntoIterator for &'a FileStructure {
402    type Item = Self;
403
404    type IntoIter = FileIterator<'a>;
405
406    fn into_iter(self) -> Self::IntoIter {
407        self.iter()
408    }
409}
410
411impl Display for FileStructure {
412    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413        match &self.kind {
414            FileType::Dir(files) => {
415                writeln!(f, "{}: [", self.path)?;
416                for file in files {
417                    writeln!(f, "{file},")?;
418                }
419                writeln!(f, "]")
420            },
421            _ => write!(f, "{}", self.path)
422        }
423    }
424}
425
426#[cfg(test)]
427mod test {
428    use super::{FileStructure, FileType};
429
430    #[test]
431    fn test_get() {
432        let structure = FileStructure {
433            path: "some".to_string(),
434            kind: FileType::Dir(vec![
435                FileStructure {
436                    path: "dir".to_string(),
437                    kind: FileType::Dir(vec![
438                        FileStructure {
439                            path: "file.txt".to_string(),
440                            kind: FileType::Blob([].into()),
441                        },
442                        FileStructure {
443                            path: "other.txt".to_string(),
444                            kind: FileType::Blob([].into()),
445                        }
446                    ]),
447                },
448                FileStructure {
449                    path: "other".to_string(),
450                    kind: crate::FileType::Dir(vec![
451                        FileStructure {
452                            path: "file.txt".to_string(),
453                            kind: FileType::Blob([].into()),
454                        }
455                    ]),
456                }
457            ]),
458        };
459        let target = FileStructure {
460            path: "file.txt".to_string(),
461            kind: FileType::Blob([].into()),
462        };
463        assert_eq!(structure.get("some/dir/file.txt"), Some(&target));
464        assert_eq!(structure.get("some/dir/none.txt"), None);
465        assert_eq!(structure.get("nothing"), None);
466    }
467
468    #[test]
469    fn test_insert() {
470        let mut structure = FileStructure::from_path("some");
471        structure.insert_dir("some/dir/");
472        structure.insert_dir("some/other");
473        structure.insert_blob("some/dir/file.txt", [].into()).unwrap();
474        structure.insert_blob("some/dir/other.txt", [].into()).unwrap();
475        structure.insert_blob("some/other/file.txt", [].into()).unwrap();
476        let expected = FileStructure {
477            path: "some".to_string(),
478            kind: FileType::Dir(vec![
479                FileStructure {
480                    path: "dir".to_string(),
481                    kind: FileType::Dir(vec![
482                        FileStructure {
483                            path: "file.txt".to_string(),
484                            kind: FileType::Blob([].into()),
485                        },
486                        FileStructure {
487                            path: "other.txt".to_string(),
488                            kind: FileType::Blob([].into()),
489                        }
490                    ]),
491                },
492                FileStructure {
493                    path: "other".to_string(),
494                    kind: crate::FileType::Dir(vec![
495                        FileStructure {
496                            path: "file.txt".to_string(),
497                            kind: FileType::Blob([].into()),
498                        }
499                    ]),
500                }
501            ]),
502        };
503        assert_eq!(structure, expected);
504    }
505
506    #[test]
507    fn test_insert_empty() {
508        let mut structure = FileStructure::default();
509        structure.insert_dir("some/dir/");
510        structure.insert_dir("some/other");
511        structure.insert_blob("some/dir/file.txt", [].into()).unwrap();
512        structure.insert_blob("some/dir/other.txt", [].into()).unwrap();
513        structure.insert_blob("some/other/file.txt", [].into()).unwrap();
514        let expected = FileStructure {
515            path: String::new(),
516            kind: FileType::Dir(vec![
517                FileStructure {
518                    path: "some".to_string(),
519                    kind: FileType::Dir(vec![
520                        FileStructure {
521                            path: "dir".to_string(),
522                            kind: FileType::Dir(vec![
523                                FileStructure {
524                                    path: "file.txt".to_string(),
525                                    kind: FileType::Blob([].into()),
526                                },
527                                FileStructure {
528                                    path: "other.txt".to_string(),
529                                    kind: FileType::Blob([].into()),
530                                }
531                            ]),
532                        },
533                        FileStructure {
534                            path: "other".to_string(),
535                            kind: crate::FileType::Dir(vec![
536                                FileStructure {
537                                    path: "file.txt".to_string(),
538                                    kind: FileType::Blob([].into()),
539                                }
540                            ]),
541                        }
542                    ]),
543                }
544            ])
545        };
546        assert_eq!(structure, expected);
547    }
548}