Skip to main content

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}