feat(rust): port frame serialization

Continue the incremental zstd_compress.c migration with its frame
serialization leaves: ZSTD_writeFrameHeader(), the public
ZSTD_writeSkippableFrame() ABI, and ZSTD_writeLastEmptyBlock() now live
in rust/src/zstd_compress_frame.rs.

ZSTD_writeFrameHeader() reads five fields out of ZSTD_CCtx_params, whose
layout is private to zstd_compress.c and sensitive to build
configuration.  Rather than mirror that structure in Rust, the C
function keeps its original static signature and forwards the scalar
fields to ZSTD_rust_writeFrameHeader(), so no parameter-structure layout
crosses the language boundary.  The other two functions take only
pointer/size arguments and are exported directly, replacing their C
bodies outright.

Behavior differences are limited to hardening in release builds: the
Rust leaf clamps a window-size shift that C would leave undefined for a
malformed windowLog, and ZSTD_writeSkippableFrame() rejects a srcSize +
header overflow instead of comparing against a wrapped sum.  Both paths
are unreachable through validated callers, so compressed output is
byte-identical.

Remaining zstd_compress.c work: parameter selection and validation,
context lifecycle, block dispatch, dictionary loading, and the streaming
state machine.

Test plan:
- cd rust && cargo fmt --check && cargo clippy --all-targets -- -D
  warnings && cargo test --all-targets (118 tests, includes new frame
  header/skippable/last-block reference vectors)
