use crate::checksum::verify_adler32;
use crate::error::{Error, Result};
use crate::limits::{MAX_COMPRESSED_BLOCK_BYTES, MAX_DECOMPRESSED_BLOCK_BYTES};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionType {
None,
Lzo,
Zlib,
}
pub fn decode_block(
context: &'static str,
block: &[u8],
expected_decompressed_len: usize,
) -> Result<Vec<u8>> {
if block.len() < 8 {
return Err(Error::truncated(context, 8, block.len()));
}
if block.len() > MAX_COMPRESSED_BLOCK_BYTES {
return Err(Error::LimitExceeded {
limit: "compressed_block_bytes",
value: block.len() as u64,
max: MAX_COMPRESSED_BLOCK_BYTES as u64,
});
}
if expected_decompressed_len > MAX_DECOMPRESSED_BLOCK_BYTES {
return Err(Error::LimitExceeded {
limit: "decompressed_block_bytes",
value: expected_decompressed_len as u64,
max: MAX_DECOMPRESSED_BLOCK_BYTES as u64,
});
}
let comp_type = parse_comp_type(&block[..4])?;
let checksum = u32::from_be_bytes([block[4], block[5], block[6], block[7]]);
let payload = &block[8..];
let decoded = match comp_type {
CompressionType::None => payload.to_vec(),
CompressionType::Zlib => miniz_oxide::inflate::decompress_to_vec_zlib(payload)
.map_err(|_| Error::InvalidData(format!("invalid zlib block in {context}")))?,
CompressionType::Lzo => decode_lzo_block(context, payload, expected_decompressed_len)?,
};
if decoded.len() != expected_decompressed_len {
return Err(Error::InvalidData(format!(
"{context} decompressed to {} bytes, expected {expected_decompressed_len}",
decoded.len()
)));
}
verify_adler32(context, &decoded, checksum)?;
Ok(decoded)
}
#[cfg(feature = "lzo")]
fn decode_lzo_block(
context: &'static str,
payload: &[u8],
expected_decompressed_len: usize,
) -> Result<Vec<u8>> {
let mut out = vec![0; expected_decompressed_len];
let actual = lzokay::decompress::decompress(payload, &mut out)
.map_err(|_| Error::InvalidData(format!("invalid lzo block in {context}")))?;
if actual != expected_decompressed_len {
return Err(Error::InvalidData(format!(
"{context} decompressed to {actual} bytes, expected {expected_decompressed_len}"
)));
}
Ok(out)
}
#[cfg(not(feature = "lzo"))]
fn decode_lzo_block(
_context: &'static str,
_payload: &[u8],
_expected_decompressed_len: usize,
) -> Result<Vec<u8>> {
Err(Error::Unsupported(
"LZO compressed blocks (enable the `lzo` feature)",
))
}
pub fn parse_comp_type(bytes: &[u8]) -> Result<CompressionType> {
match bytes {
[0, 0, 0, 0] => Ok(CompressionType::None),
[1, 0, 0, 0] => Ok(CompressionType::Lzo),
[2, 0, 0, 0] => Ok(CompressionType::Zlib),
_ => Err(Error::InvalidData(format!(
"unknown compression tag: {:02x?}",
bytes
))),
}
}
#[cfg(test)]
pub fn encode_zlib_block(data: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(8 + data.len());
out.extend_from_slice(&[2, 0, 0, 0]);
out.extend_from_slice(&crate::checksum::adler32(data).to_be_bytes());
out.extend_from_slice(&miniz_oxide::deflate::compress_to_vec_zlib(data, 6));
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_zlib_block() {
let block = encode_zlib_block(b"hello mdict");
let decoded = decode_block("test block", &block, 11).unwrap();
assert_eq!(decoded, b"hello mdict");
}
}