use std::sync::Arc;
use bytes::Bytes;
use crate::error::{Error, Result};
use super::compression::{tag_key, CompressionHeader};
use super::encoding::Streams;
use super::slice::Slice;
mod cram_flag {
pub const QUALITIES: i32 = 1;
pub const DETACHED: i32 = 2;
pub const MATE_DOWNSTREAM: i32 = 4;
pub const NO_SEQUENCE: i32 = 8;
}
mod mate_flag {
pub const REVERSE: i32 = 1;
pub const UNMAPPED: i32 = 2;
}
mod sam_flag {
pub const UNMAPPED: u16 = 0x004;
pub const MATE_UNMAPPED: u16 = 0x008;
pub const REVERSE: u16 = 0x010;
pub const MATE_REVERSE: u16 = 0x020;
}
mod cigar_op {
pub const MATCH: u32 = 0;
pub const INSERT: u32 = 1;
pub const DELETE: u32 = 2;
pub const SKIP: u32 = 3;
pub const SOFT_CLIP: u32 = 4;
pub const HARD_CLIP: u32 = 5;
pub const PAD: u32 = 6;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ReferenceBases<'a> {
pub bases: &'a [u8],
pub start: i64,
}
impl ReferenceBases<'_> {
#[inline]
fn at(&self, position: i64) -> u8 {
let index = position - self.start;
if index < 0 {
return b'N';
}
self.bases
.get(index as usize)
.copied()
.map(|b| b.to_ascii_uppercase())
.unwrap_or(b'N')
}
}
pub enum References<'a> {
None,
Fixed(ReferenceBases<'a>),
ByRefId {
source: &'a super::reference::ReferenceSource,
names: &'a [String],
},
}
struct HeldWindow {
ref_id: i32,
start: i64,
end: i64,
bases: Arc<Vec<u8>>,
}
#[derive(Debug, Default)]
struct Record {
flags: u16,
cram_flags: i32,
ref_id: i32,
start: i64,
end: i64,
read_length: i32,
read_group: i32,
name: Vec<u8>,
mapping_quality: u8,
next_ref_id: i32,
next_pos: i64,
template_length: i64,
mate_line: Option<usize>,
detached: bool,
has_rg: bool,
cigar: Vec<u32>,
sequence: Vec<u8>,
qualities: Vec<u8>,
tags: Vec<u8>,
has_md: bool,
has_nm: bool,
}
impl Record {
fn reset(&mut self) {
self.name.clear();
self.cigar.clear();
self.sequence.clear();
self.qualities.clear();
self.tags.clear();
self.flags = 0;
self.cram_flags = 0;
self.ref_id = 0;
self.start = 0;
self.end = 0;
self.read_length = 0;
self.read_group = 0;
self.mapping_quality = 0;
self.next_ref_id = -1;
self.next_pos = 0;
self.template_length = 0;
self.mate_line = None;
self.detached = false;
self.has_rg = false;
self.has_md = false;
self.has_nm = false;
}
}
#[derive(Debug)]
struct Placed {
offset: usize,
ref_id: i32,
start: i64,
end: i64,
flags: u16,
next_ref_id: i32,
next_pos: i64,
template_length: i64,
mate_line: Option<usize>,
detached: bool,
}
mod at {
pub const FLAGS: usize = 18;
pub const NEXT_REF_ID: usize = 24;
pub const NEXT_POS: usize = 28;
pub const TEMPLATE_LENGTH: usize = 32;
}
#[derive(Default)]
struct Scratch {
bytes: Vec<u8>,
md: Vec<u8>,
generated: String,
}
const MAX_READ_LENGTH: usize = 1 << 24;
const MAX_RECORDS_PER_SLICE: usize = 1 << 20;
fn budget(slice: &Slice) -> usize {
const PER_BYTE: usize = 8;
const FLOOR: usize = 4096;
let bytes = slice.core.len() + slice.external.iter().map(|(_, b)| b.len()).sum::<usize>();
bytes.saturating_mul(PER_BYTE).max(FLOOR)
}
pub fn decode_slice(
slice: &Slice,
header: &CompressionHeader,
references: References<'_>,
read_groups: &[String],
path: &str,
) -> Result<Bytes> {
let mut out: Vec<u8> =
Vec::with_capacity((slice.header.n_records.max(0) as usize * 96).min(1 << 24));
let mut placed = decode_records(slice, header, references, read_groups, path, Some(&mut out))?;
resolve_mates(&mut placed);
patch_mates(&mut out, &placed);
Ok(Bytes::from(out))
}
fn patch_mates(out: &mut [u8], placed: &[Placed]) {
for record in placed {
let start = record.offset;
out[start + at::FLAGS..start + at::FLAGS + 2].copy_from_slice(&record.flags.to_le_bytes());
out[start + at::NEXT_REF_ID..start + at::NEXT_REF_ID + 4]
.copy_from_slice(&record.next_ref_id.to_le_bytes());
let next_pos = if record.next_ref_id < 0 && record.next_pos <= 0 {
-1i32
} else {
(record.next_pos - 1) as i32
};
out[start + at::NEXT_POS..start + at::NEXT_POS + 4]
.copy_from_slice(&next_pos.to_le_bytes());
out[start + at::TEMPLATE_LENGTH..start + at::TEMPLATE_LENGTH + 4]
.copy_from_slice(&(record.template_length as i32).to_le_bytes());
}
}
pub fn slice_spans(
slice: &Slice,
header: &CompressionHeader,
path: &str,
) -> Result<Vec<(i32, i64, i64)>> {
let records = decode_records(slice, header, References::None, &[], path, None)?;
let mut spans: Vec<(i32, i64, i64)> = Vec::new();
for record in &records {
if record.ref_id < 0 {
continue;
}
let (start, end) = ((record.start - 1).max(0), record.end.max(record.start));
match spans.iter_mut().find(|(id, ..)| *id == record.ref_id) {
Some(span) => {
span.1 = span.1.min(start);
span.2 = span.2.max(end);
}
None => spans.push((record.ref_id, start, end)),
}
}
Ok(spans)
}
fn decode_records(
slice: &Slice,
header: &CompressionHeader,
references: References<'_>,
read_groups: &[String],
path: &str,
mut out: Option<&mut Vec<u8>>,
) -> Result<Vec<Placed>> {
let mut held: Option<HeldWindow> = None;
let mut streams = Streams::new(&slice.core, slice.streams(), path);
let series = &header.series;
let preservation = &header.preservation;
let n_records = slice.header.n_records as usize;
let budget = budget(slice);
if slice.header.n_records < 0 || n_records > MAX_RECORDS_PER_SLICE || n_records > budget {
return Err(Error::corrupt(
path,
slice.offset,
format!(
"a slice of {} records, which its {} bytes of data cannot hold",
slice.header.n_records,
budget / 8
),
));
}
let mut remaining = budget;
let mut placed: Vec<Placed> = Vec::with_capacity(n_records.min(1 << 20));
let mut record = Record::default();
let mut scratch = Scratch::default();
let mut last_position = i64::from(slice.header.start);
for index in 0..n_records {
record.reset();
record.flags = series.bf.decode_int(&mut streams)? as u16;
record.cram_flags = series.cf.decode_int(&mut streams)?;
record.ref_id = if slice.header.is_multi_ref() {
series.ri.decode_int(&mut streams)?
} else {
slice.header.ref_id
};
record.read_length = series.rl.decode_int(&mut streams)?;
if record.read_length < 0 || record.read_length as usize > MAX_READ_LENGTH {
return Err(Error::corrupt(
path,
slice.offset,
format!(
"record {index} of this slice is a read of {} bases",
record.read_length
),
));
}
remaining = remaining
.checked_sub(record.read_length as usize)
.ok_or_else(|| {
Error::corrupt(
path,
slice.offset,
format!(
"a slice whose reads total more than the {budget} bases its data can hold"
),
)
})?;
record.start = if preservation.ap_delta {
last_position + i64::from(series.ap.decode_int(&mut streams)?)
} else {
i64::from(series.ap.decode_int(&mut streams)?)
};
last_position = record.start;
record.read_group = series.rg.decode_int(&mut streams)?;
if preservation.read_names_included {
series
.rn
.decode_array(&mut streams, None, &mut record.name)?;
}
record.next_ref_id = -1;
record.next_pos = 0;
if record.cram_flags & cram_flag::DETACHED != 0 {
record.detached = true;
let mate_flags = series.mf.decode_int(&mut streams)?;
if mate_flags & mate_flag::REVERSE != 0 {
record.flags |= sam_flag::MATE_REVERSE;
}
if mate_flags & mate_flag::UNMAPPED != 0 {
record.flags |= sam_flag::MATE_UNMAPPED;
}
if !preservation.read_names_included {
series
.rn
.decode_array(&mut streams, None, &mut record.name)?;
}
record.next_ref_id = series.ns.decode_int(&mut streams)?;
record.next_pos = i64::from(series.np.decode_int(&mut streams)?);
record.template_length = i64::from(series.ts.decode_int(&mut streams)?);
} else if record.cram_flags & cram_flag::MATE_DOWNSTREAM != 0 {
let skip = series.nf.decode_int(&mut streams)?;
if skip < 0 {
return Err(Error::corrupt(
path,
0,
format!("a mate {skip} records downstream"),
));
}
let mate = index + skip as usize + 1;
if mate >= n_records {
return Err(Error::corrupt(
path,
0,
format!("a mate at record {mate} of a slice holding {n_records}"),
));
}
record.mate_line = Some(mate);
}
decode_tags(&mut streams, header, &mut record, path, slice.offset)?;
if record.read_group >= 0 && !record.has_rg {
if let Some(name) = read_groups.get(record.read_group as usize) {
record.tags.extend_from_slice(b"RGZ");
record.tags.extend_from_slice(name.as_bytes());
record.tags.push(0);
}
}
let bases = resolve_bases(&references, &mut held, &record)?;
let has_reference = !bases.bases.is_empty();
if record.flags & sam_flag::UNMAPPED == 0 {
decode_mapped(
&mut streams,
header,
&mut record,
bases,
has_reference,
&mut scratch,
path,
slice.offset,
)?;
} else {
decode_unmapped(&mut streams, header, &mut record, path)?;
}
let offset = match out.as_deref_mut() {
Some(out) => serialise(out, &record, index, slice, &mut scratch, path)?,
None => 0,
};
placed.push(Placed {
offset,
ref_id: record.ref_id,
start: record.start,
end: record.end,
flags: record.flags,
next_ref_id: record.next_ref_id,
next_pos: record.next_pos,
template_length: record.template_length,
mate_line: record.mate_line,
detached: record.detached,
});
}
Ok(placed)
}
fn resolve_bases<'a>(
references: &'a References<'_>,
held: &'a mut Option<HeldWindow>,
record: &Record,
) -> Result<ReferenceBases<'a>> {
match references {
References::None => Ok(ReferenceBases::default()),
References::Fixed(bases) => Ok(*bases),
References::ByRefId { source, names } => {
if record.ref_id < 0 {
return Ok(ReferenceBases::default());
}
let start = (record.start - 1).max(0);
let end = start + i64::from(record.read_length).max(1) + 1;
let stale = match held.as_ref() {
Some(window) => {
window.ref_id != record.ref_id || window.start > start || window.end < end
}
None => true,
};
if stale {
*held = None;
let Some(name) = names.get(record.ref_id as usize) else {
return Ok(ReferenceBases::default());
};
if source.has(name) {
let (bases, from) = source.window(name, start, end)?;
let length = bases.len() as i64;
*held = Some(HeldWindow {
ref_id: record.ref_id,
start: from,
end: from + length,
bases,
});
}
}
Ok(match held.as_ref() {
Some(window) => ReferenceBases {
bases: &window.bases[..],
start: window.start + 1,
},
None => ReferenceBases::default(),
})
}
}
}
fn decode_tags(
streams: &mut Streams<'_>,
header: &CompressionHeader,
record: &mut Record,
path: &str,
at: u64,
) -> Result<()> {
if header.series.tl.is_null() {
return Ok(());
}
let line = header.series.tl.decode_int(streams)?;
let list = header.tag_list(line, path)?;
if !list.is_empty() {
record.tags.reserve(64);
}
for tag in list {
let tag = *tag;
let encoding = header.tags.get(&tag_key(tag)).ok_or_else(|| {
Error::corrupt(
path,
at,
format!(
"a record carries tag {}{}:{} and the tag encoding map does not",
tag[0] as char, tag[1] as char, tag[2] as char
),
)
})?;
match &tag[..2] {
b"MD" => record.has_md = true,
b"NM" => record.has_nm = true,
b"RG" => record.has_rg = true,
_ => {}
}
record.tags.extend_from_slice(&tag);
encoding.decode_array(streams, None, &mut record.tags)?;
}
Ok(())
}
#[derive(Debug, Default)]
struct EditString {
md: Vec<u8>,
nm: i64,
run: i64,
}
impl EditString {
fn new(md: Vec<u8>) -> Self {
let mut md = md;
md.clear();
Self { md, nm: 0, run: 0 }
}
fn flush_run(&mut self) {
itoa(&mut self.md, self.run);
self.run = 0;
}
fn matched(&mut self, count: i64) {
self.run += count;
}
fn mismatch(&mut self, reference_base: u8) {
self.flush_run();
self.md.push(reference_base);
self.nm += 1;
}
fn aligned(&mut self, read_base: u8, reference_base: u8) {
if read_base.to_ascii_uppercase() == reference_base {
self.matched(1);
} else {
self.mismatch(reference_base);
}
}
fn deleted(&mut self, bases: &[u8]) {
self.flush_run();
self.md.push(b'^');
self.md.extend_from_slice(bases);
self.nm += bases.len() as i64;
}
fn inserted(&mut self, count: i64) {
self.nm += count;
}
fn finish(mut self) -> (Vec<u8>, i64) {
self.flush_run();
(self.md, self.nm)
}
}
fn itoa(out: &mut Vec<u8>, value: i64) {
if value == 0 {
out.push(b'0');
return;
}
let mut digits = [0u8; 20];
let mut n = value as u64;
let mut at = digits.len();
while n > 0 {
at -= 1;
digits[at] = b'0' + (n % 10) as u8;
n /= 10;
}
out.extend_from_slice(&digits[at..]);
}
#[allow(clippy::too_many_arguments)]
fn decode_mapped(
streams: &mut Streams<'_>,
header: &CompressionHeader,
record: &mut Record,
reference: ReferenceBases<'_>,
has_reference: bool,
scratch: &mut Scratch,
path: &str,
at: u64,
) -> Result<()> {
let series = &header.series;
let matrix = &header.preservation.substitution_matrix;
let read_length = record.read_length as usize;
let n_features = series.fn_.decode_int(streams)?;
if n_features < 0 || n_features as usize > 2 * read_length + 2 {
return Err(Error::corrupt(
path,
at,
format!("a read of {read_length} bases carrying {n_features} read features"),
));
}
let mut sequence = std::mem::take(&mut record.sequence);
sequence.clear();
sequence.reserve(read_length);
let mut qualities = std::mem::take(&mut record.qualities);
qualities.clear();
qualities.resize(read_length, 0xFF);
let mut cigar: Vec<u32> = std::mem::take(&mut record.cigar);
cigar.clear();
let mut open_op = u32::MAX;
let mut open_len = 0u32;
fn push_op(cigar: &mut Vec<u32>, open_op: &mut u32, open_len: &mut u32, op: u32, len: u32) {
if len == 0 {
return;
}
if *open_op == op {
*open_len += len;
return;
}
if *open_len > 0 {
cigar.push((*open_len << 4) | *open_op);
}
*open_op = op;
*open_len = len;
}
let no_sequence = record.cram_flags & cram_flag::NO_SEQUENCE != 0;
let mut edits = (has_reference && !no_sequence && !(record.has_md && record.has_nm))
.then(|| EditString::new(std::mem::take(&mut scratch.md)));
let mut read_pos: i64 = 1;
let mut ref_pos: i64 = record.start;
let mut feature_pos: i64 = 0;
for _ in 0..n_features {
let code = series.fc.decode_byte(streams)?;
feature_pos += i64::from(series.fp.decode_int(streams)?);
let position = feature_pos;
let earliest = if matches!(code, b'Q' | b'q') {
(read_pos - 1).max(1)
} else {
read_pos
};
if position < earliest || position > read_length as i64 + 1 {
return Err(Error::corrupt(
path,
at,
format!(
"a read feature at read position {position}, which is outside a \
{read_length}-base read read forwards from {read_pos}"
),
));
}
let gap = position - read_pos;
if gap > 0 {
for offset in 0..gap {
sequence.push(reference.at(ref_pos + offset));
}
push_op(
&mut cigar,
&mut open_op,
&mut open_len,
cigar_op::MATCH,
gap as u32,
);
if let Some(edits) = &mut edits {
edits.matched(gap);
}
read_pos += gap;
ref_pos += gap;
}
match code {
b'X' => {
let code = series.bs.decode_byte(streams)?;
sequence.push(if has_reference {
matrix.substitute(reference.at(ref_pos), code)
} else {
b'N'
});
if let Some(edits) = &mut edits {
edits.mismatch(reference.at(ref_pos));
}
push_op(&mut cigar, &mut open_op, &mut open_len, cigar_op::MATCH, 1);
read_pos += 1;
ref_pos += 1;
}
b'B' => {
let base = series.ba.decode_byte(streams)?;
let quality = series.qs.decode_byte(streams)?;
sequence.push(base);
if let Some(slot) = qualities.get_mut(read_pos as usize - 1) {
*slot = quality;
}
if let Some(edits) = &mut edits {
edits.aligned(base, reference.at(ref_pos));
}
push_op(&mut cigar, &mut open_op, &mut open_len, cigar_op::MATCH, 1);
read_pos += 1;
ref_pos += 1;
}
b'b' => {
scratch.bytes.clear();
series.bb.decode_array(streams, None, &mut scratch.bytes)?;
let len = scratch.bytes.len() as i64;
sequence.extend_from_slice(&scratch.bytes);
if let Some(edits) = &mut edits {
for (offset, base) in scratch.bytes.iter().enumerate() {
edits.aligned(*base, reference.at(ref_pos + offset as i64));
}
}
push_op(
&mut cigar,
&mut open_op,
&mut open_len,
cigar_op::MATCH,
len as u32,
);
read_pos += len;
ref_pos += len;
}
b'q' => {
scratch.bytes.clear();
series.qq.decode_array(streams, None, &mut scratch.bytes)?;
for (offset, quality) in scratch.bytes.iter().enumerate() {
if let Some(slot) = qualities.get_mut(position as usize - 1 + offset) {
*slot = *quality;
}
}
}
b'Q' => {
let quality = series.qs.decode_byte(streams)?;
if let Some(slot) = qualities.get_mut(position as usize - 1) {
*slot = quality;
}
}
b'I' => {
scratch.bytes.clear();
series.in_.decode_array(streams, None, &mut scratch.bytes)?;
let len = scratch.bytes.len() as i64;
sequence.extend_from_slice(&scratch.bytes);
if let Some(edits) = &mut edits {
edits.inserted(len);
}
push_op(
&mut cigar,
&mut open_op,
&mut open_len,
cigar_op::INSERT,
len as u32,
);
read_pos += len;
}
b'i' => {
let base = series.ba.decode_byte(streams)?;
sequence.push(base);
if let Some(edits) = &mut edits {
edits.inserted(1);
}
push_op(&mut cigar, &mut open_op, &mut open_len, cigar_op::INSERT, 1);
read_pos += 1;
}
b'S' => {
scratch.bytes.clear();
series.sc.decode_array(streams, None, &mut scratch.bytes)?;
let len = scratch.bytes.len() as i64;
sequence.extend_from_slice(&scratch.bytes);
push_op(
&mut cigar,
&mut open_op,
&mut open_len,
cigar_op::SOFT_CLIP,
len as u32,
);
read_pos += len;
}
b'D' => {
let len = i64::from(series.dl.decode_int(streams)?.max(0));
if let Some(edits) = &mut edits {
let deleted: Vec<u8> = (0..len)
.map(|offset| reference.at(ref_pos + offset))
.collect();
edits.deleted(&deleted);
}
push_op(
&mut cigar,
&mut open_op,
&mut open_len,
cigar_op::DELETE,
len as u32,
);
ref_pos += len;
}
b'N' => {
let len = series.rs.decode_int(streams)?;
push_op(
&mut cigar,
&mut open_op,
&mut open_len,
cigar_op::SKIP,
len.max(0) as u32,
);
ref_pos += i64::from(len.max(0));
}
b'P' => {
let len = series.pd.decode_int(streams)?;
push_op(
&mut cigar,
&mut open_op,
&mut open_len,
cigar_op::PAD,
len.max(0) as u32,
);
}
b'H' => {
let len = series.hc.decode_int(streams)?;
push_op(
&mut cigar,
&mut open_op,
&mut open_len,
cigar_op::HARD_CLIP,
len.max(0) as u32,
);
}
other => {
return Err(Error::corrupt(
path,
0,
format!(
"read feature code {:?} ({other:#04x}), which §10.6 does not define",
other as char
),
))
}
}
}
let remaining = read_length as i64 - (read_pos - 1);
if remaining > 0 {
for offset in 0..remaining {
sequence.push(reference.at(ref_pos + offset));
}
push_op(
&mut cigar,
&mut open_op,
&mut open_len,
cigar_op::MATCH,
remaining as u32,
);
if let Some(edits) = &mut edits {
edits.matched(remaining);
}
ref_pos += remaining;
}
if open_len > 0 {
cigar.push((open_len << 4) | open_op);
}
record.mapping_quality = series.mq.decode_int(streams)? as u8;
if record.cram_flags & cram_flag::QUALITIES != 0 {
qualities.clear();
series
.qs
.decode_array(streams, Some(read_length), &mut qualities)?;
}
if let Some(edits) = edits {
let (mut md, nm) = edits.finish();
if !record.has_md {
record.tags.extend_from_slice(b"MDZ");
record.tags.extend_from_slice(&md);
record.tags.push(0);
}
if !record.has_nm {
if (0..=255).contains(&nm) {
record.tags.extend_from_slice(b"NMC");
record.tags.push(nm as u8);
} else {
record.tags.extend_from_slice(b"NMi");
record.tags.extend_from_slice(&(nm as i32).to_le_bytes());
}
}
md.clear();
scratch.md = md;
}
record.cigar = cigar;
record.sequence = sequence;
record.qualities = qualities;
record.end = (ref_pos - 1).max(record.start);
Ok(())
}
fn decode_unmapped(
streams: &mut Streams<'_>,
header: &CompressionHeader,
record: &mut Record,
_path: &str,
) -> Result<()> {
let series = &header.series;
let read_length = record.read_length as usize;
let mut sequence = std::mem::take(&mut record.sequence);
sequence.clear();
sequence.reserve(read_length.min(1 << 16));
series
.ba
.decode_array(streams, Some(read_length), &mut sequence)?;
let mut qualities = std::mem::take(&mut record.qualities);
qualities.clear();
if record.cram_flags & cram_flag::QUALITIES != 0 {
series
.qs
.decode_array(streams, Some(read_length), &mut qualities)?;
} else {
qualities.resize(read_length, 0xFF);
}
record.sequence = sequence;
record.qualities = qualities;
record.end = record.start;
Ok(())
}
fn resolve_mates(records: &mut [Placed]) {
let n = records.len();
let mut visited = vec![false; n];
for start in 0..n {
if visited[start] || records[start].mate_line.is_none() || records[start].detached {
continue;
}
let reference_id = records[start].ref_id;
let mut leftmost = records[start].start;
let mut rightmost = records[start].end;
let mut left_count = 0usize;
let mut chain = vec![start];
let mut at = start;
visited[start] = true;
for _ in 0..n {
let record = &records[at];
if record.start < leftmost {
leftmost = record.start;
left_count = 1;
} else if record.start == leftmost {
left_count += 1;
}
if record.end > rightmost {
rightmost = record.end;
}
match records[at].mate_line {
Some(next) if !visited[next] => {
visited[next] = true;
at = next;
chain.push(at);
}
Some(_) => break,
None => {
records[at].mate_line = Some(start);
break;
}
}
}
for &index in &chain {
if records[index].detached {
continue;
}
let Some(mate) = records[index].mate_line else {
continue;
};
let (mate_ref, mate_pos, mate_flags) = {
let m = &records[mate];
(m.ref_id, m.start, m.flags)
};
let record = &mut records[index];
record.next_ref_id = mate_ref;
record.next_pos = mate_pos;
if mate_flags & sam_flag::REVERSE != 0 {
record.flags |= sam_flag::MATE_REVERSE;
}
if mate_flags & sam_flag::UNMAPPED != 0 {
record.flags |= sam_flag::MATE_UNMAPPED;
}
}
if reference_id < 0 || chain.len() < 2 {
continue;
}
if chain
.iter()
.any(|&index| records[index].ref_id != reference_id)
{
continue;
}
let length = rightmost - leftmost + 1;
let is_pair = chain.len() == 2;
let mut left_remaining = left_count;
for &index in &chain {
let record = &mut records[index];
if record.detached {
continue;
}
record.template_length = if record.start == leftmost && left_remaining > 0 {
left_remaining -= 1;
length
} else if record.end == rightmost || is_pair {
-length
} else {
0
};
}
}
}
static BASE_CODES: [u8; 256] = {
let mut table = [15u8; 256]; table[b'=' as usize] = 0;
table[b'A' as usize] = 1;
table[b'C' as usize] = 2;
table[b'M' as usize] = 3;
table[b'G' as usize] = 4;
table[b'R' as usize] = 5;
table[b'S' as usize] = 6;
table[b'V' as usize] = 7;
table[b'T' as usize] = 8;
table[b'W' as usize] = 9;
table[b'Y' as usize] = 10;
table[b'H' as usize] = 11;
table[b'K' as usize] = 12;
table[b'D' as usize] = 13;
table[b'B' as usize] = 14;
table[b'N' as usize] = 15;
table[b'a' as usize] = 1;
table[b'c' as usize] = 2;
table[b'm' as usize] = 3;
table[b'g' as usize] = 4;
table[b'r' as usize] = 5;
table[b's' as usize] = 6;
table[b'v' as usize] = 7;
table[b't' as usize] = 8;
table[b'w' as usize] = 9;
table[b'y' as usize] = 10;
table[b'h' as usize] = 11;
table[b'k' as usize] = 12;
table[b'd' as usize] = 13;
table[b'b' as usize] = 14;
table
};
fn reg2bin(start: i64, end: i64) -> u16 {
const LEVELS: [(u32, i64); 5] = [(14, 4681), (17, 585), (20, 73), (23, 9), (26, 1)];
let beg = start;
let end = (end - 1).max(start);
for (shift, first) in LEVELS {
if beg >> shift == end >> shift {
return (first + (beg >> shift)) as u16;
}
}
0
}
fn serialise(
out: &mut Vec<u8>,
record: &Record,
index: usize,
slice: &Slice,
scratch: &mut Scratch,
path: &str,
) -> Result<usize> {
let offset = out.len();
{
let name: &[u8] = if record.name.is_empty() {
scratch.generated.clear();
use std::fmt::Write as _;
let _ = write!(
scratch.generated,
"{}",
slice.header.record_counter + index as i64 + 1
);
scratch.generated.as_bytes()
} else {
&record.name
};
let name_len = name.len() + 1;
if name_len > 255 {
return Err(Error::corrupt(
path,
slice.offset,
format!("a read name of {} bytes, which bam cannot hold", name.len()),
));
}
if record.cigar.len() > u16::MAX as usize {
return Err(Error::corrupt(
path,
slice.offset,
format!(
"a cigar of {} operations, which this reader does not yet spill \
into a CG tag",
record.cigar.len()
),
));
}
let no_sequence = record.cram_flags & cram_flag::NO_SEQUENCE != 0;
let sequence: &[u8] = if no_sequence { &[] } else { &record.sequence };
let qualities: &[u8] = if no_sequence { &[] } else { &record.qualities };
let l_seq = sequence.len();
let variable =
name_len + record.cigar.len() * 4 + l_seq.div_ceil(2) + l_seq + record.tags.len();
let block_size = super::super::bam::record::RECORD_HEADER_SIZE - 4 + variable;
out.extend_from_slice(&(block_size as u32).to_le_bytes());
out.extend_from_slice(&record.ref_id.to_le_bytes());
let pos = if record.ref_id < 0 && record.start <= 0 {
-1i32
} else {
(record.start - 1) as i32
};
out.extend_from_slice(&pos.to_le_bytes());
out.push(name_len as u8);
out.push(record.mapping_quality);
let bin = if record.ref_id < 0 {
0
} else {
reg2bin(record.start - 1, record.end)
};
out.extend_from_slice(&bin.to_le_bytes());
out.extend_from_slice(&(record.cigar.len() as u16).to_le_bytes());
out.extend_from_slice(&record.flags.to_le_bytes());
out.extend_from_slice(&(l_seq as i32).to_le_bytes());
out.extend_from_slice(&record.next_ref_id.to_le_bytes());
let next_pos = if record.next_ref_id < 0 && record.next_pos <= 0 {
-1i32
} else {
(record.next_pos - 1) as i32
};
out.extend_from_slice(&next_pos.to_le_bytes());
out.extend_from_slice(&(record.template_length as i32).to_le_bytes());
out.extend_from_slice(name);
out.push(0);
for op in &record.cigar {
out.extend_from_slice(&op.to_le_bytes());
}
for pair in sequence.chunks(2) {
let high = BASE_CODES[pair[0] as usize];
let low = pair.get(1).map(|b| BASE_CODES[*b as usize]).unwrap_or(0);
out.push((high << 4) | low);
}
if qualities.len() == l_seq {
out.extend_from_slice(qualities);
} else {
out.extend_from_slice(&qualities[..qualities.len().min(l_seq)]);
out.resize(out.len() + l_seq.saturating_sub(qualities.len()), 0xFF);
}
out.extend_from_slice(&record.tags);
}
Ok(offset)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fuzz::{cram_compression_header, cram_slice};
fn header_for_tests() -> CompressionHeader {
CompressionHeader::parse(&cram_compression_header(), "test").expect("a valid header")
}
fn decode(slice: &Slice) -> Result<bytes::Bytes> {
decode_slice(slice, &header_for_tests(), References::None, &[], "test")
}
#[test]
fn a_slice_cannot_declare_more_records_than_its_bytes_could_hold() {
let mut slice = cram_slice(&[0u8; 16], 1);
slice.header.n_records = 5_000_000;
let error = decode(&slice).expect_err("five million records from sixteen bytes");
assert!(error.to_string().contains("cannot hold"), "{error}");
let mut big = cram_slice(&[0u8; 4096], 1);
big.header.n_records = (MAX_RECORDS_PER_SLICE + 1) as i32;
assert!(decode(&big).is_err(), "past the per-slice ceiling");
let ordinary = cram_slice(&[0u8; 4096], 8);
let _ = decode(&ordinary);
}
#[test]
fn a_read_longer_than_its_slice_could_hold_is_refused() {
let huge = [0xf4u8, 0x00, 0x00, 0x00, 0x00];
let slice = cram_slice(&huge, 1);
let error = decode(&slice).expect_err("a read of 2^30 bases");
assert!(error.to_string().contains("a read of"), "{error}");
}
#[test]
fn the_read_lengths_of_a_slice_are_bounded_in_total() {
let mut data = Vec::new();
for _ in 0..64 {
data.extend_from_slice(&[0xe0, 0x01, 0x86, 0xa0]);
}
let slice = cram_slice(&data, 64);
let error = decode(&slice).expect_err("sixty-four hundred-kilobase reads");
let message = error.to_string();
assert!(
message.contains("total more than") || message.contains("cannot hold"),
"{message}"
);
}
#[test]
fn reference_bases_outside_the_window_read_as_n() {
let reference = ReferenceBases {
bases: b"ACGT",
start: 101,
};
assert_eq!(reference.at(101), b'A');
assert_eq!(reference.at(104), b'T');
assert_eq!(reference.at(100), b'N');
assert_eq!(reference.at(105), b'N');
let none = ReferenceBases::default();
assert_eq!(none.at(1), b'N');
}
#[test]
fn reference_bases_are_upper_cased() {
let reference = ReferenceBases {
bases: b"acgt",
start: 1,
};
assert_eq!(reference.at(1), b'A');
assert_eq!(reference.at(3), b'G');
}
#[test]
fn reg2bin_matches_the_sam_specification() {
assert_eq!(reg2bin(0, 1), 4681);
assert_eq!(reg2bin(0, 16384), 4681);
assert_eq!(reg2bin(16384, 16385), 4682);
assert_eq!(reg2bin(0, 16385), 585);
assert_eq!(reg2bin(0, 131073), 73);
assert_eq!(reg2bin(0, 1 << 20), 73);
assert_eq!(reg2bin(0, 1 << 26), 1);
assert_eq!(reg2bin(0, 1 << 29), 0);
assert_eq!(reg2bin(1 << 28, (1 << 28) + 1), 21065);
}
#[test]
fn base_codes_cover_the_iupac_alphabet_in_both_cases() {
for (base, code) in b"=ACMGRSVTWYHKDBN".iter().zip(0u8..) {
assert_eq!(BASE_CODES[*base as usize], code, "{}", *base as char);
}
assert_eq!(BASE_CODES[b'a' as usize], BASE_CODES[b'A' as usize]);
assert_eq!(BASE_CODES[b'n' as usize], BASE_CODES[b'N' as usize]);
assert_eq!(BASE_CODES[b'*' as usize], 15);
assert_eq!(BASE_CODES[0], 15);
}
}