1use 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
13pub const DEFAULT_BUFFER_SIZE: usize = 128 * 1024;
16
17const MIN_BUFFER_SIZE: usize = 4 * 1024;
19
20pub struct FastxReader<R: Read> {
60 inner: R,
61 buf: Vec<u8>,
62 pos: usize,
64 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 pub fn new(inner: R) -> FastxReader<R> {
77 FastxReader::with_capacity(inner, DEFAULT_BUFFER_SIZE)
78 }
79
80 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 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 pub fn format(&self) -> Option<Format> {
106 self.format
107 }
108
109 pub fn quality_encoding(&self) -> QualityEncoding {
114 self.quality_encoding
115 }
116
117 pub fn line_number(&self) -> u64 {
119 self.line
120 }
121
122 pub fn into_inner(self) -> R {
124 self.inner
125 }
126
127 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 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 pub fn records(&mut self) -> Records<'_, R> {
169 Records { reader: self }
170 }
171
172 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
495pub 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#[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 pub fn new() -> ReaderBuilder {
549 ReaderBuilder::default()
550 }
551
552 pub fn format(mut self, format: Format) -> Self {
554 self.format = Some(format);
555 self
556 }
557
558 pub fn buffer_size(mut self, bytes: usize) -> Self {
560 self.buffer_size = bytes;
561 self
562 }
563
564 pub fn quality_encoding(mut self, encoding: QualityEncoding) -> Self {
586 self.quality_encoding = encoding;
587 self
588 }
589
590 pub fn max_line_length(mut self, bytes: usize) -> Self {
597 self.max_line_length = Some(bytes);
598 self
599 }
600
601 pub fn max_record_length(mut self, bytes: usize) -> Self {
606 self.max_record_length = Some(bytes);
607 self
608 }
609
610 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 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
631pub type BoxedReader = FastxReader<Box<dyn Read + Send>>;
633
634pub fn open<P: AsRef<Path>>(path: P) -> Result<BoxedReader> {
642 ReaderBuilder::default().open(path)
643}
644
645pub fn from_stdin() -> Result<BoxedReader> {
647 let stream = decompress(Box::new(io::stdin()))?;
648 Ok(FastxReader::new(stream))
649}
650
651fn 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
658fn decompress(mut stream: Box<dyn Read + Send>) -> Result<Box<dyn Read + Send>> {
663 let mut magic = [0u8; 4];
665 let mut filled = 0;
666 while filled < magic.len() {
667 match stream.read(&mut magic[filled..]) {
668 Ok(0) => break,
669 Ok(n) => filled += n,
670 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
671 Err(e) => return Err(Error::Io(e)),
672 }
673 }
674 let head = io::Cursor::new(magic[..filled].to_vec());
675 let rejoined = head.chain(stream);
676 match Compression::from_magic(&magic[..filled]) {
677 Compression::None => Ok(Box::new(rejoined)),
678 Compression::Gzip | Compression::Bgzf => gunzip(rejoined),
682 Compression::Zstd => unzstd(rejoined),
683 }
684}
685
686#[cfg(feature = "gzip")]
688fn gunzip<R: Read + Send + 'static>(stream: R) -> Result<Box<dyn Read + Send>> {
689 Ok(Box::new(flate2::read::MultiGzDecoder::new(stream)))
690}
691
692#[cfg(not(feature = "gzip"))]
693fn gunzip<R: Read + Send + 'static>(_stream: R) -> Result<Box<dyn Read + Send>> {
694 Err(Error::FeatureDisabled("gzip"))
695}
696
697#[cfg(feature = "zstd")]
699fn unzstd<R: Read + Send + 'static>(stream: R) -> Result<Box<dyn Read + Send>> {
700 Ok(Box::new(zstd::stream::read::Decoder::new(stream)?))
701}
702
703#[cfg(not(feature = "zstd"))]
704fn unzstd<R: Read + Send + 'static>(_stream: R) -> Result<Box<dyn Read + Send>> {
705 Err(Error::FeatureDisabled("zstd"))
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711
712 fn ids(data: &[u8]) -> Vec<String> {
713 FastxReader::new(data).map(|r| r.unwrap().id).collect()
714 }
715
716 #[test]
717 fn reads_simple_fasta() {
718 let data = b">a desc here\nACGT\n>b\nTTTT\nGGGG\n";
719 let records: Vec<_> = FastxReader::new(&data[..])
720 .collect::<Result<Vec<_>>>()
721 .unwrap();
722 assert_eq!(records.len(), 2);
723 assert_eq!(records[0].id, "a");
724 assert_eq!(records[0].description.as_deref(), Some("desc here"));
725 assert_eq!(records[0].seq, b"ACGT");
726 assert_eq!(records[1].seq, b"TTTTGGGG");
727 assert!(records[1].quality.is_none());
728 }
729
730 #[test]
731 fn reads_simple_fastq() {
732 let data = b"@a\nACGT\n+\nIIII\n@b desc\nTT\n+b desc\n!!\n";
733 let records: Vec<_> = FastxReader::new(&data[..])
734 .collect::<Result<Vec<_>>>()
735 .unwrap();
736 assert_eq!(records.len(), 2);
737 assert_eq!(records[0].quality.as_deref(), Some(&b"IIII"[..]));
738 assert_eq!(records[1].id, "b");
739 assert_eq!(records[1].description.as_deref(), Some("desc"));
740 assert_eq!(records[1].quality.as_deref(), Some(&b"!!"[..]));
741 }
742
743 #[test]
744 fn detects_format() {
745 let mut reader = FastxReader::new(&b">a\nAC\n"[..]);
746 assert_eq!(reader.format(), None);
747 reader.next().unwrap().unwrap();
748 assert_eq!(reader.format(), Some(Format::Fasta));
749
750 let mut reader = FastxReader::new(&b"@a\nAC\n+\nII\n"[..]);
751 reader.next().unwrap().unwrap();
752 assert_eq!(reader.format(), Some(Format::Fastq));
753 }
754
755 #[test]
756 fn handles_crlf_and_missing_final_newline() {
757 let data = b">a\r\nACGT\r\nAC\r\n>b\r\nTT";
758 let records: Vec<_> = FastxReader::new(&data[..])
759 .collect::<Result<Vec<_>>>()
760 .unwrap();
761 assert_eq!(records[0].seq, b"ACGTAC");
762 assert_eq!(records[1].seq, b"TT");
763 }
764
765 #[test]
766 fn handles_blank_lines_between_records() {
767 let data = b"\n\n>a\nACGT\n\n\n>b\nTT\n\n";
768 assert_eq!(ids(&data[..]), ["a", "b"]);
769 let records: Vec<_> = FastxReader::new(&data[..])
771 .collect::<Result<Vec<_>>>()
772 .unwrap();
773 assert_eq!(records[0].seq, b"ACGT");
774 }
775
776 #[test]
777 fn handles_empty_input() {
778 assert_eq!(FastxReader::new(&b""[..]).count(), 0);
779 assert_eq!(FastxReader::new(&b"\n\n\n"[..]).count(), 0);
780 }
781
782 #[test]
783 fn multi_line_fastq() {
784 let data = b"@a\nACGT\nACGT\n+\nIIII\nJJJJ\n@b\nTT\n+\n!!\n";
785 let records: Vec<_> = FastxReader::new(&data[..])
786 .collect::<Result<Vec<_>>>()
787 .unwrap();
788 assert_eq!(records[0].seq, b"ACGTACGT");
789 assert_eq!(records[0].quality.as_deref(), Some(&b"IIIIJJJJ"[..]));
790 assert_eq!(records[1].id, "b");
791 }
792
793 #[test]
794 fn quality_starting_with_at_sign() {
795 let data = b"@a\nACGT\n+\n@@@@\n@b\nTTTT\n+\nIIII\n";
797 let records: Vec<_> = FastxReader::new(&data[..])
798 .collect::<Result<Vec<_>>>()
799 .unwrap();
800 assert_eq!(records.len(), 2);
801 assert_eq!(records[0].quality.as_deref(), Some(&b"@@@@"[..]));
802 assert_eq!(records[1].id, "b");
803 }
804
805 #[test]
806 fn tiny_buffer_still_parses() {
807 let long = "A".repeat(50_000);
809 let data = format!(">a\n{long}\n>b\nACGT\n");
810 let mut reader = FastxReader::with_capacity(data.as_bytes(), 1);
811 let records: Vec<_> = reader.records().collect::<Result<Vec<_>>>().unwrap();
812 assert_eq!(records.len(), 2);
813 assert_eq!(records[0].seq.len(), 50_000);
814 assert_eq!(records[1].seq, b"ACGT");
815 }
816
817 #[test]
818 fn read_into_reuses_allocations() {
819 let data = b">a\nACGT\n>b\nTT\n";
820 let mut reader = FastxReader::new(&data[..]);
821 let mut record = Sequence::default();
822 assert!(reader.read_into(&mut record).unwrap());
823 assert_eq!(record.id, "a");
824 assert!(reader.read_into(&mut record).unwrap());
825 assert_eq!(record.id, "b");
826 assert_eq!(record.seq, b"TT");
827 assert!(!reader.read_into(&mut record).unwrap());
828 }
829
830 #[test]
831 fn empty_fasta_record_is_allowed() {
832 let data = b">a\n>b\nACGT\n";
833 let records: Vec<_> = FastxReader::new(&data[..])
834 .collect::<Result<Vec<_>>>()
835 .unwrap();
836 assert_eq!(records[0].seq, b"");
837 assert_eq!(records[1].seq, b"ACGT");
838 }
839
840 #[test]
841 fn rejects_garbage() {
842 let err = FastxReader::new(&b"not a sequence file\n"[..])
843 .next()
844 .unwrap()
845 .unwrap_err();
846 assert!(matches!(
847 err,
848 Error::Parse {
849 kind: ParseError::ExpectedHeader { found: b'n' },
850 ..
851 }
852 ));
853 }
854
855 #[test]
856 fn rejects_truncated_fastq() {
857 let err = FastxReader::new(&b"@a\nACGT\n"[..])
858 .next()
859 .unwrap()
860 .unwrap_err();
861 assert!(matches!(
862 err,
863 Error::Parse {
864 kind: ParseError::UnexpectedEof { .. },
865 ..
866 }
867 ));
868
869 let err = FastxReader::new(&b"@a\nACGT\n+\nII\n"[..])
870 .next()
871 .unwrap()
872 .unwrap_err();
873 assert!(matches!(
874 err,
875 Error::LengthMismatch {
876 seq: 4,
877 quality: 2,
878 ..
879 }
880 ));
881 }
882
883 #[test]
884 fn rejects_empty_id() {
885 let err = FastxReader::new(&b">\nACGT\n"[..])
886 .next()
887 .unwrap()
888 .unwrap_err();
889 assert!(matches!(
890 err,
891 Error::Parse {
892 kind: ParseError::EmptyId,
893 ..
894 }
895 ));
896 }
897
898 #[test]
899 fn reports_line_numbers() {
900 let data = b">a\nACGT\n>b\nACGT\nnope";
901 let mut reader = FastxReader::with_format(&data[..], Format::Fasta);
902 reader.next().unwrap().unwrap();
903 assert_eq!(reader.line_number(), 2);
904 }
905
906 #[test]
907 fn phred64_input_is_normalised_to_phred33() {
908 let data = b"@old\nACGT\n+\nhhhB\n";
910
911 let record = ReaderBuilder::new()
912 .quality_encoding(QualityEncoding::Phred64)
913 .build(&data[..])
914 .read_record()
915 .unwrap()
916 .unwrap();
917 assert_eq!(record.quality.as_deref(), Some(&b"III#"[..]));
918 assert_eq!(record.quality_scores().unwrap(), vec![40, 40, 40, 2]);
919
920 let record = FastxReader::new(&data[..]).read_record().unwrap().unwrap();
922 assert_eq!(record.quality.as_deref(), Some(&b"hhhB"[..]));
923 }
924
925 #[test]
926 fn line_length_limit_is_enforced() {
927 let long = format!(">a\n{}\n", "A".repeat(10_000));
928 let err = ReaderBuilder::new()
929 .max_line_length(1_000)
930 .build(long.as_bytes())
931 .read_record()
932 .unwrap_err();
933 assert!(
934 matches!(
935 err,
936 Error::TooLarge {
937 what: "line",
938 limit: 1_000,
939 ..
940 }
941 ),
942 "{err}"
943 );
944
945 let record = ReaderBuilder::new()
947 .max_line_length(1_000_000)
948 .build(long.as_bytes())
949 .read_record()
950 .unwrap()
951 .unwrap();
952 assert_eq!(record.seq.len(), 10_000);
953 }
954
955 #[test]
956 fn record_length_limit_catches_many_short_lines() {
957 let mut data = String::from(">a\n");
959 for _ in 0..200 {
960 data.push_str(&"A".repeat(50));
961 data.push('\n');
962 }
963 let err = ReaderBuilder::new()
964 .max_line_length(1_000)
965 .max_record_length(5_000)
966 .build(data.as_bytes())
967 .read_record()
968 .unwrap_err();
969 assert!(
970 matches!(
971 err,
972 Error::TooLarge {
973 what: "sequence",
974 limit: 5_000,
975 ..
976 }
977 ),
978 "{err}"
979 );
980 }
981
982 #[test]
983 fn limits_are_unlimited_by_default() {
984 let long = format!(">chrom\n{}\n", "ACGT".repeat(50_000));
986 let record = FastxReader::with_capacity(long.as_bytes(), 4096)
987 .read_record()
988 .unwrap()
989 .unwrap();
990 assert_eq!(record.seq.len(), 200_000);
991 }
992
993 #[test]
994 fn forced_format_reads_fasta_as_written() {
995 let data = b">a\nACGT\n";
996 let mut reader = FastxReader::with_format(&data[..], Format::Fasta);
997 assert_eq!(reader.format(), Some(Format::Fasta));
998 assert_eq!(reader.next().unwrap().unwrap().seq, b"ACGT");
999 }
1000}