use bids_core::error::{BidsError, Result};
use std::io::{BufReader, Read, Seek, SeekFrom};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct CtfHeader {
pub n_channels: usize,
pub n_samples: usize,
pub n_trials: usize,
pub sample_rate: f64,
pub channel_names: Vec<String>,
}
pub fn read_ctf_header(ds_path: &Path) -> Result<CtfHeader> {
let res4_path = find_res4(ds_path)?;
let file = std::fs::File::open(&res4_path)?;
let mut reader = BufReader::new(file);
let mut header_buf = [0u8; 1848];
reader.read_exact(&mut header_buf).map_err(|_| {
BidsError::DataFormat(format!("CTF .res4 file too short: {}", res4_path.display()))
})?;
let n_channels = i16::from_be_bytes([header_buf[776], header_buf[777]]) as usize;
let n_samples = i16::from_be_bytes([header_buf[778], header_buf[779]]) as usize;
let n_trials = i16::from_be_bytes([header_buf[780], header_buf[781]]) as usize;
let sample_rate = f64::from_be_bytes([
header_buf[1840],
header_buf[1841],
header_buf[1842],
header_buf[1843],
header_buf[1844],
header_buf[1845],
header_buf[1846],
header_buf[1847],
]);
let mut channel_names = Vec::with_capacity(n_channels);
reader.seek(SeekFrom::Start(1848))?;
for _ in 0..n_channels {
let mut name_buf = [0u8; 32];
reader.read_exact(&mut name_buf)?;
let name = String::from_utf8_lossy(&name_buf)
.trim_end_matches('\0')
.trim()
.to_string();
channel_names.push(name);
reader.seek(SeekFrom::Current(1320))?;
}
if n_channels == 0 || sample_rate <= 0.0 {
return Err(BidsError::DataFormat(format!(
"Invalid CTF header in {}: n_channels={}, sample_rate={}",
ds_path.display(),
n_channels,
sample_rate
)));
}
Ok(CtfHeader {
n_channels,
n_samples,
n_trials,
sample_rate,
channel_names,
})
}
fn find_res4(ds_path: &Path) -> Result<std::path::PathBuf> {
if ds_path.is_dir() {
for entry in std::fs::read_dir(ds_path)?.flatten() {
if entry.path().extension().is_some_and(|e| e == "res4") {
return Ok(entry.path());
}
}
}
Err(BidsError::DataFormat(format!(
"No .res4 file found in CTF directory: {}",
ds_path.display()
)))
}
impl std::fmt::Display for CtfHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"CTF: {} channels × {} samples × {} trials @ {:.1} Hz",
self.n_channels, self.n_samples, self.n_trials, self.sample_rate
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_missing_res4() {
let dir = std::env::temp_dir().join("bids_ctf_empty");
std::fs::create_dir_all(&dir).unwrap();
let result = read_ctf_header(&dir);
assert!(result.is_err());
std::fs::remove_dir_all(&dir).unwrap();
}
}