1use std::collections::BTreeMap;
17use std::path::{Path, PathBuf};
18use std::sync::Arc;
19
20use rich_ext::cli_doc::{Explanation, Layer as PrecedenceLayer, Precedence};
21
22use crate::error::MicroError;
23use crate::model::{Layer, MicroAsset, Origin};
24use crate::name::check_name;
25use crate::package::{self, Limits, Loaded};
26
27#[derive(Clone, Debug, Default, PartialEq, Eq)]
30pub struct MicroRoots {
31 pub builtin_set: bool,
34 pub builtin: Option<PathBuf>,
37 pub user: Option<PathBuf>,
39 pub project: Option<PathBuf>,
41 pub project_trusted: bool,
44}
45
46impl MicroRoots {
47 pub fn from_env(project_root: Option<&Path>) -> Self {
52 let home = std::env::var_os("HOME")
53 .or_else(|| std::env::var_os("USERPROFILE"))
54 .map(PathBuf::from);
55 MicroRoots {
56 builtin_set: true,
57 builtin: None,
58 user: home.as_deref().map(Self::user_dir),
59 project: project_root.map(Self::project_dir),
60 project_trusted: false,
61 }
62 }
63
64 pub fn user_dir(home: &Path) -> PathBuf {
66 home.join(".config").join("rich").join("micro")
67 }
68
69 pub fn project_dir(project_root: &Path) -> PathBuf {
71 project_root.join(".rich").join("micro")
72 }
73
74 #[must_use]
76 pub fn trust_project(mut self, trusted: bool) -> Self {
77 self.project_trusted = trusted;
78 self
79 }
80}
81
82#[derive(Clone, Debug, PartialEq, Eq)]
84pub struct Rejected {
85 pub layer: Layer,
86 pub path: PathBuf,
87 pub package: Option<String>,
89 pub error: MicroError,
90}
91
92#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct Collision {
96 pub layer: Layer,
97 pub name: String,
98 pub kept: String,
100 pub dropped: String,
102}
103
104#[derive(Clone, Debug, Default, PartialEq, Eq)]
107pub struct LoadReport {
108 pub rejected: Vec<Rejected>,
109 pub collisions: Vec<Collision>,
110 pub untrusted_project: Option<PathBuf>,
113}
114
115#[derive(Clone, Debug, Default)]
116struct LayerAssets {
117 origin: Option<String>,
119 assets: BTreeMap<String, Arc<MicroAsset>>,
120 aliases: BTreeMap<String, String>,
122}
123
124#[derive(Clone, Debug, Default)]
138pub struct MicroRegistry {
139 layers: [LayerAssets; 4],
140}
141
142impl MicroRegistry {
143 pub fn new() -> Self {
146 let mut registry = MicroRegistry::default();
147 registry.layers[Layer::BuiltIn.index()].origin = Some("rs-rich-micro".to_string());
148 registry.layers[Layer::Inline.index()].origin = Some("code".to_string());
149 registry
150 }
151
152 pub fn builtin() -> Self {
164 let mut registry = MicroRegistry::new();
165 registry.load_builtin_set(&mut LoadReport::default());
166 registry
167 }
168
169 pub fn load_builtin_set(&mut self, report: &mut LoadReport) {
171 for (pack, loaded) in crate::builtin::PACKS
172 .iter()
173 .zip(crate::builtin::packs(Layer::BuiltIn))
174 {
175 let path = PathBuf::from(format!("builtin:{pack}"));
176 match loaded {
177 Ok(pack) => {
178 for asset in pack.assets {
179 report.collisions.extend(self.insert(Layer::BuiltIn, asset));
180 }
181 for (package, error) in pack.rejected {
182 report.rejected.push(Rejected {
183 layer: Layer::BuiltIn,
184 path: path.clone(),
185 package: Some(package),
186 error,
187 });
188 }
189 }
190 Err(error) => report.rejected.push(Rejected {
191 layer: Layer::BuiltIn,
192 path,
193 package: None,
194 error,
195 }),
196 }
197 }
198 }
199
200 pub fn load(roots: &MicroRoots, limits: &Limits) -> (Self, LoadReport) {
204 let mut registry = MicroRegistry::new();
205 let mut report = LoadReport::default();
206 if roots.builtin_set {
207 registry.load_builtin_set(&mut report);
208 }
209 if let Some(dir) = &roots.builtin {
210 registry.load_dir(Layer::BuiltIn, dir, limits, &mut report);
211 }
212 if let Some(dir) = &roots.user {
213 registry.load_dir(Layer::User, dir, limits, &mut report);
214 }
215 if let Some(dir) = &roots.project {
216 if roots.project_trusted {
217 registry.load_dir(Layer::Project, dir, limits, &mut report);
218 } else if dir.is_dir() {
219 report.untrusted_project = Some(dir.clone());
220 }
221 }
222 (registry, report)
223 }
224
225 pub fn load_dir(&mut self, layer: Layer, dir: &Path, limits: &Limits, report: &mut LoadReport) {
232 if layer != Layer::BuiltIn || self.layers[layer.index()].assets.is_empty() {
233 self.layers[layer.index()].origin = Some(dir.display().to_string());
234 }
235 let entries = match std::fs::read_dir(dir) {
236 Ok(entries) => entries,
237 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return,
238 Err(error) => {
239 report.rejected.push(Rejected {
240 layer,
241 path: dir.to_path_buf(),
242 package: None,
243 error: MicroError::Io(format!("{}: {error}", dir.display())),
244 });
245 return;
246 }
247 };
248 let mut paths: Vec<PathBuf> = entries
249 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
250 .filter(|path| {
251 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
252 if name.starts_with('.') || name.is_empty() {
253 return false;
254 }
255 path.is_dir()
256 || path
257 .extension()
258 .and_then(|e| e.to_str())
259 .is_some_and(|e| e == "richmicro" || e == "zip")
260 })
261 .collect();
262 paths.sort();
263 for path in paths {
264 match package::load(&path, layer, limits) {
265 Ok(Loaded::Package(asset)) => {
266 report.collisions.extend(self.insert(layer, *asset));
267 }
268 Ok(Loaded::Pack(pack)) => {
269 for asset in pack.assets {
270 report.collisions.extend(self.insert(layer, asset));
271 }
272 for (package, error) in pack.rejected {
273 report.rejected.push(Rejected {
274 layer,
275 path: path.clone(),
276 package: Some(package),
277 error,
278 });
279 }
280 }
281 Err(error) => report.rejected.push(Rejected {
282 layer,
283 path,
284 package: None,
285 error,
286 }),
287 }
288 }
289 }
290
291 pub fn add(&mut self, layer: Layer, asset: MicroAsset) -> Result<Vec<Collision>, MicroError> {
295 asset.validate()?;
296 Ok(self.insert(layer, asset))
297 }
298
299 fn insert(&mut self, layer: Layer, asset: MicroAsset) -> Vec<Collision> {
300 let origin = Origin {
301 layer,
302 ..asset.origin().clone()
303 };
304 let asset = asset.with_origin(origin);
305 let slot = &mut self.layers[layer.index()];
306 let mut collisions = Vec::new();
307 let claim = |name: &str, asset: &MicroAsset| format!("{name} from {}", asset.origin());
308 let name = asset.name().to_string();
309 let existing = slot.assets.get(&name).cloned().or_else(|| {
310 slot.aliases
311 .get(&name)
312 .and_then(|target| slot.assets.get(target).cloned())
313 });
314 if let Some(existing) = existing {
315 collisions.push(Collision {
316 layer,
317 name: name.clone(),
318 kept: claim(existing.name(), &existing),
319 dropped: claim(&name, &asset),
320 });
321 return collisions;
322 }
323 for alias in asset.aliases() {
324 let taken = slot
325 .assets
326 .get(alias)
327 .or_else(|| slot.aliases.get(alias).and_then(|t| slot.assets.get(t)))
328 .cloned();
329 match taken {
330 Some(existing) => collisions.push(Collision {
331 layer,
332 name: alias.clone(),
333 kept: claim(existing.name(), &existing),
334 dropped: format!("alias {alias} of {}", claim(&name, &asset)),
335 }),
336 None => {
337 slot.aliases.insert(alias.clone(), name.clone());
338 }
339 }
340 }
341 slot.assets.insert(name, Arc::new(asset));
342 collisions
343 }
344
345 pub fn remove(&mut self, layer: Layer, name: &str) -> Option<Arc<MicroAsset>> {
347 let slot = &mut self.layers[layer.index()];
348 let removed = slot.assets.remove(name)?;
349 slot.aliases.retain(|_, target| target != name);
350 Some(removed)
351 }
352
353 pub fn get(&self, layer: Layer, name: &str) -> Option<&Arc<MicroAsset>> {
355 let slot = &self.layers[layer.index()];
356 slot.assets.get(name).or_else(|| {
357 slot.aliases
358 .get(name)
359 .and_then(|target| slot.assets.get(target))
360 })
361 }
362
363 pub fn resolve(&self, name: &str) -> Option<&Arc<MicroAsset>> {
365 Layer::ALL
366 .iter()
367 .rev()
368 .find_map(|layer| self.get(*layer, name))
369 }
370
371 pub fn require(&self, name: &str) -> Result<&Arc<MicroAsset>, MicroError> {
373 check_name(name)?;
374 self.resolve(name)
375 .ok_or_else(|| MicroError::UnknownAsset(name.to_string()))
376 }
377
378 pub fn shadowed(&self, name: &str) -> Vec<&Arc<MicroAsset>> {
381 let mut claims = Layer::ALL
382 .iter()
383 .rev()
384 .filter_map(|layer| self.get(*layer, name));
385 claims.next();
386 claims.collect()
387 }
388
389 pub fn names(&self) -> Vec<&str> {
391 let mut names: Vec<&str> = self
392 .layers
393 .iter()
394 .flat_map(|slot| slot.assets.keys().map(String::as_str))
395 .collect();
396 names.sort_unstable();
397 names.dedup();
398 names
399 }
400
401 pub fn assets(&self) -> Vec<&Arc<MicroAsset>> {
403 self.names()
404 .into_iter()
405 .filter_map(|name| self.resolve(name))
406 .collect()
407 }
408
409 pub fn layer(&self, layer: Layer) -> impl Iterator<Item = &Arc<MicroAsset>> {
411 self.layers[layer.index()].assets.values()
412 }
413
414 pub fn precedence(&self) -> Precedence {
417 let mut precedence = Precedence::new();
418 for layer in Layer::ALL {
419 let slot = &self.layers[layer.index()];
420 let mut entry = PrecedenceLayer::new(layer.as_str());
421 if let Some(origin) = &slot.origin {
422 entry = entry.origin(rich_ext::sanitize_terminal_controls(origin));
425 }
426 let mut values: BTreeMap<&str, String> = BTreeMap::new();
427 for (name, asset) in &slot.assets {
428 values.insert(name, describe(asset));
429 }
430 for (alias, target) in &slot.aliases {
431 values.insert(alias, format!("alias of {target}"));
432 }
433 for (key, value) in values {
434 entry = entry.value(key, value);
435 }
436 precedence = precedence.layer(entry);
437 }
438 precedence
439 }
440
441 pub fn explain(&self, name: &str) -> Option<Explanation> {
444 self.precedence().explain(name)
445 }
446}
447
448fn describe(asset: &MicroAsset) -> String {
449 let mut out = format!("{} {}", asset.name(), asset.size());
450 if let Some(version) = asset.version() {
451 out.push_str(&format!(" v{version}"));
452 }
453 if let Some(pack) = &asset.origin().pack {
454 out.push_str(&format!(" (pack {pack})"));
455 }
456 out
457}