use crate::raw::lzf;
use crate::raw::msscan::SpectrumParams;
use byteorder::{ByteOrder, LittleEndian};
#[derive(Debug, Clone)]
pub struct ProfileSpectrum {
pub mz: Vec<f64>,
pub intensity: Vec<f32>,
}
pub fn decode_profile_block(
bytes: &[u8],
params: &SpectrumParams,
min_x: f64,
max_x: f64,
) -> crate::Result<ProfileSpectrum> {
if bytes.len() < 16 {
return Err(crate::Error::Parse(
"MSProfile block too short for preamble".into(),
));
}
let uncompressed_len = params.uncompressed_byte_count.ok_or_else(|| {
crate::Error::Parse("Missing UncompressedByteCount for profile block".into())
})? as usize;
let point_count = params.point_count as usize;
if point_count == 0 {
return Ok(ProfileSpectrum {
mz: vec![],
intensity: vec![],
});
}
let dec_bytes = lzf::decompress(&bytes[16..], uncompressed_len)?;
if dec_bytes.len() < 16 + point_count * 4 {
return Err(crate::Error::Parse(
"Decompressed profile block too short for point count".into(),
));
}
let mut intensity = Vec::with_capacity(point_count);
for i in 0..point_count {
let offset = 16 + i * 4;
let int_val = LittleEndian::read_u32(&dec_bytes[offset..offset + 4]);
intensity.push(int_val as f32);
}
let mut mz = Vec::with_capacity(point_count);
let step = if point_count > 1 {
(max_x - min_x) / (point_count as f64 - 1.0)
} else {
0.0
};
for i in 0..point_count {
mz.push(min_x + (i as f64) * step);
}
Ok(ProfileSpectrum { mz, intensity })
}
#[cfg(test)]
mod tests {
use super::*;
fn params(point_count: u32, uncompressed_byte_count: Option<u32>) -> SpectrumParams {
SpectrumParams {
format_id: 1,
offset: 0,
byte_count: 0,
point_count,
uncompressed_byte_count,
}
}
fn encode_literal(data: &[u8]) -> Vec<u8> {
assert!(data.len() <= 32);
let mut out = vec![(data.len() - 1) as u8];
out.extend_from_slice(data);
out
}
#[test]
fn decodes_minimal_profile_block() {
let mut uncompressed = vec![0u8; 16];
uncompressed.extend_from_slice(&1000u32.to_le_bytes());
uncompressed.extend_from_slice(&2000u32.to_le_bytes());
let mut bytes = vec![0u8; 16]; bytes.extend(encode_literal(&uncompressed));
let params = params(2, Some(uncompressed.len() as u32));
let spectrum = decode_profile_block(&bytes, ¶ms, 100.0, 200.0).unwrap();
assert_eq!(spectrum.mz, vec![100.0, 200.0]);
assert_eq!(spectrum.intensity, vec![1000.0, 2000.0]);
}
#[test]
fn zero_points_returns_empty_spectrum_without_decompressing() {
let bytes = vec![0u8; 16];
let spectrum = decode_profile_block(&bytes, ¶ms(0, Some(999)), 0.0, 1.0).unwrap();
assert!(spectrum.mz.is_empty());
assert!(spectrum.intensity.is_empty());
}
#[test]
fn block_shorter_than_preamble_is_an_error() {
let bytes = vec![0u8; 8];
let err = decode_profile_block(&bytes, ¶ms(1, Some(20)), 0.0, 1.0).unwrap_err();
assert!(err.to_string().contains("too short for preamble"));
}
#[test]
fn missing_uncompressed_byte_count_is_an_error() {
let bytes = vec![0u8; 16];
let err = decode_profile_block(&bytes, ¶ms(1, None), 0.0, 1.0).unwrap_err();
assert!(err.to_string().contains("Missing UncompressedByteCount"));
}
#[test]
fn decompressed_data_too_short_for_point_count_is_an_error() {
let uncompressed = vec![0u8; 16];
let mut bytes = vec![0u8; 16];
bytes.extend(encode_literal(&uncompressed));
let params = params(5, Some(uncompressed.len() as u32));
let err = decode_profile_block(&bytes, ¶ms, 0.0, 1.0).unwrap_err();
assert!(err.to_string().contains("too short for point count"));
}
}