pub mod extd;
pub mod head;
pub mod json_chunks;
pub mod layr;
pub mod lhas;
pub mod ltbl;
pub mod preview;
pub mod voxl;
pub mod zdic;
pub use extd::Extension;
pub use head::Head;
pub use lhas::LayerHashes;
pub use ltbl::{LayerEntry, LayerTable};
pub use preview::{Preview, PreviewRole};
pub use zdic::ZstdDictionary;
use crate::check::Check;
use crate::error::{Error, Result};
use zstd::bulk::{Compressor, Decompressor};
pub fn compress(payload: &[u8], level: i32, dictionary: Option<&[u8]>) -> Result<Vec<u8>> {
let mut compressor = match dictionary {
Some(dictionary) => Compressor::with_dictionary(level, dictionary),
None => Compressor::new(level),
}
.map_err(|e| {
Error::new(
Check::LayrFrameDecompressedSize,
format!("could not start the zstd compressor: {e}"),
)
})?;
compressor.compress(payload).map_err(|e| {
Error::new(
Check::LayrFrameDecompressedSize,
format!("zstd compression failed: {e}"),
)
})
}
pub fn decompress(
frame: &[u8],
uncompressed_size: u64,
dictionary: Option<&[u8]>,
) -> Result<Vec<u8>> {
let bound = (frame.len() as u64).saturating_mul(MAX_ZSTD_RATIO as u64);
decompress_within(frame, uncompressed_size, dictionary, bound)
}
pub fn decompress_within(
frame: &[u8],
uncompressed_size: u64,
dictionary: Option<&[u8]>,
bound: u64,
) -> Result<Vec<u8>> {
check_allocation(uncompressed_size, bound, frame.len())?;
let capacity = usize::try_from(uncompressed_size).map_err(|_| {
Error::new(
Check::LayrFrameDecompressedSize,
format!("declared uncompressed size {uncompressed_size} is not addressable"),
)
})?;
let mut decompressor = match dictionary {
Some(dictionary) => Decompressor::with_dictionary(dictionary),
None => Decompressor::new(),
}
.map_err(|e| {
Error::new(
Check::LayrFrameDecompressedSize,
format!("could not start the zstd decompressor: {e}"),
)
})?;
let decoded = decompressor.decompress(frame, capacity).map_err(|e| {
Error::new(
Check::LayrFrameDecompressedSize,
format!("zstd decompression failed: {e}"),
)
})?;
if decoded.len() as u64 != uncompressed_size {
return Err(Error::new(
Check::LayrFrameDecompressedSize,
format!(
"the frame expands to {} bytes, not the declared {uncompressed_size}",
decoded.len()
),
));
}
Ok(decoded)
}
pub fn frame_dict_id(frame: &[u8]) -> Result<u32> {
Ok(zstd::zstd_safe::get_dict_id_from_frame(frame)
.map(|id| id.get())
.unwrap_or(0))
}
pub fn frame_content_size(frame: &[u8]) -> Result<u64> {
match zstd::zstd_safe::get_frame_content_size(frame) {
Ok(Some(size)) => Ok(size),
Ok(None) => Err(Error::new(
Check::LayrContentSizePresent,
"the frame carries no content size, so its output cannot be sized",
)),
Err(e) => Err(Error::new(
Check::LayrContentSizePresent,
format!("the frame's content size is unreadable: {e}"),
)),
}
}
pub fn decompress_frame(frame: &[u8], dictionary: Option<&[u8]>, bound: u64) -> Result<Vec<u8>> {
let size = frame_content_size(frame)?;
decompress_within(frame, size, dictionary, bound)
}
pub fn dictionary_id(dictionary: &[u8]) -> u32 {
zstd::zstd_safe::get_dict_id_from_dict(dictionary)
.map(|id| id.get())
.unwrap_or(0)
}
pub fn train_dictionary(samples: &[&[u8]], max_size: usize) -> Result<Vec<u8>> {
zstd::dict::from_samples(samples, max_size).map_err(|e| {
Error::new(
Check::ZdicDictSize,
format!("zstd dictionary training failed: {e}"),
)
})
}
pub fn allocation_bound(layers: u64, total_pixels: u32) -> u64 {
layers.saturating_mul(u64::from(total_pixels).saturating_mul(5).saturating_add(64))
}
pub fn check_allocation(uncompressed_size: u64, bound: u64, payload_len: usize) -> Result<()> {
if uncompressed_size > bound {
return Err(Error::new(
Check::FrameAllocationBound,
format!(
"declared uncompressed size {uncompressed_size} exceeds the {bound} bytes the \
structure allows for {payload_len} stored bytes"
),
));
}
Ok(())
}
pub const MAX_ZSTD_RATIO: usize = 4096;
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn zstd_round_trips_with_and_without_a_dictionary() {
let payload: Vec<u8> = (0..4096u32).map(|i| (i % 17) as u8).collect();
let framed = compress(&payload, 3, None).unwrap();
assert_eq!(frame_dict_id(&framed).unwrap(), 0);
assert_eq!(
decompress(&framed, payload.len() as u64, None).unwrap(),
payload
);
let samples: Vec<Vec<u8>> = (0..256u32)
.map(|s| (0..256u32).map(|i| ((i + s) % 23) as u8).collect())
.collect();
let refs: Vec<&[u8]> = samples.iter().map(|s| s.as_slice()).collect();
let dictionary = train_dictionary(&refs, 512).unwrap();
let dict_id = dictionary_id(&dictionary);
assert_ne!(dict_id, 0, "a trained dictionary reports its ID");
let framed = compress(&payload, 3, Some(&dictionary)).unwrap();
assert_eq!(frame_dict_id(&framed).unwrap(), dict_id);
assert_eq!(
decompress(&framed, payload.len() as u64, Some(&dictionary)).unwrap(),
payload
);
let err = decompress(&framed, payload.len() as u64 + 1, Some(&dictionary)).unwrap_err();
assert_eq!(err.check(), Check::LayrFrameDecompressedSize);
}
#[test]
fn a_writer_frame_declares_the_size_it_expands_to() {
let payload: Vec<u8> = (0..4096u32).map(|i| (i % 17) as u8).collect();
let framed = compress(&payload, 3, None).unwrap();
assert_eq!(frame_content_size(&framed).unwrap(), payload.len() as u64);
let bound = (framed.len() as u64).saturating_mul(MAX_ZSTD_RATIO as u64);
assert_eq!(decompress_frame(&framed, None, bound).unwrap(), payload);
let streamed = {
let mut encoder = zstd::stream::write::Encoder::new(Vec::new(), 3).unwrap();
encoder.write_all(&payload).unwrap();
encoder.finish().unwrap()
};
assert_eq!(
frame_content_size(&streamed).unwrap_err().check(),
Check::LayrContentSizePresent
);
}
#[test]
fn check_allocation_bounds_the_declared_size() {
let bound = (MAX_ZSTD_RATIO - 1) as u64;
assert!(check_allocation(bound, bound, 1).is_ok());
assert_eq!(
check_allocation(bound + 1, bound, 1).unwrap_err().check(),
Check::FrameAllocationBound
);
assert!(allocation_bound(2, 1000) > 0);
}
#[test]
fn a_layr_frame_is_bounded_by_its_slices_not_by_a_ratio() {
let payload = vec![0u8; 1 << 20];
let frame = compress(&payload, 3, None).unwrap();
let ratio = payload.len() as f64 / frame.len() as f64;
assert!(
ratio > MAX_ZSTD_RATIO as f64,
"this test needs a frame past the ratio guard, got {ratio:.0}:1"
);
assert_eq!(
decompress_frame(&frame, None, (frame.len() as u64) * MAX_ZSTD_RATIO as u64)
.unwrap_err()
.check(),
Check::FrameAllocationBound
);
let total_pixels = 1 << 20;
let bound = allocation_bound(1, total_pixels);
assert!(bound >= payload.len() as u64);
assert_eq!(decompress_frame(&frame, None, bound).unwrap(), payload);
}
}