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 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 #[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}