1use std::io::Read;
2
3use crate::error::{FastqError, Result};
4use crate::fastq_frame;
5
6mod chunk;
7mod frame;
8mod pair;
9mod record;
10
11pub use chunk::{FastqChunkConfig, FastqChunkSinkExt, FastqChunkStats, FastqRecordSink};
12use frame::{ChunkVisitContext, frame_records, visit_chunk_in_slab};
13use frame::{count_records_in_slab, visit_records_in_slab};
14pub use pair::{
15 FastqPair, InterleavedPairs, PairValidation, PairedFastqBatch, PairedFastqPairs,
16 PairedFastqReader, PairedRecords, PairingMode, paired_records,
17};
18use pair::{validate_even_pair_count, validate_interleaved_pair_ids, validate_paired_batches};
19pub use record::{FastqRecord, FastqVisitRecord, RecordRef, strip_pair_suffix};
20
21const DEFAULT_SLAB_SIZE: usize = 1024 * 1024;
22
23#[derive(Debug, Clone)]
28pub struct FastqConfig {
29 pub slab_size: usize,
34 pub validate: bool,
39 pub pairing: PairingMode,
41 pub pair_validation: PairValidation,
43}
44
45impl Default for FastqConfig {
46 fn default() -> Self {
47 Self {
48 slab_size: DEFAULT_SLAB_SIZE,
49 validate: true,
50 pairing: PairingMode::None,
51 pair_validation: PairValidation::Full,
52 }
53 }
54}
55
56impl FastqConfig {
57 pub fn interleaved(mut self) -> Self {
59 self.pairing = PairingMode::Interleaved;
60 self
61 }
62
63 pub fn pair_validation(mut self, pair_validation: PairValidation) -> Self {
65 self.pair_validation = pair_validation;
66 self
67 }
68}
69
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
72pub struct FastqStats {
73 pub records: u64,
75 pub bases: u64,
77 pub qualities: u64,
79 pub name_bytes: u64,
81 pub checksum: u64,
83}
84
85#[derive(Debug)]
90pub struct FastqBatch<'a> {
91 bytes: &'a [u8],
92 records: &'a [RecordRef],
93 base_offset: u64,
94 first_record_index: u64,
95 pair_validation: PairValidation,
96}
97
98impl<'a> FastqBatch<'a> {
99 pub fn len(&self) -> usize {
101 self.records.len()
102 }
103
104 pub fn is_empty(&self) -> bool {
106 self.records.is_empty()
107 }
108
109 pub fn bytes(&self) -> &'a [u8] {
111 self.bytes
112 }
113
114 pub fn record_refs(&self) -> &'a [RecordRef] {
116 self.records
117 }
118
119 pub fn base_offset(&self) -> u64 {
121 self.base_offset
122 }
123
124 pub fn first_record_index(&self) -> u64 {
126 self.first_record_index
127 }
128
129 pub fn pair_validation(&self) -> PairValidation {
131 self.pair_validation
132 }
133
134 pub fn records(&self) -> impl Iterator<Item = FastqRecord<'a>> + 'a {
136 let bytes = self.bytes;
137 let records: &'a [RecordRef] = self.records;
138 records
139 .iter()
140 .map(move |record| FastqRecord { bytes, record })
141 }
142
143 pub fn interleaved_pairs(&'a self) -> Result<InterleavedPairs<'a>> {
148 validate_even_pair_count(self)?;
149 validate_interleaved_pair_ids(self, self.pair_validation)?;
150 Ok(InterleavedPairs {
151 batch: self,
152 next: 0,
153 })
154 }
155
156 pub fn paired_with(&'a self, mate: &'a FastqBatch<'a>) -> Result<PairedRecords<'a>> {
160 validate_paired_batches(self, mate, self.pair_validation)?;
161 Ok(PairedRecords {
162 first: self,
163 second: mate,
164 next: 0,
165 })
166 }
167
168 fn record_at(&self, index: usize) -> FastqRecord<'a> {
169 let records: &'a [RecordRef] = self.records;
170 FastqRecord {
171 bytes: self.bytes,
172 record: &records[index],
173 }
174 }
175}
176
177#[derive(Debug)]
186pub struct FastqReader<R> {
187 reader: R,
188 config: FastqConfig,
189 buf: Vec<u8>,
190 len: usize,
191 next_start: usize,
192 chunk_resume_start: usize,
193 base_offset: u64,
194 record_index: u64,
195 eof: bool,
196 records: Vec<RecordRef>,
197}
198
199impl<R: Read> FastqReader<R> {
200 pub fn new(reader: R) -> Self {
202 Self::with_config(reader, FastqConfig::default())
203 }
204
205 pub fn with_config(reader: R, config: FastqConfig) -> Self {
207 let slab_size = config.slab_size.max(1024);
208 let buf = vec![0; slab_size];
209 Self {
210 reader,
211 config: FastqConfig {
212 slab_size,
213 ..config
214 },
215 buf,
216 len: 0,
217 next_start: 0,
218 chunk_resume_start: 0,
219 base_offset: 0,
220 record_index: 0,
221 eof: false,
222 records: Vec::new(),
223 }
224 }
225
226 pub fn into_inner(self) -> R {
228 self.reader
229 }
230
231 pub fn next_batch(&mut self) -> Result<Option<FastqBatch<'_>>> {
236 self.compact_carry();
237 self.fill_slab()?;
238
239 self.records.clear();
240 let first_record_index = self.record_index;
241 self.next_start = frame_records(
242 &self.buf[..self.len],
243 self.eof,
244 self.config.validate,
245 self.base_offset,
246 first_record_index,
247 &mut self.records,
248 )?;
249 self.align_interleaved_batch(first_record_index)?;
250
251 self.record_index += self.records.len() as u64;
252
253 if self.records.is_empty() {
254 if self.len == 0 && self.eof {
255 return Ok(None);
256 }
257 return Err(FastqError::RecordTooLarge {
258 slab_size: self.config.slab_size,
259 });
260 }
261
262 Ok(Some(FastqBatch {
263 bytes: &self.buf[..self.len],
264 records: &self.records,
265 base_offset: self.base_offset,
266 first_record_index,
267 pair_validation: self.config.pair_validation,
268 }))
269 }
270
271 pub fn visit_records<F>(&mut self, mut visit: F) -> Result<()>
280 where
281 F: FnMut(FastqVisitRecord<'_>) -> Result<()>,
282 {
283 loop {
284 self.compact_carry();
285 self.fill_slab()?;
286
287 let (next_start, records) = visit_records_in_slab(
288 &self.buf[..self.len],
289 self.eof,
290 self.config.validate,
291 self.base_offset,
292 self.record_index,
293 &mut visit,
294 )?;
295 self.next_start = next_start;
296 self.record_index += records;
297
298 if records == 0 {
299 if self.len == 0 && self.eof {
300 return Ok(());
301 }
302 return Err(FastqError::RecordTooLarge {
303 slab_size: self.config.slab_size,
304 });
305 }
306 }
307 }
308
309 #[inline]
316 pub fn count_records(&mut self) -> Result<FastqStats> {
317 let mut stats = FastqStats::default();
318 loop {
319 self.compact_carry();
320 self.fill_slab()?;
321
322 let (next_start, records) = count_records_in_slab(
323 &self.buf[..self.len],
324 self.eof,
325 self.config.validate,
326 self.base_offset,
327 self.record_index,
328 &mut stats,
329 )?;
330 self.next_start = next_start;
331 self.record_index += records;
332
333 if records == 0 {
334 if self.len == 0 && self.eof {
335 return Ok(stats);
336 }
337 return Err(FastqError::RecordTooLarge {
338 slab_size: self.config.slab_size,
339 });
340 }
341 }
342 }
343
344 pub fn next_chunk_with_sink<S>(
355 &mut self,
356 config: FastqChunkConfig,
357 sink: &mut S,
358 ) -> Result<Option<FastqChunkStats>>
359 where
360 S: FastqRecordSink,
361 {
362 let config = config.normalized();
363 let first_record_index = self.record_index;
364 let mut chunk = FastqChunkStats::new(first_record_index);
365
366 loop {
367 let parse_start = if self.chunk_resume_start == 0 {
368 self.compact_carry();
369 self.fill_slab()?;
370 0
371 } else {
372 let parse_start = self.chunk_resume_start;
373 self.chunk_resume_start = 0;
374 parse_start
375 };
376
377 let (next_start, records, bases, stopped) = visit_chunk_in_slab(
378 &self.buf[parse_start..self.len],
379 ChunkVisitContext {
380 eof: self.eof,
381 validate: self.config.validate,
382 base_offset: self.base_offset + parse_start as u64,
383 first_record_index: self.record_index,
384 config,
385 },
386 &chunk,
387 sink,
388 )?;
389 let next_start = parse_start + next_start;
390 if stopped && next_start < self.len {
391 self.chunk_resume_start = next_start;
392 self.next_start = 0;
393 } else {
394 self.next_start = next_start;
395 }
396 self.record_index += records;
397 chunk.records += records;
398 chunk.bases += bases;
399
400 if chunk.records > 0 && (stopped || (self.len == 0 && self.eof)) {
401 return Ok(Some(chunk));
402 }
403 if records == 0 {
404 if self.len == 0 && self.eof {
405 return if chunk.records == 0 {
406 Ok(None)
407 } else {
408 Ok(Some(chunk))
409 };
410 }
411 return Err(FastqError::RecordTooLarge {
412 slab_size: self.config.slab_size,
413 });
414 }
415 }
416 }
417
418 fn compact_carry(&mut self) {
419 if self.chunk_resume_start != 0 {
420 self.next_start = self.chunk_resume_start;
421 self.chunk_resume_start = 0;
422 }
423 if self.next_start == 0 {
424 return;
425 }
426 if self.next_start >= self.len {
427 self.base_offset += self.len as u64;
428 self.len = 0;
429 self.next_start = 0;
430 return;
431 }
432 let carry = self.len - self.next_start;
433 self.buf.copy_within(self.next_start..self.len, 0);
434 self.base_offset += self.next_start as u64;
435 self.len = carry;
436 self.next_start = 0;
437 }
438
439 fn fill_slab(&mut self) -> Result<()> {
440 while !self.eof && self.len < self.config.slab_size {
441 let n = self
442 .reader
443 .read(&mut self.buf[self.len..self.config.slab_size])?;
444 if n == 0 {
445 self.eof = true;
446 break;
447 }
448 self.len += n;
449 }
450 Ok(())
451 }
452
453 fn align_interleaved_batch(&mut self, first_record_index: u64) -> Result<()> {
454 if self.config.pairing != PairingMode::Interleaved || self.records.len().is_multiple_of(2) {
455 return Ok(());
456 }
457
458 let Some(last) = self.records.last() else {
459 return Ok(());
460 };
461 if self.eof {
462 return Err(fastq_frame::format_at(
463 "interleaved FASTQ ended with an unpaired record",
464 self.base_offset,
465 last.name.start as usize,
466 first_record_index + (self.records.len() - 1) as u64,
467 0,
468 ));
469 }
470
471 self.next_start = last.name.start as usize;
472 self.records.pop();
473 Ok(())
474 }
475
476 fn retain_records_from_parts(&mut self, next_start: usize, record_count: usize) {
477 self.chunk_resume_start = 0;
478 self.next_start = next_start;
479 self.record_index -= record_count as u64;
480 }
481}
482
483pub fn visit_fastq_bytes<F>(bytes: &[u8], config: FastqConfig, mut visit: F) -> Result<u64>
493where
494 F: FnMut(FastqVisitRecord<'_>) -> Result<()>,
495{
496 let (_, records) = visit_records_in_slab(bytes, true, config.validate, 0, 0, &mut visit)?;
497
498 Ok(records)
499}
500
501#[inline]
503pub fn count_fastq_read<R: Read>(reader: R) -> Result<FastqStats> {
504 count_fastq_read_with_config(reader, FastqConfig::default())
505}
506
507#[inline]
509pub fn count_fastq_read_with_config<R: Read>(reader: R, config: FastqConfig) -> Result<FastqStats> {
510 let mut reader = FastqReader::with_config(reader, config);
511 reader.count_records()
512}
513
514#[inline]
516pub fn count_fastq_bytes(bytes: &[u8], config: FastqConfig) -> Result<FastqStats> {
517 let mut stats = FastqStats::default();
518 count_records_in_slab(bytes, true, config.validate, 0, 0, &mut stats)?;
519 Ok(stats)
520}
521
522#[cfg(test)]
523mod tests;