1use std::fs::File;
2#[cfg(all(feature = "gzip", feature = "libdeflate"))]
3use std::io::Cursor;
4use std::io::Read;
5#[cfg(any(feature = "bgzf", feature = "gzip"))]
6use std::io::{Seek, SeekFrom};
7use std::path::Path;
8
9use crate::error::Result;
10use crate::fasta::{FastaConfig, FastaReader};
11use crate::fastq::{FastqConfig, FastqReader, PairedFastqReader};
12#[cfg(feature = "bgzf")]
13use crate::{
14 BgzfAutoReader, BgzfInflateBackend, BgzfParallelConfig, BgzfParallelReader, BgzfReader,
15 bgzf::is_bgzf_header,
16};
17
18#[cfg(all(feature = "gzip", feature = "libdeflate"))]
19const DEFAULT_LIBDEFLATE_GZIP_MAX_COMPRESSED_BYTES: usize = if usize::BITS >= 64 {
20 1024 * 1024 * 1024
21} else {
22 usize::MAX / 2
23};
24#[cfg(all(feature = "gzip", feature = "libdeflate"))]
25const DEFAULT_LIBDEFLATE_GZIP_MAX_DECOMPRESSED_BYTES: usize = if usize::BITS >= 64 {
26 1024 * 1024 * 1024
27} else {
28 usize::MAX / 2
29};
30
31#[cfg(all(feature = "gzip", feature = "libdeflate"))]
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct LibdeflateGzipLimits {
35 pub max_compressed_bytes: usize,
37 pub max_decompressed_bytes: usize,
39}
40
41#[cfg(all(feature = "gzip", feature = "libdeflate"))]
42impl Default for LibdeflateGzipLimits {
43 fn default() -> Self {
44 Self {
45 max_compressed_bytes: DEFAULT_LIBDEFLATE_GZIP_MAX_COMPRESSED_BYTES,
46 max_decompressed_bytes: DEFAULT_LIBDEFLATE_GZIP_MAX_DECOMPRESSED_BYTES,
47 }
48 }
49}
50
51#[cfg(any(feature = "bgzf", feature = "gzip"))]
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53enum InputKind {
54 Raw,
55 #[cfg(feature = "gzip")]
56 Gzip,
57 #[cfg(feature = "bgzf")]
58 Bgzf,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum DetectedInputKind {
64 Raw,
66 #[cfg(feature = "gzip")]
68 Gzip,
69 #[cfg(feature = "bgzf")]
71 Bgzf,
72}
73
74#[cfg(any(feature = "bgzf", feature = "gzip"))]
75impl From<InputKind> for DetectedInputKind {
76 fn from(value: InputKind) -> Self {
77 match value {
78 InputKind::Raw => Self::Raw,
79 #[cfg(feature = "gzip")]
80 InputKind::Gzip => Self::Gzip,
81 #[cfg(feature = "bgzf")]
82 InputKind::Bgzf => Self::Bgzf,
83 }
84 }
85}
86
87pub fn detect_file_input_kind(path: impl AsRef<Path>) -> Result<DetectedInputKind> {
89 #[cfg(any(feature = "bgzf", feature = "gzip"))]
90 {
91 let mut file = File::open(path)?;
92 detect_input_kind(&mut file).map(DetectedInputKind::from)
93 }
94 #[cfg(not(any(feature = "bgzf", feature = "gzip")))]
95 {
96 let _ = path;
97 Ok(DetectedInputKind::Raw)
98 }
99}
100
101pub fn open_fastq(path: impl AsRef<Path>) -> Result<FastqReader<Box<dyn Read + Send>>> {
107 open_fastq_with_config(path, FastqConfig::default())
108}
109
110pub fn open_fastq_with_config(
116 path: impl AsRef<Path>,
117 config: FastqConfig,
118) -> Result<FastqReader<Box<dyn Read + Send>>> {
119 let reader = open_read_by_magic(path)?;
120 Ok(FastqReader::with_config(reader, config))
121}
122
123pub fn open_fasta(path: impl AsRef<Path>) -> Result<FastaReader<Box<dyn Read + Send>>> {
129 open_fasta_with_config(path, FastaConfig::default())
130}
131
132pub fn open_fasta_with_config(
137 path: impl AsRef<Path>,
138 config: FastaConfig,
139) -> Result<FastaReader<Box<dyn Read + Send>>> {
140 let reader = open_read_by_magic(path)?;
141 Ok(FastaReader::with_config(reader, config))
142}
143
144pub fn open_fasta_for_reference(
150 path: impl AsRef<Path>,
151) -> Result<FastaReader<Box<dyn Read + Send>>> {
152 open_fasta_with_config(path, FastaConfig::reference())
153}
154
155pub fn open_paired_fastq(
157 first_path: impl AsRef<Path>,
158 second_path: impl AsRef<Path>,
159) -> Result<PairedFastqReader<Box<dyn Read + Send>, Box<dyn Read + Send>>> {
160 open_paired_fastq_with_config(first_path, second_path, FastqConfig::default())
161}
162
163pub fn open_paired_fastq_with_config(
165 first_path: impl AsRef<Path>,
166 second_path: impl AsRef<Path>,
167 config: FastqConfig,
168) -> Result<PairedFastqReader<Box<dyn Read + Send>, Box<dyn Read + Send>>> {
169 open_paired_fastq_with_configs(first_path, config.clone(), second_path, config)
170}
171
172pub fn open_paired_fastq_with_configs(
174 first_path: impl AsRef<Path>,
175 first_config: FastqConfig,
176 second_path: impl AsRef<Path>,
177 second_config: FastqConfig,
178) -> Result<PairedFastqReader<Box<dyn Read + Send>, Box<dyn Read + Send>>> {
179 let first = open_read_by_magic(first_path)?;
180 let second = open_read_by_magic(second_path)?;
181 Ok(PairedFastqReader::with_configs(
182 first,
183 first_config,
184 second,
185 second_config,
186 ))
187}
188
189fn open_read_by_magic(path: impl AsRef<Path>) -> Result<Box<dyn Read + Send>> {
190 #[cfg(any(feature = "bgzf", feature = "gzip"))]
191 let mut file = File::open(path)?;
192 #[cfg(not(any(feature = "bgzf", feature = "gzip")))]
193 let file = File::open(path)?;
194
195 #[cfg(any(feature = "bgzf", feature = "gzip"))]
196 {
197 let kind = detect_input_kind(&mut file)?;
198 file.seek(SeekFrom::Start(0))?;
199
200 match kind {
201 #[cfg(feature = "bgzf")]
202 InputKind::Bgzf => {
203 let compressed_len = file.metadata()?.len();
204 let config = BgzfParallelConfig::new(default_bgzf_workers());
205 return Ok(Box::new(BgzfAutoReader::with_config(
206 file,
207 compressed_len,
208 config,
209 )?));
210 }
211 #[cfg(feature = "gzip")]
212 InputKind::Gzip => return Ok(Box::new(flate2::read::MultiGzDecoder::new(file))),
213 InputKind::Raw => {}
214 }
215 }
216
217 let reader: Box<dyn Read + Send> = Box::new(file);
218 Ok(reader)
219}
220
221#[cfg(any(feature = "bgzf", feature = "gzip"))]
222fn detect_input_kind(file: &mut File) -> Result<InputKind> {
223 let mut prefix = [0_u8; 18];
224 let n = file.read(&mut prefix)?;
225 #[cfg(feature = "bgzf")]
226 if is_bgzf_header(&prefix[..n]) {
227 return Ok(InputKind::Bgzf);
228 }
229 #[cfg(feature = "gzip")]
230 if n >= 2 && prefix[..2] == [0x1f, 0x8b] {
231 return Ok(InputKind::Gzip);
232 }
233 Ok(InputKind::Raw)
234}
235
236#[cfg(feature = "bgzf")]
237fn default_bgzf_workers() -> usize {
238 std::thread::available_parallelism().map_or(1, usize::from)
239}
240
241#[cfg(all(feature = "gzip", feature = "libdeflate"))]
242pub fn open_fastq_gzip_libdeflate(path: impl AsRef<Path>) -> Result<FastqReader<Cursor<Vec<u8>>>> {
248 open_fastq_gzip_libdeflate_with_config(path, FastqConfig::default())
249}
250
251#[cfg(all(feature = "gzip", feature = "libdeflate"))]
252pub fn open_fastq_gzip_libdeflate_with_config(
255 path: impl AsRef<Path>,
256 config: FastqConfig,
257) -> Result<FastqReader<Cursor<Vec<u8>>>> {
258 open_fastq_gzip_libdeflate_with_limits(path, config, LibdeflateGzipLimits::default())
259}
260
261#[cfg(all(feature = "gzip", feature = "libdeflate"))]
262pub fn open_fastq_gzip_libdeflate_with_limits(
265 path: impl AsRef<Path>,
266 config: FastqConfig,
267 limits: LibdeflateGzipLimits,
268) -> Result<FastqReader<Cursor<Vec<u8>>>> {
269 let compressed = read_limited(path, limits.max_compressed_bytes)?;
270 let decoded = decompress_gzip_libdeflate_buffered(&compressed, limits)?;
271 Ok(FastqReader::with_config(Cursor::new(decoded), config))
272}
273
274#[cfg(all(feature = "gzip", feature = "libdeflate"))]
275pub fn open_fasta_gzip_libdeflate(path: impl AsRef<Path>) -> Result<FastaReader<Cursor<Vec<u8>>>> {
281 open_fasta_gzip_libdeflate_with_config(path, FastaConfig::default())
282}
283
284#[cfg(all(feature = "gzip", feature = "libdeflate"))]
285pub fn open_fasta_gzip_libdeflate_with_config(
288 path: impl AsRef<Path>,
289 config: FastaConfig,
290) -> Result<FastaReader<Cursor<Vec<u8>>>> {
291 open_fasta_gzip_libdeflate_with_limits(path, config, LibdeflateGzipLimits::default())
292}
293
294#[cfg(all(feature = "gzip", feature = "libdeflate"))]
295pub fn open_fasta_gzip_libdeflate_with_limits(
298 path: impl AsRef<Path>,
299 config: FastaConfig,
300 limits: LibdeflateGzipLimits,
301) -> Result<FastaReader<Cursor<Vec<u8>>>> {
302 let compressed = read_limited(path, limits.max_compressed_bytes)?;
303 let decoded = decompress_gzip_libdeflate_buffered(&compressed, limits)?;
304 Ok(FastaReader::with_config(Cursor::new(decoded), config))
305}
306
307#[cfg(all(feature = "gzip", feature = "libdeflate"))]
308fn read_limited(path: impl AsRef<Path>, max_bytes: usize) -> Result<Vec<u8>> {
309 let path = path.as_ref();
310 let len = std::fs::metadata(path)?.len();
311 if len > max_bytes as u64 {
312 return Err(crate::FastqError::Format(format!(
313 "gzip input exceeds libdeflate compressed limit ({len} > {max_bytes} bytes)"
314 )));
315 }
316 let mut compressed = Vec::with_capacity(usize::try_from(len).unwrap_or(0));
317 File::open(path)?.read_to_end(&mut compressed)?;
318 if compressed.len() > max_bytes {
319 return Err(crate::FastqError::Format(format!(
320 "gzip input exceeds libdeflate compressed limit ({} > {max_bytes} bytes)",
321 compressed.len()
322 )));
323 }
324 Ok(compressed)
325}
326
327#[cfg(all(feature = "gzip", feature = "libdeflate"))]
328fn decompress_gzip_libdeflate_buffered(
329 compressed: &[u8],
330 limits: LibdeflateGzipLimits,
331) -> Result<Vec<u8>> {
332 if limits.max_decompressed_bytes == 0 {
333 return Err(crate::FastqError::Format(
334 "libdeflate decompressed limit must be greater than zero".into(),
335 ));
336 }
337 ensure_single_gzip_member(compressed)?;
338 let initial = initial_gzip_output_capacity(compressed).min(limits.max_decompressed_bytes);
339 let mut out = vec![0_u8; initial.max(1)];
340 let mut decompressor = libdeflater::Decompressor::new();
341 loop {
342 match decompressor.gzip_decompress(compressed, &mut out) {
343 Ok(n) => {
344 out.truncate(n);
345 return Ok(out);
346 }
347 Err(libdeflater::DecompressionError::InsufficientSpace) => {
348 let next = out.len().checked_mul(2).ok_or_else(|| {
349 crate::FastqError::Format("gzip output size exceeds usize range".into())
350 })?;
351 if next > limits.max_decompressed_bytes {
352 return Err(crate::FastqError::Format(format!(
353 "gzip output exceeds libdeflate decompressed limit (>{} bytes)",
354 limits.max_decompressed_bytes
355 )));
356 }
357 out.resize(next.max(1), 0);
358 }
359 Err(err) => {
360 return Err(crate::FastqError::Format(format!(
361 "libdeflate gzip inflate failed: {err}"
362 )));
363 }
364 }
365 }
366}
367
368#[cfg(all(feature = "gzip", feature = "libdeflate"))]
369fn ensure_single_gzip_member(compressed: &[u8]) -> Result<()> {
370 let end = gzip_first_member_end(compressed)?;
371 if end != compressed.len() {
372 return Err(crate::FastqError::Format(
373 "libdeflate gzip opener accepts exactly one gzip member; use open_fastq/open_fasta for concatenated gzip streams".into(),
374 ));
375 }
376 Ok(())
377}
378
379#[cfg(all(feature = "gzip", feature = "libdeflate"))]
380fn gzip_first_member_end(compressed: &[u8]) -> Result<usize> {
381 use flate2::{Decompress, FlushDecompress, Status};
382
383 if compressed.len() < 18 || compressed[..2] != [0x1f, 0x8b] || compressed[2] != 8 {
384 return Err(crate::FastqError::Format("invalid gzip header".into()));
385 }
386 let flags = compressed[3];
387 if flags & 0xe0 != 0 {
388 return Err(crate::FastqError::Format(
389 "gzip header uses reserved flags".into(),
390 ));
391 }
392
393 let mut pos = 10;
394 if flags & 0x04 != 0 {
395 let xlen = read_gzip_u16(compressed, pos)? as usize;
396 pos = pos
397 .checked_add(2)
398 .and_then(|p| p.checked_add(xlen))
399 .ok_or_else(|| crate::FastqError::Format("gzip extra field is too large".into()))?;
400 if pos > compressed.len() {
401 return Err(crate::FastqError::Format(
402 "truncated gzip extra field".into(),
403 ));
404 }
405 }
406 if flags & 0x08 != 0 {
407 pos = scan_gzip_cstring(compressed, pos, "name")?;
408 }
409 if flags & 0x10 != 0 {
410 pos = scan_gzip_cstring(compressed, pos, "comment")?;
411 }
412 if flags & 0x02 != 0 {
413 pos = pos
414 .checked_add(2)
415 .ok_or_else(|| crate::FastqError::Format("gzip header CRC is too large".into()))?;
416 if pos > compressed.len() {
417 return Err(crate::FastqError::Format(
418 "truncated gzip header CRC".into(),
419 ));
420 }
421 }
422 if pos + 8 > compressed.len() {
423 return Err(crate::FastqError::Format("truncated gzip member".into()));
424 }
425
426 let deflate = &compressed[pos..];
427 let mut decoder = Decompress::new(false);
428 let mut scratch = [0_u8; 8192];
429 loop {
430 let consumed_before = decoder.total_in();
431 let produced_before = decoder.total_out();
432 let consumed = usize::try_from(consumed_before)
433 .map_err(|_| crate::FastqError::Format("gzip member exceeds usize range".into()))?;
434 let status = decoder
435 .decompress(
436 deflate.get(consumed..).ok_or_else(|| {
437 crate::FastqError::Format("truncated gzip deflate stream".into())
438 })?,
439 &mut scratch,
440 FlushDecompress::None,
441 )
442 .map_err(|err| {
443 crate::FastqError::Format(format!("gzip deflate parse failed: {err}"))
444 })?;
445 if status == Status::StreamEnd {
446 let deflate_len = usize::try_from(decoder.total_in()).map_err(|_| {
447 crate::FastqError::Format("gzip deflate stream exceeds usize range".into())
448 })?;
449 let end = pos
450 .checked_add(deflate_len)
451 .and_then(|p| p.checked_add(8))
452 .ok_or_else(|| crate::FastqError::Format("gzip member is too large".into()))?;
453 if end > compressed.len() {
454 return Err(crate::FastqError::Format("truncated gzip trailer".into()));
455 }
456 return Ok(end);
457 }
458 if decoder.total_in() == consumed_before && decoder.total_out() == produced_before {
459 return Err(crate::FastqError::Format(
460 "gzip deflate parser made no progress".into(),
461 ));
462 }
463 }
464}
465
466#[cfg(all(feature = "gzip", feature = "libdeflate"))]
467fn read_gzip_u16(bytes: &[u8], pos: usize) -> Result<u16> {
468 let Some(pair) = bytes.get(pos..pos + 2) else {
469 return Err(crate::FastqError::Format("truncated gzip header".into()));
470 };
471 Ok(u16::from_le_bytes([pair[0], pair[1]]))
472}
473
474#[cfg(all(feature = "gzip", feature = "libdeflate"))]
475fn scan_gzip_cstring(bytes: &[u8], pos: usize, field: &str) -> Result<usize> {
476 let Some(relative_end) = bytes[pos..].iter().position(|&b| b == 0) else {
477 return Err(crate::FastqError::Format(format!(
478 "unterminated gzip {field} field"
479 )));
480 };
481 Ok(pos + relative_end + 1)
482}
483
484#[cfg(all(feature = "gzip", feature = "libdeflate"))]
485fn initial_gzip_output_capacity(compressed: &[u8]) -> usize {
486 let isize = compressed
487 .len()
488 .checked_sub(4)
489 .and_then(|start| compressed.get(start..))
490 .map(|tail| u32::from_le_bytes([tail[0], tail[1], tail[2], tail[3]]) as usize)
491 .unwrap_or(0);
492 isize.max(compressed.len().saturating_mul(2)).max(1024)
493}
494
495#[cfg(feature = "bgzf")]
496pub fn open_fastq_bgzf_parallel(
498 path: impl AsRef<Path>,
499 workers: usize,
500) -> Result<FastqReader<BgzfParallelReader>> {
501 open_fastq_bgzf_parallel_with_config(path, workers, FastqConfig::default())
502}
503
504#[cfg(feature = "bgzf")]
505pub fn open_fastq_bgzf_parallel_with_config(
508 path: impl AsRef<Path>,
509 workers: usize,
510 config: FastqConfig,
511) -> Result<FastqReader<BgzfParallelReader>> {
512 open_fastq_bgzf_parallel_with_backend(path, workers, BgzfInflateBackend::default(), config)
513}
514
515#[cfg(feature = "bgzf")]
516pub fn open_fastq_bgzf_parallel_with_options(
518 path: impl AsRef<Path>,
519 bgzf_config: BgzfParallelConfig,
520 fastq_config: FastqConfig,
521) -> Result<FastqReader<BgzfParallelReader>> {
522 let file = File::open(path)?;
523 Ok(FastqReader::with_config(
524 BgzfParallelReader::with_config(file, bgzf_config)?,
525 fastq_config,
526 ))
527}
528
529#[cfg(feature = "bgzf")]
530pub fn open_fastq_bgzf_adaptive(
535 path: impl AsRef<Path>,
536 bgzf_config: BgzfParallelConfig,
537 fastq_config: FastqConfig,
538) -> Result<FastqReader<BgzfAutoReader<File>>> {
539 let path = path.as_ref();
540 let compressed_len = std::fs::metadata(path)?.len();
541 let file = File::open(path)?;
542 Ok(FastqReader::with_config(
543 BgzfAutoReader::with_config(file, compressed_len, bgzf_config)?,
544 fastq_config,
545 ))
546}
547
548#[cfg(feature = "bgzf")]
549pub fn open_fastq_bgzf_flate2(path: impl AsRef<Path>) -> Result<FastqReader<BgzfReader<File>>> {
551 open_fastq_bgzf_with_backend(path, BgzfInflateBackend::Flate2, FastqConfig::default())
552}
553
554#[cfg(feature = "bgzf")]
555pub fn open_fastq_bgzf_with_backend(
557 path: impl AsRef<Path>,
558 backend: BgzfInflateBackend,
559 config: FastqConfig,
560) -> Result<FastqReader<BgzfReader<File>>> {
561 let file = File::open(path)?;
562 Ok(FastqReader::with_config(
563 BgzfReader::with_inflate_backend(file, backend),
564 config,
565 ))
566}
567
568#[cfg(feature = "bgzf")]
569pub fn open_fastq_bgzf_parallel_with_backend(
571 path: impl AsRef<Path>,
572 workers: usize,
573 backend: BgzfInflateBackend,
574 config: FastqConfig,
575) -> Result<FastqReader<BgzfParallelReader>> {
576 let file = File::open(path)?;
577 Ok(FastqReader::with_config(
578 BgzfParallelReader::with_inflate_backend(file, workers, backend)?,
579 config,
580 ))
581}
582
583#[cfg(all(test, feature = "gzip"))]
584mod tests {
585 use std::io::Write;
586
587 use super::*;
588
589 #[cfg(feature = "libdeflate")]
590 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
591 struct StreamStats {
592 records: u64,
593 bases: u64,
594 qualities: u64,
595 name_bytes: u64,
596 checksum: u64,
597 }
598
599 #[cfg(feature = "libdeflate")]
600 impl StreamStats {
601 fn observe_record(&mut self, name: &[u8], seq: &[u8], qual: &[u8]) {
602 self.records += 1;
603 self.bases += seq.len() as u64;
604 self.qualities += qual.len() as u64;
605 self.name_bytes += name.len() as u64;
606 self.checksum = self
607 .checksum
608 .wrapping_add(seq.first().copied().unwrap_or_default() as u64)
609 .wrapping_mul(1_099_511_628_211)
610 .wrapping_add(seq.len() as u64);
611 }
612 }
613
614 #[cfg(feature = "libdeflate")]
615 fn consume_fastq<R: Read>(reader: &mut FastqReader<R>) -> Result<StreamStats> {
616 let mut stats = StreamStats::default();
617 while let Some(batch) = reader.next_batch()? {
618 for record in batch.records() {
619 stats.observe_record(record.name(), record.seq(), record.qual());
620 }
621 }
622 Ok(stats)
623 }
624
625 #[cfg(feature = "libdeflate")]
626 fn consume_fasta<R: Read>(reader: &mut FastaReader<R>) -> Result<StreamStats> {
627 let mut stats = StreamStats::default();
628 while let Some(batch) = reader.next_batch()? {
629 for record in batch.records() {
630 stats.observe_record(record.name(), record.seq(), b"");
631 }
632 }
633 Ok(stats)
634 }
635
636 #[cfg(all(feature = "gzip", feature = "libdeflate"))]
637 fn gzip_member(payload: &[u8]) -> Vec<u8> {
638 let mut out = Vec::new();
639 {
640 let mut encoder = flate2::write::GzEncoder::new(&mut out, flate2::Compression::fast());
641 encoder.write_all(payload).unwrap();
642 encoder.finish().unwrap();
643 }
644 out
645 }
646
647 #[test]
648 #[cfg(feature = "gzip")]
649 fn opens_gzip_by_magic() {
650 let dir = std::env::temp_dir();
651 let path = dir.join(format!("dino_seq-{}.fq.gz", std::process::id()));
652 let file = File::create(&path).unwrap();
653 let mut encoder = flate2::write::GzEncoder::new(file, flate2::Compression::fast());
654 encoder.write_all(b"@r1\nACGT\n+\nIIII\n").unwrap();
655 encoder.finish().unwrap();
656
657 let mut reader = open_fastq(&path).unwrap();
658 let batch = reader.next_batch().unwrap().unwrap();
659 let rec = batch.records().next().unwrap();
660 assert_eq!(rec.seq(), b"ACGT");
661
662 std::fs::remove_file(path).unwrap();
663 }
664
665 #[test]
666 #[cfg(feature = "gzip")]
667 fn opens_gzip_fasta_by_magic() {
668 let dir = std::env::temp_dir();
669 let path = dir.join(format!("dino_seq-{}.fa.gz", std::process::id()));
670 let file = File::create(&path).unwrap();
671 let mut encoder = flate2::write::GzEncoder::new(file, flate2::Compression::fast());
672 encoder.write_all(b">seq1\nAC\nGT\n").unwrap();
673 encoder.finish().unwrap();
674
675 let mut reader = open_fasta(&path).unwrap();
676 let batch = reader.next_batch().unwrap().unwrap();
677 let rec = batch.records().next().unwrap();
678 assert_eq!(rec.name_without_gt(), b"seq1");
679 assert_eq!(rec.seq(), b"ACGT");
680
681 std::fs::remove_file(path).unwrap();
682 }
683
684 #[test]
685 #[cfg(feature = "bgzf")]
686 fn opens_bgzf_by_magic() {
687 let dir = std::env::temp_dir();
688 let path = dir.join(format!("dino_seq-{}.bgz", std::process::id()));
689 let mut writer = crate::BgzfWriter::new(File::create(&path).unwrap());
690 writer.write_all(b"@r1\nACGT\n+\nIIII\n").unwrap();
691 writer.finish().unwrap();
692
693 let mut reader = open_fastq(&path).unwrap();
694 let batch = reader.next_batch().unwrap().unwrap();
695 let rec = batch.records().next().unwrap();
696 assert_eq!(rec.seq(), b"ACGT");
697
698 let mut parallel = open_fastq_bgzf_parallel(&path, 2).unwrap();
699 let batch = parallel.next_batch().unwrap().unwrap();
700 let rec = batch.records().next().unwrap();
701 assert_eq!(rec.seq(), b"ACGT");
702
703 std::fs::remove_file(path).unwrap();
704 }
705
706 #[test]
707 #[cfg(feature = "bgzf")]
708 fn opens_bgzf_fasta_by_magic() {
709 let dir = std::env::temp_dir();
710 let path = dir.join(format!("dino_seq-fasta-{}.bgz", std::process::id()));
711 let mut writer = crate::BgzfWriter::new(File::create(&path).unwrap());
712 writer.write_all(b">seq1\nAC\nGT\n").unwrap();
713 writer.finish().unwrap();
714
715 let mut reader = open_fasta(&path).unwrap();
716 let batch = reader.next_batch().unwrap().unwrap();
717 let rec = batch.records().next().unwrap();
718 assert_eq!(rec.name_without_gt(), b"seq1");
719 assert_eq!(rec.seq(), b"ACGT");
720
721 std::fs::remove_file(path).unwrap();
722 }
723
724 #[test]
725 #[cfg(feature = "bgzf")]
726 fn public_bgzf_adaptive_opener_selects_serial_and_parallel() {
727 let dir = std::env::temp_dir();
728 let small_path = dir.join(format!("dino_seq-bgzf-small-{}.bgz", std::process::id()));
729 let large_path = dir.join(format!("dino_seq-bgzf-large-{}.bgz", std::process::id()));
730
731 let mut small_writer = crate::BgzfWriter::new(File::create(&small_path).unwrap());
732 small_writer.write_all(b"@r1\nACGT\n+\nIIII\n").unwrap();
733 small_writer.finish().unwrap();
734
735 let mut large_writer = crate::BgzfWriter::new(File::create(&large_path).unwrap());
736 for i in 0..2048 {
737 writeln!(large_writer, "@r{i}\nACGTACGTACGTACGT\n+\nIIIIIIIIIIIIIIII").unwrap();
738 }
739 large_writer.finish().unwrap();
740
741 let serial = open_fastq_bgzf_adaptive(
742 &small_path,
743 BgzfParallelConfig::new(2).with_parallel_min_compressed_bytes(u64::MAX),
744 FastqConfig::default(),
745 )
746 .unwrap()
747 .into_inner();
748 assert!(matches!(serial, crate::BgzfAutoReader::Serial(_)));
749
750 let parallel = open_fastq_bgzf_adaptive(
751 &large_path,
752 BgzfParallelConfig::new(2).with_parallel_min_compressed_bytes(0),
753 FastqConfig::default(),
754 )
755 .unwrap()
756 .into_inner();
757 assert!(matches!(parallel, crate::BgzfAutoReader::Parallel(_)));
758
759 std::fs::remove_file(small_path).unwrap();
760 std::fs::remove_file(large_path).unwrap();
761 }
762
763 #[test]
764 #[cfg(all(feature = "bgzf", feature = "libdeflate"))]
765 fn explicit_bgzf_backend_openers_parse_same_file() {
766 let dir = std::env::temp_dir();
767 let path = dir.join(format!("dino_seq-backend-{}.bgz", std::process::id()));
768 let mut writer = crate::BgzfWriter::new(File::create(&path).unwrap());
769 writer
770 .write_all(b"@r1\nACGT\n+\nIIII\n@r2\nTGCA\n+\nJJJJ\n")
771 .unwrap();
772 writer.finish().unwrap();
773
774 let mut auto = open_fastq(&path).unwrap();
775 let mut flate2 = open_fastq_bgzf_flate2(&path).unwrap();
776 let mut libdeflate = open_fastq_bgzf_with_backend(
777 &path,
778 BgzfInflateBackend::Libdeflate,
779 FastqConfig::default(),
780 )
781 .unwrap();
782
783 let auto_stats = consume_fastq(&mut auto).unwrap();
784 let flate2_stats = consume_fastq(&mut flate2).unwrap();
785 let libdeflate_stats = consume_fastq(&mut libdeflate).unwrap();
786
787 assert_eq!(auto_stats, flate2_stats);
788 assert_eq!(auto_stats, libdeflate_stats);
789
790 std::fs::remove_file(path).unwrap();
791 }
792
793 #[test]
794 #[cfg(all(feature = "gzip", feature = "libdeflate"))]
795 fn explicit_libdeflate_gzip_opener_parses_same_file() {
796 let dir = std::env::temp_dir();
797 let path = dir.join(format!(
798 "dino_seq-libdeflate-gzip-{}.fq.gz",
799 std::process::id()
800 ));
801 let file = File::create(&path).unwrap();
802 let mut encoder = flate2::write::GzEncoder::new(file, flate2::Compression::fast());
803 encoder
804 .write_all(b"@r1\nACGT\n+\nIIII\n@r2\nTGCA\n+\nJJJJ\n")
805 .unwrap();
806 encoder.finish().unwrap();
807
808 let mut flate2 = open_fastq(&path).unwrap();
809 let mut libdeflate = open_fastq_gzip_libdeflate(&path).unwrap();
810
811 let flate2_stats = consume_fastq(&mut flate2).unwrap();
812 let libdeflate_stats = consume_fastq(&mut libdeflate).unwrap();
813 assert_eq!(flate2_stats, libdeflate_stats);
814
815 std::fs::remove_file(path).unwrap();
816 }
817
818 #[test]
819 #[cfg(all(feature = "gzip", feature = "libdeflate"))]
820 fn explicit_libdeflate_gzip_rejects_concatenated_fastq_members() {
821 let dir = std::env::temp_dir();
822 let path = dir.join(format!(
823 "dino_seq-libdeflate-gzip-multi-{}.fq.gz",
824 std::process::id()
825 ));
826 let mut encoded = gzip_member(b"@r1\nACGT\n+\nIIII\n");
827 encoded.extend_from_slice(&gzip_member(b"@r2\nTGCA\n+\nJJJJ\n"));
828 std::fs::write(&path, encoded).unwrap();
829
830 let mut default_reader = open_fastq(&path).unwrap();
831 let default_stats = consume_fastq(&mut default_reader).unwrap();
832 assert_eq!(default_stats.records, 2);
833
834 let err = match open_fastq_gzip_libdeflate(&path) {
835 Ok(_) => panic!("concatenated gzip member was accepted"),
836 Err(err) => err,
837 };
838 assert!(err.to_string().contains("one gzip member"));
839
840 std::fs::remove_file(path).unwrap();
841 }
842
843 #[test]
844 #[cfg(all(feature = "gzip", feature = "libdeflate"))]
845 fn explicit_libdeflate_gzip_limits_compressed_input() {
846 let dir = std::env::temp_dir();
847 let path = dir.join(format!(
848 "dino_seq-libdeflate-gzip-limit-{}.fq.gz",
849 std::process::id()
850 ));
851 std::fs::write(&path, gzip_member(b"@r1\nACGT\n+\nIIII\n")).unwrap();
852
853 let err = match open_fastq_gzip_libdeflate_with_limits(
854 &path,
855 FastqConfig::default(),
856 LibdeflateGzipLimits {
857 max_compressed_bytes: 1,
858 max_decompressed_bytes: 1024,
859 },
860 ) {
861 Ok(_) => panic!("compressed limit was not enforced"),
862 Err(err) => err,
863 };
864 assert!(err.to_string().contains("compressed limit"));
865
866 std::fs::remove_file(path).unwrap();
867 }
868
869 #[test]
870 #[cfg(all(feature = "gzip", feature = "libdeflate"))]
871 fn explicit_libdeflate_gzip_limits_decompressed_output() {
872 let dir = std::env::temp_dir();
873 let path = dir.join(format!(
874 "dino_seq-libdeflate-gzip-output-limit-{}.fq.gz",
875 std::process::id()
876 ));
877 std::fs::write(&path, gzip_member(b"@r1\nACGT\n+\nIIII\n")).unwrap();
878
879 let err = match open_fastq_gzip_libdeflate_with_limits(
880 &path,
881 FastqConfig::default(),
882 LibdeflateGzipLimits {
883 max_compressed_bytes: 1024,
884 max_decompressed_bytes: 4,
885 },
886 ) {
887 Ok(_) => panic!("decompressed limit was not enforced"),
888 Err(err) => err,
889 };
890 assert!(err.to_string().contains("decompressed limit"));
891
892 std::fs::remove_file(path).unwrap();
893 }
894
895 #[test]
896 #[cfg(all(feature = "gzip", feature = "libdeflate"))]
897 fn explicit_libdeflate_gzip_fasta_opener_parses_same_file() {
898 let dir = std::env::temp_dir();
899 let path = dir.join(format!(
900 "dino_seq-libdeflate-gzip-fasta-{}.fa.gz",
901 std::process::id()
902 ));
903 let file = File::create(&path).unwrap();
904 let mut encoder = flate2::write::GzEncoder::new(file, flate2::Compression::fast());
905 encoder.write_all(b">seq1\nAC\nGT\n").unwrap();
906 encoder.finish().unwrap();
907
908 let mut flate2 = open_fasta(&path).unwrap();
909 let mut libdeflate = open_fasta_gzip_libdeflate(&path).unwrap();
910
911 let flate2_stats = consume_fasta(&mut flate2).unwrap();
912 let libdeflate_stats = consume_fasta(&mut libdeflate).unwrap();
913 assert_eq!(flate2_stats, libdeflate_stats);
914
915 std::fs::remove_file(path).unwrap();
916 }
917
918 #[test]
919 #[cfg(all(feature = "gzip", feature = "libdeflate"))]
920 fn explicit_libdeflate_gzip_rejects_concatenated_fasta_members() {
921 let dir = std::env::temp_dir();
922 let path = dir.join(format!(
923 "dino_seq-libdeflate-gzip-fasta-multi-{}.fa.gz",
924 std::process::id()
925 ));
926 let mut encoded = gzip_member(b">seq1\nAC\n");
927 encoded.extend_from_slice(&gzip_member(b">seq2\nGT\n"));
928 std::fs::write(&path, encoded).unwrap();
929
930 let mut default_reader = open_fasta(&path).unwrap();
931 let default_stats = consume_fasta(&mut default_reader).unwrap();
932 assert_eq!(default_stats.records, 2);
933
934 let err = match open_fasta_gzip_libdeflate(&path) {
935 Ok(_) => panic!("concatenated gzip member was accepted"),
936 Err(err) => err,
937 };
938 assert!(err.to_string().contains("one gzip member"));
939
940 std::fs::remove_file(path).unwrap();
941 }
942
943 #[test]
944 fn paired_openers_parse_matching_files() {
945 let dir = std::env::temp_dir();
946 let r1_path = dir.join(format!("dino_seq-r1-{}.fq", std::process::id()));
947 let r2_path = dir.join(format!("dino_seq-r2-{}.fq", std::process::id()));
948 std::fs::write(&r1_path, b"@frag/1\nACGT\n+\nIIII\n").unwrap();
949 std::fs::write(&r2_path, b"@frag/2\nTGCA\n+\nJJJJ\n").unwrap();
950
951 let mut reader = open_paired_fastq(&r1_path, &r2_path).unwrap();
952 let batch = reader.next_pair_batch().unwrap().unwrap();
953 let pair = batch.pairs().next().unwrap();
954 assert_eq!(pair.pair_id(), b"frag");
955 assert_eq!(pair.first().seq(), b"ACGT");
956 assert_eq!(pair.second().seq(), b"TGCA");
957
958 std::fs::remove_file(r1_path).unwrap();
959 std::fs::remove_file(r2_path).unwrap();
960 }
961
962 #[test]
963 fn open_fasta_parses_raw_file() {
964 let dir = std::env::temp_dir();
965 let path = dir.join(format!("dino_seq-{}.fa", std::process::id()));
966 std::fs::write(&path, b">seq1\nAC\nGT\n").unwrap();
967
968 let mut reader = open_fasta(&path).unwrap();
969 let batch = reader.next_batch().unwrap().unwrap();
970 let rec = batch.records().next().unwrap();
971 assert_eq!(rec.id_token(), b"seq1");
972 assert_eq!(rec.seq(), b"ACGT");
973
974 std::fs::remove_file(path).unwrap();
975 }
976}