use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
use crate::error::Result;
pub(crate) const BGZF_CACHE_SIZE: usize = 64 * 1024 * 1024;
#[repr(C)]
struct RawHtsFile {
_private: [u8; 0],
}
#[repr(C)]
struct RawSamHdr {
_private: [u8; 0],
}
#[repr(C)]
struct RawBam1 {
_private: [u8; 0],
}
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_read(fp: *mut RawHtsFile) -> *mut RawSamHdr;
fn sam_hdr_write(fp: *mut RawHtsFile, h: *const RawSamHdr) -> c_int;
fn sam_hdr_destroy(h: *mut RawSamHdr);
fn sam_hdr_dup(h: *const RawSamHdr) -> *mut RawSamHdr;
fn sam_hdr_nref(h: *const RawSamHdr) -> c_int;
fn sam_hdr_tid2len(h: *const RawSamHdr, tid: c_int) -> i64;
fn sam_idx_init(
fp: *mut RawHtsFile,
h: *mut RawSamHdr,
min_shift: c_int,
fnidx: *const c_char,
) -> c_int;
fn sam_idx_save(fp: *mut RawHtsFile) -> c_int;
fn bam_init1() -> *mut RawBam1;
fn bam_destroy1(b: *mut RawBam1);
fn sam_read1(fp: *mut RawHtsFile, h: *mut RawSamHdr, b: *mut RawBam1) -> c_int;
fn sam_write1(fp: *mut RawHtsFile, h: *const RawSamHdr, b: *const RawBam1) -> c_int;
fn hts_set_threads(fp: *mut RawHtsFile, n: c_int) -> c_int;
fn qbix_hts_shim_bam_qname(b: *mut RawBam1) -> *const c_char;
fn qbix_hts_shim_sam_hdr_text(h: *const RawSamHdr) -> *const c_char;
fn qbix_hts_shim_sam_hdr_text_len(h: *const RawSamHdr) -> usize;
fn qbix_hts_shim_set_unknown_sort_order(h: *mut RawSamHdr) -> c_int;
fn qbix_hts_shim_add_pg(
h: *mut RawSamHdr,
version: *const c_char,
command_line: *const c_char,
) -> c_int;
fn qbix_hts_shim_bgzf_tell(fp: *mut RawHtsFile) -> i64;
fn qbix_hts_shim_bgzf_seek(fp: *mut RawHtsFile, offset: i64) -> i64;
fn qbix_hts_shim_bgzf_set_cache_size(fp: *mut RawHtsFile, size: c_int) -> c_int;
#[cfg(feature = "biosyntax")]
fn qbix_hts_shim_sam_format1(
h: *const RawSamHdr,
b: *const RawBam1,
out: *mut *mut c_char,
len: *mut usize,
) -> c_int;
#[cfg(feature = "biosyntax")]
fn qbix_hts_shim_free(ptr: *mut std::ffi::c_void);
}
pub(crate) struct HtsFile(*mut RawHtsFile);
pub(crate) struct Header(*mut RawSamHdr);
pub(crate) struct BamRecord(*mut RawBam1);
impl HtsFile {
pub(crate) fn close(mut self) -> Result<()> {
let file = std::mem::replace(&mut self.0, std::ptr::null_mut());
if unsafe { hts_close(file) } < 0 {
return Err("[qbix] could not close alignment output".to_string());
}
Ok(())
}
pub(crate) fn start_index(&self, header: &Header, path: &CStr, csi: bool) -> Result<()> {
let min_shift = if csi { 14 } else { 0 };
if unsafe { sam_idx_init(self.0, header.0, min_shift, path.as_ptr()) } < 0 {
return Err("[qbix] could not initialize BAM coordinate index".to_string());
}
Ok(())
}
pub(crate) fn save_index(&self) -> Result<()> {
if unsafe { sam_idx_save(self.0) } < 0 {
return Err("[qbix] could not write BAM coordinate index".to_string());
}
Ok(())
}
pub(crate) fn open(path: &str, mode: &str) -> std::result::Result<Self, ()> {
let path = CString::new(path).map_err(|_| ())?;
let mode = CString::new(mode).map_err(|_| ())?;
let fp = unsafe { hts_open(path.as_ptr(), mode.as_ptr()) };
if fp.is_null() {
Err(())
} else {
Ok(Self(fp))
}
}
pub(crate) fn read_header(&self) -> std::result::Result<Header, ()> {
let header = unsafe { sam_hdr_read(self.0) };
if header.is_null() {
Err(())
} else {
Ok(Header(header))
}
}
pub(crate) fn set_threads(&self, threads: usize) -> Result<()> {
if threads <= 1 {
return Ok(());
}
let threads = c_int::try_from(threads)
.map_err(|_| "[qbix] thread count is too large for htslib".to_string())?;
if unsafe { hts_set_threads(self.0, threads) } < 0 {
return Err("[qbix] hts_set_threads failed".to_string());
}
Ok(())
}
pub(crate) fn set_bgzf_cache_size(&self, size: usize) -> Result<()> {
let size = c_int::try_from(size)
.map_err(|_| "[qbix] BGZF cache size is too large for htslib".to_string())?;
if unsafe { qbix_hts_shim_bgzf_set_cache_size(self.0, size) } != 0 {
return Err("[qbix] bgzf_set_cache_size failed".to_string());
}
Ok(())
}
pub(crate) fn read_next(&self, header: &Header, record: &BamRecord) -> i32 {
unsafe { sam_read1(self.0, header.0, record.0) }
}
pub(crate) fn write_record(&self, header: &Header, record: &BamRecord) -> Result<()> {
if unsafe { sam_write1(self.0, header.0, record.0) } < 0 {
return Err("[qbix] sam_write1 failed".to_string());
}
Ok(())
}
pub(crate) fn write_header(&self, header: &Header) -> Result<()> {
if unsafe { sam_hdr_write(self.0, header.0) } < 0 {
return Err("[qbix] sam_hdr_write failed".to_string());
}
Ok(())
}
pub(crate) fn read_record_at(
&self,
header: &Header,
record: &BamRecord,
offset: i64,
) -> Result<()> {
if offset < 0 {
return Err("[qbix] invalid negative BGZF offset".to_string());
}
if unsafe { qbix_hts_shim_bgzf_seek(self.0, offset) } != 0 {
return Err("[qbix] bgzf_seek failed".to_string());
}
if self.read_next(header, record) < 0 {
return Err("[qbix] sam_read1 failed".to_string());
}
Ok(())
}
pub(crate) fn tell(&self) -> Result<i64> {
let offset = unsafe { qbix_hts_shim_bgzf_tell(self.0) };
if offset < 0 {
Err("[qbix] bgzf_tell failed".to_string())
} else {
Ok(offset)
}
}
}
impl Drop for HtsFile {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
hts_close(self.0);
}
}
}
}
impl Drop for Header {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
sam_hdr_destroy(self.0);
}
}
}
}
impl Header {
pub(crate) fn is_coordinate_sorted(&self) -> Result<bool> {
Ok(self.text()?.split(|byte| *byte == b'\n').any(|line| {
line.starts_with(b"@HD\t")
&& line
.split(|byte| *byte == b'\t')
.any(|field| field == b"SO:coordinate")
}))
}
pub(crate) fn needs_csi(&self) -> Result<bool> {
let nref = unsafe { sam_hdr_nref(self.0) };
if nref < 0 {
return Err("[qbix] could not inspect BAM references".to_string());
}
for tid in 0..nref {
if unsafe { sam_hdr_tid2len(self.0, tid) } > 1_i64 << 29 {
return Ok(true);
}
}
Ok(false)
}
pub(crate) fn duplicate(&self) -> Result<Self> {
let header = unsafe { sam_hdr_dup(self.0) };
if header.is_null() {
Err("[qbix] could not duplicate BAM header".to_string())
} else {
Ok(Self(header))
}
}
pub(crate) fn set_unknown_sort_order(&self) -> Result<()> {
if unsafe { qbix_hts_shim_set_unknown_sort_order(self.0) } < 0 {
return Err("[qbix] could not update output sort order".to_string());
}
Ok(())
}
pub(crate) fn add_qbix_program(&self, command_line: &str) -> Result<()> {
let version = CString::new(crate::VERSION).expect("package version has no NUL");
let command_line = CString::new(command_line)
.map_err(|_| "[qbix] command line contains a NUL byte".to_string())?;
if unsafe { qbix_hts_shim_add_pg(self.0, version.as_ptr(), command_line.as_ptr()) } < 0 {
return Err("[qbix] could not add qbix program record".to_string());
}
Ok(())
}
pub(crate) fn text(&self) -> Result<&[u8]> {
if self.0.is_null() {
return Err("[qbix] invalid BAM header".to_string());
}
let text = unsafe { qbix_hts_shim_sam_hdr_text(self.0) };
let len = unsafe { qbix_hts_shim_sam_hdr_text_len(self.0) };
if text.is_null() && len > 0 {
return Err("[qbix] invalid BAM header text".to_string());
}
Ok(if len == 0 {
&[]
} else {
unsafe { std::slice::from_raw_parts(text as *const u8, len) }
})
}
pub(crate) fn text_hash(&self) -> Result<u64> {
Ok(fnv1a64(self.text()?))
}
}
impl BamRecord {
pub(crate) fn new() -> Result<Self> {
let record = unsafe { bam_init1() };
if record.is_null() {
Err("[qbix] could not allocate BAM record".to_string())
} else {
Ok(Self(record))
}
}
pub(crate) fn qname(&self) -> Result<&str> {
if self.0.is_null() {
return Err("[qbix] invalid BAM record".to_string());
}
let qname = unsafe { qbix_hts_shim_bam_qname(self.0) };
if qname.is_null() {
return Err("[qbix] invalid BAM record".to_string());
}
let cstr = unsafe { CStr::from_ptr(qname) };
cstr.to_str()
.map_err(|_| "[qbix] BAM read name is not valid UTF-8".to_string())
}
#[cfg(feature = "biosyntax")]
pub(crate) fn format_sam(&self, header: &Header) -> Result<Vec<u8>> {
let mut ptr = std::ptr::null_mut();
let mut len = 0usize;
let ret = unsafe { qbix_hts_shim_sam_format1(header.0, self.0, &mut ptr, &mut len) };
if ret < 0 || ptr.is_null() {
return Err("[qbix] sam_format1 failed".to_string());
}
let bytes = unsafe { std::slice::from_raw_parts(ptr as *const u8, len) }.to_vec();
unsafe {
qbix_hts_shim_free(ptr.cast());
}
Ok(bytes)
}
}
fn fnv1a64(bytes: &[u8]) -> u64 {
let mut hash = 0xcbf29ce484222325u64;
for &byte in bytes {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
impl Drop for BamRecord {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
bam_destroy1(self.0);
}
}
}
}