use std::fmt::Formatter;
use bc_crypto::hash::crc32;
use bc_ur::prelude::*;
use miniz_oxide::{deflate::compress_to_vec, inflate::decompress_to_vec};
use crate::{DigestProvider, Error, Result, digest::Digest, tags};
#[derive(Clone, Eq, PartialEq)]
pub struct Compressed {
checksum: u32,
decompressed_size: usize,
compressed_data: Vec<u8>,
digest: Option<Digest>,
}
impl Compressed {
pub fn new(
checksum: u32,
decompressed_size: usize,
compressed_data: Vec<u8>,
digest: Option<Digest>,
) -> Result<Self> {
if compressed_data.len() > decompressed_size {
return Err(Error::compression(
"compressed data is larger than decompressed size",
));
}
Ok(Self {
checksum,
decompressed_size,
compressed_data,
digest,
})
}
pub fn from_decompressed_data(
decompressed_data: impl AsRef<[u8]>,
digest: Option<Digest>,
) -> Self {
let decompressed_data = decompressed_data.as_ref();
let compressed_data = compress_to_vec(decompressed_data, 6);
let checksum = crc32(decompressed_data);
let decompressed_size = decompressed_data.len();
let compressed_size = compressed_data.len();
if compressed_size != 0 && compressed_size < decompressed_size {
Self {
checksum,
decompressed_size,
compressed_data,
digest,
}
} else {
Self {
checksum,
decompressed_size,
compressed_data: decompressed_data.to_vec(),
digest,
}
}
}
pub fn decompress(&self) -> Result<Vec<u8>> {
let compressed_size = self.compressed_data.len();
if compressed_size >= self.decompressed_size {
return Ok(self.compressed_data.clone());
}
let decompressed_data = decompress_to_vec(&self.compressed_data)
.map_err(|_| Error::compression("corrupt compressed data"))?;
if crc32(&decompressed_data) != self.checksum {
return Err(Error::compression(
"compressed data checksum mismatch",
));
}
Ok(decompressed_data)
}
pub fn compressed_size(&self) -> usize { self.compressed_data.len() }
pub fn compression_ratio(&self) -> f64 {
(self.compressed_size() as f64) / (self.decompressed_size as f64)
}
pub fn digest_opt(&self) -> Option<Digest> { self.digest }
pub fn has_digest(&self) -> bool { self.digest.is_some() }
}
impl DigestProvider for Compressed {
fn digest(&self) -> Digest { self.digest.unwrap() }
}
impl std::fmt::Debug for Compressed {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Compressed(checksum: {}, size: {}/{}, ratio: {:.2}, digest: {})",
hex::encode(self.checksum.to_be_bytes()),
self.compressed_size(),
self.decompressed_size,
self.compression_ratio(),
self.digest_opt()
.map(|d| d.short_description())
.unwrap_or_else(|| "None".to_string())
)
}
}
impl AsRef<Compressed> for Compressed {
fn as_ref(&self) -> &Compressed { self }
}
impl CBORTagged for Compressed {
fn cbor_tags() -> Vec<Tag> { tags_for_values(&[tags::TAG_COMPRESSED]) }
}
impl From<Compressed> for CBOR {
fn from(value: Compressed) -> Self { value.tagged_cbor() }
}
impl CBORTaggedEncodable for Compressed {
fn untagged_cbor(&self) -> CBOR {
let mut elements = vec![
self.checksum.into(),
self.decompressed_size.into(),
CBOR::to_byte_string(&self.compressed_data),
];
if let Some(digest) = self.digest {
elements.push(digest.into());
}
CBORCase::Array(elements).into()
}
}
impl TryFrom<CBOR> for Compressed {
type Error = dcbor::Error;
fn try_from(cbor: CBOR) -> dcbor::Result<Self> {
Self::from_tagged_cbor(cbor)
}
}
impl CBORTaggedDecodable for Compressed {
fn from_untagged_cbor(cbor: CBOR) -> dcbor::Result<Self> {
let elements = cbor.try_into_array()?;
if elements.len() < 3 || elements.len() > 4 {
return Err("invalid number of elements in compressed".into());
}
let checksum = elements[0].clone().try_into()?;
let decompressed_size = elements[1].clone().try_into()?;
let compressed_data = elements[2].clone().try_into_byte_string()?;
let digest = if elements.len() == 4 {
Some(elements[3].clone().try_into()?)
} else {
None
};
Ok(Self::new(
checksum,
decompressed_size,
compressed_data,
digest,
)?)
}
}
#[cfg(test)]
mod tests {
use crate::Compressed;
#[test]
fn test_1() {
let source =
b"Lorem ipsum dolor sit amet consectetur adipiscing elit mi nibh ornare proin blandit diam ridiculus, faucibus mus dui eu vehicula nam donec dictumst sed vivamus bibendum aliquet efficitur. Felis imperdiet sodales dictum morbi vivamus augue dis duis aliquet velit ullamcorper porttitor, lobortis dapibus hac purus aliquam natoque iaculis blandit montes nunc pretium.";
let compressed = Compressed::from_decompressed_data(source, None);
assert_eq!(
format!("{:?}", compressed),
"Compressed(checksum: 3eeb10a0, size: 217/364, ratio: 0.60, digest: None)"
);
assert_eq!(compressed.decompress().unwrap(), source);
}
#[test]
fn test_2() {
let source = b"Lorem ipsum dolor sit amet consectetur adipiscing";
let compressed = Compressed::from_decompressed_data(source, None);
assert_eq!(
format!("{:?}", compressed),
"Compressed(checksum: 29db1793, size: 45/49, ratio: 0.92, digest: None)"
);
assert_eq!(compressed.decompress().unwrap(), source);
}
#[test]
fn test_3() {
let source = b"Lorem";
let compressed = Compressed::from_decompressed_data(source, None);
assert_eq!(
format!("{:?}", compressed),
"Compressed(checksum: 44989b39, size: 5/5, ratio: 1.00, digest: None)"
);
assert_eq!(compressed.decompress().unwrap(), source);
}
#[test]
fn test_4() {
let source = b"";
let compressed = Compressed::from_decompressed_data(source, None);
assert_eq!(
format!("{:?}", compressed),
"Compressed(checksum: 00000000, size: 0/0, ratio: NaN, digest: None)"
);
assert_eq!(compressed.decompress().unwrap(), source);
}
}