use std::io::{Read, Seek, SeekFrom};
use crate::Result;
use crate::error::SlowError;
use crate::header::{Header, ReadGroup, RecordCompression, SignalCompression};
pub(crate) const MAGIC: &[u8; 6] = b"BLOW5\x01";
pub(crate) const EOF_MARKER: &[u8; 5] = b"5WOLB";
const HDR_SIZE_OFFSET: u64 = 64;
const SIGNAL_PRESS_MIN: (u8, u8) = (0, 2);
pub(crate) fn parse_header<R: Read + Seek>(reader: &mut R) -> Result<Header> {
let mut magic = [0u8; 6];
reader.read_exact(&mut magic)?;
if &magic != MAGIC {
return Err(SlowError::InvalidFormat(format!(
"not a BLOW5 file: expected magic {:?}, got {:?}",
MAGIC, magic
)));
}
let mut v = [0u8; 3];
reader.read_exact(&mut v)?;
let (major, minor, patch) = (v[0], v[1], v[2]);
let mut b = [0u8; 1];
reader.read_exact(&mut b)?;
let record_compression = RecordCompression::from_byte(b[0]).ok_or_else(|| {
SlowError::InvalidFormat(format!("unknown record compression byte: {}", b[0]))
})?;
let mut rg = [0u8; 4];
reader.read_exact(&mut rg)?;
let num_read_groups = u32::from_le_bytes(rg);
const MAX_READ_GROUPS: u32 = 65536;
if num_read_groups > MAX_READ_GROUPS {
return Err(SlowError::InvalidFormat(format!(
"num_read_groups {num_read_groups} exceeds {MAX_READ_GROUPS} limit"
)));
}
let signal_compression = if (major, minor) >= SIGNAL_PRESS_MIN {
reader.read_exact(&mut b)?;
SignalCompression::from_byte(b[0]).ok_or_else(|| {
SlowError::InvalidFormat(format!("unknown signal compression byte: {}", b[0]))
})?
} else {
SignalCompression::None
};
reader.seek(SeekFrom::Start(HDR_SIZE_OFFSET))?;
let mut sz = [0u8; 4];
reader.read_exact(&mut sz)?;
let header_size_raw = u32::from_le_bytes(sz);
const MAX_HEADER_SIZE: u32 = 1024 * 1024;
if header_size_raw > MAX_HEADER_SIZE {
return Err(SlowError::InvalidFormat(format!(
"ASCII header size {header_size_raw} exceeds {MAX_HEADER_SIZE} byte limit"
)));
}
let header_size = header_size_raw as usize;
let mut ascii = vec![0u8; header_size];
reader.read_exact(&mut ascii)?;
let text = std::str::from_utf8(&ascii)
.map_err(|e| SlowError::InvalidFormat(format!("header is not valid UTF-8: {e}")))?;
let (read_groups, aux_meta) = parse_ascii_section(text, num_read_groups)?;
Ok(Header {
version: (major, minor, patch),
num_read_groups,
record_compression,
signal_compression,
read_groups,
aux_meta,
})
}
fn parse_ascii_section(
text: &str,
num_read_groups: u32,
) -> Result<(Vec<ReadGroup>, crate::aux::AuxMeta)> {
let n = num_read_groups as usize;
let mut read_groups = vec![ReadGroup::default(); n];
let mut type_line: Option<&str> = None;
let mut name_line: Option<&str> = None;
for line in text.lines() {
if let Some(rest) = line.strip_prefix('@') {
let mut parts = rest.split('\t');
let attr = parts.next().ok_or_else(|| {
SlowError::InvalidFormat("empty @ attribute line in header".into())
})?;
for (rg, value) in parts.enumerate() {
if rg < n {
read_groups[rg]
.attributes
.insert(attr.to_string(), value.to_string());
}
}
} else if line.starts_with('#') {
if type_line.is_none() {
type_line = Some(line);
} else if name_line.is_none() {
name_line = Some(line);
}
}
}
let aux_meta = match (type_line, name_line) {
(Some(tl), Some(nl)) => crate::aux::parse_aux_meta_from_lines(tl, nl)?,
_ => crate::aux::AuxMeta::default(),
};
Ok((read_groups, aux_meta))
}