1use std::collections::{HashMap, HashSet};
2use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
3use std::ops::Range;
4
5use crate::error::{FastqError, FastqPosition, Result};
6#[cfg(feature = "bgzf")]
7use crate::{BgzfDecodedBlockReader, BgzfIndex, BgzfIndexEntry, BgzfSeekReader, BgzfVirtualOffset};
8use memchr::memchr;
9
10const DEFAULT_BATCH_RECORDS: usize = 1024;
11const DEFAULT_READER_BUFFER_SIZE: usize = 64 * 1024;
12const REFERENCE_BATCH_RECORDS: usize = 16;
13const REFERENCE_READER_BUFFER_SIZE: usize = 256 * 1024;
14const REFERENCE_EXPECTED_SEQ_LEN: usize = 1024 * 1024;
15const TWO_LINE_STREAM_BUFFER_SIZE: usize = 64 * 1024;
16const INDEXED_FETCH_SCRATCH_SIZE: usize = 64 * 1024;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19struct ByteRange {
20 start: u32,
21 end: u32,
22}
23
24impl ByteRange {
25 fn to_usize(self) -> Range<usize> {
26 self.start as usize..self.end as usize
27 }
28}
29
30#[derive(Debug, Clone)]
32pub struct FastaConfig {
33 pub batch_records: usize,
38 pub buffer_size: usize,
44 pub expected_seq_len: usize,
49}
50
51impl Default for FastaConfig {
52 fn default() -> Self {
53 Self {
54 batch_records: DEFAULT_BATCH_RECORDS,
55 buffer_size: DEFAULT_READER_BUFFER_SIZE,
56 expected_seq_len: 0,
57 }
58 }
59}
60
61impl FastaConfig {
62 pub fn reference() -> Self {
64 Self {
65 batch_records: REFERENCE_BATCH_RECORDS,
66 buffer_size: REFERENCE_READER_BUFFER_SIZE,
67 expected_seq_len: REFERENCE_EXPECTED_SEQ_LEN,
68 }
69 }
70
71 pub fn two_line(mut self) -> Self {
73 self.buffer_size = TWO_LINE_STREAM_BUFFER_SIZE;
74 self
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct FastaRecordRef {
81 pub name: Range<u32>,
83 pub seq: Range<u32>,
85}
86
87#[derive(Debug, Clone, Copy)]
89pub struct FastaRecord<'a> {
90 bytes: &'a [u8],
91 record: &'a FastaRecordRef,
92 id_token: ByteRange,
93}
94
95impl<'a> FastaRecord<'a> {
96 pub fn name(self) -> &'a [u8] {
98 &self.bytes[to_usize(self.record.name.clone())]
99 }
100
101 pub fn name_without_gt(self) -> &'a [u8] {
103 let name = self.name();
104 name.strip_prefix(b">").unwrap_or(name)
105 }
106
107 pub fn id_token(self) -> &'a [u8] {
109 &self.bytes[self.id_token.to_usize()]
110 }
111
112 pub fn seq(self) -> &'a [u8] {
114 &self.bytes[to_usize(self.record.seq.clone())]
115 }
116}
117
118#[derive(Debug, Clone, Copy)]
120pub struct FastaVisitRecord<'a> {
121 name: &'a [u8],
122 seq: &'a [u8],
123}
124
125impl<'a> FastaVisitRecord<'a> {
126 pub fn name(self) -> &'a [u8] {
128 self.name
129 }
130
131 pub fn name_without_gt(self) -> &'a [u8] {
133 self.name.strip_prefix(b">").unwrap_or(self.name)
134 }
135
136 pub fn id_token(self) -> &'a [u8] {
138 let name = self.name_without_gt();
139 let end = name
140 .iter()
141 .position(u8::is_ascii_whitespace)
142 .unwrap_or(name.len());
143 &name[..end]
144 }
145
146 pub fn seq(self) -> &'a [u8] {
148 self.seq
149 }
150}
151
152const FASTA_STATS_CHECKSUM_INIT: u64 = 0xcbf2_9ce4_8422_2325;
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub struct FastaStats {
157 pub records: u64,
159 pub bases: u64,
161 pub checksum: u64,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum FastaShape {
168 Empty,
170 TwoLine,
172 Multiline,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct FastaIndexEntry {
184 pub name: Vec<u8>,
186 pub len: u64,
188 pub offset: u64,
190 pub line_bases: u64,
192 pub line_width: u64,
194 #[cfg(feature = "bgzf")]
196 pub virtual_offset: Option<BgzfVirtualOffset>,
197}
198
199impl FastaIndexEntry {
200 pub fn sequence_offset(&self, pos: u64) -> Result<u64> {
205 if pos > self.len {
206 return Err(FastqError::Format(
207 "FASTA sequence position exceeds reference length".into(),
208 ));
209 }
210 if pos == self.len {
211 return self.sequence_end_offset();
212 }
213 if self.line_bases == 0 {
214 return Err(FastqError::Format(
215 "FASTA index entry has zero line_bases for non-empty sequence".into(),
216 ));
217 }
218 let line = pos / self.line_bases;
219 let in_line = pos % self.line_bases;
220 let line_offset = line.checked_mul(self.line_width).ok_or_else(|| {
221 FastqError::Format("FASTA index offset calculation overflowed".into())
222 })?;
223 self.offset
224 .checked_add(line_offset)
225 .and_then(|offset| offset.checked_add(in_line))
226 .ok_or_else(|| FastqError::Format("FASTA index offset calculation overflowed".into()))
227 }
228
229 pub fn sequence_spans(&self, range: Range<u64>) -> Result<Vec<Range<u64>>> {
235 self.validate_range(range.clone())?;
236 let mut spans = Vec::new();
237 let mut pos = range.start;
238 while pos < range.end {
239 if self.line_bases == 0 {
240 return Err(FastqError::Format(
241 "FASTA index entry has zero line_bases for non-empty range".into(),
242 ));
243 }
244 let in_line = pos % self.line_bases;
245 let take = (range.end - pos).min(self.line_bases - in_line);
246 let start = self.sequence_offset(pos)?;
247 let end = start.checked_add(take).ok_or_else(|| {
248 FastqError::Format("FASTA index span calculation overflowed".into())
249 })?;
250 spans.push(start..end);
251 pos += take;
252 }
253 Ok(spans)
254 }
255
256 fn sequence_end_offset(&self) -> Result<u64> {
257 if self.len == 0 {
258 return Ok(self.offset);
259 }
260 self.sequence_offset(self.len - 1).map(|offset| offset + 1)
261 }
262
263 fn validate_range(&self, range: Range<u64>) -> Result<()> {
264 if range.start > range.end {
265 return Err(FastqError::Format(
266 "FASTA range start must be <= end".into(),
267 ));
268 }
269 if range.end > self.len {
270 return Err(FastqError::Format(
271 "FASTA range end exceeds reference length".into(),
272 ));
273 }
274 Ok(())
275 }
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub struct FastaPartitionConfig {
281 pub target_partitions: usize,
285 pub overlap_bases: u64,
291}
292
293impl FastaPartitionConfig {
294 pub fn new(target_partitions: usize, overlap_bases: u64) -> Self {
296 Self {
297 target_partitions,
298 overlap_bases,
299 }
300 }
301}
302
303#[derive(Debug, Clone, PartialEq, Eq)]
305pub struct FastaPartition {
306 pub partition_index: usize,
308 pub name: Vec<u8>,
310 pub core: Range<u64>,
312 pub fetch: Range<u64>,
315}
316
317impl FastaPartition {
318 pub fn core_len(&self) -> u64 {
320 self.core.end - self.core.start
321 }
322
323 pub fn core_offset_in_fetch(&self) -> u64 {
326 self.core.start - self.fetch.start
327 }
328}
329
330pub fn plan_fasta_partitions(
338 index: &FastaIndex,
339 config: FastaPartitionConfig,
340) -> Result<Vec<FastaPartition>> {
341 let target_partitions = config.target_partitions.max(1);
342 let total_bases = index.entries().iter().try_fold(0_u64, |acc, entry| {
343 acc.checked_add(entry.len)
344 .ok_or_else(|| FastqError::Format("FASTA partition total length overflowed".into()))
345 })?;
346 if total_bases == 0 {
347 return Ok(Vec::new());
348 }
349 let target_partitions_u64 = u64::try_from(target_partitions)
350 .map_err(|_| FastqError::Format("FASTA partition count exceeds u64 range".into()))?;
351 let target_bases = total_bases.div_ceil(target_partitions_u64).max(1);
352 let mut partitions = Vec::new();
353
354 for entry in index.entries() {
355 let mut start = 0_u64;
356 while start < entry.len {
357 let remaining = entry.len - start;
358 let take = remaining.min(target_bases);
359 let core = start..start + take;
360 let fetch_start = core.start.saturating_sub(config.overlap_bases);
361 let fetch_end = core.end.saturating_add(config.overlap_bases).min(entry.len);
362 partitions.push(FastaPartition {
363 partition_index: partitions.len(),
364 name: entry.name.clone(),
365 core: core.clone(),
366 fetch: fetch_start..fetch_end,
367 });
368 start = core.end;
369 }
370 }
371
372 Ok(partitions)
373}
374
375#[derive(Debug, Clone, PartialEq, Eq)]
377pub struct FastaReferenceChunk {
378 pub name: Vec<u8>,
380 pub global_offset: u64,
382 pub seq: Vec<u8>,
384}
385
386#[derive(Debug, Clone, Copy)]
388pub struct FastaReferenceChunkRef<'a> {
389 pub name: &'a [u8],
391 pub global_offset: u64,
393 pub seq: &'a [u8],
395}
396
397pub trait FastaReferenceChunkSink {
399 fn chunk(&mut self, chunk: FastaReferenceChunkRef<'_>) -> Result<()>;
401}
402
403impl<F> FastaReferenceChunkSink for F
404where
405 F: FnMut(FastaReferenceChunkRef<'_>) -> Result<()>,
406{
407 fn chunk(&mut self, chunk: FastaReferenceChunkRef<'_>) -> Result<()> {
408 self(chunk)
409 }
410}
411
412pub struct FastaReferenceChunks<'a, R> {
414 reader: &'a mut IndexedFastaReader<R>,
415 name: Vec<u8>,
416 entry: FastaIndexEntry,
417 next_offset: u64,
418 end: u64,
419 chunk_bases: u64,
420 initialized: bool,
421}
422
423#[cfg(feature = "bgzf")]
425pub struct BgzfFastaReferenceChunks<'a, R> {
426 reader: &'a mut BgzfIndexedFastaReader<R>,
427 name: Vec<u8>,
428 entry: FastaIndexEntry,
429 next_offset: u64,
430 end: u64,
431 chunk_bases: u64,
432 initialized: bool,
433}
434
435impl<R: Read + Seek> Iterator for FastaReferenceChunks<'_, R> {
436 type Item = Result<FastaReferenceChunk>;
437
438 fn next(&mut self) -> Option<Self::Item> {
439 if self.next_offset >= self.end {
440 return None;
441 }
442 let start = self.next_offset;
443 if !self.initialized {
444 if let Err(err) =
445 seek_indexed_sequence_start(&mut self.reader.inner, &self.entry, start)
446 {
447 self.next_offset = self.end;
448 return Some(Err(err));
449 }
450 self.initialized = true;
451 }
452 let capacity = indexed_chunk_capacity(start, self.end, self.chunk_bases);
453 let mut seq = match capacity {
454 Ok(capacity) => Vec::with_capacity(capacity),
455 Err(err) => {
456 self.next_offset = self.end;
457 return Some(Err(err));
458 }
459 };
460 match read_next_indexed_sequence_chunk(
461 &mut self.reader.inner,
462 &self.entry,
463 start,
464 self.end,
465 self.chunk_bases,
466 &mut seq,
467 &mut self.reader.scratch,
468 ) {
469 Ok(next_offset) => {
470 self.next_offset = next_offset;
471 Some(Ok(FastaReferenceChunk {
472 name: self.name.clone(),
473 global_offset: start,
474 seq,
475 }))
476 }
477 Err(err) => {
478 self.next_offset = self.end;
479 Some(Err(err))
480 }
481 }
482 }
483}
484
485#[cfg(feature = "bgzf")]
486impl<R: Read + Seek> Iterator for BgzfFastaReferenceChunks<'_, R> {
487 type Item = Result<FastaReferenceChunk>;
488
489 fn next(&mut self) -> Option<Self::Item> {
490 if self.next_offset >= self.end {
491 return None;
492 }
493 let start = self.next_offset;
494 if !self.initialized {
495 if let Err(err) = seek_bgzf_indexed_sequence_start(
496 &mut self.reader.inner,
497 &self.reader.bgzf_index,
498 &self.entry,
499 start,
500 ) {
501 self.next_offset = self.end;
502 return Some(Err(err));
503 }
504 self.initialized = true;
505 }
506 let capacity = indexed_chunk_capacity(start, self.end, self.chunk_bases);
507 let mut seq = match capacity {
508 Ok(capacity) => Vec::with_capacity(capacity),
509 Err(err) => {
510 self.next_offset = self.end;
511 return Some(Err(err));
512 }
513 };
514 match read_next_indexed_sequence_chunk(
515 &mut self.reader.inner,
516 &self.entry,
517 start,
518 self.end,
519 self.chunk_bases,
520 &mut seq,
521 &mut self.reader.scratch,
522 ) {
523 Ok(next_offset) => {
524 self.next_offset = next_offset;
525 Some(Ok(FastaReferenceChunk {
526 name: self.name.clone(),
527 global_offset: start,
528 seq,
529 }))
530 }
531 Err(err) => {
532 self.next_offset = self.end;
533 Some(Err(err))
534 }
535 }
536 }
537}
538
539#[derive(Debug, Clone, Default, PartialEq, Eq)]
541pub struct FastaIndex {
542 entries: Vec<FastaIndexEntry>,
543 name_to_index: HashMap<Vec<u8>, usize>,
544}
545
546impl FastaIndex {
547 pub fn entries(&self) -> &[FastaIndexEntry] {
549 &self.entries
550 }
551
552 pub fn is_empty(&self) -> bool {
554 self.entries.is_empty()
555 }
556
557 pub fn len(&self) -> usize {
559 self.entries.len()
560 }
561
562 pub fn get(&self, name: &[u8]) -> Option<&FastaIndexEntry> {
564 self.name_to_index
565 .get(name)
566 .and_then(|&idx| self.entries.get(idx))
567 }
568
569 pub fn to_fai_string(&self) -> String {
571 let mut out = String::new();
572 for entry in &self.entries {
573 out.push_str(&String::from_utf8_lossy(&entry.name));
574 out.push('\t');
575 out.push_str(&entry.len.to_string());
576 out.push('\t');
577 out.push_str(&entry.offset.to_string());
578 out.push('\t');
579 out.push_str(&entry.line_bases.to_string());
580 out.push('\t');
581 out.push_str(&entry.line_width.to_string());
582 out.push('\n');
583 }
584 out
585 }
586
587 pub fn from_fai_bytes(bytes: &[u8]) -> Result<Self> {
589 let mut entries = Vec::new();
590 let mut seen = HashSet::new();
591 for (line_idx, line) in bytes.split(|&b| b == b'\n').enumerate() {
592 let line = trim_line(line);
593 if line.is_empty() {
594 continue;
595 }
596 let fields = line.split(|&b| b == b'\t').collect::<Vec<_>>();
597 if fields.len() != 5 {
598 return Err(FastqError::Format(format!(
599 "invalid .fai line {}: expected 5 tab-delimited fields",
600 line_idx + 1
601 )));
602 }
603 if fields[0].is_empty() {
604 return Err(FastqError::Format(format!(
605 "invalid .fai line {}: empty reference name",
606 line_idx + 1
607 )));
608 }
609 if !seen.insert(fields[0].to_vec()) {
610 return Err(FastqError::Format(format!(
611 "invalid .fai line {}: duplicate reference name",
612 line_idx + 1
613 )));
614 }
615 let len = parse_fai_u64(fields[1], line_idx + 1, "length")?;
616 let offset = parse_fai_u64(fields[2], line_idx + 1, "offset")?;
617 let line_bases = parse_fai_u64(fields[3], line_idx + 1, "line_bases")?;
618 let line_width = parse_fai_u64(fields[4], line_idx + 1, "line_width")?;
619 if len > 0 && line_bases == 0 {
620 return Err(FastqError::Format(format!(
621 "invalid .fai line {}: non-empty reference has zero line_bases",
622 line_idx + 1
623 )));
624 }
625 if line_width < line_bases {
626 return Err(FastqError::Format(format!(
627 "invalid .fai line {}: line_width is smaller than line_bases",
628 line_idx + 1
629 )));
630 }
631 entries.push(FastaIndexEntry {
632 name: fields[0].to_vec(),
633 len,
634 offset,
635 line_bases,
636 line_width,
637 #[cfg(feature = "bgzf")]
638 virtual_offset: None,
639 });
640 }
641 Ok(Self::from_entries(entries))
642 }
643
644 pub fn from_fai_str(text: &str) -> Result<Self> {
646 Self::from_fai_bytes(text.as_bytes())
647 }
648
649 pub fn from_fai_read<R: Read>(mut reader: R) -> Result<Self> {
651 let mut bytes = Vec::new();
652 reader.read_to_end(&mut bytes)?;
653 Self::from_fai_bytes(&bytes)
654 }
655
656 fn from_entries(entries: Vec<FastaIndexEntry>) -> Self {
657 let name_to_index = entries
658 .iter()
659 .enumerate()
660 .map(|(idx, entry)| (entry.name.clone(), idx))
661 .collect();
662 Self {
663 entries,
664 name_to_index,
665 }
666 }
667}
668
669fn parse_fai_u64(value: &[u8], line: usize, field: &str) -> Result<u64> {
670 let text = std::str::from_utf8(value).map_err(|_| {
671 FastqError::Format(format!(
672 "invalid .fai line {line}: {field} is not valid UTF-8"
673 ))
674 })?;
675 text.parse::<u64>().map_err(|_| {
676 FastqError::Format(format!(
677 "invalid .fai line {line}: {field} is not an unsigned integer"
678 ))
679 })
680}
681
682fn indexed_physical_window(entry: &FastaIndexEntry, range: Range<u64>) -> Result<Range<u64>> {
683 entry.validate_range(range.clone())?;
684 let start = entry.sequence_offset(range.start)?;
685 let end = if range.start == range.end {
686 start
687 } else {
688 entry
689 .sequence_offset(range.end - 1)?
690 .checked_add(1)
691 .ok_or_else(|| FastqError::Format("FASTA index span calculation overflowed".into()))?
692 };
693 if end < start {
694 return Err(FastqError::Format(
695 "FASTA index physical range is invalid".into(),
696 ));
697 }
698 Ok(start..end)
699}
700
701fn copy_indexed_sequence_window<R: Read>(
702 reader: &mut R,
703 entry: &FastaIndexEntry,
704 mut physical_offset: u64,
705 mut len: u64,
706 expected_bases: usize,
707 out: &mut Vec<u8>,
708 scratch: &mut Vec<u8>,
709) -> Result<()> {
710 if len == 0 {
711 return Ok(());
712 }
713 if entry.line_width == 0 {
714 return Err(FastqError::Format(
715 "FASTA index entry has zero line_width for non-empty range".into(),
716 ));
717 }
718 if scratch.len() < INDEXED_FETCH_SCRATCH_SIZE {
719 scratch.resize(INDEXED_FETCH_SCRATCH_SIZE, 0);
720 }
721 while len > 0 {
722 let take = usize::try_from(len.min(scratch.len() as u64))
723 .map_err(|_| FastqError::Format("FASTA fetch span exceeds usize range".into()))?;
724 reader.read_exact(&mut scratch[..take])?;
725 let mut cursor = 0;
726 while cursor < take {
727 let cursor_offset = physical_offset.checked_add(cursor as u64).ok_or_else(|| {
728 FastqError::Format("FASTA fetch physical offset overflowed".into())
729 })?;
730 let rel = cursor_offset.checked_sub(entry.offset).ok_or_else(|| {
731 FastqError::Format("FASTA index physical offset precedes sequence offset".into())
732 })?;
733 let in_line = rel % entry.line_width;
734 let remaining = take - cursor;
735 if in_line >= entry.line_bases {
736 let skip = (entry.line_width - in_line).min(remaining as u64);
737 cursor += usize::try_from(skip).map_err(|_| {
738 FastqError::Format("FASTA fetch span exceeds usize range".into())
739 })?;
740 continue;
741 }
742
743 let run = (entry.line_bases - in_line).min(remaining as u64);
744 let run = usize::try_from(run)
745 .map_err(|_| FastqError::Format("FASTA fetch span exceeds usize range".into()))?;
746 out.extend_from_slice(&scratch[cursor..cursor + run]);
747 if out.len() > expected_bases {
748 return Err(FastqError::Format(
749 "FASTA fetch produced more bases than expected".into(),
750 ));
751 }
752 cursor += run;
753 }
754 physical_offset = physical_offset
755 .checked_add(take as u64)
756 .ok_or_else(|| FastqError::Format("FASTA fetch physical offset overflowed".into()))?;
757 len -= take as u64;
758 }
759 if out.len() != expected_bases {
760 return Err(FastqError::Format(
761 "FASTA fetch produced fewer bases than expected".into(),
762 ));
763 }
764 Ok(())
765}
766
767fn seek_indexed_sequence_start<R: Seek>(
768 reader: &mut R,
769 entry: &FastaIndexEntry,
770 seq_pos: u64,
771) -> Result<()> {
772 let offset = entry.sequence_offset(seq_pos)?;
773 reader.seek(SeekFrom::Start(offset))?;
774 Ok(())
775}
776
777#[cfg(feature = "bgzf")]
778fn seek_bgzf_indexed_sequence_start<R: Read + Seek>(
779 reader: &mut BgzfSeekReader<R>,
780 bgzf_index: &BgzfIndex,
781 entry: &FastaIndexEntry,
782 seq_pos: u64,
783) -> Result<()> {
784 let offset = entry.sequence_offset(seq_pos)?;
785 let virtual_offset = bgzf_index
786 .virtual_offset_for_uncompressed_offset(offset)?
787 .ok_or_else(|| FastqError::Bgzf("BGZF span offset is not indexed".into()))?;
788 reader.seek_virtual_offset(virtual_offset)?;
789 Ok(())
790}
791
792fn indexed_chunk_capacity(start: u64, end: u64, chunk_bases: u64) -> Result<usize> {
793 usize::try_from(end.saturating_sub(start).min(chunk_bases.max(1)))
794 .map_err(|_| FastqError::Format("FASTA chunk length exceeds usize range".into()))
795}
796
797fn read_next_indexed_sequence_chunk<R: Read>(
798 reader: &mut R,
799 entry: &FastaIndexEntry,
800 start: u64,
801 end: u64,
802 chunk_bases: u64,
803 out: &mut Vec<u8>,
804 scratch: &mut Vec<u8>,
805) -> Result<u64> {
806 if entry.line_bases == 0 {
807 return Err(FastqError::Format(
808 "FASTA index entry has zero line_bases for non-empty range".into(),
809 ));
810 }
811 if entry.line_width < entry.line_bases {
812 return Err(FastqError::Format(
813 "FASTA index entry has line_width smaller than line_bases".into(),
814 ));
815 }
816
817 let chunk_end = start.saturating_add(chunk_bases.max(1)).min(end);
818 out.clear();
819 out.reserve(indexed_chunk_capacity(start, chunk_end, chunk_bases)?);
820
821 let mut pos = start;
822 while pos < chunk_end {
823 let in_line = pos % entry.line_bases;
824 let take = (chunk_end - pos).min(entry.line_bases - in_line);
825 read_exact_into_vec(reader, out, take)?;
826 pos += take;
827 if pos < end && pos.is_multiple_of(entry.line_bases) {
828 skip_indexed_separator(reader, entry.line_width - entry.line_bases, scratch)?;
829 }
830 }
831 Ok(pos)
832}
833
834fn read_exact_into_vec<R: Read>(reader: &mut R, out: &mut Vec<u8>, len: u64) -> Result<()> {
835 let len = usize::try_from(len)
836 .map_err(|_| FastqError::Format("FASTA sequence run exceeds usize range".into()))?;
837 let old_len = out.len();
838 out.resize(old_len + len, 0);
839 reader.read_exact(&mut out[old_len..])?;
840 Ok(())
841}
842
843fn skip_indexed_separator<R: Read>(
844 reader: &mut R,
845 mut len: u64,
846 scratch: &mut Vec<u8>,
847) -> Result<()> {
848 if len == 0 {
849 return Ok(());
850 }
851 if scratch.len() < INDEXED_FETCH_SCRATCH_SIZE {
852 scratch.resize(INDEXED_FETCH_SCRATCH_SIZE, 0);
853 }
854 while len > 0 {
855 let take = usize::try_from(len.min(scratch.len() as u64))
856 .map_err(|_| FastqError::Format("FASTA separator span exceeds usize range".into()))?;
857 reader.read_exact(&mut scratch[..take])?;
858 len -= take as u64;
859 }
860 Ok(())
861}
862
863struct IndexedSequenceStream<'a, R> {
864 reader: &'a mut R,
865 scratch: &'a mut Vec<u8>,
866 entry: &'a FastaIndexEntry,
867 next_offset: u64,
868 end: u64,
869 chunk_bases: u64,
870}
871
872impl<R: Read> IndexedSequenceStream<'_, R> {
873 fn next_chunk_into(&mut self, out: &mut Vec<u8>) -> Result<Option<u64>> {
874 if self.next_offset >= self.end {
875 return Ok(None);
876 }
877 let start = self.next_offset;
878 self.next_offset = read_next_indexed_sequence_chunk(
879 self.reader,
880 self.entry,
881 start,
882 self.end,
883 self.chunk_bases,
884 out,
885 self.scratch,
886 )?;
887 Ok(Some(start))
888 }
889}
890
891fn stream_indexed_reference_chunks_into<R, S>(
892 stream: &mut IndexedSequenceStream<'_, R>,
893 name: &[u8],
894 out: &mut Vec<u8>,
895 sink: &mut S,
896) -> Result<()>
897where
898 R: Read,
899 S: FastaReferenceChunkSink,
900{
901 while let Some(start) = stream.next_chunk_into(out)? {
902 sink.chunk(FastaReferenceChunkRef {
903 name,
904 global_offset: start,
905 seq: out,
906 })?;
907 }
908 Ok(())
909}
910
911pub struct IndexedFastaReader<R> {
913 inner: R,
914 index: FastaIndex,
915 scratch: Vec<u8>,
916}
917
918impl<R: Read + Seek> IndexedFastaReader<R> {
919 pub fn new(inner: R, index: FastaIndex) -> Self {
921 Self {
922 inner,
923 index,
924 scratch: Vec::new(),
925 }
926 }
927
928 pub fn index(&self) -> &FastaIndex {
930 &self.index
931 }
932
933 pub fn fetch_into(&mut self, name: &[u8], range: Range<u64>, out: &mut Vec<u8>) -> Result<()> {
935 let entry = self.index.get(name).ok_or_else(|| {
936 FastqError::Format(format!(
937 "FASTA reference not found in index: {}",
938 String::from_utf8_lossy(name)
939 ))
940 })?;
941 let window = indexed_physical_window(entry, range.clone())?;
942 out.clear();
943 let expected_bases = usize::try_from(range.end - range.start).map_err(|_| {
944 FastqError::Format("FASTA fetch range length exceeds usize range".into())
945 })?;
946 out.reserve(expected_bases);
947 self.inner.seek(SeekFrom::Start(window.start))?;
948 copy_indexed_sequence_window(
949 &mut self.inner,
950 entry,
951 window.start,
952 window.end - window.start,
953 expected_bases,
954 out,
955 &mut self.scratch,
956 )?;
957 Ok(())
958 }
959
960 pub fn fetch(&mut self, name: &[u8], range: Range<u64>) -> Result<Vec<u8>> {
962 let mut out = Vec::new();
963 self.fetch_into(name, range, &mut out)?;
964 Ok(out)
965 }
966
967 pub fn reference_chunks(
973 &mut self,
974 name: &[u8],
975 range: Range<u64>,
976 chunk_bases: u64,
977 ) -> Result<FastaReferenceChunks<'_, R>> {
978 let entry = self.index.get(name).ok_or_else(|| {
979 FastqError::Format(format!(
980 "FASTA reference not found in index: {}",
981 String::from_utf8_lossy(name)
982 ))
983 })?;
984 entry.validate_range(range.clone())?;
985 let entry = entry.clone();
986 Ok(FastaReferenceChunks {
987 reader: self,
988 name: name.to_vec(),
989 entry,
990 next_offset: range.start,
991 end: range.end,
992 chunk_bases: chunk_bases.max(1),
993 initialized: false,
994 })
995 }
996
997 pub fn reference_chunks_into<S>(
1003 &mut self,
1004 name: &[u8],
1005 range: Range<u64>,
1006 chunk_bases: u64,
1007 out: &mut Vec<u8>,
1008 sink: &mut S,
1009 ) -> Result<()>
1010 where
1011 S: FastaReferenceChunkSink,
1012 {
1013 let entry = self.index.get(name).ok_or_else(|| {
1014 FastqError::Format(format!(
1015 "FASTA reference not found in index: {}",
1016 String::from_utf8_lossy(name)
1017 ))
1018 })?;
1019 entry.validate_range(range.clone())?;
1020 if range.start < range.end {
1021 seek_indexed_sequence_start(&mut self.inner, entry, range.start)?;
1022 }
1023 let mut stream = IndexedSequenceStream {
1024 reader: &mut self.inner,
1025 scratch: &mut self.scratch,
1026 entry,
1027 next_offset: range.start,
1028 end: range.end,
1029 chunk_bases,
1030 };
1031 stream_indexed_reference_chunks_into(&mut stream, name, out, sink)
1032 }
1033
1034 pub fn fetch_partition(&mut self, partition: &FastaPartition) -> Result<FastaReferenceChunk> {
1036 self.fetch(&partition.name, partition.fetch.clone())
1037 .map(|seq| FastaReferenceChunk {
1038 name: partition.name.clone(),
1039 global_offset: partition.fetch.start,
1040 seq,
1041 })
1042 }
1043
1044 pub fn into_inner(self) -> (R, FastaIndex) {
1046 (self.inner, self.index)
1047 }
1048}
1049
1050#[cfg(feature = "bgzf")]
1053pub struct BgzfIndexedFastaReader<R> {
1054 inner: BgzfSeekReader<R>,
1055 fasta_index: FastaIndex,
1056 bgzf_index: BgzfIndex,
1057 scratch: Vec<u8>,
1058}
1059
1060#[cfg(feature = "bgzf")]
1061impl<R: Read + Seek> BgzfIndexedFastaReader<R> {
1062 pub fn new(inner: R, fasta_index: FastaIndex, bgzf_index: BgzfIndex) -> Self {
1064 Self {
1065 inner: BgzfSeekReader::new(inner),
1066 fasta_index,
1067 bgzf_index,
1068 scratch: Vec::new(),
1069 }
1070 }
1071
1072 pub fn fasta_index(&self) -> &FastaIndex {
1074 &self.fasta_index
1075 }
1076
1077 pub fn bgzf_index(&self) -> &BgzfIndex {
1079 &self.bgzf_index
1080 }
1081
1082 pub fn fetch_into(&mut self, name: &[u8], range: Range<u64>, out: &mut Vec<u8>) -> Result<()> {
1084 let entry = self.fasta_index.get(name).ok_or_else(|| {
1085 FastqError::Format(format!(
1086 "FASTA reference not found in index: {}",
1087 String::from_utf8_lossy(name)
1088 ))
1089 })?;
1090 let window = indexed_physical_window(entry, range.clone())?;
1091 out.clear();
1092 let expected_bases = usize::try_from(range.end - range.start).map_err(|_| {
1093 FastqError::Format("FASTA fetch range length exceeds usize range".into())
1094 })?;
1095 out.reserve(expected_bases);
1096 if expected_bases == 0 {
1097 return Ok(());
1098 }
1099 let virtual_offset = self
1100 .bgzf_index
1101 .virtual_offset_for_uncompressed_offset(window.start)?
1102 .ok_or_else(|| FastqError::Bgzf("BGZF span offset is not indexed".into()))?;
1103 self.inner.seek_virtual_offset(virtual_offset)?;
1104 copy_indexed_sequence_window(
1105 &mut self.inner,
1106 entry,
1107 window.start,
1108 window.end - window.start,
1109 expected_bases,
1110 out,
1111 &mut self.scratch,
1112 )?;
1113 Ok(())
1114 }
1115
1116 pub fn fetch(&mut self, name: &[u8], range: Range<u64>) -> Result<Vec<u8>> {
1118 let mut out = Vec::new();
1119 self.fetch_into(name, range, &mut out)?;
1120 Ok(out)
1121 }
1122
1123 pub fn reference_chunks(
1129 &mut self,
1130 name: &[u8],
1131 range: Range<u64>,
1132 chunk_bases: u64,
1133 ) -> Result<BgzfFastaReferenceChunks<'_, R>> {
1134 let entry = self.fasta_index.get(name).ok_or_else(|| {
1135 FastqError::Format(format!(
1136 "FASTA reference not found in index: {}",
1137 String::from_utf8_lossy(name)
1138 ))
1139 })?;
1140 entry.validate_range(range.clone())?;
1141 let entry = entry.clone();
1142 Ok(BgzfFastaReferenceChunks {
1143 reader: self,
1144 name: name.to_vec(),
1145 entry,
1146 next_offset: range.start,
1147 end: range.end,
1148 chunk_bases: chunk_bases.max(1),
1149 initialized: false,
1150 })
1151 }
1152
1153 pub fn reference_chunks_into<S>(
1159 &mut self,
1160 name: &[u8],
1161 range: Range<u64>,
1162 chunk_bases: u64,
1163 out: &mut Vec<u8>,
1164 sink: &mut S,
1165 ) -> Result<()>
1166 where
1167 S: FastaReferenceChunkSink,
1168 {
1169 let entry = self.fasta_index.get(name).ok_or_else(|| {
1170 FastqError::Format(format!(
1171 "FASTA reference not found in index: {}",
1172 String::from_utf8_lossy(name)
1173 ))
1174 })?;
1175 entry.validate_range(range.clone())?;
1176 if range.start < range.end {
1177 seek_bgzf_indexed_sequence_start(
1178 &mut self.inner,
1179 &self.bgzf_index,
1180 entry,
1181 range.start,
1182 )?;
1183 }
1184 let mut stream = IndexedSequenceStream {
1185 reader: &mut self.inner,
1186 scratch: &mut self.scratch,
1187 entry,
1188 next_offset: range.start,
1189 end: range.end,
1190 chunk_bases,
1191 };
1192 stream_indexed_reference_chunks_into(&mut stream, name, out, sink)
1193 }
1194
1195 pub fn fetch_partition(&mut self, partition: &FastaPartition) -> Result<FastaReferenceChunk> {
1197 self.fetch(&partition.name, partition.fetch.clone())
1198 .map(|seq| FastaReferenceChunk {
1199 name: partition.name.clone(),
1200 global_offset: partition.fetch.start,
1201 seq,
1202 })
1203 }
1204}
1205
1206impl Default for FastaStats {
1207 fn default() -> Self {
1208 Self {
1209 records: 0,
1210 bases: 0,
1211 checksum: FASTA_STATS_CHECKSUM_INIT,
1212 }
1213 }
1214}
1215
1216impl FastaStats {
1217 pub fn observe_sequence(&mut self, seq: &[u8]) {
1219 self.observe_sequence_parts(
1220 seq.len() as u64,
1221 seq.first().copied().unwrap_or_default(),
1222 seq.last().copied().unwrap_or_default(),
1223 );
1224 }
1225
1226 fn observe_sequence_parts(&mut self, len: u64, first: u8, last: u8) {
1227 self.records += 1;
1228 self.bases += len;
1229 self.checksum ^= len;
1230 self.checksum = self
1231 .checksum
1232 .rotate_left(5)
1233 .wrapping_mul(0x0000_0100_0000_01b3);
1234 self.checksum ^= first as u64;
1235 self.checksum ^= (last as u64) << 8;
1236 }
1237}
1238
1239pub trait FastaRecordSink {
1241 fn record(&mut self, record: FastaVisitRecord<'_>) -> Result<()>;
1243}
1244
1245impl<F> FastaRecordSink for F
1246where
1247 F: FnMut(FastaVisitRecord<'_>) -> Result<()>,
1248{
1249 fn record(&mut self, record: FastaVisitRecord<'_>) -> Result<()> {
1250 self(record)
1251 }
1252}
1253
1254#[derive(Debug)]
1256pub struct FastaBatch<'a> {
1257 bytes: &'a [u8],
1258 records: &'a [FastaRecordRef],
1259 id_tokens: &'a [ByteRange],
1260 first_record_index: u64,
1261}
1262
1263impl<'a> FastaBatch<'a> {
1264 pub fn len(&self) -> usize {
1266 self.records.len()
1267 }
1268
1269 pub fn is_empty(&self) -> bool {
1271 self.records.is_empty()
1272 }
1273
1274 pub fn bytes(&self) -> &'a [u8] {
1276 self.bytes
1277 }
1278
1279 pub fn record_refs(&self) -> &'a [FastaRecordRef] {
1281 self.records
1282 }
1283
1284 pub fn first_record_index(&self) -> u64 {
1286 self.first_record_index
1287 }
1288
1289 pub fn records(&self) -> impl Iterator<Item = FastaRecord<'_>> {
1291 self.records
1292 .iter()
1293 .zip(self.id_tokens.iter().cloned())
1294 .map(|(record, id_token)| FastaRecord {
1295 bytes: self.bytes,
1296 record,
1297 id_token,
1298 })
1299 }
1300
1301 pub fn to_owned_batch(&self) -> OwnedFastaBatch {
1303 OwnedFastaBatch {
1304 bytes: self.bytes.to_vec(),
1305 records: self.records.to_vec(),
1306 id_tokens: self.id_tokens.to_vec(),
1307 first_record_index: self.first_record_index,
1308 }
1309 }
1310}
1311
1312#[derive(Debug, Clone, PartialEq, Eq)]
1314pub struct OwnedFastaBatch {
1315 bytes: Vec<u8>,
1316 records: Vec<FastaRecordRef>,
1317 id_tokens: Vec<ByteRange>,
1318 first_record_index: u64,
1319}
1320
1321impl OwnedFastaBatch {
1322 pub fn len(&self) -> usize {
1324 self.records.len()
1325 }
1326
1327 pub fn is_empty(&self) -> bool {
1329 self.records.is_empty()
1330 }
1331
1332 pub fn bytes(&self) -> &[u8] {
1334 &self.bytes
1335 }
1336
1337 pub fn record_refs(&self) -> &[FastaRecordRef] {
1339 &self.records
1340 }
1341
1342 pub fn first_record_index(&self) -> u64 {
1344 self.first_record_index
1345 }
1346
1347 pub fn records(&self) -> impl Iterator<Item = OwnedFastaRecord<'_>> {
1349 self.records
1350 .iter()
1351 .zip(self.id_tokens.iter().cloned())
1352 .map(|(record, id_token)| OwnedFastaRecord {
1353 bytes: &self.bytes,
1354 record,
1355 id_token,
1356 })
1357 }
1358}
1359
1360#[derive(Debug, Clone, Copy)]
1362pub struct OwnedFastaRecord<'a> {
1363 bytes: &'a [u8],
1364 record: &'a FastaRecordRef,
1365 id_token: ByteRange,
1366}
1367
1368impl<'a> OwnedFastaRecord<'a> {
1369 pub fn name(self) -> &'a [u8] {
1371 &self.bytes[to_usize(self.record.name.clone())]
1372 }
1373
1374 pub fn name_without_gt(self) -> &'a [u8] {
1376 let name = self.name();
1377 name.strip_prefix(b">").unwrap_or(name)
1378 }
1379
1380 pub fn id_token(self) -> &'a [u8] {
1382 &self.bytes[self.id_token.to_usize()]
1383 }
1384
1385 pub fn seq(self) -> &'a [u8] {
1387 &self.bytes[to_usize(self.record.seq.clone())]
1388 }
1389}
1390
1391#[derive(Debug)]
1397pub struct FastaReader<R> {
1398 reader: BufReader<R>,
1399 config: FastaConfig,
1400 bytes: Vec<u8>,
1401 records: Vec<FastaRecordRef>,
1402 id_tokens: Vec<ByteRange>,
1403 line: Vec<u8>,
1404 pending_header: Option<PendingHeader>,
1405 byte_offset: u64,
1406 record_index: u64,
1407 eof: bool,
1408}
1409
1410#[derive(Debug)]
1411struct PendingHeader {
1412 bytes: Vec<u8>,
1413 byte_offset: u64,
1414}
1415
1416impl<R: Read> FastaReader<R> {
1417 pub fn new(reader: R) -> Self {
1419 Self::with_config(reader, FastaConfig::default())
1420 }
1421
1422 pub fn with_config(reader: R, config: FastaConfig) -> Self {
1424 let config = FastaConfig {
1425 batch_records: config.batch_records.max(1),
1426 buffer_size: config.buffer_size.max(1024),
1427 expected_seq_len: config.expected_seq_len,
1428 };
1429 let seq_capacity = config
1430 .batch_records
1431 .saturating_mul(config.expected_seq_len)
1432 .min(8 * 1024 * 1024);
1433 Self {
1434 reader: BufReader::with_capacity(config.buffer_size, reader),
1435 bytes: Vec::with_capacity(seq_capacity),
1436 records: Vec::with_capacity(config.batch_records),
1437 id_tokens: Vec::with_capacity(config.batch_records),
1438 config,
1439 line: Vec::new(),
1440 pending_header: None,
1441 byte_offset: 0,
1442 record_index: 0,
1443 eof: false,
1444 }
1445 }
1446
1447 pub fn into_inner(self) -> R {
1449 self.reader.into_inner()
1450 }
1451
1452 pub fn next_batch(&mut self) -> Result<Option<FastaBatch<'_>>> {
1457 self.bytes.clear();
1458 self.records.clear();
1459 self.id_tokens.clear();
1460 let first_record_index = self.record_index;
1461
1462 while self.records.len() < self.config.batch_records {
1463 let Some(header) = self.next_header()? else {
1464 break;
1465 };
1466 self.push_record(header)?;
1467 }
1468
1469 if self.records.is_empty() {
1470 return Ok(None);
1471 }
1472
1473 self.record_index += self.records.len() as u64;
1474 Ok(Some(FastaBatch {
1475 bytes: &self.bytes,
1476 records: &self.records,
1477 id_tokens: &self.id_tokens,
1478 first_record_index,
1479 }))
1480 }
1481
1482 pub fn next_owned_batch(&mut self) -> Result<Option<OwnedFastaBatch>> {
1484 self.next_batch()
1485 .map(|batch| batch.map(|batch| batch.to_owned_batch()))
1486 }
1487
1488 pub fn visit_records<F>(&mut self, mut visit: F) -> Result<()>
1493 where
1494 F: FnMut(FastaVisitRecord<'_>) -> Result<()>,
1495 {
1496 self.visit_records_with_sink(&mut visit)
1497 }
1498
1499 pub fn visit_records_with_sink<S>(&mut self, sink: &mut S) -> Result<()>
1501 where
1502 S: FastaRecordSink,
1503 {
1504 while let Some(header) = self.next_header()? {
1505 self.visit_record(header, sink)?;
1506 self.record_index += 1;
1507 }
1508 Ok(())
1509 }
1510
1511 pub fn stats(&mut self) -> Result<FastaStats> {
1517 let mut stats = FastaStats::default();
1518 while let Some(header) = self.next_header()? {
1519 self.observe_record_stats(header, &mut stats)?;
1520 self.record_index += 1;
1521 }
1522 Ok(stats)
1523 }
1524
1525 fn observe_record_stats(
1526 &mut self,
1527 _header: PendingHeader,
1528 stats: &mut FastaStats,
1529 ) -> Result<()> {
1530 let record_index = self.record_index;
1531 let mut bases = 0_u64;
1532 let mut first = 0_u8;
1533 let mut last = 0_u8;
1534
1535 loop {
1536 let line_start = self.byte_offset;
1537 let n = self.read_line()?;
1538 if n == 0 {
1539 self.eof = true;
1540 break;
1541 }
1542 let trimmed = trim_line(&self.line);
1543 if trimmed.starts_with(b">") {
1544 validate_header(trimmed, line_start, record_index + 1)?;
1545 self.pending_header = Some(PendingHeader {
1546 bytes: trimmed.to_vec(),
1547 byte_offset: line_start,
1548 });
1549 break;
1550 }
1551 if trimmed.is_empty() {
1552 continue;
1553 }
1554 if bases == 0 {
1555 first = trimmed[0];
1556 }
1557 last = *trimmed.last().unwrap_or(&0);
1558 bases += trimmed.len() as u64;
1559 }
1560
1561 stats.observe_sequence_parts(bases, first, last);
1562 Ok(())
1563 }
1564
1565 fn visit_record<S>(&mut self, header: PendingHeader, sink: &mut S) -> Result<()>
1566 where
1567 S: FastaRecordSink,
1568 {
1569 let record_index = self.record_index;
1570 self.bytes.clear();
1571
1572 loop {
1573 let line_start = self.byte_offset;
1574 let n = self.read_line()?;
1575 if n == 0 {
1576 self.eof = true;
1577 break;
1578 }
1579 let trimmed = trim_line(&self.line);
1580 if trimmed.starts_with(b">") {
1581 validate_header(trimmed, line_start, record_index + 1)?;
1582 self.pending_header = Some(PendingHeader {
1583 bytes: trimmed.to_vec(),
1584 byte_offset: line_start,
1585 });
1586 break;
1587 }
1588 if trimmed.is_empty() {
1589 continue;
1590 }
1591 self.bytes.extend_from_slice(trimmed);
1592 }
1593
1594 let _ = header.byte_offset;
1595 sink.record(FastaVisitRecord {
1596 name: &header.bytes,
1597 seq: &self.bytes,
1598 })
1599 }
1600
1601 fn next_header(&mut self) -> Result<Option<PendingHeader>> {
1602 if let Some(header) = self.pending_header.take() {
1603 return Ok(Some(header));
1604 }
1605 if self.eof {
1606 return Ok(None);
1607 }
1608
1609 loop {
1610 let line_start = self.byte_offset;
1611 let n = self.read_line()?;
1612 if n == 0 {
1613 self.eof = true;
1614 return Ok(None);
1615 }
1616 let trimmed = trim_line(&self.line);
1617 if trimmed.is_empty() {
1618 continue;
1619 }
1620 if !trimmed.starts_with(b">") {
1621 return Err(format_at(
1622 "FASTA record header must start with `>`",
1623 line_start,
1624 self.record_index,
1625 ));
1626 }
1627 validate_header(trimmed, line_start, self.record_index)?;
1628 return Ok(Some(PendingHeader {
1629 bytes: trimmed.to_vec(),
1630 byte_offset: line_start,
1631 }));
1632 }
1633 }
1634
1635 fn push_record(&mut self, header: PendingHeader) -> Result<()> {
1636 let record_index = self.record_index + self.records.len() as u64;
1637 let name_start = checked_u32(self.bytes.len())?;
1638 self.bytes.extend_from_slice(&header.bytes);
1639 let name_end = checked_u32(self.bytes.len())?;
1640 let id_token = id_token_range(&self.bytes, name_start..name_end)?;
1641 let seq_start = checked_u32(self.bytes.len())?;
1642
1643 loop {
1644 let line_start = self.byte_offset;
1645 let n = self.read_line()?;
1646 if n == 0 {
1647 self.eof = true;
1648 break;
1649 }
1650 let trimmed = trim_line(&self.line);
1651 if trimmed.starts_with(b">") {
1652 validate_header(trimmed, line_start, record_index + 1)?;
1653 self.pending_header = Some(PendingHeader {
1654 bytes: trimmed.to_vec(),
1655 byte_offset: line_start,
1656 });
1657 break;
1658 }
1659 if trimmed.is_empty() {
1660 continue;
1661 }
1662 self.bytes.extend_from_slice(trimmed);
1663 }
1664
1665 let seq_end = checked_u32(self.bytes.len())?;
1666 self.records.push(FastaRecordRef {
1667 name: name_start..name_end,
1668 seq: seq_start..seq_end,
1669 });
1670 self.id_tokens.push(id_token);
1671 let _ = header.byte_offset;
1672 Ok(())
1673 }
1674
1675 fn read_line(&mut self) -> Result<usize> {
1676 self.line.clear();
1677 let n = self.reader.read_until(b'\n', &mut self.line)?;
1678 self.byte_offset += n as u64;
1679 Ok(n)
1680 }
1681}
1682
1683pub fn count_fasta_read<R: Read>(reader: R) -> Result<FastaStats> {
1688 let mut reader = BufReader::with_capacity(DEFAULT_READER_BUFFER_SIZE, reader);
1689 count_fasta_bufread(&mut reader)
1690}
1691
1692fn count_fasta_bufread<R: BufRead>(reader: &mut R) -> Result<FastaStats> {
1693 let mut scanner = FastaCountScanner::default();
1694 let mut carry = Vec::new();
1695 let mut carry_start = 0_u64;
1696 let mut byte_offset = 0_u64;
1697
1698 loop {
1699 let available = reader.fill_buf()?;
1700 if available.is_empty() {
1701 break;
1702 }
1703
1704 let mut consumed = 0;
1705 while consumed < available.len() {
1706 let line_start = byte_offset + consumed as u64;
1707 let Some(relative_newline) = memchr(b'\n', &available[consumed..]) else {
1708 if carry.is_empty() {
1709 carry_start = line_start;
1710 }
1711 carry.extend_from_slice(&available[consumed..]);
1712 consumed = available.len();
1713 break;
1714 };
1715
1716 let line_end = consumed + relative_newline;
1717 if carry.is_empty() {
1718 scanner.observe_line(trim_line(&available[consumed..line_end]), line_start)?;
1719 } else {
1720 carry.extend_from_slice(&available[consumed..line_end]);
1721 scanner.observe_line(trim_line(&carry), carry_start)?;
1722 carry.clear();
1723 }
1724 consumed = line_end + 1;
1725 }
1726
1727 reader.consume(consumed);
1728 byte_offset += consumed as u64;
1729 }
1730
1731 if !carry.is_empty() {
1732 scanner.observe_line(trim_line(&carry), carry_start)?;
1733 }
1734
1735 scanner.finish()
1736}
1737
1738#[derive(Debug, Default)]
1739struct FastaCountScanner {
1740 stats: FastaStats,
1741 in_record: bool,
1742 record_bases: u64,
1743 first_base: u8,
1744 last_base: u8,
1745 record_index: u64,
1746}
1747
1748impl FastaCountScanner {
1749 fn observe_line(&mut self, line: &[u8], line_start: u64) -> Result<()> {
1750 if line.is_empty() {
1751 return Ok(());
1752 }
1753
1754 if line.starts_with(b">") {
1755 self.finish_record();
1756 validate_header(line, line_start, self.record_index)?;
1757 self.in_record = true;
1758 return Ok(());
1759 }
1760
1761 if !self.in_record {
1762 return Err(format_at(
1763 "FASTA record header must start with `>`",
1764 line_start,
1765 self.record_index,
1766 ));
1767 }
1768
1769 if self.record_bases == 0 {
1770 self.first_base = line[0];
1771 }
1772 self.last_base = line.last().copied().unwrap_or_default();
1773 self.record_bases += line.len() as u64;
1774 Ok(())
1775 }
1776
1777 fn finish(mut self) -> Result<FastaStats> {
1778 self.finish_record();
1779 Ok(self.stats)
1780 }
1781
1782 fn finish_record(&mut self) {
1783 if !self.in_record {
1784 return;
1785 }
1786 self.stats
1787 .observe_sequence_parts(self.record_bases, self.first_base, self.last_base);
1788 self.record_index += 1;
1789 self.in_record = false;
1790 self.record_bases = 0;
1791 self.first_base = 0;
1792 self.last_base = 0;
1793 }
1794}
1795
1796pub fn count_fasta_bytes(bytes: &[u8]) -> Result<FastaStats> {
1801 let mut scanner = FastaCountScanner::default();
1802 let mut cursor = 0;
1803
1804 while cursor < bytes.len() {
1805 let line_start = cursor as u64;
1806 let line = next_trimmed_line(bytes, &mut cursor);
1807 scanner.observe_line(line, line_start)?;
1808 }
1809
1810 scanner.finish()
1811}
1812
1813pub fn build_fasta_index<R: Read>(reader: R) -> Result<FastaIndex> {
1819 let mut builder = FastaIndexBuilder::default();
1820 let mut reader = BufReader::new(reader);
1821 build_fasta_index_bufread(&mut reader, &mut builder)?;
1822 Ok(builder.finish())
1823}
1824
1825#[cfg(feature = "bgzf")]
1831pub fn build_fasta_index_bgzf<R: Read>(reader: R) -> Result<FastaIndex> {
1832 let mut block_reader = BgzfDecodedBlockReader::new(reader);
1833 let mut builder = FastaIndexBuilder::default();
1834 let mut bgzf_entries = Vec::new();
1835 let mut line = Vec::new();
1836
1837 while let Some(block) = block_reader.next_block()? {
1838 bgzf_entries.push(block.index_entry());
1839 observe_fasta_index_chunk(block.bytes(), &mut line, &mut builder)?;
1840 }
1841 if !line.is_empty() {
1842 builder.observe_physical_line(&line)?;
1843 }
1844 builder.finish_current()?;
1845
1846 let mut index = builder.finish();
1847 for entry in &mut index.entries {
1848 entry.virtual_offset = bgzf_virtual_offset_for(&bgzf_entries, entry.offset)?;
1849 }
1850 Ok(index)
1851}
1852
1853#[cfg(feature = "bgzf")]
1854fn bgzf_virtual_offset_for(
1855 entries: &[BgzfIndexEntry],
1856 offset: u64,
1857) -> Result<Option<BgzfVirtualOffset>> {
1858 let idx = entries.partition_point(|entry| entry.uncompressed_offset <= offset);
1859 let Some(entry) = idx.checked_sub(1).and_then(|idx| entries.get(idx)) else {
1860 return Ok(None);
1861 };
1862 entry.virtual_offset_for(offset)
1863}
1864
1865#[derive(Debug, Default)]
1866struct FastaIndexBuilder {
1867 entries: Vec<FastaIndexEntry>,
1868 seen_names: HashSet<Vec<u8>>,
1869 current: Option<FastaIndexRecord>,
1870 byte_offset: u64,
1871}
1872
1873#[derive(Debug)]
1874struct FastaIndexRecord {
1875 name: Vec<u8>,
1876 len: u64,
1877 offset: Option<u64>,
1878 line_bases: Option<u64>,
1879 line_width: Option<u64>,
1880 last_line_bases: Option<u64>,
1881 last_line_width: Option<u64>,
1882 record_index: u64,
1883}
1884
1885fn build_fasta_index_bufread<R: BufRead>(
1886 reader: &mut R,
1887 builder: &mut FastaIndexBuilder,
1888) -> Result<()> {
1889 let mut line = Vec::new();
1890 loop {
1891 let available = reader.fill_buf()?;
1892 if available.is_empty() {
1893 break;
1894 }
1895 let consumed = observe_fasta_index_chunk(available, &mut line, builder)?;
1896 reader.consume(consumed);
1897 }
1898 if !line.is_empty() {
1899 builder.observe_physical_line(&line)?;
1900 }
1901 builder.finish_current()?;
1902 Ok(())
1903}
1904
1905fn observe_fasta_index_chunk(
1906 bytes: &[u8],
1907 carry: &mut Vec<u8>,
1908 builder: &mut FastaIndexBuilder,
1909) -> Result<usize> {
1910 let mut consumed = 0;
1911 while consumed < bytes.len() {
1912 let Some(relative_newline) = memchr(b'\n', &bytes[consumed..]) else {
1913 carry.extend_from_slice(&bytes[consumed..]);
1914 return Ok(bytes.len());
1915 };
1916 let line_end = consumed + relative_newline + 1;
1917 if carry.is_empty() {
1918 builder.observe_physical_line(&bytes[consumed..line_end])?;
1919 } else {
1920 carry.extend_from_slice(&bytes[consumed..line_end]);
1921 builder.observe_physical_line(carry)?;
1922 carry.clear();
1923 }
1924 consumed = line_end;
1925 }
1926 Ok(consumed)
1927}
1928
1929impl FastaIndexBuilder {
1930 fn observe_physical_line(&mut self, line: &[u8]) -> Result<()> {
1931 let line_start = self.byte_offset;
1932 self.byte_offset += line.len() as u64;
1933 let trimmed = trim_line(line);
1934 if trimmed.is_empty() {
1935 self.observe_blank(line_start)
1936 } else if trimmed.starts_with(b">") {
1937 self.start_record(trimmed, line_start)
1938 } else {
1939 self.observe_sequence_line(trimmed.len() as u64, line.len() as u64, line_start)
1940 }
1941 }
1942
1943 fn start_record(&mut self, header: &[u8], byte_offset: u64) -> Result<()> {
1944 self.finish_current()?;
1945 validate_header(header, byte_offset, self.entries.len() as u64)?;
1946 let name = fasta_index_name(header);
1947 if self.seen_names.contains(name) {
1948 return Err(format_at(
1949 "duplicate FASTA index reference name",
1950 byte_offset,
1951 self.entries.len() as u64,
1952 ));
1953 }
1954 self.seen_names.insert(name.to_vec());
1955 self.current = Some(FastaIndexRecord {
1956 name: name.to_vec(),
1957 len: 0,
1958 offset: None,
1959 line_bases: None,
1960 line_width: None,
1961 last_line_bases: None,
1962 last_line_width: None,
1963 record_index: self.entries.len() as u64,
1964 });
1965 Ok(())
1966 }
1967
1968 fn observe_blank(&mut self, byte_offset: u64) -> Result<()> {
1969 if self
1970 .current
1971 .as_ref()
1972 .and_then(|record| record.offset)
1973 .is_some()
1974 {
1975 return Err(format_at(
1976 "FASTA index does not support blank sequence lines",
1977 byte_offset,
1978 self.current
1979 .as_ref()
1980 .map_or(0, |record| record.record_index),
1981 ));
1982 }
1983 Ok(())
1984 }
1985
1986 fn observe_sequence_line(&mut self, bases: u64, width: u64, byte_offset: u64) -> Result<()> {
1987 let Some(record) = self.current.as_mut() else {
1988 return Err(format_at(
1989 "FASTA record header must start with `>`",
1990 byte_offset,
1991 self.entries.len() as u64,
1992 ));
1993 };
1994 if record.offset.is_none() {
1995 record.offset = Some(byte_offset);
1996 record.line_bases = Some(bases);
1997 record.line_width = Some(width);
1998 } else if let (Some(last_bases), Some(last_width)) =
1999 (record.last_line_bases, record.last_line_width)
2000 {
2001 let expected_bases = record.line_bases.unwrap_or(last_bases);
2002 let expected_width = record.line_width.unwrap_or(last_width);
2003 if bases > expected_bases {
2004 return Err(format_at(
2005 "FASTA final sequence line is longer than the first sequence line",
2006 byte_offset,
2007 record.record_index,
2008 ));
2009 }
2010 if last_bases != expected_bases || last_width != expected_width {
2011 return Err(format_at(
2012 "non-final FASTA sequence line has inconsistent wrapping",
2013 byte_offset,
2014 record.record_index,
2015 ));
2016 }
2017 }
2018 record.len += bases;
2019 record.last_line_bases = Some(bases);
2020 record.last_line_width = Some(width);
2021 Ok(())
2022 }
2023
2024 fn finish_current(&mut self) -> Result<()> {
2025 let Some(record) = self.current.take() else {
2026 return Ok(());
2027 };
2028 let offset = record.offset.unwrap_or(self.byte_offset);
2029 let line_bases = record.line_bases.unwrap_or(0);
2030 let line_width = record.line_width.unwrap_or(0);
2031 self.entries.push(FastaIndexEntry {
2032 name: record.name,
2033 len: record.len,
2034 offset,
2035 line_bases,
2036 line_width,
2037 #[cfg(feature = "bgzf")]
2038 virtual_offset: None,
2039 });
2040 Ok(())
2041 }
2042
2043 fn finish(mut self) -> FastaIndex {
2044 let name_to_index = self
2045 .entries
2046 .iter()
2047 .enumerate()
2048 .map(|(idx, entry)| (entry.name.clone(), idx))
2049 .collect();
2050 FastaIndex {
2051 entries: std::mem::take(&mut self.entries),
2052 name_to_index,
2053 }
2054 }
2055}
2056
2057fn fasta_index_name(header: &[u8]) -> &[u8] {
2058 let name = header.strip_prefix(b">").unwrap_or(header);
2059 let end = name
2060 .iter()
2061 .position(u8::is_ascii_whitespace)
2062 .unwrap_or(name.len());
2063 &name[..end]
2064}
2065
2066pub fn visit_fasta_bytes<F>(bytes: &[u8], mut visit: F) -> Result<u64>
2075where
2076 F: FnMut(FastaVisitRecord<'_>) -> Result<()>,
2077{
2078 let mut cursor = 0;
2079 let mut pending_header = None;
2080 let mut record_index = 0;
2081 let mut folded = Vec::new();
2082
2083 while let Some((header_offset, header)) =
2084 take_next_header(bytes, &mut cursor, &mut pending_header, record_index)?
2085 {
2086 validate_header(header, header_offset, record_index)?;
2087
2088 folded.clear();
2089 let mut first_seq = None;
2090
2091 loop {
2092 if cursor >= bytes.len() {
2093 break;
2094 }
2095 let line_start = cursor as u64;
2096 let line = next_trimmed_line(bytes, &mut cursor);
2097 if line.starts_with(b">") {
2098 validate_header(line, line_start, record_index + 1)?;
2099 pending_header = Some((line_start, line));
2100 break;
2101 }
2102 if line.is_empty() {
2103 continue;
2104 }
2105
2106 first_seq = Some(line);
2107 break;
2108 }
2109
2110 let Some(first_seq) = first_seq else {
2111 visit(FastaVisitRecord {
2112 name: header,
2113 seq: b"",
2114 })?;
2115 record_index += 1;
2116 continue;
2117 };
2118
2119 if cursor >= bytes.len() || bytes[cursor] == b'>' {
2120 visit(FastaVisitRecord {
2121 name: header,
2122 seq: first_seq,
2123 })?;
2124 record_index += 1;
2125 continue;
2126 }
2127
2128 let mut seq_line_count = 1_usize;
2129 loop {
2130 if cursor >= bytes.len() {
2131 break;
2132 }
2133 let line_start = cursor as u64;
2134 let line = next_trimmed_line(bytes, &mut cursor);
2135 if line.starts_with(b">") {
2136 validate_header(line, line_start, record_index + 1)?;
2137 pending_header = Some((line_start, line));
2138 break;
2139 }
2140 if line.is_empty() {
2141 continue;
2142 }
2143
2144 seq_line_count += 1;
2145 if seq_line_count == 2 {
2146 folded.extend_from_slice(first_seq);
2147 }
2148 folded.extend_from_slice(line);
2149 }
2150
2151 let seq = if seq_line_count == 1 {
2152 first_seq
2153 } else {
2154 &folded
2155 };
2156 visit(FastaVisitRecord { name: header, seq })?;
2157 record_index += 1;
2158 }
2159
2160 Ok(record_index)
2161}
2162
2163pub fn detect_fasta_shape(bytes: &[u8]) -> Result<FastaShape> {
2169 let mut cursor = 0;
2170 let mut saw_record = false;
2171
2172 while cursor < bytes.len() {
2173 let header_offset = cursor as u64;
2174 let header = next_trimmed_line(bytes, &mut cursor);
2175 if header.is_empty() {
2176 if saw_record {
2177 return Ok(FastaShape::Multiline);
2178 }
2179 continue;
2180 }
2181 if !header.starts_with(b">") {
2182 return Err(format_at(
2183 "FASTA record header must start with `>`",
2184 header_offset,
2185 0,
2186 ));
2187 }
2188 validate_header(header, header_offset, 0)?;
2189 saw_record = true;
2190
2191 let mut seq_lines = 0_usize;
2192 while cursor < bytes.len() {
2193 let checkpoint = cursor;
2194 let line = next_trimmed_line(bytes, &mut cursor);
2195 if line.starts_with(b">") {
2196 cursor = checkpoint;
2197 break;
2198 }
2199 if line.is_empty() {
2200 return Ok(FastaShape::Multiline);
2201 }
2202 seq_lines += 1;
2203 if seq_lines > 1 {
2204 return Ok(FastaShape::Multiline);
2205 }
2206 }
2207 if seq_lines != 1 {
2208 return Ok(FastaShape::Multiline);
2209 }
2210 }
2211
2212 if saw_record {
2213 Ok(FastaShape::TwoLine)
2214 } else {
2215 Ok(FastaShape::Empty)
2216 }
2217}
2218
2219pub fn visit_fasta_bytes_auto<F>(bytes: &[u8], visit: F) -> Result<u64>
2221where
2222 F: FnMut(FastaVisitRecord<'_>) -> Result<()>,
2223{
2224 match detect_fasta_shape(bytes)? {
2225 FastaShape::TwoLine => visit_two_line_fasta_bytes(bytes, visit),
2226 FastaShape::Empty | FastaShape::Multiline => visit_fasta_bytes(bytes, visit),
2227 }
2228}
2229
2230pub fn count_two_line_fasta_bytes(bytes: &[u8]) -> Result<FastaStats> {
2237 let mut cursor = 0;
2238 let mut record_index = 0;
2239 let mut stats = FastaStats::default();
2240
2241 while cursor < bytes.len() {
2242 let header_offset = cursor as u64;
2243 if bytes[cursor] != b'>' {
2244 return Err(format_at(
2245 "two-line FASTA record header must start with `>`",
2246 header_offset,
2247 record_index,
2248 ));
2249 }
2250 let header = next_trimmed_line(bytes, &mut cursor);
2251 validate_header(header, header_offset, record_index)?;
2252 if cursor >= bytes.len() {
2253 return Err(format_at(
2254 "two-line FASTA record is missing a sequence line",
2255 cursor as u64,
2256 record_index,
2257 ));
2258 }
2259
2260 let seq_offset = cursor as u64;
2261 let seq = next_trimmed_line(bytes, &mut cursor);
2262 if seq.is_empty() || seq.starts_with(b">") {
2263 return Err(format_at(
2264 "two-line FASTA record is missing a sequence line",
2265 seq_offset,
2266 record_index,
2267 ));
2268 }
2269 if cursor < bytes.len() && bytes[cursor] != b'>' {
2270 return Err(format_at(
2271 "two-line FASTA sequence must be followed by a header",
2272 cursor as u64,
2273 record_index + 1,
2274 ));
2275 }
2276
2277 stats.observe_sequence(seq);
2278 record_index += 1;
2279 }
2280
2281 Ok(stats)
2282}
2283
2284pub fn visit_two_line_fasta_bytes<F>(bytes: &[u8], mut visit: F) -> Result<u64>
2293where
2294 F: FnMut(FastaVisitRecord<'_>) -> Result<()>,
2295{
2296 let mut cursor = 0;
2297 let mut record_index = 0;
2298
2299 while cursor < bytes.len() {
2300 let header_offset = cursor as u64;
2301 if bytes[cursor] != b'>' {
2302 return Err(format_at(
2303 "two-line FASTA record header must start with `>`",
2304 header_offset,
2305 record_index,
2306 ));
2307 }
2308 let header = next_trimmed_line(bytes, &mut cursor);
2309 validate_header(header, header_offset, record_index)?;
2310 if cursor >= bytes.len() {
2311 return Err(format_at(
2312 "two-line FASTA record is missing a sequence line",
2313 cursor as u64,
2314 record_index,
2315 ));
2316 }
2317
2318 let seq_offset = cursor as u64;
2319 let seq = next_trimmed_line(bytes, &mut cursor);
2320 if seq.is_empty() || seq.starts_with(b">") {
2321 return Err(format_at(
2322 "two-line FASTA record is missing a sequence line",
2323 seq_offset,
2324 record_index,
2325 ));
2326 }
2327 if cursor < bytes.len() && bytes[cursor] != b'>' {
2328 return Err(format_at(
2329 "two-line FASTA sequence must be followed by a header",
2330 cursor as u64,
2331 record_index + 1,
2332 ));
2333 }
2334
2335 visit(FastaVisitRecord { name: header, seq })?;
2336 record_index += 1;
2337 }
2338
2339 Ok(record_index)
2340}
2341
2342pub fn visit_two_line_fasta_read<R, F>(reader: R, visit: F) -> Result<u64>
2352where
2353 R: Read,
2354 F: FnMut(FastaVisitRecord<'_>) -> Result<()>,
2355{
2356 let mut reader = BufReader::with_capacity(TWO_LINE_STREAM_BUFFER_SIZE, reader);
2357 visit_two_line_fasta_bufread(&mut reader, visit)
2358}
2359
2360pub fn count_two_line_fasta_read<R: Read>(reader: R) -> Result<FastaStats> {
2366 let mut reader = BufReader::with_capacity(TWO_LINE_STREAM_BUFFER_SIZE, reader);
2367 count_two_line_fasta_bufread(&mut reader)
2368}
2369
2370fn visit_two_line_fasta_bufread<R, F>(reader: &mut R, mut visit: F) -> Result<u64>
2371where
2372 R: BufRead,
2373 F: FnMut(FastaVisitRecord<'_>) -> Result<()>,
2374{
2375 let mut state = TwoLineStreamState::Header;
2376 let mut record_index = 0;
2377 let mut header = Vec::new();
2378 let mut carry = Vec::new();
2379
2380 loop {
2381 let available = reader.fill_buf()?;
2382 if available.is_empty() {
2383 break;
2384 }
2385
2386 let mut consumed = 0;
2387 while consumed < available.len() {
2388 let Some(relative_newline) = memchr(b'\n', &available[consumed..]) else {
2389 carry.extend_from_slice(&available[consumed..]);
2390 consumed = available.len();
2391 break;
2392 };
2393 let line_end = consumed + relative_newline;
2394 let line = &available[consumed..line_end];
2395 process_two_line_stream_line(
2396 line,
2397 &mut carry,
2398 &mut state,
2399 &mut header,
2400 &mut record_index,
2401 &mut visit,
2402 )?;
2403 consumed = line_end + 1;
2404 }
2405 reader.consume(consumed);
2406 }
2407
2408 if !carry.is_empty() {
2409 process_two_line_stream_line(
2410 b"",
2411 &mut carry,
2412 &mut state,
2413 &mut header,
2414 &mut record_index,
2415 &mut visit,
2416 )?;
2417 }
2418
2419 match state {
2420 TwoLineStreamState::Header => Ok(record_index),
2421 TwoLineStreamState::Seq => Err(format_at(
2422 "two-line FASTA record is missing a sequence line",
2423 0,
2424 record_index,
2425 )),
2426 }
2427}
2428
2429fn count_two_line_fasta_bufread<R: BufRead>(reader: &mut R) -> Result<FastaStats> {
2430 let mut stats = FastaStats::default();
2431 let mut state = TwoLineStreamState::Header;
2432 let mut record_index = 0;
2433 let mut carry = Vec::new();
2434
2435 loop {
2436 let available = reader.fill_buf()?;
2437 if available.is_empty() {
2438 break;
2439 }
2440
2441 let mut consumed = 0;
2442 while consumed < available.len() {
2443 let Some(relative_newline) = memchr(b'\n', &available[consumed..]) else {
2444 carry.extend_from_slice(&available[consumed..]);
2445 consumed = available.len();
2446 break;
2447 };
2448 let line_end = consumed + relative_newline;
2449 let line = &available[consumed..line_end];
2450 process_two_line_count_line(
2451 line,
2452 &mut carry,
2453 &mut state,
2454 &mut record_index,
2455 &mut stats,
2456 )?;
2457 consumed = line_end + 1;
2458 }
2459 reader.consume(consumed);
2460 }
2461
2462 if !carry.is_empty() {
2463 process_two_line_count_line(b"", &mut carry, &mut state, &mut record_index, &mut stats)?;
2464 }
2465
2466 if state == TwoLineStreamState::Seq {
2467 return Err(format_at(
2468 "two-line FASTA record is missing a sequence line",
2469 0,
2470 record_index,
2471 ));
2472 }
2473 Ok(stats)
2474}
2475
2476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2477enum TwoLineStreamState {
2478 Header,
2479 Seq,
2480}
2481
2482fn process_two_line_stream_line<F>(
2483 line: &[u8],
2484 carry: &mut Vec<u8>,
2485 state: &mut TwoLineStreamState,
2486 header: &mut Vec<u8>,
2487 record_index: &mut u64,
2488 visit: &mut F,
2489) -> Result<()>
2490where
2491 F: FnMut(FastaVisitRecord<'_>) -> Result<()>,
2492{
2493 if carry.is_empty() {
2494 process_complete_two_line_stream_line(line, state, header, record_index, visit)
2495 } else {
2496 carry.extend_from_slice(line);
2497 let owned_line = trim_line(carry);
2498 process_complete_two_line_stream_line(owned_line, state, header, record_index, visit)?;
2499 carry.clear();
2500 Ok(())
2501 }
2502}
2503
2504fn process_complete_two_line_stream_line<F>(
2505 line: &[u8],
2506 state: &mut TwoLineStreamState,
2507 header: &mut Vec<u8>,
2508 record_index: &mut u64,
2509 visit: &mut F,
2510) -> Result<()>
2511where
2512 F: FnMut(FastaVisitRecord<'_>) -> Result<()>,
2513{
2514 let line = trim_line(line);
2515 if *state == TwoLineStreamState::Header {
2516 if !line.starts_with(b">") {
2517 return Err(format_at(
2518 "two-line FASTA record header must start with `>`",
2519 0,
2520 *record_index,
2521 ));
2522 }
2523 validate_header(line, 0, *record_index)?;
2524 header.clear();
2525 header.extend_from_slice(line);
2526 *state = TwoLineStreamState::Seq;
2527 return Ok(());
2528 }
2529
2530 if line.is_empty() || line.starts_with(b">") {
2531 return Err(format_at(
2532 "two-line FASTA record is missing a sequence line",
2533 0,
2534 *record_index,
2535 ));
2536 }
2537 visit(FastaVisitRecord {
2538 name: header,
2539 seq: line,
2540 })?;
2541 *record_index += 1;
2542 *state = TwoLineStreamState::Header;
2543 Ok(())
2544}
2545
2546fn process_two_line_count_line(
2547 line: &[u8],
2548 carry: &mut Vec<u8>,
2549 state: &mut TwoLineStreamState,
2550 record_index: &mut u64,
2551 stats: &mut FastaStats,
2552) -> Result<()> {
2553 if carry.is_empty() {
2554 process_complete_two_line_count_line(line, state, record_index, stats)
2555 } else {
2556 carry.extend_from_slice(line);
2557 let owned_line = trim_line(carry);
2558 process_complete_two_line_count_line(owned_line, state, record_index, stats)?;
2559 carry.clear();
2560 Ok(())
2561 }
2562}
2563
2564fn process_complete_two_line_count_line(
2565 line: &[u8],
2566 state: &mut TwoLineStreamState,
2567 record_index: &mut u64,
2568 stats: &mut FastaStats,
2569) -> Result<()> {
2570 let line = trim_line(line);
2571 if *state == TwoLineStreamState::Header {
2572 if !line.starts_with(b">") {
2573 return Err(format_at(
2574 "two-line FASTA record header must start with `>`",
2575 0,
2576 *record_index,
2577 ));
2578 }
2579 validate_header(line, 0, *record_index)?;
2580 *state = TwoLineStreamState::Seq;
2581 return Ok(());
2582 }
2583
2584 if line.is_empty() || line.starts_with(b">") {
2585 return Err(format_at(
2586 "two-line FASTA record is missing a sequence line",
2587 0,
2588 *record_index,
2589 ));
2590 }
2591 stats.observe_sequence(line);
2592 *record_index += 1;
2593 *state = TwoLineStreamState::Header;
2594 Ok(())
2595}
2596
2597fn take_next_header<'a>(
2598 bytes: &'a [u8],
2599 cursor: &mut usize,
2600 pending_header: &mut Option<(u64, &'a [u8])>,
2601 record_index: u64,
2602) -> Result<Option<(u64, &'a [u8])>> {
2603 if let Some(header) = pending_header.take() {
2604 return Ok(Some(header));
2605 }
2606
2607 while *cursor < bytes.len() {
2608 let line_start = *cursor as u64;
2609 let line = next_trimmed_line(bytes, cursor);
2610 if line.is_empty() {
2611 continue;
2612 }
2613 if !line.starts_with(b">") {
2614 return Err(format_at(
2615 "FASTA record header must start with `>`",
2616 line_start,
2617 record_index,
2618 ));
2619 }
2620 return Ok(Some((line_start, line)));
2621 }
2622
2623 Ok(None)
2624}
2625
2626fn next_trimmed_line<'a>(bytes: &'a [u8], cursor: &mut usize) -> &'a [u8] {
2627 let start = *cursor;
2628 match memchr(b'\n', &bytes[start..]) {
2629 Some(relative) => {
2630 let end = start + relative;
2631 *cursor = end + 1;
2632 trim_line(&bytes[start..end])
2633 }
2634 None => {
2635 *cursor = bytes.len();
2636 trim_line(&bytes[start..])
2637 }
2638 }
2639}
2640
2641fn trim_line(line: &[u8]) -> &[u8] {
2642 let line = line.strip_suffix(b"\n").unwrap_or(line);
2643 line.strip_suffix(b"\r").unwrap_or(line)
2644}
2645
2646fn validate_header(header: &[u8], byte_offset: u64, record_index: u64) -> Result<()> {
2647 if header.len() == 1 || fasta_index_name(header).is_empty() {
2648 return Err(format_at("empty FASTA id", byte_offset, record_index));
2649 }
2650 Ok(())
2651}
2652
2653fn id_token_range(bytes: &[u8], name: Range<u32>) -> Result<ByteRange> {
2654 let name_range = to_usize(name.clone());
2655 let header = bytes
2656 .get(name_range)
2657 .ok_or_else(|| FastqError::Format("FASTA header byte range exceeds batch buffer".into()))?;
2658 let without_gt = header.strip_prefix(b">").unwrap_or(header);
2659 let token_end = without_gt
2660 .iter()
2661 .position(u8::is_ascii_whitespace)
2662 .unwrap_or(without_gt.len());
2663 let start = name.start + u32::from(header.starts_with(b">"));
2664 let token_end_u32 = u32::try_from(token_end)
2665 .map_err(|_| FastqError::Format("FASTA id token range exceeds u32 range".into()))?;
2666 let end = start
2667 .checked_add(token_end_u32)
2668 .ok_or_else(|| FastqError::Format("FASTA id token range overflowed".into()))?;
2669 Ok(ByteRange { start, end })
2670}
2671
2672fn checked_u32(value: usize) -> Result<u32> {
2673 u32::try_from(value)
2674 .map_err(|_| FastqError::Format("FASTA batch byte offsets exceed u32 range".into()))
2675}
2676
2677fn format_at(message: impl Into<String>, byte_offset: u64, record_index: u64) -> FastqError {
2678 FastqError::FormatAt {
2679 message: message.into(),
2680 position: FastqPosition::new(byte_offset, record_index, 0),
2681 }
2682}
2683
2684fn to_usize(range: Range<u32>) -> Range<usize> {
2685 range.start as usize..range.end as usize
2686}
2687
2688#[cfg(test)]
2689mod tests;