1use std::sync::{Arc, OnceLock};
18
19use bytes::Bytes;
20
21use crate::error::{Error, Result};
22
23pub const RECORD_HEADER_SIZE: usize = 36;
25
26#[derive(Debug, Clone, Copy)]
28pub struct RecordCore {
29 pub ref_id: i32,
30 pub pos: i32,
31 pub mapq: u8,
32 pub bai_bin: u16,
33 pub n_cigar_op: u16,
34 pub flag: u16,
35 pub l_seq: i32,
36 pub next_ref_id: i32,
37 pub next_pos: i32,
38 pub tlen: i32,
39}
40
41#[derive(Debug, Clone, Copy)]
43struct Layout {
44 name: usize,
45 cigar: usize,
46 seq: usize,
47 qual: usize,
48 tags: usize,
49 end: usize,
50}
51
52#[derive(Debug, Clone, PartialEq)]
54pub enum TagValue {
55 Char(char),
56 Int(i64),
57 Float(f32),
58 Str(String),
59 IntArray(Vec<i64>),
60 FloatArray(Vec<f32>),
61}
62
63pub struct BamRecord {
64 raw: Bytes,
66 core: RecordCore,
67 layout: Layout,
68 end: i64,
71 cigar_ops: Vec<u32>,
72 has_tags: bool,
74 cigar: OnceLock<String>,
75 sequence: OnceLock<String>,
76 qualities: OnceLock<String>,
77 tags: OnceLock<Result<Vec<(String, TagValue)>>>,
80 chr_names: Arc<Vec<String>>,
82}
83
84impl std::fmt::Debug for BamRecord {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_struct("BamRecord")
87 .field("chr", &self.chr())
88 .field("start", &self.start())
89 .field("end", &self.end)
90 .field("flag", &self.core.flag)
91 .finish()
92 }
93}
94
95impl BamRecord {
96 fn name_of(&self, index: i32) -> &str {
97 if index < 0 {
98 return "*";
99 }
100 self.chr_names
101 .get(index as usize)
102 .map(String::as_str)
103 .unwrap_or("*")
104 }
105
106 pub fn chr(&self) -> &str {
107 self.name_of(self.core.ref_id)
108 }
109
110 pub fn start(&self) -> i64 {
111 self.core.pos as i64
112 }
113
114 pub fn end(&self) -> i64 {
117 self.end
118 }
119
120 pub fn read_name(&self) -> &str {
121 let bytes = &self.raw[self.layout.name..self.layout.cigar];
122 let bytes = bytes.strip_suffix(&[0]).unwrap_or(bytes);
124 match std::str::from_utf8(bytes) {
131 Ok(name) => name,
132 Err(e) => std::str::from_utf8(&bytes[..e.valid_up_to()]).unwrap_or(""),
133 }
134 }
135
136 pub fn flag(&self) -> u16 {
137 self.core.flag
138 }
139
140 pub fn mapping_quality(&self) -> u8 {
141 self.core.mapq
142 }
143
144 pub fn bai_bin(&self) -> u16 {
145 self.core.bai_bin
146 }
147
148 pub fn next_chr(&self) -> &str {
149 self.name_of(self.core.next_ref_id)
150 }
151
152 pub fn next_start(&self) -> i64 {
153 self.core.next_pos as i64
154 }
155
156 pub fn template_length(&self) -> i64 {
157 self.core.tlen as i64
158 }
159
160 pub fn cigar(&self) -> &str {
167 self.cigar.get_or_init(|| decode_cigar(&self.cigar_ops))
168 }
169
170 pub fn reference_length(&self) -> i64 {
172 reference_length(&self.cigar_ops)
173 }
174
175 pub fn query_length(&self) -> i64 {
177 query_length(&self.cigar_ops)
178 }
179
180 pub fn sequence(&self) -> &str {
183 self.sequence.get_or_init(|| {
184 decode_sequence(
185 self.core.l_seq as i64,
186 &self.raw[self.layout.seq..self.layout.qual],
187 )
188 })
189 }
190
191 pub fn qualities(&self) -> &str {
194 self.qualities
195 .get_or_init(|| decode_qualities(&self.raw[self.layout.qual..self.layout.tags]))
196 }
197
198 pub fn tags(&self) -> Result<&[(String, TagValue)]> {
203 if !self.has_tags {
204 return Ok(&[]);
205 }
206 let parsed = self
207 .tags
208 .get_or_init(|| parse_tags(&self.raw[self.layout.tags..self.layout.end]));
209 match parsed {
210 Ok(tags) => Ok(tags),
211 Err(e) => Err(Error::corrupt(
221 format!("{}:{}-{}", self.chr(), self.start(), self.end()),
222 self.core.pos.max(0) as u64,
223 format!("alignment {}: {e}", self.read_name()),
224 )),
225 }
226 }
227
228 pub fn is_paired(&self) -> bool {
231 self.core.flag & 0x001 != 0
232 }
233 pub fn is_proper_pair(&self) -> bool {
234 self.core.flag & 0x002 != 0
235 }
236 pub fn is_mapped(&self) -> bool {
237 self.core.flag & 0x004 == 0
238 }
239 pub fn is_next_mapped(&self) -> bool {
240 self.core.flag & 0x008 == 0
241 }
242 pub fn is_reverse(&self) -> bool {
243 self.core.flag & 0x010 != 0
244 }
245 pub fn is_next_reverse(&self) -> bool {
246 self.core.flag & 0x020 != 0
247 }
248 pub fn is_first_in_pair(&self) -> bool {
249 self.core.flag & 0x040 != 0
250 }
251 pub fn is_last_in_pair(&self) -> bool {
252 self.core.flag & 0x080 != 0
253 }
254 pub fn is_secondary_or_supplementary(&self) -> bool {
255 self.core.flag & (0x100 | 0x800) != 0
256 }
257 pub fn is_failed_qc_or_duplicate(&self) -> bool {
258 self.core.flag & (0x200 | 0x400) != 0
259 }
260}
261
262fn reference_length(ops: &[u32]) -> i64 {
264 ops.iter()
265 .filter(|op| matches!(*op & 0xF, 0 | 2 | 3 | 7 | 8))
266 .map(|op| (op >> 4) as i64)
267 .sum()
268}
269
270fn query_length(ops: &[u32]) -> i64 {
272 ops.iter()
273 .filter(|op| matches!(*op & 0xF, 0 | 1 | 4 | 7 | 8))
274 .map(|op| (op >> 4) as i64)
275 .sum()
276}
277
278fn decode_cigar(ops: &[u32]) -> String {
279 const OPS: &[u8] = b"MIDNSHP=X";
280 let mut out = String::with_capacity(ops.len() * 4);
281 for op in ops {
282 use std::fmt::Write as _;
283 let _ = write!(out, "{}", op >> 4);
284 out.push(match OPS.get((op & 0xF) as usize) {
285 Some(c) => *c as char,
286 None => '?',
287 });
288 }
289 out
290}
291
292fn decode_sequence(l_seq: i64, packed: &[u8]) -> String {
293 if l_seq <= 0 {
296 return "*".to_string();
297 }
298 const LOOKUP: &[u8] = b"=ACMGRSVTWYHKDBN";
299 let len = l_seq.min(packed.len() as i64 * 2) as usize;
303 let mut out = String::with_capacity(len);
304 for i in 0..len {
305 let byte = packed[i / 2];
306 let code = if i % 2 == 0 { byte >> 4 } else { byte & 0xF };
307 out.push(LOOKUP[code as usize] as char);
308 }
309 out
310}
311
312fn decode_qualities(quals: &[u8]) -> String {
313 if quals.iter().all(|q| *q == 0xFF) {
318 return "*".to_string();
319 }
320 quals
325 .iter()
326 .map(|q| {
327 if *q == 0xFF {
328 '*'
329 } else {
330 char::from_u32(*q as u32 + 33).unwrap_or('?')
331 }
332 })
333 .collect()
334}
335
336fn parse_tags(raw: &[u8]) -> Result<Vec<(String, TagValue)>> {
342 let mut out = Vec::new();
343 let mut at = 0usize;
344 let size = raw.len();
345 let bad = |what: String| Error::invalid(what);
349
350 while at < size {
351 if at + 3 > size {
352 return Err(bad(
353 "truncated bam tag (no room for its tag and type)".into()
354 ));
355 }
356 let tag = String::from_utf8_lossy(&raw[at..at + 2]).into_owned();
359 let kind = raw[at + 2];
360 at += 3;
361
362 let value = match kind {
363 b'B' => {
364 if at + 5 > size {
365 return Err(bad(format!("truncated bam array tag {tag}")));
366 }
367 let subtype = raw[at];
368 let count = u32::from_le_bytes([raw[at + 1], raw[at + 2], raw[at + 3], raw[at + 4]])
369 as usize;
370 at += 5;
371 let element = match subtype {
372 b'c' | b'C' => 1usize,
373 b's' | b'S' => 2,
374 b'i' | b'I' | b'f' => 4,
375 other => {
376 return Err(bad(format!(
377 "unsupported bam array tag subtype {}",
378 other as char
379 )))
380 }
381 };
382 let bytes = count
383 .checked_mul(element)
384 .filter(|n| at + n <= size)
385 .ok_or_else(|| bad(format!("bam array tag {tag} runs past its record")))?;
386 let data = &raw[at..at + bytes];
387 at += bytes;
388 read_array(subtype, data)
389 }
390 b'Z' | b'H' => {
391 let start = at;
394 while at < size && raw[at] != 0 {
395 at += 1;
396 }
397 if at >= size {
398 return Err(bad(format!("unterminated bam string tag {tag}")));
399 }
400 let value = String::from_utf8_lossy(&raw[start..at]).into_owned();
401 at += 1;
402 TagValue::Str(value)
403 }
404 other => {
405 let width = match other {
406 b'A' | b'c' | b'C' => 1usize,
407 b's' | b'S' => 2,
408 b'i' | b'I' | b'f' => 4,
409 _ => return Err(bad(format!("unsupported tag type {}", other as char))),
410 };
411 if at + width > size {
412 return Err(bad(format!("bam tag {tag} runs past its record")));
413 }
414 let data = &raw[at..at + width];
415 at += width;
416 read_scalar(other, data)
417 }
418 };
419 out.push((tag, value));
420 }
421 Ok(out)
422}
423
424fn read_scalar(kind: u8, data: &[u8]) -> TagValue {
425 match kind {
426 b'A' => TagValue::Char(data[0] as char),
427 b'c' => TagValue::Int(data[0] as i8 as i64),
428 b'C' => TagValue::Int(data[0] as i64),
429 b's' => TagValue::Int(i16::from_le_bytes([data[0], data[1]]) as i64),
430 b'S' => TagValue::Int(u16::from_le_bytes([data[0], data[1]]) as i64),
431 b'i' => TagValue::Int(i32::from_le_bytes([data[0], data[1], data[2], data[3]]) as i64),
432 b'I' => TagValue::Int(u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as i64),
433 _ => TagValue::Float(f32::from_le_bytes([data[0], data[1], data[2], data[3]])),
434 }
435}
436
437fn read_array(subtype: u8, data: &[u8]) -> TagValue {
438 match subtype {
439 b'f' => TagValue::FloatArray(
440 data.chunks_exact(4)
441 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
442 .collect(),
443 ),
444 b'c' => TagValue::IntArray(data.iter().map(|b| *b as i8 as i64).collect()),
445 b'C' => TagValue::IntArray(data.iter().map(|b| *b as i64).collect()),
446 b's' => TagValue::IntArray(
447 data.chunks_exact(2)
448 .map(|c| i16::from_le_bytes([c[0], c[1]]) as i64)
449 .collect(),
450 ),
451 b'S' => TagValue::IntArray(
452 data.chunks_exact(2)
453 .map(|c| u16::from_le_bytes([c[0], c[1]]) as i64)
454 .collect(),
455 ),
456 b'i' => TagValue::IntArray(
457 data.chunks_exact(4)
458 .map(|c| i32::from_le_bytes([c[0], c[1], c[2], c[3]]) as i64)
459 .collect(),
460 ),
461 _ => TagValue::IntArray(
462 data.chunks_exact(4)
463 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) as i64)
464 .collect(),
465 ),
466 }
467}
468
469fn is_long_cigar_placeholder(ops: &[u32], l_seq: i32) -> bool {
477 ops.len() == 2
478 && (ops[0] & 0xF) == 4 && (ops[0] >> 4) as i64 == l_seq as i64
480 && (ops[1] & 0xF) == 3 }
482
483fn read_long_cigar(tag_bytes: &[u8]) -> Vec<u32> {
494 let Ok(tags) = parse_tags(tag_bytes) else {
495 return Vec::new();
496 };
497 for (tag, value) in tags {
498 if tag != "CG" {
499 continue;
500 }
501 if let TagValue::IntArray(values) = value {
502 return values.into_iter().map(|v| v as u32).collect();
503 }
504 }
505 Vec::new()
506}
507
508#[derive(Debug, Clone, Copy)]
512pub struct RecordFilter {
513 pub enabled: bool,
514}
515
516impl Default for RecordFilter {
517 fn default() -> Self {
518 Self { enabled: true }
519 }
520}
521
522#[derive(Debug, Clone, Copy)]
524pub struct EntryFilter {
525 pub chr_index: Option<i32>,
527 pub start: i64,
528 pub end: Option<i64>,
530 pub standard_flags: bool,
531}
532
533impl EntryFilter {
534 pub fn accepts(&self, chr_index: i32, start: i64, end: i64, flag: u16) -> bool {
535 if let Some(wanted) = self.chr_index {
536 if chr_index != wanted {
537 return false;
538 }
539 }
540 if let Some(region_end) = self.end {
541 if region_end <= self.start {
543 return false;
544 }
545 if start >= region_end || end.max(start + 1) <= self.start {
551 return false;
552 }
553 }
554 if self.standard_flags {
555 if flag & 0x004 != 0 {
556 return false; }
558 if flag & 0x001 != 0 && flag & 0x002 == 0 {
559 return false; }
561 if flag & (0x100 | 0x800) != 0 {
562 return false; }
564 if flag & (0x200 | 0x400) != 0 {
565 return false; }
567 }
568 true
569 }
570}
571
572pub fn decode_block(
578 block: &Bytes,
579 parse_tags_flag: bool,
580 filter: &EntryFilter,
581 chr_names: &Arc<Vec<String>>,
582 path: &str,
583) -> Result<Vec<BamRecord>> {
584 let size = block.len();
585 let mut at = 0usize;
586 let mut out = Vec::new();
587
588 while at < size {
589 if at + 4 > size {
590 return Err(Error::corrupt(
591 path,
592 at as u64,
593 "truncated bam record (no room for its length)",
594 ));
595 }
596 let block_size =
597 u32::from_le_bytes([block[at], block[at + 1], block[at + 2], block[at + 3]]) as usize;
598 let record_end = at + 4 + block_size;
599 if block_size < RECORD_HEADER_SIZE - 4 || record_end > size {
600 return Err(Error::corrupt(
601 path,
602 at as u64,
603 format!(
604 "truncated bam record (declares {block_size} bytes, {} left in the block)",
605 size - at - 4
606 ),
607 ));
608 }
609
610 let r = &block[at..record_end];
611 let i32_at = |o: usize| i32::from_le_bytes([r[o], r[o + 1], r[o + 2], r[o + 3]]);
612 let u16_at = |o: usize| u16::from_le_bytes([r[o], r[o + 1]]);
613 let core = RecordCore {
614 ref_id: i32_at(4),
615 pos: i32_at(8),
616 mapq: r[13],
617 bai_bin: u16_at(14),
618 n_cigar_op: u16_at(16),
619 flag: u16_at(18),
620 l_seq: i32_at(20),
621 next_ref_id: i32_at(24),
622 next_pos: i32_at(28),
623 tlen: i32_at(32),
624 };
625 let l_read_name = r[12] as usize;
626
627 let name = RECORD_HEADER_SIZE;
630 let cigar = name + l_read_name;
631 let seq = cigar + core.n_cigar_op as usize * 4;
632 let qual = seq + (core.l_seq as usize).div_ceil(2);
633 let tags = qual + core.l_seq.max(0) as usize;
634 if l_read_name < 1 || core.l_seq < 0 || tags > r.len() {
635 return Err(Error::corrupt(
636 path,
637 at as u64,
638 format!(
639 "bam record at {at} declares fields that do not fit its {block_size} bytes"
640 ),
641 ));
642 }
643 let layout = Layout {
644 name,
645 cigar,
646 seq,
647 qual,
648 tags,
649 end: r.len(),
650 };
651
652 let mut ops: Vec<u32> = r[cigar..seq]
653 .chunks_exact(4)
654 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
655 .collect();
656 let mut end = core.pos as i64 + reference_length(&ops);
657
658 if !filter.accepts(core.ref_id, core.pos as i64, end, core.flag) {
661 at = record_end;
662 continue;
663 }
664
665 if is_long_cigar_placeholder(&ops, core.l_seq) {
670 let real = read_long_cigar(&r[tags..]);
671 if !real.is_empty() {
672 end = core.pos as i64 + reference_length(&real);
673 ops = real;
674 }
675 }
676
677 out.push(BamRecord {
678 raw: block.slice(at..record_end),
679 core,
680 layout,
681 end,
682 cigar_ops: ops,
683 has_tags: parse_tags_flag,
684 cigar: OnceLock::new(),
685 sequence: OnceLock::new(),
686 qualities: OnceLock::new(),
687 tags: OnceLock::new(),
688 chr_names: chr_names.clone(),
689 });
690 at = record_end;
691 }
692 Ok(out)
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698
699 fn names() -> Arc<Vec<String>> {
700 Arc::new(vec!["chr1".to_string(), "chr2".to_string()])
701 }
702
703 #[allow(clippy::too_many_arguments)]
705 fn record(
706 ref_id: i32,
707 pos: i32,
708 flag: u16,
709 name: &str,
710 cigar: &[u32],
711 seq: &[u8],
712 quals: &[u8],
713 tags: &[u8],
714 ) -> Vec<u8> {
715 let l_seq = seq.len() as i32;
716 let packed: Vec<u8> = seq
717 .chunks(2)
718 .map(|pair| {
719 let hi = base_code(pair[0]);
720 let lo = pair.get(1).map(|b| base_code(*b)).unwrap_or(0);
721 (hi << 4) | lo
722 })
723 .collect();
724
725 let mut body = Vec::new();
726 body.extend_from_slice(&ref_id.to_le_bytes());
727 body.extend_from_slice(&pos.to_le_bytes());
728 body.push(name.len() as u8 + 1);
729 body.push(60); body.extend_from_slice(&0u16.to_le_bytes()); body.extend_from_slice(&(cigar.len() as u16).to_le_bytes());
732 body.extend_from_slice(&flag.to_le_bytes());
733 body.extend_from_slice(&l_seq.to_le_bytes());
734 body.extend_from_slice(&(-1i32).to_le_bytes()); body.extend_from_slice(&(-1i32).to_le_bytes()); body.extend_from_slice(&0i32.to_le_bytes()); body.extend_from_slice(name.as_bytes());
738 body.push(0);
739 for op in cigar {
740 body.extend_from_slice(&op.to_le_bytes());
741 }
742 body.extend_from_slice(&packed);
743 body.extend_from_slice(quals);
744 body.extend_from_slice(tags);
745
746 let mut out = ((body.len()) as u32).to_le_bytes().to_vec();
747 out.extend_from_slice(&body);
748 out
749 }
750
751 fn base_code(base: u8) -> u8 {
752 b"=ACMGRSVTWYHKDBN"
753 .iter()
754 .position(|b| *b == base)
755 .unwrap_or(15) as u8
756 }
757
758 fn op(len: u32, kind: u32) -> u32 {
759 (len << 4) | kind
760 }
761
762 fn any() -> EntryFilter {
763 EntryFilter {
764 chr_index: None,
765 start: 0,
766 end: None,
767 standard_flags: false,
768 }
769 }
770
771 fn decode(bytes: Vec<u8>, tags: bool) -> Vec<BamRecord> {
772 decode_block(&Bytes::from(bytes), tags, &any(), &names(), "test").unwrap()
773 }
774
775 #[test]
776 fn a_record_decodes_to_its_documented_fields() {
777 let bytes = record(
778 0,
779 100,
780 0x10,
781 "read1",
782 &[op(5, 4), op(80, 0)],
783 b"ACGTA",
784 &[30; 5],
785 &[],
786 );
787 let records = decode(bytes, false);
788 assert_eq!(records.len(), 1);
789 let r = &records[0];
790 assert_eq!(r.chr(), "chr1");
791 assert_eq!(r.start(), 100);
792 assert_eq!(r.end(), 180); assert_eq!(r.read_name(), "read1");
794 assert_eq!(r.cigar(), "5S80M");
795 assert_eq!(r.sequence(), "ACGTA");
796 assert_eq!(r.qualities(), "?????");
798 assert_eq!(r.mapping_quality(), 60);
799 assert_eq!(r.reference_length(), 80);
800 assert_eq!(r.query_length(), 85);
801 assert!(r.is_reverse() && !r.is_paired());
802 assert_eq!(r.next_chr(), "*");
803 }
804
805 #[test]
806 fn an_absent_sequence_and_absent_qualities_read_as_a_star() {
807 let bytes = record(0, 10, 0, "r", &[op(10, 0)], b"", &[], &[]);
808 let records = decode(bytes, false);
809 assert_eq!(records[0].sequence(), "*");
810 assert_eq!(records[0].qualities(), "*");
811
812 let bytes = record(0, 10, 0, "r", &[op(4, 0)], b"ACGT", &[0xFF; 4], &[]);
814 assert_eq!(decode(bytes, false)[0].qualities(), "*");
815 }
816
817 #[test]
818 fn one_missing_quality_among_real_ones_is_a_star_in_place() {
819 let bytes = record(
820 0,
821 10,
822 0,
823 "r",
824 &[op(4, 0)],
825 b"ACGT",
826 &[30, 0xFF, 30, 30],
827 &[],
828 );
829 let quals = decode(bytes, false)[0].qualities().to_string();
830 assert_eq!(quals.chars().nth(1), Some('*'));
831 assert_eq!(quals.len(), 4);
832 }
833
834 #[test]
835 fn an_odd_length_sequence_does_not_decode_its_padding() {
836 let bytes = record(0, 10, 0, "r", &[op(3, 0)], b"ACG", &[30; 3], &[]);
837 assert_eq!(decode(bytes, false)[0].sequence(), "ACG");
838 }
839
840 #[test]
841 fn tags_decode_to_their_declared_types_in_file_order() {
842 let mut tags = Vec::new();
843 tags.extend_from_slice(b"NMi");
844 tags.extend_from_slice(&3i32.to_le_bytes());
845 tags.extend_from_slice(b"RGZgroup1\0");
846 tags.extend_from_slice(b"XAA");
847 tags.push(b'x');
848 tags.extend_from_slice(b"XFf");
849 tags.extend_from_slice(&1.5f32.to_le_bytes());
850 tags.extend_from_slice(b"XBBc");
851 tags.extend_from_slice(&3u32.to_le_bytes());
852 tags.extend_from_slice(&[1u8, 2, 253]);
853
854 let bytes = record(0, 10, 0, "r", &[op(4, 0)], b"ACGT", &[30; 4], &tags);
855 let records = decode(bytes, true);
856 let got = records[0].tags().unwrap();
857 assert_eq!(got[0], ("NM".into(), TagValue::Int(3)));
858 assert_eq!(got[1], ("RG".into(), TagValue::Str("group1".into())));
859 assert_eq!(got[2], ("XA".into(), TagValue::Char('x')));
860 assert_eq!(got[3], ("XF".into(), TagValue::Float(1.5)));
861 assert_eq!(got[4], ("XB".into(), TagValue::IntArray(vec![1, 2, -3])));
863 }
864
865 #[test]
866 fn tags_off_means_no_tags_rather_than_an_error() {
867 let mut tags = b"NMi".to_vec();
868 tags.extend_from_slice(&3i32.to_le_bytes());
869 let bytes = record(0, 10, 0, "r", &[op(4, 0)], b"ACGT", &[30; 4], &tags);
870 assert!(decode(bytes, false)[0].tags().unwrap().is_empty());
871 }
872
873 #[test]
874 fn a_record_with_malformed_tags_reads_fine_and_fails_at_tags() {
875 let tags = b"RGZunterminated".to_vec();
877 let bytes = record(0, 10, 0, "r", &[op(4, 0)], b"ACGT", &[30; 4], &tags);
878 let records = decode(bytes, true);
879 assert_eq!(records[0].start(), 10);
881 assert_eq!(records[0].sequence(), "ACGT");
882 let err = records[0].tags().unwrap_err().to_string();
883 assert!(err.contains("unterminated"), "{err}");
884 }
885
886 #[test]
887 fn a_long_cigar_is_read_from_its_cg_tag_and_the_tag_is_kept() {
888 let real = [op(4, 0), op(6, 2)]; let mut tags = b"CGBI".to_vec();
891 tags.extend_from_slice(&(real.len() as u32).to_le_bytes());
892 for o in real {
893 tags.extend_from_slice(&o.to_le_bytes());
894 }
895 let placeholder = [op(4, 4), op(10, 3)]; let bytes = record(0, 50, 0, "r", &placeholder, b"ACGT", &[30; 4], &tags);
897
898 let records = decode(bytes, true);
899 assert_eq!(records[0].cigar(), "4M6D", "the placeholder was returned");
900 assert_eq!(records[0].end(), 60);
901 assert!(records[0]
903 .tags()
904 .unwrap()
905 .iter()
906 .any(|(tag, _)| tag == "CG"));
907 }
908
909 #[test]
910 fn the_standard_filter_drops_what_it_documents() {
911 let filter = EntryFilter {
912 chr_index: None,
913 start: 0,
914 end: None,
915 standard_flags: true,
916 };
917 assert!(filter.accepts(0, 10, 20, 0x002)); assert!(filter.accepts(0, 10, 20, 0)); assert!(!filter.accepts(0, 10, 20, 0x004)); assert!(!filter.accepts(0, 10, 20, 0x001)); assert!(!filter.accepts(0, 10, 20, 0x100)); assert!(!filter.accepts(0, 10, 20, 0x800)); assert!(!filter.accepts(0, 10, 20, 0x200)); assert!(!filter.accepts(0, 10, 20, 0x400)); }
926
927 #[test]
928 fn an_alignment_covering_no_reference_still_overlaps_a_window_on_it() {
929 let filter = EntryFilter {
932 chr_index: Some(0),
933 start: 100,
934 end: Some(200),
935 standard_flags: false,
936 };
937 assert!(filter.accepts(0, 100, 100, 0), "a window starting on it");
938 assert!(filter.accepts(0, 150, 150, 0));
939 assert!(!filter.accepts(0, 99, 99, 0));
940 assert!(!filter.accepts(0, 200, 200, 0));
941 }
942
943 #[test]
944 fn an_empty_region_overlaps_nothing() {
945 let filter = EntryFilter {
946 chr_index: Some(0),
947 start: 100,
948 end: Some(100),
949 standard_flags: false,
950 };
951 assert!(!filter.accepts(0, 100, 200, 0));
952 }
953
954 #[test]
955 fn a_truncated_record_is_corrupt_not_a_panic() {
956 let mut bytes = record(0, 10, 0, "r", &[op(4, 0)], b"ACGT", &[30; 4], &[]);
957 bytes.truncate(bytes.len() - 6);
958 let err = decode_block(&Bytes::from(bytes), false, &any(), &names(), "test")
959 .unwrap_err()
960 .to_string();
961 assert!(err.contains("truncated bam record"), "{err}");
962 }
963
964 #[test]
965 fn a_record_declaring_fields_past_its_own_length_is_refused() {
966 let mut bytes = record(0, 10, 0, "r", &[op(4, 0)], b"ACGT", &[30; 4], &[]);
967 bytes[16 + 4] = 244;
969 bytes[17 + 4] = 1;
970 let err = decode_block(&Bytes::from(bytes), false, &any(), &names(), "test")
971 .unwrap_err()
972 .to_string();
973 assert!(err.contains("do not fit"), "{err}");
974 }
975
976 #[test]
977 fn several_records_in_one_block_all_decode() {
978 let mut bytes = record(0, 10, 0, "a", &[op(4, 0)], b"ACGT", &[30; 4], &[]);
979 bytes.extend(record(0, 20, 0, "b", &[op(4, 0)], b"TGCA", &[31; 4], &[]));
980 bytes.extend(record(1, 30, 0, "c", &[op(4, 0)], b"GGGG", &[32; 4], &[]));
981 let records = decode(bytes, false);
982 assert_eq!(records.len(), 3);
983 assert_eq!(records[2].chr(), "chr2");
984 assert_eq!(records[1].read_name(), "b");
985 }
986}