qbix 0.0.3

Random access to BAM records by read name using a compact .qbi index
use std::ffi::CString;
use std::os::raw::{c_char, c_int};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

#[repr(C)]
struct RawHtsFile {
    _private: [u8; 0],
}

#[repr(C)]
struct RawSamHdr {
    _private: [u8; 0],
}

#[repr(C)]
struct RawBam1 {
    _private: [u8; 0],
}

#[link(name = "hts")]
#[link(name = "deflate")]
#[link(name = "z")]
extern "C" {
    fn hts_open(path: *const c_char, mode: *const c_char) -> *mut RawHtsFile;
    fn hts_close(fp: *mut RawHtsFile) -> c_int;
    fn sam_hdr_parse(l_text: usize, text: *const c_char) -> *mut RawSamHdr;
    fn sam_hdr_write(fp: *mut RawHtsFile, h: *const RawSamHdr) -> c_int;
    fn sam_hdr_destroy(h: *mut RawSamHdr);
    fn bam_init1() -> *mut RawBam1;
    fn bam_destroy1(b: *mut RawBam1);
    fn bam_set1(
        bam: *mut RawBam1,
        l_qname: usize,
        qname: *const c_char,
        flag: u16,
        tid: i32,
        pos: i64,
        mapq: u8,
        n_cigar: usize,
        cigar: *const u32,
        mtid: i32,
        mpos: i64,
        isize: i64,
        l_seq: usize,
        seq: *const c_char,
        qual: *const c_char,
        l_aux: usize,
    ) -> c_int;
    fn sam_write1(fp: *mut RawHtsFile, h: *const RawSamHdr, b: *const RawBam1) -> c_int;
}

pub struct TempDir {
    path: PathBuf,
}

impl TempDir {
    pub fn new(name: &str) -> Self {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let path = std::env::temp_dir().join(format!("qbix-{name}-{}-{nanos}", std::process::id()));
        std::fs::create_dir_all(&path).unwrap();
        Self { path }
    }

    pub fn path(&self) -> &std::path::Path {
        &self.path
    }
}

impl Drop for TempDir {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.path);
    }
}

pub fn write_unmapped_bam(path: &str, readnames: &[&str]) {
    let bam = HtsFile::open(path, "wb");
    let header = Header::parse("@HD\tVN:1.6\tSO:unknown\n@SQ\tSN:chr1\tLN:1000\n");
    assert_eq!(unsafe { sam_hdr_write(bam.0, header.0) }, 0);

    let record = BamRecord::new();
    for readname in readnames {
        record.set_unmapped(readname);
        assert!(unsafe { sam_write1(bam.0, header.0, record.0) } >= 0);
    }
}

struct HtsFile(*mut RawHtsFile);
struct Header(*mut RawSamHdr);
struct BamRecord(*mut RawBam1);

impl HtsFile {
    fn open(path: &str, mode: &str) -> Self {
        let path = CString::new(path).unwrap();
        let mode = CString::new(mode).unwrap();
        let fp = unsafe { hts_open(path.as_ptr(), mode.as_ptr()) };
        assert!(!fp.is_null());
        Self(fp)
    }
}

impl Drop for HtsFile {
    fn drop(&mut self) {
        if !self.0.is_null() {
            unsafe {
                hts_close(self.0);
            }
        }
    }
}

impl Header {
    fn parse(text: &str) -> Self {
        let text = CString::new(text).unwrap();
        let header = unsafe { sam_hdr_parse(text.as_bytes().len(), text.as_ptr()) };
        assert!(!header.is_null());
        Self(header)
    }
}

impl Drop for Header {
    fn drop(&mut self) {
        if !self.0.is_null() {
            unsafe {
                sam_hdr_destroy(self.0);
            }
        }
    }
}

impl BamRecord {
    fn new() -> Self {
        let record = unsafe { bam_init1() };
        assert!(!record.is_null());
        Self(record)
    }

    fn set_unmapped(&self, readname: &str) {
        let readname = CString::new(readname).unwrap();
        let ret = unsafe {
            bam_set1(
                self.0,
                readname.as_bytes().len(),
                readname.as_ptr(),
                4,
                -1,
                -1,
                0,
                0,
                std::ptr::null(),
                -1,
                -1,
                0,
                0,
                std::ptr::null(),
                std::ptr::null(),
                0,
            )
        };
        assert!(ret >= 0);
    }
}

impl Drop for BamRecord {
    fn drop(&mut self) {
        if !self.0.is_null() {
            unsafe {
                bam_destroy1(self.0);
            }
        }
    }
}