use std::fs::File;
use std::io::{BufReader, Seek, SeekFrom};
use std::path::Path;
const BUF_SIZE: usize = 512 * 1024;
use crate::Result;
use crate::error::SlowError;
use crate::header::{Header, RecordCompression};
use crate::index::Index;
use crate::record::Record;
use crate::slow5::PrimaryColIndices;
enum IndexedReaderState {
Blow5,
Slow5 { col_idx: PrimaryColIndices },
}
pub struct Slow5IndexedReader {
file: File,
index: Index,
header: Header,
state: IndexedReaderState,
}
unsafe impl Sync for Slow5IndexedReader {}
impl Slow5IndexedReader {
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 extension '.{other}': expected '.blow5' or '.slow5'"
))),
}
}
fn open_blow5(path: &Path) -> Result<Self> {
let mut buf_reader = BufReader::with_capacity(BUF_SIZE, File::open(path)?);
let header = crate::blow5::parse_header(&mut buf_reader)?;
let start_rec_offset = buf_reader.stream_position().map_err(SlowError::Io)?;
let mut file = buf_reader.into_inner();
let idx_path = sidecar_idx_path(path)?;
let index = if idx_path.exists() {
Index::read_from_path(&idx_path, header.version)?
} else {
file.seek(SeekFrom::Start(start_rec_offset))
.map_err(SlowError::Io)?;
let index = {
let mut scan = BufReader::with_capacity(BUF_SIZE, &mut file);
Index::build_from_reader(&mut scan, header.record_compression, start_rec_offset)?
};
if let Err(e) = index.write_to_path(&idx_path, header.version) {
eprintln!("slow5lib: warning: failed to write index to {idx_path:?}: {e}");
}
index
};
Ok(Self {
file,
index,
header,
state: IndexedReaderState::Blow5,
})
}
fn open_slow5(path: &Path) -> Result<Self> {
let mut buf_reader = BufReader::with_capacity(BUF_SIZE, File::open(path)?);
let (header, col_idx) = crate::slow5::parse_header(&mut buf_reader)?;
let start_rec_offset = buf_reader.stream_position().map_err(SlowError::Io)?;
let mut file = buf_reader.into_inner();
let idx_path = sidecar_idx_path(path)?;
let index = if idx_path.exists() {
Index::read_from_path(&idx_path, header.version)?
} else {
file.seek(SeekFrom::Start(start_rec_offset))
.map_err(SlowError::Io)?;
let index = {
let mut scan = BufReader::with_capacity(BUF_SIZE, &mut file);
Index::build_from_slow5_reader(&mut scan, start_rec_offset)?
};
if let Err(e) = index.write_to_path(&idx_path, header.version) {
eprintln!("slow5lib: warning: failed to write index to {idx_path:?}: {e}");
}
index
};
Ok(Self {
file,
index,
header,
state: IndexedReaderState::Slow5 { col_idx },
})
}
pub fn header(&self) -> &Header {
&self.header
}
pub fn read_ids(&self) -> impl Iterator<Item = &str> {
self.index.read_ids()
}
pub fn get(&self, read_id: &str) -> Result<Record> {
let entry = self
.index
.get(read_id)
.ok_or_else(|| SlowError::NotFound(read_id.to_string()))?;
match &self.state {
IndexedReaderState::Blow5 => {
let mut buf = vec![0u8; entry.size as usize];
read_exact_at(&self.file, &mut buf, entry.offset)?;
let record_bytes = &buf[8..];
let decompressed = match self.header.record_compression {
RecordCompression::None => record_bytes.to_vec(),
RecordCompression::Zstd => {
crate::compression::decompress_record_zstd(record_bytes)?
}
RecordCompression::Zlib => {
crate::compression::decompress_record_zlib(record_bytes)?
}
};
crate::reader::parse_raw_record(
&decompressed,
self.header.signal_compression,
&self.header.aux_meta,
)?
.decompress()
}
IndexedReaderState::Slow5 { col_idx } => {
let mut buf = vec![0u8; entry.size as usize];
read_exact_at(&self.file, &mut buf, entry.offset)?;
let line = std::str::from_utf8(&buf).map_err(|e| {
SlowError::InvalidFormat(format!("SLOW5 line is not valid UTF-8: {e}"))
})?;
crate::slow5::parse_record_line(line.trim_end(), col_idx, &self.header.aux_meta)?
.decompress()
}
}
}
}
fn sidecar_idx_path(path: &Path) -> Result<std::path::PathBuf> {
let name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| SlowError::InvalidFormat("path has no file name".into()))?;
Ok(path.with_file_name(format!("{name}.idx")))
}
fn read_exact_at(file: &File, buf: &mut [u8], offset: u64) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::FileExt;
file.read_exact_at(buf, offset).map_err(SlowError::Io)
}
#[cfg(not(unix))]
{
let _ = (file, buf, offset);
Err(SlowError::InvalidFormat(
"indexed random access is not yet supported on this platform".into(),
))
}
}