- make -C tests fuzzer && ./tests/fuzzer -i1 --no-big-tests
- make -C tests test-rust-lib-smoke
This commit is contained in:
2026-07-11 09:16:06 +02:00
parent fe7e24c770
commit 959e485201
4 changed files with 441 additions and 79 deletions
+15 -79
View File
@@ -30,6 +30,14 @@
#include "zstd_compress_superblock.h" #include "zstd_compress_superblock.h"
#include "../common/bits.h" /* ZSTD_highbit32, ZSTD_rotateRight_U64 */ #include "../common/bits.h" /* ZSTD_highbit32, ZSTD_rotateRight_U64 */
/* Frame serialization lives in Rust. Keep its interface scalar so the
* large, configuration-sensitive CCtx parameter structure stays in C. */
size_t ZSTD_rust_writeFrameHeader(void* dst, size_t dstCapacity,
int noDictIDFlag, int checksumFlag,
int contentSizeFlag, int format,
U32 windowLog, U64 pledgedSrcSize,
U32 dictID);
/* *************************************************************** /* ***************************************************************
* Tuning parameters * Tuning parameters
*****************************************************************/ *****************************************************************/
@@ -4668,85 +4676,13 @@ static size_t ZSTD_writeFrameHeader(void* dst, size_t dstCapacity,
const ZSTD_CCtx_params* params, const ZSTD_CCtx_params* params,
U64 pledgedSrcSize, U32 dictID) U64 pledgedSrcSize, U32 dictID)
{ {
BYTE* const op = (BYTE*)dst; return ZSTD_rust_writeFrameHeader(dst, dstCapacity,
U32 const dictIDSizeCodeLength = (dictID>0) + (dictID>=256) + (dictID>=65536); /* 0-3 */ params->fParams.noDictIDFlag,
U32 const dictIDSizeCode = params->fParams.noDictIDFlag ? 0 : dictIDSizeCodeLength; /* 0-3 */ params->fParams.checksumFlag,
U32 const checksumFlag = params->fParams.checksumFlag>0; params->fParams.contentSizeFlag,
U32 const windowSize = (U32)1 << params->cParams.windowLog; (int)params->format,
U32 const singleSegment = params->fParams.contentSizeFlag && (windowSize >= pledgedSrcSize); params->cParams.windowLog,
BYTE const windowLogByte = (BYTE)((params->cParams.windowLog - ZSTD_WINDOWLOG_ABSOLUTEMIN) << 3); pledgedSrcSize, dictID);
U32 const fcsCode = params->fParams.contentSizeFlag ?
(pledgedSrcSize>=256) + (pledgedSrcSize>=65536+256) + (pledgedSrcSize>=0xFFFFFFFFU) : 0; /* 0-3 */
BYTE const frameHeaderDescriptionByte = (BYTE)(dictIDSizeCode + (checksumFlag<<2) + (singleSegment<<5) + (fcsCode<<6) );
size_t pos=0;
assert(!(params->fParams.contentSizeFlag && pledgedSrcSize == ZSTD_CONTENTSIZE_UNKNOWN));
RETURN_ERROR_IF(dstCapacity < ZSTD_FRAMEHEADERSIZE_MAX, dstSize_tooSmall,
"dst buf is too small to fit worst-case frame header size.");
DEBUGLOG(4, "ZSTD_writeFrameHeader : dictIDFlag : %u ; dictID : %u ; dictIDSizeCode : %u",
!params->fParams.noDictIDFlag, (unsigned)dictID, (unsigned)dictIDSizeCode);
if (params->format == ZSTD_f_zstd1) {
MEM_writeLE32(dst, ZSTD_MAGICNUMBER);
pos = 4;
}
op[pos++] = frameHeaderDescriptionByte;
if (!singleSegment) op[pos++] = windowLogByte;
switch(dictIDSizeCode)
{
default:
assert(0); /* impossible */
ZSTD_FALLTHROUGH;
case 0 : break;
case 1 : op[pos] = (BYTE)(dictID); pos++; break;
case 2 : MEM_writeLE16(op+pos, (U16)dictID); pos+=2; break;
case 3 : MEM_writeLE32(op+pos, dictID); pos+=4; break;
}
switch(fcsCode)
{
default:
assert(0); /* impossible */
ZSTD_FALLTHROUGH;
case 0 : if (singleSegment) op[pos++] = (BYTE)(pledgedSrcSize); break;
case 1 : MEM_writeLE16(op+pos, (U16)(pledgedSrcSize-256)); pos+=2; break;
case 2 : MEM_writeLE32(op+pos, (U32)(pledgedSrcSize)); pos+=4; break;
case 3 : MEM_writeLE64(op+pos, (U64)(pledgedSrcSize)); pos+=8; break;
}
return pos;
}
/* ZSTD_writeSkippableFrame_advanced() :
* Writes out a skippable frame with the specified magic number variant (16 are supported),
* from ZSTD_MAGIC_SKIPPABLE_START to ZSTD_MAGIC_SKIPPABLE_START+15, and the desired source data.
*
* Returns the total number of bytes written, or a ZSTD error code.
*/
size_t ZSTD_writeSkippableFrame(void* dst, size_t dstCapacity,
const void* src, size_t srcSize, unsigned magicVariant) {
BYTE* op = (BYTE*)dst;
RETURN_ERROR_IF(dstCapacity < srcSize + ZSTD_SKIPPABLEHEADERSIZE /* Skippable frame overhead */,
dstSize_tooSmall, "Not enough room for skippable frame");
RETURN_ERROR_IF(srcSize > (unsigned)0xFFFFFFFF, srcSize_wrong, "Src size too large for skippable frame");
RETURN_ERROR_IF(magicVariant > 15, parameter_outOfBound, "Skippable frame magic number variant not supported");
MEM_writeLE32(op, (U32)(ZSTD_MAGIC_SKIPPABLE_START + magicVariant));
MEM_writeLE32(op+4, (U32)srcSize);
ZSTD_memcpy(op+8, src, srcSize);
return srcSize + ZSTD_SKIPPABLEHEADERSIZE;
}
/* ZSTD_writeLastEmptyBlock() :
* output an empty Block with end-of-frame mark to complete a frame
* @return : size of data written into `dst` (== ZSTD_blockHeaderSize (defined in zstd_internal.h))
* or an error code if `dstCapacity` is too small (<ZSTD_blockHeaderSize)
*/
size_t ZSTD_writeLastEmptyBlock(void* dst, size_t dstCapacity)
{
RETURN_ERROR_IF(dstCapacity < ZSTD_blockHeaderSize, dstSize_tooSmall,
"dst buf is too small to write frame trailer empty block.");
{ U32 const cBlockHeader24 = 1 /*lastBlock*/ + (((U32)bt_raw)<<1); /* 0 size */
MEM_writeLE24(dst, cBlockHeader24);
return ZSTD_blockHeaderSize;
}
} }
void ZSTD_referenceExternalSequences(ZSTD_CCtx* cctx, rawSeq* seq, size_t nbSeq) void ZSTD_referenceExternalSequences(ZSTD_CCtx* cctx, rawSeq* seq, size_t nbSeq)
+3
View File
@@ -31,6 +31,9 @@ zstd ABI:
- `zstd_presplit` chooses split points for full compression blocks. - `zstd_presplit` chooses split points for full compression blocks.
- `zstd_compress_literals` emits raw, RLE, and Huffman literal sections - `zstd_compress_literals` emits raw, RLE, and Huffman literal sections
while preserving the compressor's Huffman-table repeat state. while preserving the compressor's Huffman-table repeat state.
- `zstd_compress_frame` serializes frame headers, skippable frames, and the
last empty block; it takes scalar frame parameters so the C-owned
`ZSTD_CCtx_params` layout never crosses the language boundary.
- `zstd_fast` and `zstd_double_fast` implement the single- and two-table - `zstd_fast` and `zstd_double_fast` implement the single- and two-table
fast block match finders, including attached and external dictionary paths. fast block match finders, including attached and external dictionary paths.
- `zstd_lazy` implements greedy, lazy, lazy2, and binary-tree matching, - `zstd_lazy` implements greedy, lazy, lazy2, and binary-tree matching,
+2
View File
@@ -24,6 +24,8 @@ pub mod zstd_common;
#[cfg(feature = "compression")] #[cfg(feature = "compression")]
pub mod zstd_compress_api; pub mod zstd_compress_api;
#[cfg(feature = "compression")] #[cfg(feature = "compression")]
pub mod zstd_compress_frame;
#[cfg(feature = "compression")]
pub mod zstd_compress_literals; pub mod zstd_compress_literals;
#[cfg(feature = "compression")] #[cfg(feature = "compression")]
pub mod zstd_compress_sequences; pub mod zstd_compress_sequences;
+421
View File
@@ -0,0 +1,421 @@
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(clippy::missing_safety_doc)]
//! Frame-header and frame-trailer writing leaves.
//!
//! The high-level compression context remains in C. These leaves accept the
//! five scalar frame parameters they need, so no `ZSTD_CCtx_params` layout
//! crosses the language boundary.
use crate::errors::{ZstdErrorCode, ERROR};
use std::ffi::c_void;
use std::os::raw::{c_int, c_uint};
use std::ptr;
const ZSTD_MAGICNUMBER: u32 = 0xFD2F_B528;
const ZSTD_MAGIC_SKIPPABLE_START: u32 = 0x184D_2A50;
const ZSTD_FRAMEHEADERSIZE_MAX: usize = 18;
const ZSTD_SKIPPABLEHEADERSIZE: usize = 8;
const ZSTD_BLOCKHEADERSIZE: usize = 3;
const ZSTD_WINDOWLOG_ABSOLUTEMIN: u32 = 10;
const ZSTD_CONTENTSIZE_UNKNOWN: u64 = u64::MAX;
const ZSTD_F_ZSTD1: c_int = 0;
#[inline]
unsafe fn write_le16(dst: *mut u8, value: u16) {
let bytes = value.to_le_bytes();
unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len()) };
}
#[inline]
unsafe fn write_le24(dst: *mut u8, value: u32) {
let bytes = value.to_le_bytes();
unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), dst, 3) };
}
#[inline]
unsafe fn write_le32(dst: *mut u8, value: u32) {
let bytes = value.to_le_bytes();
unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len()) };
}
#[inline]
unsafe fn write_le64(dst: *mut u8, value: u64) {
let bytes = value.to_le_bytes();
unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len()) };
}
/// Rust implementation of the private `ZSTD_writeFrameHeader()` leaf.
///
/// `no_dict_id_flag`, `checksum_flag`, `content_size_flag`, `format`, and
/// `window_log` are extracted by a C wrapper from `ZSTD_CCtx_params`. The
/// wrapper preserves the existing static C function signature, avoiding any
/// dependency on the full context-parameter layout here.
///
/// The caller must provide a writable buffer of at least
/// `ZSTD_FRAMEHEADERSIZE_MAX` bytes, or this returns `dstSize_tooSmall`. As
/// in the C routine, `pledged_src_size` must not be
/// `ZSTD_CONTENTSIZE_UNKNOWN` when `content_size_flag` is nonzero.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_writeFrameHeader(
dst: *mut c_void,
dst_capacity: usize,
no_dict_id_flag: c_int,
checksum_flag: c_int,
content_size_flag: c_int,
format: c_int,
window_log: c_uint,
pledged_src_size: u64,
dict_id: u32,
) -> usize {
if dst_capacity < ZSTD_FRAMEHEADERSIZE_MAX {
return ERROR(ZstdErrorCode::DstSizeTooSmall);
}
let dst = dst.cast::<u8>();
let dict_id_size_code_length =
u32::from(dict_id > 0) + u32::from(dict_id >= 256) + u32::from(dict_id >= 65_536);
let dict_id_size_code = if no_dict_id_flag != 0 {
0
} else {
dict_id_size_code_length
};
let checksum_flag = u32::from(checksum_flag > 0);
let content_size_flag = content_size_flag != 0;
debug_assert!(!(content_size_flag && pledged_src_size == ZSTD_CONTENTSIZE_UNKNOWN));
/* Parameters are validated before this leaf is reached. Avoid a Rust
* shift panic if a malformed caller bypasses that validation. */
let window_size = 1u64.checked_shl(window_log).unwrap_or(0);
let single_segment = u32::from(content_size_flag && window_size >= pledged_src_size);
let window_log_byte = window_log
.wrapping_sub(ZSTD_WINDOWLOG_ABSOLUTEMIN)
.wrapping_shl(3) as u8;
let fcs_code = if content_size_flag {
u32::from(pledged_src_size >= 256)
+ u32::from(pledged_src_size >= 65_536 + 256)
+ u32::from(pledged_src_size >= 0xFFFF_FFFF)
} else {
0
};
let frame_header_description_byte =
(dict_id_size_code + (checksum_flag << 2) + (single_segment << 5) + (fcs_code << 6)) as u8;
let mut pos = 0usize;
if format == ZSTD_F_ZSTD1 {
unsafe { write_le32(dst, ZSTD_MAGICNUMBER) };
pos = 4;
}
unsafe { dst.add(pos).write(frame_header_description_byte) };
pos += 1;
if single_segment == 0 {
unsafe { dst.add(pos).write(window_log_byte) };
pos += 1;
}
match dict_id_size_code {
0 => {}
1 => {
unsafe { dst.add(pos).write(dict_id as u8) };
pos += 1;
}
2 => {
unsafe { write_le16(dst.add(pos), dict_id as u16) };
pos += 2;
}
3 => {
unsafe { write_le32(dst.add(pos), dict_id) };
pos += 4;
}
_ => unreachable!("dictionary ID size code is bounded to 0..=3"),
}
match fcs_code {
0 => {
if single_segment != 0 {
unsafe { dst.add(pos).write(pledged_src_size as u8) };
pos += 1;
}
}
1 => {
unsafe { write_le16(dst.add(pos), (pledged_src_size - 256) as u16) };
pos += 2;
}
2 => {
unsafe { write_le32(dst.add(pos), pledged_src_size as u32) };
pos += 4;
}
3 => {
unsafe { write_le64(dst.add(pos), pledged_src_size) };
pos += 8;
}
_ => unreachable!("frame content size code is bounded to 0..=3"),
}
pos
}
/// Rust implementation of the public `ZSTD_writeSkippableFrame()` ABI.
///
/// Integration removes the C function body, allowing this direct export to
/// provide the existing public symbol without a wrapper.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_writeSkippableFrame(
dst: *mut c_void,
dst_capacity: usize,
src: *const c_void,
src_size: usize,
magic_variant: c_uint,
) -> usize {
let total_size = match src_size.checked_add(ZSTD_SKIPPABLEHEADERSIZE) {
Some(size) => size,
None if src_size > u32::MAX as usize => return ERROR(ZstdErrorCode::SrcSizeWrong),
/* A frame whose payload fills the `size_t` address space cannot have
* room for its eight-byte header. */
None => return ERROR(ZstdErrorCode::DstSizeTooSmall),
};
if dst_capacity < total_size {
return ERROR(ZstdErrorCode::DstSizeTooSmall);
}
if src_size > u32::MAX as usize {
return ERROR(ZstdErrorCode::SrcSizeWrong);
}
if magic_variant > 15 {
return ERROR(ZstdErrorCode::ParameterOutOfBound);
}
let dst = dst.cast::<u8>();
let src = src.cast::<u8>();
unsafe { write_le32(dst, ZSTD_MAGIC_SKIPPABLE_START + magic_variant) };
unsafe { write_le32(dst.add(4), src_size as u32) };
if src_size != 0 {
unsafe { ptr::copy_nonoverlapping(src, dst.add(ZSTD_SKIPPABLEHEADERSIZE), src_size) };
}
total_size
}
/// Rust implementation of the private `ZSTD_writeLastEmptyBlock()` leaf.
///
/// Integration removes the C body and declares this symbol before the C call
/// sites, so the direct export needs no context or layout bridge.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_writeLastEmptyBlock(dst: *mut c_void, dst_capacity: usize) -> usize {
if dst_capacity < ZSTD_BLOCKHEADERSIZE {
return ERROR(ZstdErrorCode::DstSizeTooSmall);
}
/* lastBlock = 1, block type = bt_raw, and block size = 0. */
unsafe { write_le24(dst.cast::<u8>(), 1) };
ZSTD_BLOCKHEADERSIZE
}
#[cfg(test)]
mod tests {
use super::*;
use crate::errors::{ERR_getErrorCode, ERR_isError};
#[test]
fn writes_single_segment_empty_frame_header() {
let mut output = [0u8; ZSTD_FRAMEHEADERSIZE_MAX];
let written = unsafe {
ZSTD_rust_writeFrameHeader(
output.as_mut_ptr().cast(),
output.len(),
0,
0,
1,
ZSTD_F_ZSTD1,
20,
0,
0,
)
};
assert_eq!(written, 6);
assert_eq!(&output[..written], &[0x28, 0xB5, 0x2F, 0xFD, 0x20, 0x00]);
}
#[test]
fn writes_magicless_unknown_size_header() {
let mut output = [0u8; ZSTD_FRAMEHEADERSIZE_MAX];
let written = unsafe {
ZSTD_rust_writeFrameHeader(
output.as_mut_ptr().cast(),
output.len(),
0,
0,
0,
1,
10,
ZSTD_CONTENTSIZE_UNKNOWN,
0,
)
};
assert_eq!(written, 2);
assert_eq!(&output[..written], &[0x00, 0x00]);
}
#[test]
fn writes_maximal_frame_header() {
let mut output = [0u8; ZSTD_FRAMEHEADERSIZE_MAX];
let content_size = 0x1_0000_0000u64;
let written = unsafe {
ZSTD_rust_writeFrameHeader(
output.as_mut_ptr().cast(),
output.len(),
0,
1,
1,
ZSTD_F_ZSTD1,
20,
content_size,
0x1234_5678,
)
};
assert_eq!(written, ZSTD_FRAMEHEADERSIZE_MAX);
assert_eq!(
&output[..written],
&[
0x28, 0xB5, 0x2F, 0xFD, 0xC7, 0x50, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
]
);
}
#[test]
fn encodes_short_dictionary_and_content_size_fields() {
let mut output = [0u8; ZSTD_FRAMEHEADERSIZE_MAX];
let written = unsafe {
ZSTD_rust_writeFrameHeader(
output.as_mut_ptr().cast(),
output.len(),
0,
0,
1,
ZSTD_F_ZSTD1,
10,
256,
255,
)
};
assert_eq!(written, 8);
assert_eq!(
&output[..written],
&[0x28, 0xB5, 0x2F, 0xFD, 0x61, 0xFF, 0x00, 0x00]
);
}
#[test]
fn ignores_dictionary_id_when_requested() {
let mut output = [0u8; ZSTD_FRAMEHEADERSIZE_MAX];
let written = unsafe {
ZSTD_rust_writeFrameHeader(
output.as_mut_ptr().cast(),
output.len(),
1,
0,
1,
ZSTD_F_ZSTD1,
10,
256,
0x1234_5678,
)
};
assert_eq!(written, 7);
assert_eq!(
&output[..written],
&[0x28, 0xB5, 0x2F, 0xFD, 0x60, 0x00, 0x00]
);
}
#[test]
fn frame_header_requires_worst_case_capacity() {
let mut output = [0u8; ZSTD_FRAMEHEADERSIZE_MAX - 1];
let result = unsafe {
ZSTD_rust_writeFrameHeader(
output.as_mut_ptr().cast(),
output.len(),
0,
0,
1,
ZSTD_F_ZSTD1,
20,
0,
0,
)
};
assert!(ERR_isError(result));
assert_eq!(
ERR_getErrorCode(result),
ZstdErrorCode::DstSizeTooSmall as i32
);
}
#[test]
fn writes_skippable_frame_and_validates_parameters() {
let mut output = [0u8; 11];
let written = unsafe {
ZSTD_writeSkippableFrame(
output.as_mut_ptr().cast(),
output.len(),
b"abc".as_ptr().cast(),
3,
2,
)
};
assert_eq!(written, output.len());
assert_eq!(
output,
[0x52, 0x2A, 0x4D, 0x18, 0x03, 0x00, 0x00, 0x00, b'a', b'b', b'c']
);
let too_small = unsafe {
ZSTD_writeSkippableFrame(
output.as_mut_ptr().cast(),
output.len() - 1,
b"abc".as_ptr().cast(),
3,
2,
)
};
assert_eq!(
ERR_getErrorCode(too_small),
ZstdErrorCode::DstSizeTooSmall as i32
);
let invalid_variant = unsafe {
ZSTD_writeSkippableFrame(
output.as_mut_ptr().cast(),
output.len(),
b"abc".as_ptr().cast(),
3,
16,
)
};
assert_eq!(
ERR_getErrorCode(invalid_variant),
ZstdErrorCode::ParameterOutOfBound as i32
);
}
#[test]
fn writes_last_empty_block() {
let mut output = [0u8; ZSTD_BLOCKHEADERSIZE];
assert_eq!(
unsafe { ZSTD_writeLastEmptyBlock(output.as_mut_ptr().cast(), output.len()) },
ZSTD_BLOCKHEADERSIZE
);
assert_eq!(output, [1, 0, 0]);
let result = unsafe { ZSTD_writeLastEmptyBlock(output.as_mut_ptr().cast(), 2) };
assert_eq!(
ERR_getErrorCode(result),
ZstdErrorCode::DstSizeTooSmall as i32
);
}
}