From d316a14ec3c929ef33e2007cce67faabc0dce11a Mon Sep 17 00:00:00 2001 From: ddidderr Date: Fri, 10 Jul 2026 22:51:07 +0200 Subject: [PATCH] feat(rust): export public sequence helpers Move the context-free ZSTD_sequenceBound and ZSTD_mergeBlockDelimiters APIs into the Rust compression helper module. Preserve the public sequence ABI, block-delimiter semantics, and unsigned literal-length wrapping behavior while leaving context-driven sequence generation in C. Test Plan: - cargo clippy - cargo clippy --benches - cargo clippy --tests - cargo +nightly fmt - cargo test zstd_compress_api::tests -- --nocapture - cargo test --target i686-unknown-linux-gnu zstd_compress_api::tests Refs: Rust compression-bound export 3e2635f4 --- lib/compress/zstd_compress.c | 22 +-------- rust/src/zstd_compress_api.rs | 91 +++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 20 deletions(-) diff --git a/lib/compress/zstd_compress.c b/lib/compress/zstd_compress.c index 9eabaa7a3..50efbe42b 100644 --- a/lib/compress/zstd_compress.c +++ b/lib/compress/zstd_compress.c @@ -3501,11 +3501,7 @@ static size_t ZSTD_copyBlockSequences(SeqCollector* seqCollector, const SeqStore return 0; } -size_t ZSTD_sequenceBound(size_t srcSize) { - const size_t maxNbSeq = (srcSize / ZSTD_MINMATCH_MIN) + 1; - const size_t maxNbDelims = (srcSize / ZSTD_BLOCKSIZE_MAX_MIN) + 1; - return maxNbSeq + maxNbDelims; -} +/* ZSTD_sequenceBound() lives in rust/src/zstd_compress_api.rs. */ size_t ZSTD_generateSequences(ZSTD_CCtx* zc, ZSTD_Sequence* outSeqs, size_t outSeqsSize, const void* src, size_t srcSize) @@ -3542,21 +3538,7 @@ size_t ZSTD_generateSequences(ZSTD_CCtx* zc, ZSTD_Sequence* outSeqs, return zc->seqCollector.seqIndex; } -size_t ZSTD_mergeBlockDelimiters(ZSTD_Sequence* sequences, size_t seqsSize) { - size_t in = 0; - size_t out = 0; - for (; in < seqsSize; ++in) { - if (sequences[in].offset == 0 && sequences[in].matchLength == 0) { - if (in != seqsSize - 1) { - sequences[in+1].litLength += sequences[in].litLength; - } - } else { - sequences[out] = sequences[in]; - ++out; - } - } - return out; -} +/* ZSTD_mergeBlockDelimiters() lives in rust/src/zstd_compress_api.rs. */ /* Unrolled loop to read four size_ts of input at a time. Returns 1 if is RLE, 0 if not. */ static int ZSTD_isRLE(const BYTE* src, size_t length) { diff --git a/rust/src/zstd_compress_api.rs b/rust/src/zstd_compress_api.rs index 8f57cae8e..d5958028c 100644 --- a/rust/src/zstd_compress_api.rs +++ b/rust/src/zstd_compress_api.rs @@ -12,6 +12,18 @@ const ZSTD_MAX_INPUT_SIZE: usize = 0xFF00_FF00_FF00_FF00; const ZSTD_MAX_INPUT_SIZE: usize = 0xFF00_FF00; const SMALL_INPUT_THRESHOLD: usize = 128 << 10; +const ZSTD_MINMATCH_MIN: usize = 3; +const ZSTD_BLOCKSIZE_MAX_MIN: usize = 1 << 10; + +/// ABI-compatible `ZSTD_Sequence` from `zstd.h`. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ZstdSequence { + offset: u32, + lit_length: u32, + match_length: u32, + rep: u32, +} /// Returns the maximum one-pass compressed size, matching /// `ZSTD_COMPRESSBOUND()` and its public error contract. @@ -29,6 +41,40 @@ pub extern "C" fn ZSTD_compressBound(src_size: usize) -> usize { src_size.wrapping_add(src_size >> 8).wrapping_add(margin) } +/// Returns the maximum number of public sequences generated for an input. +#[no_mangle] +pub extern "C" fn ZSTD_sequenceBound(src_size: usize) -> usize { + (src_size / ZSTD_MINMATCH_MIN + 1) + (src_size / ZSTD_BLOCKSIZE_MAX_MIN + 1) +} + +/// Removes explicit block delimiters, carrying their literals into the next +/// sequence exactly as the C public helper does. +/// +/// # Safety +/// +/// When `sequences_size` is nonzero, `sequences` must reference that many +/// initialized `ZSTD_Sequence` values. +#[no_mangle] +pub unsafe extern "C" fn ZSTD_mergeBlockDelimiters( + sequences: *mut ZstdSequence, + sequences_size: usize, +) -> usize { + let mut output = 0usize; + for input in 0..sequences_size { + let sequence = unsafe { sequences.add(input).read() }; + if sequence.offset == 0 && sequence.match_length == 0 { + if input != sequences_size - 1 { + let next = unsafe { &mut *sequences.add(input + 1) }; + next.lit_length = next.lit_length.wrapping_add(sequence.lit_length); + } + } else { + unsafe { sequences.add(output).write(sequence) }; + output += 1; + } + } + output +} + #[cfg(test)] mod tests { use super::*; @@ -42,4 +88,49 @@ mod tests { assert_eq!(ZSTD_compressBound(SMALL_INPUT_THRESHOLD), 131_584); assert!(ERR_isError(ZSTD_compressBound(ZSTD_MAX_INPUT_SIZE))); } + + #[test] + fn sequence_bound_matches_the_public_formula() { + assert_eq!(ZSTD_sequenceBound(0), 2); + assert_eq!(ZSTD_sequenceBound(3), 3); + assert_eq!(ZSTD_sequenceBound(1024), 344); + } + + #[test] + fn merge_block_delimiters_carries_literals_forward() { + let mut sequences = [ + ZstdSequence { + offset: 0, + lit_length: 3, + match_length: 0, + rep: 0, + }, + ZstdSequence { + offset: 7, + lit_length: 1, + match_length: 4, + rep: 2, + }, + ZstdSequence { + offset: 0, + lit_length: 9, + match_length: 0, + rep: 0, + }, + ]; + + assert_eq!( + unsafe { ZSTD_mergeBlockDelimiters(sequences.as_mut_ptr(), sequences.len()) }, + 1 + ); + assert_eq!( + sequences[0], + ZstdSequence { + offset: 7, + lit_length: 4, + match_length: 4, + rep: 2, + } + ); + } }