1use std::fs::File;
57use std::io::{BufReader, BufWriter, Read, Write};
58use std::path::Path;
59
60use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
61
62use crate::error::{IoError, Result};
63
64const MAGIC: &[u8; 8] = b"SCIRS2DF";
67const FORMAT_VERSION: u8 = 1;
68
69#[derive(Debug, Clone, PartialEq)]
82pub enum DataRecord {
83 Scalar(f64),
85 Vector(Vec<f64>),
87 Matrix(Vec<Vec<f64>>),
89 Text(String),
91}
92
93impl DataRecord {
94 pub fn named(name: impl Into<String>, record: DataRecord) -> (String, DataRecord) {
103 (name.into(), record)
104 }
105
106 fn tag(&self) -> u8 {
107 match self {
108 DataRecord::Scalar(_) => 0,
109 DataRecord::Vector(_) => 1,
110 DataRecord::Matrix(_) => 2,
111 DataRecord::Text(_) => 3,
112 }
113 }
114}
115
116pub struct BinaryWriter {
135 inner: BufWriter<File>,
136}
137
138impl BinaryWriter {
139 pub fn create<P: AsRef<Path>>(path: P) -> Result<Self> {
141 let path = path.as_ref();
142 let file = File::create(path)
143 .map_err(|e| IoError::FileError(format!("cannot create {}: {e}", path.display())))?;
144 Ok(Self {
145 inner: BufWriter::new(file),
146 })
147 }
148
149 pub fn write_u8(&mut self, val: u8) -> Result<()> {
153 self.inner
154 .write_u8(val)
155 .map_err(|e| IoError::FileError(format!("write_u8: {e}")))
156 }
157
158 pub fn write_u16(&mut self, val: u16) -> Result<()> {
160 self.inner
161 .write_u16::<LittleEndian>(val)
162 .map_err(|e| IoError::FileError(format!("write_u16: {e}")))
163 }
164
165 pub fn write_u32(&mut self, val: u32) -> Result<()> {
167 self.inner
168 .write_u32::<LittleEndian>(val)
169 .map_err(|e| IoError::FileError(format!("write_u32: {e}")))
170 }
171
172 pub fn write_u64(&mut self, val: u64) -> Result<()> {
174 self.inner
175 .write_u64::<LittleEndian>(val)
176 .map_err(|e| IoError::FileError(format!("write_u64: {e}")))
177 }
178
179 pub fn write_i8(&mut self, val: i8) -> Result<()> {
181 self.inner
182 .write_i8(val)
183 .map_err(|e| IoError::FileError(format!("write_i8: {e}")))
184 }
185
186 pub fn write_i16(&mut self, val: i16) -> Result<()> {
188 self.inner
189 .write_i16::<LittleEndian>(val)
190 .map_err(|e| IoError::FileError(format!("write_i16: {e}")))
191 }
192
193 pub fn write_i32(&mut self, val: i32) -> Result<()> {
195 self.inner
196 .write_i32::<LittleEndian>(val)
197 .map_err(|e| IoError::FileError(format!("write_i32: {e}")))
198 }
199
200 pub fn write_i64(&mut self, val: i64) -> Result<()> {
202 self.inner
203 .write_i64::<LittleEndian>(val)
204 .map_err(|e| IoError::FileError(format!("write_i64: {e}")))
205 }
206
207 pub fn write_f32(&mut self, val: f32) -> Result<()> {
211 self.inner
212 .write_f32::<LittleEndian>(val)
213 .map_err(|e| IoError::FileError(format!("write_f32: {e}")))
214 }
215
216 pub fn write_f64(&mut self, val: f64) -> Result<()> {
218 self.inner
219 .write_f64::<LittleEndian>(val)
220 .map_err(|e| IoError::FileError(format!("write_f64: {e}")))
221 }
222
223 pub fn write_bytes(&mut self, bytes: &[u8]) -> Result<()> {
227 self.inner
228 .write_all(bytes)
229 .map_err(|e| IoError::FileError(format!("write_bytes: {e}")))
230 }
231
232 pub fn write_string(&mut self, s: &str) -> Result<()> {
234 let bytes = s.as_bytes();
235 let len = bytes.len();
236 if len > u32::MAX as usize {
237 return Err(IoError::SerializationError(format!(
238 "string too long ({len} bytes); maximum is {}",
239 u32::MAX
240 )));
241 }
242 self.write_u32(len as u32)?;
243 self.write_bytes(bytes)
244 }
245
246 pub fn write_array_f64(&mut self, arr: &[f64]) -> Result<()> {
249 self.write_u64(arr.len() as u64)?;
250 for &v in arr {
251 self.write_f64(v)?;
252 }
253 Ok(())
254 }
255
256 pub fn flush(&mut self) -> Result<()> {
258 self.inner
259 .flush()
260 .map_err(|e| IoError::FileError(format!("flush: {e}")))
261 }
262}
263
264pub struct BinaryReader {
282 inner: BufReader<File>,
283}
284
285impl BinaryReader {
286 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
288 let path = path.as_ref();
289 let file = File::open(path)
290 .map_err(|e| IoError::FileNotFound(format!("{}: {e}", path.display())))?;
291 Ok(Self {
292 inner: BufReader::new(file),
293 })
294 }
295
296 pub fn read_u8(&mut self) -> Result<u8> {
300 self.inner
301 .read_u8()
302 .map_err(|e| IoError::FileError(format!("read_u8: {e}")))
303 }
304
305 pub fn read_u16(&mut self) -> Result<u16> {
307 self.inner
308 .read_u16::<LittleEndian>()
309 .map_err(|e| IoError::FileError(format!("read_u16: {e}")))
310 }
311
312 pub fn read_u32(&mut self) -> Result<u32> {
314 self.inner
315 .read_u32::<LittleEndian>()
316 .map_err(|e| IoError::FileError(format!("read_u32: {e}")))
317 }
318
319 pub fn read_u64(&mut self) -> Result<u64> {
321 self.inner
322 .read_u64::<LittleEndian>()
323 .map_err(|e| IoError::FileError(format!("read_u64: {e}")))
324 }
325
326 pub fn read_i8(&mut self) -> Result<i8> {
328 self.inner
329 .read_i8()
330 .map_err(|e| IoError::FileError(format!("read_i8: {e}")))
331 }
332
333 pub fn read_i16(&mut self) -> Result<i16> {
335 self.inner
336 .read_i16::<LittleEndian>()
337 .map_err(|e| IoError::FileError(format!("read_i16: {e}")))
338 }
339
340 pub fn read_i32(&mut self) -> Result<i32> {
342 self.inner
343 .read_i32::<LittleEndian>()
344 .map_err(|e| IoError::FileError(format!("read_i32: {e}")))
345 }
346
347 pub fn read_i64(&mut self) -> Result<i64> {
349 self.inner
350 .read_i64::<LittleEndian>()
351 .map_err(|e| IoError::FileError(format!("read_i64: {e}")))
352 }
353
354 pub fn read_f32(&mut self) -> Result<f32> {
358 self.inner
359 .read_f32::<LittleEndian>()
360 .map_err(|e| IoError::FileError(format!("read_f32: {e}")))
361 }
362
363 pub fn read_f64(&mut self) -> Result<f64> {
365 self.inner
366 .read_f64::<LittleEndian>()
367 .map_err(|e| IoError::FileError(format!("read_f64: {e}")))
368 }
369
370 pub fn read_bytes(&mut self, n: usize) -> Result<Vec<u8>> {
374 let mut buf = vec![0u8; n];
375 self.inner
376 .read_exact(&mut buf)
377 .map_err(|e| IoError::FileError(format!("read_bytes({n}): {e}")))?;
378 Ok(buf)
379 }
380
381 pub fn read_string(&mut self) -> Result<String> {
383 let len = self.read_u32()? as usize;
384 let bytes = self.read_bytes(len)?;
385 String::from_utf8(bytes)
386 .map_err(|e| IoError::ParseError(format!("string UTF-8 error: {e}")))
387 }
388
389 pub fn read_array_f64(&mut self) -> Result<Vec<f64>> {
391 let count = self.read_u64()? as usize;
392 let mut arr = Vec::with_capacity(count);
393 for _ in 0..count {
394 arr.push(self.read_f64()?);
395 }
396 Ok(arr)
397 }
398}
399
400pub struct ScirsDataFile;
408
409impl ScirsDataFile {
410 pub fn write<P: AsRef<Path>>(path: P, records: &[(String, DataRecord)]) -> Result<()> {
414 write_scirs(path, records)
415 }
416
417 pub fn read<P: AsRef<Path>>(path: P) -> Result<Vec<(String, DataRecord)>> {
421 read_scirs(path)
422 }
423}
424
425pub fn write_scirs<P: AsRef<Path>>(path: P, records: &[(String, DataRecord)]) -> Result<()> {
437 let mut w = BinaryWriter::create(path)?;
438
439 w.write_bytes(MAGIC)?;
441 w.write_u8(FORMAT_VERSION)?;
442
443 let n = records.len();
444 if n > u32::MAX as usize {
445 return Err(IoError::SerializationError(format!(
446 "too many records ({n}); max is {}",
447 u32::MAX
448 )));
449 }
450 w.write_u32(n as u32)?;
451
452 for (name, record) in records {
454 w.write_string(name)?;
455 w.write_u8(record.tag())?;
456
457 match record {
458 DataRecord::Scalar(v) => {
459 w.write_f64(*v)?;
460 }
461 DataRecord::Vector(arr) => {
462 w.write_array_f64(arr)?;
463 }
464 DataRecord::Matrix(rows) => {
465 let n_rows = rows.len() as u64;
466 let n_cols = rows.first().map(|r| r.len()).unwrap_or(0) as u64;
467 w.write_u64(n_rows)?;
468 w.write_u64(n_cols)?;
469 for row in rows {
470 if row.len() as u64 != n_cols {
472 return Err(IoError::SerializationError(format!(
473 "jagged matrix: expected {n_cols} columns per row, got {}",
474 row.len()
475 )));
476 }
477 for &v in row {
478 w.write_f64(v)?;
479 }
480 }
481 }
482 DataRecord::Text(s) => {
483 w.write_string(s)?;
484 }
485 }
486 }
487
488 w.flush()
489}
490
491pub fn read_scirs<P: AsRef<Path>>(path: P) -> Result<Vec<(String, DataRecord)>> {
501 let mut r = BinaryReader::open(path)?;
502
503 let magic_bytes = r.read_bytes(8)?;
505 if magic_bytes != MAGIC {
506 return Err(IoError::FormatError(format!(
507 "bad magic: expected {:?}, got {:?}",
508 MAGIC, magic_bytes
509 )));
510 }
511
512 let version = r.read_u8()?;
514 if version != FORMAT_VERSION {
515 return Err(IoError::FormatError(format!(
516 "unsupported SCIRS2DF version {version}; this reader supports only version {FORMAT_VERSION}"
517 )));
518 }
519
520 let n_records = r.read_u32()? as usize;
521 let mut records = Vec::with_capacity(n_records);
522
523 for rec_idx in 0..n_records {
524 let name = r
525 .read_string()
526 .map_err(|e| IoError::ParseError(format!("record {rec_idx}: name read error: {e}")))?;
527 let tag = r.read_u8().map_err(|e| {
528 IoError::ParseError(format!("record {rec_idx} '{name}': tag read error: {e}"))
529 })?;
530
531 let record = match tag {
532 0 => {
533 let v = r.read_f64().map_err(|e| {
534 IoError::ParseError(format!(
535 "record {rec_idx} '{name}': Scalar read error: {e}"
536 ))
537 })?;
538 DataRecord::Scalar(v)
539 }
540 1 => {
541 let arr = r.read_array_f64().map_err(|e| {
542 IoError::ParseError(format!(
543 "record {rec_idx} '{name}': Vector read error: {e}"
544 ))
545 })?;
546 DataRecord::Vector(arr)
547 }
548 2 => {
549 let n_rows = r.read_u64().map_err(|e| {
550 IoError::ParseError(format!(
551 "record {rec_idx} '{name}': Matrix rows count error: {e}"
552 ))
553 })? as usize;
554 let n_cols = r.read_u64().map_err(|e| {
555 IoError::ParseError(format!(
556 "record {rec_idx} '{name}': Matrix cols count error: {e}"
557 ))
558 })? as usize;
559 let mut matrix = Vec::with_capacity(n_rows);
560 for row_idx in 0..n_rows {
561 let mut row = Vec::with_capacity(n_cols);
562 for col_idx in 0..n_cols {
563 let v = r.read_f64().map_err(|e| {
564 IoError::ParseError(format!(
565 "record {rec_idx} '{name}': Matrix[{row_idx}][{col_idx}] error: {e}"
566 ))
567 })?;
568 row.push(v);
569 }
570 matrix.push(row);
571 }
572 DataRecord::Matrix(matrix)
573 }
574 3 => {
575 let s = r.read_string().map_err(|e| {
576 IoError::ParseError(format!("record {rec_idx} '{name}': Text read error: {e}"))
577 })?;
578 DataRecord::Text(s)
579 }
580 other => {
581 return Err(IoError::FormatError(format!(
582 "record {rec_idx} '{name}': unknown type tag {other}"
583 )))
584 }
585 };
586
587 records.push((name, record));
588 }
589
590 Ok(records)
591}
592
593#[cfg(test)]
596mod tests {
597 use super::*;
598
599 fn temp_path(name: &str) -> std::path::PathBuf {
600 let dir = std::env::temp_dir().join("scirs2_binary_format_tests");
601 std::fs::create_dir_all(&dir).expect("mkdir");
602 dir.join(name)
603 }
604
605 #[test]
608 fn test_u8_roundtrip() {
609 let path = temp_path("u8.bin");
610 let mut w = BinaryWriter::create(&path).expect("create");
611 w.write_u8(0).expect("write 0");
612 w.write_u8(255).expect("write 255");
613 w.flush().expect("flush");
614
615 let mut r = BinaryReader::open(&path).expect("open");
616 assert_eq!(r.read_u8().expect("r0"), 0);
617 assert_eq!(r.read_u8().expect("r255"), 255);
618 }
619
620 #[test]
621 fn test_i8_roundtrip() {
622 let path = temp_path("i8.bin");
623 let mut w = BinaryWriter::create(&path).expect("create");
624 w.write_i8(-128).expect("write");
625 w.write_i8(127).expect("write");
626 w.flush().expect("flush");
627
628 let mut r = BinaryReader::open(&path).expect("open");
629 assert_eq!(r.read_i8().expect("r-128"), -128);
630 assert_eq!(r.read_i8().expect("r127"), 127);
631 }
632
633 #[test]
634 fn test_u16_roundtrip() {
635 let path = temp_path("u16.bin");
636 let mut w = BinaryWriter::create(&path).expect("create");
637 w.write_u16(0x1234).expect("write");
638 w.flush().expect("flush");
639 let mut r = BinaryReader::open(&path).expect("open");
640 assert_eq!(r.read_u16().expect("read"), 0x1234);
641 }
642
643 #[test]
644 fn test_u32_roundtrip() {
645 let path = temp_path("u32.bin");
646 let mut w = BinaryWriter::create(&path).expect("create");
647 w.write_u32(0xDEAD_BEEF).expect("write");
648 w.flush().expect("flush");
649 let mut r = BinaryReader::open(&path).expect("open");
650 assert_eq!(r.read_u32().expect("read"), 0xDEAD_BEEF);
651 }
652
653 #[test]
654 fn test_u64_roundtrip() {
655 let path = temp_path("u64.bin");
656 let mut w = BinaryWriter::create(&path).expect("create");
657 w.write_u64(u64::MAX).expect("write");
658 w.flush().expect("flush");
659 let mut r = BinaryReader::open(&path).expect("open");
660 assert_eq!(r.read_u64().expect("read"), u64::MAX);
661 }
662
663 #[test]
664 fn test_i16_roundtrip() {
665 let path = temp_path("i16.bin");
666 let mut w = BinaryWriter::create(&path).expect("create");
667 w.write_i16(-32000).expect("write");
668 w.flush().expect("flush");
669 let mut r = BinaryReader::open(&path).expect("open");
670 assert_eq!(r.read_i16().expect("read"), -32000);
671 }
672
673 #[test]
674 fn test_i32_roundtrip() {
675 let path = temp_path("i32.bin");
676 let mut w = BinaryWriter::create(&path).expect("create");
677 w.write_i32(-1_000_000).expect("write");
678 w.flush().expect("flush");
679 let mut r = BinaryReader::open(&path).expect("open");
680 assert_eq!(r.read_i32().expect("read"), -1_000_000);
681 }
682
683 #[test]
684 fn test_i64_roundtrip() {
685 let path = temp_path("i64.bin");
686 let mut w = BinaryWriter::create(&path).expect("create");
687 w.write_i64(i64::MIN).expect("write");
688 w.flush().expect("flush");
689 let mut r = BinaryReader::open(&path).expect("open");
690 assert_eq!(r.read_i64().expect("read"), i64::MIN);
691 }
692
693 #[test]
694 fn test_f32_roundtrip() {
695 let path = temp_path("f32.bin");
696 let mut w = BinaryWriter::create(&path).expect("create");
697 w.write_f32(std::f32::consts::E).expect("write");
698 w.flush().expect("flush");
699 let mut r = BinaryReader::open(&path).expect("open");
700 let v = r.read_f32().expect("read");
701 assert!((v - std::f32::consts::E).abs() < 1e-5);
702 }
703
704 #[test]
705 fn test_f64_roundtrip() {
706 let path = temp_path("f64.bin");
707 let mut w = BinaryWriter::create(&path).expect("create");
708 w.write_f64(std::f64::consts::PI).expect("write");
709 w.flush().expect("flush");
710 let mut r = BinaryReader::open(&path).expect("open");
711 let v = r.read_f64().expect("read");
712 assert!((v - std::f64::consts::PI).abs() < 1e-15);
713 }
714
715 #[test]
716 fn test_bytes_roundtrip() {
717 let path = temp_path("bytes.bin");
718 let data: Vec<u8> = (0u8..=255).collect();
719 let mut w = BinaryWriter::create(&path).expect("create");
720 w.write_bytes(&data).expect("write");
721 w.flush().expect("flush");
722 let mut r = BinaryReader::open(&path).expect("open");
723 let read_back = r.read_bytes(256).expect("read");
724 assert_eq!(read_back, data);
725 }
726
727 #[test]
728 fn test_string_roundtrip() {
729 let path = temp_path("string.bin");
730 let orig = "Hello, SCIRS2 科学 🔬";
731 let mut w = BinaryWriter::create(&path).expect("create");
732 w.write_string(orig).expect("write");
733 w.flush().expect("flush");
734 let mut r = BinaryReader::open(&path).expect("open");
735 let s = r.read_string().expect("read");
736 assert_eq!(s, orig);
737 }
738
739 #[test]
740 fn test_empty_string_roundtrip() {
741 let path = temp_path("empty_str.bin");
742 let mut w = BinaryWriter::create(&path).expect("create");
743 w.write_string("").expect("write");
744 w.flush().expect("flush");
745 let mut r = BinaryReader::open(&path).expect("open");
746 assert_eq!(r.read_string().expect("read"), "");
747 }
748
749 #[test]
750 fn test_array_f64_roundtrip() {
751 let path = temp_path("arr_f64.bin");
752 let arr = vec![1.1, 2.2, 3.3, 4.4, 5.5];
753 let mut w = BinaryWriter::create(&path).expect("create");
754 w.write_array_f64(&arr).expect("write");
755 w.flush().expect("flush");
756 let mut r = BinaryReader::open(&path).expect("open");
757 let read_back = r.read_array_f64().expect("read");
758 assert_eq!(read_back.len(), arr.len());
759 for (a, b) in arr.iter().zip(read_back.iter()) {
760 assert!((a - b).abs() < 1e-15);
761 }
762 }
763
764 #[test]
765 fn test_empty_array_f64_roundtrip() {
766 let path = temp_path("empty_arr.bin");
767 let arr: Vec<f64> = vec![];
768 let mut w = BinaryWriter::create(&path).expect("create");
769 w.write_array_f64(&arr).expect("write");
770 w.flush().expect("flush");
771 let mut r = BinaryReader::open(&path).expect("open");
772 let read_back = r.read_array_f64().expect("read");
773 assert!(read_back.is_empty());
774 }
775
776 #[test]
779 fn test_scirs_scalar_roundtrip() {
780 let path = temp_path("scalar.scirs2");
781 let records = vec![DataRecord::named(
782 "pi",
783 DataRecord::Scalar(std::f64::consts::PI),
784 )];
785 write_scirs(&path, &records).expect("write");
786 let loaded = read_scirs(&path).expect("read");
787 assert_eq!(loaded.len(), 1);
788 let (name, rec) = &loaded[0];
789 assert_eq!(name, "pi");
790 assert!(matches!(rec, DataRecord::Scalar(v) if (v - std::f64::consts::PI).abs() < 1e-15));
791 }
792
793 #[test]
794 fn test_scirs_vector_roundtrip() {
795 let path = temp_path("vector.scirs2");
796 let arr = vec![1.0, 2.0, 3.0, 4.0, 5.0];
797 let records = vec![DataRecord::named("vec", DataRecord::Vector(arr.clone()))];
798 write_scirs(&path, &records).expect("write");
799 let loaded = read_scirs(&path).expect("read");
800 let (name, rec) = &loaded[0];
801 assert_eq!(name, "vec");
802 if let DataRecord::Vector(v) = rec {
803 assert_eq!(v, &arr);
804 } else {
805 panic!("expected Vector");
806 }
807 }
808
809 #[test]
810 fn test_scirs_matrix_roundtrip() {
811 let path = temp_path("matrix.scirs2");
812 let matrix = vec![
813 vec![1.0, 2.0, 3.0],
814 vec![4.0, 5.0, 6.0],
815 vec![7.0, 8.0, 9.0],
816 ];
817 let records = vec![DataRecord::named("mat", DataRecord::Matrix(matrix.clone()))];
818 write_scirs(&path, &records).expect("write");
819 let loaded = read_scirs(&path).expect("read");
820 let (name, rec) = &loaded[0];
821 assert_eq!(name, "mat");
822 if let DataRecord::Matrix(m) = rec {
823 assert_eq!(m, &matrix);
824 } else {
825 panic!("expected Matrix");
826 }
827 }
828
829 #[test]
830 fn test_scirs_text_roundtrip() {
831 let path = temp_path("text.scirs2");
832 let text = "SciRS2 binary format test — 科学 🧪".to_string();
833 let records = vec![DataRecord::named("desc", DataRecord::Text(text.clone()))];
834 write_scirs(&path, &records).expect("write");
835 let loaded = read_scirs(&path).expect("read");
836 let (name, rec) = &loaded[0];
837 assert_eq!(name, "desc");
838 if let DataRecord::Text(s) = rec {
839 assert_eq!(s, &text);
840 } else {
841 panic!("expected Text");
842 }
843 }
844
845 #[test]
846 fn test_scirs_multiple_records_roundtrip() {
847 let path = temp_path("multi.scirs2");
848 let records = vec![
849 DataRecord::named("alpha", DataRecord::Scalar(1.0)),
850 DataRecord::named("beta", DataRecord::Vector(vec![10.0, 20.0, 30.0])),
851 DataRecord::named(
852 "gamma",
853 DataRecord::Matrix(vec![vec![1.0, 2.0], vec![3.0, 4.0]]),
854 ),
855 DataRecord::named("delta", DataRecord::Text("delta record".to_string())),
856 ];
857 write_scirs(&path, &records).expect("write");
858 let loaded = read_scirs(&path).expect("read");
859 assert_eq!(loaded.len(), 4);
860 assert!(matches!(&loaded[0].1, DataRecord::Scalar(v) if (v - 1.0).abs() < 1e-15));
861 assert!(matches!(&loaded[1].1, DataRecord::Vector(v) if v.len() == 3));
862 assert!(matches!(&loaded[2].1, DataRecord::Matrix(m) if m.len() == 2));
863 assert!(matches!(&loaded[3].1, DataRecord::Text(s) if s == "delta record"));
864 }
865
866 #[test]
867 fn test_scirs_empty_file() {
868 let path = temp_path("empty.scirs2");
869 write_scirs(&path, &[]).expect("write");
870 let loaded = read_scirs(&path).expect("read");
871 assert!(loaded.is_empty());
872 }
873
874 #[test]
875 fn test_scirs_wrong_magic_is_error() {
876 use std::io::Write;
877 let path = temp_path("bad_magic.scirs2");
878 let mut f = File::create(&path).expect("create");
879 f.write_all(b"BADMAGIC\x01\x00\x00\x00\x00").expect("write");
880 assert!(read_scirs(&path).is_err());
881 }
882
883 #[test]
884 fn test_scirs_wrong_version_is_error() {
885 use std::io::Write;
886 let path = temp_path("bad_version.scirs2");
887 let mut f = File::create(&path).expect("create");
888 f.write_all(b"SCIRS2DF").expect("magic");
890 f.write_all(&[99u8]).expect("version");
891 f.write_all(&[0u8; 4]).expect("count");
892 assert!(read_scirs(&path).is_err());
893 }
894
895 #[test]
896 fn test_scirs_jagged_matrix_is_error() {
897 let path = temp_path("jagged.scirs2");
898 let jagged_matrix = vec![
899 vec![1.0, 2.0, 3.0],
900 vec![4.0, 5.0], ];
902 let records = vec![DataRecord::named("bad", DataRecord::Matrix(jagged_matrix))];
903 assert!(write_scirs(&path, &records).is_err());
904 }
905
906 #[test]
907 fn test_scirs_empty_vector_roundtrip() {
908 let path = temp_path("empty_vec.scirs2");
909 let records = vec![DataRecord::named("empty", DataRecord::Vector(vec![]))];
910 write_scirs(&path, &records).expect("write");
911 let loaded = read_scirs(&path).expect("read");
912 assert!(matches!(&loaded[0].1, DataRecord::Vector(v) if v.is_empty()));
913 }
914
915 #[test]
916 fn test_scirs_data_file_struct_api() {
917 let path = temp_path("struct_api.scirs2");
918 let records = vec![DataRecord::named("x", DataRecord::Scalar(2.0))];
919 ScirsDataFile::write(&path, &records).expect("write");
920 let loaded = ScirsDataFile::read(&path).expect("read");
921 assert_eq!(loaded.len(), 1);
922 assert_eq!(loaded[0].0, "x");
923 }
924
925 #[test]
926 fn test_data_record_named_helper() {
927 let (name, rec) = DataRecord::named("foo", DataRecord::Scalar(42.0));
928 assert_eq!(name, "foo");
929 assert!(matches!(rec, DataRecord::Scalar(v) if (v - 42.0).abs() < 1e-15));
930 }
931}