1use std::collections::HashMap;
36use std::fs::File;
37use std::io::{BufWriter, Read, Write};
38use std::path::Path;
39
40use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
41
42use crate::error::{IoError, Result};
43
44pub fn msgpack_encode(value: &serde_json::Value) -> Result<Vec<u8>> {
48 rmp_serde::to_vec(value)
50 .map_err(|e| IoError::SerializationError(format!("msgpack encode failed: {e}")))
51}
52
53pub fn msgpack_decode(bytes: &[u8]) -> Result<serde_json::Value> {
55 rmp_serde::from_slice(bytes)
56 .map_err(|e| IoError::DeserializationError(format!("msgpack decode failed: {e}")))
57}
58
59const BAF_MAGIC: &[u8; 8] = b"SCIRSARR";
62const BAF_DTYPE_F64: u8 = 0;
63const BAF_DTYPE_I32: u8 = 1;
64
65pub struct BinaryArrayFile;
69
70impl BinaryArrayFile {
71 pub fn write_f64(path: &Path, data: &[f64], shape: &[usize]) -> Result<()> {
77 let expected: usize = shape.iter().product();
78 if data.len() != expected {
79 return Err(IoError::ValidationError(format!(
80 "data length {} does not match shape product {expected}",
81 data.len()
82 )));
83 }
84 let file = File::create(path)
85 .map_err(|e| IoError::FileError(format!("cannot create {:?}: {e}", path)))?;
86 let mut w = BufWriter::new(file);
87 baf_write_header(&mut w, BAF_DTYPE_F64, shape)?;
88 for &v in data {
89 w.write_f64::<LittleEndian>(v)
90 .map_err(|e| IoError::FileError(format!("write f64 failed: {e}")))?;
91 }
92 w.flush()
93 .map_err(|e| IoError::FileError(format!("flush failed: {e}")))
94 }
95
96 pub fn read_f64(path: &Path) -> Result<(Vec<f64>, Vec<usize>)> {
98 let mut r = open_for_read(path)?;
99 let (dtype, shape) = baf_read_header(&mut r)?;
100 if dtype != BAF_DTYPE_F64 {
101 return Err(IoError::FormatError(format!(
102 "expected dtype f64 (0), got {dtype}"
103 )));
104 }
105 let n: usize = shape.iter().product();
106 let mut data = Vec::with_capacity(n);
107 for _ in 0..n {
108 data.push(
109 r.read_f64::<LittleEndian>()
110 .map_err(|e| IoError::FileError(format!("read f64 failed: {e}")))?,
111 );
112 }
113 Ok((data, shape))
114 }
115
116 pub fn write_i32(path: &Path, data: &[i32], shape: &[usize]) -> Result<()> {
120 let expected: usize = shape.iter().product();
121 if data.len() != expected {
122 return Err(IoError::ValidationError(format!(
123 "data length {} does not match shape product {expected}",
124 data.len()
125 )));
126 }
127 let file = File::create(path)
128 .map_err(|e| IoError::FileError(format!("cannot create {:?}: {e}", path)))?;
129 let mut w = BufWriter::new(file);
130 baf_write_header(&mut w, BAF_DTYPE_I32, shape)?;
131 for &v in data {
132 w.write_i32::<LittleEndian>(v)
133 .map_err(|e| IoError::FileError(format!("write i32 failed: {e}")))?;
134 }
135 w.flush()
136 .map_err(|e| IoError::FileError(format!("flush failed: {e}")))
137 }
138
139 pub fn read_i32(path: &Path) -> Result<(Vec<i32>, Vec<usize>)> {
141 let mut r = open_for_read(path)?;
142 let (dtype, shape) = baf_read_header(&mut r)?;
143 if dtype != BAF_DTYPE_I32 {
144 return Err(IoError::FormatError(format!(
145 "expected dtype i32 (1), got {dtype}"
146 )));
147 }
148 let n: usize = shape.iter().product();
149 let mut data = Vec::with_capacity(n);
150 for _ in 0..n {
151 data.push(
152 r.read_i32::<LittleEndian>()
153 .map_err(|e| IoError::FileError(format!("read i32 failed: {e}")))?,
154 );
155 }
156 Ok((data, shape))
157 }
158}
159
160fn baf_write_header<W: Write>(w: &mut W, dtype: u8, shape: &[usize]) -> Result<()> {
163 w.write_all(BAF_MAGIC)
164 .map_err(|e| IoError::FileError(format!("write magic failed: {e}")))?;
165 w.write_u8(dtype)
166 .map_err(|e| IoError::FileError(format!("write dtype failed: {e}")))?;
167 w.write_u32::<LittleEndian>(shape.len() as u32)
168 .map_err(|e| IoError::FileError(format!("write ndim failed: {e}")))?;
169 for &dim in shape {
170 w.write_u64::<LittleEndian>(dim as u64)
171 .map_err(|e| IoError::FileError(format!("write dim failed: {e}")))?;
172 }
173 Ok(())
174}
175
176fn baf_read_header<R: Read>(r: &mut R) -> Result<(u8, Vec<usize>)> {
177 let mut magic = [0u8; 8];
178 r.read_exact(&mut magic)
179 .map_err(|e| IoError::FileError(format!("read magic failed: {e}")))?;
180 if &magic != BAF_MAGIC {
181 return Err(IoError::FormatError(
182 "not a BinaryArrayFile (bad magic bytes)".to_string(),
183 ));
184 }
185 let dtype = r
186 .read_u8()
187 .map_err(|e| IoError::FileError(format!("read dtype failed: {e}")))?;
188 let ndim = r
189 .read_u32::<LittleEndian>()
190 .map_err(|e| IoError::FileError(format!("read ndim failed: {e}")))?;
191 let mut shape = Vec::with_capacity(ndim as usize);
192 for _ in 0..ndim {
193 let d = r
194 .read_u64::<LittleEndian>()
195 .map_err(|e| IoError::FileError(format!("read dim failed: {e}")))?;
196 shape.push(d as usize);
197 }
198 Ok((dtype, shape))
199}
200
201fn open_for_read(path: &Path) -> Result<std::io::BufReader<File>> {
202 let file =
203 File::open(path).map_err(|e| IoError::FileError(format!("cannot open {:?}: {e}", path)))?;
204 Ok(std::io::BufReader::new(file))
205}
206
207const COLF_MAGIC: &[u8; 8] = b"SCIRCOLF";
210
211const COLF_TAG_F64: u8 = 0;
212const COLF_TAG_I64: u8 = 1;
213const COLF_TAG_BOOL: u8 = 2;
214const COLF_TAG_TEXT: u8 = 3;
215
216#[derive(Debug, Clone, PartialEq)]
218pub enum ColumnData {
219 F64(Vec<f64>),
221 I64(Vec<i64>),
223 Bool(Vec<bool>),
225 Text(Vec<String>),
227}
228
229impl ColumnData {
230 pub fn len(&self) -> usize {
232 match self {
233 ColumnData::F64(v) => v.len(),
234 ColumnData::I64(v) => v.len(),
235 ColumnData::Bool(v) => v.len(),
236 ColumnData::Text(v) => v.len(),
237 }
238 }
239
240 pub fn is_empty(&self) -> bool {
242 self.len() == 0
243 }
244}
245
246pub struct ColumnarFile;
251
252impl ColumnarFile {
253 pub fn write(path: &Path, columns: &HashMap<String, ColumnData>) -> Result<()> {
255 let mut names: Vec<&String> = columns.keys().collect();
257 names.sort();
258
259 let file = File::create(path)
260 .map_err(|e| IoError::FileError(format!("cannot create {:?}: {e}", path)))?;
261 let mut w = BufWriter::new(file);
262
263 w.write_all(COLF_MAGIC)
265 .map_err(|e| IoError::FileError(format!("write magic failed: {e}")))?;
266 w.write_u32::<LittleEndian>(names.len() as u32)
268 .map_err(|e| IoError::FileError(format!("write ncols failed: {e}")))?;
269
270 for name in &names {
271 let col = &columns[*name];
272 colf_write_column(&mut w, name, col)?;
273 }
274
275 w.flush()
276 .map_err(|e| IoError::FileError(format!("flush failed: {e}")))
277 }
278
279 pub fn read(path: &Path) -> Result<HashMap<String, ColumnData>> {
281 let mut r = open_for_read(path)?;
282 colf_read_header(&mut r)?;
283 let ncols = r
284 .read_u32::<LittleEndian>()
285 .map_err(|e| IoError::FileError(format!("read ncols failed: {e}")))?;
286 let mut map = HashMap::with_capacity(ncols as usize);
287 for _ in 0..ncols {
288 let (name, col) = colf_read_column(&mut r)?;
289 map.insert(name, col);
290 }
291 Ok(map)
292 }
293
294 pub fn read_column(path: &Path, col_name: &str) -> Result<ColumnData> {
298 let mut r = open_for_read(path)?;
299 colf_read_header(&mut r)?;
300 let ncols = r
301 .read_u32::<LittleEndian>()
302 .map_err(|e| IoError::FileError(format!("read ncols failed: {e}")))?;
303 for _ in 0..ncols {
304 let (name, col) = colf_read_column(&mut r)?;
305 if name == col_name {
306 return Ok(col);
307 }
308 }
309 Err(IoError::NotFound(format!(
310 "column '{col_name}' not found in {:?}",
311 path
312 )))
313 }
314}
315
316fn colf_write_column<W: Write>(w: &mut W, name: &str, col: &ColumnData) -> Result<()> {
319 let name_bytes = name.as_bytes();
321 w.write_u32::<LittleEndian>(name_bytes.len() as u32)
322 .map_err(|e| IoError::FileError(format!("write name_len failed: {e}")))?;
323 w.write_all(name_bytes)
324 .map_err(|e| IoError::FileError(format!("write name failed: {e}")))?;
325
326 let (tag, nrows) = match col {
328 ColumnData::F64(v) => (COLF_TAG_F64, v.len()),
329 ColumnData::I64(v) => (COLF_TAG_I64, v.len()),
330 ColumnData::Bool(v) => (COLF_TAG_BOOL, v.len()),
331 ColumnData::Text(v) => (COLF_TAG_TEXT, v.len()),
332 };
333 w.write_u8(tag)
334 .map_err(|e| IoError::FileError(format!("write tag failed: {e}")))?;
335 w.write_u64::<LittleEndian>(nrows as u64)
336 .map_err(|e| IoError::FileError(format!("write nrows failed: {e}")))?;
337
338 match col {
340 ColumnData::F64(v) => {
341 for &x in v {
342 w.write_f64::<LittleEndian>(x)
343 .map_err(|e| IoError::FileError(format!("write f64 datum failed: {e}")))?;
344 }
345 }
346 ColumnData::I64(v) => {
347 for &x in v {
348 w.write_i64::<LittleEndian>(x)
349 .map_err(|e| IoError::FileError(format!("write i64 datum failed: {e}")))?;
350 }
351 }
352 ColumnData::Bool(v) => {
353 for &x in v {
354 w.write_u8(if x { 1 } else { 0 })
355 .map_err(|e| IoError::FileError(format!("write bool datum failed: {e}")))?;
356 }
357 }
358 ColumnData::Text(v) => {
359 for s in v {
360 let bytes = s.as_bytes();
361 w.write_u32::<LittleEndian>(bytes.len() as u32)
362 .map_err(|e| IoError::FileError(format!("write text len failed: {e}")))?;
363 w.write_all(bytes)
364 .map_err(|e| IoError::FileError(format!("write text bytes failed: {e}")))?;
365 }
366 }
367 }
368 Ok(())
369}
370
371fn colf_read_header<R: Read>(r: &mut R) -> Result<()> {
372 let mut magic = [0u8; 8];
373 r.read_exact(&mut magic)
374 .map_err(|e| IoError::FileError(format!("read magic failed: {e}")))?;
375 if &magic != COLF_MAGIC {
376 return Err(IoError::FormatError(
377 "not a ColumnarFile (bad magic bytes)".to_string(),
378 ));
379 }
380 Ok(())
381}
382
383fn colf_read_column<R: Read>(r: &mut R) -> Result<(String, ColumnData)> {
384 let name_len = r
386 .read_u32::<LittleEndian>()
387 .map_err(|e| IoError::FileError(format!("read name_len failed: {e}")))?;
388 let mut name_bytes = vec![0u8; name_len as usize];
389 r.read_exact(&mut name_bytes)
390 .map_err(|e| IoError::FileError(format!("read name bytes failed: {e}")))?;
391 let name = String::from_utf8(name_bytes)
392 .map_err(|e| IoError::FormatError(format!("column name is not valid UTF-8: {e}")))?;
393
394 let tag = r
396 .read_u8()
397 .map_err(|e| IoError::FileError(format!("read tag failed: {e}")))?;
398 let nrows = r
399 .read_u64::<LittleEndian>()
400 .map_err(|e| IoError::FileError(format!("read nrows failed: {e}")))?;
401 let nrows = nrows as usize;
402
403 let col = match tag {
405 COLF_TAG_F64 => {
406 let mut v = Vec::with_capacity(nrows);
407 for _ in 0..nrows {
408 v.push(
409 r.read_f64::<LittleEndian>()
410 .map_err(|e| IoError::FileError(format!("read f64 datum: {e}")))?,
411 );
412 }
413 ColumnData::F64(v)
414 }
415 COLF_TAG_I64 => {
416 let mut v = Vec::with_capacity(nrows);
417 for _ in 0..nrows {
418 v.push(
419 r.read_i64::<LittleEndian>()
420 .map_err(|e| IoError::FileError(format!("read i64 datum: {e}")))?,
421 );
422 }
423 ColumnData::I64(v)
424 }
425 COLF_TAG_BOOL => {
426 let mut v = Vec::with_capacity(nrows);
427 for _ in 0..nrows {
428 let b = r
429 .read_u8()
430 .map_err(|e| IoError::FileError(format!("read bool datum: {e}")))?;
431 v.push(b != 0);
432 }
433 ColumnData::Bool(v)
434 }
435 COLF_TAG_TEXT => {
436 let mut v = Vec::with_capacity(nrows);
437 for _ in 0..nrows {
438 let len = r
439 .read_u32::<LittleEndian>()
440 .map_err(|e| IoError::FileError(format!("read text len: {e}")))?;
441 let mut buf = vec![0u8; len as usize];
442 r.read_exact(&mut buf)
443 .map_err(|e| IoError::FileError(format!("read text bytes: {e}")))?;
444 let s = String::from_utf8(buf).map_err(|e| {
445 IoError::FormatError(format!("text column contains invalid UTF-8: {e}"))
446 })?;
447 v.push(s);
448 }
449 ColumnData::Text(v)
450 }
451 other => {
452 return Err(IoError::FormatError(format!(
453 "unknown column type tag {other}"
454 )))
455 }
456 };
457
458 Ok((name, col))
459}
460
461#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
470 fn test_msgpack_encode_decode_object() {
471 let val = serde_json::json!({"name": "Alice", "score": 99, "active": true});
472 let encoded = msgpack_encode(&val).expect("encode");
473 let decoded = msgpack_decode(&encoded).expect("decode");
474 assert_eq!(decoded["name"], "Alice");
475 assert_eq!(decoded["score"], 99);
476 assert_eq!(decoded["active"], true);
477 }
478
479 #[test]
480 fn test_msgpack_encode_decode_array() {
481 let val = serde_json::json!([1, 2, 3, 4, 5]);
482 let encoded = msgpack_encode(&val).expect("encode");
483 let decoded = msgpack_decode(&encoded).expect("decode");
484 assert_eq!(decoded[2], 3);
485 }
486
487 #[test]
488 fn test_msgpack_encode_decode_null() {
489 let val = serde_json::Value::Null;
490 let encoded = msgpack_encode(&val).expect("encode");
491 let decoded = msgpack_decode(&encoded).expect("decode");
492 assert!(decoded.is_null());
493 }
494
495 #[test]
496 fn test_msgpack_decode_invalid_bytes_errors() {
497 let bad = vec![0xc1u8]; assert!(msgpack_decode(&bad).is_err());
499 }
500
501 #[test]
504 fn test_binary_array_file_f64_roundtrip() {
505 let dir = std::env::temp_dir().join("scirs2_io_baf_f64_test");
506 std::fs::create_dir_all(&dir).expect("mkdir");
507 let path = dir.join("array.baf");
508
509 let data: Vec<f64> = (0..12).map(|i| i as f64 * 0.5).collect();
510 let shape = vec![3usize, 4];
511
512 BinaryArrayFile::write_f64(&path, &data, &shape).expect("write f64");
513 let (loaded, loaded_shape) = BinaryArrayFile::read_f64(&path).expect("read f64");
514
515 assert_eq!(loaded_shape, shape);
516 assert_eq!(loaded.len(), 12);
517 for (orig, got) in data.iter().zip(loaded.iter()) {
518 assert!((orig - got).abs() < 1e-12, "mismatch: {orig} vs {got}");
519 }
520
521 let _ = std::fs::remove_dir_all(&dir);
522 }
523
524 #[test]
525 fn test_binary_array_file_f64_1d() {
526 let dir = std::env::temp_dir().join("scirs2_io_baf_f64_1d_test");
527 std::fs::create_dir_all(&dir).expect("mkdir");
528 let path = dir.join("vec.baf");
529
530 let data = vec![1.0f64, 2.0, 3.0, 4.0, 5.0];
531 BinaryArrayFile::write_f64(&path, &data, &[5]).expect("write");
532 let (loaded, shape) = BinaryArrayFile::read_f64(&path).expect("read");
533 assert_eq!(shape, vec![5]);
534 assert_eq!(loaded, data);
535
536 let _ = std::fs::remove_dir_all(&dir);
537 }
538
539 #[test]
542 fn test_binary_array_file_i32_roundtrip() {
543 let dir = std::env::temp_dir().join("scirs2_io_baf_i32_test");
544 std::fs::create_dir_all(&dir).expect("mkdir");
545 let path = dir.join("labels.baf");
546
547 let data: Vec<i32> = (0..20).collect();
548 let shape = vec![4usize, 5];
549
550 BinaryArrayFile::write_i32(&path, &data, &shape).expect("write i32");
551 let (loaded, loaded_shape) = BinaryArrayFile::read_i32(&path).expect("read i32");
552
553 assert_eq!(loaded_shape, shape);
554 assert_eq!(loaded, data);
555
556 let _ = std::fs::remove_dir_all(&dir);
557 }
558
559 #[test]
560 fn test_binary_array_file_wrong_dtype_error() {
561 let dir = std::env::temp_dir().join("scirs2_io_baf_dtype_err_test");
562 std::fs::create_dir_all(&dir).expect("mkdir");
563 let path = dir.join("typed.baf");
564
565 BinaryArrayFile::write_f64(&path, &[1.0, 2.0], &[2]).expect("write f64");
567 assert!(BinaryArrayFile::read_i32(&path).is_err());
568
569 let _ = std::fs::remove_dir_all(&dir);
570 }
571
572 #[test]
575 fn test_columnar_file_all_types_roundtrip() {
576 let dir = std::env::temp_dir().join("scirs2_io_colf_alltype_test");
577 std::fs::create_dir_all(&dir).expect("mkdir");
578 let path = dir.join("cols.scircolf");
579
580 let mut cols: HashMap<String, ColumnData> = HashMap::new();
581 cols.insert(
582 "temps".to_string(),
583 ColumnData::F64(vec![20.5, 21.1, 19.8, 22.3]),
584 );
585 cols.insert(
586 "counts".to_string(),
587 ColumnData::I64(vec![100i64, 200, 300, 400]),
588 );
589 cols.insert(
590 "active".to_string(),
591 ColumnData::Bool(vec![true, false, true, true]),
592 );
593 cols.insert(
594 "labels".to_string(),
595 ColumnData::Text(vec![
596 "alpha".to_string(),
597 "beta".to_string(),
598 "gamma".to_string(),
599 "delta".to_string(),
600 ]),
601 );
602
603 ColumnarFile::write(&path, &cols).expect("write columnar");
604 let loaded = ColumnarFile::read(&path).expect("read columnar");
605
606 assert_eq!(loaded.len(), 4);
607
608 match &loaded["temps"] {
609 ColumnData::F64(v) => {
610 assert!((v[0] - 20.5).abs() < 1e-10);
611 assert!((v[2] - 19.8).abs() < 1e-10);
612 }
613 _ => panic!("expected F64"),
614 }
615 match &loaded["counts"] {
616 ColumnData::I64(v) => assert_eq!(v[1], 200),
617 _ => panic!("expected I64"),
618 }
619 match &loaded["active"] {
620 ColumnData::Bool(v) => {
621 assert!(v[0]);
622 assert!(!v[1]);
623 }
624 _ => panic!("expected Bool"),
625 }
626 match &loaded["labels"] {
627 ColumnData::Text(v) => assert_eq!(v[2], "gamma"),
628 _ => panic!("expected Text"),
629 }
630
631 let _ = std::fs::remove_dir_all(&dir);
632 }
633
634 #[test]
635 fn test_columnar_file_read_single_column() {
636 let dir = std::env::temp_dir().join("scirs2_io_colf_single_col_test");
637 std::fs::create_dir_all(&dir).expect("mkdir");
638 let path = dir.join("single.scircolf");
639
640 let mut cols: HashMap<String, ColumnData> = HashMap::new();
641 cols.insert("x".to_string(), ColumnData::F64(vec![1.1, 2.2, 3.3]));
642 cols.insert("y".to_string(), ColumnData::I64(vec![10, 20, 30]));
643
644 ColumnarFile::write(&path, &cols).expect("write");
645 let y_col = ColumnarFile::read_column(&path, "y").expect("read column y");
646
647 match y_col {
648 ColumnData::I64(v) => assert_eq!(v, vec![10i64, 20, 30]),
649 _ => panic!("expected I64"),
650 }
651
652 assert!(ColumnarFile::read_column(&path, "z").is_err());
654
655 let _ = std::fs::remove_dir_all(&dir);
656 }
657
658 #[test]
659 fn test_columnar_file_empty_columns() {
660 let dir = std::env::temp_dir().join("scirs2_io_colf_empty_test");
661 std::fs::create_dir_all(&dir).expect("mkdir");
662 let path = dir.join("empty.scircolf");
663
664 let mut cols: HashMap<String, ColumnData> = HashMap::new();
665 cols.insert("e".to_string(), ColumnData::F64(vec![]));
666
667 ColumnarFile::write(&path, &cols).expect("write");
668 let loaded = ColumnarFile::read(&path).expect("read");
669 assert!(loaded["e"].is_empty());
670
671 let _ = std::fs::remove_dir_all(&dir);
672 }
673}