gwseq-io 0.2.1

Rust library for processing bigWig, bigBed, BAM, CRAM and HiC files
Documentation
//! LZMA — a block compression method, and the second of the two here that is
//! not CRAM's own format.
//!
//! Reference: CRAM §14.3, which is one sentence long and says the thing that
//! matters: "CRAM uses the xz Stream format to encapsulate this algorithm".
//! So a block is not a bare LZMA stream and not LZMA2 — it is a complete `.xz`
//! file, header, block, index and footer, and it is decoded as one.
//!
//! The implementation is [`lzma-rs`](https://docs.rs/lzma-rs), pure Rust with
//! no C anywhere in it, which is the only reason it is here — see
//! `ARCHITECTURE.md` §13.
//!
//! Unlike the other seven methods, nothing produces this by accident. No
//! `samtools` profile turns it on; it takes an explicit
//! `--output-fmt-option use_lzma=1`. That makes it the rarest method in the
//! format and the last one this reader learned, and it is here because a file
//! that uses it was previously refused outright — a whole file unreadable for
//! a flag someone set once.

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

/// The xz stream header's magic, §2.1.1.1 of the xz file format: `0xFD`, `7zXZ`
/// and a nul.
const MAGIC: &[u8] = b"\xfd7zXZ\x00";

/// Decompress one xz 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>> {
    // The same check §4.3 demands of bzip2, for the same reason and with more
    // of a signature to check: six bytes rather than three.
    if !data.starts_with(MAGIC) {
        return Err(Error::corrupt(
            path,
            offset,
            "an lzma block that does not begin with the xz stream signature",
        ));
    }
    let mut out = Bounded {
        out: Vec::with_capacity(raw_size.min(1 << 20)),
        limit: raw_size.min(super::MAX_BLOCK_RAW_SIZE),
    };
    // `lzma-rs` writes into a sink rather than handing back a reader, so the
    // bound goes on the sink — `inflate_bounded`'s job, done the other way
    // round. Same rule: stop at the ceiling rather than allocate to it and
    // check afterwards.
    lzma_rs::xz_decompress(&mut std::io::Cursor::new(data), &mut out).map_err(|e| {
        Error::corrupt(
            path,
            offset,
            format!("could not inflate an lzma block: {e}"),
        )
    })?;
    Ok(out.out)
}

/// A sink that refuses to grow past `limit`.
struct Bounded {
    out: Vec<u8>,
    limit: usize,
}

impl std::io::Write for Bounded {
    fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
        if self.out.len() + data.len() > self.limit {
            return Err(std::io::Error::other(format!(
                "inflating past the {} bytes it declared",
                self.limit
            )));
        }
        self.out.write(data)
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

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

    fn compress(data: &[u8]) -> Vec<u8> {
        let mut out = Vec::new();
        lzma_rs::xz_compress(&mut std::io::Cursor::new(data), &mut out).expect("compress");
        out
    }

    #[test]
    fn an_xz_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 xz at all!!!", 16, "test", 0).expect_err("no signature");
        assert!(error.to_string().contains("xz stream signature"), "{error}");
    }

    /// A bare LZMA stream, which is what the method's name suggests and what
    /// §14.3 says it is not.
    #[test]
    fn a_bare_lzma_stream_is_refused_rather_than_half_read() {
        let mut bare = Vec::new();
        lzma_rs::lzma_compress(
            &mut std::io::Cursor::new(&b"ACGT".repeat(100)[..]),
            &mut bare,
        )
        .expect("compress");
        assert!(decode(&bare, 400, "test", 0).is_err(), "a bare lzma stream");
    }

    #[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");
    }

    /// A block that inflates past what it declared is stopped at the ceiling
    /// rather than allowed to allocate to whatever it wants.
    #[test]
    fn a_stream_longer_than_its_declared_size_is_refused() {
        let data = b"ACGT".repeat(1000);
        let compressed = compress(&data);
        let error = decode(&compressed, 100, "test", 0).expect_err("past its declared size");
        assert!(error.to_string().contains("lzma block"), "{error}");
    }

    #[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);
        }
    }
}