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::borrowed::SequenceRef;
9use crate::error::{Error, ParseError, Result};
10use crate::format::{Compression, Format};
11use crate::qual::{self, QualityEncoding};
12use crate::record::Sequence;
13
14/// Default read buffer, large enough to hold a full 150 bp read set line and to
15/// keep syscall overhead negligible.
16pub const DEFAULT_BUFFER_SIZE: usize = 128 * 1024;
17
18/// Smallest buffer we will honour; smaller values just make parsing slower.
19const MIN_BUFFER_SIZE: usize = 4 * 1024;
20
21/// A streaming reader for FASTA and FASTQ.
22///
23/// The reader owns a single growable buffer and never holds more than one record
24/// plus the buffer in memory, so a 300 GB FASTQ costs the same as a 300 byte one.
25/// Records with lines longer than the buffer are handled by growing the buffer on
26/// demand, which means a chromosome-on-one-line FASTA works too.
27///
28/// # Examples
29///
30/// ```
31/// use fastx::FastxReader;
32///
33/// let data = b">read1 first\nACGT\nACGT\n>read2\nTTTT\n";
34/// let mut reader = FastxReader::new(&data[..]);
35///
36/// let first = reader.next().unwrap()?;
37/// assert_eq!(first.id, "read1");
38/// assert_eq!(first.description.as_deref(), Some("first"));
39/// assert_eq!(first.seq, b"ACGTACGT"); // multi-line sequences are joined
40///
41/// assert_eq!(reader.count(), 1); // one record left
42/// # Ok::<(), fastx::Error>(())
43/// ```
44///
45/// Reuse one record to parse without allocating:
46///
47/// ```
48/// use fastx::{FastxReader, Sequence};
49///
50/// let data = b"@r1\nACGT\n+\nIIII\n@r2\nTTTT\n+\n!!!!\n";
51/// let mut reader = FastxReader::new(&data[..]);
52/// let mut record = Sequence::default();
53/// let mut bases = 0;
54/// while reader.read_into(&mut record)? {
55///     bases += record.len();
56/// }
57/// assert_eq!(bases, 8);
58/// # Ok::<(), fastx::Error>(())
59/// ```
60pub struct FastxReader<R: Read> {
61    inner: R,
62    buf: Vec<u8>,
63    /// Cursor of the next unconsumed byte in `buf`.
64    pos: usize,
65    /// Number of valid bytes in `buf`.
66    end: usize,
67    eof: bool,
68    format: Option<Format>,
69    line: u64,
70    quality_encoding: QualityEncoding,
71    max_line_length: Option<usize>,
72    max_record_length: Option<usize>,
73    /// The previous record's description buffer, kept so that parsing a header
74    /// does not allocate once per record. See `Sequence::set_header_reusing`.
75    spare_description: String,
76    /// Where `read_ref` joins a multi-line sequence, which cannot be borrowed
77    /// from the buffer because the newlines are in the way. Untouched for
78    /// single-line records, which is the common case.
79    joined_seq: Vec<u8>,
80    /// The same for a multi-line quality string.
81    joined_quality: Vec<u8>,
82    /// Ranges of the last scanned record's parts, in `buf`. Reused between
83    /// records so that `read_ref` allocates nothing per record either.
84    scan_header: (usize, usize),
85    scan_seq: Vec<(usize, usize)>,
86    scan_quality: Vec<(usize, usize)>,
87    /// True when the record spans several lines and so had to be joined.
88    joined: bool,
89}
90
91/// A cursor that walks lines in the buffered region without consuming them.
92///
93/// The scanners need to look ahead across a whole record and then either commit
94/// or ask for more input, so they cannot advance the reader as they go.
95struct Lines {
96    pos: usize,
97    end: usize,
98    eof: bool,
99    /// How many lines the cursor has walked, to advance the line counter on
100    /// commit.
101    consumed: u64,
102}
103
104impl Lines {
105    fn new(pos: usize, end: usize, eof: bool) -> Lines {
106        Lines {
107            pos,
108            end,
109            eof,
110            consumed: 0,
111        }
112    }
113
114    /// True when the cursor has reached the end of a stream that has no more
115    /// data coming.
116    fn exhausted(&self) -> bool {
117        self.pos == self.end && self.eof
118    }
119
120    /// The first byte of the next line, without consuming it.
121    fn peek_first_byte(&self, buf: &[u8]) -> Option<u8> {
122        buf.get(self.pos).copied().filter(|_| self.pos < self.end)
123    }
124
125    /// Consume one line, returning its bounds with any line terminator trimmed.
126    ///
127    /// `None` means the buffer holds no complete line — either more input is
128    /// needed, or the stream has ended.
129    fn next(&mut self, buf: &[u8]) -> Option<(usize, usize)> {
130        match memchr::memchr(b'\n', &buf[self.pos..self.end]) {
131            Some(offset) => {
132                let newline = self.pos + offset;
133                let start = self.pos;
134                let mut stop = newline;
135                if stop > start && buf[stop - 1] == b'\r' {
136                    stop -= 1;
137                }
138                self.pos = newline + 1;
139                self.consumed += 1;
140                Some((start, stop))
141            }
142            // A final line without a trailing newline still counts, but only once
143            // the reader knows nothing more is coming.
144            None if self.eof && self.pos < self.end => {
145                let start = self.pos;
146                let mut stop = self.end;
147                if stop > start && buf[stop - 1] == b'\r' {
148                    stop -= 1;
149                }
150                self.pos = self.end;
151                self.consumed += 1;
152                Some((start, stop))
153            }
154            None => None,
155        }
156    }
157}
158
159/// End offset of `region[start..stop]` with a trailing `\r` removed.
160fn trim_cr(region: &[u8], start: usize, stop: usize) -> usize {
161    if stop > start && region[stop - 1] == b'\r' {
162        stop - 1
163    } else {
164        stop
165    }
166}
167
168/// Concatenate byte ranges of `buf` into `out`, which is cleared first.
169fn join_ranges(ranges: &[(usize, usize)], buf: &[u8], out: &mut Vec<u8>) {
170    out.clear();
171    for &(start, stop) in ranges {
172        out.extend_from_slice(&buf[start..stop]);
173    }
174}
175
176impl<R: Read> FastxReader<R> {
177    /// A reader that determines the format from the first record header.
178    pub fn new(inner: R) -> FastxReader<R> {
179        FastxReader::with_capacity(inner, DEFAULT_BUFFER_SIZE)
180    }
181
182    /// A reader with an explicit format, skipping auto-detection.
183    pub fn with_format(inner: R, format: Format) -> FastxReader<R> {
184        let mut reader = FastxReader::new(inner);
185        reader.format = Some(format);
186        reader
187    }
188
189    /// A reader with a custom buffer size (clamped to at least 4 KiB).
190    pub fn with_capacity(inner: R, capacity: usize) -> FastxReader<R> {
191        FastxReader {
192            inner,
193            buf: vec![0; capacity.max(MIN_BUFFER_SIZE)],
194            pos: 0,
195            end: 0,
196            eof: false,
197            format: None,
198            line: 0,
199            quality_encoding: QualityEncoding::Phred33,
200            max_line_length: None,
201            max_record_length: None,
202            spare_description: String::new(),
203            joined_seq: Vec::new(),
204            joined_quality: Vec::new(),
205            scan_header: (0, 0),
206            scan_seq: Vec::new(),
207            scan_quality: Vec::new(),
208            joined: false,
209        }
210    }
211
212    /// The format, once known. `None` before the first record has been read on
213    /// an auto-detecting reader.
214    pub fn format(&self) -> Option<Format> {
215        self.format
216    }
217
218    /// The quality encoding of the *input*.
219    ///
220    /// Records themselves always come out as Phred+33: anything else is
221    /// normalised while parsing, so downstream code never has to ask.
222    pub fn quality_encoding(&self) -> QualityEncoding {
223        self.quality_encoding
224    }
225
226    /// The 1-based number of the last line consumed; useful for error messages.
227    pub fn line_number(&self) -> u64 {
228        self.line
229    }
230
231    /// Unwrap the underlying reader, discarding any buffered bytes.
232    pub fn into_inner(self) -> R {
233        self.inner
234    }
235
236    /// Parse the next record into `record`, reusing its allocations.
237    ///
238    /// Returns `Ok(false)` at end of input. This is the allocation-free core of
239    /// the reader; [`Iterator::next`] is a thin wrapper over it.
240    pub fn read_into(&mut self, record: &mut Sequence) -> Result<bool> {
241        let format = match self.format {
242            Some(format) => {
243                if !self.skip_blank_lines()? {
244                    return Ok(false);
245                }
246                format
247            }
248            None => match self.detect_format()? {
249                Some(format) => {
250                    self.format = Some(format);
251                    format
252                }
253                None => return Ok(false),
254            },
255        };
256        // Take the previous description's buffer back before `clear` drops it, so
257        // that one allocation serves the whole file instead of one per record.
258        if let Some(mut previous) = record.description.take() {
259            if previous.capacity() > self.spare_description.capacity() {
260                previous.clear();
261                self.spare_description = previous;
262            }
263        }
264        record.clear();
265        match format {
266            Format::Fasta => self.read_fasta_into(record)?,
267            Format::Fastq => self.read_fastq_into(record)?,
268        }
269        Ok(true)
270    }
271
272    /// Parse the next record into a fresh [`Sequence`].
273    pub fn read_record(&mut self) -> Result<Option<Sequence>> {
274        let mut record = Sequence::default();
275        if self.read_into(&mut record)? {
276            Ok(Some(record))
277        } else {
278            Ok(None)
279        }
280    }
281
282    /// Borrowing iterator over the remaining records.
283    ///
284    /// Prefer this over consuming the reader when you still need it afterwards.
285    pub fn records(&mut self) -> Records<'_, R> {
286        Records { reader: self }
287    }
288
289    /// Run `f` on every remaining record, reusing a single buffer.
290    ///
291    /// This is the fastest way to consume a file and the one to reach for in
292    /// pipelines: no per-record allocation, no `Result` per record to unwrap.
293    ///
294    /// ```
295    /// use fastx::FastxReader;
296    ///
297    /// let data = b">a\nACGT\n>b\nGGCC\n";
298    /// let mut total = 0;
299    /// FastxReader::new(&data[..]).for_each_record(|r| { total += r.len(); Ok(()) })?;
300    /// assert_eq!(total, 8);
301    /// # Ok::<(), fastx::Error>(())
302    /// ```
303    pub fn for_each_record<F>(&mut self, mut f: F) -> Result<()>
304    where
305        F: FnMut(&Sequence) -> Result<()>,
306    {
307        let mut record = Sequence::default();
308        while self.read_into(&mut record)? {
309            f(&record)?;
310        }
311        Ok(())
312    }
313
314    /// Parse the next record without copying it, borrowing from the buffer.
315    ///
316    /// The returned record is valid until the next one is read, which is what
317    /// lets this skip the copy that [`FastxReader::read_into`] makes. Nothing is
318    /// copied for FASTQ or for single-line FASTA; a sequence spread over several
319    /// lines has to be joined, so those records are assembled in a scratch buffer
320    /// the reader owns and borrowed from there instead.
321    ///
322    /// Because each record borrows the reader, this cannot be an [`Iterator`] —
323    /// use it in a `while let` loop, or reach for
324    /// [`FastxReader::for_each_ref`].
325    ///
326    /// ```
327    /// use fastx::FastxReader;
328    ///
329    /// let data = b"@r1\nACGT\n+\nIIII\n@r2\nTT\n+\n!!\n";
330    /// let mut reader = FastxReader::new(&data[..]);
331    /// let mut bases = 0;
332    /// while let Some(record) = reader.read_ref()? {
333    ///     bases += record.len();
334    /// }
335    /// assert_eq!(bases, 6);
336    /// # Ok::<(), fastx::Error>(())
337    /// ```
338    pub fn read_ref(&mut self) -> Result<Option<SequenceRef<'_>>> {
339        let format = match self.format {
340            Some(format) => {
341                if !self.skip_blank_lines()? {
342                    return Ok(None);
343                }
344                format
345            }
346            None => match self.detect_format()? {
347                Some(format) => {
348                    self.format = Some(format);
349                    format
350                }
351                None => return Ok(None),
352            },
353        };
354        match format {
355            Format::Fasta => self.read_fasta_ref(),
356            Format::Fastq => self.read_fastq_ref(),
357        }
358        .map(Some)
359    }
360
361    /// Run `f` on every remaining record without copying any of them.
362    ///
363    /// The borrow-free way to use [`FastxReader::read_ref`]: the closure gets
364    /// each record in turn, and none of them outlive the call.
365    ///
366    /// ```
367    /// use fastx::FastxReader;
368    ///
369    /// let data = b">a\nACGT\n>b\nGGCC\n";
370    /// let mut gc = 0;
371    /// FastxReader::new(&data[..]).for_each_ref(|record| {
372    ///     gc += record.base_counts().g + record.base_counts().c;
373    ///     Ok(())
374    /// })?;
375    /// assert_eq!(gc, 6);
376    /// # Ok::<(), fastx::Error>(())
377    /// ```
378    pub fn for_each_ref<F>(&mut self, mut f: F) -> Result<()>
379    where
380        F: FnMut(SequenceRef<'_>) -> Result<()>,
381    {
382        while let Some(record) = self.read_ref()? {
383            f(record)?;
384        }
385        Ok(())
386    }
387
388    /// Count the remaining records without keeping them.
389    pub fn count_records(&mut self) -> Result<u64> {
390        let mut n = 0;
391        let mut record = Sequence::default();
392        while self.read_into(&mut record)? {
393            n += 1;
394        }
395        Ok(n)
396    }
397
398    // ----- parsing ---------------------------------------------------------
399
400    fn read_fasta_into(&mut self, record: &mut Sequence) -> Result<()> {
401        let (start, end) = match self.read_line()? {
402            Some(range) => range,
403            None => {
404                return Err(Error::parse(
405                    self.line,
406                    ParseError::UnexpectedEof {
407                        expected: "a FASTA header",
408                    },
409                ))
410            }
411        };
412        if self.buf[start] != b'>' && self.buf[start] != b';' {
413            return Err(Error::parse(
414                self.line,
415                ParseError::ExpectedHeader {
416                    found: self.buf[start],
417                },
418            ));
419        }
420        record.set_header_reusing(&self.buf[start + 1..end], &mut self.spare_description);
421        if record.id.is_empty() {
422            return Err(Error::parse(self.line, ParseError::EmptyId));
423        }
424        loop {
425            match self.peek_byte()? {
426                None | Some(b'>') => break,
427                _ => {
428                    let (start, end) = self.read_line()?.expect("peeked byte is available");
429                    record.seq.extend_from_slice(&self.buf[start..end]);
430                    self.check_record_limit(record.seq.len(), "sequence")?;
431                }
432            }
433        }
434        Ok(())
435    }
436
437    fn read_fastq_into(&mut self, record: &mut Sequence) -> Result<()> {
438        let (start, end) = match self.read_line()? {
439            Some(range) => range,
440            None => {
441                return Err(Error::parse(
442                    self.line,
443                    ParseError::UnexpectedEof {
444                        expected: "a FASTQ header",
445                    },
446                ))
447            }
448        };
449        if self.buf[start] != b'@' {
450            return Err(Error::parse(
451                self.line,
452                ParseError::ExpectedHeader {
453                    found: self.buf[start],
454                },
455            ));
456        }
457        record.set_header_reusing(&self.buf[start + 1..end], &mut self.spare_description);
458        if record.id.is_empty() {
459            return Err(Error::parse(self.line, ParseError::EmptyId));
460        }
461
462        // Sequence lines, up to the '+' separator. Multi-line FASTQ is rare but
463        // legal, and a '+' can never start a sequence line.
464        loop {
465            match self.peek_byte()? {
466                None => {
467                    return Err(Error::parse(
468                        self.line,
469                        ParseError::UnexpectedEof {
470                            expected: "a FASTQ '+' separator",
471                        },
472                    ))
473                }
474                Some(b'+') => {
475                    self.read_line()?;
476                    break;
477                }
478                _ => {
479                    let (start, end) = self.read_line()?.expect("peeked byte is available");
480                    record.seq.extend_from_slice(&self.buf[start..end]);
481                    self.check_record_limit(record.seq.len(), "sequence")?;
482                }
483            }
484        }
485
486        // Quality lines. Because a quality character may itself be '@', the only
487        // safe terminator is having collected as many scores as bases.
488        let quality = record.quality.get_or_insert_with(Vec::new);
489        while quality.len() < record.seq.len() {
490            match self.read_line()? {
491                Some((start, end)) => quality.extend_from_slice(&self.buf[start..end]),
492                None => {
493                    return Err(Error::LengthMismatch {
494                        id: record.id.clone(),
495                        seq: record.seq.len(),
496                        quality: quality.len(),
497                    })
498                }
499            }
500        }
501        if quality.len() != record.seq.len() {
502            return Err(Error::LengthMismatch {
503                id: record.id.clone(),
504                seq: record.seq.len(),
505                quality: quality.len(),
506            });
507        }
508        // Normalise to Phred+33 so that a `Sequence` has exactly one encoding,
509        // whatever the file used.
510        if self.quality_encoding != QualityEncoding::Phred33 {
511            let from = self.quality_encoding.offset();
512            for c in quality.iter_mut() {
513                *c = qual::encode(qual::score(*c, from), qual::PHRED33);
514            }
515        }
516        Ok(())
517    }
518
519    /// Fail rather than buffer without bound when a line exceeds its limit.
520    fn check_line_limit(&self, length: usize) -> Result<()> {
521        match self.max_line_length {
522            Some(limit) if length > limit => Err(Error::TooLarge {
523                line: self.line + 1,
524                what: "line",
525                limit,
526            }),
527            _ => Ok(()),
528        }
529    }
530
531    /// Fail rather than grow without bound when a record exceeds its limit.
532    fn check_record_limit(&self, length: usize, what: &'static str) -> Result<()> {
533        match self.max_record_length {
534            Some(limit) if length > limit => Err(Error::TooLarge {
535                line: self.line,
536                what,
537                limit,
538            }),
539            _ => Ok(()),
540        }
541    }
542
543    // ----- borrowed parsing --------------------------------------------------
544
545    /// Parse a FASTQ record already known to be buffered, or borrow one.
546    ///
547    /// Every offset is computed after the final refill, because refilling
548    /// compacts the buffer and moves the bytes the offsets refer to.
549    fn read_fastq_ref(&mut self) -> Result<SequenceRef<'_>> {
550        loop {
551            if self.scan_fastq_quick()? || self.scan_fastq()? {
552                break;
553            }
554            if self.eof {
555                return Err(Error::parse(
556                    self.line,
557                    ParseError::UnexpectedEof {
558                        expected: "a complete FASTQ record",
559                    },
560                ));
561            }
562            self.check_record_limit(self.end - self.pos, "record")?;
563            self.refill()?;
564        }
565
566        if self.joined {
567            join_ranges(&self.scan_seq, &self.buf, &mut self.joined_seq);
568            join_ranges(&self.scan_quality, &self.buf, &mut self.joined_quality);
569        }
570        let (seq, quality) = if self.joined {
571            (&self.joined_seq[..], &self.joined_quality[..])
572        } else {
573            let seq = self.scan_seq[0];
574            let quality = self.scan_quality[0];
575            (&self.buf[seq.0..seq.1], &self.buf[quality.0..quality.1])
576        };
577        let (id, description) =
578            crate::record::split_header(&self.buf[self.scan_header.0..self.scan_header.1]);
579        if id.is_empty() {
580            return Err(Error::parse(self.line, ParseError::EmptyId));
581        }
582        Ok(SequenceRef::new(id, description, seq, Some(quality)))
583    }
584
585    /// The FASTA equivalent: a record runs until the next header or end of input.
586    fn read_fasta_ref(&mut self) -> Result<SequenceRef<'_>> {
587        loop {
588            if self.scan_fasta()? {
589                break;
590            }
591            self.check_record_limit(self.end - self.pos, "record")?;
592            self.refill()?;
593        }
594
595        if self.joined {
596            join_ranges(&self.scan_seq, &self.buf, &mut self.joined_seq);
597        }
598        let seq = if self.joined {
599            &self.joined_seq[..]
600        } else {
601            match self.scan_seq.first() {
602                Some(&(start, stop)) => &self.buf[start..stop],
603                None => &[][..],
604            }
605        };
606        let (id, description) =
607            crate::record::split_header(&self.buf[self.scan_header.0..self.scan_header.1]);
608        if id.is_empty() {
609            return Err(Error::parse(self.line, ParseError::EmptyId));
610        }
611        Ok(SequenceRef::new(id, description, seq, None))
612    }
613
614    /// The shape almost every FASTQ record has: four lines, one each.
615    ///
616    /// Worth a path of its own because the general scanner pays for generality it
617    /// does not need here — a `Vec` push and clear per part, a second pass over
618    /// those `Vec`s to check limits and a third to sum lengths. This finds the
619    /// four newlines with a single `memchr_iter`, so one SIMD setup covers the
620    /// whole record instead of four, and keeps the offsets in locals.
621    ///
622    /// Returns `false` when the record is not this shape, or when more input is
623    /// needed; either way the general scanner then runs and decides which it was.
624    fn scan_fastq_quick(&mut self) -> Result<bool> {
625        let region = &self.buf[self.pos..self.end];
626        let mut newlines = memchr::memchr_iter(b'\n', region);
627        let (Some(a), Some(b), Some(c), Some(d)) = (
628            newlines.next(),
629            newlines.next(),
630            newlines.next(),
631            newlines.next(),
632        ) else {
633            return Ok(false);
634        };
635
636        // Bounds within the buffer, with the terminators trimmed.
637        let base = self.pos;
638        let header = (base, trim_cr(region, 0, a) + base);
639        let seq = (base + a + 1, trim_cr(region, a + 1, b) + base);
640        let plus = (base + b + 1, trim_cr(region, b + 1, c) + base);
641        let quality = (base + c + 1, trim_cr(region, c + 1, d) + base);
642
643        // Anything unusual — a missing separator, or a record whose parts run
644        // over several lines — goes to the general scanner rather than being
645        // half-handled here.
646        if self.buf[header.0] != b'@'
647            || self.buf[plus.0] != b'+'
648            || quality.1 - quality.0 != seq.1 - seq.0
649        {
650            return Ok(false);
651        }
652
653        self.check_line_limit(seq.1 - seq.0)?;
654        self.check_line_limit(quality.1 - quality.0)?;
655        self.check_record_limit(seq.1 - seq.0, "sequence")?;
656
657        self.scan_header = (header.0 + 1, header.1);
658        self.scan_seq.clear();
659        self.scan_seq.push(seq);
660        self.scan_quality.clear();
661        self.scan_quality.push(quality);
662        self.joined = false;
663        self.pos = base + d + 1;
664        self.line += 4;
665        Ok(true)
666    }
667
668    /// Look for a complete FASTQ record in the buffered region without consuming
669    /// it. Returns false when more input is needed.
670    ///
671    /// On success `pos` and `line` have advanced past the record and the scan
672    /// fields describe where its parts are.
673    fn scan_fastq(&mut self) -> Result<bool> {
674        let mut lines = Lines::new(self.pos, self.end, self.eof);
675
676        let header = match lines.next(&self.buf) {
677            Some(range) => range,
678            None => return Ok(false),
679        };
680        if self.buf[header.0] != b'@' {
681            return Err(Error::parse(
682                self.line + 1,
683                ParseError::ExpectedHeader {
684                    found: self.buf[header.0],
685                },
686            ));
687        }
688
689        // Sequence lines, up to the '+' separator: '+' can never start one.
690        self.scan_seq.clear();
691        let mut seq_len = 0;
692        loop {
693            match lines.peek_first_byte(&self.buf) {
694                None => return Ok(false),
695                Some(b'+') => {
696                    lines.next(&self.buf);
697                    break;
698                }
699                Some(_) => match lines.next(&self.buf) {
700                    None => return Ok(false),
701                    Some(range) => {
702                        seq_len += range.1 - range.0;
703                        self.scan_seq.push(range);
704                    }
705                },
706            }
707        }
708
709        // Quality lines, until as many scores as bases: a quality character may
710        // itself be '@', so length is the only safe terminator.
711        self.scan_quality.clear();
712        let mut quality_len = 0;
713        while quality_len < seq_len {
714            match lines.next(&self.buf) {
715                None => return Ok(false),
716                Some(range) => {
717                    quality_len += range.1 - range.0;
718                    self.scan_quality.push(range);
719                }
720            }
721        }
722        if quality_len != seq_len {
723            let (id, _) = crate::record::split_header(&self.buf[header.0 + 1..header.1]);
724            return Err(Error::LengthMismatch {
725                id: String::from_utf8_lossy(id).into_owned(),
726                seq: seq_len,
727                quality: quality_len,
728            });
729        }
730
731        self.finish_scan(header, &lines)
732    }
733
734    /// The FASTA equivalent: consume lines until the next header or end of input.
735    fn scan_fasta(&mut self) -> Result<bool> {
736        let mut lines = Lines::new(self.pos, self.end, self.eof);
737
738        let header = match lines.next(&self.buf) {
739            Some(range) => range,
740            None => return Ok(false),
741        };
742        if self.buf[header.0] != b'>' && self.buf[header.0] != b';' {
743            return Err(Error::parse(
744                self.line + 1,
745                ParseError::ExpectedHeader {
746                    found: self.buf[header.0],
747                },
748            ));
749        }
750
751        self.scan_seq.clear();
752        self.scan_quality.clear();
753        loop {
754            match lines.peek_first_byte(&self.buf) {
755                // A header ends the record; end of input does too, but only once
756                // the reader knows there is no more coming.
757                Some(b'>') => break,
758                None if lines.exhausted() => break,
759                None => return Ok(false),
760                Some(_) => match lines.next(&self.buf) {
761                    None => return Ok(false),
762                    Some(range) => self.scan_seq.push(range),
763                },
764            }
765        }
766
767        self.finish_scan(header, &lines)
768    }
769
770    /// Commit a successful scan: record where the parts are and consume the
771    /// input the scanner walked over.
772    fn finish_scan(&mut self, header: (usize, usize), lines: &Lines) -> Result<bool> {
773        for &(start, stop) in self.scan_seq.iter().chain(self.scan_quality.iter()) {
774            self.check_line_limit(stop - start)?;
775        }
776        let length: usize = self.scan_seq.iter().map(|&(a, b)| b - a).sum();
777        self.check_record_limit(length, "sequence")?;
778
779        self.scan_header = (header.0 + 1, header.1);
780        // More than one line for either part means the bytes are not contiguous,
781        // so they have to be joined into a scratch buffer rather than borrowed.
782        self.joined = self.scan_seq.len() > 1 || self.scan_quality.len() > 1;
783        self.pos = lines.pos;
784        self.line += lines.consumed;
785        Ok(true)
786    }
787
788    /// Advance past blank lines. Returns false at end of input.
789    fn skip_blank_lines(&mut self) -> Result<bool> {
790        loop {
791            match self.peek_byte()? {
792                None => return Ok(false),
793                Some(b'\n') => {
794                    self.pos += 1;
795                    self.line += 1;
796                }
797                Some(b'\r') => self.pos += 1,
798                Some(_) => return Ok(true),
799            }
800        }
801    }
802
803    /// Sniff the format from the first meaningful byte without consuming it.
804    fn detect_format(&mut self) -> Result<Option<Format>> {
805        if !self.skip_blank_lines()? {
806            return Ok(None);
807        }
808        let byte = self.buf[self.pos];
809        match Format::from_first_byte(byte) {
810            Some(format) => Ok(Some(format)),
811            None => Err(Error::parse(
812                self.line + 1,
813                ParseError::ExpectedHeader { found: byte },
814            )),
815        }
816    }
817
818    // ----- buffer management ------------------------------------------------
819
820    /// Consume one line, returning its bounds in `self.buf` without the line
821    /// terminator. Returns `None` only at end of input.
822    fn read_line(&mut self) -> Result<Option<(usize, usize)>> {
823        let mut search_from = self.pos;
824        loop {
825            if let Some(offset) = memchr::memchr(b'\n', &self.buf[search_from..self.end]) {
826                let newline = search_from + offset;
827                let start = self.pos;
828                let mut stop = newline;
829                if stop > start && self.buf[stop - 1] == b'\r' {
830                    stop -= 1;
831                }
832                self.check_line_limit(stop - start)?;
833                self.pos = newline + 1;
834                self.line += 1;
835                return Ok(Some((start, stop)));
836            }
837            if self.eof {
838                if self.pos == self.end {
839                    return Ok(None);
840                }
841                // Final line without a trailing newline.
842                let start = self.pos;
843                let mut stop = self.end;
844                if stop > start && self.buf[stop - 1] == b'\r' {
845                    stop -= 1;
846                }
847                self.check_line_limit(stop - start)?;
848                self.pos = self.end;
849                self.line += 1;
850                return Ok(Some((start, stop)));
851            }
852            // No newline yet, so everything buffered belongs to the current line.
853            // Checking here as well is what stops the buffer growing without
854            // bound on input that never supplies a newline at all.
855            self.check_line_limit(self.end - self.pos)?;
856            let previous_end = self.end;
857            let shift = self.refill()?;
858            search_from = previous_end - shift;
859        }
860    }
861
862    /// Ensure at least one byte is buffered and return it without consuming.
863    fn peek_byte(&mut self) -> Result<Option<u8>> {
864        while self.pos == self.end && !self.eof {
865            self.refill()?;
866        }
867        if self.pos == self.end {
868            Ok(None)
869        } else {
870            Ok(Some(self.buf[self.pos]))
871        }
872    }
873
874    /// Move unconsumed bytes to the front, grow if the buffer is full, then read.
875    /// Returns how far indices into `buf` shifted left.
876    fn refill(&mut self) -> Result<usize> {
877        let mut shift = 0;
878        if self.pos > 0 {
879            self.buf.copy_within(self.pos..self.end, 0);
880            shift = self.pos;
881            self.end -= self.pos;
882            self.pos = 0;
883        }
884        if self.end == self.buf.len() {
885            // A single line longer than the buffer: double it.
886            let grown = self.buf.len().saturating_mul(2).max(MIN_BUFFER_SIZE);
887            self.buf.resize(grown, 0);
888        }
889        loop {
890            match self.inner.read(&mut self.buf[self.end..]) {
891                Ok(0) => {
892                    self.eof = true;
893                    break;
894                }
895                Ok(n) => {
896                    self.end += n;
897                    break;
898                }
899                Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
900                Err(e) => return Err(Error::Io(e)),
901            }
902        }
903        Ok(shift)
904    }
905}
906
907impl<R: Read> fmt::Debug for FastxReader<R> {
908    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
909        f.debug_struct("FastxReader")
910            .field("format", &self.format)
911            .field("buffer_size", &self.buf.len())
912            .field("buffered", &(self.end - self.pos))
913            .field("line", &self.line)
914            .field("eof", &self.eof)
915            .finish_non_exhaustive()
916    }
917}
918
919impl<R: Read> Iterator for FastxReader<R> {
920    type Item = Result<Sequence>;
921
922    fn next(&mut self) -> Option<Self::Item> {
923        match self.read_record() {
924            Ok(Some(record)) => Some(Ok(record)),
925            Ok(None) => None,
926            Err(e) => Some(Err(e)),
927        }
928    }
929}
930
931/// Borrowing iterator returned by [`FastxReader::records`].
932pub struct Records<'a, R: Read> {
933    reader: &'a mut FastxReader<R>,
934}
935
936impl<R: Read> Iterator for Records<'_, R> {
937    type Item = Result<Sequence>;
938
939    fn next(&mut self) -> Option<Self::Item> {
940        match self.reader.read_record() {
941            Ok(Some(record)) => Some(Ok(record)),
942            Ok(None) => None,
943            Err(e) => Some(Err(e)),
944        }
945    }
946}
947
948/// Configuration for a [`FastxReader`].
949///
950/// ```
951/// use fastx::{Format, ReaderBuilder};
952///
953/// let data = b">a\nACGT\n";
954/// let mut reader = ReaderBuilder::new()
955///     .format(Format::Fasta)
956///     .buffer_size(64 * 1024)
957///     .build(&data[..]);
958/// assert_eq!(reader.next().unwrap()?.id, "a");
959/// # Ok::<(), fastx::Error>(())
960/// ```
961#[derive(Debug, Clone)]
962pub struct ReaderBuilder {
963    format: Option<Format>,
964    buffer_size: usize,
965    quality_encoding: QualityEncoding,
966    max_line_length: Option<usize>,
967    max_record_length: Option<usize>,
968}
969
970impl Default for ReaderBuilder {
971    fn default() -> Self {
972        ReaderBuilder {
973            format: None,
974            buffer_size: DEFAULT_BUFFER_SIZE,
975            quality_encoding: QualityEncoding::Phred33,
976            max_line_length: None,
977            max_record_length: None,
978        }
979    }
980}
981
982impl ReaderBuilder {
983    /// A builder with default settings.
984    pub fn new() -> ReaderBuilder {
985        ReaderBuilder::default()
986    }
987
988    /// Force a format instead of detecting it.
989    pub fn format(mut self, format: Format) -> Self {
990        self.format = Some(format);
991        self
992    }
993
994    /// Size of the internal read buffer, in bytes.
995    pub fn buffer_size(mut self, bytes: usize) -> Self {
996        self.buffer_size = bytes;
997        self
998    }
999
1000    /// The quality encoding of the input.
1001    ///
1002    /// Old Illumina 1.3–1.7 files are Phred+64. Set this and the reader will
1003    /// convert quality strings to Phred+33 as it parses, so every [`Sequence`]
1004    /// this crate produces uses one encoding.
1005    ///
1006    /// ```
1007    /// use fastx::{qual::QualityEncoding, ReaderBuilder};
1008    ///
1009    /// // 'h' is Q40 in Phred+64.
1010    /// let data = b"@old\nACGT\n+\nhhhh\n";
1011    /// let record = ReaderBuilder::new()
1012    ///     .quality_encoding(QualityEncoding::Phred64)
1013    ///     .build(&data[..])
1014    ///     .read_record()?
1015    ///     .unwrap();
1016    ///
1017    /// assert_eq!(record.quality.as_deref(), Some(&b"IIII"[..])); // now Phred+33
1018    /// assert_eq!(record.mean_quality(), Some(40.0));
1019    /// # Ok::<(), fastx::Error>(())
1020    /// ```
1021    pub fn quality_encoding(mut self, encoding: QualityEncoding) -> Self {
1022        self.quality_encoding = encoding;
1023        self
1024    }
1025
1026    /// Refuse lines longer than `bytes` instead of growing the buffer.
1027    ///
1028    /// Unlimited by default, because a chromosome legitimately arrives on a
1029    /// single line. Set it when reading files you do not control: without a
1030    /// limit, one unterminated line can grow the buffer until the process is
1031    /// killed.
1032    pub fn max_line_length(mut self, bytes: usize) -> Self {
1033        self.max_line_length = Some(bytes);
1034        self
1035    }
1036
1037    /// Refuse records whose sequence exceeds `bytes`.
1038    ///
1039    /// Unlimited by default. This bounds a record assembled from many short
1040    /// lines, which `max_line_length` alone does not catch.
1041    pub fn max_record_length(mut self, bytes: usize) -> Self {
1042        self.max_record_length = Some(bytes);
1043        self
1044    }
1045
1046    /// Build a reader around any [`Read`].
1047    pub fn build<R: Read>(&self, inner: R) -> FastxReader<R> {
1048        let mut reader = FastxReader::with_capacity(inner, self.buffer_size);
1049        reader.format = self.format;
1050        reader.quality_encoding = self.quality_encoding;
1051        reader.max_line_length = self.max_line_length;
1052        reader.max_record_length = self.max_record_length;
1053        reader
1054    }
1055
1056    /// Open a path, transparently decompressing gzip and inferring the format.
1057    pub fn open<P: AsRef<Path>>(&self, path: P) -> Result<FastxReader<Box<dyn Read + Send>>> {
1058        let path = path.as_ref();
1059        let mut builder = self.clone();
1060        if builder.format.is_none() {
1061            builder.format = Format::from_path(path);
1062        }
1063        Ok(builder.build(open_reader(path)?))
1064    }
1065}
1066
1067/// The boxed reader type produced by [`open`] and [`from_stdin`].
1068pub type BoxedReader = FastxReader<Box<dyn Read + Send>>;
1069
1070/// Open a FASTA/FASTQ file, transparently handling gzip.
1071///
1072/// The format is taken from the extension when recognisable and otherwise from
1073/// the first byte of the (decompressed) stream. gzip is detected from the file's
1074/// magic bytes, so a compressed file without a `.gz` suffix works as well.
1075///
1076/// Requires the `gzip` feature for compressed input.
1077pub fn open<P: AsRef<Path>>(path: P) -> Result<BoxedReader> {
1078    ReaderBuilder::default().open(path)
1079}
1080
1081/// Read records from standard input (gzip is detected from the magic bytes).
1082pub fn from_stdin() -> Result<BoxedReader> {
1083    let stream = decompress(Box::new(io::stdin()))?;
1084    Ok(FastxReader::new(stream))
1085}
1086
1087/// Wrap a file in a decompressing reader when needed.
1088fn open_reader(path: &Path) -> Result<Box<dyn Read + Send>> {
1089    let file = File::open(path)
1090        .map_err(|e| Error::Io(io::Error::new(e.kind(), format!("{}: {e}", path.display()))))?;
1091    decompress(Box::new(BufReader::with_capacity(64 * 1024, file)))
1092}
1093
1094/// Peek at the magic bytes and wrap the stream in a decompressor if needed.
1095///
1096/// The bytes read for sniffing are put back in front of the stream, so this
1097/// works on a pipe as well as on a file — nothing is seeked.
1098fn decompress(mut stream: Box<dyn Read + Send>) -> Result<Box<dyn Read + Send>> {
1099    // Enough to cover a whole BGZF block header, so that BGZF can be told from
1100    // plain gzip here and decompressed across cores. zstd needs four bytes,
1101    // gzip two.
1102    let mut magic = [0u8; HEADER_LEN_PROBE];
1103    let mut filled = 0;
1104    while filled < magic.len() {
1105        match stream.read(&mut magic[filled..]) {
1106            Ok(0) => break,
1107            Ok(n) => filled += n,
1108            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
1109            Err(e) => return Err(Error::Io(e)),
1110        }
1111    }
1112    let head = io::Cursor::new(magic[..filled].to_vec());
1113    let rejoined = head.chain(stream);
1114    match Compression::from_magic(&magic[..filled]) {
1115        Compression::None => Ok(Box::new(rejoined)),
1116        // BGZF is gzip, and reading it sequentially needs no special handling —
1117        // MultiGzDecoder walks the members. Only random access cares, and that
1118        // goes through `crate::bgzf`.
1119        Compression::Gzip | Compression::Bgzf => gunzip(rejoined, &magic[..filled]),
1120        Compression::Zstd => unzstd(rejoined),
1121    }
1122}
1123
1124/// Bytes sniffed before deciding how to decompress: a BGZF header is 18 bytes
1125/// with the standard single `BC` extra subfield.
1126const HEADER_LEN_PROBE: usize = 18;
1127
1128/// Decode gzip, using every core when the input is BGZF.
1129///
1130/// BGZF blocks are independent, so a whole-file pass over one can inflate across
1131/// cores. Plain gzip is a single deflate stream and cannot: there, and without
1132/// the `parallel` feature, `MultiGzDecoder` walks the members one at a time.
1133#[cfg(feature = "gzip")]
1134fn gunzip<R: Read + Send + 'static>(stream: R, head: &[u8]) -> Result<Box<dyn Read + Send>> {
1135    #[cfg(feature = "parallel")]
1136    if crate::bgzf::is_bgzf(head) {
1137        return Ok(Box::new(crate::bgzf::ParallelBgzfReader::new(stream)));
1138    }
1139    let _ = head;
1140    Ok(Box::new(flate2::read::MultiGzDecoder::new(stream)))
1141}
1142
1143#[cfg(not(feature = "gzip"))]
1144fn gunzip<R: Read + Send + 'static>(_stream: R, _head: &[u8]) -> Result<Box<dyn Read + Send>> {
1145    Err(Error::FeatureDisabled("gzip"))
1146}
1147
1148/// Decode a Zstandard stream, including one made of several frames.
1149#[cfg(feature = "zstd")]
1150fn unzstd<R: Read + Send + 'static>(stream: R) -> Result<Box<dyn Read + Send>> {
1151    Ok(Box::new(zstd::stream::read::Decoder::new(stream)?))
1152}
1153
1154#[cfg(not(feature = "zstd"))]
1155fn unzstd<R: Read + Send + 'static>(_stream: R) -> Result<Box<dyn Read + Send>> {
1156    Err(Error::FeatureDisabled("zstd"))
1157}
1158
1159#[cfg(test)]
1160mod tests {
1161    use super::*;
1162
1163    fn ids(data: &[u8]) -> Vec<String> {
1164        FastxReader::new(data).map(|r| r.unwrap().id).collect()
1165    }
1166
1167    #[test]
1168    fn reads_simple_fasta() {
1169        let data = b">a desc here\nACGT\n>b\nTTTT\nGGGG\n";
1170        let records: Vec<_> = FastxReader::new(&data[..])
1171            .collect::<Result<Vec<_>>>()
1172            .unwrap();
1173        assert_eq!(records.len(), 2);
1174        assert_eq!(records[0].id, "a");
1175        assert_eq!(records[0].description.as_deref(), Some("desc here"));
1176        assert_eq!(records[0].seq, b"ACGT");
1177        assert_eq!(records[1].seq, b"TTTTGGGG");
1178        assert!(records[1].quality.is_none());
1179    }
1180
1181    #[test]
1182    fn reads_simple_fastq() {
1183        let data = b"@a\nACGT\n+\nIIII\n@b desc\nTT\n+b desc\n!!\n";
1184        let records: Vec<_> = FastxReader::new(&data[..])
1185            .collect::<Result<Vec<_>>>()
1186            .unwrap();
1187        assert_eq!(records.len(), 2);
1188        assert_eq!(records[0].quality.as_deref(), Some(&b"IIII"[..]));
1189        assert_eq!(records[1].id, "b");
1190        assert_eq!(records[1].description.as_deref(), Some("desc"));
1191        assert_eq!(records[1].quality.as_deref(), Some(&b"!!"[..]));
1192    }
1193
1194    #[test]
1195    fn detects_format() {
1196        let mut reader = FastxReader::new(&b">a\nAC\n"[..]);
1197        assert_eq!(reader.format(), None);
1198        reader.next().unwrap().unwrap();
1199        assert_eq!(reader.format(), Some(Format::Fasta));
1200
1201        let mut reader = FastxReader::new(&b"@a\nAC\n+\nII\n"[..]);
1202        reader.next().unwrap().unwrap();
1203        assert_eq!(reader.format(), Some(Format::Fastq));
1204    }
1205
1206    #[test]
1207    fn handles_crlf_and_missing_final_newline() {
1208        let data = b">a\r\nACGT\r\nAC\r\n>b\r\nTT";
1209        let records: Vec<_> = FastxReader::new(&data[..])
1210            .collect::<Result<Vec<_>>>()
1211            .unwrap();
1212        assert_eq!(records[0].seq, b"ACGTAC");
1213        assert_eq!(records[1].seq, b"TT");
1214    }
1215
1216    #[test]
1217    fn handles_blank_lines_between_records() {
1218        let data = b"\n\n>a\nACGT\n\n\n>b\nTT\n\n";
1219        assert_eq!(ids(&data[..]), ["a", "b"]);
1220        // The blank line inside record `a` must not become part of the sequence.
1221        let records: Vec<_> = FastxReader::new(&data[..])
1222            .collect::<Result<Vec<_>>>()
1223            .unwrap();
1224        assert_eq!(records[0].seq, b"ACGT");
1225    }
1226
1227    #[test]
1228    fn handles_empty_input() {
1229        assert_eq!(FastxReader::new(&b""[..]).count(), 0);
1230        assert_eq!(FastxReader::new(&b"\n\n\n"[..]).count(), 0);
1231    }
1232
1233    #[test]
1234    fn multi_line_fastq() {
1235        let data = b"@a\nACGT\nACGT\n+\nIIII\nJJJJ\n@b\nTT\n+\n!!\n";
1236        let records: Vec<_> = FastxReader::new(&data[..])
1237            .collect::<Result<Vec<_>>>()
1238            .unwrap();
1239        assert_eq!(records[0].seq, b"ACGTACGT");
1240        assert_eq!(records[0].quality.as_deref(), Some(&b"IIIIJJJJ"[..]));
1241        assert_eq!(records[1].id, "b");
1242    }
1243
1244    #[test]
1245    fn quality_starting_with_at_sign() {
1246        // '@' is a legal quality character (Q31 in Phred+33).
1247        let data = b"@a\nACGT\n+\n@@@@\n@b\nTTTT\n+\nIIII\n";
1248        let records: Vec<_> = FastxReader::new(&data[..])
1249            .collect::<Result<Vec<_>>>()
1250            .unwrap();
1251        assert_eq!(records.len(), 2);
1252        assert_eq!(records[0].quality.as_deref(), Some(&b"@@@@"[..]));
1253        assert_eq!(records[1].id, "b");
1254    }
1255
1256    #[test]
1257    fn tiny_buffer_still_parses() {
1258        // Force many refills and a line longer than the initial buffer.
1259        let long = "A".repeat(50_000);
1260        let data = format!(">a\n{long}\n>b\nACGT\n");
1261        let mut reader = FastxReader::with_capacity(data.as_bytes(), 1);
1262        let records: Vec<_> = reader.records().collect::<Result<Vec<_>>>().unwrap();
1263        assert_eq!(records.len(), 2);
1264        assert_eq!(records[0].seq.len(), 50_000);
1265        assert_eq!(records[1].seq, b"ACGT");
1266    }
1267
1268    #[test]
1269    fn description_buffer_is_reused_without_leaking_between_records() {
1270        // The reader keeps the previous description's buffer to avoid allocating
1271        // per record. The case that breaks a naive version of that is a record
1272        // with a description followed by one without: the buffer has to be
1273        // reclaimed while the field still reads as `None`.
1274        let data = b"@a first one\nAC\n+\nII\n\
1275                     @b\nGT\n+\nII\n\
1276                     @c third\nTT\n+\nII\n\
1277                     @d\nCC\n+\nII\n\
1278                     @e a much longer description than any before it\nGG\n+\nII\n";
1279        let mut reader = FastxReader::new(&data[..]);
1280        let mut record = Sequence::default();
1281        let mut seen = Vec::new();
1282        while reader.read_into(&mut record).unwrap() {
1283            seen.push((record.id.clone(), record.description.clone()));
1284        }
1285        assert_eq!(
1286            seen,
1287            vec![
1288                ("a".to_string(), Some("first one".to_string())),
1289                ("b".to_string(), None),
1290                ("c".to_string(), Some("third".to_string())),
1291                ("d".to_string(), None),
1292                (
1293                    "e".to_string(),
1294                    Some("a much longer description than any before it".to_string())
1295                ),
1296            ]
1297        );
1298
1299        // Reading into a record the caller already filled must not blend the two.
1300        let mut record = Sequence::fasta("old", b"AAAA").with_description("stale description");
1301        let data = b"@new\nAC\n+\nII\n";
1302        assert!(FastxReader::new(&data[..]).read_into(&mut record).unwrap());
1303        assert_eq!(record.id, "new");
1304        assert_eq!(record.description, None);
1305        assert_eq!(record.seq, b"AC");
1306    }
1307
1308    #[test]
1309    fn read_into_reuses_allocations() {
1310        let data = b">a\nACGT\n>b\nTT\n";
1311        let mut reader = FastxReader::new(&data[..]);
1312        let mut record = Sequence::default();
1313        assert!(reader.read_into(&mut record).unwrap());
1314        assert_eq!(record.id, "a");
1315        assert!(reader.read_into(&mut record).unwrap());
1316        assert_eq!(record.id, "b");
1317        assert_eq!(record.seq, b"TT");
1318        assert!(!reader.read_into(&mut record).unwrap());
1319    }
1320
1321    #[test]
1322    fn empty_fasta_record_is_allowed() {
1323        let data = b">a\n>b\nACGT\n";
1324        let records: Vec<_> = FastxReader::new(&data[..])
1325            .collect::<Result<Vec<_>>>()
1326            .unwrap();
1327        assert_eq!(records[0].seq, b"");
1328        assert_eq!(records[1].seq, b"ACGT");
1329    }
1330
1331    #[test]
1332    fn rejects_garbage() {
1333        let err = FastxReader::new(&b"not a sequence file\n"[..])
1334            .next()
1335            .unwrap()
1336            .unwrap_err();
1337        assert!(matches!(
1338            err,
1339            Error::Parse {
1340                kind: ParseError::ExpectedHeader { found: b'n' },
1341                ..
1342            }
1343        ));
1344    }
1345
1346    #[test]
1347    fn rejects_truncated_fastq() {
1348        let err = FastxReader::new(&b"@a\nACGT\n"[..])
1349            .next()
1350            .unwrap()
1351            .unwrap_err();
1352        assert!(matches!(
1353            err,
1354            Error::Parse {
1355                kind: ParseError::UnexpectedEof { .. },
1356                ..
1357            }
1358        ));
1359
1360        let err = FastxReader::new(&b"@a\nACGT\n+\nII\n"[..])
1361            .next()
1362            .unwrap()
1363            .unwrap_err();
1364        assert!(matches!(
1365            err,
1366            Error::LengthMismatch {
1367                seq: 4,
1368                quality: 2,
1369                ..
1370            }
1371        ));
1372    }
1373
1374    #[test]
1375    fn rejects_empty_id() {
1376        let err = FastxReader::new(&b">\nACGT\n"[..])
1377            .next()
1378            .unwrap()
1379            .unwrap_err();
1380        assert!(matches!(
1381            err,
1382            Error::Parse {
1383                kind: ParseError::EmptyId,
1384                ..
1385            }
1386        ));
1387    }
1388
1389    #[test]
1390    fn reports_line_numbers() {
1391        let data = b">a\nACGT\n>b\nACGT\nnope";
1392        let mut reader = FastxReader::with_format(&data[..], Format::Fasta);
1393        reader.next().unwrap().unwrap();
1394        assert_eq!(reader.line_number(), 2);
1395    }
1396
1397    #[test]
1398    fn phred64_input_is_normalised_to_phred33() {
1399        // 'h' is Q40 in Phred+64, 'B' is Q2.
1400        let data = b"@old\nACGT\n+\nhhhB\n";
1401
1402        let record = ReaderBuilder::new()
1403            .quality_encoding(QualityEncoding::Phred64)
1404            .build(&data[..])
1405            .read_record()
1406            .unwrap()
1407            .unwrap();
1408        assert_eq!(record.quality.as_deref(), Some(&b"III#"[..]));
1409        assert_eq!(record.quality_scores().unwrap(), vec![40, 40, 40, 2]);
1410
1411        // Without the setting the same bytes are read as Phred+33 verbatim.
1412        let record = FastxReader::new(&data[..]).read_record().unwrap().unwrap();
1413        assert_eq!(record.quality.as_deref(), Some(&b"hhhB"[..]));
1414    }
1415
1416    #[test]
1417    fn line_length_limit_is_enforced() {
1418        let long = format!(">a\n{}\n", "A".repeat(10_000));
1419        let err = ReaderBuilder::new()
1420            .max_line_length(1_000)
1421            .build(long.as_bytes())
1422            .read_record()
1423            .unwrap_err();
1424        assert!(
1425            matches!(
1426                err,
1427                Error::TooLarge {
1428                    what: "line",
1429                    limit: 1_000,
1430                    ..
1431                }
1432            ),
1433            "{err}"
1434        );
1435
1436        // Under the limit it parses normally.
1437        let record = ReaderBuilder::new()
1438            .max_line_length(1_000_000)
1439            .build(long.as_bytes())
1440            .read_record()
1441            .unwrap()
1442            .unwrap();
1443        assert_eq!(record.seq.len(), 10_000);
1444    }
1445
1446    #[test]
1447    fn record_length_limit_catches_many_short_lines() {
1448        // 200 lines of 50 bases: every line is small, the record is not.
1449        let mut data = String::from(">a\n");
1450        for _ in 0..200 {
1451            data.push_str(&"A".repeat(50));
1452            data.push('\n');
1453        }
1454        let err = ReaderBuilder::new()
1455            .max_line_length(1_000)
1456            .max_record_length(5_000)
1457            .build(data.as_bytes())
1458            .read_record()
1459            .unwrap_err();
1460        assert!(
1461            matches!(
1462                err,
1463                Error::TooLarge {
1464                    what: "sequence",
1465                    limit: 5_000,
1466                    ..
1467                }
1468            ),
1469            "{err}"
1470        );
1471    }
1472
1473    #[test]
1474    fn limits_are_unlimited_by_default() {
1475        // A single line far larger than the buffer must still be accepted.
1476        let long = format!(">chrom\n{}\n", "ACGT".repeat(50_000));
1477        let record = FastxReader::with_capacity(long.as_bytes(), 4096)
1478            .read_record()
1479            .unwrap()
1480            .unwrap();
1481        assert_eq!(record.seq.len(), 200_000);
1482    }
1483
1484    #[test]
1485    fn forced_format_reads_fasta_as_written() {
1486        let data = b">a\nACGT\n";
1487        let mut reader = FastxReader::with_format(&data[..], Format::Fasta);
1488        assert_eq!(reader.format(), Some(Format::Fasta));
1489        assert_eq!(reader.next().unwrap().unwrap().seq, b"ACGT");
1490    }
1491}