use crate::error::{Error, Result};
use minibwa_sys as sys;
use std::ffi::{CStr, CString};
use std::path::Path;
pub struct Index {
ptr: *mut sys::mb_idx_t,
}
unsafe impl Send for Index {}
unsafe impl Sync for Index {}
fn c_path(p: &Path) -> Result<CString> {
CString::new(p.to_string_lossy().as_bytes())
.map_err(|_| Error::InvalidInput(format!("path has NUL: {}", p.display())))
}
impl Index {
pub fn build_from_fasta(
fasta: impl AsRef<Path>,
prefix: impl AsRef<Path>,
meth: bool,
threads: u32,
) -> Result<()> {
let fasta = fasta.as_ref();
let prefix = prefix.as_ref();
let meta = std::fs::metadata(fasta).map_err(|e| Error::IndexBuild {
fasta: fasta.to_path_buf(),
msg: format!("cannot stat FASTA: {e}"),
})?;
if meta.len() == 0 {
return Err(Error::IndexBuild {
fasta: fasta.to_path_buf(),
msg: "FASTA is empty".into(),
});
}
if let Some(parent) = prefix.parent() {
if !parent.as_os_str().is_empty() && !parent.is_dir() {
return Err(Error::IndexBuild {
fasta: fasta.to_path_buf(),
msg: format!("output directory does not exist: {}", parent.display()),
});
}
}
let c_fa = c_path(fasta)?;
let c_prefix = c_path(prefix)?;
let rc = unsafe {
sys::mb_index_build(
c_fa.as_ptr(),
c_prefix.as_ptr(),
meth as i32,
threads as i32,
)
};
if rc != 0 {
return Err(Error::IndexBuild {
fasta: fasta.to_path_buf(),
msg: shim_last_error(),
});
}
Ok(())
}
pub fn load(prefix: impl AsRef<Path>, meth: bool) -> Result<Index> {
let prefix = prefix.as_ref();
let c_prefix = c_path(prefix)?;
let ptr = unsafe { sys::mb_idx_load(c_prefix.as_ptr(), meth as i32) };
if ptr.is_null() {
return Err(Error::IndexLoad {
path: prefix.to_path_buf(),
msg: "mb_idx_load returned NULL (missing or corrupt index files)".into(),
});
}
Ok(Index { ptr })
}
pub fn n_contigs(&self) -> usize {
let mut n = 0usize;
while !unsafe { sys::mb_idx_ctg_name(self.ptr, n as i32) }.is_null() {
n += 1;
}
n
}
pub fn contig_name(&self, tid: usize) -> Option<&str> {
let p = unsafe { sys::mb_idx_ctg_name(self.ptr, tid as i32) };
if p.is_null() {
return None;
}
unsafe { CStr::from_ptr(p) }.to_str().ok()
}
pub fn contig_len(&self, tid: usize) -> Option<i64> {
let len = unsafe { sys::mb_idx_ctg_len(self.ptr, tid as i32) };
if len < 0 {
None
} else {
Some(len)
}
}
pub fn contigs(&self) -> impl Iterator<Item = (&str, i64)> {
(0..self.n_contigs()).map(move |tid| {
(
self.contig_name(tid).unwrap(),
self.contig_len(tid).unwrap(),
)
})
}
pub(crate) fn as_ptr(&self) -> *const sys::mb_idx_t {
self.ptr
}
}
impl Drop for Index {
fn drop(&mut self) {
unsafe { sys::mb_idx_destroy(self.ptr) };
}
}
fn shim_last_error() -> String {
let p = unsafe { sys::mb_shim_last_error() };
if p.is_null() {
return String::new();
}
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::{synthetic_reference, write_fasta, write_multi_fasta};
fn tmpdir(tag: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("minibwa_idx_{tag}_{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn build_load_contigs() {
let dir = tmpdir("blc");
let seq = synthetic_reference(3000, 7);
let fa = write_fasta(&dir, "chr1", &seq);
let prefix = dir.join("idx");
Index::build_from_fasta(&fa, &prefix, false, 1).unwrap();
let idx = Index::load(&prefix, false).unwrap();
assert_eq!(idx.n_contigs(), 1);
assert_eq!(idx.contig_name(0), Some("chr1"));
assert_eq!(idx.contig_len(0), Some(3000));
assert_eq!(idx.contig_name(1), None);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn multi_contig_index() {
let dir = tmpdir("multi");
let seq1 = synthetic_reference(2000, 17);
let seq2 = synthetic_reference(3000, 23);
let fa = write_multi_fasta(&dir, &[("chr1", &seq1), ("chr2", &seq2)]);
let prefix = dir.join("idx");
Index::build_from_fasta(&fa, &prefix, false, 1).unwrap();
let idx = Index::load(&prefix, false).unwrap();
assert_eq!(idx.n_contigs(), 2);
assert_eq!(idx.contig_name(0), Some("chr1"));
assert_eq!(idx.contig_len(0), Some(2000i64));
assert_eq!(idx.contig_name(1), Some("chr2"));
assert_eq!(idx.contig_len(1), Some(3000i64));
assert_eq!(idx.contig_len(2), None);
assert_eq!(
idx.contigs().collect::<Vec<_>>(),
vec![("chr1", 2000i64), ("chr2", 3000i64)]
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn build_empty_fasta_errors() {
let dir = tmpdir("empty");
let fa = dir.join("empty.fa");
std::fs::File::create(&fa).unwrap(); let res = Index::build_from_fasta(&fa, dir.join("idx"), false, 1);
assert!(matches!(res, Err(crate::Error::IndexBuild { .. })));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn build_bad_output_dir_errors() {
let dir = tmpdir("baddir");
let seq = synthetic_reference(2000, 7);
let fa = write_fasta(&dir, "chr1", &seq);
let res = Index::build_from_fasta(&fa, dir.join("nodir/idx"), false, 1);
assert!(matches!(res, Err(crate::Error::IndexBuild { .. })));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn build_missing_fasta_errors_not_aborts() {
let dir = tmpdir("missing");
let res = Index::build_from_fasta(dir.join("does-not-exist.fa"), dir.join("idx"), false, 1);
assert!(matches!(res, Err(crate::Error::IndexBuild { .. })));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn load_missing_errors() {
let dir = tmpdir("loadmiss");
let res = Index::load(dir.join("nope"), false);
assert!(matches!(res, Err(crate::Error::IndexLoad { .. })));
std::fs::remove_dir_all(&dir).ok();
}
}