1use std::collections::{HashMap, VecDeque};
7use std::path::{Path, PathBuf};
8
9use crate::error::{Error, Issue, Result, code};
10use crate::meta::{Meta, MetaLoad, load};
11use crate::util::{self, META_FILE};
12
13pub const HARD_MAX_DEPTH: usize = 128;
15
16pub struct Visit {
18 pub dir: PathBuf,
20 pub rel: String,
22 pub depth: usize,
24 pub meta: Option<Box<Meta>>,
26 pub parent: Option<usize>,
28 pub readable: bool,
30}
31
32pub struct Scan {
34 pub root: PathBuf,
36 pub visits: Vec<Visit>,
38 pub issues: Vec<Issue>,
40 pub by_id: HashMap<String, Vec<usize>>,
42 pub root_index: Option<usize>,
44 pub max_depth: usize,
46}
47
48impl Scan {
49 pub fn resolve(&self, id: &str) -> Option<usize> {
51 self.by_id.get(id).and_then(|v| v.first().copied())
52 }
53
54 pub fn ancestors(&self, mut idx: usize) -> Vec<usize> {
56 let mut out = vec![idx];
57 while let Some(p) = self.visits[idx].parent {
58 out.push(p);
59 idx = p;
60 }
61 out.reverse();
62 out
63 }
64}
65
66#[derive(Debug, Clone)]
68pub struct Bundle {
69 pub root: PathBuf,
71}
72
73impl Bundle {
74 pub fn new(root: impl Into<PathBuf>) -> Result<Self> {
76 let root = root.into();
77 if !root.exists() {
78 return Err(Error::NotFound(root.display().to_string()));
79 }
80 if !root.is_dir() {
81 return Err(Error::BadArg(format!(
82 "{} 不是目录(`.str` 是目录 bundle)",
83 root.display()
84 )));
85 }
86 let root = std::fs::canonicalize(&root).map_err(|e| Error::io(&root, e))?;
89 Ok(Self { root })
90 }
91
92 pub fn name(&self) -> String {
94 self.root
95 .file_name()
96 .map(|s| s.to_string_lossy().to_string())
97 .unwrap_or_else(|| self.root.display().to_string())
98 }
99
100 pub fn meta_path(&self, dir: &Path) -> PathBuf {
102 dir.join(META_FILE)
103 }
104
105 pub fn has_meta(&self, dir: &Path) -> bool {
107 self.meta_path(dir).is_file()
108 }
109
110 pub fn rel(&self, path: &Path) -> String {
112 util::rel_display(&self.root, path)
113 }
114
115 pub fn read_meta(&self, dir: &Path) -> Result<MetaLoad> {
117 let p = self.meta_path(dir);
118 let rel = self.rel(&p);
119 load(&p, &rel)
120 }
121
122 pub fn list_names(&self, dir: &Path) -> Result<Vec<(String, bool)>> {
124 let mut out = Vec::new();
125 let rd = std::fs::read_dir(dir).map_err(|e| Error::io(dir, e))?;
126 for ent in rd {
127 let ent = ent.map_err(|e| Error::io(dir, e))?;
128 let name = ent.file_name().to_string_lossy().to_string();
129 let is_dir = ent
130 .file_type()
131 .map(|t| t.is_dir())
132 .unwrap_or(false);
133 out.push((name, is_dir));
134 }
135 out.sort();
136 Ok(out)
137 }
138
139 pub fn child_dirs(&self, dir: &Path) -> Result<Vec<PathBuf>> {
141 Ok(self
142 .list_names(dir)?
143 .into_iter()
144 .filter(|(_, is_dir)| *is_dir)
145 .map(|(n, _)| dir.join(n))
146 .collect())
147 }
148
149 fn contains_meta_deeper(&self, dir: &Path) -> bool {
151 for entry in walkdir::WalkDir::new(dir)
152 .max_depth(8)
153 .into_iter()
154 .filter_entry(|e| {
155 if e.depth() == 0 {
156 return true;
157 }
158 let name = e.file_name().to_string_lossy().to_string();
159 !util::is_os_noise(&name) && !util::is_sub_bundle(&name)
161 })
162 .flatten()
163 {
164 if entry.depth() == 0 || !entry.file_type().is_dir() {
165 continue;
166 }
167 if entry.path().join(META_FILE).is_file() {
168 return true;
169 }
170 }
171 false
172 }
173
174 pub fn scan(&self) -> Result<Scan> {
176 let mut visits: Vec<Visit> = Vec::new();
177 let mut issues: Vec<Issue> = Vec::new();
178 let mut by_id: HashMap<String, Vec<usize>> = HashMap::new();
179
180 let root_meta = self.meta_path(&self.root);
181 if !root_meta.is_file() {
182 issues.push(Issue::error(
183 code::META_MISSING,
184 ".",
185 "bundle 根目录缺少 `._meta`",
186 ));
187 return Ok(Scan {
188 root: self.root.clone(),
189 visits,
190 issues,
191 by_id,
192 root_index: None,
193 max_depth: 0,
194 });
195 }
196
197 let (root_parsed, mut root_issues) = match self.read_meta(&self.root)? {
198 MetaLoad::Ok(m, i) => (Some(m), i),
199 MetaLoad::Failed(i) => (None, i),
200 };
201 issues.append(&mut root_issues);
202 if let Some(m) = &root_parsed {
203 if let Some(id) = &m.id {
204 by_id.entry(id.clone()).or_default().push(0);
205 }
206 }
207 visits.push(Visit {
208 dir: self.root.clone(),
209 rel: ".".to_string(),
210 depth: 0,
211 meta: root_parsed,
212 parent: None,
213 readable: true,
214 });
215
216 let mut queue: VecDeque<usize> = VecDeque::from([0usize]);
217 let mut max_depth = 0usize;
218
219 while let Some(cur) = queue.pop_front() {
220 let dir = visits[cur].dir.clone();
221 let depth = visits[cur].depth;
222 if depth >= HARD_MAX_DEPTH {
223 continue;
224 }
225 for child in self.child_dirs(&dir)? {
226 let name = child
227 .file_name()
228 .map(|s| s.to_string_lossy().to_string())
229 .unwrap_or_default();
230 if util::is_os_noise(&name) {
231 continue;
232 }
233 if util::is_sub_bundle(&name) {
234 continue;
236 }
237 let rel = self.rel(&child);
238 if !self.has_meta(&child) {
239 if !util::is_reserved_name(&name) && self.contains_meta_deeper(&child) {
241 issues.push(Issue::error(
242 code::META_MISSING,
243 rel,
244 "父目录不是分支(缺少 `._meta`),其内出现 `._meta`,无法建立分支层级",
245 ));
246 }
247 continue;
248 }
249 let d = depth + 1;
250 let (parsed, mut iss) = match self.read_meta(&child)? {
251 MetaLoad::Ok(m, i) => (Some(m), i),
252 MetaLoad::Failed(i) => (None, i),
253 };
254 issues.append(&mut iss);
255 let idx = visits.len();
256 if let Some(m) = &parsed {
257 if let Some(id) = &m.id {
258 by_id.entry(id.clone()).or_default().push(idx);
259 }
260 }
261 visits.push(Visit {
262 dir: child.clone(),
263 rel,
264 depth: d,
265 meta: parsed,
266 parent: Some(cur),
267 readable: true,
268 });
269 max_depth = max_depth.max(d);
270 queue.push_back(idx);
271 }
272 }
273
274 Ok(Scan {
275 root: self.root.clone(),
276 visits,
277 issues,
278 by_id,
279 root_index: Some(0),
280 max_depth,
281 })
282 }
283}