use std::path::Path;
pub const STRIDE_A: usize = 22;
pub const STRIDE_B: usize = 30;
pub const VARIANT_A_TYPE_MARKER: u32 = 0x1800;
#[derive(Debug, Clone)]
pub struct ScanIndexA {
pub dat_offset: u32,
pub n_records: u16,
pub retention_time_min: f32,
pub peak_count: u16,
}
#[derive(Debug, Clone)]
pub struct ScanIndexB {
pub dat_offset: u32,
pub retention_time_min: f32,
}
#[derive(Debug, Clone)]
pub enum ScanIndex {
A(Vec<ScanIndexA>),
B(Vec<ScanIndexB>),
}
impl ScanIndex {
pub fn from_path(path: &Path) -> crate::Result<Self> {
let bytes = std::fs::read(path)?;
Self::from_bytes(&bytes)
}
pub fn from_bytes(data: &[u8]) -> crate::Result<Self> {
let len = data.len();
let fits_a = len % STRIDE_A == 0;
let fits_b = len % STRIDE_B == 0;
match (fits_a, fits_b) {
(true, false) => Ok(ScanIndex::A(parse_variant_a(data)?)),
(false, true) => Ok(ScanIndex::B(parse_variant_b(data)?)),
(true, true) if len == 0 => Ok(ScanIndex::A(Vec::new())),
(true, true) => {
Ok(ScanIndex::A(parse_variant_a(data)?))
}
(false, false) => Err(crate::Error::Parse(format!(
"_FUNCnnn.IDX: size {len} is not a multiple of {STRIDE_A} (Variant A) \
or {STRIDE_B} (Variant B)"
))),
}
}
pub fn len(&self) -> usize {
match self {
ScanIndex::A(v) => v.len(),
ScanIndex::B(v) => v.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
fn parse_variant_a(data: &[u8]) -> crate::Result<Vec<ScanIndexA>> {
let n = data.len() / STRIDE_A;
let mut records = Vec::with_capacity(n);
for i in 0..n {
let off = i * STRIDE_A;
let rec = &data[off..off + STRIDE_A];
let dat_offset = u32::from_le_bytes(rec[0x00..0x04].try_into().unwrap());
let packed = u32::from_le_bytes(rec[0x04..0x08].try_into().unwrap());
let n_records = (packed & 0xFFFF) as u16;
let retention_time_min = f32::from_le_bytes(rec[0x0C..0x10].try_into().unwrap());
let peak_count = u16::from_le_bytes(rec[0x10..0x12].try_into().unwrap());
records.push(ScanIndexA {
dat_offset,
n_records,
retention_time_min,
peak_count,
});
}
Ok(records)
}
fn parse_variant_b(data: &[u8]) -> crate::Result<Vec<ScanIndexB>> {
let n = data.len() / STRIDE_B;
let mut records = Vec::with_capacity(n);
for i in 0..n {
let off = i * STRIDE_B;
let rec = &data[off..off + STRIDE_B];
let retention_time_min = f32::from_le_bytes(rec[0x0C..0x10].try_into().unwrap());
let dat_offset = u32::from_le_bytes(rec[0x16..0x1A].try_into().unwrap());
records.push(ScanIndexB {
dat_offset,
retention_time_min,
});
}
Ok(records)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_a_record(dat_off: u32, n_recs: u16, rt: f32, peaks: u16) -> [u8; STRIDE_A] {
let mut rec = [0u8; STRIDE_A];
rec[0x00..0x04].copy_from_slice(&dat_off.to_le_bytes());
let packed: u32 = ((VARIANT_A_TYPE_MARKER) << 16) | n_recs as u32;
rec[0x04..0x08].copy_from_slice(&packed.to_le_bytes());
rec[0x0C..0x10].copy_from_slice(&rt.to_le_bytes());
rec[0x10..0x12].copy_from_slice(&peaks.to_le_bytes());
rec
}
fn make_b_record(dat_off: u32, rt: f32) -> [u8; STRIDE_B] {
let mut rec = [0u8; STRIDE_B];
rec[0x0C..0x10].copy_from_slice(&rt.to_le_bytes());
rec[0x16..0x1A].copy_from_slice(&dat_off.to_le_bytes());
rec
}
fn a_data(records: &[[u8; STRIDE_A]]) -> Vec<u8> {
records.iter().flat_map(|r| r.iter().copied()).collect()
}
fn b_data(records: &[[u8; STRIDE_B]]) -> Vec<u8> {
records.iter().flat_map(|r| r.iter().copied()).collect()
}
#[test]
fn parse_variant_a_blank_scan() {
let data = a_data(&[make_a_record(0x00000000, 2, 0.0273, 0)]);
let idx = ScanIndex::from_bytes(&data).unwrap();
let ScanIndex::A(recs) = idx else {
panic!("expected Variant A")
};
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].dat_offset, 0);
assert_eq!(recs[0].n_records, 2);
assert!((recs[0].retention_time_min - 0.0273).abs() < 1e-4);
assert_eq!(recs[0].peak_count, 0);
}
#[test]
fn parse_variant_a_data_scan() {
let data = a_data(&[make_a_record(0x00000024, 1050, 0.0829, 17)]);
let ScanIndex::A(recs) = ScanIndex::from_bytes(&data).unwrap() else {
panic!("expected Variant A")
};
assert_eq!(recs[0].dat_offset, 0x24);
assert_eq!(recs[0].n_records, 1050);
assert!((recs[0].retention_time_min - 0.0829).abs() < 1e-4);
assert_eq!(recs[0].peak_count, 17);
}
#[test]
fn parse_variant_a_multiple_records() {
let data = a_data(&[
make_a_record(0x00000000, 2, 0.0273, 0),
make_a_record(0x0000000c, 2, 0.0458, 0),
make_a_record(0x00000018, 2, 0.0644, 0),
make_a_record(0x00000024, 1050, 0.0829, 17),
]);
let ScanIndex::A(recs) = ScanIndex::from_bytes(&data).unwrap() else {
panic!("expected Variant A")
};
assert_eq!(recs.len(), 4);
assert_eq!(recs[3].dat_offset, 0x24);
assert_eq!(recs[3].n_records, 1050);
}
#[test]
fn variant_a_rt_is_monotonically_increasing() {
let rts = [0.0273f32, 0.0458, 0.0644, 0.0829];
let data = a_data(&rts.map(|rt| make_a_record(0, 2, rt, 0)));
let ScanIndex::A(recs) = ScanIndex::from_bytes(&data).unwrap() else {
panic!("expected Variant A")
};
for w in recs.windows(2) {
assert!(
w[1].retention_time_min > w[0].retention_time_min,
"RT not monotonic: {} <= {}",
w[1].retention_time_min,
w[0].retention_time_min
);
}
}
#[test]
fn parse_variant_b_record() {
let data = b_data(&[make_b_record(0x000047c8, 0.0540)]);
let ScanIndex::B(recs) = ScanIndex::from_bytes(&data).unwrap() else {
panic!("expected Variant B")
};
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].dat_offset, 0x47c8);
assert!((recs[0].retention_time_min - 0.0540).abs() < 1e-4);
}
#[test]
fn parse_variant_b_multiple_records() {
let data = b_data(&[
make_b_record(0x00000000, 0.0368),
make_b_record(0x000047c8, 0.0540),
make_b_record(0x00008768, 0.0711),
make_b_record(0x0000ccb8, 0.0883),
]);
let ScanIndex::B(recs) = ScanIndex::from_bytes(&data).unwrap() else {
panic!("expected Variant B")
};
assert_eq!(recs.len(), 4);
assert_eq!(recs[0].dat_offset, 0x00000000);
assert_eq!(recs[3].dat_offset, 0x0000ccb8);
}
#[test]
fn variant_b_dat_offsets_increase() {
let offsets = [0x00000000u32, 0x000047c8, 0x00008768, 0x0000ccb8];
let data = b_data(&offsets.map(|o| make_b_record(o, 0.1)));
let ScanIndex::B(recs) = ScanIndex::from_bytes(&data).unwrap() else {
panic!("expected Variant B")
};
for w in recs.windows(2) {
assert!(w[1].dat_offset > w[0].dat_offset);
}
}
#[test]
fn empty_file_is_variant_a() {
let idx = ScanIndex::from_bytes(&[]).unwrap();
assert!(matches!(idx, ScanIndex::A(_)));
assert!(idx.is_empty());
}
#[test]
fn bad_size_is_error() {
let data = vec![0u8; 25]; let err = ScanIndex::from_bytes(&data).unwrap_err();
assert!(err.to_string().contains("Variant A"));
}
#[test]
fn len_matches_record_count() {
let data = a_data(&[make_a_record(0, 2, 0.1, 0), make_a_record(12, 2, 0.2, 0)]);
let idx = ScanIndex::from_bytes(&data).unwrap();
assert_eq!(idx.len(), 2);
}
}