use super::{DecodeContext, View};
use crate::CodecError;
use zstd::zstd_safe::{self, DCtx, DParameter, InBuffer, OutBuffer};
const MAX_DECODER_WINDOW: u64 = 1_u64 << if usize::BITS == 32 { 30 } else { 31 };
pub struct ZstdDecoder<'ctx, 'input> {
decoder: DCtx<'static>,
ctx: &'ctx DecodeContext<'input>,
input: InBuffer<'input>,
finished: bool,
window_bytes: u64,
}
impl<'input> DecodeContext<'input> {
pub fn open_zstd<'ctx>(
&'ctx self,
source: View<'input>,
) -> Result<ZstdDecoder<'ctx, 'input>, CodecError> {
let mut offset = 0;
let mut largest_window = 0_u64;
let allowance = self.per_expand_allowance();
while offset < source.window().len() {
let frame = source
.child(source.start() + offset, source.end())
.ok_or_else(|| {
CodecError::Malformed("Zstandard frame offset escapes input".into())
})?;
let FrameWindow { declared, buffered } = frame_window(frame)?;
if buffered > allowance {
return Err(CodecError::NotImplemented(format!(
"Zstandard window of {buffered} bytes exceeds the \
{allowance}-byte per-expansion allowance"
)));
}
if declared > MAX_DECODER_WINDOW {
return Err(window_unsupported(declared));
}
largest_window = largest_window.max(declared);
let size = zstd_safe::find_frame_compressed_size(frame.window())
.map_err(|code| zstd_error(self, code, declared))?;
if size == 0 || size > frame.window().len() {
return Err(CodecError::Malformed("invalid Zstandard frame size".into()));
}
offset += size;
}
if offset == 0 {
return Err(CodecError::Malformed("empty Zstandard stream".into()));
}
let mut decoder = DCtx::try_create().ok_or_else(|| {
self.budget
.retained_allocation_failed(largest_window, "Zstandard context")
})?;
let minimum_window = largest_window.max(1024);
let window_log = u64::BITS - (minimum_window - 1).leading_zeros();
decoder
.set_parameter(DParameter::WindowLogMax(window_log))
.map_err(|code| zstd_error(self, code, largest_window))?;
Ok(ZstdDecoder {
decoder,
ctx: self,
input: InBuffer {
src: source.window(),
pos: 0,
},
finished: false,
window_bytes: largest_window,
})
}
}
impl ZstdDecoder<'_, '_> {
pub fn read_chunk(&mut self, bytes: &mut [u8]) -> Result<usize, CodecError> {
if self.finished || bytes.is_empty() {
return Ok(0);
}
loop {
let before = self.input.pos;
let mut chunk = [0_u8; 16 * 1024];
let capacity = bytes.len().min(chunk.len());
let mut output = OutBuffer::around(&mut chunk[..capacity]);
let remaining = self
.decoder
.decompress_stream(&mut output, &mut self.input)
.map_err(|code| zstd_error(self.ctx, code, self.window_bytes))?;
let produced = output.pos();
bytes[..produced].copy_from_slice(&chunk[..produced]);
if remaining == 0 && self.input.pos == self.input.src.len() {
self.finished = true;
}
if produced != 0 || self.finished {
return Ok(produced);
}
if before == self.input.pos {
return Err(CodecError::Malformed("truncated Zstandard stream".into()));
}
}
}
}
fn window_unsupported(window: u64) -> CodecError {
CodecError::NotImplemented(format!(
"Zstandard window of {window} bytes exceeds the {MAX_DECODER_WINDOW}-byte decoder window"
))
}
fn zstd_error(ctx: &DecodeContext<'_>, code: usize, window: u64) -> CodecError {
let error = unsafe { zstd_safe::zstd_sys::ZSTD_getErrorCode(code) };
match error {
zstd_safe::zstd_sys::ZSTD_ErrorCode::ZSTD_error_memory_allocation => ctx
.budget
.retained_allocation_failed(window, "Zstandard decoder allocation"),
zstd_safe::zstd_sys::ZSTD_ErrorCode::ZSTD_error_frameParameter_windowTooLarge => {
window_unsupported(window)
}
_ => CodecError::malformed(format_args!(
"invalid Zstandard stream: {}",
zstd_safe::get_error_name(code)
)),
}
}
struct FrameWindow {
declared: u64,
buffered: u64,
}
fn frame_window(mut frame: View<'_>) -> Result<FrameWindow, CodecError> {
let magic = frame.req_u32_le()?;
if (0x184d_2a50..=0x184d_2a5f).contains(&magic) {
frame.req_u32_le()?;
return Ok(FrameWindow {
declared: 0,
buffered: 0,
});
}
if magic != 0xfd2f_b528 {
return Err(CodecError::Malformed("invalid Zstandard magic".into()));
}
let flags = frame.req_u8()?;
if flags & 8 != 0 {
return Err(CodecError::Malformed(
"reserved Zstandard frame flag".into(),
));
}
let single_segment = flags & 32 != 0;
let window = if single_segment {
None
} else {
let descriptor = frame.req_u8()?;
let base = 1_u64 << (10 + u32::from(descriptor >> 3));
Some(base + (base / 8) * u64::from(descriptor & 7))
};
let dictionary_bytes = match flags & 3 {
0 => 0,
1 => 1,
2 => 2,
_ => 4,
};
frame.req_take(dictionary_bytes)?;
let content_size = match flags >> 6 {
0 if single_segment => Some(u64::from(frame.req_u8()?)),
0 => None,
1 => Some(u64::from(frame.req_u16_le()?) + 256),
2 => Some(u64::from(frame.req_u32_le()?)),
_ => Some(frame.req_u64_le()?),
};
let declared = window.or(content_size).unwrap_or(0);
Ok(FrameWindow {
declared,
buffered: content_size.map_or(declared, |size| size.min(declared)),
})
}
#[cfg(test)]
mod tests {
use super::MAX_DECODER_WINDOW;
use crate::decode::{DecodeArena, DecodeContext, DecodePolicy, ExpandSpec};
use crate::CodecError;
fn frame_with_window(window: u64) -> Vec<u8> {
let exponent = u8::try_from(window.trailing_zeros() - 10).expect("window exponent");
vec![0x28, 0xb5, 0x2f, 0xfd, 0, exponent << 3, 1, 0, 0]
}
#[test]
fn zstd_window_past_the_per_expand_allowance_is_unsupported_and_leaves_the_session_open() {
let window = 32 << 20;
let bytes = frame_with_window(window);
let policy = DecodePolicy::service();
let arena = DecodeArena::new();
let (ctx, root) = DecodeContext::from_root_bytes(&bytes, &arena, &policy).expect("root");
let allowance = ctx.per_expand_allowance();
assert!(allowance < window);
assert!(window < policy.limits.max_decompressed_bytes_per_expand);
let Err(CodecError::NotImplemented(message)) = ctx.open_zstd(root) else {
panic!("a window above the allowance is unsupported");
};
assert_eq!(
message,
format!(
"Zstandard window of {window} bytes exceeds the \
{allowance}-byte per-expansion allowance"
)
);
assert_eq!(ctx.resource_refusal(), None);
let mut writer = ctx
.begin_expand(ExpandSpec::Exact(1))
.expect("later expansions are admitted");
writer.write(b"x").expect("write");
writer.finalize().expect("finalize");
assert!(ctx.finish_session().is_ok());
}
#[test]
fn zstd_window_is_admitted_by_a_smaller_declared_content_size() {
let window = 32 << 20;
let content = [7_u8; 256];
let mut bytes = vec![
0x28,
0xb5,
0x2f,
0xfd,
0x40,
15 << 3,
0,
0,
0x01,
0x08,
0x00,
];
bytes.extend_from_slice(&content);
let policy = DecodePolicy::service();
let arena = DecodeArena::new();
let (ctx, root) = DecodeContext::from_root_bytes(&bytes, &arena, &policy).expect("root");
assert!(ctx.per_expand_allowance() < window);
let mut decoder = ctx.open_zstd(root).expect("the content size fits");
let mut output = Vec::new();
let mut chunk = [0; 64];
loop {
let read = decoder.read_chunk(&mut chunk).expect("read");
if read == 0 {
break;
}
output.extend_from_slice(&chunk[..read]);
}
assert_eq!(output, content);
assert_eq!(ctx.resource_refusal(), None);
}
#[test]
fn zstd_window_above_the_decoder_is_unsupported_and_leaves_the_session_open() {
let window = MAX_DECODER_WINDOW * 2;
let mut bytes = frame_with_window(window);
bytes.resize(usize::try_from(window / 256).expect("padding length"), 0);
let mut policy = DecodePolicy::service();
policy.limits.max_decompressed_bytes_per_expand = window;
let arena = DecodeArena::new();
let (ctx, root) = DecodeContext::from_root_bytes(&bytes, &arena, &policy).expect("root");
assert!(ctx.per_expand_allowance() >= window);
assert!(matches!(
ctx.open_zstd(root),
Err(CodecError::NotImplemented(message)) if message.contains("decoder window")
));
assert_eq!(ctx.resource_refusal(), None);
assert!(ctx.finish_session().is_ok());
}
#[test]
fn zstd_checks_each_window_before_decoder_allocation() {
let mut bytes = zstd::bulk::compress(b"prefix", 0).expect("frame");
bytes.extend_from_slice(&[0x28, 0xb5, 0x2f, 0xfd, 0, 0xa0, 1, 0, 0]);
let arena = DecodeArena::new();
let (ctx, root) =
DecodeContext::from_root_bytes(&bytes, &arena, &DecodePolicy::service()).expect("root");
assert!(
matches!(ctx.open_zstd(root), Err(CodecError::NotImplemented(message)) if message.starts_with(&format!("Zstandard window of {} bytes", 1_u64 << 30)) && message.ends_with("per-expansion allowance"))
);
assert_eq!(ctx.resource_refusal(), None);
}
#[test]
fn zstd_decodes_a_frame() {
let bytes = zstd::bulk::compress(b"payload", 0).expect("frame");
let arena = DecodeArena::new();
let (ctx, root) =
DecodeContext::from_root_bytes(&bytes, &arena, &DecodePolicy::service()).expect("root");
let mut decoder = ctx.open_zstd(root).expect("decoder");
let mut output = [0; 7];
assert_eq!(decoder.read_chunk(&mut output).expect("read"), 7);
assert_eq!(&output, b"payload");
}
}