use std::fmt;
use std::fs::File;
use std::io::{self, BufReader, Read};
use std::path::Path;
use crate::borrowed::SequenceRef;
use crate::error::{Error, ParseError, Result};
use crate::format::{Compression, Format};
use crate::qual::{self, QualityEncoding};
use crate::record::Sequence;
pub const DEFAULT_BUFFER_SIZE: usize = 128 * 1024;
const MIN_BUFFER_SIZE: usize = 4 * 1024;
pub struct FastxReader<R: Read> {
inner: R,
buf: Vec<u8>,
pos: usize,
end: usize,
eof: bool,
format: Option<Format>,
line: u64,
quality_encoding: QualityEncoding,
max_line_length: Option<usize>,
max_record_length: Option<usize>,
spare_description: String,
joined_seq: Vec<u8>,
joined_quality: Vec<u8>,
scan_header: (usize, usize),
scan_seq: Vec<(usize, usize)>,
scan_quality: Vec<(usize, usize)>,
joined: bool,
}
struct Lines {
pos: usize,
end: usize,
eof: bool,
consumed: u64,
}
impl Lines {
fn new(pos: usize, end: usize, eof: bool) -> Lines {
Lines {
pos,
end,
eof,
consumed: 0,
}
}
fn exhausted(&self) -> bool {
self.pos == self.end && self.eof
}
fn peek_first_byte(&self, buf: &[u8]) -> Option<u8> {
buf.get(self.pos).copied().filter(|_| self.pos < self.end)
}
fn next(&mut self, buf: &[u8]) -> Option<(usize, usize)> {
match memchr::memchr(b'\n', &buf[self.pos..self.end]) {
Some(offset) => {
let newline = self.pos + offset;
let start = self.pos;
let mut stop = newline;
if stop > start && buf[stop - 1] == b'\r' {
stop -= 1;
}
self.pos = newline + 1;
self.consumed += 1;
Some((start, stop))
}
None if self.eof && self.pos < self.end => {
let start = self.pos;
let mut stop = self.end;
if stop > start && buf[stop - 1] == b'\r' {
stop -= 1;
}
self.pos = self.end;
self.consumed += 1;
Some((start, stop))
}
None => None,
}
}
}
fn trim_cr(region: &[u8], start: usize, stop: usize) -> usize {
if stop > start && region[stop - 1] == b'\r' {
stop - 1
} else {
stop
}
}
fn join_ranges(ranges: &[(usize, usize)], buf: &[u8], out: &mut Vec<u8>) {
out.clear();
for &(start, stop) in ranges {
out.extend_from_slice(&buf[start..stop]);
}
}
impl<R: Read> FastxReader<R> {
pub fn new(inner: R) -> FastxReader<R> {
FastxReader::with_capacity(inner, DEFAULT_BUFFER_SIZE)
}
pub fn with_format(inner: R, format: Format) -> FastxReader<R> {
let mut reader = FastxReader::new(inner);
reader.format = Some(format);
reader
}
pub fn with_capacity(inner: R, capacity: usize) -> FastxReader<R> {
FastxReader {
inner,
buf: vec![0; capacity.max(MIN_BUFFER_SIZE)],
pos: 0,
end: 0,
eof: false,
format: None,
line: 0,
quality_encoding: QualityEncoding::Phred33,
max_line_length: None,
max_record_length: None,
spare_description: String::new(),
joined_seq: Vec::new(),
joined_quality: Vec::new(),
scan_header: (0, 0),
scan_seq: Vec::new(),
scan_quality: Vec::new(),
joined: false,
}
}
pub fn format(&self) -> Option<Format> {
self.format
}
pub fn quality_encoding(&self) -> QualityEncoding {
self.quality_encoding
}
pub fn line_number(&self) -> u64 {
self.line
}
pub fn into_inner(self) -> R {
self.inner
}
pub fn read_into(&mut self, record: &mut Sequence) -> Result<bool> {
let format = match self.format {
Some(format) => {
if !self.skip_blank_lines()? {
return Ok(false);
}
format
}
None => match self.detect_format()? {
Some(format) => {
self.format = Some(format);
format
}
None => return Ok(false),
},
};
if let Some(mut previous) = record.description.take() {
if previous.capacity() > self.spare_description.capacity() {
previous.clear();
self.spare_description = previous;
}
}
record.clear();
match format {
Format::Fasta => self.read_fasta_into(record)?,
Format::Fastq => self.read_fastq_into(record)?,
}
Ok(true)
}
pub fn read_record(&mut self) -> Result<Option<Sequence>> {
let mut record = Sequence::default();
if self.read_into(&mut record)? {
Ok(Some(record))
} else {
Ok(None)
}
}
pub fn records(&mut self) -> Records<'_, R> {
Records { reader: self }
}
pub fn for_each_record<F>(&mut self, mut f: F) -> Result<()>
where
F: FnMut(&Sequence) -> Result<()>,
{
let mut record = Sequence::default();
while self.read_into(&mut record)? {
f(&record)?;
}
Ok(())
}
pub fn read_ref(&mut self) -> Result<Option<SequenceRef<'_>>> {
let format = match self.format {
Some(format) => {
if !self.skip_blank_lines()? {
return Ok(None);
}
format
}
None => match self.detect_format()? {
Some(format) => {
self.format = Some(format);
format
}
None => return Ok(None),
},
};
match format {
Format::Fasta => self.read_fasta_ref(),
Format::Fastq => self.read_fastq_ref(),
}
.map(Some)
}
pub fn for_each_ref<F>(&mut self, mut f: F) -> Result<()>
where
F: FnMut(SequenceRef<'_>) -> Result<()>,
{
while let Some(record) = self.read_ref()? {
f(record)?;
}
Ok(())
}
pub fn count_records(&mut self) -> Result<u64> {
let mut n = 0;
let mut record = Sequence::default();
while self.read_into(&mut record)? {
n += 1;
}
Ok(n)
}
fn read_fasta_into(&mut self, record: &mut Sequence) -> Result<()> {
let (start, end) = match self.read_line()? {
Some(range) => range,
None => {
return Err(Error::parse(
self.line,
ParseError::UnexpectedEof {
expected: "a FASTA header",
},
))
}
};
if self.buf[start] != b'>' && self.buf[start] != b';' {
return Err(Error::parse(
self.line,
ParseError::ExpectedHeader {
found: self.buf[start],
},
));
}
record.set_header_reusing(&self.buf[start + 1..end], &mut self.spare_description);
if record.id.is_empty() {
return Err(Error::parse(self.line, ParseError::EmptyId));
}
loop {
match self.peek_byte()? {
None | Some(b'>') => break,
_ => {
let (start, end) = self.read_line()?.expect("peeked byte is available");
record.seq.extend_from_slice(&self.buf[start..end]);
self.check_record_limit(record.seq.len(), "sequence")?;
}
}
}
Ok(())
}
fn read_fastq_into(&mut self, record: &mut Sequence) -> Result<()> {
let (start, end) = match self.read_line()? {
Some(range) => range,
None => {
return Err(Error::parse(
self.line,
ParseError::UnexpectedEof {
expected: "a FASTQ header",
},
))
}
};
if self.buf[start] != b'@' {
return Err(Error::parse(
self.line,
ParseError::ExpectedHeader {
found: self.buf[start],
},
));
}
record.set_header_reusing(&self.buf[start + 1..end], &mut self.spare_description);
if record.id.is_empty() {
return Err(Error::parse(self.line, ParseError::EmptyId));
}
loop {
match self.peek_byte()? {
None => {
return Err(Error::parse(
self.line,
ParseError::UnexpectedEof {
expected: "a FASTQ '+' separator",
},
))
}
Some(b'+') => {
self.read_line()?;
break;
}
_ => {
let (start, end) = self.read_line()?.expect("peeked byte is available");
record.seq.extend_from_slice(&self.buf[start..end]);
self.check_record_limit(record.seq.len(), "sequence")?;
}
}
}
let quality = record.quality.get_or_insert_with(Vec::new);
while quality.len() < record.seq.len() {
match self.read_line()? {
Some((start, end)) => quality.extend_from_slice(&self.buf[start..end]),
None => {
return Err(Error::LengthMismatch {
id: record.id.clone(),
seq: record.seq.len(),
quality: quality.len(),
})
}
}
}
if quality.len() != record.seq.len() {
return Err(Error::LengthMismatch {
id: record.id.clone(),
seq: record.seq.len(),
quality: quality.len(),
});
}
if self.quality_encoding != QualityEncoding::Phred33 {
let from = self.quality_encoding.offset();
for c in quality.iter_mut() {
*c = qual::encode(qual::score(*c, from), qual::PHRED33);
}
}
Ok(())
}
fn check_line_limit(&self, length: usize) -> Result<()> {
match self.max_line_length {
Some(limit) if length > limit => Err(Error::TooLarge {
line: self.line + 1,
what: "line",
limit,
}),
_ => Ok(()),
}
}
fn check_record_limit(&self, length: usize, what: &'static str) -> Result<()> {
match self.max_record_length {
Some(limit) if length > limit => Err(Error::TooLarge {
line: self.line,
what,
limit,
}),
_ => Ok(()),
}
}
fn read_fastq_ref(&mut self) -> Result<SequenceRef<'_>> {
loop {
if self.scan_fastq_quick()? || self.scan_fastq()? {
break;
}
if self.eof {
return Err(Error::parse(
self.line,
ParseError::UnexpectedEof {
expected: "a complete FASTQ record",
},
));
}
self.check_record_limit(self.end - self.pos, "record")?;
self.refill()?;
}
if self.joined {
join_ranges(&self.scan_seq, &self.buf, &mut self.joined_seq);
join_ranges(&self.scan_quality, &self.buf, &mut self.joined_quality);
}
let (seq, quality) = if self.joined {
(&self.joined_seq[..], &self.joined_quality[..])
} else {
let seq = self.scan_seq[0];
let quality = self.scan_quality[0];
(&self.buf[seq.0..seq.1], &self.buf[quality.0..quality.1])
};
let (id, description) =
crate::record::split_header(&self.buf[self.scan_header.0..self.scan_header.1]);
if id.is_empty() {
return Err(Error::parse(self.line, ParseError::EmptyId));
}
Ok(SequenceRef::new(id, description, seq, Some(quality)))
}
fn read_fasta_ref(&mut self) -> Result<SequenceRef<'_>> {
loop {
if self.scan_fasta()? {
break;
}
self.check_record_limit(self.end - self.pos, "record")?;
self.refill()?;
}
if self.joined {
join_ranges(&self.scan_seq, &self.buf, &mut self.joined_seq);
}
let seq = if self.joined {
&self.joined_seq[..]
} else {
match self.scan_seq.first() {
Some(&(start, stop)) => &self.buf[start..stop],
None => &[][..],
}
};
let (id, description) =
crate::record::split_header(&self.buf[self.scan_header.0..self.scan_header.1]);
if id.is_empty() {
return Err(Error::parse(self.line, ParseError::EmptyId));
}
Ok(SequenceRef::new(id, description, seq, None))
}
fn scan_fastq_quick(&mut self) -> Result<bool> {
let region = &self.buf[self.pos..self.end];
let mut newlines = memchr::memchr_iter(b'\n', region);
let (Some(a), Some(b), Some(c), Some(d)) = (
newlines.next(),
newlines.next(),
newlines.next(),
newlines.next(),
) else {
return Ok(false);
};
let base = self.pos;
let header = (base, trim_cr(region, 0, a) + base);
let seq = (base + a + 1, trim_cr(region, a + 1, b) + base);
let plus = (base + b + 1, trim_cr(region, b + 1, c) + base);
let quality = (base + c + 1, trim_cr(region, c + 1, d) + base);
if self.buf[header.0] != b'@'
|| self.buf[plus.0] != b'+'
|| quality.1 - quality.0 != seq.1 - seq.0
{
return Ok(false);
}
self.check_line_limit(seq.1 - seq.0)?;
self.check_line_limit(quality.1 - quality.0)?;
self.check_record_limit(seq.1 - seq.0, "sequence")?;
self.scan_header = (header.0 + 1, header.1);
self.scan_seq.clear();
self.scan_seq.push(seq);
self.scan_quality.clear();
self.scan_quality.push(quality);
self.joined = false;
self.pos = base + d + 1;
self.line += 4;
Ok(true)
}
fn scan_fastq(&mut self) -> Result<bool> {
let mut lines = Lines::new(self.pos, self.end, self.eof);
let header = match lines.next(&self.buf) {
Some(range) => range,
None => return Ok(false),
};
if self.buf[header.0] != b'@' {
return Err(Error::parse(
self.line + 1,
ParseError::ExpectedHeader {
found: self.buf[header.0],
},
));
}
self.scan_seq.clear();
let mut seq_len = 0;
loop {
match lines.peek_first_byte(&self.buf) {
None => return Ok(false),
Some(b'+') => {
lines.next(&self.buf);
break;
}
Some(_) => match lines.next(&self.buf) {
None => return Ok(false),
Some(range) => {
seq_len += range.1 - range.0;
self.scan_seq.push(range);
}
},
}
}
self.scan_quality.clear();
let mut quality_len = 0;
while quality_len < seq_len {
match lines.next(&self.buf) {
None => return Ok(false),
Some(range) => {
quality_len += range.1 - range.0;
self.scan_quality.push(range);
}
}
}
if quality_len != seq_len {
let (id, _) = crate::record::split_header(&self.buf[header.0 + 1..header.1]);
return Err(Error::LengthMismatch {
id: String::from_utf8_lossy(id).into_owned(),
seq: seq_len,
quality: quality_len,
});
}
self.finish_scan(header, &lines)
}
fn scan_fasta(&mut self) -> Result<bool> {
let mut lines = Lines::new(self.pos, self.end, self.eof);
let header = match lines.next(&self.buf) {
Some(range) => range,
None => return Ok(false),
};
if self.buf[header.0] != b'>' && self.buf[header.0] != b';' {
return Err(Error::parse(
self.line + 1,
ParseError::ExpectedHeader {
found: self.buf[header.0],
},
));
}
self.scan_seq.clear();
self.scan_quality.clear();
loop {
match lines.peek_first_byte(&self.buf) {
Some(b'>') => break,
None if lines.exhausted() => break,
None => return Ok(false),
Some(_) => match lines.next(&self.buf) {
None => return Ok(false),
Some(range) => self.scan_seq.push(range),
},
}
}
self.finish_scan(header, &lines)
}
fn finish_scan(&mut self, header: (usize, usize), lines: &Lines) -> Result<bool> {
for &(start, stop) in self.scan_seq.iter().chain(self.scan_quality.iter()) {
self.check_line_limit(stop - start)?;
}
let length: usize = self.scan_seq.iter().map(|&(a, b)| b - a).sum();
self.check_record_limit(length, "sequence")?;
self.scan_header = (header.0 + 1, header.1);
self.joined = self.scan_seq.len() > 1 || self.scan_quality.len() > 1;
self.pos = lines.pos;
self.line += lines.consumed;
Ok(true)
}
fn skip_blank_lines(&mut self) -> Result<bool> {
loop {
match self.peek_byte()? {
None => return Ok(false),
Some(b'\n') => {
self.pos += 1;
self.line += 1;
}
Some(b'\r') => self.pos += 1,
Some(_) => return Ok(true),
}
}
}
fn detect_format(&mut self) -> Result<Option<Format>> {
if !self.skip_blank_lines()? {
return Ok(None);
}
let byte = self.buf[self.pos];
match Format::from_first_byte(byte) {
Some(format) => Ok(Some(format)),
None => Err(Error::parse(
self.line + 1,
ParseError::ExpectedHeader { found: byte },
)),
}
}
fn read_line(&mut self) -> Result<Option<(usize, usize)>> {
let mut search_from = self.pos;
loop {
if let Some(offset) = memchr::memchr(b'\n', &self.buf[search_from..self.end]) {
let newline = search_from + offset;
let start = self.pos;
let mut stop = newline;
if stop > start && self.buf[stop - 1] == b'\r' {
stop -= 1;
}
self.check_line_limit(stop - start)?;
self.pos = newline + 1;
self.line += 1;
return Ok(Some((start, stop)));
}
if self.eof {
if self.pos == self.end {
return Ok(None);
}
let start = self.pos;
let mut stop = self.end;
if stop > start && self.buf[stop - 1] == b'\r' {
stop -= 1;
}
self.check_line_limit(stop - start)?;
self.pos = self.end;
self.line += 1;
return Ok(Some((start, stop)));
}
self.check_line_limit(self.end - self.pos)?;
let previous_end = self.end;
let shift = self.refill()?;
search_from = previous_end - shift;
}
}
fn peek_byte(&mut self) -> Result<Option<u8>> {
while self.pos == self.end && !self.eof {
self.refill()?;
}
if self.pos == self.end {
Ok(None)
} else {
Ok(Some(self.buf[self.pos]))
}
}
fn refill(&mut self) -> Result<usize> {
let mut shift = 0;
if self.pos > 0 {
self.buf.copy_within(self.pos..self.end, 0);
shift = self.pos;
self.end -= self.pos;
self.pos = 0;
}
if self.end == self.buf.len() {
let grown = self.buf.len().saturating_mul(2).max(MIN_BUFFER_SIZE);
self.buf.resize(grown, 0);
}
loop {
match self.inner.read(&mut self.buf[self.end..]) {
Ok(0) => {
self.eof = true;
break;
}
Ok(n) => {
self.end += n;
break;
}
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(Error::Io(e)),
}
}
Ok(shift)
}
}
impl<R: Read> fmt::Debug for FastxReader<R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FastxReader")
.field("format", &self.format)
.field("buffer_size", &self.buf.len())
.field("buffered", &(self.end - self.pos))
.field("line", &self.line)
.field("eof", &self.eof)
.finish_non_exhaustive()
}
}
impl<R: Read> Iterator for FastxReader<R> {
type Item = Result<Sequence>;
fn next(&mut self) -> Option<Self::Item> {
match self.read_record() {
Ok(Some(record)) => Some(Ok(record)),
Ok(None) => None,
Err(e) => Some(Err(e)),
}
}
}
pub struct Records<'a, R: Read> {
reader: &'a mut FastxReader<R>,
}
impl<R: Read> Iterator for Records<'_, R> {
type Item = Result<Sequence>;
fn next(&mut self) -> Option<Self::Item> {
match self.reader.read_record() {
Ok(Some(record)) => Some(Ok(record)),
Ok(None) => None,
Err(e) => Some(Err(e)),
}
}
}
#[derive(Debug, Clone)]
pub struct ReaderBuilder {
format: Option<Format>,
buffer_size: usize,
quality_encoding: QualityEncoding,
max_line_length: Option<usize>,
max_record_length: Option<usize>,
}
impl Default for ReaderBuilder {
fn default() -> Self {
ReaderBuilder {
format: None,
buffer_size: DEFAULT_BUFFER_SIZE,
quality_encoding: QualityEncoding::Phred33,
max_line_length: None,
max_record_length: None,
}
}
}
impl ReaderBuilder {
pub fn new() -> ReaderBuilder {
ReaderBuilder::default()
}
pub fn format(mut self, format: Format) -> Self {
self.format = Some(format);
self
}
pub fn buffer_size(mut self, bytes: usize) -> Self {
self.buffer_size = bytes;
self
}
pub fn quality_encoding(mut self, encoding: QualityEncoding) -> Self {
self.quality_encoding = encoding;
self
}
pub fn max_line_length(mut self, bytes: usize) -> Self {
self.max_line_length = Some(bytes);
self
}
pub fn max_record_length(mut self, bytes: usize) -> Self {
self.max_record_length = Some(bytes);
self
}
pub fn build<R: Read>(&self, inner: R) -> FastxReader<R> {
let mut reader = FastxReader::with_capacity(inner, self.buffer_size);
reader.format = self.format;
reader.quality_encoding = self.quality_encoding;
reader.max_line_length = self.max_line_length;
reader.max_record_length = self.max_record_length;
reader
}
pub fn open<P: AsRef<Path>>(&self, path: P) -> Result<FastxReader<Box<dyn Read + Send>>> {
let path = path.as_ref();
let mut builder = self.clone();
if builder.format.is_none() {
builder.format = Format::from_path(path);
}
Ok(builder.build(open_reader(path)?))
}
}
pub type BoxedReader = FastxReader<Box<dyn Read + Send>>;
pub fn open<P: AsRef<Path>>(path: P) -> Result<BoxedReader> {
ReaderBuilder::default().open(path)
}
pub fn from_stdin() -> Result<BoxedReader> {
let stream = decompress(Box::new(io::stdin()))?;
Ok(FastxReader::new(stream))
}
fn open_reader(path: &Path) -> Result<Box<dyn Read + Send>> {
let file = File::open(path)
.map_err(|e| Error::Io(io::Error::new(e.kind(), format!("{}: {e}", path.display()))))?;
decompress(Box::new(BufReader::with_capacity(64 * 1024, file)))
}
fn decompress(mut stream: Box<dyn Read + Send>) -> Result<Box<dyn Read + Send>> {
let mut magic = [0u8; HEADER_LEN_PROBE];
let mut filled = 0;
while filled < magic.len() {
match stream.read(&mut magic[filled..]) {
Ok(0) => break,
Ok(n) => filled += n,
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(Error::Io(e)),
}
}
let head = io::Cursor::new(magic[..filled].to_vec());
let rejoined = head.chain(stream);
match Compression::from_magic(&magic[..filled]) {
Compression::None => Ok(Box::new(rejoined)),
Compression::Gzip | Compression::Bgzf => gunzip(rejoined, &magic[..filled]),
Compression::Zstd => unzstd(rejoined),
}
}
const HEADER_LEN_PROBE: usize = 18;
#[cfg(feature = "gzip")]
fn gunzip<R: Read + Send + 'static>(stream: R, head: &[u8]) -> Result<Box<dyn Read + Send>> {
#[cfg(feature = "parallel")]
if crate::bgzf::is_bgzf(head) {
return Ok(Box::new(crate::bgzf::ParallelBgzfReader::new(stream)));
}
let _ = head;
Ok(Box::new(flate2::read::MultiGzDecoder::new(stream)))
}
#[cfg(not(feature = "gzip"))]
fn gunzip<R: Read + Send + 'static>(_stream: R, _head: &[u8]) -> Result<Box<dyn Read + Send>> {
Err(Error::FeatureDisabled("gzip"))
}
#[cfg(feature = "zstd")]
fn unzstd<R: Read + Send + 'static>(stream: R) -> Result<Box<dyn Read + Send>> {
Ok(Box::new(zstd::stream::read::Decoder::new(stream)?))
}
#[cfg(not(feature = "zstd"))]
fn unzstd<R: Read + Send + 'static>(_stream: R) -> Result<Box<dyn Read + Send>> {
Err(Error::FeatureDisabled("zstd"))
}
#[cfg(test)]
mod tests {
use super::*;
fn ids(data: &[u8]) -> Vec<String> {
FastxReader::new(data).map(|r| r.unwrap().id).collect()
}
#[test]
fn reads_simple_fasta() {
let data = b">a desc here\nACGT\n>b\nTTTT\nGGGG\n";
let records: Vec<_> = FastxReader::new(&data[..])
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(records.len(), 2);
assert_eq!(records[0].id, "a");
assert_eq!(records[0].description.as_deref(), Some("desc here"));
assert_eq!(records[0].seq, b"ACGT");
assert_eq!(records[1].seq, b"TTTTGGGG");
assert!(records[1].quality.is_none());
}
#[test]
fn reads_simple_fastq() {
let data = b"@a\nACGT\n+\nIIII\n@b desc\nTT\n+b desc\n!!\n";
let records: Vec<_> = FastxReader::new(&data[..])
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(records.len(), 2);
assert_eq!(records[0].quality.as_deref(), Some(&b"IIII"[..]));
assert_eq!(records[1].id, "b");
assert_eq!(records[1].description.as_deref(), Some("desc"));
assert_eq!(records[1].quality.as_deref(), Some(&b"!!"[..]));
}
#[test]
fn detects_format() {
let mut reader = FastxReader::new(&b">a\nAC\n"[..]);
assert_eq!(reader.format(), None);
reader.next().unwrap().unwrap();
assert_eq!(reader.format(), Some(Format::Fasta));
let mut reader = FastxReader::new(&b"@a\nAC\n+\nII\n"[..]);
reader.next().unwrap().unwrap();
assert_eq!(reader.format(), Some(Format::Fastq));
}
#[test]
fn handles_crlf_and_missing_final_newline() {
let data = b">a\r\nACGT\r\nAC\r\n>b\r\nTT";
let records: Vec<_> = FastxReader::new(&data[..])
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(records[0].seq, b"ACGTAC");
assert_eq!(records[1].seq, b"TT");
}
#[test]
fn handles_blank_lines_between_records() {
let data = b"\n\n>a\nACGT\n\n\n>b\nTT\n\n";
assert_eq!(ids(&data[..]), ["a", "b"]);
let records: Vec<_> = FastxReader::new(&data[..])
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(records[0].seq, b"ACGT");
}
#[test]
fn handles_empty_input() {
assert_eq!(FastxReader::new(&b""[..]).count(), 0);
assert_eq!(FastxReader::new(&b"\n\n\n"[..]).count(), 0);
}
#[test]
fn multi_line_fastq() {
let data = b"@a\nACGT\nACGT\n+\nIIII\nJJJJ\n@b\nTT\n+\n!!\n";
let records: Vec<_> = FastxReader::new(&data[..])
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(records[0].seq, b"ACGTACGT");
assert_eq!(records[0].quality.as_deref(), Some(&b"IIIIJJJJ"[..]));
assert_eq!(records[1].id, "b");
}
#[test]
fn quality_starting_with_at_sign() {
let data = b"@a\nACGT\n+\n@@@@\n@b\nTTTT\n+\nIIII\n";
let records: Vec<_> = FastxReader::new(&data[..])
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(records.len(), 2);
assert_eq!(records[0].quality.as_deref(), Some(&b"@@@@"[..]));
assert_eq!(records[1].id, "b");
}
#[test]
fn tiny_buffer_still_parses() {
let long = "A".repeat(50_000);
let data = format!(">a\n{long}\n>b\nACGT\n");
let mut reader = FastxReader::with_capacity(data.as_bytes(), 1);
let records: Vec<_> = reader.records().collect::<Result<Vec<_>>>().unwrap();
assert_eq!(records.len(), 2);
assert_eq!(records[0].seq.len(), 50_000);
assert_eq!(records[1].seq, b"ACGT");
}
#[test]
fn description_buffer_is_reused_without_leaking_between_records() {
let data = b"@a first one\nAC\n+\nII\n\
@b\nGT\n+\nII\n\
@c third\nTT\n+\nII\n\
@d\nCC\n+\nII\n\
@e a much longer description than any before it\nGG\n+\nII\n";
let mut reader = FastxReader::new(&data[..]);
let mut record = Sequence::default();
let mut seen = Vec::new();
while reader.read_into(&mut record).unwrap() {
seen.push((record.id.clone(), record.description.clone()));
}
assert_eq!(
seen,
vec![
("a".to_string(), Some("first one".to_string())),
("b".to_string(), None),
("c".to_string(), Some("third".to_string())),
("d".to_string(), None),
(
"e".to_string(),
Some("a much longer description than any before it".to_string())
),
]
);
let mut record = Sequence::fasta("old", b"AAAA").with_description("stale description");
let data = b"@new\nAC\n+\nII\n";
assert!(FastxReader::new(&data[..]).read_into(&mut record).unwrap());
assert_eq!(record.id, "new");
assert_eq!(record.description, None);
assert_eq!(record.seq, b"AC");
}
#[test]
fn read_into_reuses_allocations() {
let data = b">a\nACGT\n>b\nTT\n";
let mut reader = FastxReader::new(&data[..]);
let mut record = Sequence::default();
assert!(reader.read_into(&mut record).unwrap());
assert_eq!(record.id, "a");
assert!(reader.read_into(&mut record).unwrap());
assert_eq!(record.id, "b");
assert_eq!(record.seq, b"TT");
assert!(!reader.read_into(&mut record).unwrap());
}
#[test]
fn empty_fasta_record_is_allowed() {
let data = b">a\n>b\nACGT\n";
let records: Vec<_> = FastxReader::new(&data[..])
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(records[0].seq, b"");
assert_eq!(records[1].seq, b"ACGT");
}
#[test]
fn rejects_garbage() {
let err = FastxReader::new(&b"not a sequence file\n"[..])
.next()
.unwrap()
.unwrap_err();
assert!(matches!(
err,
Error::Parse {
kind: ParseError::ExpectedHeader { found: b'n' },
..
}
));
}
#[test]
fn rejects_truncated_fastq() {
let err = FastxReader::new(&b"@a\nACGT\n"[..])
.next()
.unwrap()
.unwrap_err();
assert!(matches!(
err,
Error::Parse {
kind: ParseError::UnexpectedEof { .. },
..
}
));
let err = FastxReader::new(&b"@a\nACGT\n+\nII\n"[..])
.next()
.unwrap()
.unwrap_err();
assert!(matches!(
err,
Error::LengthMismatch {
seq: 4,
quality: 2,
..
}
));
}
#[test]
fn rejects_empty_id() {
let err = FastxReader::new(&b">\nACGT\n"[..])
.next()
.unwrap()
.unwrap_err();
assert!(matches!(
err,
Error::Parse {
kind: ParseError::EmptyId,
..
}
));
}
#[test]
fn reports_line_numbers() {
let data = b">a\nACGT\n>b\nACGT\nnope";
let mut reader = FastxReader::with_format(&data[..], Format::Fasta);
reader.next().unwrap().unwrap();
assert_eq!(reader.line_number(), 2);
}
#[test]
fn phred64_input_is_normalised_to_phred33() {
let data = b"@old\nACGT\n+\nhhhB\n";
let record = ReaderBuilder::new()
.quality_encoding(QualityEncoding::Phred64)
.build(&data[..])
.read_record()
.unwrap()
.unwrap();
assert_eq!(record.quality.as_deref(), Some(&b"III#"[..]));
assert_eq!(record.quality_scores().unwrap(), vec![40, 40, 40, 2]);
let record = FastxReader::new(&data[..]).read_record().unwrap().unwrap();
assert_eq!(record.quality.as_deref(), Some(&b"hhhB"[..]));
}
#[test]
fn line_length_limit_is_enforced() {
let long = format!(">a\n{}\n", "A".repeat(10_000));
let err = ReaderBuilder::new()
.max_line_length(1_000)
.build(long.as_bytes())
.read_record()
.unwrap_err();
assert!(
matches!(
err,
Error::TooLarge {
what: "line",
limit: 1_000,
..
}
),
"{err}"
);
let record = ReaderBuilder::new()
.max_line_length(1_000_000)
.build(long.as_bytes())
.read_record()
.unwrap()
.unwrap();
assert_eq!(record.seq.len(), 10_000);
}
#[test]
fn record_length_limit_catches_many_short_lines() {
let mut data = String::from(">a\n");
for _ in 0..200 {
data.push_str(&"A".repeat(50));
data.push('\n');
}
let err = ReaderBuilder::new()
.max_line_length(1_000)
.max_record_length(5_000)
.build(data.as_bytes())
.read_record()
.unwrap_err();
assert!(
matches!(
err,
Error::TooLarge {
what: "sequence",
limit: 5_000,
..
}
),
"{err}"
);
}
#[test]
fn limits_are_unlimited_by_default() {
let long = format!(">chrom\n{}\n", "ACGT".repeat(50_000));
let record = FastxReader::with_capacity(long.as_bytes(), 4096)
.read_record()
.unwrap()
.unwrap();
assert_eq!(record.seq.len(), 200_000);
}
#[test]
fn forced_format_reads_fasta_as_written() {
let data = b">a\nACGT\n";
let mut reader = FastxReader::with_format(&data[..], Format::Fasta);
assert_eq!(reader.format(), Some(Format::Fasta));
assert_eq!(reader.next().unwrap().unwrap().seq, b"ACGT");
}
}