use byteorder::{ByteOrder, LittleEndian};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct SpectrumParams {
pub format_id: u32,
pub offset: u64,
pub byte_count: u32,
pub point_count: u32,
pub uncompressed_byte_count: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct ScanRecord {
pub scan_id: u32,
pub retention_time_min: f64,
pub ms_level: u32,
pub min_x: Option<f64>,
pub max_x: Option<f64>,
pub mrm_channel_id: Option<u32>,
pub target_mz: Option<f64>,
pub collision_energy: Option<f64>,
pub profile_params: Option<SpectrumParams>,
pub centroid_params: Option<SpectrumParams>,
}
#[derive(Debug, Clone)]
pub struct MSScan {
pub global_header_size: u32,
pub stride: u32,
pub records: Vec<ScanRecord>,
}
impl MSScan {
pub fn from_path(path: &Path) -> crate::Result<Self> {
let bytes = std::fs::read(path)?;
Self::from_bytes(&bytes)
}
pub fn from_bytes(bytes: &[u8]) -> crate::Result<Self> {
if bytes.len() < 92 {
return Err(crate::Error::Parse(
"MSScan.bin too small for header".into(),
));
}
let magic = LittleEndian::read_u32(&bytes[0..4]);
if magic != 257 {
return Err(crate::Error::Parse(format!(
"Invalid MSScan magic: {}",
magic
)));
}
let global_header_size = LittleEndian::read_u32(&bytes[88..92]);
if bytes.len() < global_header_size as usize {
return Err(crate::Error::Parse("MSScan.bin truncated header".into()));
}
let payload_len = bytes.len() - global_header_size as usize;
let possible_strides = [284, 220, 216, 196, 186];
let mut stride = 0;
if payload_len >= 4 {
let first_scan_id = LittleEndian::read_u32(
&bytes[global_header_size as usize..global_header_size as usize + 4],
);
for s in possible_strides.iter() {
let s_val = *s as usize;
if payload_len % s_val == 0 {
if payload_len == s_val {
stride = *s;
break;
} else if payload_len > s_val {
let next_scan_id = LittleEndian::read_u32(
&bytes[global_header_size as usize + s_val
..global_header_size as usize + s_val + 4],
);
let ms_level_2 = LittleEndian::read_u16(
&bytes[global_header_size as usize + s_val + 20
..global_header_size as usize + s_val + 22],
);
if (ms_level_2 == 1 || ms_level_2 == 2)
&& next_scan_id > first_scan_id
&& next_scan_id < first_scan_id + 100000
{
stride = *s;
break;
}
}
}
}
}
if stride == 0 {
return Err(crate::Error::Parse(format!(
"Cannot determine record stride for payload len {}",
payload_len
)));
}
let num_scans = payload_len / stride as usize;
let mut records = Vec::with_capacity(num_scans);
for i in 0..num_scans {
let offset = global_header_size as usize + i * stride as usize;
let record_bytes = &bytes[offset..offset + stride as usize];
let scan_id = LittleEndian::read_u32(&record_bytes[0..4]);
let mrm_channel_id = if stride == 186 || stride == 196 {
Some(LittleEndian::read_u32(&record_bytes[4..8]))
} else {
None
};
let retention_time_min = LittleEndian::read_f64(&record_bytes[12..20]);
let ms_level = LittleEndian::read_u16(&record_bytes[20..22]) as u32;
let mut min_x = None;
let mut max_x = None;
if stride >= 284 {
min_x = Some(LittleEndian::read_f64(&record_bytes[244..252]));
max_x = Some(LittleEndian::read_f64(&record_bytes[252..260]));
}
let mut profile_params = None;
let mut centroid_params = None;
let block_offsets = match stride {
186 => vec![136],
196 => vec![144],
216 => vec![152],
220 => vec![156],
284 => vec![156, 220],
_ => vec![],
};
for bo in block_offsets {
let format_id = if stride == 186 {
LittleEndian::read_u16(&record_bytes[bo..bo + 2]) as u32
} else {
LittleEndian::read_u32(&record_bytes[bo..bo + 4])
};
let p_off = if stride == 186 { bo + 2 } else { bo + 4 };
let spec_offset = LittleEndian::read_u64(&record_bytes[p_off..p_off + 8]);
let byte_count = LittleEndian::read_u32(&record_bytes[p_off + 8..p_off + 12]);
let point_count = LittleEndian::read_u32(&record_bytes[p_off + 12..p_off + 16]);
let (unc_byte_count, _next_bo) = if format_id == 1 {
(
Some(LittleEndian::read_u32(
&record_bytes[p_off + 16..p_off + 20],
)),
bo + 24,
)
} else {
(None, bo + 20)
};
let params = SpectrumParams {
format_id,
offset: spec_offset,
byte_count,
point_count,
uncompressed_byte_count: unc_byte_count,
};
match format_id {
1 => profile_params = Some(params),
2 | 3 => centroid_params = Some(params),
_ => {} }
}
let (target_mz, collision_energy) = if ms_level >= 2 && mrm_channel_id.is_none() {
(
Some(LittleEndian::read_f64(&record_bytes[84..92])),
Some(LittleEndian::read_f64(&record_bytes[76..84])),
)
} else {
(None, None)
};
records.push(ScanRecord {
scan_id,
retention_time_min,
ms_level,
min_x,
max_x,
mrm_channel_id,
target_mz,
collision_energy,
profile_params,
centroid_params,
});
}
Ok(MSScan {
global_header_size,
stride,
records,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const GLOBAL_HEADER_SIZE: usize = 92;
fn header() -> Vec<u8> {
let mut h = vec![0u8; GLOBAL_HEADER_SIZE];
LittleEndian::write_u32(&mut h[0..4], 257); LittleEndian::write_u32(&mut h[88..92], GLOBAL_HEADER_SIZE as u32);
h
}
fn record(stride: usize, scan_id: u32, ms_level: u16) -> Vec<u8> {
let mut r = vec![0u8; stride];
LittleEndian::write_u32(&mut r[0..4], scan_id);
LittleEndian::write_f64(&mut r[12..20], 0.0);
LittleEndian::write_u16(&mut r[20..22], ms_level);
r
}
fn msscan_bytes(stride: u32, records: &[(u32, u16)]) -> Vec<u8> {
let mut bytes = header();
for &(scan_id, ms_level) in records {
bytes.extend(record(stride as usize, scan_id, ms_level));
}
bytes
}
#[test]
fn detects_each_candidate_stride() {
for &stride in &[284u32, 220, 216, 196, 186] {
let bytes = msscan_bytes(stride, &[(1, 1), (2, 1)]);
let scan = MSScan::from_bytes(&bytes)
.unwrap_or_else(|e| panic!("stride {stride} failed to parse: {e}"));
assert_eq!(scan.stride, stride, "wrong stride detected for {stride}");
assert_eq!(scan.records.len(), 2);
assert_eq!(scan.records[0].scan_id, 1);
assert_eq!(scan.records[1].scan_id, 2);
}
}
#[test]
fn detects_stride_from_single_exact_record() {
let bytes = msscan_bytes(186, &[(1, 1)]);
let scan = MSScan::from_bytes(&bytes).unwrap();
assert_eq!(scan.stride, 186);
assert_eq!(scan.records.len(), 1);
}
#[test]
fn short_payload_does_not_panic() {
let mut bytes = header();
bytes.extend([0u8, 0u8]); let result = MSScan::from_bytes(&bytes);
assert!(result.is_err());
}
#[test]
fn empty_payload_is_a_parse_error_not_a_panic() {
let bytes = header();
let result = MSScan::from_bytes(&bytes);
assert!(result.is_err());
}
#[test]
fn too_small_for_header_is_a_parse_error() {
let result = MSScan::from_bytes(&[0u8; 10]);
assert!(result.is_err());
}
#[test]
fn bad_magic_is_a_parse_error() {
let mut bytes = header();
LittleEndian::write_u32(&mut bytes[0..4], 999);
let result = MSScan::from_bytes(&bytes);
assert!(result.is_err());
}
}