extern crate alloc;
use alloc::vec::Vec;
use std::fs::File;
use std::io::{BufReader, Cursor, Read, Seek};
use std::path::Path;
use crate::{
domain::{ChannelMetadata, GraphMetadata},
error::{BiopacError, Warning},
parser::headers::{dtype::SampleType, parse_headers},
};
#[derive(Debug, Clone)]
pub struct InspectReport {
pub graph_metadata: GraphMetadata,
pub channels: Vec<ChannelInspect>,
pub foreign_data_len: usize,
pub data_start_offset: u64,
pub warnings: Vec<Warning>,
}
#[derive(Debug, Clone)]
pub struct ChannelInspect {
pub metadata: ChannelMetadata,
pub dtype: &'static str,
}
pub fn inspect_file(path: impl AsRef<Path>) -> Result<InspectReport, BiopacError> {
let file = File::open(path.as_ref()).map_err(BiopacError::Io)?;
let mut reader = BufReader::new(file);
build_report(&mut reader)
}
pub fn inspect_bytes(bytes: &[u8]) -> Result<InspectReport, BiopacError> {
let mut cursor = Cursor::new(bytes);
build_report(&mut cursor)
}
fn build_report<R: Read + Seek>(reader: &mut R) -> Result<InspectReport, BiopacError> {
let h = parse_headers(reader)?;
let channels = h
.channel_metadata
.into_iter()
.zip(h.sample_types)
.map(|(metadata, st)| ChannelInspect {
metadata,
dtype: match st {
SampleType::I16 => "I16",
SampleType::F64 => "F64",
},
})
.collect();
Ok(InspectReport {
graph_metadata: h.graph_metadata,
channels,
foreign_data_len: h.foreign_data.len(),
data_start_offset: h.data_start_offset,
warnings: h.warnings,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inspect_bytes_returns_report() -> Result<(), BiopacError> {
let bytes = build_minimal_pre4(1);
let report = inspect_bytes(&bytes)?;
assert_eq!(report.graph_metadata.channel_count, 1);
assert_eq!(report.channels.len(), 1);
assert_eq!(report.channels.first().map(|c| c.dtype), Some("I16"));
assert!(report.data_start_offset > 0);
Ok(())
}
#[test]
fn inspect_bytes_foreign_data_len_is_zero_for_minimal_file() -> Result<(), BiopacError> {
let bytes = build_minimal_pre4(2);
let report = inspect_bytes(&bytes)?;
assert_eq!(report.foreign_data_len, 0);
assert_eq!(report.channels.len(), 2);
Ok(())
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
reason = "test helper: channel count and header length are small known-bounded values"
)]
fn build_minimal_pre4(n_channels: usize) -> Vec<u8> {
let chan_hdr_len: usize = 112;
let mut buf: Vec<u8> = Vec::new();
let mut gh = [0u8; 256];
gh[2..6].copy_from_slice(&38i32.to_le_bytes()); gh[6..10].copy_from_slice(&256i32.to_le_bytes()); gh[10..12].copy_from_slice(&(n_channels as i16).to_le_bytes()); gh[16..24].copy_from_slice(&1.0f64.to_le_bytes()); gh[252..254].copy_from_slice(&(chan_hdr_len as i16).to_le_bytes()); buf.extend_from_slice(&gh);
for i in 0..n_channels {
let mut ch = [0u8; 112];
ch[0..4].copy_from_slice(&(chan_hdr_len as i32).to_le_bytes()); let name = alloc::format!("CH{i}");
let name_bytes = name.as_bytes();
let len = name_bytes.len().min(39);
if let (Some(dst), Some(src)) = (ch.get_mut(6..6 + len), name_bytes.get(..len)) {
dst.copy_from_slice(src);
}
ch[88..92].copy_from_slice(&0i32.to_le_bytes());
ch[92..100].copy_from_slice(&1.0f64.to_le_bytes());
ch[100..108].copy_from_slice(&0.0f64.to_le_bytes());
buf.extend_from_slice(&ch);
}
buf.extend_from_slice(&0i32.to_le_bytes());
for _ in 0..n_channels {
buf.extend_from_slice(&4u16.to_le_bytes()); buf.extend_from_slice(&2u16.to_le_bytes()); }
buf
}
}