use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Compression {
None,
Zlib,
Lzo,
Zstd,
}
impl Compression {
pub fn from_byte(v: u8) -> Result<Self> {
match v {
0 => Ok(Compression::None),
1 => Ok(Compression::Zlib),
2 => Ok(Compression::Lzo),
3 => Ok(Compression::Zstd),
other => Err(Error::UnsupportedFeature(format!(
"extent uses compression type {other}, which is not one of \
zlib (1), LZO (2) or zstd (3)"
))),
}
}
pub fn is_compressed(self) -> bool {
self != Compression::None
}
}
pub const MAX_UNCOMPRESSED: usize = 128 * 1024;
pub const MAX_COMPRESSED: u64 = 128 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShortDecode {
Pad,
Refuse,
}
pub fn decompress(
algo: Compression,
input: &[u8],
ram_bytes: usize,
sectorsize: usize,
short: ShortDecode,
) -> Result<Vec<u8>> {
if ram_bytes > MAX_UNCOMPRESSED {
return Err(Error::BadSuperblock(format!(
"{algo:?} extent says it decodes to {ram_bytes} bytes, more than the \
{MAX_UNCOMPRESSED} a compressed extent can hold"
)));
}
let out = match algo {
Compression::None => input.to_vec(),
Compression::Zlib => decompress_zlib(input, ram_bytes)?,
Compression::Lzo => decompress_lzo(input, ram_bytes, sectorsize)?,
Compression::Zstd => decompress_zstd(input, ram_bytes)?,
};
if out.len() > ram_bytes {
return Err(Error::BadSuperblock(format!(
"{algo:?} extent decoded to {} bytes, more than the {ram_bytes} it records",
out.len()
)));
}
if out.len() < ram_bytes && short == ShortDecode::Refuse {
return Err(Error::BadSuperblock(format!(
"{algo:?} extent decoded to {} bytes where it records {ram_bytes}, and it is not \
the file's last extent, so the rest is missing rather than absent",
out.len()
)));
}
let mut out = out;
out.resize(ram_bytes, 0);
Ok(out)
}
fn decompress_zlib(input: &[u8], ram_bytes: usize) -> Result<Vec<u8>> {
miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(input, ram_bytes).map_err(|e| {
Error::BadSuperblock(format!(
"zlib extent failed to decode ({:?}) after {} bytes",
e.status,
e.output.len()
))
})
}
fn decompress_zstd(input: &[u8], ram_bytes: usize) -> Result<Vec<u8>> {
use std::io::Read;
let decoder = ruzstd::decoding::StreamingDecoder::new(input)
.map_err(|e| Error::BadSuperblock(format!("zstd extent has no readable frame: {e}")))?;
let mut out = Vec::with_capacity(ram_bytes);
decoder
.take(ram_bytes as u64)
.read_to_end(&mut out)
.map_err(|e| Error::BadSuperblock(format!("zstd extent failed to decode: {e}")))?;
Ok(out)
}
const LZO_LEN: usize = 4;
fn decompress_lzo(input: &[u8], ram_bytes: usize, sectorsize: usize) -> Result<Vec<u8>> {
if sectorsize < LZO_LEN {
return Err(Error::BadSuperblock(format!(
"sector size {sectorsize} is too small to hold an LZO segment header"
)));
}
if input.len() < LZO_LEN {
return Err(Error::BadSuperblock(format!(
"LZO extent is {} bytes, too short to hold its length header",
input.len()
)));
}
let total = read_u32(input, 0)? as usize;
if total > input.len() {
return Err(Error::BadSuperblock(format!(
"LZO extent declares {total} compressed bytes but only {} are present",
input.len()
)));
}
let mut out = Vec::with_capacity(ram_bytes);
let mut at = LZO_LEN;
while at < total && out.len() < ram_bytes {
let room = sectorsize - (at % sectorsize);
if room < LZO_LEN {
at += room;
continue;
}
let seg_len = read_u32(input, at)? as usize;
at += LZO_LEN;
if seg_len == 0 {
return Err(Error::BadSuperblock(
"LZO extent contains a zero-length segment".into(),
));
}
let end = at.checked_add(seg_len).ok_or_else(|| {
Error::BadSuperblock("LZO segment length overflows the extent".into())
})?;
if end > input.len() {
return Err(Error::BadSuperblock(format!(
"LZO segment at {at} runs {seg_len} bytes past the end of a {}-byte extent",
input.len()
)));
}
let want = sectorsize.min(ram_bytes - out.len());
let chunk = lzo1x::decompress(&input[at..end], want).map_err(|e| {
Error::BadSuperblock(format!("LZO segment at {at} failed to decode: {e}"))
})?;
out.extend_from_slice(&chunk);
at = end;
}
Ok(out)
}
fn read_u32(b: &[u8], at: usize) -> Result<u32> {
b.get(at..at + 4)
.map(|s| u32::from_le_bytes(s.try_into().expect("4 bytes")))
.ok_or_else(|| {
Error::BadSuperblock(format!(
"LZO extent ended at {} with a length prefix expected at {at}",
b.len()
))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_extent_decoding_to_more_than_a_compression_unit_is_refused() {
for algo in [Compression::Zlib, Compression::Lzo, Compression::Zstd] {
let outcome = decompress(
algo,
&[0u8; 4],
MAX_UNCOMPRESSED + 1,
4096,
ShortDecode::Pad,
);
let why = format!("{outcome:?}");
assert!(
why.contains("more than the"),
"{algo:?} accepted a declared size of {} and was refused as {why}",
MAX_UNCOMPRESSED + 1
);
assert!(decompress(algo, &[0u8; 4], 1 << 55, 4096, ShortDecode::Pad).is_err());
}
}
#[test]
fn compression_bytes_map_to_the_defined_algorithms() {
assert_eq!(Compression::from_byte(0).unwrap(), Compression::None);
assert_eq!(Compression::from_byte(1).unwrap(), Compression::Zlib);
assert_eq!(Compression::from_byte(2).unwrap(), Compression::Lzo);
assert_eq!(Compression::from_byte(3).unwrap(), Compression::Zstd);
}
#[test]
fn an_unknown_compression_type_is_refused() {
let err = Compression::from_byte(4).unwrap_err();
assert!(format!("{err}").contains("compression type 4"), "got {err}");
}
#[test]
fn none_passes_bytes_through() {
let got = decompress(Compression::None, b"hello", 5, 4096, ShortDecode::Pad).unwrap();
assert_eq!(got, b"hello");
}
#[test]
fn zlib_round_trips() {
let plain: Vec<u8> = (0..10_000u32).map(|i| (i % 251) as u8).collect();
let packed = miniz_oxide::deflate::compress_to_vec_zlib(&plain, 6);
let got = decompress(
Compression::Zlib,
&packed,
plain.len(),
4096,
ShortDecode::Pad,
)
.unwrap();
assert_eq!(got, plain);
}
#[test]
fn a_stream_longer_than_the_item_records_is_refused() {
let plain = vec![7u8; 5000];
let packed = miniz_oxide::deflate::compress_to_vec_zlib(&plain, 6);
assert!(decompress(Compression::Zlib, &packed, 100, 4096, ShortDecode::Pad).is_err());
}
#[test]
fn a_short_decode_is_zero_padded_to_the_recorded_length() {
let plain = b"abc".to_vec();
let packed = miniz_oxide::deflate::compress_to_vec_zlib(&plain, 6);
let got = decompress(Compression::Zlib, &packed, 16, 4096, ShortDecode::Pad).unwrap();
assert_eq!(&got[..3], b"abc");
assert_eq!(&got[3..], &[0u8; 13]);
}
fn lzo_frame(segments: &[Vec<u8>], sectorsize: usize) -> Vec<u8> {
let mut out = vec![0u8; LZO_LEN];
for seg in segments {
if sectorsize - (out.len() % sectorsize) < LZO_LEN {
let pad = sectorsize - (out.len() % sectorsize);
out.resize(out.len() + pad, 0);
}
out.extend_from_slice(&(seg.len() as u32).to_le_bytes());
out.extend_from_slice(seg);
}
let total = out.len() as u32;
out[0..LZO_LEN].copy_from_slice(&total.to_le_bytes());
out
}
#[test]
fn lzo_declaring_more_than_it_holds_is_refused() {
let mut framed = vec![0u8; 8];
framed[0..4].copy_from_slice(&999u32.to_le_bytes());
let err = decompress(Compression::Lzo, &framed, 4096, 4096, ShortDecode::Pad).unwrap_err();
assert!(format!("{err}").contains("only 8 are present"), "got {err}");
}
#[test]
fn lzo_shorter_than_its_header_is_refused() {
let err = decompress(Compression::Lzo, &[1, 2], 4096, 4096, ShortDecode::Pad).unwrap_err();
assert!(format!("{err}").contains("too short"), "got {err}");
}
#[test]
fn lzo_with_a_zero_length_segment_is_refused() {
let framed = lzo_frame(&[vec![]], 4096);
let err = decompress(Compression::Lzo, &framed, 4096, 4096, ShortDecode::Pad).unwrap_err();
assert!(format!("{err}").contains("zero-length"), "got {err}");
}
#[test]
fn lzo_with_a_segment_past_the_end_is_refused() {
let mut framed = vec![0u8; 12];
framed[0..4].copy_from_slice(&12u32.to_le_bytes());
framed[4..8].copy_from_slice(&9999u32.to_le_bytes());
let err = decompress(Compression::Lzo, &framed, 4096, 4096, ShortDecode::Pad).unwrap_err();
assert!(format!("{err}").contains("past the end"), "got {err}");
}
fn lzo_literals(bytes: &[u8]) -> Vec<u8> {
assert!((4..=238).contains(&bytes.len()));
let mut out = vec![17 + bytes.len() as u8];
out.extend_from_slice(bytes);
out.extend_from_slice(&[0x11, 0, 0]);
out
}
#[test]
fn an_lzo_payload_crosses_a_sector_boundary_and_is_read_through() {
let sectorsize = 64;
let a: Vec<u8> = (0..44u8).collect();
let b: Vec<u8> = (100..110u8).collect();
let first = lzo_literals(&a);
let second = lzo_literals(&b);
assert_eq!(first.len(), 48);
let framed = lzo_frame(&[first, second], sectorsize);
assert_eq!(read_u32(&framed, 56).unwrap(), 14, "fixture: header at 56");
let got = decompress(
Compression::Lzo,
&framed,
a.len() + b.len(),
sectorsize,
ShortDecode::Pad,
)
.expect("a payload crossing a sector is legal");
assert_eq!(&got[..a.len()], &a[..]);
assert_eq!(&got[a.len()..], &b[..]);
}
#[test]
fn lzo_segment_headers_do_not_straddle_a_sector() {
let sectorsize = 64;
let first = vec![0xAAu8; sectorsize - LZO_LEN - LZO_LEN - 2];
let second = vec![0xBBu8; 8];
let framed = lzo_frame(&[first.clone(), second.clone()], sectorsize);
assert_eq!(
read_u32(&framed, sectorsize).unwrap() as usize,
second.len()
);
assert_eq!(
&framed[sectorsize + LZO_LEN..sectorsize + LZO_LEN + 8],
&second[..]
);
}
}