hts 0.1.18

Rust binding for htslib
Documentation
use std::cell::RefCell;
use std::ffi::{CStr, CString};
use std::ops::{Deref, DerefMut};
use std::path::Path;
use std::ptr::NonNull;

use crate::alloc::MemoryPool;
use crate::hts_ffi::{
    hts_close, hts_itr_next, hts_open, hts_set_fai_filename, htslib_obj, sam_hdr_destroy,
    sam_hdr_read, sam_index_load, sam_itr_queryi, sam_read1, HtslibReturnValue,
};
use crate::{HtsError, HtsResult};

use super::alignment::{Alignment, AlignmentReader};

enum IndexFile {
    OnDisk(Vec<u8>),
    Loaded(NonNull<htslib_obj::HtsIndex>),
}

impl IndexFile {
    fn new(path: &Path) -> HtsResult<Self> {
        let path = CString::new({
            if let Some(path) = path.to_str() {
                path
            } else {
                return Err(HtsError::InvalidFileName);
            }
        })
        .map_err(|_| HtsError::InvalidFileName)?;
        Ok(IndexFile::OnDisk(path.into_bytes()))
    }

    fn ensure_index_loaded(
        this: &RefCell<Self>,
        file: *mut htslib_obj::HtsFile,
    ) -> HtsResult<NonNull<htslib_obj::HtsIndex>> {
        match this.borrow().deref() {
            Self::Loaded(idx) => Ok(*idx),
            Self::OnDisk(path) => {
                let ret = unsafe {
                    NonNull::new_unchecked(
                        sam_index_load(file, path.as_ptr() as *const _).into_hts_result()?,
                    )
                };
                let mut this = this.borrow_mut();
                *this = Self::Loaded(ret);
                Ok(ret)
            }
        }
    }
}

pub struct AlignmentFile {
    header: NonNull<htslib_obj::SamHeader>,
    // Since we need use a shared reference else where, so
    // the file pointer should not be covariant
    file: *mut htslib_obj::HtsFile,
    index: RefCell<IndexFile>,
    pool: MemoryPool<htslib_obj::Bam>,
    chrom_name: Box<[HtsResult<String>]>,
}

impl AlignmentReader for AlignmentFile {
    fn read_next(&self) -> HtsResult<Option<Alignment>> {
        if let Some(mut core_obj) = self.pool.alloc_obj() {
            let rc = unsafe { sam_read1(self.file, self.header.as_ptr(), core_obj.deref_mut()) };
            if rc == -1 {
                return Ok(None);
            }
            rc.into_hts_result()?;
            return Ok(Some(Alignment::from_hts_object(core_obj, self)));
        }
        Err(HtsError::HtsLibError)
    }
}

pub struct AlignmentFileView<'a> {
    file: &'a AlignmentFile,
    iter: *mut htslib_obj::HtsIterator,
}

impl<'a> AlignmentReader for AlignmentFileView<'a> {
    fn read_next(&self) -> HtsResult<Option<Alignment>> {
        if let Some(mut core_obj) = self.file.pool.alloc_obj() {
            let rc = unsafe {
                hts_itr_next(
                    (*self.file.file).fp.bgzf,
                    self.iter,
                    core_obj.deref_mut() as *mut _ as _,
                    self.file.file as _,
                )
            };
            if rc == -1 {
                return Ok(None);
            }
            rc.into_hts_result()?;
            return Ok(Some(Alignment::from_hts_object(core_obj, self.file)));
        }
        Err(HtsError::HtsLibError)
    }
}

impl AlignmentFile {
    pub(crate) fn header(&self) -> *const htslib_obj::SamHeader {
        self.header.as_ptr()
    }
    fn raw_chrom_names(&self) -> impl Iterator<Item = &CStr> {
        unsafe {
            std::slice::from_raw_parts(
                self.header.as_ref().target_name,
                self.header.as_ref().n_targets as usize,
            )
            .iter()
            .map(|&p| CStr::from_ptr(p))
        }
    }

    pub fn with_reference<P: AsRef<Path>>(&mut self, fai: P) -> HtsResult<()> {
        if let Some(Ok(fai)) = fai.as_ref().to_str().map(|e| CString::new(e)) {
            unsafe {
                hts_set_fai_filename(self.file, fai.as_bytes().as_ptr() as _).into_hts_result()?
            };
            return Ok(());
        }
        Err(HtsError::InvalidFileName)
    }

    pub fn get_chrom_id_by_name(&self, name: &str) -> Option<usize> {
        self.raw_chrom_names()
            .enumerate()
            .find(|(_, this)| this.to_bytes() == name.as_bytes())
            .map(|(result, _)| result)
    }

    pub fn get_chrom_name_by_id(&self, id: usize) -> HtsResult<&str> {
        let ntargets = unsafe { self.header.as_ref().n_targets as usize };
        if id >= ntargets {
            return Err(HtsError::NoSuchChromosome);
        }
        match &self.chrom_name[id] {
            Ok(ref name) => Ok(name),
            Err(e) => Err(*e),
        }
    }

    pub fn ranged<'a>(
        &'a self,
        chrom: &str,
        begin: i64,
        end: i64,
    ) -> HtsResult<AlignmentFileView<'a>> {
        let index = IndexFile::ensure_index_loaded(&self.index, self.file)?;
        if let Some(cid) = self.get_chrom_id_by_name(chrom) {
            let iter = unsafe {
                sam_itr_queryi(index.as_ref(), cid as i32, begin, end).into_hts_result()?
            };
            return Ok(AlignmentFileView { file: self, iter });
        }
        Err(HtsError::NoSuchChromosome)
    }

    pub fn open<T: AsRef<Path>>(path: T) -> HtsResult<Self> {
        let index = IndexFile::new(path.as_ref())?;

        let file = if let IndexFile::OnDisk(ref path) = index {
            unsafe {
                hts_open(path.as_ptr() as *const _, b"rb\0".as_ptr() as *const _)
                    .into_hts_result()?
            }
        } else {
            unreachable!();
        };
        let header = unsafe { NonNull::new_unchecked(sam_hdr_read(file).into_hts_result()?) };
        let pool = MemoryPool::with_capacity(128);

        let mut chrom_name = vec![];

        for &raw_chrom_name in unsafe {
            std::slice::from_raw_parts(
                header.as_ref().target_name,
                header.as_ref().n_targets as usize,
            )
        } {
            let raw_chrom_name = unsafe { CStr::from_ptr(raw_chrom_name) };
            chrom_name.push(
                raw_chrom_name
                    .to_str()
                    .map(|s| s.to_owned())
                    .map_err(|_| HtsError::InvalidChromName),
            );
        }
        let chrom_name = chrom_name.into_boxed_slice();

        Ok(Self {
            header,
            file,
            pool,
            index: RefCell::new(index),
            chrom_name,
        })
    }
}

impl Drop for AlignmentFile {
    fn drop(&mut self) {
        unsafe {
            sam_hdr_destroy(self.header.as_mut());
            hts_close(self.file);
        }
    }
}