ciri_toolkit/sam_bam.rs
1//! Multi-format I/O module: SAM and BAM support.
2//!
3//! This module provides utilities for detecting the format of genomic alignment files
4//! and validating their sort order. CIRI-toolkit requires Name-sorted or Unsorted
5//! inputs for correct BSJ identification.
6
7use anyhow::{anyhow, Result};
8use noodles::bam;
9use std::fs::File;
10use std::io::Read;
11
12/// Supported input alignment formats.
13pub enum InputFormat {
14 /// Sequence Alignment Map (Text-based)
15 Sam,
16 /// Binary Alignment Map (BGZF-compressed)
17 Bam,
18}
19
20/// Detects the alignment-file format from the leading magic bytes.
21///
22/// This check is intentionally lightweight because it runs before any expensive
23/// validation or parser setup.
24///
25/// # Arguments
26/// * `path` - Path to the input file.
27///
28/// # Returns
29/// `Ok(InputFormat::Bam)` for BAM/BGZF, `Ok(InputFormat::Sam)` otherwise.
30pub fn detect_format(path: &str) -> Result<InputFormat> {
31 let mut file = File::open(path)?;
32 let mut magic = [0u8; 4];
33 // Attempt to read the first 4 bytes.
34 if file.read_exact(&mut magic).is_err() {
35 return Ok(InputFormat::Sam); // Small file or header-only
36 }
37
38 // BAM files start with "BAM\x01". BGZF files start with GZIP magic + extra field.
39 if &magic == b"BAM\x01" || &magic == b"\x1f\x8b\x08\x04" {
40 Ok(InputFormat::Bam)
41 } else {
42 Ok(InputFormat::Sam)
43 }
44}
45
46/// Validates that a BAM file is not coordinate-sorted.
47///
48/// CIRI-toolkit's logic depends on Read 1 and Read 2 being close to each other in the file.
49/// Coordinate-sorted files scatter read pairs across the genome, breaking the streaming logic.
50/// The check is conservative: false negatives are preferable to silently running
51/// the pipeline on a definitely unsupported ordering.
52///
53/// # Arguments
54/// * `path` - Path to the BAM file.
55pub fn check_bam_sorting(path: &str) -> Result<()> {
56 let file = File::open(path)?;
57 let mut reader = bam::io::Reader::new(file);
58 let header = reader.read_header()?;
59
60 // Convert header to string for robust sort-order check.
61 let header_str = format!("{:?}", header);
62 if header_str.contains("SO:coordinate") || header_str.contains("SortOrder(coordinate)") {
63 return Err(anyhow!("Coordinate-sorted BAM detected. CIRI-toolkit requires Name-sorted or Unsorted BAM. Please use 'samtools sort -n' first."));
64 }
65 Ok(())
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use std::io::Write;
72 use tempfile::NamedTempFile;
73
74 #[test]
75 fn test_detect_sam() {
76 let mut file = NamedTempFile::new().unwrap();
77 writeln!(file, "@HD\tVN:1.6\tSO:queryname").unwrap();
78 assert!(matches!(
79 detect_format(file.path().to_str().unwrap()).unwrap(),
80 InputFormat::Sam
81 ));
82 }
83
84 #[test]
85 fn test_detect_bam_bgzf() {
86 let mut file = NamedTempFile::new().unwrap();
87 // Standard BGZF magic bytes.
88 file.write_all(&[0x1f, 0x8b, 0x08, 0x04]).unwrap();
89 assert!(matches!(
90 detect_format(file.path().to_str().unwrap()).unwrap(),
91 InputFormat::Bam
92 ));
93 }
94
95 #[test]
96 fn test_detect_bam_magic() {
97 let mut file = NamedTempFile::new().unwrap();
98 // Standard BAM magic bytes.
99 file.write_all(b"BAM\x01").unwrap();
100 assert!(matches!(
101 detect_format(file.path().to_str().unwrap()).unwrap(),
102 InputFormat::Bam
103 ));
104 }
105}