1use crate::{CMAExportType, DataError};
4use serde::{Deserialize, Serialize};
5use std::io::{BufReader, Read, Write};
6use std::{collections::HashMap, fs, path::Path};
7
8#[derive(Serialize, Deserialize, Debug, Default, Clone)]
20pub struct CMCase {
21 pub n_div: [u32; 3],
22 pub description: String,
23 pub time_per_flow_map: f64,
24 paths: HashMap<CMAExportType, String>,
25 #[serde(rename = "is_reursive", alias = "is_recursive")]
31 pub is_recursive: bool,
32}
33
34impl std::fmt::Display for CMCase {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 writeln!(f, "CMCase Configuration:")?;
37 writeln!(
38 f,
39 " - Number of Divisions: [{}x{}x{}]",
40 self.n_div[0], self.n_div[1], self.n_div[2]
41 )?;
42 writeln!(f, " - Description: {}", self.description)?;
43 writeln!(
44 f,
45 " - Time per Flow Map: {:.2} seconds",
46 self.time_per_flow_map
47 )?;
48
49 writeln!(f, " - Export Paths:\n")?;
51 for export_type in self.paths.keys() {
52 writeln!(f, " - {:?}\n", export_type)?;
53 }
54
55 writeln!(
56 f,
57 " - Recursive: {}",
58 if self.is_recursive { "Yes" } else { "No" }
59 )?;
60 Ok(())
61 }
62}
63
64pub const DEFAULT_CASE_FILE_NAME: &str = "cma_case";
65
66impl CMCase {
67 pub fn n_compartment(&self) -> u32 {
68 if self.n_div.contains(&0) {
69 return 1; }
71 self.n_div.iter().product()
72 }
73
74 pub fn toggle_recursive(&mut self) {
75 self.is_recursive = !self.is_recursive;
76 }
77
78 pub fn is_two_phase_flow(&self) -> bool {
79 self.paths.contains_key(&CMAExportType::GasVolume)
80 }
81
82 pub fn add(&mut self, stype: CMAExportType, relative_path: &str) {
83 self.paths.insert(stype, relative_path.to_string());
84 }
85
86 pub fn resolve(&self, root: &str, stype: CMAExportType) -> Option<String> {
87 let rel = self.paths.get(&stype)?;
88 Some(Path::new(root).join(rel).to_str()?.to_string())
89 }
90
91 pub fn resolve_all(&self, root: &str, stype: CMAExportType) -> Option<Vec<String>> {
92 let rel = self.paths.get(&stype)?;
93 if self.is_recursive {
94 Some(
95 self.get_folders(root)
96 .ok()?
97 .iter()
98 .map(|folder_name| {
99 Path::new(root)
100 .join(folder_name)
101 .join(rel)
102 .to_str()
103 .map(|s| s.to_string())
104 })
105 .collect::<Option<Vec<_>>>()?,
106 )
107 } else {
108 let pa = self.resolve(root, stype)?;
109 Some(vec![pa])
110 }
111 }
112
113 pub fn prepend_path(mut self, prep: &str) -> Self {
114 for (_key, path) in self.paths.iter_mut() {
115 *path = format!("{}/{}", prep, path);
116 }
117 self
118 }
119
120 pub fn get_folders(&self, root: &str) -> Result<Vec<String>, DataError> {
126 let mut folders: Vec<(usize, String)> = std::fs::read_dir(root)?
127 .filter_map(|entry| {
128 let dir = entry.ok()?;
129 let file_name = dir.file_name();
130 let file_name_str = file_name.to_string_lossy();
131 let index: usize = file_name_str.strip_prefix("i_")?.parse().ok()?;
132 Some((index, file_name_str.to_string()))
133 })
134 .collect();
135
136 folders.sort_by_key(|(index, _)| *index);
137 Ok(folders.into_iter().map(|(_, name)| name).collect())
138 }
139
140 fn check(&self) -> bool {
141 let has_gas_volume = self.paths.contains_key(&CMAExportType::GasVolume);
142 let has_gas_flow = self.paths.contains_key(&CMAExportType::GasFlow);
143
144 let has_liq_volume = self.paths.contains_key(&CMAExportType::LiquidVolume);
145 let has_liq_flow = self.paths.contains_key(&CMAExportType::LiquidFlow);
146
147 let ok_gas = if has_gas_volume && !has_gas_flow {
148 false
149 } else {
150 !has_gas_flow || has_gas_volume
152 };
153
154 let ok_liq = if has_liq_volume && !has_liq_flow {
155 false
156 } else {
157 !has_liq_flow || has_liq_volume
159 };
160
161 ok_liq && ok_gas
162 }
163
164 pub fn new(
165 n_div: [u32; 3],
166 time_per_flow_map: f64,
167 description: Option<String>,
168 recursive: bool,
169 ) -> Self {
170 let description = description.unwrap_or(String::from("Case"));
171 Self {
172 n_div,
173 time_per_flow_map,
174 is_recursive: recursive,
175 description,
176 paths: HashMap::new(),
177 }
178 }
179}
180
181pub trait CMCaseReader {
186 fn read_case(path: &Path) -> Result<CMCase, DataError>;
196}
197
198pub fn read_case(path: &Path) -> Result<CMCase, DataError> {
199 let mut file = std::fs::File::open(path)?;
200 let mut buffer = [0; 4]; let n = file.read(&mut buffer)?;
202
203 if n > 0 && (buffer[0] == b'{' || buffer[0] == b'[') {
205 CMCaseJson::read_case(path)
206 } else {
207 CCMCaseInfo::read_case(path)
208 }
209}
210
211pub trait CMCaseWriter {
216 fn write_case(case: CMCase, path: &Path) -> Result<(), DataError>;
227}
228pub struct CMCaseJson;
230
231pub struct CCMCaseInfo;
233
234impl CMCaseReader for CMCaseJson {
235 fn read_case(path: &Path) -> Result<CMCase, DataError> {
236 let mut file = std::fs::File::open(path)?;
237 let mut contents = String::new();
238 file.read_to_string(&mut contents)?;
239
240 let case = serde_json::from_str(&contents).map_err(|_| DataError::Serde)?;
241 Ok(case)
242 }
243}
244
245impl CMCaseWriter for CMCaseJson {
246 fn write_case(case: CMCase, path: &Path) -> Result<(), DataError> {
247 if !case.check() {
248 return Err(DataError::BadData);
249 }
250
251 let json_string = serde_json::to_string(&case).map_err(|_| DataError::Serde)?;
252 let mut file = std::fs::File::create(path)?;
253 file.write_all(json_string.as_bytes())?;
254
255 Ok(())
256 }
257}
258
259impl CMCaseReader for CCMCaseInfo {
260 fn read_case(path: &Path) -> Result<CMCase, DataError> {
261 let root = match path.parent() {
266 Some(p) if !p.as_os_str().is_empty() => p,
267 _ => Path::new("."),
268 };
269 let is_recursive = std::fs::read_dir(root)
270 .map(|entries| {
271 entries.flatten().any(|dir| {
272 dir.file_name()
273 .to_string_lossy()
274 .strip_prefix("i_")
275 .is_some_and(|index| index.parse::<usize>().is_ok())
276 })
277 })
278 .unwrap_or(false);
279
280 let file = fs::File::open(path)?;
281 let mut buffer = BufReader::new(file);
282
283 let mut char_buf = [0u8; 1];
284 let mut buf = [0u8; 4];
285 let mut buffer_8bytes = [0u8; 8];
286
287 let mut case = CMCase::default();
288 if is_recursive {
289 case.toggle_recursive();
290 }
291 for i in &mut case.n_div {
292 buffer.read_exact(&mut buf)?;
293 *i = u32::from_le_bytes(buf);
294 }
295
296 buffer.read_exact(&mut buf)?;
297 let string_size = u32::from_le_bytes(buf);
298
299 let mut string_buf = vec![0; string_size as usize];
300 buffer.read_exact(&mut string_buf)?;
301 case.description = String::from_utf8(string_buf).map_err(|_| DataError::BadData)?;
302
303 buffer.read_exact(&mut buffer_8bytes)?;
304
305 case.time_per_flow_map = f64::from_le_bytes(buffer_8bytes);
306
307 buffer.read_exact(&mut buffer_8bytes)?;
308 let map_size = usize::from_le_bytes(buffer_8bytes);
309
310 for _ in 0..map_size {
311 buffer.read_exact(&mut char_buf)?;
312
313 let key = CMAExportType::from(i8::from_le_bytes(char_buf));
314
315 buffer.read_exact(&mut buf)?;
316 let string_size = u32::from_le_bytes(buf);
317
318 let mut string_buf = vec![0; string_size as usize];
319 buffer.read_exact(&mut string_buf)?;
320 let value = String::from_utf8(string_buf).map_err(|_| DataError::BadData)?;
321
322 case.paths.insert(key, value);
323 }
324
325 Ok(case)
326 }
327}
328
329impl CMCaseWriter for CCMCaseInfo {
330 fn write_case(case: CMCase, path: &Path) -> Result<(), DataError> {
331 if !case.check() {
332 return Err(DataError::BadData);
333 }
334
335 let mut file = std::fs::File::create(path)?;
336
337 for &div in &case.n_div {
338 file.write_all(&div.to_le_bytes())?;
339 }
340
341 let description_bytes = case.description.as_bytes();
342 file.write_all(&(description_bytes.len() as u32).to_le_bytes())?;
343 file.write_all(description_bytes)?;
344
345 file.write_all(&case.time_per_flow_map.to_le_bytes())?;
346
347 file.write_all(&(case.paths.len() as u64).to_le_bytes())?;
348
349 for (key, value) in &case.paths {
350 file.write_all(&(*key as i8).to_le_bytes())?;
351 let value_bytes = value.as_bytes();
352 file.write_all(&(value_bytes.len() as u32).to_le_bytes())?;
353 file.write_all(value_bytes)?;
354 }
355
356 Ok(())
357 }
358}
359
360#[cfg(test)]
361mod test {
362
363 use std::fs::remove_file;
364
365 use super::*;
366
367 fn commomn_read_test<T: CMCaseReader>() {
368 let manifest_dir = env!("CARGO_MANIFEST_DIR"); let binding = Path::new(manifest_dir)
370 .join("test_data")
371 .join(DEFAULT_CASE_FILE_NAME);
372 let path = binding.as_path();
373
374 println!("{:?}", path);
375
376 let rcase = T::read_case(path);
377
378 assert!(rcase.is_ok());
379
380 let case = rcase.unwrap();
381
382 assert!(case.n_div == [6, 6, 12]);
383 assert!(case.description == *"Sanofi");
384 assert!(case.time_per_flow_map == 0.);
385 assert!(case.paths.get(&CMAExportType::LiquidVolume).unwrap() == "./raw/./vofL.raw");
386
387 eprintln!("{:?}", case)
388 }
389
390 fn commomn_write_read_test<T: CMCaseReader, F: CMCaseWriter>() {
391 let manifest_dir = env!("CARGO_MANIFEST_DIR"); let binding = Path::new(manifest_dir).join("test_data/cma_case");
393 let path = binding.as_path();
394
395 let rcase = T::read_case(path);
396
397 assert!(rcase.is_ok());
398
399 let case = rcase.unwrap();
400
401 assert!(F::write_case(case, Path::new("./case_test")).is_ok());
402
403 let wr_case = T::read_case(Path::new("./case_test"));
404 assert!(wr_case.is_ok());
405 let wr_case = wr_case.unwrap();
406 assert!(wr_case.n_div == [6, 6, 12]);
407 assert!(wr_case.description == *"Sanofi");
408 assert!(wr_case.time_per_flow_map == 0.);
409 assert!(wr_case.paths.get(&CMAExportType::LiquidVolume).unwrap() == "./raw/./vofL.raw");
410
411 std::fs::remove_file("./case_test").unwrap();
412 }
413
414 #[test]
415 fn test_read_c_compatible() {
416 commomn_read_test::<CCMCaseInfo>();
417 }
418
419 #[test]
420 fn test_read_write_c_compatible() {
421 commomn_write_read_test::<CCMCaseInfo, CCMCaseInfo>();
422 }
423
424 #[test]
425 fn test_conversion() {
426 let manifest_dir = env!("CARGO_MANIFEST_DIR"); let binding = Path::new(manifest_dir)
428 .join("test_data")
429 .join(DEFAULT_CASE_FILE_NAME);
430 let c_path = binding.as_path();
431
432 let reference_case = CCMCaseInfo::read_case(c_path).unwrap();
433
434 let rcase = CCMCaseInfo::read_case(c_path).unwrap();
435
436 CMCaseJson::write_case(rcase, Path::new("test_2case.json")).unwrap();
437
438 let converted = CMCaseJson::read_case(Path::new("test_2case.json")).unwrap();
439
440 assert!(reference_case.n_div == converted.n_div);
441 assert!(reference_case.description == converted.description);
442 assert!(reference_case.time_per_flow_map == converted.time_per_flow_map);
443 assert!(reference_case.paths == converted.paths);
444
445 remove_file(Path::new("test_2case.json")).expect("Failed to remove test file")
446 }
447
448 fn common_write_read_test<T: CMCaseWriter + CMCaseReader>(path: &Path) -> Result<(), ()> {
449 let case = CMCase {
450 n_div: [4, 5, 1],
451 description: "Test".to_string(),
452 time_per_flow_map: 0.01,
453 paths: HashMap::new(),
454 is_recursive: false,
455 };
456
457 T::write_case(case, path).map_err(|_| ())?;
458 let read_case = T::read_case(path).map_err(|_| ())?;
459 assert!(read_case.n_div == [4, 5, 1]);
460 assert!(read_case.description == *"Test");
461 assert!(read_case.time_per_flow_map == 0.01);
462 Ok(())
463 }
464
465 #[test]
466 fn test_read_json() {
467 let path = Path::new("test_case.json");
468 let case = CMCase {
469 n_div: [4, 5, 1],
470 description: "Test".to_string(),
471 time_per_flow_map: 0.01,
472 paths: HashMap::new(),
473 is_recursive: false,
474 };
475
476 CMCaseJson::write_case(case, path).expect("Failed to write case");
477 let read_case = CMCaseJson::read_case(path).expect("Failed to read case");
478 assert!(read_case.n_div == [4, 5, 1]);
479 assert!(read_case.description == *"Test");
480 assert!(read_case.time_per_flow_map == 0.01);
481
482 remove_file(path).expect("Failed to remove test file");
483 }
484
485 #[test]
486 fn test_read_write() {
487 let path = Path::new("test_case_common.json");
488 common_write_read_test::<CMCaseJson>(path).expect("Common write-read test failed");
489 remove_file(path).expect("Failed to remove test file");
490 }
491
492 #[test]
493 fn get_folders_on_missing_root_is_an_error() {
494 let case = CMCase::new([1, 1, 1], 1., None, true);
495
496 assert!(case.get_folders("./this_directory_does_not_exist").is_err());
497 assert!(
498 case.resolve_all("./this_directory_does_not_exist", CMAExportType::LiquidFlow)
499 .is_none()
500 );
501 }
502
503 #[test]
504 fn get_folders_is_ordered_numerically() {
505 let root = std::env::temp_dir().join("cmtool_get_folders_test");
506 let _ = std::fs::remove_dir_all(&root);
507 for name in ["i_10", "i_2", "i_0", "i_notanumber", "raw"] {
509 std::fs::create_dir_all(root.join(name)).unwrap();
510 }
511
512 let case = CMCase::new([1, 1, 1], 1., None, true);
513 let folders = case.get_folders(root.to_str().unwrap()).unwrap();
514
515 assert_eq!(folders, vec!["i_0", "i_2", "i_10"]);
516
517 std::fs::remove_dir_all(&root).unwrap();
518 }
519
520 #[test]
523 fn c_compatible_rejects_invalid_utf8() {
524 let path = std::env::temp_dir().join("cmtool_bad_utf8_cma_case");
525
526 let mut bytes = Vec::new();
527 for div in [6u32, 6, 12] {
528 bytes.extend_from_slice(&div.to_le_bytes());
529 }
530 let description = [0xff_u8, 0xfe, 0xfd, 0xfc]; bytes.extend_from_slice(&(description.len() as u32).to_le_bytes());
532 bytes.extend_from_slice(&description);
533 bytes.extend_from_slice(&0f64.to_le_bytes());
534 bytes.extend_from_slice(&0u64.to_le_bytes()); std::fs::write(&path, &bytes).unwrap();
536
537 assert!(matches!(
538 CCMCaseInfo::read_case(&path),
539 Err(DataError::BadData)
540 ));
541
542 remove_file(&path).unwrap();
543 }
544
545 #[test]
548 fn recursive_flag_keeps_its_legacy_wire_name() {
549 let mut case = CMCase::new([1, 1, 1], 1., None, false);
550 case.toggle_recursive();
551 assert!(case.is_recursive);
552
553 let json = serde_json::to_string(&case).unwrap();
554 assert!(json.contains("\"is_reursive\":true"), "{}", json);
555
556 let legacy: CMCase = serde_json::from_str(
557 r#"{"n_div":[1,1,1],"description":"d","time_per_flow_map":1.0,"paths":{},"is_reursive":true}"#,
558 )
559 .unwrap();
560 assert!(legacy.is_recursive);
561
562 let renamed: CMCase = serde_json::from_str(
563 r#"{"n_div":[1,1,1],"description":"d","time_per_flow_map":1.0,"paths":{},"is_recursive":true}"#,
564 )
565 .unwrap();
566 assert!(renamed.is_recursive);
567 }
568}