use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Read, Seek};
use std::path::Path;
const BUF_SIZE: usize = 512 * 1024;
use crate::Result;
use crate::aux::AuxMeta;
use crate::error::SlowError;
use crate::header::{Header, RecordCompression, SignalCompression};
use crate::record::{RawRecord, Record};
pub struct Slow5Reader {
header: Header,
state: ReaderState,
}
enum ReaderState {
Blow5 {
inner: BufReader<File>,
record_compression: RecordCompression,
signal_compression: SignalCompression,
#[allow(dead_code)]
start_rec_offset: u64,
},
Slow5 {
inner: BufReader<File>,
col_idx: crate::slow5::PrimaryColIndices,
},
}
impl Slow5Reader {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"blow5" => Self::open_blow5(path),
"slow5" => Self::open_slow5(path),
other => Err(SlowError::InvalidFormat(format!(
"unrecognised file extension '.{other}': expected '.blow5' or '.slow5'"
))),
}
}
fn open_blow5(path: &Path) -> Result<Self> {
let file = File::open(path)?;
let mut inner = BufReader::with_capacity(BUF_SIZE, file);
let header = crate::blow5::parse_header(&mut inner)?;
let start_rec_offset = inner.stream_position().map_err(SlowError::Io)?;
let record_compression = header.record_compression;
let signal_compression = header.signal_compression;
Ok(Self {
header,
state: ReaderState::Blow5 {
inner,
record_compression,
signal_compression,
start_rec_offset,
},
})
}
fn open_slow5(path: &Path) -> Result<Self> {
let file = File::open(path)?;
let mut inner = BufReader::with_capacity(BUF_SIZE, file);
let (header, col_idx) = crate::slow5::parse_header(&mut inner)?;
Ok(Self {
header,
state: ReaderState::Slow5 { inner, col_idx },
})
}
pub fn header(&self) -> &Header {
&self.header
}
pub fn records_raw(&mut self) -> RecordsRaw<'_> {
RecordsRaw {
reader: self,
exhausted: false,
}
}
pub fn records(&mut self) -> impl Iterator<Item = Result<Record>> + '_ {
self.records_raw()
.map(|r| r.and_then(RawRecord::decompress))
}
}
#[cfg(feature = "rayon")]
impl Slow5Reader {
pub fn par_records(&mut self) -> impl rayon::iter::ParallelIterator<Item = Result<Record>> {
use rayon::iter::{IntoParallelIterator, ParallelIterator};
let raw: Vec<Result<RawRecord>> = self.records_raw().collect();
raw.into_par_iter()
.map(|r| r.and_then(RawRecord::decompress))
}
}
pub struct RecordsRaw<'a> {
reader: &'a mut Slow5Reader,
exhausted: bool,
}
impl<'a> Iterator for RecordsRaw<'a> {
type Item = Result<RawRecord>;
fn next(&mut self) -> Option<Self::Item> {
if self.exhausted {
return None;
}
let result = match &mut self.reader.state {
ReaderState::Blow5 {
inner,
record_compression,
signal_compression,
..
} => read_next_raw_record(
inner,
*record_compression,
*signal_compression,
&self.reader.header.aux_meta,
),
ReaderState::Slow5 { inner, col_idx } => {
crate::slow5::read_next_record(inner, col_idx, &self.reader.header.aux_meta)
}
};
match result {
Ok(None) => {
self.exhausted = true;
None
}
Ok(Some(r)) => Some(Ok(r)),
Err(e) => {
self.exhausted = true;
Some(Err(e))
}
}
}
}
fn read_next_raw_record<R: Read>(
reader: &mut R,
record_compression: RecordCompression,
signal_compression: SignalCompression,
aux_meta: &AuxMeta,
) -> Result<Option<RawRecord>> {
let mut prefix = [0u8; 5];
match reader.read_exact(&mut prefix) {
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
return Err(SlowError::InvalidFormat(
"unexpected EOF before record size or EOF marker".into(),
));
}
Err(e) => return Err(SlowError::Io(e)),
Ok(()) => {}
}
if &prefix == crate::blow5::EOF_MARKER {
return Ok(None);
}
let mut suffix = [0u8; 3];
reader.read_exact(&mut suffix)?;
let record_size_raw = u64::from_le_bytes([
prefix[0], prefix[1], prefix[2], prefix[3], prefix[4], suffix[0], suffix[1], suffix[2],
]);
const MAX_RECORD_SIZE: u64 = 256 * 1024 * 1024;
if record_size_raw > MAX_RECORD_SIZE {
return Err(SlowError::InvalidFormat(format!(
"record_size {record_size_raw} exceeds {MAX_RECORD_SIZE} byte limit"
)));
}
let record_size = record_size_raw as usize;
let mut record_bytes = vec![0u8; record_size];
reader.read_exact(&mut record_bytes)?;
let buf = match record_compression {
RecordCompression::None => record_bytes,
RecordCompression::Zstd => crate::compression::decompress_record_zstd(&record_bytes)?,
RecordCompression::Zlib => crate::compression::decompress_record_zlib(&record_bytes)?,
};
parse_raw_record(&buf, signal_compression, aux_meta).map(Some)
}
#[allow(unused_assignments)] pub(crate) fn parse_raw_record(
buf: &[u8],
signal_compression: SignalCompression,
aux_meta: &AuxMeta,
) -> Result<RawRecord> {
let mut pos: usize = 0;
macro_rules! read_bytes {
($n:expr) => {{
let end = pos.checked_add($n).ok_or_else(|| {
SlowError::InvalidFormat(format!("record offset overflow at {pos} + {}", $n))
})?;
if end > buf.len() {
return Err(SlowError::InvalidFormat(format!(
"record buffer too short at offset {pos}: need {}, have {}",
$n,
buf.len() - pos
)));
}
let slice = &buf[pos..end];
pos = end;
slice
}};
}
macro_rules! read_u16 {
() => {{
let b = read_bytes!(2);
u16::from_le_bytes([b[0], b[1]])
}};
}
macro_rules! read_u32 {
() => {{
let b = read_bytes!(4);
u32::from_le_bytes([b[0], b[1], b[2], b[3]])
}};
}
macro_rules! read_u64 {
() => {{
let b = read_bytes!(8);
u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
}};
}
macro_rules! read_f64 {
() => {{
let b = read_bytes!(8);
f64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
}};
}
let read_id_len = read_u16!() as usize;
let read_id_bytes = read_bytes!(read_id_len);
let read_id = std::str::from_utf8(read_id_bytes)
.map_err(|e| SlowError::InvalidFormat(format!("read_id is not valid UTF-8: {e}")))?
.to_string();
let read_group = read_u32!();
let digitisation = read_f64!();
let offset = read_f64!();
let range = read_f64!();
let sampling_rate = read_f64!();
let len_raw_signal = read_u64!();
let signal_byte_count = match signal_compression {
SignalCompression::None => (len_raw_signal as usize)
.checked_mul(2)
.ok_or_else(|| SlowError::InvalidFormat("len_raw_signal overflow".into()))?,
SignalCompression::SvbZd | SignalCompression::ExZd => len_raw_signal as usize,
};
let signal_bytes = read_bytes!(signal_byte_count).to_vec();
let aux = if aux_meta.is_empty() {
HashMap::new()
} else {
let mut aux_pos = pos;
let mut map = HashMap::with_capacity(aux_meta.len());
for (name, typ) in aux_meta.names.iter().zip(aux_meta.types.iter()) {
let val = crate::aux::decode_binary(buf, &mut aux_pos, typ)?;
map.insert(name.clone(), val);
}
map
};
Ok(RawRecord {
read_id,
read_group,
digitisation,
offset,
range,
sampling_rate,
signal_bytes,
signal_compression,
aux,
})
}