use std::collections::HashMap;
use crate::bytes::LeCursor;
use crate::error::{Error, Result};
use super::container::read_itf8;
use super::encoding::Encoding;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubstitutionMatrix {
table: [[u8; 4]; 5],
}
const BASES: [u8; 5] = *b"ACGTN";
impl Default for SubstitutionMatrix {
fn default() -> Self {
Self {
table: [[b'N'; 4]; 5],
}
}
}
impl SubstitutionMatrix {
pub fn parse(bytes: &[u8]) -> Self {
let mut table = [[b'N'; 4]; 5];
for (r, &packed) in bytes.iter().enumerate().take(5) {
let mut shift = 6;
for &base in BASES.iter() {
if base == BASES[r] {
continue;
}
let code = (packed >> shift) & 3;
table[r][code as usize] = base;
shift -= 2;
}
}
Self { table }
}
pub fn substitute(&self, reference: u8, code: u8) -> u8 {
let row = match reference.to_ascii_uppercase() {
b'A' => 0,
b'C' => 1,
b'G' => 2,
b'T' => 3,
_ => 4,
};
self.table[row][(code & 3) as usize]
}
}
#[derive(Debug, Clone)]
pub struct PreservationMap {
pub read_names_included: bool,
pub ap_delta: bool,
pub reference_required: bool,
pub substitution_matrix: SubstitutionMatrix,
pub tag_dictionary: Vec<Vec<[u8; 3]>>,
}
impl Default for PreservationMap {
fn default() -> Self {
Self {
read_names_included: true,
ap_delta: true,
reference_required: true,
substitution_matrix: SubstitutionMatrix::default(),
tag_dictionary: Vec::new(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct DataSeries {
pub bf: Encoding,
pub cf: Encoding,
pub ri: Encoding,
pub rl: Encoding,
pub ap: Encoding,
pub rg: Encoding,
pub rn: Encoding,
pub mf: Encoding,
pub ns: Encoding,
pub np: Encoding,
pub ts: Encoding,
pub nf: Encoding,
pub tl: Encoding,
pub fn_: Encoding,
pub fc: Encoding,
pub fp: Encoding,
pub dl: Encoding,
pub bb: Encoding,
pub qq: Encoding,
pub bs: Encoding,
pub in_: Encoding,
pub rs: Encoding,
pub pd: Encoding,
pub hc: Encoding,
pub sc: Encoding,
pub mq: Encoding,
pub ba: Encoding,
pub qs: Encoding,
}
impl DataSeries {
fn set(&mut self, key: [u8; 2], encoding: Encoding) {
let slot = match &key {
b"BF" => &mut self.bf,
b"CF" => &mut self.cf,
b"RI" => &mut self.ri,
b"RL" => &mut self.rl,
b"AP" => &mut self.ap,
b"RG" => &mut self.rg,
b"RN" => &mut self.rn,
b"MF" => &mut self.mf,
b"NS" => &mut self.ns,
b"NP" => &mut self.np,
b"TS" => &mut self.ts,
b"NF" => &mut self.nf,
b"TL" => &mut self.tl,
b"FN" => &mut self.fn_,
b"FC" => &mut self.fc,
b"FP" => &mut self.fp,
b"DL" => &mut self.dl,
b"BB" => &mut self.bb,
b"QQ" => &mut self.qq,
b"BS" => &mut self.bs,
b"IN" => &mut self.in_,
b"RS" => &mut self.rs,
b"PD" => &mut self.pd,
b"HC" => &mut self.hc,
b"SC" => &mut self.sc,
b"MQ" => &mut self.mq,
b"BA" => &mut self.ba,
b"QS" => &mut self.qs,
_ => return,
};
*slot = encoding;
}
fn each(&self) -> impl Iterator<Item = &Encoding> {
[
&self.bf, &self.cf, &self.ri, &self.rl, &self.ap, &self.rg, &self.rn, &self.mf,
&self.ns, &self.np, &self.ts, &self.nf, &self.tl, &self.fn_, &self.fc, &self.fp,
&self.dl, &self.bb, &self.qq, &self.bs, &self.in_, &self.rs, &self.pd, &self.hc,
&self.sc, &self.mq, &self.ba, &self.qs,
]
.into_iter()
}
}
#[derive(Debug, Clone)]
pub struct CompressionHeader {
pub preservation: PreservationMap,
pub series: DataSeries,
pub tags: HashMap<i32, Encoding>,
}
impl CompressionHeader {
pub fn parse(data: &[u8], path: &str) -> Result<Self> {
let mut cursor = LeCursor::new(data, 0, path);
let preservation = Self::parse_preservation(&mut cursor)?;
let series = Self::parse_series(&mut cursor)?;
let tags = Self::parse_tags(&mut cursor)?;
Ok(Self {
preservation,
series,
tags,
})
}
fn map_extent(cursor: &mut LeCursor<'_>) -> Result<(usize, usize)> {
let n_bytes = read_itf8(cursor)?;
if n_bytes < 0 {
return Err(Error::corrupt(
cursor.path(),
cursor.file_offset(),
format!("a compression header map of {n_bytes} bytes"),
));
}
let end = cursor.position() + n_bytes as usize;
let count = read_itf8(cursor)?;
if count < 0 {
return Err(Error::corrupt(
cursor.path(),
cursor.file_offset(),
format!("a compression header map of {count} entries"),
));
}
Ok((end, count as usize))
}
fn parse_preservation(cursor: &mut LeCursor<'_>) -> Result<PreservationMap> {
let (end, count) = Self::map_extent(cursor)?;
let mut map = PreservationMap::default();
for _ in 0..count {
let key: [u8; 2] = cursor.take(2)?.try_into().expect("two bytes");
match &key {
b"RN" => map.read_names_included = cursor.take(1)?[0] != 0,
b"AP" => map.ap_delta = cursor.take(1)?[0] != 0,
b"RR" => map.reference_required = cursor.take(1)?[0] != 0,
b"SM" => map.substitution_matrix = SubstitutionMatrix::parse(cursor.take(5)?),
b"TD" => {
let n = read_itf8(cursor)?;
if n < 0 {
return Err(Error::corrupt(
cursor.path(),
cursor.file_offset(),
format!("a tag dictionary of {n} bytes"),
));
}
map.tag_dictionary = parse_tag_dictionary(cursor.take(n as usize)?);
}
_ => break,
}
}
cursor.seek(end)?;
Ok(map)
}
fn parse_series(cursor: &mut LeCursor<'_>) -> Result<DataSeries> {
let (end, count) = Self::map_extent(cursor)?;
let mut series = DataSeries::default();
for _ in 0..count {
let key: [u8; 2] = cursor.take(2)?.try_into().expect("two bytes");
series.set(key, Encoding::read(cursor)?);
}
cursor.seek(end)?;
Ok(series)
}
fn parse_tags(cursor: &mut LeCursor<'_>) -> Result<HashMap<i32, Encoding>> {
let (end, count) = Self::map_extent(cursor)?;
let mut tags = HashMap::with_capacity(count.min(1024));
for _ in 0..count {
let key = read_itf8(cursor)?;
tags.insert(key, Encoding::read(cursor)?);
}
cursor.seek(end)?;
Ok(tags)
}
pub fn block_ids(&self) -> Vec<i32> {
let mut out = Vec::new();
for encoding in self.series.each() {
encoding.block_ids(&mut out);
}
for encoding in self.tags.values() {
encoding.block_ids(&mut out);
}
out.sort_unstable();
out.dedup();
out
}
pub fn tag_list(&self, index: i32, path: &str) -> Result<&[[u8; 3]]> {
self.preservation
.tag_dictionary
.get(usize::try_from(index).unwrap_or(usize::MAX))
.map(Vec::as_slice)
.ok_or_else(|| {
Error::corrupt(
path,
0,
format!(
"a record names tag list {index}, and the dictionary holds {}",
self.preservation.tag_dictionary.len()
),
)
})
}
}
fn parse_tag_dictionary(data: &[u8]) -> Vec<Vec<[u8; 3]>> {
let mut out = Vec::new();
let mut rest = data;
while !rest.is_empty() {
let end = memchr::memchr(0, rest).unwrap_or(rest.len());
let (entry, tail) = rest.split_at(end);
out.push(entry.chunks_exact(3).map(|c| [c[0], c[1], c[2]]).collect());
rest = tail.get(1..).unwrap_or(&[]);
}
out
}
pub fn tag_key(tag: [u8; 3]) -> i32 {
(i32::from(tag[0]) << 16) | (i32::from(tag[1]) << 8) | i32::from(tag[2])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_spec_substitution_matrix_inverts_to_the_table_it_prints() {
let matrix = SubstitutionMatrix::parse(&[0x63, 0x4b, 0x87, 0x27, 0x1b]);
let expected: [(u8, [u8; 4]); 5] = [
(b'A', *b"TCGN"),
(b'C', *b"GATN"),
(b'G', *b"CTAN"),
(b'T', *b"AGCN"),
(b'N', *b"ACGT"),
];
for (reference, row) in expected {
for (code, base) in row.iter().enumerate() {
assert_eq!(
matrix.substitute(reference, code as u8),
*base,
"ref {} code {code}",
reference as char
);
}
}
}
#[test]
fn every_row_of_a_well_formed_substitution_matrix_is_a_permutation() {
let permutations: Vec<[u8; 4]> = {
let mut out = Vec::new();
for a in 0..4u8 {
for b in 0..4u8 {
for c in 0..4u8 {
for d in 0..4u8 {
let code = [a, b, c, d];
let mut sorted = code;
sorted.sort_unstable();
if sorted == [0, 1, 2, 3] {
out.push(code);
}
}
}
}
}
out
};
assert_eq!(permutations.len(), 24);
for (i, _) in permutations.iter().enumerate() {
let packed: Vec<u8> = (0..5)
.map(|r| {
let p = permutations[(i + r) % permutations.len()];
(p[0] << 6) | (p[1] << 4) | (p[2] << 2) | p[3]
})
.collect();
let matrix = SubstitutionMatrix::parse(&packed);
for &reference in BASES.iter() {
let mut seen: Vec<u8> = (0..4).map(|c| matrix.substitute(reference, c)).collect();
seen.sort_unstable();
let mut want: Vec<u8> = BASES.iter().copied().filter(|b| *b != reference).collect();
want.sort_unstable();
assert_eq!(seen, want, "ref {}", reference as char);
}
}
}
#[test]
fn a_code_no_substitute_claims_reads_as_an_unknown_base() {
let matrix = SubstitutionMatrix::parse(&[0x00, 0, 0, 0, 0]);
assert_eq!(matrix.substitute(b'A', 1), b'N');
assert_eq!(matrix.substitute(b'A', 2), b'N');
}
#[test]
fn the_reference_base_is_matched_case_insensitively() {
let matrix = SubstitutionMatrix::parse(&[0x63, 0x4b, 0x87, 0x27, 0x1b]);
assert_eq!(matrix.substitute(b'a', 0), matrix.substitute(b'A', 0));
assert_eq!(matrix.substitute(b'g', 2), matrix.substitute(b'G', 2));
assert_eq!(matrix.substitute(b'M', 1), matrix.substitute(b'N', 1));
}
#[test]
fn the_tag_dictionary_splits_into_lists_of_three_byte_descriptors() {
let entries = parse_tag_dictionary(b"X1CBCZSAZ\0X1CBCZ\0");
assert_eq!(entries.len(), 2);
assert_eq!(entries[0], vec![*b"X1C", *b"BCZ", *b"SAZ"]);
assert_eq!(entries[1], vec![*b"X1C", *b"BCZ"]);
}
#[test]
fn a_tag_key_is_its_three_bytes_read_as_an_integer() {
assert_eq!(tag_key(*b"OQZ"), 0x004F_515A);
}
#[test]
fn the_eof_containers_empty_compression_header_parses() {
let header = CompressionHeader::parse(&[0x01, 0x00, 0x01, 0x00, 0x01, 0x00], "test")
.expect("an empty header");
assert!(header.tags.is_empty());
assert!(header.series.bf.is_null());
assert!(header.preservation.read_names_included);
assert!(header.preservation.ap_delta);
assert!(header.preservation.reference_required);
}
#[test]
fn a_preservation_map_reads_its_flags_and_matrix() {
let mut body = vec![0u8];
body.extend_from_slice(b"RN");
body.push(0);
body.extend_from_slice(b"AP");
body.push(0);
body.extend_from_slice(b"SM");
body.extend_from_slice(&[0x63, 0x4b, 0x87, 0x27, 0x1b]);
body.extend_from_slice(b"TD");
body.push(4);
body.extend_from_slice(b"X1C\0");
body[0] = 4;
let mut data = vec![body.len() as u8];
data.extend_from_slice(&body);
data.extend_from_slice(&[0x01, 0x00, 0x01, 0x00]);
let header = CompressionHeader::parse(&data, "test").expect("a header");
assert!(!header.preservation.read_names_included);
assert!(!header.preservation.ap_delta);
assert!(header.preservation.reference_required); assert_eq!(header.preservation.tag_dictionary, vec![vec![*b"X1C"]]);
assert_eq!(
header.preservation.substitution_matrix.substitute(b'A', 0),
b'T'
);
}
}