prefix_file_tree/
iter.rs

1use crate::{Entry, scheme::Scheme};
2use std::ffi::OsString;
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, thiserror::Error)]
6pub enum Error {
7    #[error("I/O error")]
8    Io(#[from] std::io::Error),
9    #[error("Invalid prefix part path")]
10    InvalidPrefixPart(PathBuf),
11    #[error("Invalid file stem")]
12    InvalidFileStem(PathBuf),
13    #[error("Expected file")]
14    ExpectedFile(PathBuf),
15    #[error("Expected directory")]
16    ExpectedDirectory(PathBuf),
17    #[error("Invalid extension")]
18    InvalidExtension(Option<OsString>),
19    #[error("Invalid file stem length")]
20    InvalidFileStemLength(Option<usize>),
21    #[error("Scheme parse error")]
22    Scheme(#[from] crate::scheme::Error),
23}
24
25pub struct Entries<'a, S> {
26    stack: Vec<Vec<PathBuf>>,
27    level: Option<usize>,
28    tree: &'a crate::Tree<S>,
29}
30
31impl<'a, S> Entries<'a, S> {
32    pub(crate) fn new(tree: &'a crate::Tree<S>) -> Self {
33        Self {
34            stack: vec![vec![tree.base.clone()]],
35            level: None,
36            tree,
37        }
38    }
39
40    fn is_last(&self) -> bool {
41        self.level == Some(self.tree.prefix_part_lengths.len())
42    }
43
44    fn current_prefix_part_length(&self) -> Option<usize> {
45        self.level
46            .and_then(|level| self.tree.prefix_part_lengths.get(level))
47            .copied()
48    }
49
50    fn increment_level(&mut self) {
51        self.level = Some(self.level.take().map_or(0, |level| level + 1));
52    }
53
54    const fn decrement_level(&mut self) {
55        if let Some(level) = self.level.take()
56            && level != 0
57        {
58            self.level = Some(level - 1);
59        }
60    }
61
62    fn validate_extension<P: AsRef<Path>>(&self, path: P) -> Result<(), Option<OsString>> {
63        match &self.tree.extension_constraint {
64            None => Ok(()),
65            Some(crate::constraint::Extension::None) => path
66                .as_ref()
67                .extension()
68                .map_or(Ok(()), |extension| Err(Some(extension.to_os_string()))),
69            Some(crate::constraint::Extension::Any) => {
70                path.as_ref().extension().map_or(Err(None), |_| Ok(()))
71            }
72            Some(crate::constraint::Extension::Fixed(expected_extension)) => {
73                path.as_ref().extension().map_or(Err(None), |extension| {
74                    if **expected_extension == *extension {
75                        Ok(())
76                    } else {
77                        Err(Some(extension.to_os_string()))
78                    }
79                })
80            }
81        }
82    }
83
84    fn validate_file_stem_length<P: AsRef<Path>>(&self, path: P) -> Result<(), Option<usize>> {
85        match &self.tree.length_constraint {
86            None => Ok(()),
87            Some(crate::constraint::Length::Fixed(length)) => {
88                path.as_ref().file_stem().map_or(Err(None), |file_stem| {
89                    if file_stem.len() == *length {
90                        Ok(())
91                    } else {
92                        Err(Some(file_stem.len()))
93                    }
94                })
95            }
96            Some(crate::constraint::Length::Range(minimum, maximum)) => {
97                path.as_ref().file_stem().map_or(Err(None), |file_stem| {
98                    if file_stem.len() >= *minimum && file_stem.len() < *maximum {
99                        Ok(())
100                    } else {
101                        Err(Some(file_stem.len()))
102                    }
103                })
104            }
105        }
106    }
107}
108
109impl<S: Scheme> Iterator for Entries<'_, S> {
110    type Item = Result<Entry<S::Name>, Error>;
111
112    fn next(&mut self) -> Option<Self::Item> {
113        self.stack.pop().and_then(|mut next_paths| {
114            if let Some(next_path) = next_paths.pop() {
115                if self.is_last() {
116                    self.stack.push(next_paths);
117
118                    Some(self.path_to_entry(next_path))
119                } else {
120                    self.increment_level();
121
122                    self.path_to_paths(next_path, self.current_prefix_part_length())
123                        .map_or_else(
124                            |error| Some(Err(error)),
125                            |next_level| {
126                                self.stack.push(next_paths);
127                                self.stack.push(next_level);
128
129                                self.next()
130                            },
131                        )
132                }
133            } else {
134                self.decrement_level();
135
136                self.next()
137            }
138        })
139    }
140}
141
142impl<S: Scheme> Entries<'_, S> {
143    fn path_to_entry(&self, path: PathBuf) -> Result<Entry<S::Name>, Error> {
144        if path.is_file() {
145            self.validate_extension(&path)
146                .map_err(Error::InvalidExtension)?;
147
148            self.validate_file_stem_length(&path)
149                .map_err(Error::InvalidFileStemLength)?;
150
151            let file_stem = path
152                .file_stem()
153                .ok_or_else(|| Error::InvalidFileStem(path.clone()))?;
154
155            let name = self.tree.scheme.name_from_file_stem(file_stem)?;
156
157            Ok(Entry { name, path })
158        } else {
159            Err(Error::ExpectedFile(path))
160        }
161    }
162    fn path_to_paths(
163        &self,
164        path: PathBuf,
165        prefix_part_length: Option<usize>,
166    ) -> Result<Vec<PathBuf>, Error> {
167        if path.is_dir() {
168            let mut paths = std::fs::read_dir(path)?
169                .map(|entry| entry.map(|entry| entry.path()))
170                .collect::<Result<Vec<PathBuf>, std::io::Error>>()
171                .map_err(Error::from)?;
172
173            // If our ordering for prefix parts fails, we simply leave them in the original order.
174            //
175            // The error should be caught by later validation.
176            paths.sort_by(|a, b| {
177                let directory_name_a = a.file_name();
178                let directory_name_b = b.file_name();
179
180                directory_name_a
181                    .zip(directory_name_b)
182                    .and_then(|(directory_name_a, directory_name_b)| {
183                        self.tree
184                            .scheme
185                            .cmp_prefix_part(directory_name_a, directory_name_b)
186                            .ok()
187                    })
188                    .unwrap_or(std::cmp::Ordering::Equal)
189                    .reverse()
190            });
191
192            match prefix_part_length {
193                Some(prefix_part_length) => {
194                    let invalid_path = paths.iter().find(|path| {
195                        path.file_name()
196                            .is_none_or(|directory_name| directory_name.len() != prefix_part_length)
197                    });
198
199                    // Clippy is wrong here, since `map_or` would require us to clone `paths`.
200                    #[allow(clippy::option_if_let_else)]
201                    match invalid_path {
202                        Some(invalid_path) => Err(Error::InvalidPrefixPart(invalid_path.clone())),
203                        None => Ok(paths),
204                    }
205                }
206                None => Ok(paths),
207            }
208        } else {
209            Err(Error::ExpectedDirectory(path))
210        }
211    }
212}