gwseq-io 0.2.1

Rust library for processing bigWig, bigBed, BAM, CRAM and HiC files
Documentation
//! bzip2 — a block compression method, and the one thing here that is not
//! CRAM's own format.
//!
//! Reference: CRAM §14 (the method byte) and `docs/cram_codecs_v3.1.md` §4.3
//! (the arithmetic coder's `Ext` flag, which routes a whole stream here so that
//! `Pack` and `Stripe` can be applied before it — §4.3 calls that a layering
//! mistake kept for 3.0 compatibility).
//!
//! Unlike everything else in this module this is a general-purpose compressor
//! with an implementation to depend on, so there is one:
//! [`bzip2`](https://docs.rs/bzip2) over `libbz2-rs-sys`, pure Rust from the
//! people who wrote `zlib-rs`. The C bindings are that crate's opt-in
//! `bzip2-sys` feature and this workspace must never enable it — see
//! `ARCHITECTURE.md` §13.
//!
//! It is worth being clear about why this is here at all, because the byte
//! share understates it badly. `samtools` chooses bzip2 per block, by size, in
//! its `small` and `archive` profiles — no explicit option, both CRAM versions
//! — and on a real file it lands on `BF`, `AP`, `MQ` and `NF`: bitflags,
//! alignment positions, mapping qualities. That is under a tenth of the bytes
//! and all of whether the file opens.

use ::bzip2::read::BzDecoder;

use crate::error::{Error, Result};

/// The stream signature, which §4.3 requires be checked: `BZh` and a level
/// digit.
const MAGIC: &[u8] = b"BZh";

/// Decompress one bzip2 stream.
///
/// `raw_size` is a ceiling on the reserve rather than a promise — the crate's
/// rule about never allocating what a file named — and the caller checks the
/// length that actually came out.
pub fn decode(data: &[u8], raw_size: usize, path: &str, offset: u64) -> Result<Vec<u8>> {
    // §4.3: "the 'magic number' *must* be validated to check the external
    // codec being used", because `Ext` was left open to other codecs. Checked
    // here rather than in the caller so that both routes in — a §14 block
    // method and the arithmetic coder's `Ext` flag — get it.
    if !data.starts_with(MAGIC) {
        return Err(Error::corrupt(
            path,
            offset,
            "a bzip2 block that does not begin with the bzip2 signature",
        ));
    }
    let mut out = Vec::with_capacity(raw_size.min(1 << 20));
    // Bounded rather than checked afterwards; see `codecs::inflate_bounded`.
    super::inflate_bounded(
        BzDecoder::new(data),
        &mut out,
        raw_size,
        "bzip2",
        path,
        offset,
    )?;
    Ok(out)
}

#[cfg(test)]
mod tests {
    use super::*;

    use std::io::Write as _;

    fn compress(data: &[u8]) -> Vec<u8> {
        let mut encoder =
            ::bzip2::write::BzEncoder::new(Vec::new(), ::bzip2::Compression::default());
        encoder.write_all(data).expect("compress");
        encoder.finish().expect("finish")
    }

    #[test]
    fn a_bzip2_stream_round_trips() {
        let data = b"ACGTACGTNNNN".repeat(500);
        let compressed = compress(&data);
        assert!(compressed.starts_with(MAGIC));
        assert_eq!(
            decode(&compressed, data.len(), "test", 0).expect("decode"),
            data
        );
    }

    #[test]
    fn an_empty_stream_round_trips() {
        let compressed = compress(b"");
        assert_eq!(decode(&compressed, 0, "test", 0).expect("decode"), b"");
    }

    #[test]
    fn a_stream_without_the_signature_is_refused() {
        let error = decode(b"not bzip2 at all", 16, "test", 0).expect_err("no signature");
        assert!(error.to_string().contains("bzip2 signature"), "{error}");
    }

    #[test]
    fn a_truncated_stream_is_refused_rather_than_returning_what_it_had() {
        let compressed = compress(&b"ACGT".repeat(1000));
        let error = decode(&compressed[..compressed.len() / 2], 4000, "test", 0);
        assert!(error.is_err(), "half a stream decoded");
    }

    #[test]
    fn every_prefix_fails_without_panicking() {
        let compressed = compress(&b"ACGTNNNN".repeat(100));
        for cut in 0..compressed.len() {
            let _ = decode(&compressed[..cut], 800, "test", 0);
        }
    }
}