use std::ffi::{CStr, CString};
use std::path::Path;
use std::ptr::NonNull;
use crate::error::YaraError;
use crate::ffi_helpers::{
bytes_to_cstring, collect_contig_lengths, collect_contig_names, path_to_cstring,
};
use crate::options::{MapperOptions, SecondaryMode};
use crate::record::{CigarOp, YaraRecord};
#[derive(Debug, Clone, Copy)]
pub struct ReadEnd<'a> {
pub seq: &'a [u8],
pub qual: &'a [u8],
}
#[derive(Default)]
pub struct ReadBatch {
names: Vec<CString>,
r1_seqs: Vec<CString>,
r1_quals: Vec<CString>,
r2_seqs: Vec<CString>,
r2_quals: Vec<CString>,
}
impl ReadBatch {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_capacity(n: usize) -> Self {
Self {
names: Vec::with_capacity(n),
r1_seqs: Vec::with_capacity(n),
r1_quals: Vec::with_capacity(n),
r2_seqs: Vec::with_capacity(n),
r2_quals: Vec::with_capacity(n),
}
}
pub fn push(&mut self, name: &str, r1: ReadEnd<'_>, r2: ReadEnd<'_>) -> Result<(), YaraError> {
if r1.seq.len() != r1.qual.len() {
return Err(YaraError::InvalidInput(format!(
"r1 seq/qual length mismatch: {} vs {}",
r1.seq.len(),
r1.qual.len()
)));
}
if r2.seq.len() != r2.qual.len() {
return Err(YaraError::InvalidInput(format!(
"r2 seq/qual length mismatch: {} vs {}",
r2.seq.len(),
r2.qual.len()
)));
}
self.names
.push(CString::new(name).map_err(|e| YaraError::InvalidInput(format!("name: {e}")))?);
self.r1_seqs.push(bytes_to_cstring(r1.seq));
self.r1_quals.push(bytes_to_cstring(r1.qual));
self.r2_seqs.push(bytes_to_cstring(r2.seq));
self.r2_quals.push(bytes_to_cstring(r2.qual));
Ok(())
}
#[must_use]
pub fn len(&self) -> usize {
self.names.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.names.is_empty()
}
pub fn clear(&mut self) {
self.names.clear();
self.r1_seqs.clear();
self.r1_quals.clear();
self.r2_seqs.clear();
self.r2_quals.clear();
}
}
pub struct YaraMapper {
handle: NonNull<yara_mapper_sys::YaraMapperHandle>,
secondary_mode: SecondaryMode,
}
unsafe impl Send for YaraMapper {}
impl YaraMapper {
pub fn open<P: AsRef<Path>>(
index_prefix: P,
options: &MapperOptions,
) -> Result<Self, YaraError> {
let prefix_cstr =
path_to_cstring(index_prefix.as_ref(), "index_prefix", YaraError::IndexOpen)?;
let ffi_opts = options.to_ffi();
let mut error_buf = vec![0u8; 1024];
let handle = unsafe {
yara_mapper_sys::yara_mapper_open(
prefix_cstr.as_ptr(),
&ffi_opts,
error_buf.as_mut_ptr().cast(),
error_buf.len(),
)
};
NonNull::new(handle)
.map(|h| Self { handle: h, secondary_mode: options.secondary_mode })
.ok_or_else(|| {
let msg = unsafe { CStr::from_ptr(error_buf.as_ptr().cast()) };
YaraError::IndexOpen(msg.to_string_lossy().into_owned())
})
}
pub fn map_paired(&self, reads: &ReadBatch) -> Result<Vec<YaraRecord>, YaraError> {
if reads.is_empty() {
return Ok(Vec::new());
}
let name_ptrs = cstring_ptrs(&reads.names);
let r1_seq_ptrs = cstring_ptrs(&reads.r1_seqs);
let r1_qual_ptrs = cstring_ptrs(&reads.r1_quals);
let r2_seq_ptrs = cstring_ptrs(&reads.r2_seqs);
let r2_qual_ptrs = cstring_ptrs(&reads.r2_quals);
let batch = yara_mapper_sys::YaraReadBatch {
names: name_ptrs.as_ptr(),
r1_seqs: r1_seq_ptrs.as_ptr(),
r1_quals: r1_qual_ptrs.as_ptr(),
r2_seqs: r2_seq_ptrs.as_ptr(),
r2_quals: r2_qual_ptrs.as_ptr(),
count: reads.len(),
};
let capacity = reads.len() * records_per_pair(self.secondary_mode);
let mut out_records: Vec<yara_mapper_sys::YaraAlignmentRecord> =
vec![unsafe { std::mem::zeroed() }; capacity];
let mut error_buf = [0u8; 1024];
let count = unsafe {
yara_mapper_sys::yara_mapper_map_paired(
self.handle.as_ptr(),
&batch,
out_records.as_mut_ptr(),
capacity,
error_buf.as_mut_ptr().cast(),
error_buf.len(),
)
};
if count < 0 {
let msg = unsafe { CStr::from_ptr(error_buf.as_ptr().cast()) };
return Err(YaraError::Mapping(msg.to_string_lossy().into_owned()));
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "count is non-negative and bounded by capacity (which is usize)"
)]
let n = count as usize;
let results: Vec<YaraRecord> = out_records[..n].iter().map(convert_record).collect();
unsafe {
yara_mapper_sys::yara_mapper_free_records(out_records.as_mut_ptr(), n);
}
Ok(results)
}
#[must_use]
pub fn contig_count(&self) -> usize {
unsafe { yara_mapper_sys::yara_mapper_contig_count(self.handle.as_ptr()) }
}
#[must_use]
pub fn contig_names(&self) -> Vec<String> {
let n = self.contig_count();
unsafe {
collect_contig_names(n, |i| {
yara_mapper_sys::yara_mapper_contig_name(self.handle.as_ptr(), i)
})
}
}
#[must_use]
pub fn contig_lengths(&self) -> Vec<usize> {
let n = self.contig_count();
collect_contig_lengths(n, |i| unsafe {
yara_mapper_sys::yara_mapper_contig_length(self.handle.as_ptr(), i)
})
}
}
impl Drop for YaraMapper {
fn drop(&mut self) {
unsafe { yara_mapper_sys::yara_mapper_close(self.handle.as_ptr()) }
}
}
fn records_per_pair(mode: SecondaryMode) -> usize {
match mode {
SecondaryMode::Tag | SecondaryMode::Omit => 2,
SecondaryMode::Record => 10,
}
}
fn cstring_ptrs(strings: &[CString]) -> Vec<*const i8> {
strings.iter().map(|s| s.as_ptr()).collect()
}
fn convert_record(rec: &yara_mapper_sys::YaraAlignmentRecord) -> YaraRecord {
let cigar = if !rec.cigar.is_null() && rec.cigar_len > 0 {
let slice = unsafe { std::slice::from_raw_parts(rec.cigar, rec.cigar_len as usize) };
slice.iter().map(|&encoded| CigarOp::from_bam(encoded)).collect()
} else {
Vec::new()
};
let seq = if !rec.seq.is_null() && rec.seq_len > 0 {
let slice =
unsafe { std::slice::from_raw_parts(rec.seq.cast::<u8>(), rec.seq_len as usize) };
Some(slice.to_vec())
} else {
None
};
let qual = if !rec.qual.is_null() && rec.seq_len > 0 {
let slice =
unsafe { std::slice::from_raw_parts(rec.qual.cast::<u8>(), rec.seq_len as usize) };
Some(slice.to_vec())
} else {
None
};
let xa = if rec.xa.is_null() {
None
} else {
Some(unsafe { CStr::from_ptr(rec.xa) }.to_string_lossy().into_owned())
};
YaraRecord {
read_pair_index: rec.read_pair_index,
is_read1: rec.is_read1 != 0,
contig_id: rec.contig_id,
pos: rec.pos,
is_reverse: rec.is_reverse != 0,
is_secondary: rec.is_secondary != 0,
is_unmapped: rec.is_unmapped != 0,
mapq: rec.mapq,
nm: rec.nm,
x0: rec.x0,
x1: rec.x1,
mate_contig_id: rec.mate_contig_id,
mate_pos: rec.mate_pos,
tlen: rec.tlen,
flag: rec.flag,
cigar,
seq,
qual,
xa,
}
}