use bytes::Bytes;
use crate::bytes::LeCursor;
use crate::error::{Error, Result};
use crate::genomic::IndexedLoc;
pub const WIG_HEADER_SIZE: usize = 24;
pub const ZOOM_RECORD_SIZE: usize = 32;
pub const BED_RECORD_HEADER_SIZE: usize = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WigEncoding {
BedGraph = 1,
VarStep = 2,
FixedStep = 3,
}
impl WigEncoding {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
1 => Some(WigEncoding::BedGraph),
2 => Some(WigEncoding::VarStep),
3 => Some(WigEncoding::FixedStep),
_ => None,
}
}
pub fn item_size(self) -> usize {
match self {
WigEncoding::BedGraph => 12,
WigEncoding::VarStep => 8,
WigEncoding::FixedStep => 4,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct WigSectionHeader {
pub chr_index: u32,
pub chr_start: i64,
#[allow(dead_code)]
pub chr_end: i64,
pub item_step: i64,
pub item_span: i64,
pub encoding: WigEncoding,
pub item_count: u16,
}
pub fn read_wig_header(block: &[u8], path: &str) -> Result<WigSectionHeader> {
if block.len() < WIG_HEADER_SIZE {
return Err(Error::corrupt(
path,
0,
format!(
"wig section header needs {WIG_HEADER_SIZE} bytes, its block holds {}",
block.len()
),
));
}
let mut c = LeCursor::new(block, 0, path);
let chr_index = c.read_u32()?;
let chr_start = c.read_u32()? as i64;
let chr_end = c.read_u32()? as i64;
let item_step = c.read_u32()? as i64;
let item_span = c.read_u32()? as i64;
let type_byte = c.read_u8()?;
c.skip(1)?; let item_count = c.read_u16()?;
let encoding = WigEncoding::from_u8(type_byte)
.ok_or_else(|| Error::corrupt(path, 20, format!("wig data type {type_byte} invalid")))?;
Ok(WigSectionHeader {
chr_index,
chr_start,
chr_end,
item_step,
item_span,
encoding,
item_count,
})
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DataInterval {
pub chr_index: u32,
pub start: i64,
pub end: i64,
pub value: f32,
pub valid_count: i64,
pub min_value: f32,
pub max_value: f32,
pub sum_squared: f64,
}
pub fn read_wig_item(
block: &[u8],
header: &WigSectionHeader,
index: usize,
path: &str,
) -> Result<DataInterval> {
let item_size = header.encoding.item_size();
let offset = WIG_HEADER_SIZE + index * item_size;
let mut c = LeCursor::new(block, 0, path);
c.seek(offset)?;
let (start, end, value) = match header.encoding {
WigEncoding::BedGraph => {
let start = c.read_u32()? as i64;
let end = c.read_u32()? as i64;
(start, end, c.read_f32()?)
}
WigEncoding::VarStep => {
let start = c.read_u32()? as i64;
(start, start + header.item_span, c.read_f32()?)
}
WigEncoding::FixedStep => {
let start = header.chr_start + index as i64 * header.item_step;
(start, start + header.item_span, c.read_f32()?)
}
};
let valid_count = end - start;
Ok(DataInterval {
chr_index: header.chr_index,
start,
end,
value,
valid_count,
min_value: value,
max_value: value,
sum_squared: value as f64 * value as f64 * valid_count as f64,
})
}
#[derive(Debug, Clone, Copy)]
pub struct ZoomRecord {
pub chr_index: u32,
pub chr_start: i64,
pub chr_end: i64,
pub valid_count: i64,
pub min_value: f32,
pub max_value: f32,
pub sum_data: f32,
pub sum_squared: f32,
}
pub fn read_zoom_record(block: &[u8], offset: usize, path: &str) -> Result<ZoomRecord> {
let mut c = LeCursor::new(block, 0, path);
c.seek(offset)?;
Ok(ZoomRecord {
chr_index: c.read_u32()?,
chr_start: c.read_u32()? as i64,
chr_end: c.read_u32()? as i64,
valid_count: c.read_u32()? as i64,
min_value: c.read_f32()?,
max_value: c.read_f32()?,
sum_data: c.read_f32()?,
sum_squared: c.read_f32()?,
})
}
#[derive(Debug, Clone)]
pub struct BedEntry {
pub chr: String,
pub start: i64,
pub end: i64,
pub fields: Vec<(String, String)>,
}
const MAX_INFLATED_SIZE: usize = 1 << 30;
const MAX_INFLATE_RESERVE: usize = 1 << 20;
pub fn decompress(block: Bytes, uncompress_buffer_size: u32, path: &str) -> Result<Bytes> {
decompress_limited(block, uncompress_buffer_size, path, MAX_INFLATED_SIZE)
}
fn decompress_limited(
block: Bytes,
uncompress_buffer_size: u32,
path: &str,
max_size: usize,
) -> Result<Bytes> {
if uncompress_buffer_size == 0 {
return Ok(block);
}
use std::io::Read;
let reserve = (uncompress_buffer_size as usize).min(MAX_INFLATE_RESERVE.min(max_size));
let mut out = Vec::with_capacity(reserve);
flate2::read::ZlibDecoder::new(&block[..])
.take(max_size as u64 + 1)
.read_to_end(&mut out)
.map_err(|e| Error::corrupt(path, 0, format!("could not inflate data block: {e}")))?;
if out.len() > max_size {
return Err(Error::corrupt(
path,
0,
format!("decompressed data exceeds limit ({max_size})"),
));
}
Ok(Bytes::from(out))
}
#[derive(Debug, Clone, Copy)]
struct LocBounds {
min_chr: u32,
min_start: i64,
max_chr: u32,
max_end: i64,
}
impl LocBounds {
fn of(locs: &[IndexedLoc], range: std::ops::Range<usize>) -> Self {
let first = &locs[range.start];
let last = &locs[range.end - 1];
let max_chr = last.chr_index as u32;
let mut max_end = last.binned_end;
for loc in locs[range.start..range.end - 1].iter().rev() {
if loc.chr_index as u32 != max_chr {
break;
}
max_end = max_end.max(loc.binned_end);
}
Self {
min_chr: first.chr_index as u32,
min_start: first.binned_start,
max_chr,
max_end,
}
}
}
#[derive(Debug)]
pub struct DataIntervals {
block: Bytes,
path: String,
bounds: LocBounds,
header: Option<WigSectionHeader>,
count: usize,
index: usize,
}
impl DataIntervals {
pub fn new(
block: Bytes,
zoom: bool,
locs: &[IndexedLoc],
range: std::ops::Range<usize>,
path: &str,
) -> Result<Self> {
let (header, count) = if zoom {
(None, block.len() / ZOOM_RECORD_SIZE)
} else {
let header = read_wig_header(&block, path)?;
let count = header.item_count as usize;
let item_size = header.encoding.item_size();
if WIG_HEADER_SIZE + count * item_size > block.len() {
return Err(Error::corrupt(
path,
0,
format!(
"wig section declares {count} items of type {:?}, which do not fit \
its {} byte block",
header.encoding,
block.len()
),
));
}
(Some(header), count)
};
Ok(Self {
block,
path: path.to_string(),
bounds: LocBounds::of(locs, range),
header,
count,
index: 0,
})
}
}
impl Iterator for DataIntervals {
type Item = Result<DataInterval>;
fn next(&mut self) -> Option<Self::Item> {
while self.index < self.count {
let index = self.index;
self.index += 1;
let data = match self.header {
Some(header) => match read_wig_item(&self.block, &header, index, &self.path) {
Ok(d) => d,
Err(e) => return Some(Err(e)),
},
None => {
let record =
match read_zoom_record(&self.block, index * ZOOM_RECORD_SIZE, &self.path) {
Ok(r) => r,
Err(e) => return Some(Err(e)),
};
if record.valid_count == 0 {
continue;
}
DataInterval {
chr_index: record.chr_index,
start: record.chr_start,
end: record.chr_end,
value: record.sum_data / record.valid_count as f32,
valid_count: record.valid_count,
min_value: record.min_value,
max_value: record.max_value,
sum_squared: record.sum_squared as f64,
}
}
};
let b = &self.bounds;
if data.chr_index < b.min_chr {
continue;
}
if data.chr_index == b.min_chr && data.end <= b.min_start {
continue;
}
if data.chr_index > b.max_chr {
break;
}
if data.chr_index == b.max_chr && data.start >= b.max_end {
break;
}
return Some(Ok(data));
}
None
}
}
#[derive(Debug)]
pub struct BedRecords<'a> {
block: Bytes,
path: &'a str,
auto_sql: &'a indexmap::IndexMap<String, String>,
field_count: usize,
kept: usize,
bounds: LocBounds,
offset: usize,
}
impl<'a> BedRecords<'a> {
pub fn new(
block: Bytes,
auto_sql: &'a indexmap::IndexMap<String, String>,
col_count: usize,
locs: &[IndexedLoc],
range: std::ops::Range<usize>,
path: &'a str,
) -> Result<Self> {
if auto_sql.len() < 3 {
return Err(Error::format(
path,
format!(
"bed entries need the 3 standard fields, autosql describes {}",
auto_sql.len()
),
));
}
let field_count = auto_sql.len() - 3;
let kept = if col_count == 0 {
field_count
} else {
field_count.min(col_count.saturating_sub(3))
};
Ok(Self {
block,
path,
auto_sql,
field_count,
kept,
bounds: LocBounds::of(locs, range),
offset: 0,
})
}
fn read_fields(&self, tail: &[u8]) -> Result<Vec<(String, String)>> {
let mut fields = Vec::with_capacity(self.kept);
let mut found = 0usize;
if !tail.is_empty() || self.field_count > 0 {
for part in tail.split(|b| *b == b'\t') {
if found < self.kept {
let name = self
.auto_sql
.get_index(3 + found)
.map(|(k, _)| k.clone())
.unwrap_or_else(|| format!("field{}", 4 + found));
fields.push((name, String::from_utf8_lossy(part).into_owned()));
}
found += 1;
}
}
if found != self.field_count {
return Err(Error::corrupt(
self.path,
self.offset as u64,
format!(
"invalid bed entry (found {found} fields past the first 3, \
autosql declares {})",
self.field_count
),
));
}
Ok(fields)
}
}
impl Iterator for BedRecords<'_> {
type Item = Result<(u32, i64, i64, Vec<(String, String)>)>;
fn next(&mut self) -> Option<Self::Item> {
while self.offset < self.block.len() {
if self.offset + BED_RECORD_HEADER_SIZE > self.block.len() {
return Some(Err(Error::corrupt(
self.path,
self.offset as u64,
format!(
"truncated bed record at {} ({} bytes left in its block)",
self.offset,
self.block.len() - self.offset
),
)));
}
let head = &self.block[self.offset..self.offset + BED_RECORD_HEADER_SIZE];
let chr_index = u32::from_le_bytes([head[0], head[1], head[2], head[3]]);
let start = u32::from_le_bytes([head[4], head[5], head[6], head[7]]) as i64;
let end = u32::from_le_bytes([head[8], head[9], head[10], head[11]]) as i64;
let tail_start = self.offset + BED_RECORD_HEADER_SIZE;
let Some(nul) = memchr::memchr(0, &self.block[tail_start..]) else {
return Some(Err(Error::corrupt(
self.path,
tail_start as u64,
"invalid bed entry (null terminator not found)",
)));
};
let tail_end = tail_start + nul;
let b = &self.bounds;
let reach = end.max(start + 1);
let reachable =
!(chr_index < b.min_chr || (chr_index == b.min_chr && reach <= b.min_start));
let past = chr_index > b.max_chr || (chr_index == b.max_chr && start >= b.max_end);
let fields = if reachable && !past {
match self.read_fields(&self.block[tail_start..tail_end]) {
Ok(f) => Some(f),
Err(e) => return Some(Err(e)),
}
} else {
None
};
self.offset = tail_end + 1;
if past {
break;
}
if let Some(fields) = fields {
return Some(Ok((chr_index, start, end, fields)));
}
}
None
}
}
pub fn visit_bed_records(
block: &[u8],
path: &str,
mut visit: impl FnMut(u32, i64, i64),
) -> Result<()> {
let mut offset = 0usize;
while offset < block.len() {
if offset + BED_RECORD_HEADER_SIZE > block.len() {
return Err(Error::corrupt(
path,
offset as u64,
format!(
"truncated bed record at {offset} ({} bytes left in its block)",
block.len() - offset
),
));
}
let head = &block[offset..offset + BED_RECORD_HEADER_SIZE];
let chr_index = u32::from_le_bytes([head[0], head[1], head[2], head[3]]);
let start = u32::from_le_bytes([head[4], head[5], head[6], head[7]]) as i64;
let end = u32::from_le_bytes([head[8], head[9], head[10], head[11]]) as i64;
let tail_start = offset + BED_RECORD_HEADER_SIZE;
let Some(nul) = memchr::memchr(0, &block[tail_start..]) else {
return Err(Error::corrupt(
path,
tail_start as u64,
"invalid bed entry (null terminator not found)",
));
};
offset = tail_start + nul + 1;
visit(chr_index, start, end);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn deflated(bytes: &[u8]) -> Vec<u8> {
use std::io::Write as _;
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
e.write_all(bytes).unwrap();
e.finish().unwrap()
}
#[test]
fn a_declared_buffer_size_is_a_hint_and_not_an_allocation() {
let block = bytes::Bytes::from(deflated(&[7u8; 32]));
let out = decompress(block, u32::MAX, "corrupt.bigwig").unwrap();
assert_eq!(out.len(), 32);
assert!(out.iter().all(|b| *b == 7));
}
#[test]
fn a_block_that_inflates_past_the_limit_is_refused_rather_than_read() {
let block = bytes::Bytes::from(deflated(&vec![0u8; 4097]));
let err = decompress_limited(block, 4096, "bomb.bigwig", 4096)
.unwrap_err()
.to_string();
assert!(err.contains("exceeds limit (4096)"), "{err}");
}
#[test]
fn a_block_exactly_at_the_limit_is_still_read() {
let block = bytes::Bytes::from(deflated(&vec![0u8; 4096]));
assert_eq!(
decompress_limited(block, 4096, "big.bigwig", 4096)
.unwrap()
.len(),
4096
);
}
fn loc(chr: usize, start: i64, end: i64) -> IndexedLoc {
IndexedLoc {
chr_index: chr,
start,
end,
binned_start: start,
binned_end: end,
bin_size: 1.0,
reverse: false,
output_start: 0,
output_end: (end - start) as usize,
}
}
fn wig_block(
encoding: WigEncoding,
items: &[(u32, u32, f32)],
span: u32,
step: u32,
) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&7u32.to_le_bytes()); b.extend_from_slice(&items[0].0.to_le_bytes()); b.extend_from_slice(&items[items.len() - 1].1.to_le_bytes()); b.extend_from_slice(&step.to_le_bytes());
b.extend_from_slice(&span.to_le_bytes());
b.push(encoding as u8);
b.push(0);
b.extend_from_slice(&(items.len() as u16).to_le_bytes());
assert_eq!(b.len(), WIG_HEADER_SIZE);
for (start, end, value) in items {
match encoding {
WigEncoding::BedGraph => {
b.extend_from_slice(&start.to_le_bytes());
b.extend_from_slice(&end.to_le_bytes());
b.extend_from_slice(&value.to_le_bytes());
}
WigEncoding::VarStep => {
b.extend_from_slice(&start.to_le_bytes());
b.extend_from_slice(&value.to_le_bytes());
}
WigEncoding::FixedStep => b.extend_from_slice(&value.to_le_bytes()),
}
}
b
}
fn collect(block: Vec<u8>, zoom: bool, locs: &[IndexedLoc]) -> Vec<DataInterval> {
DataIntervals::new(Bytes::from(block), zoom, locs, 0..locs.len(), "test")
.unwrap()
.map(|r| r.unwrap())
.collect()
}
#[test]
fn the_three_encodings_decode_to_the_same_intervals() {
let items = [(100u32, 110u32, 1.5f32), (110, 120, 2.5), (120, 130, 3.5)];
let locs = [loc(7, 0, 1000)];
let bg = collect(
wig_block(WigEncoding::BedGraph, &items, 10, 10),
false,
&locs,
);
let vs = collect(
wig_block(WigEncoding::VarStep, &items, 10, 10),
false,
&locs,
);
let fs = collect(
wig_block(WigEncoding::FixedStep, &items, 10, 10),
false,
&locs,
);
assert_eq!(bg.len(), 3);
assert_eq!(bg, vs);
assert_eq!(bg, fs);
assert_eq!(bg[0].start, 100);
assert_eq!(bg[0].end, 110);
assert_eq!(bg[0].value, 1.5);
assert_eq!(bg[0].valid_count, 10);
assert_eq!(bg[0].min_value, 1.5);
assert_eq!(bg[0].sum_squared, 1.5f64 * 1.5 * 10.0);
assert_eq!(bg[0].chr_index, 7);
}
#[test]
fn items_outside_the_batchs_bounds_are_skipped_and_the_walk_stops() {
let items = [
(0u32, 10u32, 1.0f32),
(10, 20, 2.0),
(100, 110, 3.0),
(200, 210, 4.0),
(300, 310, 5.0),
];
let locs = [loc(7, 100, 210)];
let got = collect(
wig_block(WigEncoding::BedGraph, &items, 10, 10),
false,
&locs,
);
assert_eq!(got.iter().map(|d| d.start).collect::<Vec<_>>(), [100, 200]);
}
#[test]
fn a_block_straddling_two_chromosomes_keeps_only_the_reachable_side() {
let mut b = wig_block(WigEncoding::BedGraph, &[(0, 10, 1.0)], 10, 10);
b[0..4].copy_from_slice(&8u32.to_le_bytes());
let locs = [loc(8, 0, 10)];
assert_eq!(collect(b.clone(), false, &locs).len(), 1);
let locs = [loc(9, 0, 10)];
assert!(collect(b, false, &locs).is_empty());
}
#[test]
fn zoom_records_become_intervals_and_zero_count_ones_are_dropped() {
let mut b = Vec::new();
for (start, end, valid, min, max, sum, sq) in [
(0u32, 100u32, 100u32, 1.0f32, 5.0f32, 300.0f32, 1000.0f32),
(100, 200, 0, 0.0, 0.0, 0.0, 0.0), (200, 300, 50, 2.0, 4.0, 150.0, 500.0),
] {
b.extend_from_slice(&7u32.to_le_bytes());
b.extend_from_slice(&start.to_le_bytes());
b.extend_from_slice(&end.to_le_bytes());
b.extend_from_slice(&valid.to_le_bytes());
b.extend_from_slice(&min.to_le_bytes());
b.extend_from_slice(&max.to_le_bytes());
b.extend_from_slice(&sum.to_le_bytes());
b.extend_from_slice(&sq.to_le_bytes());
}
let locs = [loc(7, 0, 1000)];
let got = collect(b, true, &locs);
assert_eq!(got.len(), 2);
assert_eq!(got[0].value, 3.0);
assert_eq!(got[0].valid_count, 100);
assert_eq!(got[0].min_value, 1.0);
assert_eq!(got[0].max_value, 5.0);
assert_eq!(got[1].value, 3.0);
assert_eq!(got[1].valid_count, 50);
}
#[test]
fn a_bad_wig_type_is_corrupt_not_a_panic() {
let mut b = wig_block(WigEncoding::BedGraph, &[(0, 10, 1.0)], 10, 10);
b[20] = 9;
let locs = [loc(7, 0, 10)];
let err = DataIntervals::new(Bytes::from(b), false, &locs, 0..1, "test").unwrap_err();
assert!(err.to_string().contains("wig data type 9 invalid"), "{err}");
}
#[test]
fn a_block_declaring_more_items_than_it_holds_is_refused_up_front() {
let mut b = wig_block(WigEncoding::BedGraph, &[(0, 10, 1.0)], 10, 10);
b[22..24].copy_from_slice(&500u16.to_le_bytes());
let locs = [loc(7, 0, 10)];
let err = DataIntervals::new(Bytes::from(b), false, &locs, 0..1, "test").unwrap_err();
assert!(err.to_string().contains("do not fit"), "{err}");
}
#[test]
fn a_block_too_short_for_a_header_is_refused() {
let locs = [loc(7, 0, 10)];
let err =
DataIntervals::new(Bytes::from(vec![0u8; 8]), false, &locs, 0..1, "test").unwrap_err();
assert!(err.to_string().contains("header needs 24 bytes"), "{err}");
}
#[test]
fn an_uncompressed_file_hands_its_block_back_untouched() {
let block = Bytes::from(vec![1u8, 2, 3]);
assert_eq!(decompress(block.clone(), 0, "test").unwrap(), block);
}
#[test]
fn a_compressed_block_round_trips() {
use std::io::Write;
let raw: Vec<u8> = (0..5000).map(|i| (i % 251) as u8).collect();
let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::new(6));
encoder.write_all(&raw).unwrap();
let compressed = Bytes::from(encoder.finish().unwrap());
assert_eq!(&decompress(compressed, 8192, "test").unwrap()[..], &raw[..]);
}
#[test]
fn garbage_where_a_deflate_stream_should_be_is_corrupt() {
let err = decompress(Bytes::from(vec![9u8; 64]), 4096, "test").unwrap_err();
assert!(err.to_string().contains("could not inflate"), "{err}");
}
#[test]
fn bounds_take_the_widest_end_on_the_last_chromosome() {
let locs = [loc(1, 0, 10), loc(2, 0, 500), loc(2, 100, 200)];
let bounds = LocBounds::of(&locs, 0..3);
assert_eq!(bounds.min_chr, 1);
assert_eq!(bounds.min_start, 0);
assert_eq!(bounds.max_chr, 2);
assert_eq!(bounds.max_end, 500);
}
}