Skip to main content

fastx/
reader.rs

1//! Streaming FASTA/FASTQ reader.
2
3use std::fmt;
4use std::fs::File;
5use std::io::{self, BufReader, Read};
6use std::path::Path;
7
8use crate::error::{Error, ParseError, Result};
9use crate::format::{Compression, Format};
10use crate::qual::{self, QualityEncoding};
11use crate::record::Sequence;
12
13/// Default read buffer, large enough to hold a full 150 bp read set line and to
14/// keep syscall overhead negligible.
15pub const DEFAULT_BUFFER_SIZE: usize = 128 * 1024;
16
17/// Smallest buffer we will honour; smaller values just make parsing slower.
18const MIN_BUFFER_SIZE: usize = 4 * 1024;
19
20/// A streaming reader for FASTA and FASTQ.
21///
22/// The reader owns a single growable buffer and never holds more than one record
23/// plus the buffer in memory, so a 300 GB FASTQ costs the same as a 300 byte one.
24/// Records with lines longer than the buffer are handled by growing the buffer on
25/// demand, which means a chromosome-on-one-line FASTA works too.
26///
27/// # Examples
28///
29/// ```
30/// use fastx::FastxReader;
31///
32/// let data = b">read1 first\nACGT\nACGT\n>read2\nTTTT\n";
33/// let mut reader = FastxReader::new(&data[..]);
34///
35/// let first = reader.next().unwrap()?;
36/// assert_eq!(first.id, "read1");
37/// assert_eq!(first.description.as_deref(), Some("first"));
38/// assert_eq!(first.seq, b"ACGTACGT"); // multi-line sequences are joined
39///
40/// assert_eq!(reader.count(), 1); // one record left
41/// # Ok::<(), fastx::Error>(())
42/// ```
43///
44/// Reuse one record to parse without allocating:
45///
46/// ```
47/// use fastx::{FastxReader, Sequence};
48///
49/// let data = b"@r1\nACGT\n+\nIIII\n@r2\nTTTT\n+\n!!!!\n";
50/// let mut reader = FastxReader::new(&data[..]);
51/// let mut record = Sequence::default();
52/// let mut bases = 0;
53/// while reader.read_into(&mut record)? {
54///     bases += record.len();
55/// }
56/// assert_eq!(bases, 8);
57/// # Ok::<(), fastx::Error>(())
58/// ```
59pub struct FastxReader<R: Read> {
60    inner: R,
61    buf: Vec<u8>,
62    /// Cursor of the next unconsumed byte in `buf`.
63    pos: usize,
64    /// Number of valid bytes in `buf`.
65    end: usize,
66    eof: bool,
67    format: Option<Format>,
68    line: u64,
69    quality_encoding: QualityEncoding,
70    max_line_length: Option<usize>,
71    max_record_length: Option<usize>,
72}
73
74impl<R: Read> FastxReader<R> {
75    /// A reader that determines the format from the first record header.
76    pub fn new(inner: R) -> FastxReader<R> {
77        FastxReader::with_capacity(inner, DEFAULT_BUFFER_SIZE)
78    }
79
80    /// A reader with an explicit format, skipping auto-detection.
81    pub fn with_format(inner: R, format: Format) -> FastxReader<R> {
82        let mut reader = FastxReader::new(inner);
83        reader.format = Some(format);
84        reader
85    }
86
87    /// A reader with a custom buffer size (clamped to at least 4 KiB).
88    pub fn with_capacity(inner: R, capacity: usize) -> FastxReader<R> {
89        FastxReader {
90            inner,
91            buf: vec![0; capacity.max(MIN_BUFFER_SIZE)],
92            pos: 0,
93            end: 0,
94            eof: false,
95            format: None,
96            line: 0,
97            quality_encoding: QualityEncoding::Phred33,
98            max_line_length: None,
99            max_record_length: None,
100        }
101    }
102
103    /// The format, once known. `None` before the first record has been read on
104    /// an auto-detecting reader.
105    pub fn format(&self) -> Option<Format> {
106        self.format
107    }
108
109    /// The quality encoding of the *input*.
110    ///
111    /// Records themselves always come out as Phred+33: anything else is
112    /// normalised while parsing, so downstream code never has to ask.
113    pub fn quality_encoding(&self) -> QualityEncoding {
114        self.quality_encoding
115    }
116
117    /// The 1-based number of the last line consumed; useful for error messages.
118    pub fn line_number(&self) -> u64 {
119        self.line
120    }
121
122    /// Unwrap the underlying reader, discarding any buffered bytes.
123    pub fn into_inner(self) -> R {
124        self.inner
125    }
126
127    /// Parse the next record into `record`, reusing its allocations.
128    ///
129    /// Returns `Ok(false)` at end of input. This is the allocation-free core of
130    /// the reader; [`Iterator::next`] is a thin wrapper over it.
131    pub fn read_into(&mut self, record: &mut Sequence) -> Result<bool> {
132        let format = match self.format {
133            Some(format) => {
134                if !self.skip_blank_lines()? {
135                    return Ok(false);
136                }
137                format
138            }
139            None => match self.detect_format()? {
140                Some(format) => {
141                    self.format = Some(format);
142                    format
143                }
144                None => return Ok(false),
145            },
146        };
147        record.clear();
148        match format {
149            Format::Fasta => self.read_fasta_into(record)?,
150            Format::Fastq => self.read_fastq_into(record)?,
151        }
152        Ok(true)
153    }
154
155    /// Parse the next record into a fresh [`Sequence`].
156    pub fn read_record(&mut self) -> Result<Option<Sequence>> {
157        let mut record = Sequence::default();
158        if self.read_into(&mut record)? {
159            Ok(Some(record))
160        } else {
161            Ok(None)
162        }
163    }
164
165    /// Borrowing iterator over the remaining records.
166    ///
167    /// Prefer this over consuming the reader when you still need it afterwards.
168    pub fn records(&mut self) -> Records<'_, R> {
169        Records { reader: self }
170    }
171
172    /// Run `f` on every remaining record, reusing a single buffer.
173    ///
174    /// This is the fastest way to consume a file and the one to reach for in
175    /// pipelines: no per-record allocation, no `Result` per record to unwrap.
176    ///
177    /// ```
178    /// use fastx::FastxReader;
179    ///
180    /// let data = b">a\nACGT\n>b\nGGCC\n";
181    /// let mut total = 0;
182    /// FastxReader::new(&data[..]).for_each_record(|r| { total += r.len(); Ok(()) })?;
183    /// assert_eq!(total, 8);
184    /// # Ok::<(), fastx::Error>(())
185    /// ```
186    pub fn for_each_record<F>(&mut self, mut f: F) -> Result<()>
187    where
188        F: FnMut(&Sequence) -> Result<()>,
189    {
190        let mut record = Sequence::default();
191        while self.read_into(&mut record)? {
192            f(&record)?;
193        }
194        Ok(())
195    }
196
197    /// Count the remaining records without keeping them.
198    pub fn count_records(&mut self) -> Result<u64> {
199        let mut n = 0;
200        let mut record = Sequence::default();
201        while self.read_into(&mut record)? {
202            n += 1;
203        }
204        Ok(n)
205    }
206
207    // ----- parsing ---------------------------------------------------------
208
209    fn read_fasta_into(&mut self, record: &mut Sequence) -> Result<()> {
210        let (start, end) = match self.read_line()? {
211            Some(range) => range,
212            None => {
213                return Err(Error::parse(
214                    self.line,
215                    ParseError::UnexpectedEof {
216                        expected: "a FASTA header",
217                    },
218                ))
219            }
220        };
221        if self.buf[start] != b'>' && self.buf[start] != b';' {
222            return Err(Error::parse(
223                self.line,
224                ParseError::ExpectedHeader {
225                    found: self.buf[start],
226                },
227            ));
228        }
229        record.set_header(&self.buf[start + 1..end]);
230        if record.id.is_empty() {
231            return Err(Error::parse(self.line, ParseError::EmptyId));
232        }
233        loop {
234            match self.peek_byte()? {
235                None | Some(b'>') => break,
236                _ => {
237                    let (start, end) = self.read_line()?.expect("peeked byte is available");
238                    record.seq.extend_from_slice(&self.buf[start..end]);
239                    self.check_record_limit(record.seq.len(), "sequence")?;
240                }
241            }
242        }
243        Ok(())
244    }
245
246    fn read_fastq_into(&mut self, record: &mut Sequence) -> Result<()> {
247        let (start, end) = match self.read_line()? {
248            Some(range) => range,
249            None => {
250                return Err(Error::parse(
251                    self.line,
252                    ParseError::UnexpectedEof {
253                        expected: "a FASTQ header",
254                    },
255                ))
256            }
257        };
258        if self.buf[start] != b'@' {
259            return Err(Error::parse(
260                self.line,
261                ParseError::ExpectedHeader {
262                    found: self.buf[start],
263                },
264            ));
265        }
266        record.set_header(&self.buf[start + 1..end]);
267        if record.id.is_empty() {
268            return Err(Error::parse(self.line, ParseError::EmptyId));
269        }
270
271        // Sequence lines, up to the '+' separator. Multi-line FASTQ is rare but
272        // legal, and a '+' can never start a sequence line.
273        loop {
274            match self.peek_byte()? {
275                None => {
276                    return Err(Error::parse(
277                        self.line,
278                        ParseError::UnexpectedEof {
279                            expected: "a FASTQ '+' separator",
280                        },
281                    ))
282                }
283                Some(b'+') => {
284                    self.read_line()?;
285                    break;
286                }
287                _ => {
288                    let (start, end) = self.read_line()?.expect("peeked byte is available");
289                    record.seq.extend_from_slice(&self.buf[start..end]);
290                    self.check_record_limit(record.seq.len(), "sequence")?;
291                }
292            }
293        }
294
295        // Quality lines. Because a quality character may itself be '@', the only
296        // safe terminator is having collected as many scores as bases.
297        let quality = record.quality.get_or_insert_with(Vec::new);
298        while quality.len() < record.seq.len() {
299            match self.read_line()? {
300                Some((start, end)) => quality.extend_from_slice(&self.buf[start..end]),
301                None => {
302                    return Err(Error::LengthMismatch {
303                        id: record.id.clone(),
304                        seq: record.seq.len(),
305                        quality: quality.len(),
306                    })
307                }
308            }
309        }
310        if quality.len() != record.seq.len() {
311            return Err(Error::LengthMismatch {
312                id: record.id.clone(),
313                seq: record.seq.len(),
314                quality: quality.len(),
315            });
316        }
317        // Normalise to Phred+33 so that a `Sequence` has exactly one encoding,
318        // whatever the file used.
319        if self.quality_encoding != QualityEncoding::Phred33 {
320            let from = self.quality_encoding.offset();
321            for c in quality.iter_mut() {
322                *c = qual::encode(qual::score(*c, from), qual::PHRED33);
323            }
324        }
325        Ok(())
326    }
327
328    /// Fail rather than buffer without bound when a line exceeds its limit.
329    fn check_line_limit(&self, length: usize) -> Result<()> {
330        match self.max_line_length {
331            Some(limit) if length > limit => Err(Error::TooLarge {
332                line: self.line + 1,
333                what: "line",
334                limit,
335            }),
336            _ => Ok(()),
337        }
338    }
339
340    /// Fail rather than grow without bound when a record exceeds its limit.
341    fn check_record_limit(&self, length: usize, what: &'static str) -> Result<()> {
342        match self.max_record_length {
343            Some(limit) if length > limit => Err(Error::TooLarge {
344                line: self.line,
345                what,
346                limit,
347            }),
348            _ => Ok(()),
349        }
350    }
351
352    /// Advance past blank lines. Returns false at end of input.
353    fn skip_blank_lines(&mut self) -> Result<bool> {
354        loop {
355            match self.peek_byte()? {
356                None => return Ok(false),
357                Some(b'\n') => {
358                    self.pos += 1;
359                    self.line += 1;
360                }
361                Some(b'\r') => self.pos += 1,
362                Some(_) => return Ok(true),
363            }
364        }
365    }
366
367    /// Sniff the format from the first meaningful byte without consuming it.
368    fn detect_format(&mut self) -> Result<Option<Format>> {
369        if !self.skip_blank_lines()? {
370            return Ok(None);
371        }
372        let byte = self.buf[self.pos];
373        match Format::from_first_byte(byte) {
374            Some(format) => Ok(Some(format)),
375            None => Err(Error::parse(
376                self.line + 1,
377                ParseError::ExpectedHeader { found: byte },
378            )),
379        }
380    }
381
382    // ----- buffer management ------------------------------------------------
383
384    /// Consume one line, returning its bounds in `self.buf` without the line
385    /// terminator. Returns `None` only at end of input.
386    fn read_line(&mut self) -> Result<Option<(usize, usize)>> {
387        let mut search_from = self.pos;
388        loop {
389            if let Some(offset) = memchr::memchr(b'\n', &self.buf[search_from..self.end]) {
390                let newline = search_from + offset;
391                let start = self.pos;
392                let mut stop = newline;
393                if stop > start && self.buf[stop - 1] == b'\r' {
394                    stop -= 1;
395                }
396                self.check_line_limit(stop - start)?;
397                self.pos = newline + 1;
398                self.line += 1;
399                return Ok(Some((start, stop)));
400            }
401            if self.eof {
402                if self.pos == self.end {
403                    return Ok(None);
404                }
405                // Final line without a trailing newline.
406                let start = self.pos;
407                let mut stop = self.end;
408                if stop > start && self.buf[stop - 1] == b'\r' {
409                    stop -= 1;
410                }
411                self.check_line_limit(stop - start)?;
412                self.pos = self.end;
413                self.line += 1;
414                return Ok(Some((start, stop)));
415            }
416            // No newline yet, so everything buffered belongs to the current line.
417            // Checking here as well is what stops the buffer growing without
418            // bound on input that never supplies a newline at all.
419            self.check_line_limit(self.end - self.pos)?;
420            let previous_end = self.end;
421            let shift = self.refill()?;
422            search_from = previous_end - shift;
423        }
424    }
425
426    /// Ensure at least one byte is buffered and return it without consuming.
427    fn peek_byte(&mut self) -> Result<Option<u8>> {
428        while self.pos == self.end && !self.eof {
429            self.refill()?;
430        }
431        if self.pos == self.end {
432            Ok(None)
433        } else {
434            Ok(Some(self.buf[self.pos]))
435        }
436    }
437
438    /// Move unconsumed bytes to the front, grow if the buffer is full, then read.
439    /// Returns how far indices into `buf` shifted left.
440    fn refill(&mut self) -> Result<usize> {
441        let mut shift = 0;
442        if self.pos > 0 {
443            self.buf.copy_within(self.pos..self.end, 0);
444            shift = self.pos;
445            self.end -= self.pos;
446            self.pos = 0;
447        }
448        if self.end == self.buf.len() {
449            // A single line longer than the buffer: double it.
450            let grown = self.buf.len().saturating_mul(2).max(MIN_BUFFER_SIZE);
451            self.buf.resize(grown, 0);
452        }
453        loop {
454            match self.inner.read(&mut self.buf[self.end..]) {
455                Ok(0) => {
456                    self.eof = true;
457                    break;
458                }
459                Ok(n) => {
460                    self.end += n;
461                    break;
462                }
463                Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
464                Err(e) => return Err(Error::Io(e)),
465            }
466        }
467        Ok(shift)
468    }
469}
470
471impl<R: Read> fmt::Debug for FastxReader<R> {
472    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
473        f.debug_struct("FastxReader")
474            .field("format", &self.format)
475            .field("buffer_size", &self.buf.len())
476            .field("buffered", &(self.end - self.pos))
477            .field("line", &self.line)
478            .field("eof", &self.eof)
479            .finish_non_exhaustive()
480    }
481}
482
483impl<R: Read> Iterator for FastxReader<R> {
484    type Item = Result<Sequence>;
485
486    fn next(&mut self) -> Option<Self::Item> {
487        match self.read_record() {
488            Ok(Some(record)) => Some(Ok(record)),
489            Ok(None) => None,
490            Err(e) => Some(Err(e)),
491        }
492    }
493}
494
495/// Borrowing iterator returned by [`FastxReader::records`].
496pub struct Records<'a, R: Read> {
497    reader: &'a mut FastxReader<R>,
498}
499
500impl<R: Read> Iterator for Records<'_, R> {
501    type Item = Result<Sequence>;
502
503    fn next(&mut self) -> Option<Self::Item> {
504        match self.reader.read_record() {
505            Ok(Some(record)) => Some(Ok(record)),
506            Ok(None) => None,
507            Err(e) => Some(Err(e)),
508        }
509    }
510}
511
512/// Configuration for a [`FastxReader`].
513///
514/// ```
515/// use fastx::{Format, ReaderBuilder};
516///
517/// let data = b">a\nACGT\n";
518/// let mut reader = ReaderBuilder::new()
519///     .format(Format::Fasta)
520///     .buffer_size(64 * 1024)
521///     .build(&data[..]);
522/// assert_eq!(reader.next().unwrap()?.id, "a");
523/// # Ok::<(), fastx::Error>(())
524/// ```
525#[derive(Debug, Clone)]
526pub struct ReaderBuilder {
527    format: Option<Format>,
528    buffer_size: usize,
529    quality_encoding: QualityEncoding,
530    max_line_length: Option<usize>,
531    max_record_length: Option<usize>,
532}
533
534impl Default for ReaderBuilder {
535    fn default() -> Self {
536        ReaderBuilder {
537            format: None,
538            buffer_size: DEFAULT_BUFFER_SIZE,
539            quality_encoding: QualityEncoding::Phred33,
540            max_line_length: None,
541            max_record_length: None,
542        }
543    }
544}
545
546impl ReaderBuilder {
547    /// A builder with default settings.
548    pub fn new() -> ReaderBuilder {
549        ReaderBuilder::default()
550    }
551
552    /// Force a format instead of detecting it.
553    pub fn format(mut self, format: Format) -> Self {
554        self.format = Some(format);
555        self
556    }
557
558    /// Size of the internal read buffer, in bytes.
559    pub fn buffer_size(mut self, bytes: usize) -> Self {
560        self.buffer_size = bytes;
561        self
562    }
563
564    /// The quality encoding of the input.
565    ///
566    /// Old Illumina 1.3–1.7 files are Phred+64. Set this and the reader will
567    /// convert quality strings to Phred+33 as it parses, so every [`Sequence`]
568    /// this crate produces uses one encoding.
569    ///
570    /// ```
571    /// use fastx::{qual::QualityEncoding, ReaderBuilder};
572    ///
573    /// // 'h' is Q40 in Phred+64.
574    /// let data = b"@old\nACGT\n+\nhhhh\n";
575    /// let record = ReaderBuilder::new()
576    ///     .quality_encoding(QualityEncoding::Phred64)
577    ///     .build(&data[..])
578    ///     .read_record()?
579    ///     .unwrap();
580    ///
581    /// assert_eq!(record.quality.as_deref(), Some(&b"IIII"[..])); // now Phred+33
582    /// assert_eq!(record.mean_quality(), Some(40.0));
583    /// # Ok::<(), fastx::Error>(())
584    /// ```
585    pub fn quality_encoding(mut self, encoding: QualityEncoding) -> Self {
586        self.quality_encoding = encoding;
587        self
588    }
589
590    /// Refuse lines longer than `bytes` instead of growing the buffer.
591    ///
592    /// Unlimited by default, because a chromosome legitimately arrives on a
593    /// single line. Set it when reading files you do not control: without a
594    /// limit, one unterminated line can grow the buffer until the process is
595    /// killed.
596    pub fn max_line_length(mut self, bytes: usize) -> Self {
597        self.max_line_length = Some(bytes);
598        self
599    }
600
601    /// Refuse records whose sequence exceeds `bytes`.
602    ///
603    /// Unlimited by default. This bounds a record assembled from many short
604    /// lines, which `max_line_length` alone does not catch.
605    pub fn max_record_length(mut self, bytes: usize) -> Self {
606        self.max_record_length = Some(bytes);
607        self
608    }
609
610    /// Build a reader around any [`Read`].
611    pub fn build<R: Read>(&self, inner: R) -> FastxReader<R> {
612        let mut reader = FastxReader::with_capacity(inner, self.buffer_size);
613        reader.format = self.format;
614        reader.quality_encoding = self.quality_encoding;
615        reader.max_line_length = self.max_line_length;
616        reader.max_record_length = self.max_record_length;
617        reader
618    }
619
620    /// Open a path, transparently decompressing gzip and inferring the format.
621    pub fn open<P: AsRef<Path>>(&self, path: P) -> Result<FastxReader<Box<dyn Read + Send>>> {
622        let path = path.as_ref();
623        let mut builder = self.clone();
624        if builder.format.is_none() {
625            builder.format = Format::from_path(path);
626        }
627        Ok(builder.build(open_reader(path)?))
628    }
629}
630
631/// The boxed reader type produced by [`open`] and [`from_stdin`].
632pub type BoxedReader = FastxReader<Box<dyn Read + Send>>;
633
634/// Open a FASTA/FASTQ file, transparently handling gzip.
635///
636/// The format is taken from the extension when recognisable and otherwise from
637/// the first byte of the (decompressed) stream. gzip is detected from the file's
638/// magic bytes, so a compressed file without a `.gz` suffix works as well.
639///
640/// Requires the `gzip` feature for compressed input.
641pub fn open<P: AsRef<Path>>(path: P) -> Result<BoxedReader> {
642    ReaderBuilder::default().open(path)
643}
644
645/// Read records from standard input (gzip is detected from the magic bytes).
646pub fn from_stdin() -> Result<BoxedReader> {
647    let stream = decompress(Box::new(io::stdin()))?;
648    Ok(FastxReader::new(stream))
649}
650
651/// Wrap a file in a decompressing reader when needed.
652fn open_reader(path: &Path) -> Result<Box<dyn Read + Send>> {
653    let file = File::open(path)
654        .map_err(|e| Error::Io(io::Error::new(e.kind(), format!("{}: {e}", path.display()))))?;
655    decompress(Box::new(BufReader::with_capacity(64 * 1024, file)))
656}
657
658/// Peek at the magic bytes and wrap the stream in a gzip decoder if needed.
659fn decompress(mut stream: Box<dyn Read + Send>) -> Result<Box<dyn Read + Send>> {
660    let mut magic = [0u8; 2];
661    let mut filled = 0;
662    while filled < magic.len() {
663        match stream.read(&mut magic[filled..]) {
664            Ok(0) => break,
665            Ok(n) => filled += n,
666            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
667            Err(e) => return Err(Error::Io(e)),
668        }
669    }
670    let head = io::Cursor::new(magic[..filled].to_vec());
671    let rejoined = head.chain(stream);
672    match Compression::from_magic(&magic[..filled]) {
673        Compression::None => Ok(Box::new(rejoined)),
674        // BGZF is gzip, and reading it sequentially needs no special handling —
675        // MultiGzDecoder walks the members. Only random access cares, and that
676        // goes through `crate::bgzf`.
677        Compression::Gzip | Compression::Bgzf => gunzip(rejoined),
678    }
679}
680
681/// MultiGzDecoder also handles BGZF, which is a series of concatenated members.
682#[cfg(feature = "gzip")]
683fn gunzip<R: Read + Send + 'static>(stream: R) -> Result<Box<dyn Read + Send>> {
684    Ok(Box::new(flate2::read::MultiGzDecoder::new(stream)))
685}
686
687#[cfg(not(feature = "gzip"))]
688fn gunzip<R: Read + Send + 'static>(_stream: R) -> Result<Box<dyn Read + Send>> {
689    Err(Error::FeatureDisabled("gzip"))
690}
691
692#[cfg(test)]
693mod tests {
694    use super::*;
695
696    fn ids(data: &[u8]) -> Vec<String> {
697        FastxReader::new(data).map(|r| r.unwrap().id).collect()
698    }
699
700    #[test]
701    fn reads_simple_fasta() {
702        let data = b">a desc here\nACGT\n>b\nTTTT\nGGGG\n";
703        let records: Vec<_> = FastxReader::new(&data[..])
704            .collect::<Result<Vec<_>>>()
705            .unwrap();
706        assert_eq!(records.len(), 2);
707        assert_eq!(records[0].id, "a");
708        assert_eq!(records[0].description.as_deref(), Some("desc here"));
709        assert_eq!(records[0].seq, b"ACGT");
710        assert_eq!(records[1].seq, b"TTTTGGGG");
711        assert!(records[1].quality.is_none());
712    }
713
714    #[test]
715    fn reads_simple_fastq() {
716        let data = b"@a\nACGT\n+\nIIII\n@b desc\nTT\n+b desc\n!!\n";
717        let records: Vec<_> = FastxReader::new(&data[..])
718            .collect::<Result<Vec<_>>>()
719            .unwrap();
720        assert_eq!(records.len(), 2);
721        assert_eq!(records[0].quality.as_deref(), Some(&b"IIII"[..]));
722        assert_eq!(records[1].id, "b");
723        assert_eq!(records[1].description.as_deref(), Some("desc"));
724        assert_eq!(records[1].quality.as_deref(), Some(&b"!!"[..]));
725    }
726
727    #[test]
728    fn detects_format() {
729        let mut reader = FastxReader::new(&b">a\nAC\n"[..]);
730        assert_eq!(reader.format(), None);
731        reader.next().unwrap().unwrap();
732        assert_eq!(reader.format(), Some(Format::Fasta));
733
734        let mut reader = FastxReader::new(&b"@a\nAC\n+\nII\n"[..]);
735        reader.next().unwrap().unwrap();
736        assert_eq!(reader.format(), Some(Format::Fastq));
737    }
738
739    #[test]
740    fn handles_crlf_and_missing_final_newline() {
741        let data = b">a\r\nACGT\r\nAC\r\n>b\r\nTT";
742        let records: Vec<_> = FastxReader::new(&data[..])
743            .collect::<Result<Vec<_>>>()
744            .unwrap();
745        assert_eq!(records[0].seq, b"ACGTAC");
746        assert_eq!(records[1].seq, b"TT");
747    }
748
749    #[test]
750    fn handles_blank_lines_between_records() {
751        let data = b"\n\n>a\nACGT\n\n\n>b\nTT\n\n";
752        assert_eq!(ids(&data[..]), ["a", "b"]);
753        // The blank line inside record `a` must not become part of the sequence.
754        let records: Vec<_> = FastxReader::new(&data[..])
755            .collect::<Result<Vec<_>>>()
756            .unwrap();
757        assert_eq!(records[0].seq, b"ACGT");
758    }
759
760    #[test]
761    fn handles_empty_input() {
762        assert_eq!(FastxReader::new(&b""[..]).count(), 0);
763        assert_eq!(FastxReader::new(&b"\n\n\n"[..]).count(), 0);
764    }
765
766    #[test]
767    fn multi_line_fastq() {
768        let data = b"@a\nACGT\nACGT\n+\nIIII\nJJJJ\n@b\nTT\n+\n!!\n";
769        let records: Vec<_> = FastxReader::new(&data[..])
770            .collect::<Result<Vec<_>>>()
771            .unwrap();
772        assert_eq!(records[0].seq, b"ACGTACGT");
773        assert_eq!(records[0].quality.as_deref(), Some(&b"IIIIJJJJ"[..]));
774        assert_eq!(records[1].id, "b");
775    }
776
777    #[test]
778    fn quality_starting_with_at_sign() {
779        // '@' is a legal quality character (Q31 in Phred+33).
780        let data = b"@a\nACGT\n+\n@@@@\n@b\nTTTT\n+\nIIII\n";
781        let records: Vec<_> = FastxReader::new(&data[..])
782            .collect::<Result<Vec<_>>>()
783            .unwrap();
784        assert_eq!(records.len(), 2);
785        assert_eq!(records[0].quality.as_deref(), Some(&b"@@@@"[..]));
786        assert_eq!(records[1].id, "b");
787    }
788
789    #[test]
790    fn tiny_buffer_still_parses() {
791        // Force many refills and a line longer than the initial buffer.
792        let long = "A".repeat(50_000);
793        let data = format!(">a\n{long}\n>b\nACGT\n");
794        let mut reader = FastxReader::with_capacity(data.as_bytes(), 1);
795        let records: Vec<_> = reader.records().collect::<Result<Vec<_>>>().unwrap();
796        assert_eq!(records.len(), 2);
797        assert_eq!(records[0].seq.len(), 50_000);
798        assert_eq!(records[1].seq, b"ACGT");
799    }
800
801    #[test]
802    fn read_into_reuses_allocations() {
803        let data = b">a\nACGT\n>b\nTT\n";
804        let mut reader = FastxReader::new(&data[..]);
805        let mut record = Sequence::default();
806        assert!(reader.read_into(&mut record).unwrap());
807        assert_eq!(record.id, "a");
808        assert!(reader.read_into(&mut record).unwrap());
809        assert_eq!(record.id, "b");
810        assert_eq!(record.seq, b"TT");
811        assert!(!reader.read_into(&mut record).unwrap());
812    }
813
814    #[test]
815    fn empty_fasta_record_is_allowed() {
816        let data = b">a\n>b\nACGT\n";
817        let records: Vec<_> = FastxReader::new(&data[..])
818            .collect::<Result<Vec<_>>>()
819            .unwrap();
820        assert_eq!(records[0].seq, b"");
821        assert_eq!(records[1].seq, b"ACGT");
822    }
823
824    #[test]
825    fn rejects_garbage() {
826        let err = FastxReader::new(&b"not a sequence file\n"[..])
827            .next()
828            .unwrap()
829            .unwrap_err();
830        assert!(matches!(
831            err,
832            Error::Parse {
833                kind: ParseError::ExpectedHeader { found: b'n' },
834                ..
835            }
836        ));
837    }
838
839    #[test]
840    fn rejects_truncated_fastq() {
841        let err = FastxReader::new(&b"@a\nACGT\n"[..])
842            .next()
843            .unwrap()
844            .unwrap_err();
845        assert!(matches!(
846            err,
847            Error::Parse {
848                kind: ParseError::UnexpectedEof { .. },
849                ..
850            }
851        ));
852
853        let err = FastxReader::new(&b"@a\nACGT\n+\nII\n"[..])
854            .next()
855            .unwrap()
856            .unwrap_err();
857        assert!(matches!(
858            err,
859            Error::LengthMismatch {
860                seq: 4,
861                quality: 2,
862                ..
863            }
864        ));
865    }
866
867    #[test]
868    fn rejects_empty_id() {
869        let err = FastxReader::new(&b">\nACGT\n"[..])
870            .next()
871            .unwrap()
872            .unwrap_err();
873        assert!(matches!(
874            err,
875            Error::Parse {
876                kind: ParseError::EmptyId,
877                ..
878            }
879        ));
880    }
881
882    #[test]
883    fn reports_line_numbers() {
884        let data = b">a\nACGT\n>b\nACGT\nnope";
885        let mut reader = FastxReader::with_format(&data[..], Format::Fasta);
886        reader.next().unwrap().unwrap();
887        assert_eq!(reader.line_number(), 2);
888    }
889
890    #[test]
891    fn phred64_input_is_normalised_to_phred33() {
892        // 'h' is Q40 in Phred+64, 'B' is Q2.
893        let data = b"@old\nACGT\n+\nhhhB\n";
894
895        let record = ReaderBuilder::new()
896            .quality_encoding(QualityEncoding::Phred64)
897            .build(&data[..])
898            .read_record()
899            .unwrap()
900            .unwrap();
901        assert_eq!(record.quality.as_deref(), Some(&b"III#"[..]));
902        assert_eq!(record.quality_scores().unwrap(), vec![40, 40, 40, 2]);
903
904        // Without the setting the same bytes are read as Phred+33 verbatim.
905        let record = FastxReader::new(&data[..]).read_record().unwrap().unwrap();
906        assert_eq!(record.quality.as_deref(), Some(&b"hhhB"[..]));
907    }
908
909    #[test]
910    fn line_length_limit_is_enforced() {
911        let long = format!(">a\n{}\n", "A".repeat(10_000));
912        let err = ReaderBuilder::new()
913            .max_line_length(1_000)
914            .build(long.as_bytes())
915            .read_record()
916            .unwrap_err();
917        assert!(
918            matches!(
919                err,
920                Error::TooLarge {
921                    what: "line",
922                    limit: 1_000,
923                    ..
924                }
925            ),
926            "{err}"
927        );
928
929        // Under the limit it parses normally.
930        let record = ReaderBuilder::new()
931            .max_line_length(1_000_000)
932            .build(long.as_bytes())
933            .read_record()
934            .unwrap()
935            .unwrap();
936        assert_eq!(record.seq.len(), 10_000);
937    }
938
939    #[test]
940    fn record_length_limit_catches_many_short_lines() {
941        // 200 lines of 50 bases: every line is small, the record is not.
942        let mut data = String::from(">a\n");
943        for _ in 0..200 {
944            data.push_str(&"A".repeat(50));
945            data.push('\n');
946        }
947        let err = ReaderBuilder::new()
948            .max_line_length(1_000)
949            .max_record_length(5_000)
950            .build(data.as_bytes())
951            .read_record()
952            .unwrap_err();
953        assert!(
954            matches!(
955                err,
956                Error::TooLarge {
957                    what: "sequence",
958                    limit: 5_000,
959                    ..
960                }
961            ),
962            "{err}"
963        );
964    }
965
966    #[test]
967    fn limits_are_unlimited_by_default() {
968        // A single line far larger than the buffer must still be accepted.
969        let long = format!(">chrom\n{}\n", "ACGT".repeat(50_000));
970        let record = FastxReader::with_capacity(long.as_bytes(), 4096)
971            .read_record()
972            .unwrap()
973            .unwrap();
974        assert_eq!(record.seq.len(), 200_000);
975    }
976
977    #[test]
978    fn forced_format_reads_fasta_as_written() {
979        let data = b">a\nACGT\n";
980        let mut reader = FastxReader::with_format(&data[..], Format::Fasta);
981        assert_eq!(reader.format(), Some(Format::Fasta));
982        assert_eq!(reader.next().unwrap().unwrap().seq, b"ACGT");
983    }
984}