Chunked reads beat an h5py process pool; unlimited writer B-trees; Blosc2; 599/697 conformance #16
@@ -148,6 +148,180 @@ pub fn decompress_chunk_exact(
|
|||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Buffers a chunk decoder keeps between chunks, so decoding a dataset's
|
||||||
|
/// chunks one after another reuses the same memory instead of allocating
|
||||||
|
/// (and faulting in) fresh buffers for every chunk and every filter stage.
|
||||||
|
///
|
||||||
|
/// Use one per thread with [`decompress_chunk_exact_with`]. Buffers larger
|
||||||
|
/// than [`DecodeScratch::RETAIN_BYTES`] are released by
|
||||||
|
/// [`DecodeScratch::trim`], so a scratch kept for a long time (a
|
||||||
|
/// thread-local, say) does not hold on to a huge chunk's memory.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DecodeScratch {
|
||||||
|
a: Vec<u8>,
|
||||||
|
b: Vec<u8>,
|
||||||
|
#[cfg(feature = "deflate")]
|
||||||
|
inflater: Option<flate2::Decompress>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Debug for DecodeScratch {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
f.debug_struct("DecodeScratch")
|
||||||
|
.field("a_capacity", &self.a.capacity())
|
||||||
|
.field("b_capacity", &self.b.capacity())
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which buffer holds the data between two filter stages.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum Stage {
|
||||||
|
/// `compressed[..len]`: still (a prefix of) the stored bytes.
|
||||||
|
Stored(usize),
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecodeScratch {
|
||||||
|
/// Largest buffer [`trim`](Self::trim) keeps (4 MiB).
|
||||||
|
pub const RETAIN_BYTES: usize = 4 << 20;
|
||||||
|
|
||||||
|
/// An empty scratch; buffers are allocated on first use.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release any buffer larger than [`Self::RETAIN_BYTES`].
|
||||||
|
pub fn trim(&mut self) {
|
||||||
|
for buf in [&mut self.a, &mut self.b] {
|
||||||
|
if buf.capacity() > Self::RETAIN_BYTES {
|
||||||
|
*buf = Vec::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`decompress_chunk_exact`] into reusable buffers: the decoded chunk is
|
||||||
|
/// returned as a slice of `scratch` (or of `compressed`, when every filter
|
||||||
|
/// that was applied only appended a checksum), valid until `scratch` is used
|
||||||
|
/// again.
|
||||||
|
///
|
||||||
|
/// Deflate, shuffle and Fletcher32 — h5py's and libhdf5's usual pipeline —
|
||||||
|
/// decode without allocating once `scratch` has grown to the chunk size: the
|
||||||
|
/// inflater writes into a kept buffer (and its state is reset, not
|
||||||
|
/// rebuilt), shuffle interleaves into the other buffer, and Fletcher32 checks
|
||||||
|
/// the checksum and drops it in place. Every other filter goes through the
|
||||||
|
/// filter registry as [`decompress_chunk_masked`] does, and its output
|
||||||
|
/// replaces a scratch buffer. The result is byte for byte what
|
||||||
|
/// [`decompress_chunk_exact`] returns, with the same errors.
|
||||||
|
pub fn decompress_chunk_exact_with<'s>(
|
||||||
|
compressed: &'s [u8],
|
||||||
|
pipeline: &FilterPipeline,
|
||||||
|
chunk_size: usize,
|
||||||
|
element_size: u32,
|
||||||
|
filter_mask: u32,
|
||||||
|
coords: &[u64],
|
||||||
|
scratch: &'s mut DecodeScratch,
|
||||||
|
) -> Result<&'s [u8], FormatError> {
|
||||||
|
// Same per-stage bounds as `decompress_chunk_masked`.
|
||||||
|
let mut bounds = [0usize; 32];
|
||||||
|
let mut bounds_vec = Vec::new();
|
||||||
|
let bounds: &mut [usize] = if pipeline.filters.len() <= bounds.len() {
|
||||||
|
&mut bounds[..pipeline.filters.len()]
|
||||||
|
} else {
|
||||||
|
bounds_vec.resize(pipeline.filters.len(), 0);
|
||||||
|
&mut bounds_vec
|
||||||
|
};
|
||||||
|
let mut size = chunk_size;
|
||||||
|
for (i, filter) in pipeline.filters.iter().enumerate() {
|
||||||
|
bounds[i] = size;
|
||||||
|
if !filter_skipped(filter_mask, i) {
|
||||||
|
size = filter_output_bound(filter.filter_id, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stage = Stage::Stored(compressed.len());
|
||||||
|
for (i, filter) in pipeline.filters.iter().enumerate().rev() {
|
||||||
|
if filter_skipped(filter_mask, i) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ctx = FilterContext {
|
||||||
|
filter,
|
||||||
|
element_size: element_size as usize,
|
||||||
|
max_output: bounds[i],
|
||||||
|
};
|
||||||
|
// The stage's input, and the buffer its output goes to (the one
|
||||||
|
// not holding the input).
|
||||||
|
let (input, out): (&[u8], &mut Vec<u8>) = match stage {
|
||||||
|
Stage::Stored(len) => (&compressed[..len], &mut scratch.a),
|
||||||
|
Stage::A => (&scratch.a, &mut scratch.b),
|
||||||
|
Stage::B => (&scratch.b, &mut scratch.a),
|
||||||
|
};
|
||||||
|
let next = match stage {
|
||||||
|
Stage::Stored(_) | Stage::B => Stage::A,
|
||||||
|
Stage::A => Stage::B,
|
||||||
|
};
|
||||||
|
match filter.filter_id {
|
||||||
|
// Built in and never overridable (`register_filter` refuses
|
||||||
|
// built-in IDs), so the registry would pick exactly these.
|
||||||
|
FILTER_FLETCHER32 => {
|
||||||
|
// Check and drop the checksum where the data is.
|
||||||
|
let payload = fletcher32_payload(input)?;
|
||||||
|
stage = match stage {
|
||||||
|
Stage::Stored(_) => Stage::Stored(payload),
|
||||||
|
Stage::A => {
|
||||||
|
scratch.a.truncate(payload);
|
||||||
|
Stage::A
|
||||||
|
}
|
||||||
|
Stage::B => {
|
||||||
|
scratch.b.truncate(payload);
|
||||||
|
Stage::B
|
||||||
|
}
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FILTER_SHUFFLE => shuffle_decompress_into(input, ctx.element_size, out),
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "deflate",
|
||||||
|
not(all(target_os = "macos", feature = "system-zlib-decompress"))
|
||||||
|
))]
|
||||||
|
FILTER_DEFLATE => {
|
||||||
|
let limit = if ctx.max_output != 0 {
|
||||||
|
ctx.max_output
|
||||||
|
} else {
|
||||||
|
MAX_DECOMPRESS_SIZE
|
||||||
|
};
|
||||||
|
let size_hint = if ctx.max_output != 0 {
|
||||||
|
ctx.max_output
|
||||||
|
} else {
|
||||||
|
input.len().saturating_mul(4).min(1 << 20)
|
||||||
|
};
|
||||||
|
let inflater = scratch
|
||||||
|
.inflater
|
||||||
|
.get_or_insert_with(|| flate2::Decompress::new(true));
|
||||||
|
inflater.reset(true);
|
||||||
|
inflate_bounded_into(inflater, input, size_hint, limit, out)
|
||||||
|
.map_err(FormatError::DecompressionError)?;
|
||||||
|
}
|
||||||
|
_ => *out = filter_registry::decode(input, &ctx)?,
|
||||||
|
}
|
||||||
|
stage = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: &[u8] = match stage {
|
||||||
|
Stage::Stored(len) => &compressed[..len],
|
||||||
|
Stage::A => &scratch.a,
|
||||||
|
Stage::B => &scratch.b,
|
||||||
|
};
|
||||||
|
if chunk_size != 0 && data.len() != chunk_size {
|
||||||
|
return Err(FormatError::ChunkedReadError(format!(
|
||||||
|
"chunk at {coords:?} decoded to {} bytes, expected {chunk_size}",
|
||||||
|
data.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply a filter pipeline to compress a chunk.
|
/// Apply a filter pipeline to compress a chunk.
|
||||||
/// Filters are applied in FORWARD order for compression.
|
/// Filters are applied in FORWARD order for compression.
|
||||||
pub fn compress_chunk(
|
pub fn compress_chunk(
|
||||||
@@ -892,33 +1066,55 @@ pub(crate) fn inflate_bounded(
|
|||||||
size_hint: usize,
|
size_hint: usize,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<u8>, String> {
|
) -> Result<Vec<u8>, String> {
|
||||||
use flate2::{Decompress, FlushDecompress, Status};
|
let mut out = Vec::new();
|
||||||
|
inflate_bounded_into(
|
||||||
|
&mut flate2::Decompress::new(true),
|
||||||
|
data,
|
||||||
|
size_hint,
|
||||||
|
limit,
|
||||||
|
&mut out,
|
||||||
|
)?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`inflate_bounded`] with a fresh or reset `inflater`, into `out`: its
|
||||||
|
/// contents are replaced and its allocation reused.
|
||||||
|
#[cfg(feature = "deflate")]
|
||||||
|
fn inflate_bounded_into(
|
||||||
|
inflater: &mut flate2::Decompress,
|
||||||
|
data: &[u8],
|
||||||
|
size_hint: usize,
|
||||||
|
limit: usize,
|
||||||
|
out: &mut Vec<u8>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use flate2::{FlushDecompress, Status};
|
||||||
|
|
||||||
// One byte of headroom past the limit distinguishes an over-size stream
|
// One byte of headroom past the limit distinguishes an over-size stream
|
||||||
// from one that legitimately ends exactly at the limit.
|
// from one that legitimately ends exactly at the limit.
|
||||||
let max_capacity = limit.saturating_add(1);
|
let max_capacity = limit.saturating_add(1);
|
||||||
let mut out = Vec::new();
|
// A kept buffer may already be larger than `max_capacity`; the decoder
|
||||||
out.try_reserve_exact(size_hint.clamp(1, max_capacity))
|
// can then write past the limit, which the check below still refuses.
|
||||||
|
out.clear();
|
||||||
|
let want = size_hint.clamp(1, max_capacity);
|
||||||
|
out.try_reserve_exact(want)
|
||||||
.map_err(|e| format!("deflate: cannot allocate output: {e}"))?;
|
.map_err(|e| format!("deflate: cannot allocate output: {e}"))?;
|
||||||
|
|
||||||
let mut inflater = Decompress::new(true);
|
|
||||||
loop {
|
loop {
|
||||||
let (in_before, out_before) = (inflater.total_in(), inflater.total_out());
|
let (in_before, out_before) = (inflater.total_in(), inflater.total_out());
|
||||||
let status = inflater
|
let status = inflater
|
||||||
.decompress_vec(
|
.decompress_vec(&data[in_before as usize..], out, FlushDecompress::Finish)
|
||||||
&data[in_before as usize..],
|
|
||||||
&mut out,
|
|
||||||
FlushDecompress::Finish,
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("deflate: {e}"))?;
|
.map_err(|e| format!("deflate: {e}"))?;
|
||||||
if out.len() > limit {
|
if out.len() > limit {
|
||||||
return Err("deflate: output exceeds size limit".into());
|
return Err("deflate: output exceeds size limit".into());
|
||||||
}
|
}
|
||||||
match status {
|
match status {
|
||||||
Status::StreamEnd => return Ok(out),
|
Status::StreamEnd => return Ok(()),
|
||||||
Status::Ok | Status::BufError if out.len() == out.capacity() => {
|
Status::Ok | Status::BufError if out.len() == out.capacity() => {
|
||||||
// Out of room: double, up to the limit.
|
// Out of room: double, up to the limit.
|
||||||
let grow = out.capacity().min(max_capacity - out.capacity()).max(1);
|
let grow = out
|
||||||
|
.capacity()
|
||||||
|
.min(max_capacity.saturating_sub(out.capacity()))
|
||||||
|
.max(1);
|
||||||
out.try_reserve_exact(grow)
|
out.try_reserve_exact(grow)
|
||||||
.map_err(|e| format!("deflate: cannot allocate output: {e}"))?;
|
.map_err(|e| format!("deflate: cannot allocate output: {e}"))?;
|
||||||
}
|
}
|
||||||
@@ -1217,16 +1413,30 @@ fn zstd_compress(data: &[u8], level: u32) -> Result<Vec<u8>, FormatError> {
|
|||||||
/// On disk: all byte-0s of each element together, then all byte-1s, etc.
|
/// On disk: all byte-0s of each element together, then all byte-1s, etc.
|
||||||
/// Output: elements in natural order.
|
/// Output: elements in natural order.
|
||||||
fn shuffle_decompress(data: &[u8], element_size: usize) -> Result<Vec<u8>, FormatError> {
|
fn shuffle_decompress(data: &[u8], element_size: usize) -> Result<Vec<u8>, FormatError> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
shuffle_decompress_into(data, element_size, &mut result);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`shuffle_decompress`] into `result`, replacing its contents and reusing
|
||||||
|
/// its allocation.
|
||||||
|
fn shuffle_decompress_into(data: &[u8], element_size: usize, result: &mut Vec<u8>) {
|
||||||
if element_size <= 1 {
|
if element_size <= 1 {
|
||||||
return Ok(data.to_vec());
|
result.clear();
|
||||||
|
result.extend_from_slice(data);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// Like libhdf5, only whole elements are shuffled; trailing bytes (e.g. a
|
// Like libhdf5, only whole elements are shuffled; trailing bytes (e.g. a
|
||||||
// Fletcher32 checksum appended before the shuffle) are stored as-is.
|
// Fletcher32 checksum appended before the shuffle) are stored as-is.
|
||||||
let whole = data.len() - data.len() % element_size;
|
let whole = data.len() - data.len() % element_size;
|
||||||
let (data, tail) = data.split_at(whole);
|
let (data, tail) = data.split_at(whole);
|
||||||
let num_elements = data.len() / element_size;
|
let num_elements = data.len() / element_size;
|
||||||
let mut result = vec![0u8; whole];
|
// Every byte of `result[..whole]` is overwritten below, so a reused
|
||||||
result.reserve_exact(tail.len());
|
// buffer keeps its old bytes instead of being zeroed first; only growth
|
||||||
|
// is zero-filled.
|
||||||
|
result.truncate(whole);
|
||||||
|
result.reserve_exact(whole + tail.len() - result.len());
|
||||||
|
result.resize(whole, 0);
|
||||||
|
|
||||||
// The shuffled stream is `element_size` byte planes of `num_elements`
|
// The shuffled stream is `element_size` byte planes of `num_elements`
|
||||||
// bytes each; un-shuffling interleaves them. This is on the read path of
|
// bytes each; un-shuffling interleaves them. This is on the read path of
|
||||||
@@ -1245,10 +1455,10 @@ fn shuffle_decompress(data: &[u8], element_size: usize) -> Result<Vec<u8>, Forma
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
match element_size {
|
match element_size {
|
||||||
2 => interleave::<2>(data, num_elements, &mut result),
|
2 => interleave::<2>(data, num_elements, result),
|
||||||
4 => interleave::<4>(data, num_elements, &mut result),
|
4 => interleave::<4>(data, num_elements, result),
|
||||||
8 => interleave::<8>(data, num_elements, &mut result),
|
8 => interleave::<8>(data, num_elements, result),
|
||||||
16 => interleave::<16>(data, num_elements, &mut result),
|
16 => interleave::<16>(data, num_elements, result),
|
||||||
_ => {
|
_ => {
|
||||||
for (i, element) in result.chunks_exact_mut(element_size).enumerate() {
|
for (i, element) in result.chunks_exact_mut(element_size).enumerate() {
|
||||||
for (j, byte) in element.iter_mut().enumerate() {
|
for (j, byte) in element.iter_mut().enumerate() {
|
||||||
@@ -1258,8 +1468,6 @@ fn shuffle_decompress(data: &[u8], element_size: usize) -> Result<Vec<u8>, Forma
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.extend_from_slice(tail);
|
result.extend_from_slice(tail);
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shuffle (compress direction): group bytes by position within each element.
|
/// Shuffle (compress direction): group bytes by position within each element.
|
||||||
@@ -1411,6 +1619,12 @@ fn fletcher32_compute(data: &[u8]) -> u32 {
|
|||||||
/// Verify Fletcher32 checksum and strip it from the data.
|
/// Verify Fletcher32 checksum and strip it from the data.
|
||||||
/// The last 4 bytes are the stored checksum.
|
/// The last 4 bytes are the stored checksum.
|
||||||
fn fletcher32_verify(data: &[u8]) -> Result<Vec<u8>, FormatError> {
|
fn fletcher32_verify(data: &[u8]) -> Result<Vec<u8>, FormatError> {
|
||||||
|
fletcher32_payload(data).map(|len| data[..len].to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the Fletcher32 checksum that ends `data`; the length of the data
|
||||||
|
/// before it.
|
||||||
|
fn fletcher32_payload(data: &[u8]) -> Result<usize, FormatError> {
|
||||||
if data.len() < 4 {
|
if data.len() < 4 {
|
||||||
return Err(FormatError::FilterError(
|
return Err(FormatError::FilterError(
|
||||||
"fletcher32: data too short for checksum".into(),
|
"fletcher32: data too short for checksum".into(),
|
||||||
@@ -1430,7 +1644,7 @@ fn fletcher32_verify(data: &[u8]) -> Result<Vec<u8>, FormatError> {
|
|||||||
computed,
|
computed,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(payload.to_vec())
|
Ok(payload.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Append Fletcher32 checksum to data.
|
/// Append Fletcher32 checksum to data.
|
||||||
@@ -1545,6 +1759,113 @@ fn pcodec_decompress(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
|
/// `decompress_chunk_exact_with` returns exactly what
|
||||||
|
/// `decompress_chunk_exact` returns — data or error — for every pipeline
|
||||||
|
/// shape, filter mask and chunk size, with one scratch reused across all
|
||||||
|
/// of them in an order that grows, shrinks and swaps its buffers.
|
||||||
|
#[test]
|
||||||
|
fn decode_with_scratch_matches_the_allocating_decoder() {
|
||||||
|
let f = |filter_id: u16, client_data: Vec<u32>| FilterDescription {
|
||||||
|
filter_id,
|
||||||
|
name: None,
|
||||||
|
flags: 0,
|
||||||
|
client_data,
|
||||||
|
};
|
||||||
|
let mut pipelines = vec![
|
||||||
|
vec![f(FILTER_SHUFFLE, vec![4])],
|
||||||
|
vec![f(FILTER_FLETCHER32, vec![])],
|
||||||
|
// NetCDF-4's order: the checksum is taken before shuffle.
|
||||||
|
vec![f(FILTER_FLETCHER32, vec![]), f(FILTER_SHUFFLE, vec![4])],
|
||||||
|
];
|
||||||
|
#[cfg(feature = "deflate")]
|
||||||
|
pipelines.extend([
|
||||||
|
vec![f(FILTER_DEFLATE, vec![4])],
|
||||||
|
vec![f(FILTER_SHUFFLE, vec![4]), f(FILTER_DEFLATE, vec![4])],
|
||||||
|
// h5py's order with `fletcher32=True`: checksum last.
|
||||||
|
vec![
|
||||||
|
f(FILTER_SHUFFLE, vec![4]),
|
||||||
|
f(FILTER_DEFLATE, vec![4]),
|
||||||
|
f(FILTER_FLETCHER32, vec![]),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
f(FILTER_FLETCHER32, vec![]),
|
||||||
|
f(FILTER_SHUFFLE, vec![4]),
|
||||||
|
f(FILTER_DEFLATE, vec![1]),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
#[cfg(feature = "lzf")]
|
||||||
|
pipelines.push(vec![
|
||||||
|
f(FILTER_SHUFFLE, vec![4]),
|
||||||
|
f(crate::filter_pipeline::FILTER_LZF, vec![]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let mut scratch = DecodeScratch::new();
|
||||||
|
for elements in [1usize, 7, 4096, 3, 65536, 100] {
|
||||||
|
let data: Vec<u8> = (0..elements as u32)
|
||||||
|
.flat_map(|i| (i.wrapping_mul(2654435761) >> (i % 13)).to_le_bytes())
|
||||||
|
.collect();
|
||||||
|
for filters in &pipelines {
|
||||||
|
let pipeline = FilterPipeline {
|
||||||
|
version: 2,
|
||||||
|
filters: filters.clone(),
|
||||||
|
};
|
||||||
|
let n = filters.len() as u32;
|
||||||
|
for mask in 0..(1u32 << n) {
|
||||||
|
// Encode only the filters the mask says were applied.
|
||||||
|
let mut stored = data.clone();
|
||||||
|
for (i, filter) in filters.iter().enumerate() {
|
||||||
|
if mask & (1 << i) == 0 {
|
||||||
|
let ctx = FilterContext {
|
||||||
|
filter,
|
||||||
|
element_size: 4,
|
||||||
|
max_output: 0,
|
||||||
|
};
|
||||||
|
stored = filter_registry::encode(&stored, &ctx).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut cases = vec![(stored.clone(), data.len())];
|
||||||
|
// Corrupt: last byte flipped, truncated, wrong size.
|
||||||
|
let mut flipped = stored.clone();
|
||||||
|
*flipped.last_mut().unwrap() ^= 0x5a;
|
||||||
|
cases.push((flipped, data.len()));
|
||||||
|
cases.push((stored[..stored.len() / 2].to_vec(), data.len()));
|
||||||
|
cases.push((stored.clone(), data.len() + 4));
|
||||||
|
cases.push((stored.clone(), 0));
|
||||||
|
for (bytes, size) in cases {
|
||||||
|
let want = decompress_chunk_exact(&bytes, &pipeline, size, 4, mask, &[3]);
|
||||||
|
let got = decompress_chunk_exact_with(
|
||||||
|
&bytes,
|
||||||
|
&pipeline,
|
||||||
|
size,
|
||||||
|
4,
|
||||||
|
mask,
|
||||||
|
&[3],
|
||||||
|
&mut scratch,
|
||||||
|
)
|
||||||
|
.map(<[u8]>::to_vec);
|
||||||
|
match (&want, &got) {
|
||||||
|
(Ok(w), Ok(g)) => assert_eq!(w, g, "{filters:?} mask {mask}"),
|
||||||
|
(Err(w), Err(g)) => {
|
||||||
|
assert_eq!(w.to_string(), g.to_string(), "{filters:?}")
|
||||||
|
}
|
||||||
|
_ => panic!("{filters:?} mask {mask} size {size}: {want:?} vs {got:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Long-lived scratch gives back a huge chunk's buffers.
|
||||||
|
let big = vec![0u8; DecodeScratch::RETAIN_BYTES + 8];
|
||||||
|
let shuffle = FilterPipeline {
|
||||||
|
version: 2,
|
||||||
|
filters: vec![f(FILTER_SHUFFLE, vec![4])],
|
||||||
|
};
|
||||||
|
decompress_chunk_exact_with(&big, &shuffle, big.len(), 4, 0, &[0], &mut scratch).unwrap();
|
||||||
|
scratch.trim();
|
||||||
|
assert!(scratch.a.capacity() <= DecodeScratch::RETAIN_BYTES);
|
||||||
|
assert!(scratch.b.capacity() <= DecodeScratch::RETAIN_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
/// A chunk whose pipeline decodes to fewer bytes than the chunk holds is
|
/// A chunk whose pipeline decodes to fewer bytes than the chunk holds is
|
||||||
/// an error naming the chunk, never a short buffer the reader pads.
|
/// an error naming the chunk, never a short buffer the reader pads.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user