Skip to main content

scirs2_io/
binary_format.rs

1//! Custom binary serialisation format for scientific data.
2//!
3//! This module provides three layers of binary I/O:
4//!
5//! 1. **[`BinaryWriter`] / [`BinaryReader`]** — low-level type-safe primitives
6//!    (all integers, both float sizes, byte slices, length-prefixed strings, and
7//!    prefixed `f64` arrays).  All multi-byte values are stored in **little-endian**
8//!    byte order for maximum portability.
9//!
10//! 2. **[`ScirsDataFile`]** — a structured scientific container format built on top
11//!    of the primitives:
12//!    - Fixed 8-byte magic header (`SCIRS2DF`)
13//!    - `u8` version number
14//!    - `u32` record count
15//!    - Sequence of named, typed [`DataRecord`] entries
16//!
17//! 3. **[`DataRecord`]** — the payload variant that a record may hold:
18//!    [`Scalar`](DataRecord::Scalar), [`Vector`](DataRecord::Vector),
19//!    [`Matrix`](DataRecord::Matrix), and [`Text`](DataRecord::Text).
20//!
21//! ## File layout
22//!
23//! ```text
24//! ┌─────────────────────────────────────────────────────────┐
25//! │ Magic    : [u8; 8]  = b"SCIRS2DF"                       │
26//! │ Version  : u8       = 1                                  │
27//! │ N records: u32 LE                                        │
28//! ├─────────────────────────────────────────────────────────┤
29//! │ Record₁                                                  │
30//! │   Name   : u32 LE length-prefix + UTF-8 bytes           │
31//! │   Tag    : u8   (0=Scalar, 1=Vector, 2=Matrix, 3=Text)  │
32//! │   Data   :                                               │
33//! │     Scalar → f64 LE                                      │
34//! │     Vector → u64 LE count + count×f64 LE                │
35//! │     Matrix → u64 rows + u64 cols + rows×cols×f64 LE     │
36//! │     Text   → u32 LE length + UTF-8 bytes                 │
37//! ├─────────────────────────────────────────────────────────┤
38//! │ Record₂ …                                                │
39//! └─────────────────────────────────────────────────────────┘
40//! ```
41//!
42//! # Examples
43//!
44//! ```rust,no_run
45//! use scirs2_io::binary_format::{write_scirs, read_scirs, DataRecord};
46//!
47//! let records = vec![
48//!     DataRecord::named("pi",   DataRecord::Scalar(std::f64::consts::PI)),
49//!     DataRecord::named("data", DataRecord::Vector(vec![1.0, 2.0, 3.0])),
50//! ];
51//! // write_scirs / read_scirs take &[(name, DataRecord)] and return Vec<(name, DataRecord)>
52//! write_scirs("out.scirs2", &records).unwrap();
53//! let loaded = read_scirs("out.scirs2").unwrap();
54//! ```
55
56use 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
64// ─────────────────────────────── Magic / version ─────────────────────────────
65
66const MAGIC: &[u8; 8] = b"SCIRS2DF";
67const FORMAT_VERSION: u8 = 1;
68
69// ─────────────────────────────── DataRecord ──────────────────────────────────
70
71/// The payload carried by a single named record in a [`ScirsDataFile`].
72///
73/// # Tags (on-disk)
74///
75/// | Variant  | Tag byte |
76/// |----------|----------|
77/// | Scalar   | `0`      |
78/// | Vector   | `1`      |
79/// | Matrix   | `2`      |
80/// | Text     | `3`      |
81#[derive(Debug, Clone, PartialEq)]
82pub enum DataRecord {
83    /// A single 64-bit float.
84    Scalar(f64),
85    /// A one-dimensional array of 64-bit floats.
86    Vector(Vec<f64>),
87    /// A two-dimensional matrix stored in row-major order.
88    Matrix(Vec<Vec<f64>>),
89    /// A UTF-8 text string.
90    Text(String),
91}
92
93impl DataRecord {
94    /// Convenience constructor to bundle a name with a `DataRecord` for use
95    /// with [`write_scirs`] / [`read_scirs`].
96    ///
97    /// ```
98    /// use scirs2_io::binary_format::DataRecord;
99    /// let entry = DataRecord::named("x", DataRecord::Scalar(1.0));
100    /// assert_eq!(entry.0, "x");
101    /// ```
102    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
116// ─────────────────────────────── BinaryWriter ────────────────────────────────
117
118/// Type-safe binary writer backed by a [`BufWriter<File>`].
119///
120/// All multi-byte integer and float values are serialised in **little-endian**
121/// byte order.
122///
123/// # Examples
124///
125/// ```rust,no_run
126/// use scirs2_io::binary_format::BinaryWriter;
127///
128/// let mut w = BinaryWriter::create("output.bin").unwrap();
129/// w.write_u32(42).unwrap();
130/// w.write_f64(3.14).unwrap();
131/// w.write_string("hello").unwrap();
132/// w.flush().unwrap();
133/// ```
134pub struct BinaryWriter {
135    inner: BufWriter<File>,
136}
137
138impl BinaryWriter {
139    /// Create (or overwrite) the file at `path` and return a buffered writer.
140    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    // ── Integer writers ───────────────────────────────────────────────────────
150
151    /// Write a `u8` value.
152    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    /// Write a `u16` in little-endian byte order.
159    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    /// Write a `u32` in little-endian byte order.
166    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    /// Write a `u64` in little-endian byte order.
173    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    /// Write an `i8` value.
180    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    /// Write an `i16` in little-endian byte order.
187    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    /// Write an `i32` in little-endian byte order.
194    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    /// Write an `i64` in little-endian byte order.
201    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    // ── Float writers ─────────────────────────────────────────────────────────
208
209    /// Write a `f32` in little-endian byte order.
210    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    /// Write a `f64` in little-endian byte order.
217    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    // ── Compound writers ──────────────────────────────────────────────────────
224
225    /// Write a raw byte slice verbatim.
226    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    /// Write a length-prefixed UTF-8 string (`u32` LE length + raw bytes).
233    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    /// Write a `u64` element count followed by the raw `f64` values in
247    /// little-endian byte order.
248    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    /// Flush the underlying [`BufWriter`].
257    pub fn flush(&mut self) -> Result<()> {
258        self.inner
259            .flush()
260            .map_err(|e| IoError::FileError(format!("flush: {e}")))
261    }
262}
263
264// ─────────────────────────────── BinaryReader ────────────────────────────────
265
266/// Type-safe binary reader backed by a [`BufReader<File>`].
267///
268/// Mirror image of [`BinaryWriter`]: reads the same little-endian
269/// encoding that the writer produces.
270///
271/// # Examples
272///
273/// ```rust,no_run
274/// use scirs2_io::binary_format::BinaryReader;
275///
276/// let mut r = BinaryReader::open("output.bin").unwrap();
277/// let n   = r.read_u32().unwrap();
278/// let f   = r.read_f64().unwrap();
279/// let s   = r.read_string().unwrap();
280/// ```
281pub struct BinaryReader {
282    inner: BufReader<File>,
283}
284
285impl BinaryReader {
286    /// Open `path` for buffered reading.
287    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    // ── Integer readers ───────────────────────────────────────────────────────
297
298    /// Read a single `u8`.
299    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    /// Read a `u16` (little-endian).
306    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    /// Read a `u32` (little-endian).
313    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    /// Read a `u64` (little-endian).
320    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    /// Read a single `i8`.
327    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    /// Read an `i16` (little-endian).
334    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    /// Read an `i32` (little-endian).
341    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    /// Read an `i64` (little-endian).
348    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    // ── Float readers ─────────────────────────────────────────────────────────
355
356    /// Read a `f32` (little-endian).
357    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    /// Read a `f64` (little-endian).
364    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    // ── Compound readers ──────────────────────────────────────────────────────
371
372    /// Read exactly `n` bytes into a new `Vec<u8>`.
373    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    /// Read a length-prefixed UTF-8 string (written by [`BinaryWriter::write_string`]).
382    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    /// Read a count-prefixed `f64` array (written by [`BinaryWriter::write_array_f64`]).
390    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
400// ─────────────────────────────── ScirsDataFile ───────────────────────────────
401
402/// Structured scientific data file (`SCIRS2DF` format).
403///
404/// Wraps [`BinaryWriter`] / [`BinaryReader`] with a header and typed record
405/// framing.  Use the standalone [`write_scirs`] and [`read_scirs`] functions
406/// rather than constructing this type directly.
407pub struct ScirsDataFile;
408
409impl ScirsDataFile {
410    /// Write `records` to `path` in the `SCIRS2DF` format.
411    ///
412    /// Equivalent to calling [`write_scirs`].
413    pub fn write<P: AsRef<Path>>(path: P, records: &[(String, DataRecord)]) -> Result<()> {
414        write_scirs(path, records)
415    }
416
417    /// Read all records from a `SCIRS2DF` file at `path`.
418    ///
419    /// Equivalent to calling [`read_scirs`].
420    pub fn read<P: AsRef<Path>>(path: P) -> Result<Vec<(String, DataRecord)>> {
421        read_scirs(path)
422    }
423}
424
425// ─────────────────────────────── write_scirs ─────────────────────────────────
426
427/// Write a sequence of named [`DataRecord`]s to a `SCIRS2DF` binary file.
428///
429/// The function creates (or truncates) the file at `path`, writes the
430/// 10-byte header, then serialises each record in order.
431///
432/// # Errors
433///
434/// - [`IoError::FileError`] on any I/O failure.
435/// - [`IoError::SerializationError`] if a name or text value is too long.
436pub fn write_scirs<P: AsRef<Path>>(path: P, records: &[(String, DataRecord)]) -> Result<()> {
437    let mut w = BinaryWriter::create(path)?;
438
439    // Header
440    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    // Records
453    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                    // Validate row width.
471                    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
491// ─────────────────────────────── read_scirs ──────────────────────────────────
492
493/// Read all named [`DataRecord`]s from a `SCIRS2DF` binary file.
494///
495/// # Errors
496///
497/// - [`IoError::FileNotFound`] if `path` does not exist.
498/// - [`IoError::FormatError`] if the magic bytes or version are wrong.
499/// - [`IoError::FileError`] / [`IoError::ParseError`] on any read failure.
500pub fn read_scirs<P: AsRef<Path>>(path: P) -> Result<Vec<(String, DataRecord)>> {
501    let mut r = BinaryReader::open(path)?;
502
503    // Validate magic bytes
504    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    // Validate version
513    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// ─────────────────────────────── Tests ───────────────────────────────────────
594
595#[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    // ── BinaryWriter / BinaryReader round-trips ───────────────────────────────
606
607    #[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    // ── write_scirs / read_scirs round-trips ──────────────────────────────────
777
778    #[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        // Correct magic, wrong version (99)
889        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], // shorter row — should be rejected
901        ];
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}