use byteorder::{ByteOrder, LittleEndian};
const SEGMENT_MAGIC: u32 = 17234;
const SEGMENT_HEADER_SIZE: usize = 24;
const WIDE_TOKEN_THRESHOLD: u8 = 0x20;
pub const SAMPLE_INTERVAL_SEC: f64 = 0.5;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct LcChromatogram {
pub time_sec: Vec<f32>,
pub intensity: Vec<f32>,
}
fn parse_pages(payload: &[u8]) -> Option<Vec<&[u8]>> {
let mut pages = Vec::new();
let mut off = 0usize;
let n = payload.len();
while off < n {
if off + 2 > n {
return None;
}
let plen = LittleEndian::read_u16(&payload[off..off + 2]) as usize;
let start = off + 2;
let end = start.checked_add(plen)?;
if end + 2 > n {
return None;
}
let suffix = LittleEndian::read_u16(&payload[end..end + 2]) as usize;
if suffix != plen {
return None;
}
pages.push(&payload[start..end]);
off = end + 2;
}
Some(pages)
}
fn decode_page_tokens(page: &[u8]) -> Option<Vec<i32>> {
let mut out = Vec::new();
let mut i = 0usize;
let n = page.len();
while i < n {
let b = page[i];
if b >= WIDE_TOKEN_THRESHOLD {
if i + 1 >= n {
return None; }
let b1 = page[i + 1];
let raw = (i32::from(b & 0x1F) << 8) | i32::from(b1);
let signed = if raw >= 4096 { raw - 8192 } else { raw };
out.push(signed);
i += 2;
} else {
let signed = if b < 16 {
i32::from(b)
} else {
i32::from(b) - 32
};
out.push(signed);
i += 1;
}
}
Some(out)
}
pub fn decode_stream(data: &[u8]) -> Option<LcChromatogram> {
if data.len() < SEGMENT_HEADER_SIZE {
return None;
}
let magic = LittleEndian::read_u32(&data[0..4]);
if magic != SEGMENT_MAGIC {
return None;
}
let npts = LittleEndian::read_u32(&data[8..12]) as usize;
let blocksz = LittleEndian::read_u32(&data[12..16]) as usize;
if blocksz < SEGMENT_HEADER_SIZE || blocksz > data.len() {
return None;
}
let payload = &data[SEGMENT_HEADER_SIZE..blocksz];
let pages = parse_pages(payload)?;
let mut cumulative: i64 = 0;
let mut time_sec = Vec::with_capacity(npts);
let mut intensity = Vec::with_capacity(npts);
let mut distinct_deltas = std::collections::HashSet::new();
let mut sample_idx: usize = 0;
for page in pages {
let deltas = decode_page_tokens(page)?;
for d in deltas {
distinct_deltas.insert(d);
cumulative += i64::from(d);
#[allow(clippy::cast_precision_loss)]
let t = sample_idx as f64 * SAMPLE_INTERVAL_SEC;
time_sec.push(t as f32);
#[allow(clippy::cast_precision_loss)]
intensity.push(cumulative as f32);
sample_idx += 1;
}
}
if sample_idx != npts {
return None; }
if distinct_deltas.len() < 2 {
return None; }
Some(LcChromatogram {
time_sec,
intensity,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn write_header(buf: &mut Vec<u8>, npts: u32, blocksz: u32) {
buf.extend_from_slice(&SEGMENT_MAGIC.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&npts.to_le_bytes());
buf.extend_from_slice(&blocksz.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); }
fn write_page(buf: &mut Vec<u8>, page_bytes: &[u8]) {
let len = page_bytes.len() as u16;
buf.extend_from_slice(&len.to_le_bytes());
buf.extend_from_slice(page_bytes);
buf.extend_from_slice(&len.to_le_bytes());
}
#[test]
fn parse_pages_splits_back_to_back_length_wrapped_pages() {
let mut payload = Vec::new();
write_page(&mut payload, &[1, 2, 3]);
write_page(&mut payload, &[4, 5]);
let pages = parse_pages(&payload).expect("parse");
assert_eq!(pages, vec![&[1u8, 2, 3][..], &[4u8, 5][..]]);
}
#[test]
fn parse_pages_rejects_mismatched_suffix() {
let mut payload = Vec::new();
payload.extend_from_slice(&3u16.to_le_bytes());
payload.extend_from_slice(&[1, 2, 3]);
payload.extend_from_slice(&4u16.to_le_bytes()); assert!(parse_pages(&payload).is_none());
}
#[test]
fn decode_page_tokens_reads_signed_5bit_literals() {
let page = [0u8, 1, 15, 16, 31];
let out = decode_page_tokens(&page).expect("decode");
assert_eq!(out, vec![0, 1, 15, -16, -1]);
}
#[test]
fn decode_page_tokens_reads_signed_13bit_wide_tokens() {
let page = [0x20u8, 0x01, 0x3F, 0xFF];
let out = decode_page_tokens(&page).expect("decode");
assert_eq!(out, vec![1, -1]);
}
#[test]
fn decode_page_tokens_rejects_truncated_wide_token() {
let page = [0x20u8];
assert!(decode_page_tokens(&page).is_none());
}
#[test]
fn decode_stream_rejects_bad_magic() {
let mut data = vec![0u8; SEGMENT_HEADER_SIZE];
LittleEndian::write_u32(&mut data[0..4], 0xDEAD_BEEF);
assert!(decode_stream(&data).is_none());
}
#[test]
fn decode_stream_rejects_too_short_input() {
assert!(decode_stream(&[0u8; 4]).is_none());
}
#[test]
fn decode_stream_skips_a_single_valued_stream() {
let mut payload = Vec::new();
let page: Vec<u8> = std::iter::repeat_n([0x20u8, 0x01], 4).flatten().collect();
write_page(&mut payload, &page);
let mut data = Vec::new();
let blocksz = (SEGMENT_HEADER_SIZE + payload.len()) as u32;
write_header(&mut data, 4, blocksz);
data.extend_from_slice(&payload);
assert!(decode_stream(&data).is_none());
}
#[test]
fn decode_stream_round_trips_a_varying_two_page_stream() {
let mut payload = Vec::new();
write_page(&mut payload, &[2, 2, 31]); let wide = 100i32;
let b0 = 0x20 | ((wide >> 8) as u8 & 0x1F);
let b1 = (wide & 0xFF) as u8;
write_page(&mut payload, &[1, b0, b1]);
let mut data = Vec::new();
let blocksz = (SEGMENT_HEADER_SIZE + payload.len()) as u32;
write_header(&mut data, 5, blocksz);
data.extend_from_slice(&payload);
let chrom = decode_stream(&data).expect("decode");
assert_eq!(chrom.intensity.len(), 5);
assert_eq!(chrom.time_sec.len(), 5);
assert_eq!(chrom.intensity, vec![2.0, 4.0, 3.0, 4.0, 104.0]);
assert_eq!(chrom.time_sec[1], SAMPLE_INTERVAL_SEC as f32);
assert_eq!(chrom.time_sec[4], 4.0 * SAMPLE_INTERVAL_SEC as f32);
}
#[test]
fn decode_stream_rejects_point_count_mismatch() {
let mut payload = Vec::new();
write_page(&mut payload, &[2, 2, 31]);
let mut data = Vec::new();
let blocksz = (SEGMENT_HEADER_SIZE + payload.len()) as u32;
write_header(&mut data, 5, blocksz);
data.extend_from_slice(&payload);
assert!(decode_stream(&data).is_none());
}
}