From e80fbe4b064d8581714479f75169f02b497428e8 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 18 Jul 2026 04:14:27 +0200 Subject: [PATCH] feat(compress): move sequence policy leaves to Rust Move external-sequence postprocessing and MT overlap detection into the Rust compression modules. Keep C stateful dispatch and locking code intact while preserving error encoding, delimiter handling, and half-open range semantics. Test Plan:\n- cargo test --manifest-path rust/Cargo.toml --no-default-features --features compression\n- cargo clippy --manifest-path rust/Cargo.toml\n- cargo clippy --manifest-path rust/Cargo.toml --benches\n- cargo clippy --manifest-path rust/Cargo.toml --tests\n- make -B -C lib -j2 lib\n- make -C tests -j2 test-cli-tests --- lib/compress/zstd_compress.c | 43 +-------- lib/compress/zstdmt_compress.c | 20 +--- rust/src/zstd_compress_stats.rs | 160 ++++++++++++++++++++++++++++++++ rust/src/zstdmt_compress.rs | 101 ++++++++++++++++++++ 4 files changed, 270 insertions(+), 54 deletions(-) diff --git a/lib/compress/zstd_compress.c b/lib/compress/zstd_compress.c index 65c538206..ecd8f1aea 100644 --- a/lib/compress/zstd_compress.c +++ b/lib/compress/zstd_compress.c @@ -241,6 +241,9 @@ size_t ZSTD_rust_fastSequenceLengthSum(const ZSTD_Sequence* seqBuf, size_t seqBufSize); int ZSTD_rust_isRLE(const BYTE* src, size_t length); int ZSTD_rust_maybeRLE(const SeqStore_t* seqStore); +size_t ZSTD_rust_postProcessSequenceProducerResult( + ZSTD_Sequence* outSeqs, size_t nbExternalSeqs, + size_t outSeqsCapacity, size_t srcSize); size_t ZSTD_rust_countSeqStoreLiteralsBytes(const SeqStore_t* seqStore); size_t ZSTD_rust_countSeqStoreMatchBytes(const SeqStore_t* seqStore); void ZSTD_rust_deriveSeqStoreChunk(SeqStore_t* resultSeqStore, @@ -2333,44 +2336,8 @@ void ZSTD_resetSeqStore(SeqStore_t* ssPtr) static size_t ZSTD_postProcessSequenceProducerResult( ZSTD_Sequence* outSeqs, size_t nbExternalSeqs, size_t outSeqsCapacity, size_t srcSize ) { - RETURN_ERROR_IF( - nbExternalSeqs > outSeqsCapacity, - sequenceProducer_failed, - "External sequence producer returned error code %lu", - (unsigned long)nbExternalSeqs - ); - - RETURN_ERROR_IF( - nbExternalSeqs == 0 && srcSize > 0, - sequenceProducer_failed, - "Got zero sequences from external sequence producer for a non-empty src buffer!" - ); - - if (srcSize == 0) { - ZSTD_memset(&outSeqs[0], 0, sizeof(ZSTD_Sequence)); - return 1; - } - - { - ZSTD_Sequence const lastSeq = outSeqs[nbExternalSeqs - 1]; - - /* We can return early if lastSeq is already a block delimiter. */ - if (lastSeq.offset == 0 && lastSeq.matchLength == 0) { - return nbExternalSeqs; - } - - /* This error condition is only possible if the external matchfinder - * produced an invalid parse, by definition of ZSTD_sequenceBound(). */ - RETURN_ERROR_IF( - nbExternalSeqs == outSeqsCapacity, - sequenceProducer_failed, - "nbExternalSeqs == outSeqsCapacity but lastSeq is not a block delimiter!" - ); - - /* lastSeq is not a block delimiter, so we need to append one. */ - ZSTD_memset(&outSeqs[nbExternalSeqs], 0, sizeof(ZSTD_Sequence)); - return nbExternalSeqs + 1; - } + return ZSTD_rust_postProcessSequenceProducerResult( + outSeqs, nbExternalSeqs, outSeqsCapacity, srcSize); } /* ZSTD_fastSequenceLengthSum() : diff --git a/lib/compress/zstdmt_compress.c b/lib/compress/zstdmt_compress.c index 5d947d3a3..d33320e1e 100644 --- a/lib/compress/zstdmt_compress.c +++ b/lib/compress/zstdmt_compress.c @@ -139,6 +139,8 @@ unsigned ZSTDMT_rust_computeTargetJobLog(unsigned windowLog, unsigned chainLog, int ZSTDMT_rust_overlapLog(int overlapLog, int strategy); size_t ZSTDMT_rust_computeOverlapSize(unsigned windowLog, unsigned chainLog, int strategy, int overlapLog, int enableLdm); +int ZSTDMT_rust_isOverlapped(const void* bufferStart, size_t bufferCapacity, + const void* rangeStart, size_t rangeSize); typedef struct ZSTDMT_bufferPool_s { ZSTDMT_RustBufferPool* rustPool; @@ -1531,22 +1533,8 @@ static Range ZSTDMT_getInputDataInUse(ZSTDMT_CCtx* mtctx) */ static int ZSTDMT_isOverlapped(Buffer buffer, Range range) { - BYTE const* const bufferStart = (BYTE const*)buffer.start; - BYTE const* const rangeStart = (BYTE const*)range.start; - - if (rangeStart == NULL || bufferStart == NULL) - return 0; - - { - BYTE const* const bufferEnd = bufferStart + buffer.capacity; - BYTE const* const rangeEnd = rangeStart + range.size; - - /* Empty ranges cannot overlap */ - if (bufferStart == bufferEnd || rangeStart == rangeEnd) - return 0; - - return bufferStart < rangeEnd && rangeStart < bufferEnd; - } + return ZSTDMT_rust_isOverlapped(buffer.start, buffer.capacity, + range.start, range.size); } static int ZSTDMT_doesOverlapWindow(Buffer buffer, ZSTD_window_t window) diff --git a/rust/src/zstd_compress_stats.rs b/rust/src/zstd_compress_stats.rs index 3eb98a021..c604b055d 100644 --- a/rust/src/zstd_compress_stats.rs +++ b/rust/src/zstd_compress_stats.rs @@ -158,6 +158,56 @@ pub struct ZSTD_Sequence { pub rep: u32, } +/// Validates and post-processes sequences returned by an external sequence +/// producer. This is the Rust leaf for C's +/// `ZSTD_postProcessSequenceProducerResult()`. +/// +/// A sequence with zero offset and zero match length terminates a block. If +/// the producer did not append one, this function appends a zeroed delimiter +/// when capacity permits. Error-shaped producer counts and invalid empty +/// parses return the `sequenceProducer_failed` error unchanged. +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_postProcessSequenceProducerResult( + out_seqs: *mut ZSTD_Sequence, + nb_external_seqs: usize, + out_seqs_capacity: usize, + src_size: usize, +) -> usize { + if nb_external_seqs > out_seqs_capacity { + return ERROR(ZstdErrorCode::SequenceProducerFailed); + } + + if nb_external_seqs == 0 && src_size > 0 { + return ERROR(ZstdErrorCode::SequenceProducerFailed); + } + + if src_size == 0 { + unsafe { + ptr::write_bytes(out_seqs.cast::(), 0, size_of::()); + } + return 1; + } + + let last_seq = unsafe { *out_seqs.add(nb_external_seqs - 1) }; + + if last_seq.offset == 0 && last_seq.matchLength == 0 { + return nb_external_seqs; + } + + if nb_external_seqs == out_seqs_capacity { + return ERROR(ZstdErrorCode::SequenceProducerFailed); + } + + unsafe { + ptr::write_bytes( + out_seqs.add(nb_external_seqs).cast::(), + 0, + size_of::(), + ); + } + nb_external_seqs + 1 +} + /// Resets the state that is carried from one compressed block to the next. /// /// This is the direct Rust leaf for C's @@ -1676,6 +1726,116 @@ mod tests { ); } + #[test] + fn post_process_rejects_external_count_overflow() { + let mut out = [ZSTD_Sequence { + offset: 0xA5A5_A5A5, + litLength: 0x5A5A_5A5A, + matchLength: 0xA5A5_A5A5, + rep: 0x5A5A_5A5A, + }]; + let result = unsafe { + ZSTD_rust_postProcessSequenceProducerResult(out.as_mut_ptr(), usize::MAX, out.len(), 1) + }; + assert_eq!(result, ERROR(ZstdErrorCode::SequenceProducerFailed)); + assert_eq!(out[0].offset, 0xA5A5_A5A5); + assert_eq!(out[0].litLength, 0x5A5A_5A5A); + assert_eq!(out[0].matchLength, 0xA5A5_A5A5); + assert_eq!(out[0].rep, 0x5A5A_5A5A); + } + + #[test] + fn post_process_empty_source_zeroes_the_output_sequence() { + let mut out = [ZSTD_Sequence { + offset: 1, + litLength: 2, + matchLength: 3, + rep: 4, + }]; + let result = + unsafe { ZSTD_rust_postProcessSequenceProducerResult(out.as_mut_ptr(), 0, 1, 0) }; + assert_eq!(result, 1); + assert_eq!(out[0].offset, 0); + assert_eq!(out[0].litLength, 0); + assert_eq!(out[0].matchLength, 0); + assert_eq!(out[0].rep, 0); + } + + #[test] + fn post_process_preserves_an_existing_delimiter() { + let mut out = [ZSTD_Sequence { + offset: 0, + litLength: 0xA5A5_A5A5, + matchLength: 0, + rep: 0x5A5A_5A5A, + }]; + let result = + unsafe { ZSTD_rust_postProcessSequenceProducerResult(out.as_mut_ptr(), 1, 1, 7) }; + assert_eq!(result, 1); + assert_eq!(out[0].offset, 0); + assert_eq!(out[0].litLength, 0xA5A5_A5A5); + assert_eq!(out[0].matchLength, 0); + assert_eq!(out[0].rep, 0x5A5A_5A5A); + } + + #[test] + fn post_process_appends_a_zeroed_delimiter() { + let mut out = [ + ZSTD_Sequence { + offset: 3, + litLength: 4, + matchLength: 5, + rep: 6, + }, + ZSTD_Sequence { + offset: 0xA5A5_A5A5, + litLength: 0x5A5A_5A5A, + matchLength: 0xA5A5_A5A5, + rep: 0x5A5A_5A5A, + }, + ]; + let result = unsafe { + ZSTD_rust_postProcessSequenceProducerResult(out.as_mut_ptr(), 1, out.len(), 7) + }; + assert_eq!(result, 2); + assert_eq!(out[0].offset, 3); + assert_eq!(out[0].litLength, 4); + assert_eq!(out[0].matchLength, 5); + assert_eq!(out[0].rep, 6); + assert_eq!(out[1].offset, 0); + assert_eq!(out[1].litLength, 0); + assert_eq!(out[1].matchLength, 0); + assert_eq!(out[1].rep, 0); + } + + #[test] + fn post_process_reports_empty_parse_for_non_empty_source() { + let mut out = [ZSTD_Sequence { + offset: 0xA5A5_A5A5, + litLength: 0x5A5A_5A5A, + matchLength: 0xA5A5_A5A5, + rep: 0x5A5A_5A5A, + }]; + let result = unsafe { + ZSTD_rust_postProcessSequenceProducerResult(out.as_mut_ptr(), 0, out.len(), 1) + }; + assert_eq!(result, ERROR(ZstdErrorCode::SequenceProducerFailed)); + } + + #[test] + fn post_process_reports_full_capacity_without_a_delimiter() { + let mut out = [ZSTD_Sequence { + offset: 1, + litLength: 2, + matchLength: 3, + rep: 4, + }]; + let result = unsafe { + ZSTD_rust_postProcessSequenceProducerResult(out.as_mut_ptr(), 1, out.len(), 7) + }; + assert_eq!(result, ERROR(ZstdErrorCode::SequenceProducerFailed)); + } + #[test] fn is_rle_matches_empty_one_byte_and_unrolled_inputs() { let one_byte = [0x5Au8]; diff --git a/rust/src/zstdmt_compress.rs b/rust/src/zstdmt_compress.rs index b8575913f..1afad3d8f 100644 --- a/rust/src/zstdmt_compress.rs +++ b/rust/src/zstdmt_compress.rs @@ -199,6 +199,30 @@ fn seq_to_buffer(seq: ZstdMtRawSeqStore) -> ZstdMtBuffer { } } +#[inline] +fn is_overlapped( + buffer_start: *const c_void, + buffer_capacity: usize, + range_start: *const c_void, + range_size: usize, +) -> c_int { + if buffer_start.is_null() || range_start.is_null() { + return 0; + } + + let buffer_start = buffer_start.cast::(); + let range_start = range_start.cast::(); + let buffer_end = buffer_start.wrapping_add(buffer_capacity); + let range_end = range_start.wrapping_add(range_size); + + /* Empty ranges cannot overlap. */ + if buffer_start == buffer_end || range_start == range_end { + return 0; + } + + (buffer_start < range_end && range_start < buffer_end) as c_int +} + /// Convert a byte buffer into the raw-sequence store view used by the MT /// sequence pool. The capacity is expressed in whole `rawSeq` elements, just /// like the original C conversion leaf. @@ -215,6 +239,17 @@ pub extern "C" fn ZSTDMT_rust_seqToBuffer(seq: ZstdMtRawSeqStore) -> ZstdMtBuffe seq_to_buffer(seq) } +/// Return non-zero when two non-empty byte ranges overlap. +#[no_mangle] +pub extern "C" fn ZSTDMT_rust_isOverlapped( + bufferStart: *const c_void, + bufferCapacity: usize, + rangeStart: *const c_void, + rangeSize: usize, +) -> c_int { + is_overlapped(bufferStart, bufferCapacity, rangeStart, rangeSize) +} + #[derive(Default)] struct BufferPoolState { buffer_size: usize, @@ -825,6 +860,72 @@ mod tests { assert_eq!(empty.size, 0); } + #[test] + fn overlapped_rejects_null_ranges() { + let bytes = [0u8; 8]; + let start = bytes.as_ptr().cast::(); + + assert_eq!(ZSTDMT_rust_isOverlapped(ptr::null(), 4, start, 4), 0); + assert_eq!(ZSTDMT_rust_isOverlapped(start, 4, ptr::null(), 4), 0); + } + + #[test] + fn overlapped_rejects_empty_ranges() { + let bytes = [0u8; 8]; + let start = bytes.as_ptr().cast::(); + + assert_eq!(ZSTDMT_rust_isOverlapped(start, 0, start, 4), 0); + assert_eq!(ZSTDMT_rust_isOverlapped(start, 4, start, 0), 0); + } + + #[test] + fn overlapped_uses_half_open_range_boundaries() { + let bytes = [0u8; 16]; + let start = bytes.as_ptr(); + let middle = start.wrapping_add(4); + + assert_eq!( + ZSTDMT_rust_isOverlapped(start.cast(), 4, middle.cast(), 4), + 0 + ); + assert_eq!( + ZSTDMT_rust_isOverlapped(middle.cast(), 4, start.cast(), 4), + 0 + ); + } + + #[test] + fn overlapped_detects_contained_ranges() { + let bytes = [0u8; 16]; + let start = bytes.as_ptr(); + let inner = start.wrapping_add(4); + + assert_eq!( + ZSTDMT_rust_isOverlapped(start.cast(), 12, inner.cast(), 4), + 1 + ); + assert_eq!( + ZSTDMT_rust_isOverlapped(inner.cast(), 4, start.cast(), 12), + 1 + ); + } + + #[test] + fn overlapped_rejects_disjoint_ranges() { + let bytes = [0u8; 16]; + let start = bytes.as_ptr(); + let after = start.wrapping_add(8); + + assert_eq!( + ZSTDMT_rust_isOverlapped(start.cast(), 4, after.cast(), 4), + 0 + ); + assert_eq!( + ZSTDMT_rust_isOverlapped(after.cast(), 4, start.cast(), 4), + 0 + ); + } + #[test] fn buffer_pool_reuses_and_resizes_buffers() { let pool = unsafe { ZSTDMT_rust_buffer_pool_create(2, DEFAULT_MEM) };