1use std::path::{Path, PathBuf};
17use std::sync::Arc;
18
19use crate::{AssociationStore, MemoryStore, SearchEngine};
20use serde::{Deserialize, Serialize};
21use wm_core::Result;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum MandalaLevel {
27 Research,
29 Sandbox,
31 Production,
33 Secure,
35}
36
37impl MandalaLevel {
38 #[must_use]
40 pub const fn all() -> &'static [Self] {
41 &[
42 Self::Research,
43 Self::Sandbox,
44 Self::Production,
45 Self::Secure,
46 ]
47 }
48
49 #[must_use]
51 pub const fn as_str(self) -> &'static str {
52 match self {
53 Self::Research => "research",
54 Self::Sandbox => "sandbox",
55 Self::Production => "production",
56 Self::Secure => "secure",
57 }
58 }
59
60 #[must_use]
62 pub const fn dir_name(self) -> &'static str {
63 self.as_str()
64 }
65
66 #[must_use]
74 pub const fn map_size(self) -> usize {
75 #[cfg(windows)]
76 {
77 match self {
78 Self::Research | Self::Sandbox => 64 * 1024 * 1024, Self::Production => 128 * 1024 * 1024, Self::Secure => 256 * 1024 * 1024, }
82 }
83 #[cfg(not(windows))]
84 {
85 match self {
86 Self::Research | Self::Sandbox => 256 * 1024 * 1024, Self::Production => 1024 * 1024 * 1024, Self::Secure => 4 * 1024 * 1024 * 1024, }
90 }
91 }
92
93 #[must_use]
95 pub const fn read_only_default(self) -> bool {
96 matches!(self, Self::Secure)
97 }
98
99 #[must_use]
101 pub fn parse(s: &str) -> Option<Self> {
102 match s.to_ascii_lowercase().as_str() {
103 "research" => Some(Self::Research),
104 "sandbox" => Some(Self::Sandbox),
105 "production" => Some(Self::Production),
106 "secure" => Some(Self::Secure),
107 _ => None,
108 }
109 }
110}
111
112impl std::fmt::Display for MandalaLevel {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 f.write_str(self.as_str())
115 }
116}
117
118#[derive(Debug, Clone)]
120pub struct CompartmentConfig {
121 pub level: MandalaLevel,
123 pub path: PathBuf,
125 pub map_size: Option<usize>,
127 pub read_only: bool,
129}
130
131impl CompartmentConfig {
132 #[must_use]
134 pub fn new(level: MandalaLevel, base_path: &Path) -> Self {
135 let path = base_path.join(level.dir_name());
136 Self {
137 level,
138 path,
139 map_size: None,
140 read_only: level.read_only_default(),
141 }
142 }
143
144 #[must_use]
146 pub fn effective_map_size(&self) -> usize {
147 self.map_size.unwrap_or_else(|| self.level.map_size())
148 }
149
150 #[must_use]
152 pub const fn with_map_size(mut self, size: usize) -> Self {
153 self.map_size = Some(size);
154 self
155 }
156
157 #[must_use]
159 pub const fn read_only(mut self, ro: bool) -> Self {
160 self.read_only = ro;
161 self
162 }
163}
164
165pub struct Compartment {
170 pub config: CompartmentConfig,
172 pub store: Arc<MemoryStore>,
174 pub search: Arc<SearchEngine>,
176 pub associations: Arc<AssociationStore>,
178}
179
180impl Compartment {
181 pub fn open(config: CompartmentConfig) -> Result<Self> {
183 std::fs::create_dir_all(&config.path)
184 .map_err(|e| wm_core::CoreError::Memory(format!("create compartment dir: {e}")))?;
185
186 let store = Arc::new(MemoryStore::open(
187 &config.path,
188 config.effective_map_size(),
189 )?);
190
191 let search_path = config.path.join("tantivy");
192 std::fs::create_dir_all(&search_path)
193 .map_err(|e| wm_core::CoreError::Memory(format!("create tantivy dir: {e}")))?;
194 let search = Arc::new(SearchEngine::open(&search_path)?);
195
196 let associations = Arc::new(AssociationStore::open(store.env())?);
197
198 Ok(Self {
199 config,
200 store,
201 search,
202 associations,
203 })
204 }
205
206 #[must_use]
208 pub const fn level(&self) -> MandalaLevel {
209 self.config.level
210 }
211
212 #[must_use]
214 pub const fn is_read_only(&self) -> bool {
215 self.config.read_only
216 }
217}
218
219pub struct MandalaManager {
224 base_path: PathBuf,
226 compartments: ahash::AHashMap<MandalaLevel, Compartment>,
228}
229
230impl MandalaManager {
231 #[must_use]
234 pub fn new(base_path: &Path) -> Self {
235 Self {
236 base_path: base_path.to_path_buf(),
237 compartments: ahash::AHashMap::new(),
238 }
239 }
240
241 pub fn open_compartment(&mut self, level: MandalaLevel) -> Result<&Compartment> {
243 let config = CompartmentConfig::new(level, &self.base_path);
244 let compartment = Compartment::open(config)?;
245 self.compartments.insert(level, compartment);
246 Ok(self.compartments.get(&level).unwrap())
247 }
248
249 pub fn open_all(&mut self) -> Result<()> {
251 for level in MandalaLevel::all() {
252 self.open_compartment(*level)?;
253 }
254 Ok(())
255 }
256
257 #[must_use]
259 pub fn get(&self, level: MandalaLevel) -> Option<&Compartment> {
260 self.compartments.get(&level)
261 }
262
263 #[must_use]
265 pub fn get_mut(&mut self, level: MandalaLevel) -> Option<&mut Compartment> {
266 self.compartments.get_mut(&level)
267 }
268
269 #[must_use]
271 pub fn opened_levels(&self) -> Vec<MandalaLevel> {
272 self.compartments.keys().copied().collect()
273 }
274
275 #[must_use]
277 pub fn len(&self) -> usize {
278 self.compartments.len()
279 }
280
281 #[must_use]
283 pub fn is_empty(&self) -> bool {
284 self.compartments.is_empty()
285 }
286
287 #[must_use]
289 pub fn base_path(&self) -> &Path {
290 &self.base_path
291 }
292
293 pub fn close(&mut self, level: MandalaLevel) -> bool {
295 self.compartments.remove(&level).is_some()
296 }
297
298 pub fn close_all(&mut self) {
300 self.compartments.clear();
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn mandala_level_ordering() {
310 let levels = MandalaLevel::all();
311 assert_eq!(levels.len(), 4);
312 assert_eq!(levels[0], MandalaLevel::Research);
313 assert_eq!(levels[3], MandalaLevel::Secure);
314 }
315
316 #[test]
317 fn mandala_level_str_roundtrip() {
318 for level in MandalaLevel::all() {
319 let s = level.as_str();
320 let back = MandalaLevel::parse(s).unwrap();
321 assert_eq!(*level, back);
322 }
323 }
324
325 #[test]
326 fn mandala_level_parse_case_insensitive() {
327 assert_eq!(
328 MandalaLevel::parse("RESEARCH"),
329 Some(MandalaLevel::Research)
330 );
331 assert_eq!(
332 MandalaLevel::parse("Production"),
333 Some(MandalaLevel::Production)
334 );
335 assert_eq!(MandalaLevel::parse("unknown"), None);
336 }
337
338 #[test]
339 fn mandala_level_map_size_increases_with_strictness() {
340 let sizes: Vec<_> = MandalaLevel::all().iter().map(|l| l.map_size()).collect();
341 assert!(sizes[0] < sizes[2]); assert!(sizes[2] < sizes[3]); }
344
345 #[test]
346 fn secure_is_read_only_by_default() {
347 assert!(!MandalaLevel::Research.read_only_default());
348 assert!(!MandalaLevel::Sandbox.read_only_default());
349 assert!(!MandalaLevel::Production.read_only_default());
350 assert!(MandalaLevel::Secure.read_only_default());
351 }
352
353 #[test]
354 fn compartment_config_defaults() {
355 let tmp = tempfile::tempdir().unwrap();
356 let config = CompartmentConfig::new(MandalaLevel::Production, tmp.path());
357 assert_eq!(config.level, MandalaLevel::Production);
358 assert!(config.path.ends_with("production"));
359 assert_eq!(
360 config.effective_map_size(),
361 MandalaLevel::Production.map_size()
362 );
363 assert!(!config.read_only);
364 }
365
366 #[test]
367 fn compartment_config_overrides() {
368 let tmp = tempfile::tempdir().unwrap();
369 let config = CompartmentConfig::new(MandalaLevel::Sandbox, tmp.path())
370 .with_map_size(512 * 1024 * 1024)
371 .read_only(true);
372 assert_eq!(config.effective_map_size(), 512 * 1024 * 1024);
373 assert!(config.read_only);
374 }
375
376 #[test]
377 fn compartment_open_creates_stores() {
378 let tmp = tempfile::tempdir().unwrap();
379 let config = CompartmentConfig::new(MandalaLevel::Research, tmp.path());
380 let compartment = Compartment::open(config).unwrap();
381
382 assert_eq!(compartment.level(), MandalaLevel::Research);
384 assert!(!compartment.is_read_only());
385
386 use wm_core::Galaxy;
388 for galaxy in Galaxy::all() {
389 let _ = compartment.store.galaxy_db(galaxy).unwrap();
390 }
391 }
392
393 #[test]
394 fn compartment_open_secure_is_strict() {
395 let tmp = tempfile::tempdir().unwrap();
396 let config = CompartmentConfig::new(MandalaLevel::Secure, tmp.path());
397 let compartment = Compartment::open(config).unwrap();
398
399 assert_eq!(compartment.level(), MandalaLevel::Secure);
400 assert!(compartment.is_read_only());
401 }
402
403 #[test]
404 fn mandala_manager_open_single() {
405 let tmp = tempfile::tempdir().unwrap();
406 let mut manager = MandalaManager::new(tmp.path());
407 manager.open_compartment(MandalaLevel::Sandbox).unwrap();
408
409 assert_eq!(manager.len(), 1);
410 assert!(manager.get(MandalaLevel::Sandbox).is_some());
411 assert!(manager.get(MandalaLevel::Research).is_none());
412 }
413
414 #[test]
415 fn mandala_manager_open_all() {
416 let tmp = tempfile::tempdir().unwrap();
417 let mut manager = MandalaManager::new(tmp.path());
418 manager.open_all().unwrap();
419
420 assert_eq!(manager.len(), 4);
421 for level in MandalaLevel::all() {
422 assert!(manager.get(*level).is_some(), "missing compartment {level}");
423 }
424 }
425
426 #[test]
427 fn mandala_manager_close() {
428 let tmp = tempfile::tempdir().unwrap();
429 let mut manager = MandalaManager::new(tmp.path());
430 manager.open_all().unwrap();
431 assert_eq!(manager.len(), 4);
432
433 manager.close(MandalaLevel::Research);
434 assert_eq!(manager.len(), 3);
435 assert!(manager.get(MandalaLevel::Research).is_none());
436
437 manager.close_all();
438 assert_eq!(manager.len(), 0);
439 assert!(manager.is_empty());
440 }
441
442 #[test]
443 fn mandala_manager_opened_levels() {
444 let tmp = tempfile::tempdir().unwrap();
445 let mut manager = MandalaManager::new(tmp.path());
446 manager.open_compartment(MandalaLevel::Production).unwrap();
447 manager.open_compartment(MandalaLevel::Secure).unwrap();
448
449 let levels = manager.opened_levels();
450 assert_eq!(levels.len(), 2);
451 assert!(levels.contains(&MandalaLevel::Production));
452 assert!(levels.contains(&MandalaLevel::Secure));
453 }
454
455 #[test]
456 fn compartments_are_isolated() {
457 let tmp = tempfile::tempdir().unwrap();
458 let mut manager = MandalaManager::new(tmp.path());
459 manager.open_compartment(MandalaLevel::Research).unwrap();
460 manager.open_compartment(MandalaLevel::Production).unwrap();
461
462 let research = manager.get(MandalaLevel::Research).unwrap();
463 let production = manager.get(MandalaLevel::Production).unwrap();
464
465 let mem = crate::Memory::new(wm_core::Galaxy::Codex, "research data".into());
467 research.store.put(wm_core::Galaxy::Codex, &mem).unwrap();
468
469 let result = production
471 .store
472 .get(wm_core::Galaxy::Codex, mem.metadata.id)
473 .unwrap();
474 assert!(result.is_none(), "data leaked between compartments!");
475
476 let result = research
478 .store
479 .get(wm_core::Galaxy::Codex, mem.metadata.id)
480 .unwrap();
481 assert!(result.is_some());
482 }
483
484 #[test]
485 fn compartment_paths_are_separate() {
486 let tmp = tempfile::tempdir().unwrap();
487 let mut manager = MandalaManager::new(tmp.path());
488 manager.open_all().unwrap();
489
490 for level in MandalaLevel::all() {
491 let compartment = manager.get(*level).unwrap();
492 let path = &compartment.config.path;
493 assert!(path.ends_with(level.dir_name()));
494 assert!(path.exists(), "compartment path should exist");
495 }
496 }
497
498 #[test]
499 fn mandala_level_display() {
500 assert_eq!(format!("{}", MandalaLevel::Research), "research");
501 assert_eq!(format!("{}", MandalaLevel::Secure), "secure");
502 }
503
504 #[test]
505 fn mandala_level_serde() {
506 let json = serde_json::to_string(&MandalaLevel::Production).unwrap();
507 assert_eq!(json, "\"production\"");
508 let back: MandalaLevel = serde_json::from_str(&json).unwrap();
509 assert_eq!(back, MandalaLevel::Production);
510 }
511}