Skip to main content

zstd_pure_rs/decompress/
zstd_decompress.rs

1//! Translation of `lib/decompress/zstd_decompress.c`. The frame-level
2//! decoder: magic number, frame header, block loop, checksum validation.
3
4use crate::common::error::{ErrorCode, ERROR};
5use crate::common::mem::{MEM_readLE16, MEM_readLE32, MEM_readLE64};
6
7pub const ZSTD_MAGICNUMBER: u32 = 0xFD2FB528;
8pub const ZSTD_MAGIC_DICTIONARY: u32 = 0xEC30A437;
9pub const ZSTD_MAGIC_SKIPPABLE_START: u32 = 0x184D2A50;
10pub const ZSTD_MAGIC_SKIPPABLE_MASK: u32 = 0xFFFFFFF0;
11
12pub const ZSTD_FRAMEIDSIZE: usize = 4;
13pub const ZSTD_SKIPPABLEHEADERSIZE: usize = 8;
14pub const ZSTD_WINDOWLOG_ABSOLUTEMIN: u32 = 10;
15pub const ZSTD_WINDOWLOG_MAX_64: u32 = 31;
16pub const ZSTD_WINDOWLOG_MAX_32: u32 = 30;
17
18pub const ZSTD_CONTENTSIZE_UNKNOWN: u64 = u64::MAX;
19pub const ZSTD_CONTENTSIZE_ERROR: u64 = u64::MAX - 1;
20
21pub const ZSTD_fcs_fieldSize: [usize; 4] = [0, 2, 4, 8];
22pub const ZSTD_did_fieldSize: [usize; 4] = [0, 1, 2, 4];
23
24/// Port of `ZSTD_format_e`.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum ZSTD_format_e {
27    #[default]
28    ZSTD_f_zstd1,
29    ZSTD_f_zstd1_magicless,
30}
31
32/// Port of `ZSTD_FrameType_e`.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum ZSTD_FrameType_e {
35    #[default]
36    ZSTD_frame,
37    ZSTD_skippableFrame,
38}
39
40/// Mirror of `ZSTD_FrameHeader` (upstream public struct).
41#[derive(Debug, Clone, Copy, Default)]
42pub struct ZSTD_FrameHeader {
43    pub frameContentSize: u64,
44    pub windowSize: u64,
45    pub blockSizeMax: u32,
46    pub frameType: ZSTD_FrameType_e,
47    pub headerSize: u32,
48    pub dictID: u32,
49    pub checksumFlag: u32,
50    pub _reserved1: u32,
51    pub _reserved2: u32,
52}
53
54/// Port of `ZSTD_dStreamStage` (`zstd_decompress_internal.h:94`). The
55/// streaming-decoder's ingest sub-stage, distinct from `ZSTD_dStage`.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57pub enum ZSTD_dStreamStage {
58    #[default]
59    zdss_init = 0,
60    zdss_loadHeader = 1,
61    zdss_read = 2,
62    zdss_load = 3,
63    zdss_flush = 4,
64}
65
66/// Port of `ZSTD_dictUses_e` (`zstd_decompress_internal.h:97`). Tracks
67/// how long a dict remains bound to a DCtx: indefinitely, once, or
68/// not at all.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
70pub enum ZSTD_dictUses_e {
71    ZSTD_use_indefinitely = -1,
72    #[default]
73    ZSTD_dont_use = 0,
74    ZSTD_use_once = 1,
75}
76
77/// Port of `ZSTD_dStage` (`zstd_decompress_internal.h:89`). Lifecycle
78/// state of the streaming DCtx; consumed by `ZSTD_nextInputType` /
79/// `ZSTD_nextSrcSizeToDecompressWithInputSize` / `ZSTD_isSkipFrame`.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub enum ZSTD_dStage {
82    #[default]
83    ZSTDds_getFrameHeaderSize = 0,
84    ZSTDds_decodeFrameHeader = 1,
85    ZSTDds_decodeBlockHeader = 2,
86    ZSTDds_decompressBlock = 3,
87    ZSTDds_decompressLastBlock = 4,
88    ZSTDds_checkChecksum = 5,
89    ZSTDds_decodeSkippableHeader = 6,
90    ZSTDds_skipFrame = 7,
91}
92
93/// Port of `ZSTD_FRAMEHEADERSIZE_PREFIX` (`zstd.h:1257`). Minimum input
94/// size required to query frame-header size: 5 for zstd1, 1 for the
95/// magicless variant.
96#[inline]
97pub const fn ZSTD_FRAMEHEADERSIZE_PREFIX(format: ZSTD_format_e) -> usize {
98    match format {
99        ZSTD_format_e::ZSTD_f_zstd1 => 5,
100        ZSTD_format_e::ZSTD_f_zstd1_magicless => 1,
101    }
102}
103
104/// Port of `ZSTD_FRAMEHEADERSIZE_MIN` (`zstd.h:1258`). Minimum size of
105/// a valid frame header: 6 bytes for zstd1 (4B magic + 1B FHD + 1B
106/// window), 2 for magicless.
107#[inline]
108pub const fn ZSTD_FRAMEHEADERSIZE_MIN(format: ZSTD_format_e) -> usize {
109    match format {
110        ZSTD_format_e::ZSTD_f_zstd1 => 6,
111        ZSTD_format_e::ZSTD_f_zstd1_magicless => 2,
112    }
113}
114
115/// Port of `ZSTD_startingInputLength` / `ZSTD_FRAMEHEADERSIZE_PREFIX`.
116/// Minimum bytes needed to read the frame-header's Frame Header
117/// Descriptor (FHD) byte. For zstd1 this is 5 (4-byte magic + FHD);
118/// for the magicless variant it's 1.
119#[inline]
120pub fn ZSTD_startingInputLength(format: ZSTD_format_e) -> usize {
121    match format {
122        ZSTD_format_e::ZSTD_f_zstd1 => ZSTD_FRAMEIDSIZE + 1,
123        ZSTD_format_e::ZSTD_f_zstd1_magicless => 1,
124    }
125}
126
127/// Port of `ZSTD_frameHeaderSize_internal` / `ZSTD_frameHeaderSize`.
128/// Reads only the Frame Header Descriptor byte, returns the full
129/// header size (inclusive of magic, FHD, window descriptor, dictID,
130/// FCS).
131pub fn ZSTD_frameHeaderSize_internal(src: &[u8], format: ZSTD_format_e) -> usize {
132    let minInputSize = ZSTD_startingInputLength(format);
133    if src.len() < minInputSize {
134        return ERROR(ErrorCode::SrcSizeWrong);
135    }
136    let fhd = src[minInputSize - 1];
137    let dictID = (fhd & 3) as usize;
138    let singleSegment = ((fhd >> 5) & 1) as usize;
139    let fcsId = (fhd >> 6) as usize;
140    minInputSize
141        + (1 - singleSegment)               // window descriptor byte (absent when singleSegment)
142        + ZSTD_did_fieldSize[dictID]
143        + ZSTD_fcs_fieldSize[fcsId]
144        + (singleSegment & (fcsId == 0) as usize) // extra byte for FCS when singleSegment and fcsId==0
145}
146
147/// Port of `ZSTD_frameHeaderSize` (`zstd.h:1118`). Returns the byte
148/// count of the frame header beginning at `src`, or an error code if
149/// the header is malformed / truncated. Always assumes the default
150/// `ZSTD_f_zstd1` format — callers who need magicless support should
151/// use `ZSTD_frameHeaderSize_internal` with an explicit `format`.
152pub fn ZSTD_frameHeaderSize(src: &[u8]) -> usize {
153    ZSTD_frameHeaderSize_internal(src, ZSTD_format_e::ZSTD_f_zstd1)
154}
155
156/// Port of `ZSTD_getFrameHeader_advanced`. Returns:
157///   - 0 on success; `zfh` is populated.
158///   - >0 : `src` was too small; value is the needed byte count.
159///   - ERR_isError(rc): malformed/unsupported header.
160pub fn ZSTD_getFrameHeader_advanced(
161    zfh: &mut ZSTD_FrameHeader,
162    src: &[u8],
163    format: ZSTD_format_e,
164) -> usize {
165    let srcSize = src.len();
166    let minInputSize = ZSTD_startingInputLength(format);
167
168    if srcSize < minInputSize {
169        // Short-read handling: validate magic prefix if we have anything.
170        if srcSize > 0 && format != ZSTD_format_e::ZSTD_f_zstd1_magicless {
171            let toCopy = 4.min(srcSize);
172            let mut hbuf = ZSTD_MAGICNUMBER.to_le_bytes();
173            hbuf[..toCopy].copy_from_slice(&src[..toCopy]);
174            if MEM_readLE32(&hbuf) != ZSTD_MAGICNUMBER {
175                let mut hbuf2 = ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes();
176                hbuf2[..toCopy].copy_from_slice(&src[..toCopy]);
177                if (MEM_readLE32(&hbuf2) & ZSTD_MAGIC_SKIPPABLE_MASK) != ZSTD_MAGIC_SKIPPABLE_START
178                {
179                    return ERROR(ErrorCode::PrefixUnknown);
180                }
181            }
182        }
183        return minInputSize;
184    }
185
186    *zfh = ZSTD_FrameHeader::default();
187
188    if format != ZSTD_format_e::ZSTD_f_zstd1_magicless
189        && MEM_readLE32(&src[..4]) != ZSTD_MAGICNUMBER
190    {
191        if (MEM_readLE32(&src[..4]) & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START {
192            if srcSize < ZSTD_SKIPPABLEHEADERSIZE {
193                return ZSTD_SKIPPABLEHEADERSIZE;
194            }
195            zfh.frameType = ZSTD_FrameType_e::ZSTD_skippableFrame;
196            zfh.dictID = MEM_readLE32(&src[..4]) - ZSTD_MAGIC_SKIPPABLE_START;
197            zfh.headerSize = ZSTD_SKIPPABLEHEADERSIZE as u32;
198            zfh.frameContentSize =
199                MEM_readLE32(&src[ZSTD_FRAMEIDSIZE..ZSTD_FRAMEIDSIZE + 4]) as u64;
200            return 0;
201        }
202        return ERROR(ErrorCode::PrefixUnknown);
203    }
204
205    let fhsize = ZSTD_frameHeaderSize_internal(src, format);
206    if crate::common::error::ERR_isError(fhsize) {
207        return fhsize;
208    }
209    if srcSize < fhsize {
210        return fhsize;
211    }
212    zfh.headerSize = fhsize as u32;
213
214    let fhdByte = src[minInputSize - 1];
215    let mut pos = minInputSize;
216    let dictIDSizeCode = (fhdByte & 3) as u32;
217    let checksumFlag = ((fhdByte >> 2) & 1) as u32;
218    let singleSegment = ((fhdByte >> 5) & 1) as u32;
219    let fcsID = (fhdByte >> 6) as u32;
220    if (fhdByte & 0x08) != 0 {
221        // Reserved bit must be zero.
222        return ERROR(ErrorCode::FrameParameterUnsupported);
223    }
224
225    let mut windowSize: u64 = 0;
226    let mut dictID: u32 = 0;
227    let mut frameContentSize: u64 = ZSTD_CONTENTSIZE_UNKNOWN;
228
229    if singleSegment == 0 {
230        let wlByte = src[pos];
231        pos += 1;
232        let windowLog = ((wlByte >> 3) as u32) + ZSTD_WINDOWLOG_ABSOLUTEMIN;
233        let winLogMax = if core::mem::size_of::<usize>() == 8 {
234            ZSTD_WINDOWLOG_MAX_64
235        } else {
236            ZSTD_WINDOWLOG_MAX_32
237        };
238        if windowLog > winLogMax {
239            return ERROR(ErrorCode::FrameParameterWindowTooLarge);
240        }
241        windowSize = 1u64 << windowLog;
242        windowSize += (windowSize >> 3) * (wlByte & 7) as u64;
243    }
244    match dictIDSizeCode {
245        0 => {}
246        1 => {
247            dictID = src[pos] as u32;
248            pos += 1;
249        }
250        2 => {
251            dictID = MEM_readLE16(&src[pos..pos + 2]) as u32;
252            pos += 2;
253        }
254        _ => {
255            // 3
256            dictID = MEM_readLE32(&src[pos..pos + 4]);
257            pos += 4;
258        }
259    }
260    match fcsID {
261        0 => {
262            if singleSegment != 0 {
263                frameContentSize = src[pos] as u64;
264            }
265        }
266        1 => frameContentSize = MEM_readLE16(&src[pos..pos + 2]) as u64 + 256,
267        2 => frameContentSize = MEM_readLE32(&src[pos..pos + 4]) as u64,
268        _ => frameContentSize = MEM_readLE64(&src[pos..pos + 8]),
269    }
270    if singleSegment != 0 {
271        windowSize = frameContentSize;
272    }
273
274    zfh.frameType = ZSTD_FrameType_e::ZSTD_frame;
275    zfh.frameContentSize = frameContentSize;
276    zfh.windowSize = windowSize;
277    zfh.blockSizeMax =
278        windowSize.min(crate::decompress::zstd_decompress_block::ZSTD_BLOCKSIZE_MAX as u64) as u32;
279    zfh.dictID = dictID;
280    zfh.checksumFlag = checksumFlag;
281    0
282}
283
284/// Port of `ZSTD_getFrameHeader` (`zstd.h:1154`). Parses `src` as the
285/// start of a default `ZSTD_f_zstd1` frame and populates `zfh`.
286/// Returns 0 on success, a positive byte-count hint when `src` is
287/// short, or an error code when the header is malformed. For
288/// magicless callers use `ZSTD_getFrameHeader_advanced`.
289pub fn ZSTD_getFrameHeader(zfh: &mut ZSTD_FrameHeader, src: &[u8]) -> usize {
290    ZSTD_getFrameHeader_advanced(zfh, src, ZSTD_format_e::ZSTD_f_zstd1)
291}
292
293#[cfg(test)]
294#[allow(clippy::field_reassign_with_default)]
295mod tests {
296    use super::*;
297
298    /// Hand-build a minimal valid zstd-format dictionary:
299    /// 4-byte magic + 4-byte dictID + serialized HUF CTable + FSE OF/ML/LL + 3×u32 rep + raw content.
300    /// Uses the compressor-side serializers so the test exercises the
301    /// real writers; we verify the decoder-side parsers accept it.
302    fn build_minimal_zstd_dict(dictID: u32, content: &[u8]) -> Vec<u8> {
303        use crate::common::mem::MEM_writeLE32;
304        use crate::compress::fse_compress::{
305            FSE_buildCTable_wksp, FSE_normalizeCount, FSE_writeNCount,
306        };
307        use crate::compress::huf_compress::{
308            HUF_buildCTable_wksp, HUF_writeCTable, HUF_CTABLE_WORKSPACE_SIZE_U32,
309        };
310        use crate::decompress::zstd_decompress_block::{
311            LL_defaultNorm, LL_defaultNormLog, ML_defaultNorm, ML_defaultNormLog, MaxLL, MaxML,
312            MaxOff, OF_defaultNorm, OF_defaultNormLog,
313        };
314
315        let mut out = Vec::new();
316        // Magic + dictID.
317        let mut magic_bytes = [0u8; 4];
318        MEM_writeLE32(&mut magic_bytes, ZSTD_MAGICNUMBER_DICTIONARY);
319        out.extend_from_slice(&magic_bytes);
320        let mut id_bytes = [0u8; 4];
321        MEM_writeLE32(&mut id_bytes, dictID);
322        out.extend_from_slice(&id_bytes);
323
324        // HUF CTable — seed from content bytes so writeCTable has real weights.
325        let mut count = [0u32; 256];
326        for &b in content.iter() {
327            count[b as usize] += 1;
328        }
329        // Pad single-symbol content to avoid degenerate table.
330        for (i, c) in count.iter_mut().enumerate().take(16) {
331            if *c == 0 {
332                *c = 1;
333                let _ = i;
334            }
335        }
336        let maxSymbolValue = count
337            .iter()
338            .enumerate()
339            .rposition(|(_, &c)| c > 0)
340            .unwrap_or(0) as u32;
341        let totalCount: usize = count.iter().sum::<u32>() as usize;
342        let tableLog =
343            crate::compress::huf_compress::HUF_optimalTableLog(11, totalCount, maxSymbolValue);
344        let mut ct = vec![0u64; 257];
345        let mut wksp = vec![0u32; HUF_CTABLE_WORKSPACE_SIZE_U32];
346        FSE_buildCTable_wksp(
347            &mut [0u32; 512], // dummy to avoid unused import
348            &[0i16; 1],
349            0,
350            5,
351            &mut [0u8; 1024],
352        );
353        let _ = HUF_buildCTable_wksp(&mut ct, &count, maxSymbolValue, tableLog, &mut wksp);
354        let mut huf_hdr = vec![0u8; 512];
355        let w = HUF_writeCTable(&mut huf_hdr, &ct, maxSymbolValue, tableLog);
356        assert!(!crate::common::error::ERR_isError(w));
357        out.extend_from_slice(&huf_hdr[..w]);
358
359        // FSE OF/ML/LL tables using default distributions.
360        let mut fse_buf = vec![0u8; 256];
361        // OF
362        let w = FSE_writeNCount(&mut fse_buf, &OF_defaultNorm, MaxOff, OF_defaultNormLog);
363        assert!(!crate::common::error::ERR_isError(w));
364        out.extend_from_slice(&fse_buf[..w]);
365        // ML
366        let w = FSE_writeNCount(&mut fse_buf, &ML_defaultNorm, MaxML, ML_defaultNormLog);
367        assert!(!crate::common::error::ERR_isError(w));
368        out.extend_from_slice(&fse_buf[..w]);
369        // LL
370        let w = FSE_writeNCount(&mut fse_buf, &LL_defaultNorm, MaxLL, LL_defaultNormLog);
371        assert!(!crate::common::error::ERR_isError(w));
372        out.extend_from_slice(&fse_buf[..w]);
373        // Silence unused-import warnings.
374        let _ = FSE_normalizeCount;
375
376        // 3 × rep values — must be nonzero and ≤ content.len().
377        let safe = (content.len() as u32).clamp(1, 8);
378        for r in [safe, safe.saturating_sub(1).max(1), 1u32] {
379            let mut rb = [0u8; 4];
380            MEM_writeLE32(&mut rb, r);
381            out.extend_from_slice(&rb);
382        }
383
384        // Raw content.
385        out.extend_from_slice(content);
386        out
387    }
388
389    #[test]
390    fn insertDictionary_roundtrips_magic_prefix_dict() {
391        // Build a real zstd-format dict with a known dictID + raw
392        // content. Call `ZSTD_decompress_insertDictionary` and verify:
393        //   - dictID parsed from bytes 4..8 lands on dctx.dictID
394        //   - stream_dict holds the raw content portion
395        //   - litEntropy + fseEntropy flags turn on
396        use crate::decompress::zstd_decompress_block::ZSTD_DCtx;
397        let content = b"zstd-dict-content-bytes-for-compression-context";
398        let dict = build_minimal_zstd_dict(0x1234_5678, content);
399
400        let mut dctx = ZSTD_DCtx::new();
401        let rc = ZSTD_decompress_insertDictionary(&mut dctx, &dict);
402        assert!(
403            !crate::common::error::ERR_isError(rc),
404            "insertDictionary failed: {}",
405            crate::common::error::ERR_getErrorName(rc)
406        );
407        assert_eq!(dctx.dictID, 0x1234_5678);
408        assert_eq!(dctx.stream_dict, content);
409        assert_eq!(dctx.litEntropy, 1);
410        assert_eq!(dctx.fseEntropy, 1);
411    }
412
413    #[test]
414    fn compress_insertDictionary_roundtrips_magic_prefix() {
415        // Symmetric parity gate: build a zstd-format dict, feed it
416        // through `ZSTD_compress_insertDictionary`, verify dictID +
417        // content stashing + entropy repeatMode transitions.
418        use crate::compress::zstd_compress::{ZSTD_compress_insertDictionary, ZSTD_createCCtx};
419        use crate::compress::zstd_compress_literals::HUF_repeat;
420        use crate::decompress::zstd_ddict::ZSTD_dictContentType_e;
421        let content = b"zstd-dict-content-bytes-for-compression-context";
422        let dict = build_minimal_zstd_dict(0xABCD_1234, content);
423
424        let mut cctx = ZSTD_createCCtx().unwrap();
425        let params = cctx.requestedParams;
426        let rc = ZSTD_compress_insertDictionary(
427            &mut cctx,
428            &params,
429            &dict,
430            ZSTD_dictContentType_e::ZSTD_dct_auto,
431        );
432        assert!(
433            !crate::common::error::ERR_isError(rc),
434            "compress_insertDictionary failed: {}",
435            crate::common::error::ERR_getErrorName(rc)
436        );
437        assert_eq!(cctx.dictID, 0xABCD_1234);
438        assert_eq!(cctx.stream_dict, content);
439        // Entropy repeatMode: HUF becomes `check` (or `valid` if every
440        // symbol present), FSE tables become `check` or `valid`.
441        // Either way, it's no longer `none`.
442        assert_ne!(cctx.prevEntropy.huf.repeatMode, HUF_repeat::HUF_repeat_none);
443    }
444
445    #[test]
446    fn insertDictionary_raw_content_stashes_bytes() {
447        // Dict with no magic prefix → raw content path.
448        use crate::decompress::zstd_decompress_block::ZSTD_DCtx;
449        let raw = b"this is not a zstd dict, just arbitrary bytes";
450        let mut dctx = ZSTD_DCtx::new();
451        let rc = ZSTD_decompress_insertDictionary(&mut dctx, raw);
452        assert!(!crate::common::error::ERR_isError(rc));
453        assert_eq!(dctx.dictID, 0);
454        assert_eq!(dctx.stream_dict, raw);
455    }
456
457    #[test]
458    fn zstd_sizeof_dctx_includes_owned_tables() {
459        use crate::decompress::zstd_decompress_block::ZSTD_DCtx;
460        let dctx = ZSTD_DCtx::new();
461        let sz = ZSTD_sizeof_DCtx(&dctx);
462        // Must include at least the seq DTables (3 × ~4 KB for LL/OF/ML default).
463        assert!(sz > 4096, "sizeof_DCtx unexpectedly small: {sz}");
464        // DStream alias is the same.
465        assert_eq!(ZSTD_sizeof_DStream(&dctx), sz);
466    }
467
468    #[test]
469    fn zstd_sizeof_dctx_grows_when_dict_loaded() {
470        // Loading a 4 KB dict must bump the reported size by >= the
471        // dict's capacity — otherwise callers that size allocation
472        // pools from this helper will under-provision.
473        use crate::decompress::zstd_decompress_block::ZSTD_DCtx;
474        let mut dctx = ZSTD_DCtx::new();
475        let before = ZSTD_sizeof_DCtx(&dctx);
476        let dict = vec![0xABu8; 4096];
477        ZSTD_DCtx_loadDictionary(&mut dctx, &dict);
478        let after = ZSTD_sizeof_DCtx(&dctx);
479        assert!(
480            after >= before + dict.len(),
481            "sizeof_DCtx did not reflect loaded dict: before={before} after={after}"
482        );
483    }
484
485    #[test]
486    fn zstd_dParam_getBounds_windowLogMax_range() {
487        // Upstream: [ZSTD_WINDOWLOG_ABSOLUTEMIN, ZSTD_WINDOWLOG_MAX].
488        // That's [10, 31] on 64-bit and [10, 30] on 32-bit — NOT the
489        // 27-byte `ZSTD_WINDOWLOG_LIMIT_DEFAULT` which is merely the
490        // streaming-decoder's default cap, not the absolute bound.
491        let b = ZSTD_dParam_getBounds(ZSTD_dParameter::ZSTD_d_windowLogMax);
492        assert_eq!(b.error, 0);
493        let expected_upper = if crate::common::mem::MEM_32bits() != 0 {
494            30
495        } else {
496            31
497        };
498        assert_eq!((b.lowerBound, b.upperBound), (10, expected_upper));
499    }
500
501    #[test]
502    fn fresh_DCtx_getParameter_windowLogMax_returns_LIMIT_DEFAULT() {
503        // Upstream (zstd_decompress.c:244) initializes `maxWindowSize`
504        // to `(1 << ZSTD_WINDOWLOG_LIMIT_DEFAULT) + 1`; `getParameter`
505        // therefore returns 27 on a fresh DCtx. Previously Rust port
506        // returned 0 — diverged from C-compat callers that use
507        // getParameter to decide whether to override the cap.
508        let dctx = ZSTD_DCtx::default();
509        let mut got = -999i32;
510        ZSTD_DCtx_getParameter(&dctx, ZSTD_dParameter::ZSTD_d_windowLogMax, &mut got);
511        assert_eq!(got, ZSTD_WINDOWLOG_LIMIT_DEFAULT as i32);
512    }
513
514    #[test]
515    fn DCtx_setParameter_windowLogMax_maps_zero_to_LIMIT_DEFAULT() {
516        // Upstream contract: `ZSTD_DCtx_setParameter(d_windowLogMax, 0)`
517        // substitutes `ZSTD_WINDOWLOG_LIMIT_DEFAULT` (27) before
518        // bounds-checking and storing. C callers rely on this — 0 is
519        // the documented way to request "default cap".
520        let mut dctx = ZSTD_DCtx::default();
521        let rc = ZSTD_DCtx_setParameter(&mut dctx, ZSTD_dParameter::ZSTD_d_windowLogMax, 0);
522        assert_eq!(rc, 0);
523        let mut got = -1i32;
524        ZSTD_DCtx_getParameter(&dctx, ZSTD_dParameter::ZSTD_d_windowLogMax, &mut got);
525        assert_eq!(got, 27);
526    }
527
528    #[test]
529    fn DCtx_setParameter_windowLogMax_rejects_out_of_range() {
530        // Previously stored any value unchecked — now must emit
531        // `ParameterOutOfBound` to match upstream CHECK_DBOUNDS.
532        use crate::common::error::{ERR_getErrorCode, ERR_isError};
533        let mut dctx = ZSTD_DCtx::default();
534        for oor in [-1, 9, 40, 100] {
535            let rc = ZSTD_DCtx_setParameter(&mut dctx, ZSTD_dParameter::ZSTD_d_windowLogMax, oor);
536            assert!(ERR_isError(rc), "expected error for value={oor}");
537            assert_eq!(ERR_getErrorCode(rc), ErrorCode::ParameterOutOfBound);
538        }
539    }
540
541    #[test]
542    fn dParam_bounds_accept_windowLog_at_upper_edge_via_setMaxWindowSize() {
543        // Regression: previously `ZSTD_dParam_getBounds` reported the
544        // LIMIT_DEFAULT (27) as the upper bound, rejecting any attempt
545        // to permit a window larger than 128 MB. Upstream's bound is
546        // the absolute WINDOWLOG_MAX — callers legitimately need to
547        // raise the cap for oversized frames. Pinning both edges:
548        //   - 1 << ABSOLUTEMIN (10) must be accepted
549        //   - 1 << MAX (31 on 64-bit / 30 on 32-bit) must be accepted
550        //   - 1 << (MAX+1) would overflow on 32-bit and exceeds
551        //     usize on most build configs, so the upper-edge accept
552        //     is the tightest usable pin.
553        let mut dctx = ZSTD_DCtx::default();
554        let min_size = 1usize << 10;
555        assert_eq!(ZSTD_DCtx_setMaxWindowSize(&mut dctx, min_size), 0);
556        let upper = if crate::common::mem::MEM_32bits() != 0 {
557            30
558        } else {
559            31
560        };
561        let max_size = 1usize << upper;
562        assert_eq!(ZSTD_DCtx_setMaxWindowSize(&mut dctx, max_size), 0);
563    }
564
565    #[test]
566    fn zstd_dstream_buffer_sizes_nonzero() {
567        assert!(ZSTD_DStreamInSize() > 0);
568        assert!(ZSTD_DStreamOutSize() > 0);
569    }
570
571    #[test]
572    fn zstd_dctx_setParameter_windowLogMax_roundtrips() {
573        use crate::decompress::zstd_decompress_block::ZSTD_DCtx;
574        let mut dctx = ZSTD_DCtx::new();
575        let rc = ZSTD_DCtx_setParameter(&mut dctx, ZSTD_dParameter::ZSTD_d_windowLogMax, 20);
576        assert_eq!(rc, 0);
577        let mut v = 0i32;
578        ZSTD_DCtx_getParameter(&dctx, ZSTD_dParameter::ZSTD_d_windowLogMax, &mut v);
579        assert_eq!(v, 20);
580    }
581
582    #[test]
583    fn zstd_dctx_reset_clears_streaming_state() {
584        use crate::decompress::zstd_decompress_block::ZSTD_DCtx;
585        let mut dctx = ZSTD_DCtx::new();
586        dctx.stream_in_buffer.extend_from_slice(b"pending");
587        ZSTD_DCtx_reset(&mut dctx, ZSTD_DResetDirective::ZSTD_reset_session_only);
588        assert!(dctx.stream_in_buffer.is_empty());
589    }
590
591    #[test]
592    fn zstd_estimate_dctx_size_positive() {
593        let s = ZSTD_estimateDCtxSize();
594        assert!(s > 0);
595        // Streaming needs more than DCtx alone.
596        assert!(ZSTD_estimateDStreamSize(1 << 17) > s);
597    }
598
599    #[test]
600    fn zstd_estimate_dstream_size_from_frame_reflects_window() {
601        // A small-window frame (level 1 on small input) should need
602        // less than a maxed-out 128 KB window estimate.
603        let src: Vec<u8> = b"small src for small window".to_vec();
604        let mut dst = vec![0u8; 256];
605        let n = crate::compress::zstd_compress::ZSTD_compress(&mut dst, &src, 1);
606        dst.truncate(n);
607        let from_frame = ZSTD_estimateDStreamSize_fromFrame(&dst);
608        // Bogus input → falls back to default (1<<17 window).
609        let fallback = ZSTD_estimateDStreamSize_fromFrame(&[0xFF, 0xFF, 0xFF, 0xFF]);
610        // Both are non-zero and fallback covers at least the default.
611        assert!(from_frame > 0);
612        assert!(fallback > 0);
613    }
614
615    #[test]
616    fn zstd_decompressBlock_roundtrips_a_compressed_block_body() {
617        use crate::decompress::zstd_decompress_block::{
618            blockProperties_t, blockType_e, ZSTD_DCtx, ZSTD_blockHeaderSize, ZSTD_getcBlockSize,
619        };
620        // Produce a single compressed block via our compressor, then
621        // decode just its body (no frame header) through the public
622        // ZSTD_decompressBlock entry point.
623        let src: Vec<u8> = b"hello block api. "
624            .iter()
625            .cycle()
626            .take(200)
627            .copied()
628            .collect();
629
630        // Compress the whole thing as a frame, then extract the
631        // compressed block body by parsing the frame header manually.
632        let mut frame = vec![0u8; 1024];
633        let n = crate::compress::zstd_compress::ZSTD_compress(&mut frame, &src, 1);
634        frame.truncate(n);
635
636        // Parse frame header.
637        let mut zfh = super::ZSTD_FrameHeader::default();
638        let rc = super::ZSTD_getFrameHeader(&mut zfh, &frame);
639        assert_eq!(rc, 0);
640        let body_start = zfh.headerSize as usize;
641
642        // Parse block header.
643        let mut bp = blockProperties_t {
644            blockType: blockType_e::bt_raw,
645            lastBlock: 0,
646            origSize: 0,
647        };
648        let body_size = ZSTD_getcBlockSize(&frame[body_start..], &mut bp);
649
650        // Only test when block is actually compressed (not raw/RLE).
651        if bp.blockType == blockType_e::bt_compressed {
652            let body = &frame
653                [body_start + ZSTD_blockHeaderSize..body_start + ZSTD_blockHeaderSize + body_size];
654            let mut dctx = ZSTD_DCtx::new();
655            let mut out = vec![0u8; src.len() + 64];
656            let d = ZSTD_decompressBlock(&mut dctx, &mut out, body);
657            assert!(!crate::common::error::ERR_isError(d));
658            assert_eq!(&out[..d], &src[..]);
659        }
660    }
661
662    #[test]
663    fn windowlog_and_contentsize_constants_match_upstream() {
664        // Pin the remaining format-level constants.
665        assert_eq!(ZSTD_WINDOWLOG_ABSOLUTEMIN, 10);
666        assert_eq!(ZSTD_WINDOWLOG_MAX_64, 31);
667        assert_eq!(ZSTD_WINDOWLOG_MAX_32, 30);
668        // Content-size sentinels: UNKNOWN = -1 (u64::MAX),
669        // ERROR = -2 (u64::MAX - 1). Ordering matters so the
670        // `ret >= ZSTD_CONTENTSIZE_ERROR` check in
671        // `ZSTD_getDecompressedSize` covers both sentinels.
672        assert_eq!(ZSTD_CONTENTSIZE_UNKNOWN, u64::MAX);
673        assert_eq!(ZSTD_CONTENTSIZE_ERROR, u64::MAX - 1);
674        // UNKNOWN is strictly greater than ERROR — the ordering is
675        // what lets callers collapse both sentinels with a single
676        // `ret >= ZSTD_CONTENTSIZE_ERROR` check.
677        assert_eq!(ZSTD_CONTENTSIZE_UNKNOWN - ZSTD_CONTENTSIZE_ERROR, 1);
678    }
679
680    #[test]
681    fn frame_format_magic_and_size_constants_match_spec() {
682        // Pin the format-level constants that must match the
683        // Zstandard format specification byte-for-byte for cross-
684        // compatibility with upstream and with the file format.
685        assert_eq!(ZSTD_MAGICNUMBER, 0xFD2FB528);
686        assert_eq!(ZSTD_MAGIC_DICTIONARY, 0xEC30A437);
687        assert_eq!(ZSTD_MAGIC_SKIPPABLE_START, 0x184D2A50);
688        assert_eq!(ZSTD_MAGIC_SKIPPABLE_MASK, 0xFFFFFFF0);
689        assert_eq!(ZSTD_FRAMEIDSIZE, 4);
690        assert_eq!(ZSTD_SKIPPABLEHEADERSIZE, 8);
691        // And the skippable mask + start must cover exactly the
692        // 16 valid skippable magics (variants 0..=15).
693        for v in 0u32..=15 {
694            let magic = ZSTD_MAGIC_SKIPPABLE_START | v;
695            assert_eq!(
696                magic & ZSTD_MAGIC_SKIPPABLE_MASK,
697                ZSTD_MAGIC_SKIPPABLE_START
698            );
699        }
700    }
701
702    #[test]
703    fn zstd_isFrame_detects_regular_and_skippable() {
704        // Regular frame magic.
705        let regular = ZSTD_MAGICNUMBER.to_le_bytes();
706        assert_eq!(ZSTD_isFrame(&regular), 1);
707        // Skippable frame magic variant 0.
708        let skippable = ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes();
709        assert_eq!(ZSTD_isFrame(&skippable), 1);
710        // Skippable variant 15 (bit 3-0 can be 0..=15).
711        let skippable15 = (ZSTD_MAGIC_SKIPPABLE_START | 0x0F).to_le_bytes();
712        assert_eq!(ZSTD_isFrame(&skippable15), 1);
713        // Garbage.
714        assert_eq!(ZSTD_isFrame(&[0xDE, 0xAD, 0xBE, 0xEF]), 0);
715        // Too short.
716        assert_eq!(ZSTD_isFrame(&[0xFD, 0x2F]), 0);
717        assert_eq!(ZSTD_isFrame(&[]), 0);
718    }
719
720    #[test]
721    fn zstd_isSkippableFrame_rejects_regular_frame() {
722        let regular = ZSTD_MAGICNUMBER.to_le_bytes();
723        assert_eq!(ZSTD_isSkippableFrame(&regular), 0);
724        let skippable = (ZSTD_MAGIC_SKIPPABLE_START | 0x05).to_le_bytes();
725        assert_eq!(ZSTD_isSkippableFrame(&skippable), 1);
726    }
727
728    #[test]
729    fn zstd_isFrame_rejects_dictionary_magic() {
730        // The `ZSTD_MAGIC_DICTIONARY` prefix (0xEC30A437) identifies a
731        // zstd dictionary file, NOT a compressed frame. `ZSTD_isFrame`
732        // must return 0 — otherwise callers that sniff input type
733        // would treat a dict as decompressible.
734        let dict_magic = ZSTD_MAGIC_DICTIONARY.to_le_bytes();
735        assert_eq!(ZSTD_isFrame(&dict_magic), 0);
736        // Same magic but padded — still not a frame.
737        let mut padded = dict_magic.to_vec();
738        padded.extend_from_slice(&[0u8; 4]);
739        assert_eq!(ZSTD_isFrame(&padded), 0);
740    }
741
742    #[test]
743    fn zstd_isSkippableFrame_rejects_dictionary_magic() {
744        // Symmetric with `zstd_isFrame_rejects_dictionary_magic`.
745        // Dict magic 0xEC30A437 also isn't a skippable frame.
746        let dict_magic = ZSTD_MAGIC_DICTIONARY.to_le_bytes();
747        assert_eq!(ZSTD_isSkippableFrame(&dict_magic), 0);
748    }
749
750    #[test]
751    fn zstd_isSkippableFrame_accepts_all_16_variants() {
752        // Upstream spec allows skippable magics from
753        // ZSTD_MAGIC_SKIPPABLE_START (0x184D2A50) to
754        // ZSTD_MAGIC_SKIPPABLE_START + 15 (0x184D2A5F). Verify
755        // every variant registers as a skippable frame.
756        for v in 0u32..=15 {
757            let magic = (ZSTD_MAGIC_SKIPPABLE_START + v).to_le_bytes();
758            assert_eq!(
759                ZSTD_isSkippableFrame(&magic),
760                1,
761                "variant {v} didn't register as skippable"
762            );
763        }
764        // Variant 16 wraps into non-skippable territory (0x184D2A60).
765        let invalid = (ZSTD_MAGIC_SKIPPABLE_START + 16).to_le_bytes();
766        assert_eq!(ZSTD_isSkippableFrame(&invalid), 0);
767    }
768
769    #[test]
770    fn zstd_isSkippableFrame_rejects_short_src_safely() {
771        // Safety: sub-4-byte inputs must return 0 without panicking
772        // on OOB slicing. Symmetric with `ZSTD_isFrame`'s coverage.
773        assert_eq!(ZSTD_isSkippableFrame(&[]), 0);
774        assert_eq!(ZSTD_isSkippableFrame(&[0x50]), 0);
775        assert_eq!(ZSTD_isSkippableFrame(&[0x50, 0x2A, 0x4D]), 0);
776    }
777
778    #[test]
779    fn zstd_getDictID_fromFrame_returns_zero_when_absent() {
780        // Compress a small payload without a dict → FHD has no dictID.
781        let src: Vec<u8> = b"hello world".to_vec();
782        let mut dst = vec![0u8; 128];
783        let n = crate::compress::zstd_compress::ZSTD_compress(&mut dst, &src, 1);
784        dst.truncate(n);
785        assert_eq!(ZSTD_getDictID_fromFrame(&dst), 0);
786    }
787
788    #[test]
789    fn copyRawBlock_and_setRleBlock_cover_happy_and_dst_too_small_paths() {
790        // copyRawBlock: byte-for-byte copy + DstSizeTooSmall.
791        let src = b"hello-raw";
792        let mut dst = [0u8; 16];
793        let n = ZSTD_copyRawBlock(&mut dst, src);
794        assert_eq!(n, src.len());
795        assert_eq!(&dst[..n], src);
796
797        let mut tiny = [0u8; 4];
798        assert!(crate::common::error::ERR_isError(ZSTD_copyRawBlock(
799            &mut tiny, src
800        )));
801
802        // setRleBlock: fill N copies of a byte + DstSizeTooSmall.
803        let mut buf = vec![0u8; 32];
804        let rle_n = ZSTD_setRleBlock(&mut buf, 0xAB, 10);
805        assert_eq!(rle_n, 10);
806        assert!(buf[..10].iter().all(|&b| b == 0xAB));
807        // Bytes past regenSize must not have been touched.
808        assert!(buf[10..].iter().all(|&b| b == 0));
809
810        let mut short = [0u8; 4];
811        assert!(crate::common::error::ERR_isError(ZSTD_setRleBlock(
812            &mut short, 0xCD, 10
813        )));
814    }
815
816    #[test]
817    fn findFrameCompressedSize_matches_compressor_output_for_multi_block() {
818        // Compress a 200 KB payload → guaranteed multi-block frame
819        // (> 128 KB block boundary). `ZSTD_findFrameCompressedSize`
820        // must report exactly the compressed byte count, not include
821        // trailing garbage or under-report.
822        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
823        let src: Vec<u8> = b"multi-block frame-size probe. "
824            .iter()
825            .cycle()
826            .take(200_000)
827            .copied()
828            .collect();
829        let bound = ZSTD_compressBound(src.len());
830        let mut dst = vec![0u8; bound];
831        let n = ZSTD_compress(&mut dst, &src, 1);
832        assert!(!crate::common::error::ERR_isError(n));
833
834        // Append trailing garbage to ensure the helper reports the
835        // real frame size, not just dst.len().
836        let mut with_trailer = dst[..n].to_vec();
837        with_trailer.extend_from_slice(&[0xFFu8; 64]);
838        let reported = ZSTD_findFrameCompressedSize(&with_trailer);
839        assert!(!crate::common::error::ERR_isError(reported));
840        assert_eq!(reported, n);
841    }
842
843    #[test]
844    fn findFrameSizeInfo_reports_skippable_frame_with_zero_decompressed_bound() {
845        // For a skippable frame, the returned info must be:
846        //   nbBlocks = 0
847        //   compressedSize = 8 + user_data_len
848        //   decompressedBound = 0
849        // This shape lets callers (`decompressBound`, CLI frame-walk)
850        // skip past the skippable region without attempting to
851        // allocate space for its "content".
852        let user_data_len: u32 = 10;
853        let mut src = Vec::new();
854        src.extend_from_slice(&ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes());
855        src.extend_from_slice(&user_data_len.to_le_bytes());
856        src.extend(core::iter::repeat_n(0u8, user_data_len as usize));
857
858        let info = ZSTD_findFrameSizeInfo(&src, ZSTD_format_e::ZSTD_f_zstd1);
859        assert_eq!(info.nbBlocks, 0);
860        assert_eq!(info.compressedSize, 8 + user_data_len as usize);
861        assert_eq!(info.decompressedBound, 0);
862    }
863
864    #[test]
865    fn decompressBound_all_skippable_frames_returns_zero() {
866        // Sibling of `findDecompressedSize_all_skippable_frames_returns_zero`:
867        // `ZSTD_decompressBound` must also report 0 when all input
868        // frames are skippable (they don't contribute to the output
869        // stream).
870        let mut src = Vec::new();
871        for &user_data_len in &[5u32, 12] {
872            src.extend_from_slice(&ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes());
873            src.extend_from_slice(&user_data_len.to_le_bytes());
874            src.extend(core::iter::repeat_n(0u8, user_data_len as usize));
875        }
876        assert_eq!(ZSTD_decompressBound(&src), 0);
877    }
878
879    #[test]
880    fn findDecompressedSize_all_skippable_frames_returns_zero() {
881        // Stream of two back-to-back skippable frames with no regular
882        // frames. `ZSTD_findDecompressedSize` must return 0 since
883        // skippable frames contribute nothing to the decompressed
884        // output stream.
885        let mut src = Vec::new();
886        for &user_data_len in &[5u32, 12] {
887            src.extend_from_slice(&ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes());
888            src.extend_from_slice(&user_data_len.to_le_bytes());
889            src.extend(core::iter::repeat_n(0u8, user_data_len as usize));
890        }
891        assert_eq!(ZSTD_findDecompressedSize(&src), 0);
892    }
893
894    #[test]
895    fn findDecompressedSize_returns_UNKNOWN_when_any_frame_lacks_fcs() {
896        // Contract: `ZSTD_findDecompressedSize` must return
897        // CONTENTSIZE_UNKNOWN if any frame in the stream lacks a
898        // declared FCS — NOT a silent garbage value. Callers rely
899        // on this sentinel to decide whether to pre-allocate.
900        let raw = make_raw_hello_frame();
901
902        // Build a frame with singleSegment=0 and fcsID=0 → FCS absent.
903        let mut no_fcs = Vec::new();
904        no_fcs.extend_from_slice(&ZSTD_MAGICNUMBER.to_le_bytes());
905        no_fcs.push(0x00); // FHD
906        no_fcs.push(0x20); // wlByte → windowLog=14
907                           // Single raw block: lastBlock=1, bt_raw=0, cSize=3.
908        no_fcs.push(((3u32 << 3) | 1) as u8);
909        no_fcs.push(0);
910        no_fcs.push(0);
911        no_fcs.extend_from_slice(b"abc");
912
913        // Stream = raw(FCS=5) + no_fcs_frame. Total must be UNKNOWN
914        // because one frame doesn't declare its size.
915        let mut stream = raw;
916        stream.extend_from_slice(&no_fcs);
917        assert_eq!(ZSTD_findDecompressedSize(&stream), ZSTD_CONTENTSIZE_UNKNOWN);
918    }
919
920    #[test]
921    fn getFrameContentSize_reports_compressed_payload_size_across_fcs_encodings() {
922        // `ZSTD_getFrameContentSize` should return the declared
923        // decompressed size verbatim across all FCS-code sizes
924        // (upstream encodes 1/2/4/8 bytes depending on magnitude).
925        // Pin each of the four size regimes the encoder selects.
926        let sizes: &[u64] = &[
927            1,             // singleSegment=1, fcsCode=0 → 1-byte FCS
928            300,           // fcsCode=1 → 2-byte FCS (stored - 256)
929            100_000,       // fcsCode=2 → 4-byte FCS
930            0x1_0000_0000, // fcsCode=3 → 8-byte FCS
931        ];
932        for &sz in sizes {
933            // For sizes > 128 KB we can't realistically compress and
934            // verify, but the header still must declare the FCS —
935            // so synthesize a frame directly via ZSTD_writeFrameHeader.
936            use crate::compress::zstd_compress::{
937                ZSTD_FrameParameters, ZSTD_writeFrameHeader, ZSTD_FRAMEHEADERSIZE_MAX,
938            };
939            use crate::decompress::zstd_decompress::ZSTD_WINDOWLOG_ABSOLUTEMIN;
940            let fp = ZSTD_FrameParameters {
941                contentSizeFlag: 1,
942                checksumFlag: 0,
943                noDictIDFlag: 1,
944            };
945            let mut hdr = vec![0u8; ZSTD_FRAMEHEADERSIZE_MAX];
946            let n = ZSTD_writeFrameHeader(&mut hdr, &fp, ZSTD_WINDOWLOG_ABSOLUTEMIN + 10, sz, 0);
947            assert!(!crate::common::error::ERR_isError(n));
948            hdr.truncate(n);
949            assert_eq!(
950                ZSTD_getFrameContentSize(&hdr),
951                sz,
952                "FCS roundtrip failed for size={sz}",
953            );
954        }
955    }
956
957    #[test]
958    fn findDecompressedSize_sums_multiple_concatenated_frames() {
959        // Multi-frame streams: `ZSTD_findDecompressedSize` should
960        // return the sum of every frame's FCS, treating skippable
961        // frames as contributing zero. Pin against three concatenated
962        // regular frames of distinct sizes.
963        let parts: &[&[u8]] = &[
964            b"alpha".as_ref(),
965            b"longer-part-here".as_ref(),
966            b"c".as_ref(),
967        ];
968        let expected_total: u64 = parts.iter().map(|p| p.len() as u64).sum();
969
970        let mut stream = Vec::new();
971        for part in parts {
972            let mut buf = vec![0u8; 256];
973            let n = crate::compress::zstd_compress::ZSTD_compress(&mut buf, part, 3);
974            assert!(!crate::common::error::ERR_isError(n));
975            stream.extend_from_slice(&buf[..n]);
976        }
977        assert_eq!(ZSTD_findDecompressedSize(&stream), expected_total);
978
979        // Insert a skippable frame in the middle — doesn't contribute
980        // to the decompressed-size sum.
981        let mut stream_with_skip = Vec::new();
982        {
983            let mut buf = vec![0u8; 256];
984            let n = crate::compress::zstd_compress::ZSTD_compress(&mut buf, parts[0], 3);
985            stream_with_skip.extend_from_slice(&buf[..n]);
986        }
987        stream_with_skip.extend_from_slice(&ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes());
988        stream_with_skip.extend_from_slice(&8u32.to_le_bytes());
989        stream_with_skip.extend_from_slice(b"SKIPDATA");
990        {
991            let mut buf = vec![0u8; 256];
992            let n = crate::compress::zstd_compress::ZSTD_compress(&mut buf, parts[1], 3);
993            stream_with_skip.extend_from_slice(&buf[..n]);
994        }
995        assert_eq!(
996            ZSTD_findDecompressedSize(&stream_with_skip),
997            (parts[0].len() + parts[1].len()) as u64,
998        );
999    }
1000
1001    #[test]
1002    fn findDecompressedSize_returns_ERROR_on_trailing_garbage_after_valid_frame() {
1003        // Upstream contract: a valid frame followed by bytes that don't
1004        // form a valid frame header is treated as a corrupted stream,
1005        // not an empty-tail success. `ZSTD_findDecompressedSize` must
1006        // return `ZSTD_CONTENTSIZE_ERROR` — not `UNKNOWN`, which would
1007        // wrongly suggest "decodable, just unknown size".
1008        let mut src = make_raw_hello_frame();
1009        // Append a few bytes that look like the start of a zstd magic
1010        // but truncate before the frame header can be parsed.
1011        src.extend_from_slice(&[0x28, 0xB5, 0x2F]);
1012        let rc = ZSTD_findDecompressedSize(&src);
1013        assert_eq!(rc, ZSTD_CONTENTSIZE_ERROR);
1014    }
1015
1016    #[test]
1017    fn zstd_getDictID_fromFrame_reads_dictID_when_present() {
1018        // Build a synthetic frame with a 4-byte dictID via
1019        // ZSTD_writeFrameHeader and verify round-trip through
1020        // ZSTD_getDictID_fromFrame.
1021        use crate::compress::zstd_compress::{
1022            ZSTD_FrameParameters, ZSTD_writeFrameHeader, ZSTD_FRAMEHEADERSIZE_MAX,
1023        };
1024        let dictID_in = 0xDEAD_BEEFu32;
1025        let fParams = ZSTD_FrameParameters {
1026            contentSizeFlag: 0,
1027            checksumFlag: 0,
1028            noDictIDFlag: 0,
1029        };
1030        let mut buf = [0u8; ZSTD_FRAMEHEADERSIZE_MAX];
1031        let n = ZSTD_writeFrameHeader(&mut buf, &fParams, 17, 0, dictID_in);
1032        assert!(!crate::common::error::ERR_isError(n));
1033        let got = ZSTD_getDictID_fromFrame(&buf[..n]);
1034        assert_eq!(got, dictID_in);
1035    }
1036
1037    #[test]
1038    fn frame_header_truncated_requests_more() {
1039        // Only magic bytes — need 5 bytes (magic + FHD) to even start.
1040        let mut zfh = ZSTD_FrameHeader::default();
1041        let src = ZSTD_MAGICNUMBER.to_le_bytes();
1042        let rc = ZSTD_getFrameHeader(&mut zfh, &src);
1043        assert_eq!(rc, 5);
1044    }
1045
1046    #[test]
1047    fn createDCtx_advanced_and_createDStream_advanced_return_Some() {
1048        // Symmetric with `ZSTD_createCCtx_advanced`. The advanced
1049        // creators must return functional decoder objects.
1050        use crate::compress::zstd_compress::ZSTD_customMem;
1051        let _dctx = ZSTD_createDCtx_advanced(ZSTD_customMem::default()).unwrap();
1052        let _dstream = ZSTD_createDStream_advanced(ZSTD_customMem::default()).unwrap();
1053
1054        // And prove the returned DCtx actually decodes: compress a
1055        // payload, feed into _advanced-created DCtx, verify roundtrip.
1056        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
1057        let src: Vec<u8> = b"DCtx_advanced functional probe ".repeat(20);
1058        let bound = ZSTD_compressBound(src.len());
1059        let mut compressed = vec![0u8; bound];
1060        let n = ZSTD_compress(&mut compressed, &src, 1);
1061        let mut out = vec![0u8; src.len() + 64];
1062        let d = ZSTD_decompress(&mut out, &compressed[..n]);
1063        assert_eq!(&out[..d], &src[..]);
1064    }
1065
1066    #[test]
1067    fn decompresses_upstream_level10_large_real_text_frame() {
1068        use crate::decompress::zstd_decompress_block::{
1069            active_ll_table, active_ml_table, active_of_table, blockProperties_t, blockType_e,
1070            streaming_operation, ZSTD_DCtx, ZSTD_blockHeaderSize, ZSTD_buildDefaultSeqTables,
1071            ZSTD_decodeLiteralsBlock, ZSTD_decodeSeqHeaders, ZSTD_decoder_entropy_rep,
1072            ZSTD_decompressSequences_body, ZSTD_getcBlockSize,
1073        };
1074        use std::fs;
1075        use std::io::Write;
1076        use std::path::PathBuf;
1077        use std::process::{Command, Stdio};
1078
1079        let which = Command::new("which")
1080            .arg("zstd")
1081            .output()
1082            .expect("which zstd");
1083        if !which.status.success() {
1084            eprintln!("upstream zstd not on $PATH; skipping");
1085            return;
1086        }
1087        let zstd = PathBuf::from(String::from_utf8(which.stdout).unwrap().trim());
1088
1089        let seed = fs::read("tests/fixtures/zstd_h.txt").expect("read zstd_h.txt");
1090        let src: Vec<u8> = seed.repeat(20);
1091
1092        let mut child = Command::new(&zstd)
1093            .args(["-q", "--no-check", "-10", "-c", "-"])
1094            .stdin(Stdio::piped())
1095            .stdout(Stdio::piped())
1096            .spawn()
1097            .expect("spawn upstream zstd");
1098        child.stdin.as_mut().unwrap().write_all(&src).unwrap();
1099        let out = child.wait_with_output().expect("wait upstream zstd");
1100        assert!(
1101            out.status.success(),
1102            "upstream compression failed: {}",
1103            String::from_utf8_lossy(&out.stderr)
1104        );
1105        let mut dctx = ZSTD_DCtx::new();
1106        ZSTD_buildDefaultSeqTables(&mut dctx);
1107        let mut decoded = vec![0u8; src.len()];
1108        let mut rep = ZSTD_decoder_entropy_rep::default();
1109        let mut zfh = ZSTD_FrameHeader::default();
1110        let hdr = ZSTD_getFrameHeader_advanced(&mut zfh, &out.stdout, dctx.format);
1111        assert_eq!(hdr, 0);
1112        let mut ip = zfh.headerSize as usize;
1113        let mut op = 0usize;
1114        let mut block_idx = 0usize;
1115        loop {
1116            let mut bp = blockProperties_t {
1117                blockType: blockType_e::bt_raw,
1118                lastBlock: 0,
1119                origSize: 0,
1120            };
1121            let cblock = ZSTD_getcBlockSize(&out.stdout[ip..], &mut bp);
1122            assert!(
1123                !crate::common::error::ERR_isError(cblock),
1124                "block {block_idx}: cblock parse failed: {}",
1125                crate::common::error::ERR_getErrorName(cblock)
1126            );
1127            ip += ZSTD_blockHeaderSize;
1128            match bp.blockType {
1129                blockType_e::bt_compressed => {
1130                    let block = &out.stdout[ip..ip + cblock];
1131                    let lit_rc = ZSTD_decodeLiteralsBlock(
1132                        &mut dctx,
1133                        block,
1134                        &mut decoded[op..],
1135                        streaming_operation::not_streaming,
1136                    );
1137                    assert!(
1138                        !crate::common::error::ERR_isError(lit_rc),
1139                        "block {block_idx}: literals failed: {}",
1140                        crate::common::error::ERR_getErrorName(lit_rc)
1141                    );
1142                    let mut nb_seq = 0i32;
1143                    let seq_header =
1144                        ZSTD_decodeSeqHeaders(&mut dctx, &mut nb_seq, &block[lit_rc..]);
1145                    assert!(
1146                        !crate::common::error::ERR_isError(seq_header),
1147                        "block {block_idx}: seq headers failed: {}",
1148                        crate::common::error::ERR_getErrorName(seq_header)
1149                    );
1150                    let lit_snapshot = dctx.litExtraBuffer[..dctx.litSize].to_vec();
1151                    let ll = active_ll_table(&dctx).to_vec();
1152                    let of = active_of_table(&dctx).to_vec();
1153                    let ml = active_ml_table(&dctx).to_vec();
1154                    let seq_rc = ZSTD_decompressSequences_body(
1155                        &mut decoded,
1156                        op,
1157                        &[],
1158                        &block[lit_rc + seq_header..],
1159                        nb_seq,
1160                        &lit_snapshot,
1161                        dctx.litSize,
1162                        &ll,
1163                        &of,
1164                        &ml,
1165                        &mut rep,
1166                    );
1167                    assert!(
1168                        !crate::common::error::ERR_isError(seq_rc),
1169                        "block {block_idx}: sequence body failed: {}",
1170                        crate::common::error::ERR_getErrorName(seq_rc)
1171                    );
1172                    op += seq_rc;
1173                }
1174                blockType_e::bt_raw => {
1175                    let rc = ZSTD_copyRawBlock(&mut decoded[op..], &out.stdout[ip..ip + cblock]);
1176                    assert!(
1177                        !crate::common::error::ERR_isError(rc),
1178                        "block {block_idx}: raw block failed: {}",
1179                        crate::common::error::ERR_getErrorName(rc)
1180                    );
1181                    op += rc;
1182                }
1183                blockType_e::bt_rle => {
1184                    let rc =
1185                        ZSTD_setRleBlock(&mut decoded[op..], out.stdout[ip], bp.origSize as usize);
1186                    assert!(
1187                        !crate::common::error::ERR_isError(rc),
1188                        "block {block_idx}: rle block failed: {}",
1189                        crate::common::error::ERR_getErrorName(rc)
1190                    );
1191                    op += rc;
1192                }
1193                blockType_e::bt_reserved => panic!("block {block_idx}: reserved block"),
1194            }
1195            ip += cblock;
1196            block_idx += 1;
1197            if bp.lastBlock != 0 {
1198                break;
1199            }
1200        }
1201        let d = ZSTD_decompress(&mut decoded, &out.stdout);
1202        assert!(
1203            !crate::common::error::ERR_isError(d),
1204            "rust decompressor rejected upstream frame: {}",
1205            crate::common::error::ERR_getErrorName(d)
1206        );
1207        assert_eq!(&decoded[..op], &src[..]);
1208        assert_eq!(&decoded[..d], &src[..]);
1209    }
1210
1211    #[test]
1212    fn createDCtx_advanced_rejects_invalid_custommem_pairs() {
1213        use crate::compress::zstd_compress::ZSTD_customMem;
1214
1215        fn dummy_alloc(_opaque: usize, _size: usize) -> *mut core::ffi::c_void {
1216            core::ptr::null_mut()
1217        }
1218
1219        let invalid = ZSTD_customMem {
1220            customAlloc: Some(dummy_alloc),
1221            customFree: None,
1222            opaque: 1,
1223        };
1224
1225        assert!(ZSTD_createDCtx_advanced(invalid).is_none());
1226        assert!(ZSTD_createDStream_advanced(invalid).is_none());
1227    }
1228
1229    #[test]
1230    fn advanced_dctx_surfaces_invoke_custom_allocator_callbacks() {
1231        use crate::compress::zstd_compress::ZSTD_customMem;
1232        use core::sync::atomic::{AtomicUsize, Ordering};
1233
1234        static ALLOCS: AtomicUsize = AtomicUsize::new(0);
1235        static FREES: AtomicUsize = AtomicUsize::new(0);
1236
1237        fn counting_alloc(_opaque: usize, size: usize) -> *mut core::ffi::c_void {
1238            use std::alloc::{alloc, Layout};
1239
1240            const ALIGN: usize = 64;
1241            const HEADER_WORDS: usize = 2;
1242
1243            let total = size.max(1) + ALIGN + HEADER_WORDS * core::mem::size_of::<usize>();
1244            let layout = Layout::from_size_align(total, ALIGN).unwrap();
1245            unsafe {
1246                let base = alloc(layout);
1247                if base.is_null() {
1248                    return core::ptr::null_mut();
1249                }
1250                let payload_addr =
1251                    (base as usize + HEADER_WORDS * core::mem::size_of::<usize>() + ALIGN - 1)
1252                        & !(ALIGN - 1);
1253                let header = (payload_addr as *mut usize).sub(HEADER_WORDS);
1254                header.write(base as usize);
1255                header.add(1).write(total);
1256                ALLOCS.fetch_add(1, Ordering::SeqCst);
1257                payload_addr as *mut core::ffi::c_void
1258            }
1259        }
1260
1261        fn counting_free(_opaque: usize, address: *mut core::ffi::c_void) {
1262            use std::alloc::{dealloc, Layout};
1263
1264            const ALIGN: usize = 64;
1265            const HEADER_WORDS: usize = 2;
1266
1267            if address.is_null() {
1268                return;
1269            }
1270            unsafe {
1271                let header = (address as *mut usize).sub(HEADER_WORDS);
1272                let base = header.read() as *mut u8;
1273                let total = header.add(1).read();
1274                let layout = Layout::from_size_align(total, ALIGN).unwrap();
1275                dealloc(base, layout);
1276                FREES.fetch_add(1, Ordering::SeqCst);
1277            }
1278        }
1279
1280        let custom = ZSTD_customMem {
1281            customAlloc: Some(counting_alloc),
1282            customFree: Some(counting_free),
1283            opaque: 11,
1284        };
1285
1286        let dctx = ZSTD_createDCtx_advanced(custom).unwrap();
1287        assert_eq!(dctx.customMem, custom);
1288        let dstream = ZSTD_createDStream_advanced(custom).unwrap();
1289        assert_eq!(dstream.customMem, custom);
1290
1291        assert_eq!(ALLOCS.load(Ordering::SeqCst), 2);
1292        assert_eq!(FREES.load(Ordering::SeqCst), 0);
1293        assert_eq!(ZSTD_freeDCtx(dctx), 0);
1294        assert_eq!(ZSTD_freeDStream(Some(dstream)), 0);
1295        assert_eq!(FREES.load(Ordering::SeqCst), 2);
1296    }
1297
1298    #[test]
1299    fn ZSTD_DStream_is_alias_for_ZSTD_DCtx() {
1300        // Symmetric with the compress-side alias test. Upstream
1301        // `typedef ZSTD_DCtx ZSTD_DStream`; our `pub type` mirrors.
1302        assert_eq!(
1303            core::mem::size_of::<ZSTD_DStream>(),
1304            core::mem::size_of::<ZSTD_DCtx>()
1305        );
1306        let ds: Box<ZSTD_DStream> = ZSTD_createDStream().unwrap();
1307        assert_eq!(ZSTD_sizeof_DCtx(&ds), ZSTD_sizeof_DStream(&ds));
1308    }
1309
1310    #[test]
1311    fn ZSTD_nextInputType_e_discriminants_match_upstream() {
1312        // `ZSTD_nextInputType_e` is the return value of the public
1313        // `ZSTD_nextInputType()`. Upstream declares it as a bare
1314        // `typedef enum { ... }` so discriminants are the default
1315        // sequential 0..5 — any drift would mis-signal block/header
1316        // expectations to C callers consuming this enum.
1317        assert_eq!(ZSTD_nextInputType_e::ZSTDnit_frameHeader as u32, 0);
1318        assert_eq!(ZSTD_nextInputType_e::ZSTDnit_blockHeader as u32, 1);
1319        assert_eq!(ZSTD_nextInputType_e::ZSTDnit_block as u32, 2);
1320        assert_eq!(ZSTD_nextInputType_e::ZSTDnit_lastBlock as u32, 3);
1321        assert_eq!(ZSTD_nextInputType_e::ZSTDnit_checksum as u32, 4);
1322        assert_eq!(ZSTD_nextInputType_e::ZSTDnit_skippableFrame as u32, 5);
1323    }
1324
1325    #[test]
1326    fn decompressStream_simpleArgs_forwards_to_decompressStream() {
1327        // Parity with upstream's `ZSTD_decompressStream_simpleArgs`:
1328        // a thin forwarder over `decompressStream`. Verify a basic
1329        // roundtrip so a future refactor that accidentally decouples
1330        // them trips this gate.
1331        use crate::common::error::ERR_isError;
1332        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
1333
1334        let src = b"decompressStream_simpleArgs smoke test ".repeat(3);
1335        let mut framed = vec![0u8; ZSTD_compressBound(src.len())];
1336        let n = ZSTD_compress(&mut framed, &src, 3);
1337        assert!(!ERR_isError(n));
1338        framed.truncate(n);
1339
1340        let mut dctx = ZSTD_DCtx::new();
1341        ZSTD_initDStream(&mut dctx);
1342        let mut out = vec![0u8; src.len() + 64];
1343        let mut in_pos = 0usize;
1344        let mut out_pos = 0usize;
1345        let _ = ZSTD_decompressStream_simpleArgs(
1346            &mut dctx,
1347            &mut out,
1348            &mut out_pos,
1349            &framed,
1350            &mut in_pos,
1351        );
1352        for _ in 0..8 {
1353            if out_pos >= src.len() {
1354                break;
1355            }
1356            let _ = ZSTD_decompressStream_simpleArgs(
1357                &mut dctx,
1358                &mut out,
1359                &mut out_pos,
1360                &[],
1361                &mut 0usize,
1362            );
1363        }
1364        assert_eq!(&out[..out_pos], &src[..]);
1365    }
1366
1367    #[test]
1368    fn DCtx_reset_parameters_only_rejects_mid_stream_but_combined_variant_always_accepts() {
1369        // Mirror of the compressor-side three-way gate. Pure
1370        // `reset_parameters` must reject mid-stream with `StageWrong`;
1371        // `reset_session_only` and `reset_session_and_parameters`
1372        // are always safe since they clear streaming state first.
1373        use crate::common::error::{ERR_getErrorCode, ERR_isError};
1374        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
1375
1376        let src = b"dctx-reset-stage-semantics ".repeat(3);
1377        let mut framed = vec![0u8; ZSTD_compressBound(src.len())];
1378        let n = ZSTD_compress(&mut framed, &src, 3);
1379        assert!(!ERR_isError(n));
1380        framed.truncate(n);
1381
1382        let mut dctx = ZSTD_DCtx::new();
1383        ZSTD_initDStream(&mut dctx);
1384        let mut out = vec![0u8; src.len() + 64];
1385        let mut in_pos = 0usize;
1386        let mut out_pos = 0usize;
1387        let _ = ZSTD_decompressStream(&mut dctx, &mut out, &mut out_pos, &framed, &mut in_pos);
1388
1389        // reset_parameters alone: rejected mid-stream.
1390        let rc = ZSTD_DCtx_reset(&mut dctx, ZSTD_DResetDirective::ZSTD_reset_parameters);
1391        assert!(ERR_isError(rc));
1392        assert_eq!(ERR_getErrorCode(rc), ErrorCode::StageWrong);
1393
1394        // session_only: always OK.
1395        assert_eq!(
1396            ZSTD_DCtx_reset(&mut dctx, ZSTD_DResetDirective::ZSTD_reset_session_only),
1397            0,
1398        );
1399        // Now back in init, reset_parameters succeeds.
1400        assert_eq!(
1401            ZSTD_DCtx_reset(&mut dctx, ZSTD_DResetDirective::ZSTD_reset_parameters),
1402            0,
1403        );
1404
1405        // session_and_parameters: always OK mid-stream.
1406        ZSTD_initDStream(&mut dctx);
1407        let mut out = vec![0u8; src.len() + 64];
1408        let mut in_pos = 0usize;
1409        let mut out_pos = 0usize;
1410        let _ = ZSTD_decompressStream(&mut dctx, &mut out, &mut out_pos, &framed, &mut in_pos);
1411        assert_eq!(
1412            ZSTD_DCtx_reset(
1413                &mut dctx,
1414                ZSTD_DResetDirective::ZSTD_reset_session_and_parameters,
1415            ),
1416            0,
1417        );
1418    }
1419
1420    #[test]
1421    fn DCtx_param_setters_reject_mid_stream_with_StageWrong() {
1422        // Upstream contract (zstd_decompress.c:1809, 1908): every
1423        // DCtx parameter setter rejects mid-stream with `StageWrong`.
1424        // Unlike the compressor, there's no authorized-subset — the
1425        // decoder has to see all params up-front since they affect
1426        // header parsing.
1427        use crate::common::error::{ERR_getErrorCode, ERR_isError};
1428        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
1429
1430        let src = b"dctx-param-stage-gate ".repeat(4);
1431        let mut framed = vec![0u8; ZSTD_compressBound(src.len())];
1432        let n = ZSTD_compress(&mut framed, &src, 3);
1433        assert!(!ERR_isError(n));
1434        framed.truncate(n);
1435
1436        // setParameter.
1437        {
1438            let mut dctx = ZSTD_DCtx::new();
1439            ZSTD_initDStream(&mut dctx);
1440            // Init stage: accepted.
1441            assert_eq!(
1442                ZSTD_DCtx_setParameter(
1443                    &mut dctx,
1444                    ZSTD_dParameter::ZSTD_d_format,
1445                    ZSTD_format_e::ZSTD_f_zstd1 as i32,
1446                ),
1447                0,
1448            );
1449            // Stage input.
1450            let mut out = vec![0u8; src.len() + 64];
1451            let mut in_pos = 0usize;
1452            let mut out_pos = 0usize;
1453            let _ = ZSTD_decompressStream(&mut dctx, &mut out, &mut out_pos, &framed, &mut in_pos);
1454            let rc = ZSTD_DCtx_setParameter(
1455                &mut dctx,
1456                ZSTD_dParameter::ZSTD_d_format,
1457                ZSTD_format_e::ZSTD_f_zstd1_magicless as i32,
1458            );
1459            assert!(ERR_isError(rc));
1460            assert_eq!(ERR_getErrorCode(rc), ErrorCode::StageWrong);
1461        }
1462        // setMaxWindowSize.
1463        {
1464            let mut dctx = ZSTD_DCtx::new();
1465            ZSTD_initDStream(&mut dctx);
1466            assert_eq!(ZSTD_DCtx_setMaxWindowSize(&mut dctx, 1 << 20), 0);
1467            let mut out = vec![0u8; src.len() + 64];
1468            let mut in_pos = 0usize;
1469            let mut out_pos = 0usize;
1470            let _ = ZSTD_decompressStream(&mut dctx, &mut out, &mut out_pos, &framed, &mut in_pos);
1471            let rc = ZSTD_DCtx_setMaxWindowSize(&mut dctx, 1 << 20);
1472            assert!(ERR_isError(rc));
1473            assert_eq!(ERR_getErrorCode(rc), ErrorCode::StageWrong);
1474        }
1475    }
1476
1477    #[test]
1478    fn DCtx_dict_family_setters_reject_mid_stream_with_StageWrong() {
1479        // Symmetric to the compressor-side gate. A caller who swaps
1480        // the dict mid-stream would decouple back-ref history from
1481        // bytes already buffered in `dctx.stream_in_buffer`,
1482        // producing a valid-looking but wrong decode.
1483        use crate::common::error::{ERR_getErrorCode, ERR_isError};
1484        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
1485
1486        let dict = b"dctx-dict-family-stage-gate ".repeat(3);
1487        let src = b"some payload bytes ".repeat(4);
1488        let mut framed = vec![0u8; ZSTD_compressBound(src.len())];
1489        let n = ZSTD_compress(&mut framed, &src, 3);
1490        assert!(!ERR_isError(n));
1491        framed.truncate(n);
1492
1493        // loadDictionary.
1494        {
1495            let mut dctx = ZSTD_DCtx::new();
1496            ZSTD_initDStream(&mut dctx);
1497            assert_eq!(ZSTD_DCtx_loadDictionary(&mut dctx, &dict), 0);
1498            // Stage input into the DCtx's stream buffer.
1499            let mut out = vec![0u8; src.len() + 64];
1500            let mut in_pos = 0usize;
1501            let mut out_pos = 0usize;
1502            let _ = ZSTD_decompressStream(&mut dctx, &mut out, &mut out_pos, &framed, &mut in_pos);
1503            let rc = ZSTD_DCtx_loadDictionary(&mut dctx, &dict);
1504            assert!(ERR_isError(rc));
1505            assert_eq!(ERR_getErrorCode(rc), ErrorCode::StageWrong);
1506        }
1507        // refPrefix.
1508        {
1509            let mut dctx = ZSTD_DCtx::new();
1510            ZSTD_initDStream(&mut dctx);
1511            assert_eq!(ZSTD_DCtx_refPrefix(&mut dctx, &dict), 0);
1512            let mut out = vec![0u8; src.len() + 64];
1513            let mut in_pos = 0usize;
1514            let mut out_pos = 0usize;
1515            let _ = ZSTD_decompressStream(&mut dctx, &mut out, &mut out_pos, &framed, &mut in_pos);
1516            let rc = ZSTD_DCtx_refPrefix(&mut dctx, &dict);
1517            assert!(ERR_isError(rc));
1518            assert_eq!(ERR_getErrorCode(rc), ErrorCode::StageWrong);
1519        }
1520    }
1521
1522    #[test]
1523    fn DCtx_getParameter_defaults_on_fresh_dctx_match_upstream() {
1524        // Upstream contract (zstd_decompress.c:244):
1525        //   - d_windowLogMax: ZSTD_WINDOWLOG_LIMIT_DEFAULT (27)
1526        //   - d_format:       ZSTD_f_zstd1 (magic-prefixed)
1527        // Pin the fresh-DCtx defaults — decoder-side mirror of the
1528        // compressor gate so a future `ZSTD_DCtx::default` refactor
1529        // can't silently shift the API contract.
1530        let dctx = ZSTD_DCtx::new();
1531        let mut v = 0i32;
1532        assert_eq!(
1533            ZSTD_DCtx_getParameter(&dctx, ZSTD_dParameter::ZSTD_d_windowLogMax, &mut v),
1534            0,
1535        );
1536        assert_eq!(v, ZSTD_WINDOWLOG_LIMIT_DEFAULT as i32);
1537
1538        assert_eq!(
1539            ZSTD_DCtx_getParameter(&dctx, ZSTD_dParameter::ZSTD_d_format, &mut v),
1540            0,
1541        );
1542        assert_eq!(v, ZSTD_format_e::ZSTD_f_zstd1 as i32);
1543    }
1544
1545    #[test]
1546    fn DCtx_reset_parameters_clears_every_dict_slot_via_clearDict_helper() {
1547        // After the refactor to route `reset(parameters)` through
1548        // `ZSTD_clearDict` + `ZSTD_DCtx_resetParameters`, the param
1549        // reset must wipe ALL dict-related slots — `stream_dict`,
1550        // `dictID`, `ddict_rep`, `dictUses` — not just the subset
1551        // the earlier field-by-field body reset covered.
1552        let mut dctx = ZSTD_DCtx::new();
1553        // Seed every dict-related slot.
1554        dctx.stream_dict = b"reset-wipe-test".to_vec();
1555        dctx.dictID = 0xCA_FE_BA_BE;
1556        dctx.ddict_rep = [7, 8, 9];
1557        dctx.dictUses = ZSTD_dictUses_e::ZSTD_use_indefinitely;
1558
1559        assert_eq!(
1560            ZSTD_DCtx_reset(&mut dctx, ZSTD_DResetDirective::ZSTD_reset_parameters),
1561            0,
1562        );
1563        assert!(dctx.stream_dict.is_empty());
1564        assert_eq!(dctx.dictID, 0);
1565        assert_eq!(dctx.ddict_rep, [0u32; 3]);
1566        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_dont_use);
1567    }
1568
1569    #[test]
1570    fn DCtx_loadDictionary_persists_across_decodes() {
1571        // Complement to the refPrefix one-shot test: loadDictionary
1572        // marks `use_indefinitely`, so the dict must stay attached
1573        // across multiple decompress calls. Prevents a future
1574        // refactor from accidentally widening the auto-clear to
1575        // also demote `use_indefinitely`.
1576        use crate::common::error::ERR_isError;
1577        use crate::common::xxhash::XXH64_state_t;
1578        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
1579        use crate::decompress::zstd_decompress_block::{
1580            ZSTD_buildDefaultSeqTables, ZSTD_decoder_entropy_rep,
1581        };
1582
1583        let src = b"loadDictionary-persists-across-decodes-payload ".repeat(2);
1584        let mut framed = vec![0u8; ZSTD_compressBound(src.len())];
1585        let n = ZSTD_compress(&mut framed, &src, 3);
1586        framed.truncate(n);
1587
1588        let mut dctx = ZSTD_DCtx::new();
1589        ZSTD_buildDefaultSeqTables(&mut dctx);
1590        let dict = b"persistent-dict-bytes".to_vec();
1591        assert_eq!(ZSTD_DCtx_loadDictionary(&mut dctx, &dict), 0);
1592        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_use_indefinitely);
1593
1594        let mut out = vec![0u8; src.len() + 64];
1595        let mut rep = ZSTD_decoder_entropy_rep::default();
1596        let mut xxh = XXH64_state_t::default();
1597        // First decode.
1598        let d1 = ZSTD_decompressDCtx(&mut dctx, &mut rep, &mut xxh, &mut out, &framed);
1599        assert!(!ERR_isError(d1));
1600        // Dict must survive.
1601        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_use_indefinitely);
1602        assert_eq!(dctx.stream_dict, dict);
1603
1604        // Second decode — still attached.
1605        let d2 = ZSTD_decompressDCtx(&mut dctx, &mut rep, &mut xxh, &mut out, &framed);
1606        assert!(!ERR_isError(d2));
1607        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_use_indefinitely);
1608        assert_eq!(dctx.stream_dict, dict);
1609    }
1610
1611    #[test]
1612    fn DCtx_refDDict_empty_content_clears_dict_state() {
1613        // Parallel to `loadDictionary(&[])`: a DDict with empty
1614        // content must wipe prior dict state on `refDDict`.
1615        // Matches upstream's `zstd_decompress.c:1783` pattern —
1616        // `clearDict` unconditionally, early-return on empty content.
1617        use crate::decompress::zstd_ddict::ZSTD_DDict;
1618        let mut dctx = ZSTD_DCtx::new();
1619        assert_eq!(ZSTD_DCtx_loadDictionary(&mut dctx, b"pre-existing"), 0);
1620        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_use_indefinitely);
1621
1622        // Ref an empty DDict (zero-length content).
1623        let empty_ddict = ZSTD_DDict {
1624            dictBuffer: Vec::new(),
1625            dictContent: core::ptr::null(),
1626            dictSize: 0,
1627            dictID: 0,
1628            entropyPresent: 0,
1629        };
1630        assert_eq!(ZSTD_DCtx_refDDict(&mut dctx, &empty_ddict), 0);
1631        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_dont_use);
1632        assert!(dctx.stream_dict.is_empty());
1633        assert_eq!(dctx.dictID, 0);
1634    }
1635
1636    #[test]
1637    fn DCtx_loadDictionary_empty_slice_clears_dict_state() {
1638        // Symmetric to compressor-side gate: empty-dict load acts
1639        // as `clearDict`. Matches upstream's `zstd_decompress.c:1710`
1640        // empty-dict pattern.
1641        let mut dctx = ZSTD_DCtx::new();
1642        assert_eq!(ZSTD_DCtx_loadDictionary(&mut dctx, b"real-dict"), 0);
1643        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_use_indefinitely);
1644        assert_eq!(dctx.stream_dict, b"real-dict");
1645
1646        // Empty reload clears.
1647        assert_eq!(ZSTD_DCtx_loadDictionary(&mut dctx, &[]), 0);
1648        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_dont_use);
1649        assert!(dctx.stream_dict.is_empty());
1650        assert_eq!(dctx.dictID, 0);
1651    }
1652
1653    #[test]
1654    fn DCtx_refPrefix_empty_slice_clears_dict_state() {
1655        // Symmetric to the compressor-side gate: `refPrefix(&[])`
1656        // must act as "clear" rather than silently leaving
1657        // `dictUses = use_once` with an empty stream_dict. Matches
1658        // upstream's `zstd_decompress.c:1725` pattern (clearDict
1659        // before install, install only if non-empty).
1660        let mut dctx = ZSTD_DCtx::new();
1661        assert_eq!(ZSTD_DCtx_refPrefix(&mut dctx, b"pre-existing"), 0);
1662        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_use_once);
1663        assert!(!dctx.stream_dict.is_empty());
1664
1665        // Re-bind with empty prefix → clears everything.
1666        assert_eq!(ZSTD_DCtx_refPrefix(&mut dctx, &[]), 0);
1667        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_dont_use);
1668        assert!(dctx.stream_dict.is_empty());
1669        assert_eq!(dctx.dictID, 0);
1670    }
1671
1672    #[test]
1673    fn decompressStream_refPrefix_auto_clears_after_one_frame() {
1674        // Streaming-path sibling of `DCtx_refPrefix_auto_clears_after_one_decode`.
1675        // A prefix bound via `ZSTD_DCtx_refPrefix` on a streaming dctx
1676        // must auto-clear after the first frame in the stream, matching
1677        // the upstream `use_once` contract. Without this, a second
1678        // frame on the same stream would silently re-apply the stale
1679        // prefix as back-ref history.
1680        use crate::common::error::ERR_isError;
1681        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
1682
1683        let src = b"streaming-refPrefix-auto-clear-payload ".repeat(2);
1684        let mut framed = vec![0u8; ZSTD_compressBound(src.len())];
1685        let n = ZSTD_compress(&mut framed, &src, 3);
1686        framed.truncate(n);
1687
1688        let mut dctx = ZSTD_DCtx::new();
1689        ZSTD_initDStream(&mut dctx);
1690        // Bind a prefix (use_once).
1691        let prefix = b"streaming-one-shot-prefix".to_vec();
1692        assert_eq!(ZSTD_DCtx_refPrefix(&mut dctx, &prefix), 0);
1693        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_use_once);
1694
1695        let mut out = vec![0u8; src.len() + 64];
1696        let mut in_pos = 0usize;
1697        let mut out_pos = 0usize;
1698        let _ = ZSTD_decompressStream(&mut dctx, &mut out, &mut out_pos, &framed, &mut in_pos);
1699        for _ in 0..8 {
1700            if out_pos >= src.len() {
1701                break;
1702            }
1703            let _ = ZSTD_decompressStream(&mut dctx, &mut out, &mut out_pos, &[], &mut 0usize);
1704        }
1705        let decoded_len = out_pos;
1706        assert!(!ERR_isError(decoded_len));
1707
1708        // After the first frame, both tracker and stream_dict are
1709        // back to the uninitialized state.
1710        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_dont_use);
1711        assert!(
1712            dctx.stream_dict.is_empty(),
1713            "streaming refPrefix dict persisted after one-shot decode",
1714        );
1715    }
1716
1717    #[test]
1718    fn DCtx_refPrefix_auto_clears_after_one_decode() {
1719        // Upstream contract: `ZSTD_DCtx_refPrefix` is a one-shot
1720        // binding. After the next `ZSTD_decompressDCtx` consumes it,
1721        // the dict must be cleared — a subsequent decode on the same
1722        // dctx should NOT see the prefix. Previously our port left
1723        // `stream_dict` set forever, diverging from upstream.
1724        use crate::common::error::ERR_isError;
1725        use crate::common::xxhash::XXH64_state_t;
1726        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
1727        use crate::decompress::zstd_decompress_block::{
1728            ZSTD_buildDefaultSeqTables, ZSTD_decoder_entropy_rep,
1729        };
1730
1731        // Build a plain (dict-less) frame — decoding it should work
1732        // regardless of the prefix state.
1733        let src = b"refPrefix-auto-clear-payload ".repeat(3);
1734        let mut framed = vec![0u8; ZSTD_compressBound(src.len())];
1735        let n = ZSTD_compress(&mut framed, &src, 3);
1736        framed.truncate(n);
1737
1738        let mut dctx = ZSTD_DCtx::new();
1739        ZSTD_buildDefaultSeqTables(&mut dctx);
1740        // Attach a prefix via refPrefix (use_once lifetime).
1741        let prefix = b"one-shot-prefix-bytes".to_vec();
1742        assert_eq!(ZSTD_DCtx_refPrefix(&mut dctx, &prefix), 0);
1743        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_use_once);
1744        assert_eq!(dctx.stream_dict, prefix);
1745
1746        // Run one decode — consumes the prefix.
1747        let mut out = vec![0u8; src.len() + 64];
1748        let mut rep = ZSTD_decoder_entropy_rep::default();
1749        let mut xxh = XXH64_state_t::default();
1750        let d = ZSTD_decompressDCtx(&mut dctx, &mut rep, &mut xxh, &mut out, &framed);
1751        assert!(!ERR_isError(d));
1752
1753        // After the decode, the prefix must be gone and the tracker
1754        // must be back to `ZSTD_dont_use`.
1755        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_dont_use);
1756        assert!(
1757            dctx.stream_dict.is_empty(),
1758            "refPrefix dict persisted after one-shot decode",
1759        );
1760    }
1761
1762    #[test]
1763    fn DCtx_dict_family_setters_populate_dictUses_lifecycle_tracker() {
1764        // Upstream (zstd_decompress.c:1703, 1728, 1786) tags the dict
1765        // lifecycle via `dctx.dictUses`:
1766        //   - loadDictionary / refDDict → ZSTD_use_indefinitely
1767        //   - refPrefix → ZSTD_use_once (auto-clear after next frame)
1768        // Our port's field + setter wiring must mirror this so the
1769        // future auto-clear logic can read the right disposition.
1770        use crate::decompress::zstd_ddict::ZSTD_createDDict;
1771        let dict_bytes = b"dictUses-lifecycle-tracker".to_vec();
1772
1773        // loadDictionary → use_indefinitely.
1774        let mut dctx = ZSTD_DCtx::new();
1775        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_dont_use);
1776        assert_eq!(ZSTD_DCtx_loadDictionary(&mut dctx, &dict_bytes), 0);
1777        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_use_indefinitely);
1778
1779        // clearDict → dont_use.
1780        ZSTD_clearDict(&mut dctx);
1781        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_dont_use);
1782
1783        // refPrefix → use_once (single-frame lifetime).
1784        let mut dctx = ZSTD_DCtx::new();
1785        assert_eq!(ZSTD_DCtx_refPrefix(&mut dctx, &dict_bytes), 0);
1786        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_use_once);
1787
1788        // refDDict → use_indefinitely.
1789        let mut dctx = ZSTD_DCtx::new();
1790        let ddict = ZSTD_createDDict(&dict_bytes).expect("ddict");
1791        assert_eq!(ZSTD_DCtx_refDDict(&mut dctx, &ddict), 0);
1792        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_use_indefinitely);
1793
1794        // DCtx_reset(parameters) wipes the tracker back to dont_use.
1795        ZSTD_DCtx_reset(&mut dctx, ZSTD_DResetDirective::ZSTD_reset_parameters);
1796        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_dont_use);
1797    }
1798
1799    #[test]
1800    fn initDStream_usingDDict_parses_magic_prefix_dict_entropy() {
1801        // Sibling of `initDStream_usingDict` gate: a DDict built from
1802        // a magic-prefix dict must surface its dictID + entropy
1803        // flags on the dctx through the `usingDDict` init path.
1804        use super::tests::build_minimal_zstd_dict;
1805        use crate::decompress::zstd_ddict::ZSTD_createDDict;
1806        let content = b"initDStream_usingDDict-magic-dict-content ".repeat(2);
1807        let dict = build_minimal_zstd_dict(0x12_34_56_78, &content);
1808        let ddict = ZSTD_createDDict(&dict).expect("ddict create");
1809
1810        let mut dctx = ZSTD_DCtx::new();
1811        let rc = ZSTD_initDStream_usingDDict(&mut dctx, &ddict);
1812        assert!(!crate::common::error::ERR_isError(rc));
1813        assert_eq!(dctx.dictID, 0x12_34_56_78);
1814        assert_eq!(dctx.litEntropy, 1);
1815        assert_eq!(dctx.fseEntropy, 1);
1816    }
1817
1818    #[test]
1819    fn initDStream_usingDict_parses_magic_prefix_dict_entropy() {
1820        // Upstream contract (zstd_decompress.c:1744): the streaming
1821        // init helper routes through `ZSTD_DCtx_loadDictionary`, so
1822        // a magic-prefix zstd-format dict has its entropy tables
1823        // parsed onto the dctx. Previously our port wrote
1824        // `stream_dict = dict` directly, bypassing the magic probe —
1825        // callers seeding a magicless stream with a full-format dict
1826        // would have entropy flags stuck at 0.
1827        use super::tests::build_minimal_zstd_dict;
1828        let content = b"initDStream_usingDict-magic-dict-content ".repeat(2);
1829        let dict = build_minimal_zstd_dict(0xAB_CD_EF_00, &content);
1830
1831        let mut dctx = ZSTD_DCtx::new();
1832        let rc = ZSTD_initDStream_usingDict(&mut dctx, &dict);
1833        assert!(!crate::common::error::ERR_isError(rc));
1834        // dictID was parsed from the magic prefix.
1835        assert_eq!(dctx.dictID, 0xAB_CD_EF_00);
1836        // Entropy flags flipped on — HUF + FSE tables loaded.
1837        assert_eq!(dctx.litEntropy, 1);
1838        assert_eq!(dctx.fseEntropy, 1);
1839    }
1840
1841    #[test]
1842    fn DCtx_setMaxWindowSize_handles_non_power_of_two_via_highbit() {
1843        // Upstream stores `maxWindowSize` bytes verbatim; our port
1844        // converts to log2 since we track `d_windowLogMax` instead.
1845        // The conversion must use ceiling-log2 semantics so a value
1846        // like `(1 << 20) + 1` rounds up to windowLog = 20 rather
1847        // than collapsing to the floor (`trailing_zeros` would give
1848        // 0 → clamped to 10). Previously the `trailing_zeros`
1849        // version silently dropped the window to the minimum for
1850        // any non-power-of-2 input.
1851        let mut dctx = ZSTD_DCtx::new();
1852        // Exact power of 2 → the log2 of the power.
1853        assert_eq!(ZSTD_DCtx_setMaxWindowSize(&mut dctx, 1 << 20), 0);
1854        assert_eq!(dctx.d_windowLogMax, 20);
1855
1856        // Non-power-of-2 just above 1<<20 → still reports 20 (the
1857        // highest set bit).
1858        let mut dctx = ZSTD_DCtx::new();
1859        assert_eq!(ZSTD_DCtx_setMaxWindowSize(&mut dctx, (1usize << 20) + 1), 0,);
1860        assert_eq!(dctx.d_windowLogMax, 20);
1861    }
1862
1863    #[test]
1864    fn internal_decoder_stage_enums_match_upstream() {
1865        // Upstream `zstd_decompress_internal.h:89-97`:
1866        //   ZSTD_dStreamStage: zdss_init=0, zdss_loadHeader=1,
1867        //     zdss_read=2, zdss_load=3, zdss_flush=4
1868        //   ZSTD_dStage: 0..=7 (getFrameHeaderSize → skipFrame)
1869        //   ZSTD_dictUses_e: use_indefinitely=-1, dont_use=0, use_once=1
1870        // These feed the DCtx state machine — `dctx_is_in_init_stage`
1871        // gate checks, streaming input-type hints, dict-lifetime
1872        // tracking all consume the raw values.
1873        assert_eq!(ZSTD_dStreamStage::zdss_init as i32, 0);
1874        assert_eq!(ZSTD_dStreamStage::zdss_loadHeader as i32, 1);
1875        assert_eq!(ZSTD_dStreamStage::zdss_read as i32, 2);
1876        assert_eq!(ZSTD_dStreamStage::zdss_load as i32, 3);
1877        assert_eq!(ZSTD_dStreamStage::zdss_flush as i32, 4);
1878        assert_eq!(ZSTD_dStage::ZSTDds_getFrameHeaderSize as i32, 0);
1879        assert_eq!(ZSTD_dStage::ZSTDds_decodeFrameHeader as i32, 1);
1880        assert_eq!(ZSTD_dStage::ZSTDds_decodeBlockHeader as i32, 2);
1881        assert_eq!(ZSTD_dStage::ZSTDds_decompressBlock as i32, 3);
1882        assert_eq!(ZSTD_dStage::ZSTDds_decompressLastBlock as i32, 4);
1883        assert_eq!(ZSTD_dStage::ZSTDds_checkChecksum as i32, 5);
1884        assert_eq!(ZSTD_dStage::ZSTDds_decodeSkippableHeader as i32, 6);
1885        assert_eq!(ZSTD_dStage::ZSTDds_skipFrame as i32, 7);
1886        assert_eq!(ZSTD_dictUses_e::ZSTD_use_indefinitely as i32, -1);
1887        assert_eq!(ZSTD_dictUses_e::ZSTD_dont_use as i32, 0);
1888        assert_eq!(ZSTD_dictUses_e::ZSTD_use_once as i32, 1);
1889    }
1890
1891    #[test]
1892    fn nextSrcSizeWithInputSize_and_isSkipFrame_match_stage_rules() {
1893        use crate::decompress::zstd_decompress_block::blockType_e;
1894
1895        let mut dctx = ZSTD_DCtx::new();
1896        dctx.expected = 64;
1897        dctx.stage = ZSTD_dStage::ZSTDds_decodeBlockHeader;
1898        assert_eq!(ZSTD_nextSrcSizeToDecompressWithInputSize(&dctx, 7), 64);
1899        assert_eq!(ZSTD_isSkipFrame(&dctx), 0);
1900
1901        dctx.stage = ZSTD_dStage::ZSTDds_decompressBlock;
1902        dctx.bType = blockType_e::bt_compressed;
1903        assert_eq!(ZSTD_nextSrcSizeToDecompressWithInputSize(&dctx, 7), 64);
1904
1905        dctx.bType = blockType_e::bt_raw;
1906        assert_eq!(ZSTD_nextSrcSizeToDecompressWithInputSize(&dctx, 7), 7);
1907        assert_eq!(ZSTD_nextSrcSizeToDecompressWithInputSize(&dctx, 128), 64);
1908
1909        dctx.stage = ZSTD_dStage::ZSTDds_skipFrame;
1910        assert_eq!(ZSTD_isSkipFrame(&dctx), 1);
1911    }
1912
1913    #[test]
1914    fn format_e_and_frameType_e_discriminants_match_upstream() {
1915        // Upstream wire-level values:
1916        //   ZSTD_f_zstd1 = 0, ZSTD_f_zstd1_magicless = 1 (zstd.h:1385)
1917        //   ZSTD_frame = 0, ZSTD_skippableFrame = 1     (zstd.h:1510)
1918        // These show up as `ZSTD_FrameHeader.frameType` fields and
1919        // `ZSTD_getFrameHeader_advanced` args — FFI callers pass the
1920        // raw integer values. Lock the discriminants.
1921        assert_eq!(ZSTD_format_e::ZSTD_f_zstd1 as i32, 0);
1922        assert_eq!(ZSTD_format_e::ZSTD_f_zstd1_magicless as i32, 1);
1923        assert_eq!(ZSTD_FrameType_e::ZSTD_frame as i32, 0);
1924        assert_eq!(ZSTD_FrameType_e::ZSTD_skippableFrame as i32, 1);
1925    }
1926
1927    #[test]
1928    fn DResetDirective_discriminants_match_upstream() {
1929        // Decoder-side alias of `ZSTD_ResetDirective` — same
1930        // upstream discriminants (zstd.h:589): 1/2/3.
1931        assert_eq!(ZSTD_DResetDirective::ZSTD_reset_session_only as i32, 1);
1932        assert_eq!(ZSTD_DResetDirective::ZSTD_reset_parameters as i32, 2);
1933        assert_eq!(
1934            ZSTD_DResetDirective::ZSTD_reset_session_and_parameters as i32,
1935            3,
1936        );
1937    }
1938
1939    #[test]
1940    fn dParameter_discriminants_match_upstream_zstd_h() {
1941        // Pin the `ZSTD_dParameter` C-ABI values. Mirror of the
1942        // compressor-side gate: drift here silently mis-routes FFI
1943        // callers.
1944        assert_eq!(ZSTD_dParameter::ZSTD_d_windowLogMax as i32, 100);
1945        // `ZSTD_d_format` = `ZSTD_d_experimentalParam1` = 1000.
1946        assert_eq!(ZSTD_dParameter::ZSTD_d_format as i32, 1000);
1947    }
1948
1949    #[test]
1950    fn DCtx_setParameter_d_format_round_trips_through_getParameter() {
1951        // Upstream exposes format as `ZSTD_d_format` — set via
1952        // `ZSTD_DCtx_setParameter(d_format, value)` and read back via
1953        // `ZSTD_DCtx_getParameter`. Our port now mirrors that path
1954        // so callers who use the parametric API (not just the direct
1955        // `ZSTD_DCtx_setFormat` helper) land on the same state.
1956        use crate::common::error::{ERR_getErrorCode, ERR_isError, ErrorCode};
1957        let mut dctx = ZSTD_DCtx::new();
1958        let mut value = 0i32;
1959
1960        // Default is zstd1.
1961        assert_eq!(
1962            ZSTD_DCtx_getParameter(&dctx, ZSTD_dParameter::ZSTD_d_format, &mut value),
1963            0,
1964        );
1965        assert_eq!(value, ZSTD_format_e::ZSTD_f_zstd1 as i32);
1966
1967        // Flip to magicless via the parametric setter.
1968        assert_eq!(
1969            ZSTD_DCtx_setParameter(
1970                &mut dctx,
1971                ZSTD_dParameter::ZSTD_d_format,
1972                ZSTD_format_e::ZSTD_f_zstd1_magicless as i32,
1973            ),
1974            0,
1975        );
1976        assert_eq!(dctx.format, ZSTD_format_e::ZSTD_f_zstd1_magicless);
1977
1978        // Getter reports it back.
1979        assert_eq!(
1980            ZSTD_DCtx_getParameter(&dctx, ZSTD_dParameter::ZSTD_d_format, &mut value),
1981            0,
1982        );
1983        assert_eq!(value, ZSTD_format_e::ZSTD_f_zstd1_magicless as i32);
1984
1985        // Out-of-bounds values must be rejected, not silently clamped.
1986        let rc = ZSTD_DCtx_setParameter(&mut dctx, ZSTD_dParameter::ZSTD_d_format, 42);
1987        assert!(ERR_isError(rc));
1988        assert_eq!(ERR_getErrorCode(rc), ErrorCode::ParameterOutOfBound);
1989    }
1990
1991    #[test]
1992    fn DCtx_reset_parameters_clears_magicless_format() {
1993        // Symmetric to the compressor-side gate: a dctx `reset(parameters)`
1994        // must restore the default zstd1 format. session_only must NOT
1995        // touch it — upstream keeps format as a param, not session state.
1996        let mut dctx = ZSTD_DCtx::new();
1997        ZSTD_DCtx_setFormat(&mut dctx, ZSTD_format_e::ZSTD_f_zstd1_magicless);
1998        assert_eq!(dctx.format, ZSTD_format_e::ZSTD_f_zstd1_magicless);
1999
2000        ZSTD_DCtx_reset(&mut dctx, ZSTD_DResetDirective::ZSTD_reset_session_only);
2001        assert_eq!(dctx.format, ZSTD_format_e::ZSTD_f_zstd1_magicless);
2002
2003        ZSTD_DCtx_reset(&mut dctx, ZSTD_DResetDirective::ZSTD_reset_parameters);
2004        assert_eq!(dctx.format, ZSTD_format_e::ZSTD_f_zstd1);
2005
2006        ZSTD_DCtx_setFormat(&mut dctx, ZSTD_format_e::ZSTD_f_zstd1_magicless);
2007        ZSTD_DCtx_reset(
2008            &mut dctx,
2009            ZSTD_DResetDirective::ZSTD_reset_session_and_parameters,
2010        );
2011        assert_eq!(dctx.format, ZSTD_format_e::ZSTD_f_zstd1);
2012    }
2013
2014    #[test]
2015    fn DCtx_setFormat_stashes_format_on_dctx() {
2016        // `ZSTD_DCtx_setFormat` now stores the format on the DCtx.
2017        // Contract: setter returns 0 and the field persists for
2018        // frame-header parsing (which reads `dctx.format` via
2019        // `ZSTD_startingInputLength`).
2020        use crate::common::error::ERR_isError;
2021        let mut dctx = ZSTD_DCtx::default();
2022        assert_eq!(dctx.format, ZSTD_format_e::ZSTD_f_zstd1);
2023        let rc = ZSTD_DCtx_setFormat(&mut dctx, ZSTD_format_e::ZSTD_f_zstd1_magicless);
2024        assert!(!ERR_isError(rc));
2025        assert_eq!(dctx.format, ZSTD_format_e::ZSTD_f_zstd1_magicless);
2026    }
2027
2028    #[test]
2029    fn initDStream_hint_honors_magicless_format() {
2030        // `ZSTD_initDStream` / `ZSTD_resetDStream` return
2031        // `ZSTD_startingInputLength(dctx.format)` — upstream's first-
2032        // read hint for a streaming decoder. Previously our port
2033        // hardcoded `ZSTD_f_zstd1` (= 5 bytes: 4-byte magic + 1-byte
2034        // FHD). A magicless-mode dctx should instead get 1 (FHD only).
2035        let mut dctx = ZSTD_DCtx::default();
2036        assert_eq!(ZSTD_initDStream(&mut dctx), 5);
2037        assert_eq!(ZSTD_resetDStream(&mut dctx), 5);
2038        dctx.format = ZSTD_format_e::ZSTD_f_zstd1_magicless;
2039        assert_eq!(ZSTD_initDStream(&mut dctx), 1);
2040        assert_eq!(ZSTD_resetDStream(&mut dctx), 1);
2041    }
2042
2043    #[test]
2044    fn decompress_usingDict_honors_magicless_format_on_dctx() {
2045        // Parity gate: when a caller sets `dctx.format = magicless`
2046        // on the dctx and passes a magicless frame + raw dict to
2047        // `ZSTD_decompress_usingDict`, the decode must succeed.
2048        // Before `ZSTD_decompressFrame_withOpStart` learned about
2049        // format, this path hardcoded zstd1 and a magicless frame
2050        // would be rejected by the header probe.
2051        use crate::common::error::ERR_isError;
2052        use crate::compress::zstd_compress::{
2053            ZSTD_CCtx_setFormat, ZSTD_createCCtx, ZSTD_endStream, ZSTD_initCStream_usingDict,
2054        };
2055
2056        let dict = b"usingDict-magicless-parity-dict-bytes ".repeat(4);
2057        let src = b"payload-referencing-usingDict-magicless-parity-dict-bytes ".repeat(6);
2058        let mut cctx = ZSTD_createCCtx().unwrap();
2059        assert_eq!(
2060            ZSTD_CCtx_setFormat(&mut cctx, ZSTD_format_e::ZSTD_f_zstd1_magicless),
2061            0,
2062        );
2063        ZSTD_initCStream_usingDict(&mut cctx, &dict, 3);
2064
2065        let mut compressed = vec![0u8; 4096];
2066        let mut cp = 0usize;
2067        let mut sp = 0usize;
2068        let _ = crate::compress::zstd_compress::ZSTD_compressStream(
2069            &mut cctx,
2070            &mut compressed,
2071            &mut cp,
2072            &src,
2073            &mut sp,
2074        );
2075        loop {
2076            let r = ZSTD_endStream(&mut cctx, &mut compressed, &mut cp);
2077            assert!(!ERR_isError(r));
2078            if r == 0 {
2079                break;
2080            }
2081        }
2082        compressed.truncate(cp);
2083
2084        let mut dctx = ZSTD_DCtx::new();
2085        assert_eq!(
2086            ZSTD_DCtx_setFormat(&mut dctx, ZSTD_format_e::ZSTD_f_zstd1_magicless),
2087            0,
2088        );
2089        let mut out = vec![0u8; src.len() + 128];
2090        let d = ZSTD_decompress_usingDict(&mut dctx, &mut out, &compressed, &dict);
2091        assert!(!ERR_isError(d), "decode err: {d:#x}");
2092        assert_eq!(&out[..d], &src[..]);
2093    }
2094
2095    #[test]
2096    fn decompressStream_honors_magicless_format() {
2097        // Streaming decoder parity gate: the streaming path measures
2098        // frame size via `findFrameCompressedSize` — previously that
2099        // call was hardcoded to the zstd1 format, so magicless-format
2100        // dctxs would reject valid magicless bytes as malformed. After
2101        // the fix the streaming path threads `dctx.format` through.
2102        use crate::common::error::ERR_isError;
2103        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
2104        let src = b"streaming-magicless-decode-parity-payload ".repeat(10);
2105        let mut framed = vec![0u8; ZSTD_compressBound(src.len())];
2106        let c_sz = ZSTD_compress(&mut framed, &src, 3);
2107        assert!(!ERR_isError(c_sz));
2108        framed.truncate(c_sz);
2109        let magicless = framed[4..].to_vec();
2110
2111        let mut dctx = ZSTD_DCtx::new();
2112        let _ = ZSTD_DCtx_setFormat(&mut dctx, ZSTD_format_e::ZSTD_f_zstd1_magicless);
2113        ZSTD_initDStream(&mut dctx);
2114
2115        let mut out = vec![0u8; src.len() + 64];
2116        let mut in_pos = 0usize;
2117        let mut out_pos = 0usize;
2118        let _hint =
2119            ZSTD_decompressStream(&mut dctx, &mut out, &mut out_pos, &magicless, &mut in_pos);
2120        for _ in 0..8 {
2121            if out_pos >= src.len() {
2122                break;
2123            }
2124            let _ = ZSTD_decompressStream(&mut dctx, &mut out, &mut out_pos, &[], &mut 0usize);
2125        }
2126        assert_eq!(&out[..out_pos], &src[..]);
2127    }
2128
2129    #[test]
2130    fn decompressFrame_honors_magicless_format() {
2131        // Real parity gate: compress a payload with upstream zstd1
2132        // format → strip the 4-byte magic → set dctx.format to
2133        // ZSTD_f_zstd1_magicless → decode. The decoder must accept
2134        // the magicless bytes and reconstruct the original payload.
2135        use crate::common::xxhash::XXH64_state_t;
2136        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
2137        use crate::decompress::zstd_decompress_block::{
2138            ZSTD_DCtx, ZSTD_buildDefaultSeqTables, ZSTD_decoder_entropy_rep,
2139        };
2140
2141        let src = b"Hello, magicless zstd world! 0123456789";
2142        let mut zstd_framed = vec![0u8; ZSTD_compressBound(src.len())];
2143        let c_sz = ZSTD_compress(&mut zstd_framed, src, 1);
2144        assert!(!crate::common::error::ERR_isError(c_sz));
2145        zstd_framed.truncate(c_sz);
2146
2147        // Strip the 4-byte magic to make it magicless.
2148        let magicless = &zstd_framed[4..];
2149
2150        let mut dctx = ZSTD_DCtx::new();
2151        ZSTD_buildDefaultSeqTables(&mut dctx);
2152        let rc = ZSTD_DCtx_setFormat(&mut dctx, ZSTD_format_e::ZSTD_f_zstd1_magicless);
2153        assert!(!crate::common::error::ERR_isError(rc));
2154
2155        let mut out = vec![0u8; src.len()];
2156        let mut rep = ZSTD_decoder_entropy_rep::default();
2157        let mut xxh = XXH64_state_t::default();
2158        let mut consumed = 0usize;
2159        let decoded = ZSTD_decompressFrame(
2160            &mut dctx,
2161            &mut rep,
2162            &mut xxh,
2163            &mut out,
2164            magicless,
2165            &mut consumed,
2166        );
2167        assert!(
2168            !crate::common::error::ERR_isError(decoded),
2169            "decompressFrame failed: {}",
2170            crate::common::error::ERR_getErrorName(decoded)
2171        );
2172        assert_eq!(decoded, src.len());
2173        assert_eq!(&out[..decoded], src);
2174        // Consumed must cover the magicless input exactly (no magic bytes).
2175        assert_eq!(consumed, magicless.len());
2176    }
2177
2178    #[test]
2179    fn decompressContinue_rejects_wrong_chunk_size() {
2180        let mut dctx = ZSTD_DCtx::default();
2181        let mut dst = [0u8; 64];
2182        let src = b"some-input";
2183        let rc = ZSTD_decompressContinue(&mut dctx, &mut dst, src);
2184        assert!(crate::common::error::ERR_isError(rc));
2185        use crate::common::error::ERR_getErrorCode;
2186        assert_eq!(ERR_getErrorCode(rc), ErrorCode::SrcSizeWrong);
2187    }
2188
2189    #[test]
2190    fn decompressContinue_roundtrips_single_raw_block_frame() {
2191        use crate::common::mem::MEM_writeLE24;
2192        use crate::compress::zstd_compress::{
2193            ZSTD_FrameParameters, ZSTD_writeFrameHeader, ZSTD_FRAMEHEADERSIZE_MAX,
2194        };
2195
2196        let payload = b"legacy continue raw block";
2197        let fparams = ZSTD_FrameParameters {
2198            contentSizeFlag: 1,
2199            checksumFlag: 0,
2200            noDictIDFlag: 1,
2201        };
2202        let mut header = [0u8; ZSTD_FRAMEHEADERSIZE_MAX];
2203        let hsize = ZSTD_writeFrameHeader(&mut header, &fparams, 17, payload.len() as u64, 0);
2204        assert!(!crate::common::error::ERR_isError(hsize));
2205
2206        let mut frame = Vec::with_capacity(hsize + 3 + payload.len());
2207        frame.extend_from_slice(&header[..hsize]);
2208        let blockHeader = 1u32 | ((payload.len() as u32) << 3);
2209        let mut bh = [0u8; 3];
2210        MEM_writeLE24(&mut bh, blockHeader);
2211        frame.extend_from_slice(&bh);
2212        frame.extend_from_slice(payload);
2213
2214        let mut dctx = ZSTD_DCtx::default();
2215        let mut out = vec![0u8; payload.len()];
2216        let mut ip = 0usize;
2217        let mut op = 0usize;
2218
2219        while ip < frame.len() {
2220            let chunk = ZSTD_nextSrcSizeToDecompress(&dctx);
2221            let produced =
2222                ZSTD_decompressContinue(&mut dctx, &mut out[op..], &frame[ip..ip + chunk]);
2223            assert!(
2224                !crate::common::error::ERR_isError(produced),
2225                "decompressContinue failed at ip={ip}: {}",
2226                crate::common::error::ERR_getErrorName(produced)
2227            );
2228            ip += chunk;
2229            op += produced;
2230        }
2231
2232        assert_eq!(op, payload.len());
2233        assert_eq!(&out[..op], payload);
2234        assert_eq!(
2235            ZSTD_nextSrcSizeToDecompress(&dctx),
2236            ZSTD_startingInputLength(ZSTD_format_e::ZSTD_f_zstd1)
2237        );
2238    }
2239
2240    #[test]
2241    fn decompressContinue_multi_block_with_independent_dst_buffers() {
2242        // Build a multi-block compressed frame whose later blocks
2243        // back-reference earlier blocks. Drive it through
2244        // `ZSTD_decompressContinue` while handing each block a *fresh*
2245        // `dst` slice — the prior block's bytes do not live in
2246        // `dst[..op_start]`, so the only way back-references can
2247        // resolve is via the rolling history buffer in `dctx`.
2248        //
2249        // This is the regression gate for the dctx.historyBuffer +
2250        // ext-dict wiring in `ZSTD_execSequence`.
2251        use crate::compress::zstd_compress::ZSTD_compress;
2252
2253        // Strong cross-block repetition: a 64 KB unique preamble
2254        // followed by 8 copies of itself, total ~576 KB. At standard
2255        // block size 128 KB, blocks 2..=4 will reference into earlier
2256        // blocks' bytes. Every block boundary will see at least one
2257        // long back-reference cross it.
2258        let chunk: Vec<u8> = (0..65_536u32)
2259            .map(|i| ((i * 17 + 3) & 0xFF) as u8)
2260            .collect();
2261        let mut payload = chunk.clone();
2262        for _ in 0..8 {
2263            payload.extend_from_slice(&chunk);
2264        }
2265
2266        let mut compressed = vec![0u8; payload.len() + 1024];
2267        let n = ZSTD_compress(&mut compressed, &payload, 1);
2268        assert!(
2269            !crate::common::error::ERR_isError(n),
2270            "compress failed: {}",
2271            crate::common::error::ERR_getErrorName(n)
2272        );
2273        compressed.truncate(n);
2274
2275        // Drive ZSTD_decompressContinue chunk-by-chunk; for each
2276        // decompressBlock stage, allocate a brand-new Vec for the
2277        // block output so prior-block bytes are NOT visible to the
2278        // sequence executor through `dst`. Concatenate the per-call
2279        // outputs to verify the full frame.
2280        let mut dctx = ZSTD_DCtx::default();
2281        let mut ip = 0usize;
2282        let mut decoded: Vec<u8> = Vec::with_capacity(payload.len());
2283
2284        while ip < compressed.len() {
2285            let chunk = ZSTD_nextSrcSizeToDecompress(&dctx);
2286            if chunk == 0 {
2287                break;
2288            }
2289            // Allocate a fresh buffer big enough for any block.
2290            let block_max = dctx
2291                .fParams
2292                .blockSizeMax
2293                .max(crate::decompress::zstd_decompress_block::ZSTD_BLOCKSIZE_MAX as u32)
2294                as usize;
2295            let mut block_dst = vec![0u8; block_max + 64];
2296            let produced =
2297                ZSTD_decompressContinue(&mut dctx, &mut block_dst, &compressed[ip..ip + chunk]);
2298            assert!(
2299                !crate::common::error::ERR_isError(produced),
2300                "decompressContinue at ip={ip}, chunk={chunk}: {}",
2301                crate::common::error::ERR_getErrorName(produced)
2302            );
2303            decoded.extend_from_slice(&block_dst[..produced]);
2304            ip += chunk;
2305        }
2306
2307        assert_eq!(decoded.len(), payload.len(), "decoded length mismatch");
2308        assert_eq!(decoded, payload, "decoded bytes differ from payload");
2309    }
2310
2311    #[test]
2312    fn stream_workspace_helpers_track_overflow_and_continue_wrapper() {
2313        use crate::common::mem::MEM_writeLE24;
2314        use crate::compress::zstd_compress::{
2315            ZSTD_FrameParameters, ZSTD_writeFrameHeader, ZSTD_FRAMEHEADERSIZE_MAX,
2316        };
2317
2318        let mut dctx = ZSTD_DCtx::default();
2319        dctx.stream_in_buffer.reserve(128);
2320        dctx.stream_out_buffer.reserve(128);
2321        assert_eq!(ZSTD_DCtx_isOverflow(&dctx, 16, 16), 1);
2322        ZSTD_DCtx_updateOversizedDuration(&mut dctx, 16, 16);
2323        assert_eq!(dctx.oversizedDuration, 1);
2324        ZSTD_DCtx_updateOversizedDuration(&mut dctx, 1024, 1024);
2325        assert_eq!(dctx.oversizedDuration, 0);
2326        assert_eq!(ZSTD_checkOutBuffer(&dctx, &[], 0), 0);
2327
2328        let payload = b"continue-stream wrapper";
2329        let fparams = ZSTD_FrameParameters {
2330            contentSizeFlag: 1,
2331            checksumFlag: 0,
2332            noDictIDFlag: 1,
2333        };
2334        let mut header = [0u8; ZSTD_FRAMEHEADERSIZE_MAX];
2335        let hsize = ZSTD_writeFrameHeader(&mut header, &fparams, 17, payload.len() as u64, 0);
2336        assert!(!crate::common::error::ERR_isError(hsize));
2337
2338        let mut frame = Vec::new();
2339        frame.extend_from_slice(&header[..hsize]);
2340        let blockHeader = 1u32 | ((payload.len() as u32) << 3);
2341        let mut bh = [0u8; 3];
2342        MEM_writeLE24(&mut bh, blockHeader);
2343        frame.extend_from_slice(&bh);
2344        frame.extend_from_slice(payload);
2345
2346        let mut zds = ZSTD_DCtx::default();
2347        let mut out = vec![0u8; payload.len()];
2348        let mut ip = 0usize;
2349        let mut op = 0usize;
2350        while ip < frame.len() {
2351            let chunk = ZSTD_nextSrcSizeToDecompress(&zds);
2352            let rc =
2353                ZSTD_decompressContinueStream(&mut zds, &mut out, &mut op, &frame[ip..ip + chunk]);
2354            assert!(
2355                !crate::common::error::ERR_isError(rc),
2356                "continue stream failed at ip={ip}: {}",
2357                crate::common::error::ERR_getErrorName(rc)
2358            );
2359            ip += chunk;
2360        }
2361        assert_eq!(&out[..op], payload);
2362    }
2363
2364    #[test]
2365    fn decodeFrameHeader_sets_checksum_and_rejects_wrong_dict() {
2366        use crate::common::error::ERR_getErrorCode;
2367        use crate::compress::zstd_compress::{
2368            ZSTD_FrameParameters, ZSTD_writeFrameHeader, ZSTD_FRAMEHEADERSIZE_MAX,
2369        };
2370
2371        let fparams = ZSTD_FrameParameters {
2372            contentSizeFlag: 1,
2373            checksumFlag: 1,
2374            noDictIDFlag: 0,
2375        };
2376        let mut header = [0u8; ZSTD_FRAMEHEADERSIZE_MAX];
2377        let hsize = ZSTD_writeFrameHeader(&mut header, &fparams, 17, 11, 0x1234);
2378        assert!(!crate::common::error::ERR_isError(hsize));
2379
2380        let mut dctx = ZSTD_DCtx::default();
2381        dctx.dictID = 0x1234;
2382        let rc = ZSTD_decodeFrameHeader(&mut dctx, &header[..hsize], hsize);
2383        assert_eq!(rc, 0);
2384        assert_eq!(dctx.fParams.dictID, 0x1234);
2385        assert_eq!(dctx.validateChecksum, 1);
2386        assert_eq!(dctx.processedCSize, hsize as u64);
2387
2388        let mut wrong = ZSTD_DCtx::default();
2389        wrong.dictID = 0x5678;
2390        let rc = ZSTD_decodeFrameHeader(&mut wrong, &header[..hsize], hsize);
2391        assert!(crate::common::error::ERR_isError(rc));
2392        assert_eq!(ERR_getErrorCode(rc), ErrorCode::DictionaryWrong);
2393    }
2394
2395    #[test]
2396    fn getDDict_and_refDictContent_follow_upstream_lifecycle() {
2397        let mut dctx = ZSTD_DCtx::default();
2398        let dict = b"raw dictionary bytes";
2399
2400        assert_eq!(ZSTD_refDictContent(&mut dctx, dict), 0);
2401        dctx.dictUses = ZSTD_dictUses_e::ZSTD_use_once;
2402        assert_eq!(dctx.prefixStart, Some(dict.as_ptr() as usize));
2403        assert_eq!(
2404            dctx.previousDstEnd,
2405            Some(dict.as_ptr() as usize + dict.len())
2406        );
2407
2408        let used = ZSTD_getDDict(&mut dctx).expect("one-shot dict");
2409        assert_eq!(used, dict);
2410        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_dont_use);
2411        assert!(dctx.stream_dict.is_empty());
2412        assert!(ZSTD_getDDict(&mut dctx).is_none());
2413    }
2414
2415    #[test]
2416    fn ddict_hashset_inserts_replaces_and_expands() {
2417        use crate::compress::zstd_compress::ZSTD_customMem;
2418        use crate::decompress::zstd_ddict::ZSTD_DDict;
2419
2420        fn make_ddict(dictID: u32, content: &[u8]) -> ZSTD_DDict {
2421            let dictBuffer = content.to_vec();
2422            let dictContent = dictBuffer.as_ptr();
2423            ZSTD_DDict {
2424                dictBuffer,
2425                dictContent,
2426                dictSize: content.len(),
2427                dictID,
2428                entropyPresent: 0,
2429            }
2430        }
2431
2432        let first = make_ddict(7, b"first");
2433        let replacement = make_ddict(7, b"replacement");
2434        let mut set = ZSTD_createDDictHashSet(ZSTD_customMem::default());
2435        assert_eq!(
2436            ZSTD_DDictHashSet_addDDict(&mut set, &first, ZSTD_customMem::default()),
2437            0
2438        );
2439        assert_eq!(set.ddictPtrCount, 1);
2440        assert!(core::ptr::eq(
2441            ZSTD_DDictHashSet_getDDict(&set, 7).expect("first"),
2442            &first
2443        ));
2444
2445        assert_eq!(
2446            ZSTD_DDictHashSet_addDDict(&mut set, &replacement, ZSTD_customMem::default()),
2447            0
2448        );
2449        assert_eq!(set.ddictPtrCount, 1);
2450        assert!(core::ptr::eq(
2451            ZSTD_DDictHashSet_getDDict(&set, 7).expect("replacement"),
2452            &replacement
2453        ));
2454
2455        let many: Vec<Box<ZSTD_DDict>> = (100..121)
2456            .map(|id| Box::new(make_ddict(id, &[id as u8; 3])))
2457            .collect();
2458        for ddict in &many {
2459            assert_eq!(
2460                ZSTD_DDictHashSet_addDDict(&mut set, ddict.as_ref(), ZSTD_customMem::default()),
2461                0
2462            );
2463        }
2464        assert!(set.ddictPtrTableSize > DDICT_HASHSET_TABLE_BASE_SIZE);
2465        for ddict in &many {
2466            let found = ZSTD_DDictHashSet_getDDict(&set, ddict.dictID).expect("present");
2467            assert!(core::ptr::eq(found, ddict.as_ref()));
2468        }
2469        assert!(ZSTD_DDictHashSet_getDDict(&set, 0xFFFF).is_none());
2470
2471        let mut dctx = ZSTD_DCtx::default();
2472        dctx.fParams.dictID = 117;
2473        let selected = ZSTD_DCtx_selectFrameDDict(&mut dctx, &set).expect("selected");
2474        assert_eq!(selected.dictID, 117);
2475        assert_eq!(dctx.dictID, 117);
2476        assert_eq!(dctx.dictUses, ZSTD_dictUses_e::ZSTD_use_indefinitely);
2477        assert_eq!(dctx.stream_dict, [117u8; 3]);
2478    }
2479
2480    #[test]
2481    fn decoder_rejects_corrupted_xxh64_trailer_with_checksumWrong() {
2482        // Compress with `--check` flag, flip one byte of the XXH64
2483        // trailer, and verify the decoder surfaces ChecksumWrong —
2484        // NOT a silent decode-and-pass-bad-bytes-up-the-stack.
2485        use crate::compress::match_state::ZSTD_compressionParameters;
2486        use crate::compress::zstd_compress::{
2487            ZSTD_FrameParameters, ZSTD_compressBound, ZSTD_compressFrame_fast, ZSTD_getCParams,
2488        };
2489
2490        let src: Vec<u8> = b"payload with xxh64 trailer ".repeat(40);
2491        let bound = ZSTD_compressBound(src.len());
2492        let mut dst = vec![0u8; bound];
2493        let cp: ZSTD_compressionParameters = ZSTD_getCParams(3, src.len() as u64, 0);
2494        let fp = ZSTD_FrameParameters {
2495            contentSizeFlag: 1,
2496            checksumFlag: 1,
2497            noDictIDFlag: 1,
2498        };
2499        let n = ZSTD_compressFrame_fast(&mut dst, &src, cp, fp);
2500        assert!(!crate::common::error::ERR_isError(n));
2501        dst.truncate(n);
2502
2503        // Flip the last byte — the low 8 bits of the XXH64 trailer.
2504        let last = dst.len() - 1;
2505        dst[last] ^= 0x01;
2506
2507        let mut out = vec![0u8; src.len() + 64];
2508        let rc = ZSTD_decompress(&mut out, &dst);
2509        assert!(
2510            crate::common::error::ERR_isError(rc),
2511            "decoder missed corrupted checksum (rc={rc})"
2512        );
2513        assert_eq!(
2514            crate::common::error::ERR_getErrorCode(rc),
2515            ErrorCode::ChecksumWrong,
2516            "expected ChecksumWrong, got {:?}",
2517            crate::common::error::ERR_getErrorCode(rc)
2518        );
2519    }
2520
2521    #[test]
2522    fn resetDStream_clears_streaming_state_and_returns_next_hint() {
2523        // Contract:
2524        //   - clears stream_in_buffer, stream_out_buffer, drain cursor
2525        //   - returns `ZSTD_startingInputLength(format)` — the number
2526        //     of bytes needed to query the next frame header
2527        //     (5 for regular zstd1, 1 for magicless)
2528        // Preserves stream_dict (that's session-level state on the DCtx).
2529        use crate::decompress::zstd_decompress_block::ZSTD_DCtx;
2530        let mut dctx = ZSTD_DCtx::new();
2531        dctx.stream_in_buffer.extend_from_slice(b"pending-in");
2532        dctx.stream_out_buffer.extend_from_slice(b"pending-out");
2533        dctx.stream_out_drained = 4;
2534        dctx.stream_dict = b"sticky-dict".to_vec();
2535
2536        let hint = ZSTD_resetDStream(&mut dctx);
2537        assert_eq!(hint, ZSTD_startingInputLength(ZSTD_format_e::ZSTD_f_zstd1));
2538        assert_eq!(hint, 5);
2539        assert!(dctx.stream_in_buffer.is_empty());
2540        assert!(dctx.stream_out_buffer.is_empty());
2541        assert_eq!(dctx.stream_out_drained, 0);
2542        // Dict survives the reset.
2543        assert_eq!(dctx.stream_dict, b"sticky-dict");
2544    }
2545
2546    #[test]
2547    fn multi_frame_roundtrip_across_3_frames_with_interleaved_skippables() {
2548        // Three regular frames with distinct payloads + two
2549        // skippable frames at start and middle. All five frames
2550        // must decode in order, skippable frames contributing no
2551        // output bytes.
2552        use crate::compress::zstd_compress::{
2553            ZSTD_compress, ZSTD_compressBound, ZSTD_writeSkippableFrame,
2554        };
2555
2556        let payloads: [&[u8]; 3] = [
2557            b"payload-alpha ",
2558            b"payload-beta-is-a-bit-longer ",
2559            b"payload-gamma! ",
2560        ];
2561
2562        let mut combined = Vec::new();
2563        let mut expected = Vec::new();
2564        // Skippable at start (meta).
2565        let mut skip = vec![0u8; 32];
2566        let n = ZSTD_writeSkippableFrame(&mut skip, b"leading", 0);
2567        combined.extend_from_slice(&skip[..n]);
2568        // First regular frame.
2569        for (i, payload) in payloads.iter().enumerate() {
2570            let bound = ZSTD_compressBound(payload.len());
2571            let mut c = vec![0u8; bound];
2572            let n = ZSTD_compress(&mut c, payload, 1);
2573            assert!(!crate::common::error::ERR_isError(n));
2574            combined.extend_from_slice(&c[..n]);
2575            expected.extend_from_slice(payload);
2576            // Interleave a skippable after frame 1 (between 1 and 2).
2577            if i == 0 {
2578                let mut skip2 = vec![0u8; 24];
2579                let n2 = ZSTD_writeSkippableFrame(&mut skip2, b"mid", 7);
2580                combined.extend_from_slice(&skip2[..n2]);
2581            }
2582        }
2583
2584        let mut out = vec![0u8; expected.len() + 64];
2585        let d = ZSTD_decompress(&mut out, &combined);
2586        assert!(!crate::common::error::ERR_isError(d));
2587        assert_eq!(d, expected.len());
2588        assert_eq!(&out[..d], &expected[..]);
2589    }
2590
2591    #[test]
2592    fn zstd_decompress_loops_over_concatenated_frames() {
2593        // Upstream contract: `ZSTD_decompress` walks multiple frames
2594        // in `src`, appending each payload into `dst`. Skippable
2595        // frames are silently advanced past without consuming dst
2596        // space.
2597        use crate::compress::zstd_compress::{
2598            ZSTD_compress, ZSTD_compressBound, ZSTD_writeSkippableFrame,
2599        };
2600        let src = b"concat probe content ".to_vec();
2601        let bound = ZSTD_compressBound(src.len());
2602        let mut frame = vec![0u8; bound];
2603        let n = ZSTD_compress(&mut frame, &src, 1);
2604        frame.truncate(n);
2605
2606        // Layout: frame || skippable || frame — confirms both
2607        // skippable-passthrough and per-frame output accumulation.
2608        let mut combined = frame.clone();
2609        let mut skip = vec![0u8; 16];
2610        let skip_n = ZSTD_writeSkippableFrame(&mut skip, b"meta", 1);
2611        combined.extend_from_slice(&skip[..skip_n]);
2612        combined.extend_from_slice(&frame);
2613
2614        let mut out = vec![0u8; src.len() * 2 + 64];
2615        let d = ZSTD_decompress(&mut out, &combined);
2616        assert!(!crate::common::error::ERR_isError(d));
2617        assert_eq!(d, src.len() * 2);
2618        assert_eq!(&out[..src.len()], &src[..]);
2619        assert_eq!(&out[src.len()..src.len() * 2], &src[..]);
2620    }
2621
2622    #[test]
2623    fn decompress_rejects_truncated_frame_body_with_error() {
2624        // Compress a payload, chop off some bytes from the MIDDLE of
2625        // the compressed stream (leaving header intact), and verify
2626        // the decoder surfaces an error rather than succeeding with
2627        // partial / corrupted output.
2628        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
2629        let src: Vec<u8> = b"truncation probe content ".repeat(30);
2630        let bound = ZSTD_compressBound(src.len());
2631        let mut compressed = vec![0u8; bound];
2632        let n = ZSTD_compress(&mut compressed, &src, 3);
2633        assert!(!crate::common::error::ERR_isError(n));
2634
2635        // Truncate to 70% — well past the header but before
2636        // completing the block body.
2637        let truncated_len = n * 7 / 10;
2638        assert!(truncated_len > 10 && truncated_len < n);
2639        let truncated = &compressed[..truncated_len];
2640
2641        let mut out = vec![0u8; src.len() + 64];
2642        let rc = ZSTD_decompress(&mut out, truncated);
2643        assert!(
2644            crate::common::error::ERR_isError(rc),
2645            "decoder accepted truncated input (rc={rc}) — must reject",
2646        );
2647    }
2648
2649    #[test]
2650    fn two_independent_dctxs_decode_same_frame_to_same_output() {
2651        // Isolation contract mirror of the CCtx-side test. Two DCtxes
2652        // decoding the same frame (with different dicts loaded) must
2653        // produce identical output for the frame — per-DCtx state
2654        // (stream_dict / windowLogMax / entropy tables) must not
2655        // leak between DCtx instances.
2656        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
2657        let src: Vec<u8> = b"DCtx-isolation test payload. ".repeat(30);
2658        let bound = ZSTD_compressBound(src.len());
2659        let mut compressed = vec![0u8; bound];
2660        let n = ZSTD_compress(&mut compressed, &src, 3);
2661        assert!(!crate::common::error::ERR_isError(n));
2662        compressed.truncate(n);
2663
2664        // DCtx A with one dict loaded; DCtx B with a different dict.
2665        // Neither dict affects the non-dict frame we compressed above.
2666        let mut a = ZSTD_DCtx::new();
2667        let mut b = ZSTD_DCtx::new();
2668        ZSTD_DCtx_loadDictionary(&mut a, b"some-dict-A");
2669        ZSTD_DCtx_loadDictionary(&mut b, b"other-dict-B");
2670
2671        let mut out_a = vec![0u8; src.len() + 64];
2672        let d_a = ZSTD_decompress(&mut out_a, &compressed);
2673        assert_eq!(&out_a[..d_a], &src[..]);
2674
2675        let mut out_b = vec![0u8; src.len() + 64];
2676        let d_b = ZSTD_decompress(&mut out_b, &compressed);
2677        assert_eq!(&out_b[..d_b], &src[..]);
2678
2679        // Neither DCtx should have its stream_dict disturbed.
2680        assert_eq!(a.stream_dict, b"some-dict-A");
2681        assert_eq!(b.stream_dict, b"other-dict-B");
2682    }
2683
2684    #[test]
2685    fn zstd_decompress_rejects_too_small_dst_buffer() {
2686        // Symmetric with the compress-side too-small-dst test.
2687        // `ZSTD_decompress` must return a ZSTD_isError when the dst
2688        // can't hold the decompressed output, not panic on OOB writes.
2689        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
2690        let src: Vec<u8> = b"payload that won't fit in a tiny dst. ".repeat(20);
2691        let bound = ZSTD_compressBound(src.len());
2692        let mut compressed = vec![0u8; bound];
2693        let n = ZSTD_compress(&mut compressed, &src, 1);
2694        assert!(!crate::common::error::ERR_isError(n));
2695
2696        // dst far smaller than the real decompressed size.
2697        let mut tiny_dst = [0u8; 16];
2698        let rc = ZSTD_decompress(&mut tiny_dst, &compressed[..n]);
2699        assert!(crate::common::error::ERR_isError(rc));
2700    }
2701
2702    #[test]
2703    fn zstd_decompress_rejects_garbage_without_panicking() {
2704        // Safety gate: feeding arbitrary bytes into `ZSTD_decompress`
2705        // must surface a ZSTD_isError return — never panic. This is
2706        // the contract callers rely on when accepting compressed
2707        // input from the network / disk.
2708        let mut dst = vec![0u8; 1024];
2709        // Empty src is NOT garbage — it's a valid zero-frame stream
2710        // returning 0 bytes (matches upstream). The garbage inputs
2711        // below are all malformed and MUST surface an error.
2712        let test_inputs: Vec<Vec<u8>> = vec![
2713            vec![0u8],       // 1 byte (below magic)
2714            vec![0u8; 3],    // below magic size
2715            vec![0xFFu8; 8], // bogus magic
2716            {
2717                // Valid magic but truncated mid-FHD.
2718                let mut v = ZSTD_MAGICNUMBER.to_le_bytes().to_vec();
2719                v.push(0x20);
2720                v
2721            },
2722            {
2723                // Valid magic but FHD reserved bit set — must reject.
2724                let mut v = ZSTD_MAGICNUMBER.to_le_bytes().to_vec();
2725                v.extend_from_slice(&[0x28, 100]); // reserved bit 3 set
2726                v
2727            },
2728            (0..200u8).map(|i| i.wrapping_mul(17)).collect(), // pseudo-random
2729        ];
2730        // And confirm empty input returns 0 bytes cleanly (not an error).
2731        let empty_rc = ZSTD_decompress(&mut dst, &[]);
2732        assert!(!crate::common::error::ERR_isError(empty_rc));
2733        assert_eq!(empty_rc, 0);
2734        for (i, input) in test_inputs.iter().enumerate() {
2735            let rc = ZSTD_decompress(&mut dst, input);
2736            assert!(
2737                crate::common::error::ERR_isError(rc),
2738                "input #{i} (len={}) should have errored but returned {rc}",
2739                input.len(),
2740            );
2741        }
2742    }
2743
2744    #[test]
2745    fn startingInputLength_differs_by_format() {
2746        // zstd1: 4-byte magic + 1-byte FHD = 5.
2747        // magicless: just the 1-byte FHD = 1.
2748        assert_eq!(ZSTD_startingInputLength(ZSTD_format_e::ZSTD_f_zstd1), 5);
2749        assert_eq!(
2750            ZSTD_startingInputLength(ZSTD_format_e::ZSTD_f_zstd1_magicless),
2751            1
2752        );
2753    }
2754
2755    #[test]
2756    fn getFrameHeader_advanced_magicless_skips_magic_check() {
2757        // In magicless mode the FHD is at offset 0 (no 4-byte magic).
2758        // Build a magicless frame: FHD=0x20 (singleSegment=1, fcsID=0)
2759        // + 1-byte FCS. Parser must accept it without looking for the
2760        // zstd magic number.
2761        let src = [0x20u8, 42];
2762        let mut zfh = ZSTD_FrameHeader::default();
2763        let rc =
2764            ZSTD_getFrameHeader_advanced(&mut zfh, &src, ZSTD_format_e::ZSTD_f_zstd1_magicless);
2765        assert_eq!(rc, 0);
2766        assert_eq!(zfh.frameContentSize, 42);
2767        // Header size in magicless mode drops by 4 (no magic).
2768        assert_eq!(zfh.headerSize, 2);
2769    }
2770
2771    #[test]
2772    fn frameHeaderSize_rejects_too_short_input() {
2773        // `ZSTD_frameHeaderSize` needs at least magic (4) + FHD (1)
2774        // bytes to read the FHD. A shorter input must return a
2775        // ZSTD_isError (specifically SrcSizeWrong), not index OOB.
2776        assert!(crate::common::error::ERR_isError(ZSTD_frameHeaderSize(&[])));
2777        assert!(crate::common::error::ERR_isError(ZSTD_frameHeaderSize(&[
2778            0xFDu8
2779        ])));
2780        assert!(crate::common::error::ERR_isError(ZSTD_frameHeaderSize(&[
2781            0u8, 0, 0, 0
2782        ])));
2783    }
2784
2785    #[test]
2786    fn frameHeaderSize_returns_exact_size_for_each_layout() {
2787        // The layout-specific frame header sizes are:
2788        //   - singleSegment=1, fcsID=0 → 6 bytes (magic + FHD + 1 FCS)
2789        //   - singleSegment=0, fcsID=0 → 6 bytes (magic + FHD + 1 wlByte + 0 FCS)
2790        //   - singleSegment=1, fcsID=1 → 7 bytes (magic + FHD + 2 FCS)
2791        //   - singleSegment=1, fcsID=2 → 9 bytes (magic + FHD + 4 FCS)
2792        //   - singleSegment=1, fcsID=3 → 13 bytes (magic + FHD + 8 FCS)
2793        // All numbers match what frame_header_fcs_size_variants observed
2794        // via the full parser; this test pins the stand-alone helper.
2795        let magic = ZSTD_MAGICNUMBER.to_le_bytes();
2796
2797        // fcsID=0, singleSegment=1 → 6 bytes.
2798        let mut src = magic.to_vec();
2799        src.push(0x20); // FHD = singleSegment=1, fcsID=0
2800        src.push(42); // FCS byte
2801        assert_eq!(ZSTD_frameHeaderSize(&src), 6);
2802
2803        // fcsID=0, singleSegment=0 → 6 bytes (wlByte in place of FCS).
2804        let mut src = magic.to_vec();
2805        src.push(0x00);
2806        src.push(0x20);
2807        assert_eq!(ZSTD_frameHeaderSize(&src), 6);
2808
2809        // fcsID=1, singleSegment=1 → 7 bytes.
2810        let mut src = magic.to_vec();
2811        src.push((1 << 6) | (1 << 5));
2812        src.extend_from_slice(&744u16.to_le_bytes());
2813        assert_eq!(ZSTD_frameHeaderSize(&src), 7);
2814
2815        // fcsID=3, singleSegment=1 → 13 bytes.
2816        let mut src = magic.to_vec();
2817        src.push((3 << 6) | (1 << 5));
2818        src.extend_from_slice(&0u64.to_le_bytes());
2819        assert_eq!(ZSTD_frameHeaderSize(&src), 13);
2820    }
2821
2822    #[test]
2823    fn frame_header_rejects_oversized_windowLog() {
2824        // Windowlog 32 exceeds ZSTD_WINDOWLOG_MAX_64 (31). Construct
2825        // a frame with wlByte whose top 5 bits encode windowLog - 10.
2826        // wlByte high 5 bits = 22 → windowLog = 22 + 10 = 32 → reject.
2827        let mut src = ZSTD_MAGICNUMBER.to_le_bytes().to_vec();
2828        src.push(0x00); // FHD: all zero (multi-segment, no dict/checksum)
2829        src.push(22u8 << 3); // wlByte → windowLog = 22 + 10 = 32
2830        let mut zfh = ZSTD_FrameHeader::default();
2831        let rc = ZSTD_getFrameHeader(&mut zfh, &src);
2832        assert!(crate::common::error::ERR_isError(rc));
2833        assert_eq!(
2834            crate::common::error::ERR_getErrorCode(rc),
2835            ErrorCode::FrameParameterWindowTooLarge
2836        );
2837    }
2838
2839    #[test]
2840    fn frame_header_accepts_max_windowLog() {
2841        // windowLog = 31 (exactly at the cap) must still parse.
2842        // wlByte high 5 bits = 21 → windowLog = 21 + 10 = 31.
2843        let mut src = ZSTD_MAGICNUMBER.to_le_bytes().to_vec();
2844        src.push(0x00);
2845        src.push(21u8 << 3);
2846        let mut zfh = ZSTD_FrameHeader::default();
2847        let rc = ZSTD_getFrameHeader(&mut zfh, &src);
2848        assert_eq!(rc, 0);
2849        assert_eq!(zfh.windowSize, 1u64 << 31);
2850    }
2851
2852    #[test]
2853    fn frame_header_bad_magic_errors() {
2854        let mut zfh = ZSTD_FrameHeader::default();
2855        let src = [0xFF, 0xFF, 0xFF, 0xFF, 0, 0];
2856        let rc = ZSTD_getFrameHeader(&mut zfh, &src);
2857        assert!(crate::common::error::ERR_isError(rc));
2858        assert_eq!(
2859            crate::common::error::ERR_getErrorCode(rc),
2860            ErrorCode::PrefixUnknown
2861        );
2862    }
2863
2864    #[test]
2865    fn frame_header_skippable_frame() {
2866        // Magic skippable[0] + 4-byte frame content size.
2867        let mut src = Vec::new();
2868        src.extend_from_slice(&ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes());
2869        src.extend_from_slice(&42u32.to_le_bytes());
2870        let mut zfh = ZSTD_FrameHeader::default();
2871        let rc = ZSTD_getFrameHeader(&mut zfh, &src);
2872        assert_eq!(rc, 0);
2873        assert_eq!(zfh.frameType, ZSTD_FrameType_e::ZSTD_skippableFrame);
2874        assert_eq!(zfh.frameContentSize, 42);
2875        assert_eq!(zfh.dictID, 0);
2876        assert_eq!(zfh.headerSize, ZSTD_SKIPPABLEHEADERSIZE as u32);
2877    }
2878
2879    #[test]
2880    fn frame_header_single_segment_no_dict_no_fcs() {
2881        // FHD byte: singleSegment=1, fcsID=0 → FCS field = 1 byte,
2882        // dictID=0, checksumFlag=0, reserved=0.
2883        // FHD = (fcsID<<6)|(singleSegment<<5)|(reserved<<3)|(checksumFlag<<2)|dictID
2884        //     = (0<<6)|(1<<5)|0|0|0 = 0x20.
2885        let mut src = Vec::new();
2886        src.extend_from_slice(&ZSTD_MAGICNUMBER.to_le_bytes());
2887        src.push(0x20); // FHD
2888        src.push(100); // FCS byte (singleSegment implies size is this byte)
2889        let mut zfh = ZSTD_FrameHeader::default();
2890        let rc = ZSTD_getFrameHeader(&mut zfh, &src);
2891        assert_eq!(rc, 0);
2892        assert_eq!(zfh.frameType, ZSTD_FrameType_e::ZSTD_frame);
2893        assert_eq!(zfh.frameContentSize, 100);
2894        assert_eq!(zfh.windowSize, 100); // singleSegment: window = FCS
2895        assert_eq!(zfh.checksumFlag, 0);
2896        assert_eq!(zfh.dictID, 0);
2897        assert_eq!(zfh.headerSize, 6);
2898    }
2899
2900    #[test]
2901    fn frame_header_fcs_size_variants() {
2902        // fcsID encodes the FCS field width: 0 → 1 byte (when
2903        // singleSegment), 1 → 2 bytes + 256 offset, 2 → 4 bytes,
2904        // 3 → 8 bytes. Exercise each non-trivial variant so the
2905        // ranger decode logic (line 204-206) stays byte-exact.
2906        //
2907        // FHD layout: (fcsID<<6)|(singleSegment<<5)|(reserved<<3)|(checksumFlag<<2)|dictID
2908
2909        // --- fcsID=1: FCS = LE16 + 256. Encode FCS=1000 → raw LE16 = 744.
2910        {
2911            let mut src = Vec::new();
2912            src.extend_from_slice(&ZSTD_MAGICNUMBER.to_le_bytes());
2913            src.push((1 << 6) | (1 << 5)); // fcsID=1, singleSegment=1
2914            src.extend_from_slice(&744u16.to_le_bytes());
2915            let mut zfh = ZSTD_FrameHeader::default();
2916            let rc = ZSTD_getFrameHeader(&mut zfh, &src);
2917            assert_eq!(rc, 0);
2918            assert_eq!(zfh.frameContentSize, 1000);
2919            assert_eq!(zfh.windowSize, 1000);
2920            assert_eq!(zfh.headerSize, 7);
2921        }
2922
2923        // --- fcsID=2: FCS = LE32.
2924        {
2925            let mut src = Vec::new();
2926            src.extend_from_slice(&ZSTD_MAGICNUMBER.to_le_bytes());
2927            src.push((2 << 6) | (1 << 5)); // fcsID=2, singleSegment=1
2928            src.extend_from_slice(&123_456u32.to_le_bytes());
2929            let mut zfh = ZSTD_FrameHeader::default();
2930            let rc = ZSTD_getFrameHeader(&mut zfh, &src);
2931            assert_eq!(rc, 0);
2932            assert_eq!(zfh.frameContentSize, 123_456);
2933            assert_eq!(zfh.windowSize, 123_456);
2934            assert_eq!(zfh.headerSize, 9);
2935        }
2936
2937        // --- fcsID=3: FCS = LE64.
2938        {
2939            let mut src = Vec::new();
2940            src.extend_from_slice(&ZSTD_MAGICNUMBER.to_le_bytes());
2941            src.push((3 << 6) | (1 << 5)); // fcsID=3, singleSegment=1
2942            src.extend_from_slice(&9_999_999_999u64.to_le_bytes());
2943            let mut zfh = ZSTD_FrameHeader::default();
2944            let rc = ZSTD_getFrameHeader(&mut zfh, &src);
2945            assert_eq!(rc, 0);
2946            assert_eq!(zfh.frameContentSize, 9_999_999_999);
2947            assert_eq!(zfh.headerSize, 13);
2948        }
2949    }
2950
2951    #[test]
2952    fn frame_header_window_descriptor() {
2953        // singleSegment=0, so there's a 1-byte window descriptor.
2954        // FHD = 0x00 (fcsID=0, singleSeg=0, reserved=0, checksum=0, dictID=0).
2955        // wlByte: windowLog = (wlByte>>3) + 10; we want windowLog=14 → wlByte>>3 = 4 → wlByte = 0x20.
2956        let mut src = Vec::new();
2957        src.extend_from_slice(&ZSTD_MAGICNUMBER.to_le_bytes());
2958        src.push(0x00); // FHD
2959        src.push(0x20); // wl
2960                        // No dictID, no FCS (fcsID=0 non-single → absent).
2961        let mut zfh = ZSTD_FrameHeader::default();
2962        let rc = ZSTD_getFrameHeader(&mut zfh, &src);
2963        assert_eq!(rc, 0);
2964        assert_eq!(zfh.windowSize, 1u64 << 14);
2965        assert_eq!(zfh.frameContentSize, ZSTD_CONTENTSIZE_UNKNOWN);
2966    }
2967
2968    fn make_raw_hello_frame() -> Vec<u8> {
2969        let mut src = Vec::new();
2970        src.extend_from_slice(&ZSTD_MAGICNUMBER.to_le_bytes());
2971        src.push(0x20); // FHD: singleSegment, fcsID=0 (1-byte FCS)
2972        src.push(5); // FCS byte
2973        let bh = (5u32 << 3) | (1); // lastBlock=1, bt_raw=0, cSize=5
2974        src.push((bh & 0xFF) as u8);
2975        src.push(((bh >> 8) & 0xFF) as u8);
2976        src.push(((bh >> 16) & 0xFF) as u8);
2977        src.extend_from_slice(b"HELLO");
2978        src
2979    }
2980
2981    #[test]
2982    fn get_frame_content_size_returns_declared_fcs() {
2983        let src = make_raw_hello_frame();
2984        let fcs = ZSTD_getFrameContentSize(&src);
2985        assert_eq!(fcs, 5);
2986    }
2987
2988    #[test]
2989    fn get_frame_content_size_unknown_on_absent_fcs() {
2990        // singleSegment=0, fcsID=0 → FCS absent → CONTENTSIZE_UNKNOWN.
2991        let mut src = Vec::new();
2992        src.extend_from_slice(&ZSTD_MAGICNUMBER.to_le_bytes());
2993        src.push(0x00); // FHD
2994        src.push(0x20); // window descriptor
2995        let fcs = ZSTD_getFrameContentSize(&src);
2996        assert_eq!(fcs, ZSTD_CONTENTSIZE_UNKNOWN);
2997    }
2998
2999    #[test]
3000    fn get_frame_content_size_error_on_bad_magic() {
3001        let src = [0xFFu8; 16];
3002        let fcs = ZSTD_getFrameContentSize(&src);
3003        assert_eq!(fcs, ZSTD_CONTENTSIZE_ERROR);
3004    }
3005
3006    #[test]
3007    fn get_frame_content_size_zero_for_skippable_frame() {
3008        // A skippable frame's user data isn't decompressed content
3009        // per the spec — `ZSTD_getFrameContentSize` must report 0,
3010        // not the user-data length (which parses into
3011        // frameContentSize on the skippable path).
3012        let mut src = Vec::new();
3013        src.extend_from_slice(&ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes());
3014        src.extend_from_slice(&12u32.to_le_bytes()); // user-data len
3015        src.extend_from_slice(&[0u8; 12]);
3016        assert_eq!(ZSTD_getFrameContentSize(&src), 0);
3017    }
3018
3019    #[test]
3020    fn find_frame_compressed_size_matches_hand_count() {
3021        let src = make_raw_hello_frame();
3022        let sz = ZSTD_findFrameCompressedSize(&src);
3023        assert_eq!(sz, src.len());
3024    }
3025
3026    #[test]
3027    fn decompressionMargin_nonzero_for_valid_frame() {
3028        // Compress some bytes, then ask for the decomp margin.
3029        let src = b"the fast brown fox ".repeat(32);
3030        let mut dst = vec![0u8; 256];
3031        let n = crate::compress::zstd_compress::ZSTD_compress(&mut dst, &src, 3);
3032        dst.truncate(n);
3033        let margin = ZSTD_decompressionMargin(&dst);
3034        assert!(!crate::common::error::ERR_isError(margin));
3035        assert!(margin > 0);
3036        // Upper bound: frame header + a few blocks' overhead +
3037        // blockSizeMax (128 KB). For a tiny payload we expect
3038        // margin < 200 KB.
3039        assert!(margin < 200 * 1024);
3040    }
3041
3042    #[test]
3043    fn decompressionMargin_includes_checksum_bytes_when_flag_is_set() {
3044        // `ZSTD_decompressionMargin` must account for the 4-byte XXH64
3045        // trailer when the frame declares `checksumFlag`. Two frames
3046        // compressed from the same source, one with checksum, one
3047        // without, should differ by at least 4 bytes of margin.
3048        let src = b"fast brown fox ".repeat(32);
3049        let mut cctx_a = crate::compress::zstd_compress::ZSTD_createCCtx().unwrap();
3050        crate::compress::zstd_compress::ZSTD_CCtx_setParameter(
3051            &mut cctx_a,
3052            crate::compress::zstd_compress::ZSTD_cParameter::ZSTD_c_checksumFlag,
3053            0,
3054        );
3055        let mut dst_no_chk = vec![0u8; 256];
3056        let n_no =
3057            crate::compress::zstd_compress::ZSTD_compress2(&mut cctx_a, &mut dst_no_chk, &src);
3058        assert!(!crate::common::error::ERR_isError(n_no));
3059        dst_no_chk.truncate(n_no);
3060
3061        let mut cctx_b = crate::compress::zstd_compress::ZSTD_createCCtx().unwrap();
3062        crate::compress::zstd_compress::ZSTD_CCtx_setParameter(
3063            &mut cctx_b,
3064            crate::compress::zstd_compress::ZSTD_cParameter::ZSTD_c_checksumFlag,
3065            1,
3066        );
3067        let mut dst_chk = vec![0u8; 256];
3068        let n_chk = crate::compress::zstd_compress::ZSTD_compress2(&mut cctx_b, &mut dst_chk, &src);
3069        assert!(!crate::common::error::ERR_isError(n_chk));
3070        dst_chk.truncate(n_chk);
3071
3072        let m_no = ZSTD_decompressionMargin(&dst_no_chk);
3073        let m_chk = ZSTD_decompressionMargin(&dst_chk);
3074        assert!(!crate::common::error::ERR_isError(m_no));
3075        assert!(!crate::common::error::ERR_isError(m_chk));
3076        // The checksum-bearing margin must exceed the plain margin by
3077        // at least 4 (the trailer). Block-count overhead is identical
3078        // for the same source → any excess is just the 4-byte XXH64.
3079        assert!(
3080            m_chk >= m_no + 4,
3081            "checksum margin must include ≥4 bytes over plain margin: chk={m_chk}, no={m_no}"
3082        );
3083    }
3084
3085    #[test]
3086    fn decompressionMargin_rejects_garbage_input() {
3087        // Invalid frame header → error sentinel, not a silent 0.
3088        let rc = ZSTD_decompressionMargin(&[0u8; 32]);
3089        assert!(crate::common::error::ERR_isError(rc));
3090    }
3091
3092    #[test]
3093    fn decompressBound_empty_input_is_zero() {
3094        assert_eq!(ZSTD_decompressBound(&[]), 0);
3095    }
3096
3097    #[test]
3098    fn decompressBound_corrupted_input_returns_error_sentinel() {
3099        // Non-frame bytes can't be parsed → CONTENTSIZE_ERROR.
3100        let bogus = [0u8, 1, 2, 3, 4, 5, 6, 7];
3101        assert_eq!(ZSTD_decompressBound(&bogus), ZSTD_CONTENTSIZE_ERROR);
3102    }
3103
3104    #[test]
3105    fn decompressBound_upper_bounds_real_decompressed_size() {
3106        // Regression gate: `ZSTD_decompressBound` must never return
3107        // a value smaller than the actual decompressed size across
3108        // varied (size, level) combos. A bound underrun would let
3109        // callers allocate too-small buffers.
3110        use crate::compress::zstd_compress::{ZSTD_compress, ZSTD_compressBound};
3111        for &size in &[0usize, 1, 33, 1024, 65_536, 131_073] {
3112            let src: Vec<u8> = (0..size as u32).map(|i| (i ^ (i >> 5)) as u8).collect();
3113            for &level in &[1i32, 3, 10] {
3114                let bound = ZSTD_compressBound(src.len());
3115                let mut dst = vec![0u8; bound];
3116                let n = ZSTD_compress(&mut dst, &src, level);
3117                assert!(!crate::common::error::ERR_isError(n));
3118                let db = ZSTD_decompressBound(&dst[..n]);
3119                assert!(
3120                    db != ZSTD_CONTENTSIZE_ERROR,
3121                    "decompressBound flagged error on valid frame size={size} level={level}"
3122                );
3123                assert!(
3124                    db >= size as u64,
3125                    "bound under-reports: size={size} level={level} bound={db}",
3126                );
3127            }
3128        }
3129    }
3130
3131    #[test]
3132    fn decompressBound_sums_frames() {
3133        // Build 2 raw-HELLO frames + 1 skippable.
3134        let mut src = make_raw_hello_frame();
3135        src.extend_from_slice(&make_raw_hello_frame());
3136        src.extend_from_slice(&ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes());
3137        src.extend_from_slice(&4u32.to_le_bytes());
3138        src.extend_from_slice(&[0u8; 4]);
3139        let bound = ZSTD_decompressBound(&src);
3140        // Two raw "HELLO" = 10; skippable doesn't add content.
3141        assert_eq!(bound, 10);
3142    }
3143
3144    #[test]
3145    fn getDecompressedSize_zero_on_unknown() {
3146        // Build a tiny "hello" raw frame without content size in header.
3147        let raw = make_raw_hello_frame();
3148        let got = ZSTD_getDecompressedSize(&raw);
3149        // FCS present in raw-hello frame (5) — return it, not 0.
3150        assert_eq!(got, 5);
3151
3152        // For random non-frame bytes, returns 0.
3153        let bogus = [0u8, 1, 2, 3];
3154        assert_eq!(ZSTD_getDecompressedSize(&bogus), 0);
3155
3156        // Valid frame WITHOUT FCS (singleSegment=0, fcsID=0) should
3157        // also collapse UNKNOWN (= u64::MAX sentinel) down to 0 per
3158        // the deprecated-API convention.
3159        let mut no_fcs = Vec::new();
3160        no_fcs.extend_from_slice(&ZSTD_MAGICNUMBER.to_le_bytes());
3161        no_fcs.push(0x00); // FHD: no single-segment, no FCS
3162        no_fcs.push(0x20); // window descriptor
3163        assert_eq!(ZSTD_getDecompressedSize(&no_fcs), 0);
3164    }
3165
3166    #[test]
3167    fn decodingBufferSize_min_basic() {
3168        // Small window, small content.
3169        let sz = ZSTD_decodingBufferSize_min(1024, 1024);
3170        assert!(!crate::common::error::ERR_isError(sz));
3171        assert!(sz >= 1024);
3172    }
3173
3174    #[test]
3175    fn decodingBufferSize_min_matches_upstream_formula() {
3176        // Regression gate for upstream's formula in
3177        // `ZSTD_decodingBufferSize_internal`:
3178        //   blockSize     = min(windowSize, ZSTD_BLOCKSIZE_MAX)   (with blockSizeMax = BLOCKSIZE_MAX)
3179        //   neededRBSize  = windowSize + 2*blockSize + 2*WILDCOPY
3180        //   return          min(frameContentSize, neededRBSize)
3181        use crate::common::zstd_internal::WILDCOPY_OVERLENGTH;
3182        use crate::decompress::zstd_decompress_block::ZSTD_BLOCKSIZE_MAX;
3183
3184        // Small window: blockSize = windowSize = 1024.
3185        let w = 1024u64;
3186        let fcs = u64::MAX; // Take the RB path, not the FCS path.
3187        let expected = w + 2 * w + (WILDCOPY_OVERLENGTH as u64) * 2;
3188        assert_eq!(ZSTD_decodingBufferSize_min(w, fcs) as u64, expected);
3189
3190        // Large window (> BLOCKSIZE_MAX): blockSize = BLOCKSIZE_MAX.
3191        let w2 = 1u64 << 20;
3192        let expected2 = w2 + 2 * ZSTD_BLOCKSIZE_MAX as u64 + (WILDCOPY_OVERLENGTH as u64) * 2;
3193        assert_eq!(ZSTD_decodingBufferSize_min(w2, fcs) as u64, expected2);
3194
3195        // FCS caps the result: when frameContentSize is tiny, return it.
3196        let tiny = 200u64;
3197        assert_eq!(ZSTD_decodingBufferSize_min(w2, tiny) as u64, tiny);
3198    }
3199
3200    #[test]
3201    fn copyDCtx_deep_copies_all_state() {
3202        let mut src = ZSTD_DCtx::default();
3203        src.stream_dict = b"marker-dict".to_vec();
3204        src.d_windowLogMax = 25;
3205
3206        let mut dst = ZSTD_DCtx::default();
3207        ZSTD_copyDCtx(&mut dst, &src);
3208        assert_eq!(dst.stream_dict, b"marker-dict");
3209        assert_eq!(dst.d_windowLogMax, 25);
3210    }
3211
3212    #[test]
3213    fn initDStream_family_returns_startingInputLength() {
3214        // All three `ZSTD_initDStream*` variants must return the
3215        // starting-input length hint (5 for zstd1), matching upstream
3216        // (zstd_decompress.c:1746/1755/1766). Silent 0 previously —
3217        // broke streaming callers that used the return as the initial
3218        // minimum-bytes-to-read hint.
3219        use crate::decompress::zstd_ddict::ZSTD_createDDict;
3220        let expected = ZSTD_startingInputLength(ZSTD_format_e::ZSTD_f_zstd1);
3221        let mut a = ZSTD_DCtx::default();
3222        assert_eq!(ZSTD_initDStream(&mut a), expected);
3223        let mut b = ZSTD_DCtx::default();
3224        assert_eq!(ZSTD_initDStream_usingDict(&mut b, b"seed-dict"), expected);
3225        let mut c = ZSTD_DCtx::default();
3226        let ddict = ZSTD_createDDict(b"seed-ddict").expect("ddict");
3227        assert_eq!(ZSTD_initDStream_usingDDict(&mut c, &ddict), expected);
3228    }
3229
3230    #[test]
3231    fn initDStream_usingDDict_copies_dict_content() {
3232        use crate::decompress::zstd_ddict::ZSTD_createDDict;
3233        let dict = b"test-dict-content".to_vec();
3234        let ddict = ZSTD_createDDict(&dict).expect("ddict");
3235        let mut dctx = ZSTD_DCtx::default();
3236        let rc = ZSTD_initDStream_usingDDict(&mut dctx, &ddict);
3237        // Upstream (zstd_decompress.c:1766) returns
3238        // `ZSTD_startingInputLength(format)` = 5 for zstd1. Previously
3239        // our port returned 0 — masked divergence for C-compat
3240        // callers that used the return as the initial buffer size.
3241        assert_eq!(rc, ZSTD_startingInputLength(ZSTD_format_e::ZSTD_f_zstd1));
3242        assert_eq!(dctx.stream_dict, dict);
3243    }
3244
3245    #[test]
3246    fn nextSrcSizeToDecompress_starts_at_frame_header_prefix() {
3247        let dctx = ZSTD_DCtx::default();
3248        assert_eq!(
3249            ZSTD_nextSrcSizeToDecompress(&dctx),
3250            ZSTD_startingInputLength(ZSTD_format_e::ZSTD_f_zstd1)
3251        );
3252    }
3253
3254    #[test]
3255    fn insertBlock_updates_history_end_and_contiguity_state() {
3256        let mut dctx = ZSTD_DCtx::default();
3257        let a = vec![1u8; 8];
3258        let b = vec![2u8; 5];
3259
3260        let n = ZSTD_insertBlock(&mut dctx, &a);
3261        assert_eq!(n, a.len());
3262        assert_eq!(dctx.prefixStart, Some(a.as_ptr() as usize));
3263        assert_eq!(dctx.previousDstEnd, Some(a.as_ptr() as usize + a.len()));
3264        assert_eq!(dctx.dictEnd, None);
3265
3266        let n = ZSTD_insertBlock(&mut dctx, &b);
3267        assert_eq!(n, b.len());
3268        assert_eq!(dctx.dictEnd, Some(a.as_ptr() as usize + a.len()));
3269        assert_eq!(dctx.prefixStart, Some(b.as_ptr() as usize));
3270        assert_eq!(dctx.previousDstEnd, Some(b.as_ptr() as usize + b.len()));
3271    }
3272
3273    #[test]
3274    fn initDStream_preserves_configured_dict() {
3275        // Once a dict is loaded, ZSTD_initDStream (session_only-style
3276        // reset) must keep it so back-to-back frame decodes all see
3277        // the same dict.
3278        let mut dctx = ZSTD_DCtx::default();
3279        dctx.stream_dict = b"sticky-dict".to_vec();
3280        ZSTD_initDStream(&mut dctx);
3281        assert_eq!(dctx.stream_dict, b"sticky-dict");
3282    }
3283
3284    #[test]
3285    fn dParam_all_variants_set_get_roundtrip() {
3286        // Symmetric with CCtx side — every ZSTD_dParameter variant
3287        // should round-trip via setParameter / getParameter.
3288        let mut dctx = ZSTD_DCtx::default();
3289        let cases = [(ZSTD_dParameter::ZSTD_d_windowLogMax, 20)];
3290        for &(param, value) in &cases {
3291            ZSTD_DCtx_setParameter(&mut dctx, param, value);
3292            let mut got = -1i32;
3293            ZSTD_DCtx_getParameter(&dctx, param, &mut got);
3294            assert_eq!(got, value, "param {:?} didn't round-trip", param);
3295        }
3296    }
3297
3298    #[test]
3299    fn dctx_reset_modes_differ_correctly() {
3300        let mut dctx = ZSTD_DCtx::default();
3301        dctx.stream_dict = b"prior-dict".to_vec();
3302        dctx.d_windowLogMax = 25;
3303
3304        // session_only: keep dict + windowLogMax.
3305        ZSTD_DCtx_reset(&mut dctx, ZSTD_DResetDirective::ZSTD_reset_session_only);
3306        assert_eq!(dctx.stream_dict, b"prior-dict");
3307        assert_eq!(dctx.d_windowLogMax, 25);
3308
3309        // parameters: drop them. `d_windowLogMax` resets to
3310        // `ZSTD_WINDOWLOG_LIMIT_DEFAULT` (27), matching upstream's
3311        // `maxWindowSize = (1 << 27) + 1` initialization.
3312        ZSTD_DCtx_reset(&mut dctx, ZSTD_DResetDirective::ZSTD_reset_parameters);
3313        assert!(dctx.stream_dict.is_empty());
3314        assert_eq!(dctx.d_windowLogMax, ZSTD_WINDOWLOG_LIMIT_DEFAULT);
3315
3316        // session_and_parameters: superset — does both in one call.
3317        dctx.stream_dict = b"seed-again".to_vec();
3318        dctx.d_windowLogMax = 20;
3319        ZSTD_DCtx_reset(
3320            &mut dctx,
3321            ZSTD_DResetDirective::ZSTD_reset_session_and_parameters,
3322        );
3323        assert!(dctx.stream_dict.is_empty());
3324        assert_eq!(dctx.d_windowLogMax, ZSTD_WINDOWLOG_LIMIT_DEFAULT);
3325    }
3326
3327    #[test]
3328    fn decompress_side_free_functions_accept_none_without_panic() {
3329        // Symmetric with the compress-side contract: the two
3330        // decompression-side Option-taking freers must accept None
3331        // without panicking. (`ZSTD_freeDCtx` takes `Box<T>` directly
3332        // and has no None path.)
3333        assert_eq!(ZSTD_freeDStream(None), 0);
3334        assert_eq!(crate::decompress::zstd_ddict::ZSTD_freeDDict(None), 0);
3335    }
3336
3337    #[test]
3338    fn decompressBegin_variants_seed_dict_consistently() {
3339        // Legacy continue-style init entries must all land the same
3340        // bytes in `stream_dict`:
3341        //   - _usingDict stashes the raw dict
3342        //   - _usingDDict extracts the DDict's content, same result
3343        //   - plain _decompressBegin is a no-op (doesn't clear)
3344        use crate::decompress::zstd_ddict::ZSTD_createDDict;
3345        let dict = b"begin-usingDict-seed".to_vec();
3346
3347        let mut a = ZSTD_DCtx::default();
3348        assert_eq!(ZSTD_decompressBegin_usingDict(&mut a, &dict), 0);
3349        assert_eq!(a.stream_dict, dict);
3350
3351        let ddict = ZSTD_createDDict(&dict).expect("ddict");
3352        let mut b = ZSTD_DCtx::default();
3353        assert_eq!(ZSTD_decompressBegin_usingDDict(&mut b, &ddict), 0);
3354        assert_eq!(b.stream_dict, dict);
3355
3356        // Plain decompressBegin on a DCtx with a prior dict must
3357        // leave it intact (upstream semantic: no-op in v0.1 scope).
3358        let mut c = ZSTD_DCtx::default();
3359        c.stream_dict = b"preloaded".to_vec();
3360        assert_eq!(ZSTD_decompressBegin(&mut c), 0);
3361        assert_eq!(c.stream_dict, b"preloaded");
3362    }
3363
3364    #[test]
3365    fn DCtx_refDDict_seeds_stream_dict_and_roundtrips_via_stream_api() {
3366        // Symmetric with ZSTD_CCtx_refCDict: refDDict wires the
3367        // DDict's raw content into the DCtx so a subsequent
3368        // streaming decompress honors the dict.
3369        use crate::compress::zstd_compress::{ZSTD_compress_usingDict, ZSTD_createCCtx};
3370        use crate::decompress::zstd_ddict::ZSTD_createDDict;
3371
3372        let dict = b"refDDict-test-dict ".repeat(8);
3373        let ddict = ZSTD_createDDict(&dict).expect("ddict");
3374
3375        // State check.
3376        let mut dctx = ZSTD_DCtx::default();
3377        let rc = ZSTD_DCtx_refDDict(&mut dctx, &ddict);
3378        assert_eq!(rc, 0);
3379        assert_eq!(dctx.stream_dict, dict);
3380
3381        // Roundtrip: compress with the raw-content dict, decompress
3382        // via the streaming DCtx that was primed via refDDict.
3383        let src: Vec<u8> = b"payload wearing the refDDict-test-dict ".repeat(18);
3384        let mut cctx = ZSTD_createCCtx().unwrap();
3385        let mut cbuf = vec![0u8; 4096];
3386        let n = ZSTD_compress_usingDict(&mut cctx, &mut cbuf, &src, &dict, 3);
3387        assert!(!crate::common::error::ERR_isError(n));
3388
3389        ZSTD_initDStream(&mut dctx);
3390        let mut out = vec![0u8; src.len() + 64];
3391        let mut in_pos = 0usize;
3392        let mut out_pos = 0usize;
3393        let drain =
3394            ZSTD_decompressStream(&mut dctx, &mut out, &mut out_pos, &cbuf[..n], &mut in_pos);
3395        assert!(!crate::common::error::ERR_isError(drain));
3396        assert_eq!(&out[..out_pos], &src[..]);
3397    }
3398
3399    #[test]
3400    fn DCtx_refPrefix_roundtrips_with_compressor_using_dict() {
3401        // End-to-end: compress via ZSTD_compress_usingDict, decompress
3402        // through a streaming DCtx that was primed with
3403        // ZSTD_DCtx_refPrefix. refPrefix must be symmetric with the
3404        // compression-side dict load.
3405        use crate::compress::zstd_compress::{ZSTD_compress_usingDict, ZSTD_createCCtx};
3406        let dict = b"shared-dict-content-for-prefix-test ".repeat(8);
3407        let src = b"payload that uses shared-dict-content-for-prefix-test ".repeat(16);
3408
3409        let mut cctx = ZSTD_createCCtx().unwrap();
3410        let mut dst = vec![0u8; 4096];
3411        let n = ZSTD_compress_usingDict(&mut cctx, &mut dst, &src, &dict, 3);
3412        assert!(!crate::common::error::ERR_isError(n));
3413
3414        let mut dctx = ZSTD_DCtx::default();
3415        let rc = ZSTD_DCtx_refPrefix(&mut dctx, &dict);
3416        assert_eq!(rc, 0);
3417        ZSTD_initDStream(&mut dctx);
3418        let mut out_buf = vec![0u8; src.len() + 64];
3419        let mut in_pos = 0usize;
3420        let mut out_pos = 0usize;
3421        let drain = ZSTD_decompressStream(
3422            &mut dctx,
3423            &mut out_buf,
3424            &mut out_pos,
3425            &dst[..n],
3426            &mut in_pos,
3427        );
3428        assert!(
3429            !crate::common::error::ERR_isError(drain),
3430            "drain err: {drain:#x}"
3431        );
3432        assert_eq!(&out_buf[..out_pos], &src[..]);
3433    }
3434
3435    #[test]
3436    fn DCtx_loadDictionary_variants_store_equivalent_state() {
3437        // loadDictionary, loadDictionary_byReference, and
3438        // loadDictionary_advanced are thin wrappers that must all
3439        // land the bytes in `stream_dict`. Regression gate in case
3440        // someone re-implements one of them without the others.
3441        use crate::decompress::zstd_ddict::{ZSTD_dictContentType_e, ZSTD_dictLoadMethod_e};
3442        let dict = b"variant-equivalence-test-dict".to_vec();
3443
3444        let mut a = ZSTD_DCtx::default();
3445        ZSTD_DCtx_loadDictionary(&mut a, &dict);
3446
3447        let mut b = ZSTD_DCtx::default();
3448        ZSTD_DCtx_loadDictionary_byReference(&mut b, &dict);
3449
3450        let mut c = ZSTD_DCtx::default();
3451        ZSTD_DCtx_loadDictionary_advanced(
3452            &mut c,
3453            &dict,
3454            ZSTD_dictLoadMethod_e::ZSTD_dlm_byCopy,
3455            ZSTD_dictContentType_e::ZSTD_dct_auto,
3456        );
3457
3458        assert_eq!(a.stream_dict, dict);
3459        assert_eq!(b.stream_dict, dict);
3460        assert_eq!(c.stream_dict, dict);
3461    }
3462
3463    #[test]
3464    fn decompressStream_handles_skippable_then_regular_frame() {
3465        // Streaming decoder contract: a skippable frame fed first
3466        // should not poison subsequent decompression of a real frame
3467        // staged after it. Upstream decoders silently consume the
3468        // skippable's 8+N bytes and proceed; our port must do the
3469        // same in streaming mode.
3470        let src = b"streaming-skip-then-real ".repeat(20);
3471        let mut frame = vec![0u8; 4096];
3472        let n = crate::compress::zstd_compress::ZSTD_compress(&mut frame, &src, 3);
3473        assert!(!crate::common::error::ERR_isError(n));
3474
3475        let mut stream = Vec::new();
3476        stream.extend_from_slice(&ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes());
3477        stream.extend_from_slice(&8u32.to_le_bytes());
3478        stream.extend_from_slice(b"SKIPDATA");
3479        stream.extend_from_slice(&frame[..n]);
3480
3481        let mut dctx = ZSTD_DCtx::new();
3482        ZSTD_initDStream(&mut dctx);
3483        let mut out = vec![0u8; src.len() + 64];
3484        let mut in_pos = 0usize;
3485        let mut out_pos = 0usize;
3486        let _hint = ZSTD_decompressStream(&mut dctx, &mut out, &mut out_pos, &stream, &mut in_pos);
3487        // Keep draining until no more progress is expected (simple
3488        // cap on iterations).
3489        for _ in 0..8 {
3490            if out_pos >= src.len() {
3491                break;
3492            }
3493            let _ = ZSTD_decompressStream(&mut dctx, &mut out, &mut out_pos, &[], &mut 0usize);
3494        }
3495        assert_eq!(&out[..out_pos], &src[..]);
3496    }
3497
3498    #[test]
3499    fn decompressDCtx_truncated_frame_errors_out_cleanly() {
3500        // Feeding half of a valid frame must return an error (not
3501        // panic, not produce garbage). Upstream surfaces srcSize_wrong
3502        // through ZSTD_decompressFrame; ours should do the same.
3503        use crate::decompress::zstd_decompress_block::{
3504            ZSTD_buildDefaultSeqTables, ZSTD_decoder_entropy_rep,
3505        };
3506        let src = b"full-frame-that-we-then-truncate ".repeat(20);
3507        let mut frame = vec![0u8; 4096];
3508        let n = crate::compress::zstd_compress::ZSTD_compress(&mut frame, &src, 3);
3509        assert!(!crate::common::error::ERR_isError(n));
3510
3511        let mut dctx = ZSTD_DCtx::new();
3512        ZSTD_buildDefaultSeqTables(&mut dctx);
3513        let mut rep = ZSTD_decoder_entropy_rep::default();
3514        let mut xxh = crate::common::xxhash::XXH64_state_t::default();
3515        let mut out = vec![0u8; src.len() + 64];
3516        let decoded = ZSTD_decompressDCtx(&mut dctx, &mut rep, &mut xxh, &mut out, &frame[..n / 2]);
3517        assert!(
3518            crate::common::error::ERR_isError(decoded),
3519            "truncated frame should error, got decoded={}",
3520            decoded,
3521        );
3522    }
3523
3524    #[test]
3525    fn decompressDCtx_advances_past_skippable_frames_mid_stream() {
3526        // Regression gate: `ZSTD_decompressDCtx` must transparently
3527        // advance past skippable frames when they appear BETWEEN two
3528        // regular frames. The full decoded byte count is the sum of
3529        // regular-frame payloads; skippable frames contribute nothing
3530        // to `dst` but their magic+size+payload must be stepped over.
3531        use crate::decompress::zstd_decompress_block::{
3532            ZSTD_buildDefaultSeqTables, ZSTD_decoder_entropy_rep,
3533        };
3534        let first = b"alpha-first-frame".as_ref();
3535        let second = b"omega-second-frame".as_ref();
3536        let mut stream = Vec::new();
3537        {
3538            let mut buf = vec![0u8; 256];
3539            let n = crate::compress::zstd_compress::ZSTD_compress(&mut buf, first, 3);
3540            stream.extend_from_slice(&buf[..n]);
3541        }
3542        stream.extend_from_slice(&ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes());
3543        stream.extend_from_slice(&8u32.to_le_bytes());
3544        stream.extend_from_slice(b"SKIPDATA");
3545        {
3546            let mut buf = vec![0u8; 256];
3547            let n = crate::compress::zstd_compress::ZSTD_compress(&mut buf, second, 3);
3548            stream.extend_from_slice(&buf[..n]);
3549        }
3550
3551        let mut dctx = ZSTD_DCtx::new();
3552        ZSTD_buildDefaultSeqTables(&mut dctx);
3553        let mut rep = ZSTD_decoder_entropy_rep::default();
3554        let mut xxh = crate::common::xxhash::XXH64_state_t::default();
3555        let mut out = vec![0u8; first.len() + second.len() + 64];
3556        let decoded = ZSTD_decompressDCtx(&mut dctx, &mut rep, &mut xxh, &mut out, &stream);
3557        assert!(!crate::common::error::ERR_isError(decoded));
3558        assert_eq!(decoded, first.len() + second.len());
3559        assert_eq!(&out[..first.len()], first);
3560        assert_eq!(&out[first.len()..decoded], second);
3561    }
3562
3563    #[test]
3564    fn decompressDCtx_applies_loaded_stream_dict() {
3565        // Upstream `ZSTD_decompressDCtx` routes through
3566        // `ZSTD_decompress_usingDDict(dctx, ..., ZSTD_getDDict(dctx))`,
3567        // so a dict loaded via `ZSTD_DCtx_loadDictionary` must be
3568        // honored. Previously our port ignored `dctx.stream_dict` and
3569        // fed dict-compressed frames through the no-dict decoder,
3570        // silently producing garbage.
3571        let dict = b"shared-dict-for-decompressDCtx-apply ".repeat(8);
3572        let src: Vec<u8> = b"payload referring to shared-dict-for-decompressDCtx-apply ".repeat(15);
3573        let mut cctx = crate::compress::zstd_compress::ZSTD_createCCtx().unwrap();
3574        let mut frame = vec![0u8; 4096];
3575        let n = crate::compress::zstd_compress::ZSTD_compress_usingDict(
3576            &mut cctx, &mut frame, &src, &dict, 3,
3577        );
3578        assert!(!crate::common::error::ERR_isError(n));
3579
3580        let mut dctx = ZSTD_DCtx::new();
3581        ZSTD_DCtx_loadDictionary(&mut dctx, &dict);
3582        use crate::decompress::zstd_decompress_block::{
3583            ZSTD_buildDefaultSeqTables, ZSTD_decoder_entropy_rep,
3584        };
3585        ZSTD_buildDefaultSeqTables(&mut dctx);
3586        let mut rep = ZSTD_decoder_entropy_rep::default();
3587        let mut xxh = crate::common::xxhash::XXH64_state_t::default();
3588        let mut out = vec![0u8; src.len() + 64];
3589        let decoded = ZSTD_decompressDCtx(&mut dctx, &mut rep, &mut xxh, &mut out, &frame[..n]);
3590        assert!(!crate::common::error::ERR_isError(decoded));
3591        assert_eq!(&out[..decoded], &src[..]);
3592    }
3593
3594    #[test]
3595    fn getFrameHeader_on_skippable_frame_returns_variant_and_size() {
3596        // Upstream contract (zstd_decompress.c: `ZSTD_getFrameHeader_advanced`):
3597        // when `src` starts with a skippable magic, `zfh.dictID`
3598        // stores the magic-variant nibble (0..=15), `frameType` is
3599        // `ZSTD_skippableFrame`, `frameContentSize` is the user-data
3600        // length, and `headerSize` is `ZSTD_SKIPPABLEHEADERSIZE` (8).
3601        // Previously unpinned — a driver change here would silently
3602        // mis-describe skippable frames to callers.
3603        let mut buf = Vec::new();
3604        buf.extend_from_slice(&(ZSTD_MAGIC_SKIPPABLE_START + 7).to_le_bytes());
3605        buf.extend_from_slice(&12u32.to_le_bytes());
3606        buf.extend_from_slice(&[0xAB; 12]);
3607
3608        let mut zfh = ZSTD_FrameHeader::default();
3609        let rc = ZSTD_getFrameHeader(&mut zfh, &buf);
3610        assert_eq!(rc, 0);
3611        assert_eq!(zfh.frameType, ZSTD_FrameType_e::ZSTD_skippableFrame);
3612        assert_eq!(zfh.dictID, 7);
3613        assert_eq!(zfh.headerSize, ZSTD_SKIPPABLEHEADERSIZE as u32);
3614        assert_eq!(zfh.frameContentSize, 12);
3615    }
3616
3617    #[test]
3618    fn DCtx_loadDictionary_empty_slice_clears_previous_dict() {
3619        // Sibling of the CCtx test: the decoder's `loadDictionary`
3620        // with an empty slice must clear any previously loaded dict,
3621        // not leave stale bytes. Upstream equivalent is
3622        // `ZSTD_DCtx_loadDictionary_advanced(dctx, NULL, 0, ...)`.
3623        let mut dctx = ZSTD_DCtx::default();
3624        ZSTD_DCtx_loadDictionary(&mut dctx, b"sticky-dict");
3625        assert_eq!(dctx.stream_dict, b"sticky-dict");
3626        ZSTD_DCtx_loadDictionary(&mut dctx, &[]);
3627        assert!(dctx.stream_dict.is_empty());
3628    }
3629
3630    #[test]
3631    fn decompress_usingDict_with_empty_dict_matches_no_dict_path() {
3632        // Upstream treats an empty dict as "no dict" — decode must
3633        // succeed for frames compressed without a dict. Ensures the
3634        // newly-added dictID-mismatch check doesn't spuriously reject
3635        // no-dict frames (frame dictID=0 + dict dictID=0 → no conflict).
3636        let src = b"empty-dict == no-dict ".repeat(20);
3637        let mut cbuf = vec![0u8; 4096];
3638        let n = crate::compress::zstd_compress::ZSTD_compress(&mut cbuf, &src, 3);
3639        assert!(!crate::common::error::ERR_isError(n));
3640
3641        // Decompress with an empty dict slice.
3642        let mut dctx = ZSTD_DCtx::new();
3643        let mut out = vec![0u8; src.len() + 64];
3644        let rc = ZSTD_decompress_usingDict(&mut dctx, &mut out, &cbuf[..n], &[]);
3645        assert!(!crate::common::error::ERR_isError(rc));
3646        assert_eq!(&out[..rc], &src[..]);
3647    }
3648
3649    #[test]
3650    fn decompress_usingDict_rejects_dictID_mismatch() {
3651        // Upstream contract: when both the frame header and the dict
3652        // declare a dictID, they must match — otherwise the caller
3653        // gets `DictionaryWrong` instead of silently-corrupted output.
3654        // Previously Rust port skipped this check.
3655        use crate::common::error::{ERR_getErrorCode, ERR_isError};
3656        use crate::common::mem::MEM_writeLE32;
3657
3658        // Start from a real full dictionary, then mutate only the
3659        // dictID field to create a mismatch without corrupting the
3660        // entropy tables or raw content.
3661        let mut dict_a =
3662            include_bytes!("../../tests/fixtures/upstream-zstd/dict-files/zero-weight-dict")
3663                .to_vec();
3664        MEM_writeLE32(&mut dict_a[4..8], 0x11111111);
3665        let mut dict_b = dict_a.clone();
3666        MEM_writeLE32(&mut dict_b[4..8], 0x22222222);
3667
3668        // Compress with dict A (so frame header declares dictID=A).
3669        let src = b"dict-A-vs-dict-B dictID mismatch ".repeat(20);
3670        let mut cctx = crate::compress::zstd_compress::ZSTD_createCCtx().unwrap();
3671        let mut frame = vec![0u8; 4096];
3672        let n = crate::compress::zstd_compress::ZSTD_compress_usingDict(
3673            &mut cctx, &mut frame, &src, &dict_a, 3,
3674        );
3675        assert!(!ERR_isError(n));
3676
3677        // Decompress with dict B: dictID mismatch → DictionaryWrong.
3678        let mut dctx = ZSTD_DCtx::new();
3679        let mut out = vec![0u8; src.len() + 64];
3680        let rc = ZSTD_decompress_usingDict(&mut dctx, &mut out, &frame[..n], &dict_b);
3681        assert!(ERR_isError(rc));
3682        assert_eq!(ERR_getErrorCode(rc), ErrorCode::DictionaryWrong);
3683    }
3684
3685    #[test]
3686    fn decompress_usingDDict_rejects_dictID_mismatch() {
3687        use crate::common::error::{ERR_getErrorCode, ERR_isError};
3688        use crate::common::mem::MEM_writeLE32;
3689        use crate::decompress::zstd_ddict::ZSTD_createDDict;
3690
3691        let mut dict_a =
3692            include_bytes!("../../tests/fixtures/upstream-zstd/dict-files/zero-weight-dict")
3693                .to_vec();
3694        MEM_writeLE32(&mut dict_a[4..8], 0x11111111);
3695        let mut dict_b = dict_a.clone();
3696        MEM_writeLE32(&mut dict_b[4..8], 0x22222222);
3697
3698        let src = b"dict-A-vs-ddict-B dictID mismatch ".repeat(20);
3699        let mut cctx = crate::compress::zstd_compress::ZSTD_createCCtx().unwrap();
3700        let mut frame = vec![0u8; 4096];
3701        let n = crate::compress::zstd_compress::ZSTD_compress_usingDict(
3702            &mut cctx, &mut frame, &src, &dict_a, 3,
3703        );
3704        assert!(!ERR_isError(n));
3705
3706        let ddict_b = ZSTD_createDDict(&dict_b).expect("ddict");
3707        let mut dctx = ZSTD_DCtx::new();
3708        let mut out = vec![0u8; src.len() + 64];
3709        let rc = ZSTD_decompress_usingDDict(&mut dctx, &mut out, &frame[..n], &ddict_b);
3710        assert!(ERR_isError(rc));
3711        assert_eq!(ERR_getErrorCode(rc), ErrorCode::DictionaryWrong);
3712    }
3713
3714    #[test]
3715    fn decompress_usingDict_uses_caller_owned_dctx() {
3716        // Parity fix: previously `ZSTD_decompress_usingDict` allocated a
3717        // fresh `ZSTD_DCtx` internally and threw the caller's dctx away
3718        // — any per-session state set by the caller (e.g. blockSizeMax
3719        // from the decoded frame) would never surface. After the fix we
3720        // call `decompressBegin` on the caller's dctx and decode into
3721        // it, so observable fields like `isFrameDecompression` and
3722        // `blockSizeMax` reflect the call.
3723        use crate::common::error::ERR_isError;
3724        use crate::decompress::zstd_decompress_block::ZSTD_DCtx;
3725
3726        let src_bytes = b"use-caller-dctx-for-decompress-usingDict-parity ".repeat(4);
3727        let mut cctx = crate::compress::zstd_compress::ZSTD_createCCtx().unwrap();
3728        let mut frame = vec![0u8; 4096];
3729        // Compress without a dict — still exercises the usingDict path
3730        // with an empty dict.
3731        let n = crate::compress::zstd_compress::ZSTD_compress_usingDict(
3732            &mut cctx,
3733            &mut frame,
3734            &src_bytes,
3735            &[],
3736            3,
3737        );
3738        assert!(!ERR_isError(n));
3739
3740        // Seed a dctx with a bogus dictID to prove decompressBegin
3741        // actually runs (it resets dictID to 0).
3742        let mut dctx = ZSTD_DCtx::new();
3743        dctx.dictID = 0xDEAD_BEEF;
3744        let mut out = vec![0u8; src_bytes.len() + 64];
3745        let rc = ZSTD_decompress_usingDict(&mut dctx, &mut out, &frame[..n], &[]);
3746        assert!(!ERR_isError(rc));
3747        assert_eq!(&out[..rc], src_bytes.as_slice());
3748        // decompressBegin cleared the seeded dictID.
3749        assert_eq!(dctx.dictID, 0);
3750        // The frame-level fields land on the caller's dctx.
3751        assert_eq!(dctx.isFrameDecompression, 1);
3752        assert!(dctx.blockSizeMax > 0);
3753    }
3754
3755    #[test]
3756    fn DCtx_refPrefix_advanced_matches_plain_across_every_contentType() {
3757        // `ZSTD_DCtx_refPrefix_advanced` is the content-type-aware
3758        // sibling of `refPrefix`. v0.1 treats every content-type as
3759        // raw, so all three flavors must produce the same stream_dict
3760        // state as the plain call — any drift would signal an
3761        // accidental wiring mistake in one branch.
3762        use crate::decompress::zstd_ddict::ZSTD_dictContentType_e;
3763        let prefix = b"advanced-refPrefix-roundtrip".to_vec();
3764
3765        let mut plain = ZSTD_DCtx::default();
3766        ZSTD_DCtx_refPrefix(&mut plain, &prefix);
3767
3768        for ct in [
3769            ZSTD_dictContentType_e::ZSTD_dct_auto,
3770            ZSTD_dictContentType_e::ZSTD_dct_rawContent,
3771            ZSTD_dictContentType_e::ZSTD_dct_fullDict,
3772        ] {
3773            let mut adv = ZSTD_DCtx::default();
3774            let rc = ZSTD_DCtx_refPrefix_advanced(&mut adv, &prefix, ct);
3775            assert_eq!(rc, 0);
3776            assert_eq!(adv.stream_dict, plain.stream_dict);
3777        }
3778    }
3779
3780    #[test]
3781    fn initStatic_decompression_variants_construct_ctx_and_ddict() {
3782        let mut buf = vec![0u64; (1 << 20) / core::mem::size_of::<u64>()];
3783        let bytes = unsafe {
3784            core::slice::from_raw_parts_mut(
3785                buf.as_mut_ptr() as *mut u8,
3786                buf.len() * core::mem::size_of::<u64>(),
3787            )
3788        };
3789        let dctx = ZSTD_initStaticDCtx(bytes).expect("static dctx");
3790        assert_eq!(
3791            ZSTD_nextSrcSizeToDecompress(dctx),
3792            ZSTD_startingInputLength(ZSTD_format_e::ZSTD_f_zstd1)
3793        );
3794        let dstream = ZSTD_initStaticDStream(bytes).expect("static dstream");
3795        assert_eq!(
3796            ZSTD_nextSrcSizeToDecompress(dstream),
3797            ZSTD_startingInputLength(ZSTD_format_e::ZSTD_f_zstd1)
3798        );
3799        let dict_bytes = b"dict";
3800        let ddict = ZSTD_initStaticDDict(bytes, dict_bytes).expect("static ddict");
3801        assert_eq!(
3802            crate::decompress::zstd_ddict::ZSTD_DDict_dictContent(ddict),
3803            dict_bytes
3804        );
3805    }
3806
3807    #[test]
3808    fn DCtx_setMaxWindowSize_stores_log2() {
3809        let mut dctx = ZSTD_DCtx::default();
3810        let rc = ZSTD_DCtx_setMaxWindowSize(&mut dctx, 1 << 18);
3811        assert_eq!(rc, 0);
3812        assert_eq!(dctx.d_windowLogMax, 18u32);
3813    }
3814
3815    #[test]
3816    fn DCtx_setMaxWindowSize_rejects_out_of_bounds() {
3817        let mut dctx = ZSTD_DCtx::default();
3818        // Below min (1<<10 = 1024).
3819        assert!(crate::common::error::ERR_isError(
3820            ZSTD_DCtx_setMaxWindowSize(&mut dctx, 100)
3821        ));
3822        // Above max: upper bound is ZSTD_WINDOWLOG_MAX (31 on 64-bit,
3823        // 30 on 32-bit). 1 << (max+1) is the first out-of-range byte
3824        // count on 64-bit; on 32-bit it would overflow usize so skip.
3825        if crate::common::mem::MEM_32bits() == 0 {
3826            assert!(crate::common::error::ERR_isError(
3827                ZSTD_DCtx_setMaxWindowSize(&mut dctx, 1usize << 32)
3828            ));
3829        }
3830    }
3831
3832    #[test]
3833    fn isFrame_and_isSkippable_are_exclusive() {
3834        // A real zstd frame: magic 0xFD2FB528.
3835        let mut real = 0xFD2FB528u32.to_le_bytes().to_vec();
3836        real.resize(32, 0);
3837        assert_eq!(ZSTD_isFrame(&real), 1);
3838        assert_eq!(ZSTD_isSkippableFrame(&real), 0);
3839
3840        // A skippable frame.
3841        let mut skip = ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes().to_vec();
3842        skip.extend_from_slice(&0u32.to_le_bytes());
3843        assert_eq!(ZSTD_isFrame(&skip), 1); // isFrame includes skippable
3844        assert_eq!(ZSTD_isSkippableFrame(&skip), 1);
3845
3846        // Random bytes — neither.
3847        let rand = [0u8, 1, 2, 3, 4, 5, 6, 7];
3848        assert_eq!(ZSTD_isFrame(&rand), 0);
3849        assert_eq!(ZSTD_isSkippableFrame(&rand), 0);
3850    }
3851
3852    #[test]
3853    fn skippableFrame_all_16_variants_roundtrip() {
3854        use crate::compress::zstd_compress::ZSTD_writeSkippableFrame;
3855        for variant in 0..16u32 {
3856            let payload = [variant as u8; 7];
3857            let mut buf = vec![0u8; 32];
3858            let w = ZSTD_writeSkippableFrame(&mut buf, &payload, variant);
3859            assert!(!crate::common::error::ERR_isError(w));
3860            let mut out = [0u8; 7];
3861            let mut got_variant = 0u32;
3862            let r = ZSTD_readSkippableFrame(&mut out, Some(&mut got_variant), &buf);
3863            assert_eq!(r, 7);
3864            assert_eq!(got_variant, variant);
3865            assert_eq!(&out, &payload);
3866        }
3867    }
3868
3869    #[test]
3870    fn readSkippableFrame_returns_payload_and_variant() {
3871        // Build: magic + userData=5 + 5 payload bytes.
3872        let mut src = Vec::new();
3873        src.extend_from_slice(&(ZSTD_MAGIC_SKIPPABLE_START + 3).to_le_bytes()); // variant 3
3874        src.extend_from_slice(&5u32.to_le_bytes());
3875        src.extend_from_slice(b"hello");
3876
3877        let mut dst = [0u8; 16];
3878        let mut variant: u32 = 0;
3879        let n = ZSTD_readSkippableFrame(&mut dst, Some(&mut variant), &src);
3880        assert_eq!(n, 5);
3881        assert_eq!(&dst[..5], b"hello");
3882        assert_eq!(variant, 3);
3883    }
3884
3885    #[test]
3886    fn writeSkippableFrame_rejects_error_paths() {
3887        // Exercise every documented error path: dst too small for
3888        // header+payload, variant > 15, and verify the happy-path
3889        // boundary (dst exactly = header+payload succeeds).
3890        use crate::compress::zstd_compress::ZSTD_writeSkippableFrame;
3891        let payload = b"abcd".as_slice();
3892        // dst too small for even the 8-byte header.
3893        let mut tiny = [0u8; 4];
3894        assert!(crate::common::error::ERR_isError(ZSTD_writeSkippableFrame(
3895            &mut tiny, payload, 0
3896        )));
3897        // dst too small for header + payload.
3898        let mut no_room = [0u8; 11];
3899        assert!(crate::common::error::ERR_isError(ZSTD_writeSkippableFrame(
3900            &mut no_room,
3901            payload,
3902            0
3903        )));
3904        // magicVariant > 15 is out of spec.
3905        let mut ok_buf = [0u8; 64];
3906        assert!(crate::common::error::ERR_isError(ZSTD_writeSkippableFrame(
3907            &mut ok_buf,
3908            payload,
3909            16
3910        )));
3911        // Exact-fit dst succeeds.
3912        let mut exact = [0u8; 8 + 4];
3913        let n = ZSTD_writeSkippableFrame(&mut exact, payload, 0);
3914        assert_eq!(n, 12);
3915    }
3916
3917    #[test]
3918    fn readSkippableFrame_rejects_non_skippable() {
3919        // Plain zstd frame magic should fail.
3920        let src = 0xFD2FB528u32.to_le_bytes();
3921        let mut dst = [0u8; 16];
3922        let rc = ZSTD_readSkippableFrame(&mut dst, None, &src);
3923        assert!(crate::common::error::ERR_isError(rc));
3924    }
3925
3926    #[test]
3927    fn readSkippableFrame_rejects_short_src_and_truncated_frame() {
3928        // Three error paths: (1) src shorter than the 8-byte header,
3929        // (2) claimed frame size > src.len() (truncated mid-payload).
3930        // The existing tests cover non-skippable + small-dst; this
3931        // rounds out coverage to all 4 distinct reject paths.
3932        let mut dst = [0u8; 32];
3933
3934        // (1) src too short for skippable header.
3935        let too_short = [0u8; 4];
3936        assert!(crate::common::error::ERR_isError(ZSTD_readSkippableFrame(
3937            &mut dst, None, &too_short
3938        )));
3939
3940        // (2) header claims 20-byte payload but src is truncated to
3941        //     header + 5 bytes.
3942        let mut truncated = Vec::new();
3943        truncated.extend_from_slice(&ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes());
3944        truncated.extend_from_slice(&20u32.to_le_bytes());
3945        truncated.extend_from_slice(&[0u8; 5]);
3946        assert!(crate::common::error::ERR_isError(ZSTD_readSkippableFrame(
3947            &mut dst, None, &truncated
3948        )));
3949    }
3950
3951    #[test]
3952    fn readSkippableFrame_rejects_small_dst() {
3953        let mut src = Vec::new();
3954        src.extend_from_slice(&ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes());
3955        src.extend_from_slice(&10u32.to_le_bytes());
3956        src.extend_from_slice(&[0u8; 10]);
3957        let mut dst = [0u8; 4]; // too small
3958        let rc = ZSTD_readSkippableFrame(&mut dst, None, &src);
3959        assert!(crate::common::error::ERR_isError(rc));
3960    }
3961
3962    #[test]
3963    fn find_frame_compressed_size_skippable() {
3964        let mut src = Vec::new();
3965        src.extend_from_slice(&ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes());
3966        src.extend_from_slice(&7u32.to_le_bytes());
3967        src.extend_from_slice(&[0u8; 7]);
3968        let sz = ZSTD_findFrameCompressedSize(&src);
3969        assert_eq!(sz, ZSTD_SKIPPABLEHEADERSIZE + 7);
3970    }
3971
3972    #[test]
3973    fn find_decompressed_size_sums_frames() {
3974        // Two back-to-back raw "HELLO" frames + one skippable → 10.
3975        let mut src = make_raw_hello_frame();
3976        src.extend_from_slice(&ZSTD_MAGIC_SKIPPABLE_START.to_le_bytes());
3977        src.extend_from_slice(&3u32.to_le_bytes());
3978        src.extend_from_slice(&[0u8; 3]);
3979        src.extend_from_slice(&make_raw_hello_frame());
3980        let total = ZSTD_findDecompressedSize(&src);
3981        assert_eq!(total, 10);
3982    }
3983
3984    #[test]
3985    fn decompress_frame_raw_block_roundtrip() {
3986        // Handcraft a minimal valid zstd frame with a single raw block.
3987        //   - Magic: ZSTD_MAGICNUMBER
3988        //   - FHD: singleSegment=1, fcsID=0 → byte 0x20. FCS is 1 byte.
3989        //   - FCS byte: 5 (declared frame content size)
3990        //   - Block header: lastBlock=1, bt_raw(=0), cSize=5 → (5<<3)|(0<<1)|1 = 0x29.
3991        //   - Raw payload: "HELLO"
3992        let mut src = Vec::new();
3993        src.extend_from_slice(&ZSTD_MAGICNUMBER.to_le_bytes());
3994        src.push(0x20);
3995        src.push(5);
3996        let bh = (5u32 << 3) | 1; // lastBlock=1, bt_raw=0, cSize=5
3997        src.push((bh & 0xFF) as u8);
3998        src.push(((bh >> 8) & 0xFF) as u8);
3999        src.push(((bh >> 16) & 0xFF) as u8);
4000        src.extend_from_slice(b"HELLO");
4001
4002        let mut dst = vec![0u8; 32];
4003        let out = ZSTD_decompress(&mut dst, &src);
4004        assert!(
4005            !crate::common::error::ERR_isError(out),
4006            "err: {}",
4007            crate::common::error::ERR_getErrorName(out)
4008        );
4009        assert_eq!(out, 5);
4010        assert_eq!(&dst[..5], b"HELLO");
4011    }
4012
4013    #[test]
4014    fn decompress_frame_rle_block_roundtrip() {
4015        // singleSegment frame, FCS=10, one RLE block of 10 bytes of 'Z'.
4016        // Block header: lastBlock=1, bt_rle(=1), origSize=10 →
4017        //   (10<<3)|(1<<1)|1 = 0x53.
4018        let mut src = Vec::new();
4019        src.extend_from_slice(&ZSTD_MAGICNUMBER.to_le_bytes());
4020        src.push(0x20); // FHD single-segment, no dict, FCS 1 byte
4021        src.push(10); // FCS
4022        let bh = (10u32 << 3) | (1 << 1) | 1;
4023        src.push((bh & 0xFF) as u8);
4024        src.push(((bh >> 8) & 0xFF) as u8);
4025        src.push(((bh >> 16) & 0xFF) as u8);
4026        src.push(b'Z');
4027
4028        let mut dst = vec![0u8; 32];
4029        let out = ZSTD_decompress(&mut dst, &src);
4030        assert!(!crate::common::error::ERR_isError(out));
4031        assert_eq!(out, 10);
4032        for b in dst.iter().take(10) {
4033            assert_eq!(*b, b'Z');
4034        }
4035    }
4036
4037    #[test]
4038    fn frame_header_reserved_bit_errors() {
4039        let mut src = Vec::new();
4040        src.extend_from_slice(&ZSTD_MAGICNUMBER.to_le_bytes());
4041        src.push(0x08); // reserved bit 3 set
4042        src.push(0);
4043        let mut zfh = ZSTD_FrameHeader::default();
4044        let rc = ZSTD_getFrameHeader(&mut zfh, &src);
4045        assert!(crate::common::error::ERR_isError(rc));
4046    }
4047}
4048
4049// ZSTD_DCtx lives in `zstd_decompress_block` (since most of its fields
4050// are literal/sequence decoder state that lands first). Re-export here
4051// so upstream symbol lookups resolve.
4052pub use crate::decompress::zstd_decompress_block::ZSTD_DCtx;
4053
4054/// Port of `ZSTD_createDCtx`. Rust port returns an owned `ZSTD_DCtx`
4055/// with default seq-tables pre-built.
4056pub fn ZSTD_createDCtx() -> Box<ZSTD_DCtx> {
4057    let mut d = ZSTD_createDCtx_internal(crate::compress::zstd_compress::ZSTD_customMem::default())
4058        .expect("default customMem allocation must succeed");
4059    crate::decompress::zstd_decompress_block::ZSTD_buildDefaultSeqTables(&mut d);
4060    d
4061}
4062
4063/// Port of `ZSTD_freeDCtx`. In the Rust port, dropping the Box frees.
4064pub fn ZSTD_freeDCtx(dctx: Box<ZSTD_DCtx>) -> usize {
4065    let customMem = dctx.customMem;
4066    unsafe {
4067        crate::compress::zstd_compress::ZSTD_customFreeBox(dctx, customMem);
4068    }
4069    0
4070}
4071
4072/// Port of `ZSTD_copyRawBlock` (raw-block passthrough).
4073pub fn ZSTD_copyRawBlock(dst: &mut [u8], src: &[u8]) -> usize {
4074    if src.len() > dst.len() {
4075        return ERROR(ErrorCode::DstSizeTooSmall);
4076    }
4077    dst[..src.len()].copy_from_slice(src);
4078    src.len()
4079}
4080
4081/// Port of `ZSTD_setRleBlock` (RLE-block expansion).
4082pub fn ZSTD_setRleBlock(dst: &mut [u8], b: u8, regenSize: usize) -> usize {
4083    if regenSize > dst.len() {
4084        return ERROR(ErrorCode::DstSizeTooSmall);
4085    }
4086    for d in dst[..regenSize].iter_mut() {
4087        *d = b;
4088    }
4089    regenSize
4090}
4091
4092/// Port of `ZSTD_decompressFrame` — the block-loop driver. Given a
4093/// fully-initialized `ZSTD_DCtx`, reads the frame header, iterates
4094/// blocks until `lastBlock`, handles RAW / RLE / compressed block
4095/// types, validates the frame checksum if present, and returns the
4096/// decompressed payload size.
4097///
4098/// Rust signature note: upstream mutates `*srcPtr` and `*srcSizePtr`
4099/// to advance the source cursor; we accept `src` by value and return
4100/// the tuple `(decoded_size, src_consumed)` via out-params.
4101/// Variant of `ZSTD_decompressFrame` that begins writing at
4102/// `dst[op_start..]` instead of `dst[0..]`. Used by
4103/// `ZSTD_decompress_usingDict` to decode into a buffer whose initial
4104/// `op_start` bytes hold the dict history. Returns the number of
4105/// decoded bytes (i.e., `final_op - op_start`).
4106#[allow(clippy::too_many_arguments)]
4107pub fn ZSTD_decompressFrame_withOpStart(
4108    dctx: &mut crate::decompress::zstd_decompress_block::ZSTD_DCtx,
4109    entropy_rep: &mut crate::decompress::zstd_decompress_block::ZSTD_decoder_entropy_rep,
4110    xxh: &mut crate::common::xxhash::XXH64_state_t,
4111    dst: &mut [u8],
4112    op_start: usize,
4113    src: &[u8],
4114    src_consumed: &mut usize,
4115) -> usize {
4116    use crate::common::xxhash::{XXH64_digest, XXH64_reset, XXH64_update};
4117    use crate::decompress::zstd_decompress_block::{
4118        blockProperties_t, blockType_e, streaming_operation, ZSTD_blockHeaderSize,
4119        ZSTD_decompressBlock_internal, ZSTD_getcBlockSize,
4120    };
4121    // Minimum bytes needed to read the first frame header. Upstream's
4122    // `ZSTD_FRAMEHEADERSIZE_MIN(format)` is 6 for zstd1, 2 for
4123    // magicless — the latter lets magicless-mode decoders succeed on
4124    // payloads that the zstd1 cap would reject as "too small".
4125    let frameheadersize_min = ZSTD_FRAMEHEADERSIZE_MIN(dctx.format);
4126    let mut ip: usize = 0;
4127    let mut op: usize = op_start;
4128    let mut remaining = src.len();
4129    if remaining < frameheadersize_min + ZSTD_blockHeaderSize {
4130        return ERROR(ErrorCode::SrcSizeWrong);
4131    }
4132    let mut zfh = ZSTD_FrameHeader::default();
4133    // Thread `dctx.format` so magicless frames parse correctly.
4134    let rc = ZSTD_getFrameHeader_advanced(&mut zfh, src, dctx.format);
4135    if crate::common::error::ERR_isError(rc) {
4136        return rc;
4137    }
4138    if rc != 0 {
4139        return ERROR(ErrorCode::SrcSizeWrong);
4140    }
4141    if zfh.frameType != ZSTD_FrameType_e::ZSTD_frame {
4142        return ERROR(ErrorCode::PrefixUnknown);
4143    }
4144    dctx.isFrameDecompression = 1;
4145    dctx.blockSizeMax = zfh.blockSizeMax as usize;
4146    let validateChecksum = zfh.checksumFlag != 0;
4147    if validateChecksum {
4148        XXH64_reset(xxh, 0);
4149    }
4150    ip += zfh.headerSize as usize;
4151    remaining -= zfh.headerSize as usize;
4152    loop {
4153        let mut bp = blockProperties_t {
4154            blockType: blockType_e::bt_raw,
4155            lastBlock: 0,
4156            origSize: 0,
4157        };
4158        let cBlockSize = ZSTD_getcBlockSize(&src[ip..], &mut bp);
4159        if crate::common::error::ERR_isError(cBlockSize) {
4160            return cBlockSize;
4161        }
4162        ip += ZSTD_blockHeaderSize;
4163        remaining -= ZSTD_blockHeaderSize;
4164        if cBlockSize > remaining {
4165            return ERROR(ErrorCode::SrcSizeWrong);
4166        }
4167        let decodedSize = match bp.blockType {
4168            blockType_e::bt_compressed => ZSTD_decompressBlock_internal(
4169                dctx,
4170                entropy_rep,
4171                dst,
4172                op,
4173                &src[ip..ip + cBlockSize],
4174                streaming_operation::not_streaming,
4175            ),
4176            blockType_e::bt_raw => ZSTD_copyRawBlock(&mut dst[op..], &src[ip..ip + cBlockSize]),
4177            blockType_e::bt_rle => ZSTD_setRleBlock(&mut dst[op..], src[ip], bp.origSize as usize),
4178            blockType_e::bt_reserved => return ERROR(ErrorCode::CorruptionDetected),
4179        };
4180        if crate::common::error::ERR_isError(decodedSize) {
4181            return decodedSize;
4182        }
4183        if validateChecksum && decodedSize > 0 {
4184            XXH64_update(xxh, &dst[op..op + decodedSize]);
4185        }
4186        op += decodedSize;
4187        ip += cBlockSize;
4188        remaining -= cBlockSize;
4189        if bp.lastBlock != 0 {
4190            break;
4191        }
4192    }
4193    let decoded = op - op_start;
4194    if zfh.frameContentSize != ZSTD_CONTENTSIZE_UNKNOWN && (decoded as u64) != zfh.frameContentSize
4195    {
4196        return ERROR(ErrorCode::CorruptionDetected);
4197    }
4198    if validateChecksum {
4199        if remaining < 4 {
4200            return ERROR(ErrorCode::ChecksumWrong);
4201        }
4202        let calc = XXH64_digest(xxh) as u32;
4203        let read = MEM_readLE32(&src[ip..ip + 4]);
4204        if calc != read {
4205            return ERROR(ErrorCode::ChecksumWrong);
4206        }
4207        ip += 4;
4208    }
4209    *src_consumed = ip;
4210    decoded
4211}
4212
4213pub fn ZSTD_decompressFrame(
4214    dctx: &mut crate::decompress::zstd_decompress_block::ZSTD_DCtx,
4215    entropy_rep: &mut crate::decompress::zstd_decompress_block::ZSTD_decoder_entropy_rep,
4216    xxh: &mut crate::common::xxhash::XXH64_state_t,
4217    dst: &mut [u8],
4218    src: &[u8],
4219    src_consumed: &mut usize,
4220) -> usize {
4221    use crate::common::xxhash::{XXH64_digest, XXH64_reset, XXH64_update};
4222    use crate::decompress::zstd_decompress_block::{
4223        blockProperties_t, blockType_e, streaming_operation, ZSTD_blockHeaderSize,
4224        ZSTD_decompressBlock_internal, ZSTD_getcBlockSize,
4225    };
4226
4227    let mut ip: usize = 0;
4228    let mut op: usize = 0;
4229    let mut remaining = src.len();
4230
4231    // Honor `dctx.format`: magicless frames skip the 4-byte magic,
4232    // so the minimum header is 2 bytes + block header, not 6+3.
4233    let frameHeaderSizeMin = ZSTD_FRAMEHEADERSIZE_MIN(dctx.format);
4234    if remaining < frameHeaderSizeMin + ZSTD_blockHeaderSize {
4235        return ERROR(ErrorCode::SrcSizeWrong);
4236    }
4237
4238    // Parse frame header.
4239    let mut zfh = ZSTD_FrameHeader::default();
4240    let rc = ZSTD_getFrameHeader_advanced(&mut zfh, src, dctx.format);
4241    if crate::common::error::ERR_isError(rc) {
4242        return rc;
4243    }
4244    if rc != 0 {
4245        // Header not complete — caller gave too few bytes.
4246        return ERROR(ErrorCode::SrcSizeWrong);
4247    }
4248    if zfh.frameType != ZSTD_FrameType_e::ZSTD_frame {
4249        // Skippable frames are valid but carry no payload — caller
4250        // should use a different entry point. We reject here.
4251        return ERROR(ErrorCode::PrefixUnknown);
4252    }
4253
4254    // Apply the frame header to the DCtx. Minimal shape for block decode.
4255    dctx.isFrameDecompression = 1;
4256    dctx.blockSizeMax = zfh.blockSizeMax as usize;
4257    let validateChecksum = zfh.checksumFlag != 0;
4258    if validateChecksum {
4259        XXH64_reset(xxh, 0);
4260    }
4261
4262    ip += zfh.headerSize as usize;
4263    remaining -= zfh.headerSize as usize;
4264
4265    loop {
4266        let mut bp = blockProperties_t {
4267            blockType: blockType_e::bt_raw,
4268            lastBlock: 0,
4269            origSize: 0,
4270        };
4271        let cBlockSize = ZSTD_getcBlockSize(&src[ip..], &mut bp);
4272        if crate::common::error::ERR_isError(cBlockSize) {
4273            return cBlockSize;
4274        }
4275        ip += ZSTD_blockHeaderSize;
4276        remaining -= ZSTD_blockHeaderSize;
4277        if cBlockSize > remaining {
4278            return ERROR(ErrorCode::SrcSizeWrong);
4279        }
4280
4281        let decodedSize = match bp.blockType {
4282            blockType_e::bt_compressed => ZSTD_decompressBlock_internal(
4283                dctx,
4284                entropy_rep,
4285                dst,
4286                op,
4287                &src[ip..ip + cBlockSize],
4288                streaming_operation::not_streaming,
4289            ),
4290            blockType_e::bt_raw => ZSTD_copyRawBlock(&mut dst[op..], &src[ip..ip + cBlockSize]),
4291            blockType_e::bt_rle => ZSTD_setRleBlock(&mut dst[op..], src[ip], bp.origSize as usize),
4292            blockType_e::bt_reserved => {
4293                return ERROR(ErrorCode::CorruptionDetected);
4294            }
4295        };
4296        if crate::common::error::ERR_isError(decodedSize) {
4297            return decodedSize;
4298        }
4299        if validateChecksum && decodedSize > 0 {
4300            XXH64_update(xxh, &dst[op..op + decodedSize]);
4301        }
4302        op += decodedSize;
4303        ip += cBlockSize;
4304        remaining -= cBlockSize;
4305        if bp.lastBlock != 0 {
4306            break;
4307        }
4308    }
4309
4310    // FrameContentSize check (if declared).
4311    if zfh.frameContentSize != ZSTD_CONTENTSIZE_UNKNOWN && (op as u64) != zfh.frameContentSize {
4312        return ERROR(ErrorCode::CorruptionDetected);
4313    }
4314
4315    // Checksum trailer.
4316    if validateChecksum {
4317        if remaining < 4 {
4318            return ERROR(ErrorCode::ChecksumWrong);
4319        }
4320        let calc = XXH64_digest(xxh) as u32;
4321        let read = MEM_readLE32(&src[ip..ip + 4]);
4322        if calc != read {
4323            return ERROR(ErrorCode::ChecksumWrong);
4324        }
4325        ip += 4;
4326    }
4327
4328    *src_consumed = ip;
4329    op
4330}
4331
4332/// Port of `ZSTD_decompressDCtx`. Loops over frames in `src` —
4333/// skipping over skippable frames, decoding each regular frame —
4334/// until the source is exhausted. Matches upstream's multi-frame
4335/// contract: callers with concatenated frames see all payloads
4336/// appended into `dst`.
4337pub fn ZSTD_decompressDCtx(
4338    dctx: &mut crate::decompress::zstd_decompress_block::ZSTD_DCtx,
4339    entropy_rep: &mut crate::decompress::zstd_decompress_block::ZSTD_decoder_entropy_rep,
4340    xxh: &mut crate::common::xxhash::XXH64_state_t,
4341    dst: &mut [u8],
4342    src: &[u8],
4343) -> usize {
4344    let ddict = ZSTD_getDDict(dctx);
4345    let mut ip = 0usize;
4346    let mut op = 0usize;
4347    while ip < src.len() {
4348        let rem = &src[ip..];
4349        // Skippable frame: advance past it without writing to dst.
4350        // Upstream (zstd_decompress.c:1120) only checks for skippable
4351        // magic when `dctx->format == ZSTD_f_zstd1` — magicless-mode
4352        // frames don't have a magic prefix, so the 4-byte window would
4353        // match arbitrary first bytes and misfire.
4354        if dctx.format == ZSTD_format_e::ZSTD_f_zstd1
4355            && rem.len() >= ZSTD_SKIPPABLEHEADERSIZE
4356            && (MEM_readLE32(&rem[..4]) & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START
4357        {
4358            let sz = readSkippableFrameSize(rem);
4359            if crate::common::error::ERR_isError(sz) {
4360                return sz;
4361            }
4362            ip += sz;
4363            continue;
4364        }
4365        // Regular frame: decode into dst[op..]. Upstream routes
4366        // through `ZSTD_decompress_usingDDict(dctx, ..., ZSTD_getDDict(dctx))`
4367        // so a dict loaded via `ZSTD_DCtx_loadDictionary` / `_refDDict`
4368        // gets applied here; previously we ignored `dctx.stream_dict`,
4369        // so `ZSTD_decompressDCtx` + loaded-dict paths silently
4370        // produced garbage for dict-compressed frames.
4371        if let Some(dict) = ddict.as_deref() {
4372            // Measure this frame's compressed footprint so we can
4373            // advance the input cursor past it. Use format-aware
4374            // variant so magicless-mode callers work.
4375            let frame_sz = ZSTD_findFrameCompressedSize_advanced(rem, dctx.format);
4376            if crate::common::error::ERR_isError(frame_sz) {
4377                return frame_sz;
4378            }
4379            let decoded = ZSTD_decompress_usingDict(dctx, &mut dst[op..], &rem[..frame_sz], dict);
4380            if crate::common::error::ERR_isError(decoded) {
4381                return decoded;
4382            }
4383            ip += frame_sz;
4384            op += decoded;
4385            continue;
4386        }
4387        let mut consumed = 0usize;
4388        let decoded =
4389            ZSTD_decompressFrame(dctx, entropy_rep, xxh, &mut dst[op..], rem, &mut consumed);
4390        if crate::common::error::ERR_isError(decoded) {
4391            return decoded;
4392        }
4393        ip += consumed;
4394        op += decoded;
4395    }
4396    op
4397}
4398
4399/// Port of upstream's one-shot `ZSTD_decompress` (`zstd.h:176`).
4400/// Decompresses a complete frame in `src` into `dst` and returns the
4401/// decoded byte count (or an `ErrorCode`-bearing return value if
4402/// `ZSTD_isError`). No dctx re-use, no streaming, no dict — the
4403/// simplest entry point. For context-managed, streaming, or
4404/// dict-bearing flows use `ZSTD_decompressDCtx` /
4405/// `ZSTD_decompressStream` / `ZSTD_decompress_usingDict` instead.
4406///
4407/// `dst` must be at least as large as the frame's declared content
4408/// size (`ZSTD_getFrameContentSize`); passing a short buffer returns
4409/// a `ZSTD_error_dstSize_tooSmall` error code.
4410pub fn ZSTD_decompress(dst: &mut [u8], src: &[u8]) -> usize {
4411    use crate::common::xxhash::XXH64_state_t;
4412    use crate::decompress::zstd_decompress_block::{
4413        ZSTD_DCtx, ZSTD_buildDefaultSeqTables, ZSTD_decoder_entropy_rep,
4414    };
4415    let mut dctx = ZSTD_DCtx::new();
4416    ZSTD_buildDefaultSeqTables(&mut dctx);
4417    let mut rep = ZSTD_decoder_entropy_rep::default();
4418    let mut xxh = XXH64_state_t::default();
4419    ZSTD_decompressDCtx(&mut dctx, &mut rep, &mut xxh, dst, src)
4420}
4421
4422/// Port of `ZSTD_decompress_usingDict`. Decompresses `src` using a
4423/// raw-content `dict` as history — sequences in `src` may reference
4424/// back into the dict. Upstream does this via ext-dict plumbing; our
4425/// cut-down approach concatenates `dict || scratch` and decodes into
4426/// the scratch portion starting at `op_start = dict.len()`. Back-refs
4427/// land naturally in the dict bytes.
4428///
4429/// v0.1 scope: raw-content dicts. Pre-digested `ZSTD_dct_fullDict`
4430/// dictionaries (with embedded HUF/FSE entropy tables) aren't
4431/// seeded on this path — callers needing magic-prefix entropy seeding
4432/// should use the DCtx-based flow (`ZSTD_DCtx_loadDictionary` +
4433/// `ZSTD_decompressDCtx`), which routes through
4434/// `ZSTD_decompress_insertDictionary` + `ZSTD_loadDEntropy`.
4435pub fn ZSTD_decompress_usingDict(
4436    dctx: &mut ZSTD_DCtx,
4437    dst: &mut [u8],
4438    src: &[u8],
4439    dict: &[u8],
4440) -> usize {
4441    use crate::common::error::ERR_isError;
4442    use crate::common::xxhash::XXH64_state_t;
4443    use crate::decompress::zstd_decompress_block::ZSTD_decoder_entropy_rep;
4444
4445    // Upstream (zstd_decompress.c / ZSTD_decompress_usingDict):
4446    // when both the frame header and the dict declare a dictID, they
4447    // must match — otherwise `DictionaryWrong`. Previously the Rust
4448    // port skipped this check, silently proceeding with a mismatched
4449    // dict which corrupted output.
4450    let frame_dict_id = ZSTD_getDictID_fromFrame(src);
4451    let dict_dict_id = crate::decompress::zstd_ddict::ZSTD_getDictID_fromDict(dict);
4452    if frame_dict_id != 0 && dict_dict_id != 0 && frame_dict_id != dict_dict_id {
4453        return ERROR(ErrorCode::DictionaryWrong);
4454    }
4455
4456    // Determine how much output we need.
4457    let declared = ZSTD_getFrameContentSize(src);
4458    let out_size = if declared == ZSTD_CONTENTSIZE_UNKNOWN || declared == ZSTD_CONTENTSIZE_ERROR {
4459        dst.len()
4460    } else {
4461        declared as usize
4462    };
4463    if out_size > dst.len() {
4464        return ERROR(ErrorCode::DstSizeTooSmall);
4465    }
4466
4467    // Combined buffer: dict || scratch. Decoder writes into scratch
4468    // starting at position dict.len().
4469    let mut combined = vec![0u8; dict.len() + out_size];
4470    combined[..dict.len()].copy_from_slice(dict);
4471
4472    // Reset the caller's dctx for a fresh frame decode. Upstream uses
4473    // the caller-owned dctx throughout; previously we allocated a
4474    // throwaway `ZSTD_DCtx::new()` and discarded it, which meant the
4475    // caller's dctx state was silently ignored after the call — a
4476    // faithful-translation gap.
4477    let rc = ZSTD_decompressBegin(dctx);
4478    if ERR_isError(rc) {
4479        return rc;
4480    }
4481    let mut rep = ZSTD_decoder_entropy_rep::default();
4482    let mut xxh = XXH64_state_t::default();
4483
4484    // Call the frame decoder with an op_start-like offset. Upstream
4485    // threads this via the DCtx's prefix pointer; we use the "dst
4486    // starts with dict" convention — requires a small tweak to
4487    // ZSTD_decompressFrame to honor a starting op offset.
4488    //
4489    // Easier path for v0.1: inline a single-frame decode that honors
4490    // an op-offset into the combined buffer.
4491    let mut consumed = 0usize;
4492    let decoded = ZSTD_decompressFrame_withOpStart(
4493        dctx,
4494        &mut rep,
4495        &mut xxh,
4496        &mut combined,
4497        dict.len(),
4498        src,
4499        &mut consumed,
4500    );
4501    if ERR_isError(decoded) {
4502        return decoded;
4503    }
4504    dst[..decoded].copy_from_slice(&combined[dict.len()..dict.len() + decoded]);
4505    decoded
4506}
4507
4508/// Port of `ZSTD_dParam_getBounds`. Returns the valid range for a
4509/// decompression parameter. Upstream: `[ZSTD_WINDOWLOG_ABSOLUTEMIN,
4510/// ZSTD_WINDOWLOG_MAX]` — 10..31 on 64-bit, 10..30 on 32-bit. The
4511/// default cap is `ZSTD_WINDOWLOG_LIMIT_DEFAULT` (27), not the
4512/// upper bound.
4513pub fn ZSTD_dParam_getBounds(
4514    param: ZSTD_dParameter,
4515) -> crate::compress::zstd_compress::ZSTD_bounds {
4516    match param {
4517        ZSTD_dParameter::ZSTD_d_windowLogMax => {
4518            let upper = if crate::common::mem::MEM_32bits() != 0 {
4519                ZSTD_WINDOWLOG_MAX_32 as i32
4520            } else {
4521                ZSTD_WINDOWLOG_MAX_64 as i32
4522            };
4523            crate::compress::zstd_compress::ZSTD_bounds {
4524                error: 0,
4525                lowerBound: 10,
4526                upperBound: upper,
4527            }
4528        }
4529        ZSTD_dParameter::ZSTD_d_format => crate::compress::zstd_compress::ZSTD_bounds {
4530            error: 0,
4531            lowerBound: ZSTD_format_e::ZSTD_f_zstd1 as i32,
4532            upperBound: ZSTD_format_e::ZSTD_f_zstd1_magicless as i32,
4533        },
4534    }
4535}
4536
4537/// Port of `ZSTD_DCtx_trace_end` (zstd_decompress.c:922). Upstream
4538/// emits a decompress-end trace event when `ZSTD_TRACE` is compiled
4539/// in. v0.1 has no tracing infrastructure — intentional no-op.
4540#[inline]
4541pub fn ZSTD_DCtx_trace_end(
4542    _dctx: &ZSTD_DCtx,
4543    _uncompressedSize: u64,
4544    _compressedSize: u64,
4545    _streaming: i32,
4546) {
4547}
4548
4549/// Port of `ZSTD_clearDict` (zstd_decompress.c:316). Drops any dict
4550/// linkage from the DCtx — semantically equivalent to `refDDict(NULL)`.
4551/// Our port's DCtx keeps the dict in `stream_dict`; upstream tracks
4552/// `ddictLocal` + `ddict` + `dictUses` separately.
4553pub fn ZSTD_clearDict(dctx: &mut ZSTD_DCtx) {
4554    dctx.stream_dict.clear();
4555    dctx.dictID = 0;
4556    dctx.ddict_rep = [0; 3];
4557    // litEntropy / fseEntropy are per-frame flags — ZSTD_decompressBegin
4558    // resets them on the next frame start, so leaving them here is
4559    // consistent with upstream (clearDict doesn't touch them either).
4560    // Reset the dict-lifecycle tracker so the next loadDictionary /
4561    // refPrefix / refDDict call starts from a clean slate.
4562    dctx.dictUses = ZSTD_dictUses_e::ZSTD_dont_use;
4563}
4564
4565pub const DDICT_HASHSET_MAX_LOAD_FACTOR_COUNT_MULT: usize = 4;
4566pub const DDICT_HASHSET_MAX_LOAD_FACTOR_SIZE_MULT: usize = 3;
4567pub const DDICT_HASHSET_TABLE_BASE_SIZE: usize = 64;
4568pub const DDICT_HASHSET_RESIZE_FACTOR: usize = 2;
4569
4570/// Port of upstream `ZSTD_DDictHashSet`: an open-addressed table of
4571/// borrowed DDict references keyed by `dictID`.
4572pub struct ZSTD_DDictHashSet<'a> {
4573    pub ddictPtrTable: Vec<Option<&'a crate::decompress::zstd_ddict::ZSTD_DDict>>,
4574    pub ddictPtrTableSize: usize,
4575    pub ddictPtrCount: usize,
4576}
4577
4578/// Port of `ZSTD_DDictHashSet_getIndex`.
4579pub fn ZSTD_DDictHashSet_getIndex(hashSet: &ZSTD_DDictHashSet<'_>, dictID: u32) -> usize {
4580    let hash = crate::common::xxhash::XXH64(&dictID.to_le_bytes(), 0);
4581    (hash as usize) & (hashSet.ddictPtrTableSize - 1)
4582}
4583
4584/// Port of `ZSTD_DDictHashSet_emplaceDDict`. Inserts without resizing,
4585/// replacing an existing DDict with the same dictID.
4586pub fn ZSTD_DDictHashSet_emplaceDDict<'a>(
4587    hashSet: &mut ZSTD_DDictHashSet<'a>,
4588    ddict: &'a crate::decompress::zstd_ddict::ZSTD_DDict,
4589) -> usize {
4590    use crate::decompress::zstd_ddict::ZSTD_getDictID_fromDDict;
4591
4592    let dictID = ZSTD_getDictID_fromDDict(ddict);
4593    let mut idx = ZSTD_DDictHashSet_getIndex(hashSet, dictID);
4594    let idxRangeMask = hashSet.ddictPtrTableSize - 1;
4595    if hashSet.ddictPtrCount == hashSet.ddictPtrTableSize {
4596        return ERROR(ErrorCode::Generic);
4597    }
4598    while let Some(existing) = hashSet.ddictPtrTable[idx] {
4599        if ZSTD_getDictID_fromDDict(existing) == dictID {
4600            hashSet.ddictPtrTable[idx] = Some(ddict);
4601            return 0;
4602        }
4603        idx = (idx + 1) & idxRangeMask;
4604    }
4605    hashSet.ddictPtrTable[idx] = Some(ddict);
4606    hashSet.ddictPtrCount += 1;
4607    0
4608}
4609
4610/// Port of `ZSTD_DDictHashSet_expand`.
4611pub fn ZSTD_DDictHashSet_expand<'a>(
4612    hashSet: &mut ZSTD_DDictHashSet<'a>,
4613    _customMem: crate::compress::zstd_compress::ZSTD_customMem,
4614) -> usize {
4615    let oldTable = core::mem::take(&mut hashSet.ddictPtrTable);
4616    hashSet.ddictPtrTableSize *= DDICT_HASHSET_RESIZE_FACTOR;
4617    hashSet.ddictPtrTable = vec![None; hashSet.ddictPtrTableSize];
4618    hashSet.ddictPtrCount = 0;
4619    for ddict in oldTable.into_iter().flatten() {
4620        let rc = ZSTD_DDictHashSet_emplaceDDict(hashSet, ddict);
4621        if crate::common::error::ERR_isError(rc) {
4622            return rc;
4623        }
4624    }
4625    0
4626}
4627
4628/// Port of `ZSTD_DDictHashSet_getDDict`.
4629pub fn ZSTD_DDictHashSet_getDDict<'a>(
4630    hashSet: &ZSTD_DDictHashSet<'a>,
4631    dictID: u32,
4632) -> Option<&'a crate::decompress::zstd_ddict::ZSTD_DDict> {
4633    use crate::decompress::zstd_ddict::ZSTD_getDictID_fromDDict;
4634
4635    let mut idx = ZSTD_DDictHashSet_getIndex(hashSet, dictID);
4636    let idxRangeMask = hashSet.ddictPtrTableSize - 1;
4637    loop {
4638        match hashSet.ddictPtrTable[idx] {
4639            Some(ddict) if ZSTD_getDictID_fromDDict(ddict) == dictID => return Some(ddict),
4640            Some(_) => idx = (idx + 1) & idxRangeMask,
4641            None => return None,
4642        }
4643    }
4644}
4645
4646/// Port of `ZSTD_createDDictHashSet`.
4647pub fn ZSTD_createDDictHashSet<'a>(
4648    _customMem: crate::compress::zstd_compress::ZSTD_customMem,
4649) -> ZSTD_DDictHashSet<'a> {
4650    ZSTD_DDictHashSet {
4651        ddictPtrTable: vec![None; DDICT_HASHSET_TABLE_BASE_SIZE],
4652        ddictPtrTableSize: DDICT_HASHSET_TABLE_BASE_SIZE,
4653        ddictPtrCount: 0,
4654    }
4655}
4656
4657/// Port of `ZSTD_DDictHashSet_addDDict`.
4658pub fn ZSTD_DDictHashSet_addDDict<'a>(
4659    hashSet: &mut ZSTD_DDictHashSet<'a>,
4660    ddict: &'a crate::decompress::zstd_ddict::ZSTD_DDict,
4661    customMem: crate::compress::zstd_compress::ZSTD_customMem,
4662) -> usize {
4663    if hashSet.ddictPtrCount * DDICT_HASHSET_MAX_LOAD_FACTOR_COUNT_MULT / hashSet.ddictPtrTableSize
4664        * DDICT_HASHSET_MAX_LOAD_FACTOR_SIZE_MULT
4665        != 0
4666    {
4667        let rc = ZSTD_DDictHashSet_expand(hashSet, customMem);
4668        if crate::common::error::ERR_isError(rc) {
4669            return rc;
4670        }
4671    }
4672    ZSTD_DDictHashSet_emplaceDDict(hashSet, ddict)
4673}
4674
4675/// Port of `ZSTD_DCtx_selectFrameDDict`.
4676///
4677/// Upstream reads `dctx->ddictSet` when `ZSTD_d_refMultipleDDicts` is
4678/// enabled. This Rust port keeps the hash set explicit to avoid
4679/// storing borrowed DDict references inside the long-lived `ZSTD_DCtx`;
4680/// when a frame dictID is found, the selected DDict parameters are
4681/// copied into the DCtx exactly like `ZSTD_DCtx_refDDict`.
4682pub fn ZSTD_DCtx_selectFrameDDict<'a>(
4683    dctx: &mut ZSTD_DCtx,
4684    hashSet: &ZSTD_DDictHashSet<'a>,
4685) -> Option<&'a crate::decompress::zstd_ddict::ZSTD_DDict> {
4686    let frameDDict = ZSTD_DDictHashSet_getDDict(hashSet, dctx.fParams.dictID)?;
4687    ZSTD_clearDict(dctx);
4688    dctx.dictID = dctx.fParams.dictID;
4689    crate::decompress::zstd_ddict::ZSTD_copyDDictParameters(dctx, frameDDict);
4690    dctx.stream_dict = crate::decompress::zstd_ddict::ZSTD_DDict_dictContent(frameDDict).to_vec();
4691    dctx.dictUses = ZSTD_dictUses_e::ZSTD_use_indefinitely;
4692    Some(frameDDict)
4693}
4694
4695/// Port of `ZSTD_getDDict` (`zstd_decompress.c:1180`).
4696///
4697/// Upstream returns the currently selected `ZSTD_DDict*`, while this
4698/// Rust port stores the active dictionary content bytes in
4699/// `dctx.stream_dict`. Return an owned snapshot so callers can both
4700/// consume the dictionary lifecycle and continue mutating the DCtx
4701/// during decompression.
4702pub fn ZSTD_getDDict(dctx: &mut ZSTD_DCtx) -> Option<Vec<u8>> {
4703    match dctx.dictUses {
4704        ZSTD_dictUses_e::ZSTD_dont_use => {
4705            ZSTD_clearDict(dctx);
4706            None
4707        }
4708        ZSTD_dictUses_e::ZSTD_use_indefinitely => {
4709            if dctx.stream_dict.is_empty() {
4710                None
4711            } else {
4712                Some(dctx.stream_dict.clone())
4713            }
4714        }
4715        ZSTD_dictUses_e::ZSTD_use_once => {
4716            let dict = if dctx.stream_dict.is_empty() {
4717                None
4718            } else {
4719                Some(dctx.stream_dict.clone())
4720            };
4721            ZSTD_clearDict(dctx);
4722            dict
4723        }
4724    }
4725}
4726
4727/// Port of `ZSTD_createDCtx_internal` (`zstd_decompress.c:294`).
4728/// Upstream mallocs a DCtx from the customMem allocator, sets
4729/// `customMem`, then calls `ZSTD_initDCtx_internal`. Rust port's
4730/// `Box::new(ZSTD_DCtx::default())` gives an already-initialized
4731/// DCtx; this helper adds the explicit init-internal call for
4732/// upstream-parity behavior on field resets.
4733pub fn ZSTD_createDCtx_internal(
4734    customMem: crate::compress::zstd_compress::ZSTD_customMem,
4735) -> Option<Box<ZSTD_DCtx>> {
4736    let mut dctx = unsafe {
4737        crate::compress::zstd_compress::ZSTD_customAllocBox(ZSTD_DCtx::new(), customMem)?
4738    };
4739    dctx.customMem = customMem;
4740    ZSTD_initDCtx_internal(&mut dctx);
4741    Some(dctx)
4742}
4743
4744/// Port of `ZSTD_initDCtx_internal` (`zstd_decompress.c:252`). Resets
4745/// a freshly-allocated DCtx to a known-good starting state — upstream
4746/// zeros ddict pointers + inBuff/outBuff + `streamStage = zdss_init`,
4747/// sets `isFrameDecompression = 1`, and finally calls
4748/// `ZSTD_DCtx_resetParameters` to install upstream defaults.
4749///
4750/// Our Rust port's `ZSTD_DCtx::new()` already initializes every field,
4751/// so this helper just calls the reset-parameters path — matching
4752/// upstream semantics (fresh DCtx → default params).
4753pub fn ZSTD_initDCtx_internal(dctx: &mut ZSTD_DCtx) {
4754    dctx.isFrameDecompression = 1;
4755    ZSTD_clearDict(dctx);
4756    ZSTD_DCtx_resetParameters(dctx);
4757}
4758
4759/// Port of `ZSTD_DCtx_resetParameters` (zstd_decompress.c:240).
4760/// Resets the DCtx's decoder-parameter slots back to upstream
4761/// defaults: `maxWindowSize = (1 << ZSTD_WINDOWLOG_LIMIT_DEFAULT)`,
4762/// leaving session state untouched. Several upstream DCtx fields
4763/// (format, outBufferMode, forceIgnoreChecksum, refMultipleDDicts,
4764/// disableHufAsm, maxBlockSizeParam) aren't tracked in v0.1 — the
4765/// ones we do carry are reset here.
4766pub fn ZSTD_DCtx_resetParameters(dctx: &mut ZSTD_DCtx) {
4767    dctx.d_windowLogMax = ZSTD_WINDOWLOG_LIMIT_DEFAULT;
4768    // Include `format` — now tracked as a parameter via the
4769    // `ZSTD_d_format` enum variant, so a parameter-reset must
4770    // restore it to the default zstd1 mode.
4771    dctx.format = ZSTD_format_e::ZSTD_f_zstd1;
4772    // The dict-lifecycle tracker is scoped to parameters — a
4773    // parameter-reset returns it to `ZSTD_dont_use` to stay
4774    // consistent with `ZSTD_DCtx_reset(parameters)`.
4775    dctx.dictUses = ZSTD_dictUses_e::ZSTD_dont_use;
4776}
4777
4778/// Port of `ZSTD_dParam_withinBounds` (zstd_decompress.c:1864).
4779/// Returns 1 if `value` falls within the param's bounds, 0 otherwise
4780/// (including the bounds-error case). Used by the setParameter path
4781/// to validate callers' inputs without returning an error code.
4782pub fn ZSTD_dParam_withinBounds(dParam: ZSTD_dParameter, value: i32) -> i32 {
4783    let bounds = ZSTD_dParam_getBounds(dParam);
4784    if crate::common::error::ERR_isError(bounds.error) {
4785        return 0;
4786    }
4787    if value < bounds.lowerBound || value > bounds.upperBound {
4788        return 0;
4789    }
4790    1
4791}
4792
4793/// Port of `ZSTD_DCtx_setMaxWindowSize`. Clamps `maxWindowSize` into
4794/// `[1 << min, 1 << max]` (taken from `ZSTD_d_windowLogMax`'s bounds)
4795/// and stores it on the DCtx for subsequent streaming decompressions
4796/// to enforce against frame headers.
4797pub fn ZSTD_DCtx_setMaxWindowSize(dctx: &mut ZSTD_DCtx, maxWindowSize: usize) -> usize {
4798    use crate::common::error::{ErrorCode, ERROR};
4799    // Upstream (zstd_decompress.c:1809) gates with
4800    // `streamStage != zdss_init → StageWrong`.
4801    if !dctx_is_in_init_stage(dctx) {
4802        return ERROR(ErrorCode::StageWrong);
4803    }
4804    let bounds = ZSTD_dParam_getBounds(ZSTD_dParameter::ZSTD_d_windowLogMax);
4805    let min = 1usize << bounds.lowerBound;
4806    let max = 1usize << bounds.upperBound;
4807    if maxWindowSize < min || maxWindowSize > max {
4808        return ERROR(ErrorCode::ParameterOutOfBound);
4809    }
4810    // We store windowLog (the log2) rather than bytes to match the
4811    // existing `d_windowLogMax` parameter slot. Use `highbit`
4812    // semantics (bits - leading_zeros - 1) instead of `trailing_zeros`
4813    // so a non-power-of-2 input picks the ceiling log2 rather than
4814    // collapsing to the absolute minimum — upstream stores
4815    // maxWindowSize verbatim so behaviorally the two are equivalent
4816    // for any value that's >= `1 << bounds.lowerBound`.
4817    dctx.d_windowLogMax = ((maxWindowSize.leading_zeros() as i32 ^ (usize::BITS as i32 - 1))
4818        as u32)
4819        .max(bounds.lowerBound as u32);
4820    0
4821}
4822
4823/// Port of `ZSTD_DCtx_setFormat` (`zstd_decompress.c:1816`). Thin
4824/// wrapper around `ZSTD_DCtx_setParameter(d_format, value)` — matches
4825/// upstream's single-parameter shape. The decoder respects
4826/// `dctx.format` when parsing frame headers via
4827/// `ZSTD_startingInputLength(dctx.format)` and
4828/// `ZSTD_getFrameHeader_advanced`.
4829pub fn ZSTD_DCtx_setFormat(
4830    dctx: &mut ZSTD_DCtx,
4831    format: crate::decompress::zstd_decompress::ZSTD_format_e,
4832) -> usize {
4833    // Upstream (zstd_decompress.c:1816) routes through
4834    // `ZSTD_DCtx_setParameter(ZSTD_d_format, value)` so the bounds
4835    // check + enum-cast happens in one place.
4836    ZSTD_DCtx_setParameter(dctx, ZSTD_dParameter::ZSTD_d_format, format as i32)
4837}
4838
4839/// Port of `ZSTD_DStream`. Upstream `typedef ZSTD_DCtx ZSTD_DStream`
4840/// — same struct for both APIs.
4841pub type ZSTD_DStream = ZSTD_DCtx;
4842
4843/// Port of `ZSTD_createDStream`. Alias for `ZSTD_createDCtx`.
4844pub fn ZSTD_createDStream() -> Option<Box<ZSTD_DStream>> {
4845    Some(ZSTD_createDCtx())
4846}
4847
4848/// Port of `ZSTD_freeDStream`. Alias for `ZSTD_freeDCtx`.
4849pub fn ZSTD_freeDStream(zds: Option<Box<ZSTD_DStream>>) -> usize {
4850    if let Some(zds) = zds {
4851        return ZSTD_freeDCtx(zds);
4852    }
4853    0
4854}
4855
4856/// Proxy for upstream's `dctx.streamStage == zdss_init` check —
4857/// returns true when no streaming decompression is in flight for the
4858/// current frame. Sibling of the compressor-side
4859/// `cctx_is_in_init_stage`.
4860#[inline]
4861fn dctx_is_in_init_stage(dctx: &ZSTD_DCtx) -> bool {
4862    dctx.stream_in_buffer.is_empty()
4863        && dctx.stream_out_buffer.is_empty()
4864        && dctx.stream_out_drained == 0
4865}
4866
4867/// Port of `ZSTD_DCtx_loadDictionary`. Configures the DCtx with a
4868/// dict — routes through `ZSTD_decompress_insertDictionary` which
4869/// handles both raw-content and magic-prefixed zstd-format dicts
4870/// (parsing the entropy tables in the latter case).
4871///
4872/// Upstream (zstd_decompress.c:1704) gates with
4873/// `streamStage != zdss_init → StageWrong` — swapping a dict mid-
4874/// stream would decouple the back-ref substrate from bytes already
4875/// in the DCtx's input buffer.
4876pub fn ZSTD_DCtx_loadDictionary(dctx: &mut ZSTD_DCtx, dict: &[u8]) -> usize {
4877    if !dctx_is_in_init_stage(dctx) {
4878        return ERROR(ErrorCode::StageWrong);
4879    }
4880    // Upstream (zstd_decompress.c:1710 → loadDictionary_advanced):
4881    // `ZSTD_clearDict` first, then install only when there's
4882    // actual content. Empty-dict calls become a pure clear.
4883    ZSTD_clearDict(dctx);
4884    if dict.is_empty() {
4885        return 0;
4886    }
4887    let rc = ZSTD_decompress_insertDictionary(dctx, dict);
4888    if crate::common::error::ERR_isError(rc) {
4889        return rc;
4890    }
4891    // Upstream (zstd_decompress.c:1711) marks loaded dicts as
4892    // `use_indefinitely` so they persist across subsequent frames.
4893    // refPrefix sets `use_once` below — the distinction matters for
4894    // the post-frame auto-clear path.
4895    dctx.dictUses = ZSTD_dictUses_e::ZSTD_use_indefinitely;
4896    0
4897}
4898
4899/// Port of `ZSTD_DCtx_refPrefix`. Prefixes are always raw content;
4900/// upstream clears dictID and doesn't parse magic. Marks the binding
4901/// as `ZSTD_use_once` — the next `decompressDCtx` / `decompressStream`
4902/// frame consumes the prefix and auto-clears it, matching upstream's
4903/// single-use `refPrefix` contract (`zstd_decompress.c:1728`). Same
4904/// stage gate as the other DCtx dict-family setters.
4905pub fn ZSTD_DCtx_refPrefix(dctx: &mut ZSTD_DCtx, prefix: &[u8]) -> usize {
4906    if !dctx_is_in_init_stage(dctx) {
4907        return ERROR(ErrorCode::StageWrong);
4908    }
4909    // Upstream (zstd_decompress.c:1725 → loadDictionary_advanced)
4910    // clears prior dict state before installing. Mirror so
4911    // `refPrefix(&[])` acts as "clear" rather than leaving
4912    // `dictUses = use_once` with an empty stream_dict.
4913    ZSTD_clearDict(dctx);
4914    if !prefix.is_empty() {
4915        dctx.stream_dict = prefix.to_vec();
4916        // `dictID` was just zeroed by `clearDict` — no explicit
4917        // re-write needed for the raw-content prefix path.
4918        // Upstream (zstd_decompress.c:1728) marks prefix-dict as
4919        // `use_once` so it's auto-cleared after the next frame decodes.
4920        dctx.dictUses = ZSTD_dictUses_e::ZSTD_use_once;
4921    }
4922    0
4923}
4924
4925/// Port of `ZSTD_DCtx_refPrefix_advanced`. Upstream extends the base
4926/// `refPrefix` with an explicit `ZSTD_dictContentType_e`; v0.1
4927/// treats all content types as raw.
4928pub fn ZSTD_DCtx_refPrefix_advanced(
4929    dctx: &mut ZSTD_DCtx,
4930    prefix: &[u8],
4931    _dictContentType: crate::decompress::zstd_ddict::ZSTD_dictContentType_e,
4932) -> usize {
4933    ZSTD_DCtx_refPrefix(dctx, prefix)
4934}
4935
4936/// Port of `ZSTD_DCtx_loadDictionary_advanced`. Forwards to the core
4937/// loader. The Rust port always copies the caller bytes and lets the
4938/// auto loader distinguish raw-content from magic-prefix dictionaries.
4939pub fn ZSTD_DCtx_loadDictionary_advanced(
4940    dctx: &mut ZSTD_DCtx,
4941    dict: &[u8],
4942    _dictLoadMethod: crate::decompress::zstd_ddict::ZSTD_dictLoadMethod_e,
4943    _dictContentType: crate::decompress::zstd_ddict::ZSTD_dictContentType_e,
4944) -> usize {
4945    ZSTD_DCtx_loadDictionary(dctx, dict)
4946}
4947
4948/// Port of `ZSTD_DCtx_loadDictionary_byReference`. Forwards to the
4949/// owning loader — v0.1 doesn't split by-ref from by-copy yet.
4950#[inline]
4951pub fn ZSTD_DCtx_loadDictionary_byReference(dctx: &mut ZSTD_DCtx, dict: &[u8]) -> usize {
4952    ZSTD_DCtx_loadDictionary(dctx, dict)
4953}
4954
4955/// Port of `ZSTD_DCtx_refDDict` (`zstd_decompress.c:1780`). Wires a
4956/// pre-built DDict into the DCtx. Upstream also clears any prior
4957/// dict state + tracks the DDict in a hash set for multi-dict
4958/// lookup; our simplified port copies the DDict parameters and raw
4959/// content into the DCtx.
4960pub fn ZSTD_DCtx_refDDict(
4961    dctx: &mut ZSTD_DCtx,
4962    ddict: &crate::decompress::zstd_ddict::ZSTD_DDict,
4963) -> usize {
4964    // Upstream (zstd_decompress.c:1782) gates on `streamStage == zdss_init`.
4965    if !dctx_is_in_init_stage(dctx) {
4966        return ERROR(ErrorCode::StageWrong);
4967    }
4968    ZSTD_clearDict(dctx);
4969    let content = crate::decompress::zstd_ddict::ZSTD_DDict_dictContent(ddict);
4970    if content.is_empty() {
4971        return 0;
4972    }
4973    crate::decompress::zstd_ddict::ZSTD_copyDDictParameters(dctx, ddict);
4974    // Upstream (zstd_decompress.c:1786) marks ref'd DDict as
4975    // `use_indefinitely`.
4976    dctx.dictUses = ZSTD_dictUses_e::ZSTD_use_indefinitely;
4977    0
4978}
4979
4980/// Port of `ZSTD_DStreamInSize`. Suggested input-buffer size for
4981/// streaming decompression. Upstream returns `ZSTD_BLOCKSIZE_MAX + 3`.
4982pub fn ZSTD_DStreamInSize() -> usize {
4983    use crate::decompress::zstd_decompress_block::{ZSTD_blockHeaderSize, ZSTD_BLOCKSIZE_MAX};
4984    // Upstream (zstd_decompress.c:1696):
4985    // `ZSTD_BLOCKSIZE_MAX + ZSTD_blockHeaderSize`.
4986    ZSTD_BLOCKSIZE_MAX + ZSTD_blockHeaderSize
4987}
4988
4989/// Port of `ZSTD_DStreamOutSize`. Suggested output-buffer size —
4990/// upstream returns `ZSTD_BLOCKSIZE_MAX`.
4991pub fn ZSTD_DStreamOutSize() -> usize {
4992    crate::decompress::zstd_decompress_block::ZSTD_BLOCKSIZE_MAX
4993}
4994
4995/// Port of `ZSTD_dParameter` — parametric decoder configuration.
4996/// Only the subset we honor is exposed; callers setting unsupported
4997/// ids get `ParameterUnsupported`.
4998#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4999#[repr(i32)]
5000pub enum ZSTD_dParameter {
5001    ZSTD_d_windowLogMax = 100,
5002    /// Upstream `ZSTD_d_format` = `ZSTD_d_experimentalParam1` (1000).
5003    /// Toggles between `ZSTD_f_zstd1` and `ZSTD_f_zstd1_magicless`.
5004    ZSTD_d_format = 1000,
5005}
5006
5007/// Port of `ZSTD_DCtx_setParameter`. For `windowLogMax` we record
5008/// the bound on the DCtx for potential upper-bound checks during
5009/// frame-header parsing. v0.1 just stashes the value; the enforcement
5010/// path lands alongside the ZSTD_windowLog_max limit check.
5011///
5012/// Upstream contract (lib/decompress/zstd_decompress.c:1910): if
5013/// `value == 0`, substitute `ZSTD_WINDOWLOG_LIMIT_DEFAULT` (27);
5014/// then bounds-check via `CHECK_DBOUNDS`; store on the DCtx. Mirrors
5015/// that behavior so C callers passing 0 get the documented default.
5016pub fn ZSTD_DCtx_setParameter(dctx: &mut ZSTD_DCtx, param: ZSTD_dParameter, value: i32) -> usize {
5017    use crate::common::error::{ErrorCode, ERROR};
5018    // Upstream (zstd_decompress.c:1908) unconditionally gates
5019    // `DCtx_setParameter` on `streamStage == zdss_init`. Unlike the
5020    // compressor side, there's no "authorized subset" — every param
5021    // must be set before streaming begins. Swapping format or
5022    // windowLogMax mid-stream would decouple the frame-header probe
5023    // from bytes already buffered.
5024    if !dctx_is_in_init_stage(dctx) {
5025        return ERROR(ErrorCode::StageWrong);
5026    }
5027    match param {
5028        ZSTD_dParameter::ZSTD_d_windowLogMax => {
5029            let effective = if value == 0 {
5030                ZSTD_WINDOWLOG_LIMIT_DEFAULT as i32
5031            } else {
5032                value
5033            };
5034            let bounds = ZSTD_dParam_getBounds(ZSTD_dParameter::ZSTD_d_windowLogMax);
5035            if effective < bounds.lowerBound || effective > bounds.upperBound {
5036                return ERROR(ErrorCode::ParameterOutOfBound);
5037            }
5038            dctx.d_windowLogMax = effective as u32;
5039            0
5040        }
5041        ZSTD_dParameter::ZSTD_d_format => {
5042            // Upstream (zstd_decompress.c:1915): bounds-check against
5043            // `[ZSTD_f_zstd1, ZSTD_f_zstd1_magicless]` then stash.
5044            let bounds = ZSTD_dParam_getBounds(ZSTD_dParameter::ZSTD_d_format);
5045            if value < bounds.lowerBound || value > bounds.upperBound {
5046                return ERROR(ErrorCode::ParameterOutOfBound);
5047            }
5048            dctx.format = match value {
5049                v if v == ZSTD_format_e::ZSTD_f_zstd1_magicless as i32 => {
5050                    ZSTD_format_e::ZSTD_f_zstd1_magicless
5051                }
5052                _ => ZSTD_format_e::ZSTD_f_zstd1,
5053            };
5054            0
5055        }
5056    }
5057}
5058
5059/// Port of `ZSTD_WINDOWLOG_LIMIT_DEFAULT` — the streaming decoder's
5060/// conservative default cap (128 MB). Distinct from `ZSTD_WINDOWLOG_MAX`
5061/// which is the absolute-max permissible via `setParameter`.
5062pub const ZSTD_WINDOWLOG_LIMIT_DEFAULT: u32 = 27;
5063
5064/// Port of `ZSTD_DCtx_getParameter`.
5065pub fn ZSTD_DCtx_getParameter(dctx: &ZSTD_DCtx, param: ZSTD_dParameter, value: &mut i32) -> usize {
5066    *value = match param {
5067        ZSTD_dParameter::ZSTD_d_windowLogMax => dctx.d_windowLogMax as i32,
5068        ZSTD_dParameter::ZSTD_d_format => dctx.format as i32,
5069    };
5070    0
5071}
5072
5073/// Port of `ZSTD_DCtx_reset`. Matches upstream's three modes: clear
5074/// per-frame streaming state on `session_only`, restore default
5075/// parameters + drop the configured dict on `parameters`, do both
5076/// on `session_and_parameters`.
5077pub fn ZSTD_DCtx_reset(dctx: &mut ZSTD_DCtx, reset: ZSTD_DResetDirective) -> usize {
5078    let clear_session = matches!(
5079        reset,
5080        ZSTD_DResetDirective::ZSTD_reset_session_only
5081            | ZSTD_DResetDirective::ZSTD_reset_session_and_parameters,
5082    );
5083    let clear_params = matches!(
5084        reset,
5085        ZSTD_DResetDirective::ZSTD_reset_parameters
5086            | ZSTD_DResetDirective::ZSTD_reset_session_and_parameters,
5087    );
5088    // Upstream (zstd_decompress.c:1958) gates a pure params-reset on
5089    // `streamStage == zdss_init`. The combined variant clears session
5090    // first, so the gate only fires for `reset_parameters` alone.
5091    if clear_params && !clear_session && !dctx_is_in_init_stage(dctx) {
5092        return ERROR(ErrorCode::StageWrong);
5093    }
5094    if clear_session {
5095        ZSTD_resetDStream(dctx);
5096    }
5097    if clear_params {
5098        // Upstream (zstd_decompress.c:1958) routes through
5099        // `ZSTD_clearDict` + `ZSTD_DCtx_resetParameters` so every
5100        // dict- and parameter-related slot is wiped uniformly. We
5101        // delegate to the same pair for parity — this also clears
5102        // `dictID`, `ddict_rep`, and `dictUses` that a direct
5103        // field-by-field reset would miss.
5104        ZSTD_clearDict(dctx);
5105        ZSTD_DCtx_resetParameters(dctx);
5106    }
5107    0
5108}
5109
5110/// Port of `ZSTD_ResetDirective` (decoder-side alias).
5111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5112#[repr(i32)]
5113pub enum ZSTD_DResetDirective {
5114    // Upstream aliases this to the same `ZSTD_ResetDirective` enum
5115    // (zstd.h:589), values 1/2/3. Pin them explicitly so FFI bridges
5116    // passing raw integers route correctly.
5117    ZSTD_reset_session_only = 1,
5118    ZSTD_reset_parameters = 2,
5119    ZSTD_reset_session_and_parameters = 3,
5120}
5121
5122/// Port of `ZSTD_sizeof_DCtx`. Walks the DCtx's owned `Vec`s.
5123pub fn ZSTD_sizeof_DCtx(dctx: &ZSTD_DCtx) -> usize {
5124    core::mem::size_of::<ZSTD_DCtx>()
5125        + dctx.hufTable.capacity() * core::mem::size_of::<u32>()
5126        + dctx.workspace.capacity() * core::mem::size_of::<u32>()
5127        + dctx.litExtraBuffer.capacity()
5128        + dctx.LLTable.capacity()
5129            * core::mem::size_of::<crate::decompress::zstd_decompress_block::ZSTD_seqSymbol>()
5130        + dctx.OFTable.capacity()
5131            * core::mem::size_of::<crate::decompress::zstd_decompress_block::ZSTD_seqSymbol>()
5132        + dctx.MLTable.capacity()
5133            * core::mem::size_of::<crate::decompress::zstd_decompress_block::ZSTD_seqSymbol>()
5134        + dctx.stream_in_buffer.capacity()
5135        + dctx.stream_out_buffer.capacity()
5136        + dctx.stream_dict.capacity()
5137}
5138
5139/// Port of `ZSTD_sizeof_DStream`. Alias.
5140pub fn ZSTD_sizeof_DStream(zds: &ZSTD_DStream) -> usize {
5141    ZSTD_sizeof_DCtx(zds)
5142}
5143
5144/// Port of `ZSTD_estimateDCtxSize` (`zstd_decompress.c:229`) — upstream
5145/// returns a single `sizeof(ZSTD_DCtx)`. All the HUF / FSE / litbuffer
5146/// tables live inside the upstream DCtx struct as arrays; in our port
5147/// they're `Vec` fields, so this constant undercount-s the true heap
5148/// footprint. Use `ZSTD_sizeof_DCtx(&dctx)` on a live context to get
5149/// the Rust-accurate total including owned allocations.
5150pub fn ZSTD_estimateDCtxSize() -> usize {
5151    core::mem::size_of::<ZSTD_DCtx>()
5152}
5153
5154/// Port of `ZSTD_estimateDStreamSize` (`zstd_decompress.c:1993`).
5155/// Returns `DCtxSize + inBuffSize + outBuffSize` where
5156/// `inBuffSize = min(windowSize, BLOCKSIZE_MAX)` and `outBuffSize` is
5157/// the ring-buffer size needed for an unknown-content-size frame at
5158/// this window.
5159pub fn ZSTD_estimateDStreamSize(windowSize: usize) -> usize {
5160    use crate::decompress::zstd_decompress_block::ZSTD_BLOCKSIZE_MAX;
5161    let blockSize = windowSize.min(ZSTD_BLOCKSIZE_MAX);
5162    let inBuffSize = blockSize;
5163    let outBuffSize = ZSTD_decodingBufferSize_min(windowSize as u64, ZSTD_CONTENTSIZE_UNKNOWN);
5164    ZSTD_estimateDCtxSize() + inBuffSize + outBuffSize
5165}
5166
5167/// Port of `ZSTD_estimateDStreamSize_fromFrame`. Parses the frame
5168/// header to extract windowSize, then calls `ZSTD_estimateDStreamSize`.
5169pub fn ZSTD_estimateDStreamSize_fromFrame(src: &[u8]) -> usize {
5170    let mut zfh = ZSTD_FrameHeader::default();
5171    let rc = ZSTD_getFrameHeader(&mut zfh, src);
5172    if crate::common::error::ERR_isError(rc) || rc != 0 {
5173        return ZSTD_estimateDStreamSize(1 << 17); // default block-size estimate
5174    }
5175    ZSTD_estimateDStreamSize(zfh.windowSize as usize)
5176}
5177
5178/// Port of `ZSTD_insertBlock` (`zstd_decompress.c:887`). Inserts
5179/// `block` as raw history into the DCtx — useful when a frameless
5180/// protocol tracks uncompressed blocks alongside compressed ones.
5181pub fn ZSTD_insertBlock(dctx: &mut ZSTD_DCtx, block: &[u8]) -> usize {
5182    crate::decompress::zstd_decompress_block::ZSTD_checkContinuity(dctx, block, block.len());
5183    dctx.previousDstEnd = Some(block.as_ptr() as usize + block.len());
5184    block.len()
5185}
5186
5187/// Port of the public `ZSTD_decompressBlock`. Decompresses a single
5188/// block body WITHOUT any frame header — `src` starts at the block
5189/// data (literals + sequences), as emitted by `ZSTD_compressBlock`.
5190/// Only meaningful for callers building frameless protocols.
5191///
5192/// Rust signature: `src` is the block body bytes. Returns the number
5193/// of decoded bytes written into `dst`, or an error code.
5194pub fn ZSTD_decompressBlock(dctx: &mut ZSTD_DCtx, dst: &mut [u8], src: &[u8]) -> usize {
5195    use crate::decompress::zstd_decompress_block::{
5196        streaming_operation, ZSTD_buildDefaultSeqTables, ZSTD_decoder_entropy_rep,
5197        ZSTD_decompressBlock_internal,
5198    };
5199    ZSTD_buildDefaultSeqTables(dctx);
5200    let mut entropy_rep = ZSTD_decoder_entropy_rep::default();
5201    ZSTD_decompressBlock_internal(
5202        dctx,
5203        &mut entropy_rep,
5204        dst,
5205        0,
5206        src,
5207        streaming_operation::not_streaming,
5208    )
5209}
5210
5211/// Port of `ZSTD_decompressBlock_deprecated` (zstd_decompress_block.c:2291).
5212/// Upstream kept the legacy-name entry alongside the current
5213/// `ZSTD_decompressBlock`; both do the same work. Forwards to the
5214/// current entry.
5215pub fn ZSTD_decompressBlock_deprecated(dctx: &mut ZSTD_DCtx, dst: &mut [u8], src: &[u8]) -> usize {
5216    ZSTD_decompressBlock(dctx, dst, src)
5217}
5218
5219/// Port of `ZSTD_getBlockSize`. Returns the maximum block size the
5220/// DCtx will accept — `ZSTD_BLOCKSIZE_MAX` unless a frame header
5221/// narrowed it.
5222pub fn ZSTD_getBlockSize(_dctx: &ZSTD_DCtx) -> usize {
5223    crate::decompress::zstd_decompress_block::ZSTD_BLOCKSIZE_MAX
5224}
5225
5226/// Port of `ZSTD_isFrame`. Returns 1 if `src` starts with a zstd
5227/// magic number (regular or skippable frame), 0 otherwise. Cheap —
5228/// only reads the first 4 bytes.
5229pub fn ZSTD_isFrame(src: &[u8]) -> u32 {
5230    if src.len() < ZSTD_FRAMEIDSIZE {
5231        return 0;
5232    }
5233    let magic = MEM_readLE32(&src[..4]);
5234    if magic == ZSTD_MAGICNUMBER {
5235        return 1;
5236    }
5237    if (magic & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START {
5238        return 1;
5239    }
5240    0
5241}
5242
5243/// Port of `ZSTD_isSkippableFrame`. Returns 1 if `src` starts with
5244/// one of the 16 skippable-frame magic variants (0x184D2A5X).
5245pub fn ZSTD_isSkippableFrame(src: &[u8]) -> u32 {
5246    if src.len() < ZSTD_FRAMEIDSIZE {
5247        return 0;
5248    }
5249    let magic = MEM_readLE32(&src[..4]);
5250    if (magic & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START {
5251        return 1;
5252    }
5253    0
5254}
5255
5256/// Port of `ZSTD_decodeFrameHeader` (`zstd_decompress.c:698`).
5257/// `headerSize` must be the exact size returned by
5258/// `ZSTD_frameHeaderSize_internal`. Populates `dctx.fParams`, checks
5259/// dictionary compatibility, initializes checksum state, and accounts
5260/// for consumed compressed header bytes.
5261pub fn ZSTD_decodeFrameHeader(dctx: &mut ZSTD_DCtx, src: &[u8], headerSize: usize) -> usize {
5262    use crate::common::xxhash::XXH64_reset;
5263
5264    if src.len() < headerSize {
5265        return ERROR(ErrorCode::SrcSizeWrong);
5266    }
5267    let result = ZSTD_getFrameHeader_advanced(&mut dctx.fParams, &src[..headerSize], dctx.format);
5268    if crate::common::error::ERR_isError(result) {
5269        return result;
5270    }
5271    if result != 0 {
5272        return ERROR(ErrorCode::SrcSizeWrong);
5273    }
5274    if dctx.fParams.dictID != 0 && dctx.dictID != dctx.fParams.dictID {
5275        return ERROR(ErrorCode::DictionaryWrong);
5276    }
5277    dctx.isFrameDecompression = 1;
5278    dctx.blockSizeMax = dctx.fParams.blockSizeMax as usize;
5279    dctx.validateChecksum = dctx.fParams.checksumFlag;
5280    if dctx.validateChecksum != 0 {
5281        XXH64_reset(&mut dctx.xxhState, 0);
5282    }
5283    dctx.processedCSize = dctx.processedCSize.wrapping_add(headerSize as u64);
5284    0
5285}
5286
5287/// Port of `ZSTD_readSkippableFrame`. Copies a skippable frame's
5288/// payload into `dst` and optionally reports the low-nibble "magic
5289/// variant" the frame was tagged with.
5290///
5291/// Returns the number of payload bytes written, or a zstd error
5292/// code (dst too small, bad magic, etc).
5293pub fn ZSTD_readSkippableFrame(
5294    dst: &mut [u8],
5295    magicVariant: Option<&mut u32>,
5296    src: &[u8],
5297) -> usize {
5298    use crate::common::error::{ErrorCode, ERROR};
5299    if src.len() < ZSTD_SKIPPABLEHEADERSIZE {
5300        return ERROR(ErrorCode::SrcSizeWrong);
5301    }
5302    let magicNumber = MEM_readLE32(&src[..4]);
5303    let skippableFrameSize = readSkippableFrameSize(src);
5304    if ZSTD_isSkippableFrame(src) == 0 {
5305        return ERROR(ErrorCode::FrameParameterUnsupported);
5306    }
5307    if skippableFrameSize < ZSTD_SKIPPABLEHEADERSIZE || skippableFrameSize > src.len() {
5308        return ERROR(ErrorCode::SrcSizeWrong);
5309    }
5310    let skippableContentSize = skippableFrameSize - ZSTD_SKIPPABLEHEADERSIZE;
5311    if skippableContentSize > dst.len() {
5312        return ERROR(ErrorCode::DstSizeTooSmall);
5313    }
5314    if skippableContentSize > 0 {
5315        dst[..skippableContentSize].copy_from_slice(
5316            &src[ZSTD_SKIPPABLEHEADERSIZE..ZSTD_SKIPPABLEHEADERSIZE + skippableContentSize],
5317        );
5318    }
5319    if let Some(v) = magicVariant {
5320        *v = magicNumber - ZSTD_MAGIC_SKIPPABLE_START;
5321    }
5322    skippableContentSize
5323}
5324
5325/// Port of `ZSTD_getDictID_fromFrame`. Returns the dictID the frame
5326/// was compressed with, or 0 if the frame doesn't declare one or the
5327/// header can't be parsed.
5328pub fn ZSTD_getDictID_fromFrame(src: &[u8]) -> u32 {
5329    let mut zfh = ZSTD_FrameHeader::default();
5330    let rc = ZSTD_getFrameHeader(&mut zfh, src);
5331    if crate::common::error::ERR_isError(rc) || rc != 0 {
5332        return 0;
5333    }
5334    zfh.dictID
5335}
5336
5337/// Port of `ZSTD_decompress_usingDDict`. Decompresses `src` using a
5338/// pre-digested decompression dictionary. Uses the DDict's raw
5339/// content as history and seeds serialized dictionary entropy when
5340/// present.
5341pub fn ZSTD_decompress_usingDDict(
5342    dctx: &mut ZSTD_DCtx,
5343    dst: &mut [u8],
5344    src: &[u8],
5345    ddict: &crate::decompress::zstd_ddict::ZSTD_DDict,
5346) -> usize {
5347    use crate::common::error::ERR_isError;
5348    use crate::common::xxhash::XXH64_state_t;
5349    use crate::decompress::zstd_decompress_block::ZSTD_decoder_entropy_rep;
5350
5351    let frame_dict_id = ZSTD_getDictID_fromFrame(src);
5352    if frame_dict_id != 0 && ddict.dictID != 0 && frame_dict_id != ddict.dictID {
5353        return ERROR(ErrorCode::DictionaryWrong);
5354    }
5355
5356    let content = crate::decompress::zstd_ddict::ZSTD_DDict_dictContent(ddict);
5357    let declared = ZSTD_getFrameContentSize(src);
5358    let out_size = if declared == ZSTD_CONTENTSIZE_UNKNOWN || declared == ZSTD_CONTENTSIZE_ERROR {
5359        dst.len()
5360    } else {
5361        declared as usize
5362    };
5363    if out_size > dst.len() {
5364        return ERROR(ErrorCode::DstSizeTooSmall);
5365    }
5366
5367    let rc = ZSTD_decompressBegin(dctx);
5368    if ERR_isError(rc) {
5369        return rc;
5370    }
5371    crate::decompress::zstd_ddict::ZSTD_copyDDictParameters(dctx, ddict);
5372
5373    let mut combined = vec![0u8; content.len() + out_size];
5374    combined[..content.len()].copy_from_slice(content);
5375    let mut rep = ZSTD_decoder_entropy_rep {
5376        rep: dctx.ddict_rep,
5377    };
5378    let mut xxh = XXH64_state_t::default();
5379    let mut consumed = 0usize;
5380    let decoded = ZSTD_decompressFrame_withOpStart(
5381        dctx,
5382        &mut rep,
5383        &mut xxh,
5384        &mut combined,
5385        content.len(),
5386        src,
5387        &mut consumed,
5388    );
5389    if ERR_isError(decoded) {
5390        return decoded;
5391    }
5392    dst[..decoded].copy_from_slice(&combined[content.len()..content.len() + decoded]);
5393    decoded
5394}
5395
5396/// Port of `ZSTD_decompress_insertDictionary` (`zstd_decompress.c:1539`).
5397/// Configures the DCtx with a dict. Three paths:
5398///   - `dictSize < 8` → raw content dict, no magic.
5399///   - No magic prefix → raw content dict (`ZSTD_dct_auto` behavior).
5400///   - `ZSTD_MAGIC_DICTIONARY` prefix → parse dictID (skip 4-byte magic),
5401///     run `ZSTD_loadDEntropy` to populate HUF + FSE tables + rep[],
5402///     stash remaining bytes as the dict content.
5403///
5404/// Our port keeps the content on `stream_dict` (the concatenate-with-
5405/// src path `ZSTD_decompress_usingDict` uses). `litEntropy` and
5406/// `fseEntropy` flags are set to 1 when entropy tables were loaded,
5407/// so downstream `ZSTD_buildSeqTable` can honor set_repeat.
5408pub fn ZSTD_decompress_insertDictionary(dctx: &mut ZSTD_DCtx, dict: &[u8]) -> usize {
5409    use crate::common::error::{ERR_isError, ErrorCode, ERROR};
5410    use crate::common::mem::MEM_readLE32;
5411
5412    // Raw content path: too small or no magic → just stash bytes.
5413    if dict.len() < 8 {
5414        dctx.dictID = 0;
5415        return ZSTD_refDictContent(dctx, dict);
5416    }
5417    let magic = MEM_readLE32(&dict[..4]);
5418    if magic != ZSTD_MAGICNUMBER_DICTIONARY {
5419        dctx.dictID = 0;
5420        return ZSTD_refDictContent(dctx, dict);
5421    }
5422
5423    // Magic-prefixed zstd dict: parse dictID, entropy, rep, content.
5424    dctx.dictID = MEM_readLE32(&dict[4..8]);
5425    let mut rep = [0u32; 3];
5426    let eSize = ZSTD_loadDEntropy(dctx, &mut rep, dict);
5427    if ERR_isError(eSize) {
5428        return ERROR(ErrorCode::DictionaryCorrupted);
5429    }
5430    dctx.ddict_rep = rep;
5431    dctx.litEntropy = 1;
5432    dctx.fseEntropy = 1;
5433    // Content starts after the entropy-tables region.
5434    ZSTD_refDictContent(dctx, &dict[eSize..])
5435}
5436
5437/// Port of `ZSTD_refDictContent` (`zstd_decompress.c:1435`).
5438///
5439/// C keeps raw pointers into caller-owned dictionary bytes. The Rust
5440/// port records pointer-equivalent addresses for continuity tests and
5441/// also copies the bytes into `stream_dict`, which is the owned backing
5442/// used by the safe dictionary decompression paths.
5443pub fn ZSTD_refDictContent(dctx: &mut ZSTD_DCtx, dict: &[u8]) -> usize {
5444    let dict_start = dict.as_ptr() as usize;
5445    let dict_end = dict_start + dict.len();
5446    let previous_dst_end = dctx.previousDstEnd.unwrap_or(0);
5447    let prefix_start = dctx.prefixStart.unwrap_or(previous_dst_end);
5448
5449    dctx.dictEnd = dctx.previousDstEnd;
5450    dctx.virtualStart = Some(dict_start.wrapping_sub(previous_dst_end.wrapping_sub(prefix_start)));
5451    dctx.prefixStart = Some(dict_start);
5452    dctx.previousDstEnd = Some(dict_end);
5453    dctx.stream_dict = dict.to_vec();
5454    0
5455}
5456
5457/// Upstream `ZSTD_MAGIC_DICTIONARY` (`zstd.h:143`). 0xEC30A437 —
5458/// marks a zstd-format dictionary (vs raw-content).
5459pub const ZSTD_MAGICNUMBER_DICTIONARY: u32 = 0xEC30A437;
5460
5461/// Port of `ZSTD_loadDEntropy` (`zstd_decompress.c:1451`). Parses the
5462/// entropy-tables section of a zstd-format dictionary into the DCtx's
5463/// HUF + FSE tables.
5464///
5465/// Layout (post-magic+dictID): HUF DTable → FSE OF table → FSE ML
5466/// table → FSE LL table → 3 × u32 rep values. Returns total bytes
5467/// consumed (up to and including the rep values), or a
5468/// `DictionaryCorrupted` error.
5469///
5470/// Caller provides `repOut` for the 3 repcodes; the dict content
5471/// follows the consumed region.
5472pub fn ZSTD_loadDEntropy(dctx: &mut ZSTD_DCtx, repOut: &mut [u32; 3], dict: &[u8]) -> usize {
5473    use crate::common::error::{ERR_isError, ErrorCode, ERROR};
5474    use crate::common::mem::MEM_readLE32;
5475    use crate::decompress::huf_decompress::HUF_readDTableX2;
5476    use crate::decompress::zstd_decompress_block::{
5477        LLFSELog, LL_base, LL_bits, MLFSELog, ML_base, ML_bits, MaxLL, MaxML, MaxOff, OF_base,
5478        OF_bits, OffFSELog, ZSTD_buildFSETable,
5479    };
5480
5481    if dict.len() <= 8 {
5482        return ERROR(ErrorCode::DictionaryCorrupted);
5483    }
5484    let mut pos = 8usize; // skip magic + dictID
5485
5486    // --- HUF DTable ---
5487    let mut hufWorkspace = vec![0u32; 1024];
5488    let hSize = HUF_readDTableX2(&mut dctx.hufTable, &dict[pos..], &mut hufWorkspace, 0);
5489    if ERR_isError(hSize) {
5490        return ERROR(ErrorCode::DictionaryCorrupted);
5491    }
5492    pos += hSize;
5493
5494    // --- FSE OF table ---
5495    let mut ofcNCount = [0i16; (MaxOff + 1) as usize];
5496    let mut ofcMaxValue: u32 = MaxOff;
5497    let mut ofcLog: u32 = 0;
5498    let ofcSize = crate::common::entropy_common::FSE_readNCount(
5499        &mut ofcNCount,
5500        &mut ofcMaxValue,
5501        &mut ofcLog,
5502        &dict[pos..],
5503    );
5504    if ERR_isError(ofcSize) || ofcMaxValue > MaxOff || ofcLog > OffFSELog {
5505        return ERROR(ErrorCode::DictionaryCorrupted);
5506    }
5507    ZSTD_buildFSETable(
5508        &mut dctx.OFTable,
5509        &ofcNCount,
5510        ofcMaxValue,
5511        &OF_base,
5512        &OF_bits,
5513        ofcLog,
5514    );
5515    pos += ofcSize;
5516
5517    // --- FSE ML table ---
5518    let mut mlNCount = [0i16; (MaxML + 1) as usize];
5519    let mut mlMaxValue: u32 = MaxML;
5520    let mut mlLog: u32 = 0;
5521    let mlSize = crate::common::entropy_common::FSE_readNCount(
5522        &mut mlNCount,
5523        &mut mlMaxValue,
5524        &mut mlLog,
5525        &dict[pos..],
5526    );
5527    if ERR_isError(mlSize) || mlMaxValue > MaxML || mlLog > MLFSELog {
5528        return ERROR(ErrorCode::DictionaryCorrupted);
5529    }
5530    ZSTD_buildFSETable(
5531        &mut dctx.MLTable,
5532        &mlNCount,
5533        mlMaxValue,
5534        &ML_base,
5535        &ML_bits,
5536        mlLog,
5537    );
5538    pos += mlSize;
5539
5540    // --- FSE LL table ---
5541    let mut llNCount = [0i16; (MaxLL + 1) as usize];
5542    let mut llMaxValue: u32 = MaxLL;
5543    let mut llLog: u32 = 0;
5544    let llSize = crate::common::entropy_common::FSE_readNCount(
5545        &mut llNCount,
5546        &mut llMaxValue,
5547        &mut llLog,
5548        &dict[pos..],
5549    );
5550    if ERR_isError(llSize) || llMaxValue > MaxLL || llLog > LLFSELog {
5551        return ERROR(ErrorCode::DictionaryCorrupted);
5552    }
5553    ZSTD_buildFSETable(
5554        &mut dctx.LLTable,
5555        &llNCount,
5556        llMaxValue,
5557        &LL_base,
5558        &LL_bits,
5559        llLog,
5560    );
5561    pos += llSize;
5562
5563    // --- 3 × 4-byte rep values ---
5564    if pos + 12 > dict.len() {
5565        return ERROR(ErrorCode::DictionaryCorrupted);
5566    }
5567    let dictContentSize = dict.len() - (pos + 12);
5568    for slot in repOut.iter_mut() {
5569        let r = MEM_readLE32(&dict[pos..pos + 4]);
5570        if r == 0 || (r as usize) > dictContentSize {
5571            return ERROR(ErrorCode::DictionaryCorrupted);
5572        }
5573        *slot = r;
5574        pos += 4;
5575    }
5576
5577    pos
5578}
5579
5580/// Port of `ZSTD_decompressMultiFrame` (`zstd_decompress.c:1070`).
5581/// Walks `src` through successive frames + skippable frames,
5582/// concatenating their decompressed output into `dst`. Either `dict`
5583/// or `ddict` may be supplied (not both) — the DDict's raw content
5584/// is extracted as the effective dict.
5585///
5586/// v0.1 delegates to `ZSTD_decompress_usingDict` which already
5587/// handles multi-frame walk + skippable-frame skipping; the wrapper
5588/// exists for API-surface parity with upstream's exported name.
5589pub fn ZSTD_decompressMultiFrame(
5590    dctx: &mut ZSTD_DCtx,
5591    dst: &mut [u8],
5592    src: &[u8],
5593    dict: &[u8],
5594    ddict: Option<&crate::decompress::zstd_ddict::ZSTD_DDict>,
5595) -> usize {
5596    debug_assert!(
5597        dict.is_empty() || ddict.is_none(),
5598        "dict xor ddict, not both"
5599    );
5600    if let Some(dd) = ddict {
5601        let content = crate::decompress::zstd_ddict::ZSTD_DDict_dictContent(dd);
5602        ZSTD_decompress_usingDict(dctx, dst, src, content)
5603    } else {
5604        ZSTD_decompress_usingDict(dctx, dst, src, dict)
5605    }
5606}
5607
5608/// Port of `ZSTD_getFrameContentSize`. Returns the declared
5609/// decompressed size of the frame at `src`, `ZSTD_CONTENTSIZE_UNKNOWN`
5610/// if the FCS field is absent, or `ZSTD_CONTENTSIZE_ERROR` on a
5611/// malformed / truncated header.
5612pub fn ZSTD_getFrameContentSize(src: &[u8]) -> u64 {
5613    let mut zfh = ZSTD_FrameHeader::default();
5614    let rc = ZSTD_getFrameHeader(&mut zfh, src);
5615    if rc != 0 {
5616        return ZSTD_CONTENTSIZE_ERROR;
5617    }
5618    if zfh.frameType == ZSTD_FrameType_e::ZSTD_skippableFrame {
5619        return 0;
5620    }
5621    zfh.frameContentSize
5622}
5623
5624/// Port of `readSkippableFrameSize`. Returns the total skippable-frame
5625/// byte length (header + payload).
5626fn readSkippableFrameSize(src: &[u8]) -> usize {
5627    if src.len() < ZSTD_SKIPPABLEHEADERSIZE {
5628        return ERROR(ErrorCode::SrcSizeWrong);
5629    }
5630    let sizeU32 = MEM_readLE32(&src[ZSTD_FRAMEIDSIZE..ZSTD_FRAMEIDSIZE + 4]);
5631    let total = ZSTD_SKIPPABLEHEADERSIZE.wrapping_add(sizeU32 as usize);
5632    if (total as u32) < sizeU32 {
5633        return ERROR(ErrorCode::FrameParameterUnsupported);
5634    }
5635    if total > src.len() {
5636        return ERROR(ErrorCode::SrcSizeWrong);
5637    }
5638    total
5639}
5640
5641/// Mirror of `ZSTD_frameSizeInfo`: bookkeeping for walking frames.
5642#[derive(Debug, Clone, Copy, Default)]
5643pub struct ZSTD_frameSizeInfo {
5644    pub nbBlocks: usize,
5645    pub compressedSize: usize,
5646    pub decompressedBound: u64,
5647}
5648
5649fn frameSizeInfo_error(err_code: usize) -> ZSTD_frameSizeInfo {
5650    ZSTD_frameSizeInfo {
5651        nbBlocks: 0,
5652        compressedSize: err_code,
5653        decompressedBound: ZSTD_CONTENTSIZE_ERROR,
5654    }
5655}
5656
5657/// Port of `ZSTD_findFrameSizeInfo`. Walks a single frame — handles
5658/// both skippable and regular frames.
5659pub fn ZSTD_findFrameSizeInfo(src: &[u8], format: ZSTD_format_e) -> ZSTD_frameSizeInfo {
5660    use crate::decompress::zstd_decompress_block::{
5661        blockProperties_t, blockType_e, ZSTD_blockHeaderSize, ZSTD_getcBlockSize,
5662    };
5663    let srcSize = src.len();
5664
5665    if format == ZSTD_format_e::ZSTD_f_zstd1
5666        && srcSize >= ZSTD_SKIPPABLEHEADERSIZE
5667        && (MEM_readLE32(&src[..4]) & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START
5668    {
5669        return ZSTD_frameSizeInfo {
5670            nbBlocks: 0,
5671            compressedSize: readSkippableFrameSize(src),
5672            decompressedBound: 0,
5673        };
5674    }
5675
5676    // Regular zstd frame.
5677    let mut zfh = ZSTD_FrameHeader::default();
5678    let rc = ZSTD_getFrameHeader_advanced(&mut zfh, src, format);
5679    if crate::common::error::ERR_isError(rc) {
5680        return frameSizeInfo_error(rc);
5681    }
5682    if rc > 0 {
5683        return frameSizeInfo_error(ERROR(ErrorCode::SrcSizeWrong));
5684    }
5685
5686    let mut ip = zfh.headerSize as usize;
5687    let mut remaining = srcSize - ip;
5688    let mut nbBlocks: usize = 0;
5689
5690    loop {
5691        let mut bp = blockProperties_t {
5692            blockType: blockType_e::bt_raw,
5693            lastBlock: 0,
5694            origSize: 0,
5695        };
5696        let cBlockSize = ZSTD_getcBlockSize(&src[ip..], &mut bp);
5697        if crate::common::error::ERR_isError(cBlockSize) {
5698            return frameSizeInfo_error(cBlockSize);
5699        }
5700        if ZSTD_blockHeaderSize + cBlockSize > remaining {
5701            return frameSizeInfo_error(ERROR(ErrorCode::SrcSizeWrong));
5702        }
5703        ip += ZSTD_blockHeaderSize + cBlockSize;
5704        remaining -= ZSTD_blockHeaderSize + cBlockSize;
5705        nbBlocks += 1;
5706        if bp.lastBlock != 0 {
5707            break;
5708        }
5709    }
5710
5711    if zfh.checksumFlag != 0 {
5712        if remaining < 4 {
5713            return frameSizeInfo_error(ERROR(ErrorCode::SrcSizeWrong));
5714        }
5715        ip += 4;
5716    }
5717
5718    let compressedSize = ip;
5719    let decompressedBound = if zfh.frameContentSize != ZSTD_CONTENTSIZE_UNKNOWN {
5720        zfh.frameContentSize
5721    } else {
5722        nbBlocks as u64 * zfh.blockSizeMax as u64
5723    };
5724    ZSTD_frameSizeInfo {
5725        nbBlocks,
5726        compressedSize,
5727        decompressedBound,
5728    }
5729}
5730
5731/// Port of `ZSTD_findFrameCompressedSize_advanced`
5732/// (`zstd_decompress.c:801`). Format-aware variant of
5733/// `ZSTD_findFrameCompressedSize`.
5734pub fn ZSTD_findFrameCompressedSize_advanced(src: &[u8], format: ZSTD_format_e) -> usize {
5735    ZSTD_findFrameSizeInfo(src, format).compressedSize
5736}
5737
5738/// Port of `ZSTD_findFrameCompressedSize`. Returns the total byte
5739/// length of the first frame in `src`, or an error code. Uses the
5740/// default zstd1 format — delegates to `_advanced`.
5741#[inline]
5742pub fn ZSTD_findFrameCompressedSize(src: &[u8]) -> usize {
5743    ZSTD_findFrameCompressedSize_advanced(src, ZSTD_format_e::ZSTD_f_zstd1)
5744}
5745
5746/// Port of `ZSTD_findDecompressedSize`. Walks potentially multiple
5747/// frames (regular and skippable) and sums their declared FCS.
5748pub fn ZSTD_findDecompressedSize(src: &[u8]) -> u64 {
5749    let mut src = src;
5750    let mut total: u64 = 0;
5751    while src.len() >= ZSTD_startingInputLength(ZSTD_format_e::ZSTD_f_zstd1) {
5752        let magic = MEM_readLE32(&src[..4]);
5753        if (magic & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START {
5754            let sz = readSkippableFrameSize(src);
5755            if crate::common::error::ERR_isError(sz) {
5756                return ZSTD_CONTENTSIZE_ERROR;
5757            }
5758            src = &src[sz..];
5759            continue;
5760        }
5761        let fcs = ZSTD_getFrameContentSize(src);
5762        if fcs >= ZSTD_CONTENTSIZE_ERROR {
5763            return fcs;
5764        }
5765        if total.checked_add(fcs).is_none() {
5766            return ZSTD_CONTENTSIZE_ERROR;
5767        }
5768        total += fcs;
5769        let sz = ZSTD_findFrameCompressedSize(src);
5770        if crate::common::error::ERR_isError(sz) {
5771            return ZSTD_CONTENTSIZE_ERROR;
5772        }
5773        src = &src[sz..];
5774    }
5775    if !src.is_empty() {
5776        return ZSTD_CONTENTSIZE_ERROR;
5777    }
5778    total
5779}
5780
5781/// Port of `ZSTD_initDStream`. Resets the DCtx's per-frame streaming
5782/// state (input/output buffers + drain cursor). Preserves any
5783/// configured dict so the next frame can still resolve back-refs
5784/// into it — upstream routes through `ZSTD_DCtx_reset(session_only)`.
5785pub fn ZSTD_initDStream(zds: &mut ZSTD_DCtx) -> usize {
5786    zds.stream_in_buffer.clear();
5787    zds.stream_out_buffer.clear();
5788    zds.stream_out_drained = 0;
5789    // Upstream (zstd_decompress.c:1755) returns
5790    // `ZSTD_startingInputLength(dctx->format)`. Same contract as
5791    // `ZSTD_resetDStream` — callers use the hint to size the first
5792    // decompressStream read. Reading `dctx.format` (vs hardcoding
5793    // `ZSTD_f_zstd1`) makes magicless-mode callers get the shorter
5794    // 2-byte hint instead of the zstd1 5-byte hint.
5795    ZSTD_startingInputLength(zds.format)
5796}
5797
5798/// Port of `ZSTD_initDStream_usingDict`. Initializes streaming
5799/// decompression with a raw-content dictionary — every frame decoded
5800/// in this session will be passed the dict as history.
5801pub fn ZSTD_initDStream_usingDict(zds: &mut ZSTD_DCtx, dict: &[u8]) -> usize {
5802    let rc = ZSTD_initDStream(zds);
5803    if crate::common::error::ERR_isError(rc) {
5804        return rc;
5805    }
5806    // Route through `ZSTD_DCtx_loadDictionary` (not a direct
5807    // `stream_dict = dict` write) so magic-prefix dicts get their
5808    // entropy tables parsed onto the dctx. Matches upstream
5809    // (`zstd_decompress.c:1744`): `reset(session_only)` +
5810    // `loadDictionary` chain.
5811    if !dict.is_empty() {
5812        let rc = ZSTD_DCtx_loadDictionary(zds, dict);
5813        if crate::common::error::ERR_isError(rc) {
5814            return rc;
5815        }
5816    }
5817    ZSTD_startingInputLength(zds.format)
5818}
5819
5820/// Port of `ZSTD_DECOMPRESSION_MARGIN` macro (`zstd.h:1574`). Static
5821/// upper bound on the margin needed to safely decompress
5822/// `originalSize` bytes from a frame whose max block size is
5823/// `blockSize`. Useful when you know originalSize ahead of time and
5824/// want a compile-time margin (matching upstream's preprocessor
5825/// macro semantics).
5826#[inline]
5827pub const fn ZSTD_DECOMPRESSION_MARGIN(originalSize: usize, blockSize: usize) -> usize {
5828    use crate::compress::zstd_compress::ZSTD_FRAMEHEADERSIZE_MAX;
5829    let blocks = if originalSize == 0 {
5830        0
5831    } else {
5832        3 * originalSize.div_ceil(blockSize)
5833    };
5834    ZSTD_FRAMEHEADERSIZE_MAX + 4 + blocks + blockSize
5835}
5836
5837/// Port of `ZSTD_decompressionMargin`. Returns an upper bound on the
5838/// number of bytes of `dst` padding needed to safely decompress `src`
5839/// in-place — the caller offsets its output cursor by this much so
5840/// wildcopy overreads can't clobber unread compressed data.
5841///
5842/// The margin sums: frame header bytes + 4-byte checksum when
5843/// present + 3 bytes per block + the max block size observed across
5844/// all frames. Skippable frames count their full size.
5845pub fn ZSTD_decompressionMargin(src: &[u8]) -> usize {
5846    use crate::common::error::{ERR_isError, ErrorCode, ERROR};
5847    let mut margin: usize = 0;
5848    let mut maxBlockSize: u32 = 0;
5849    let mut cursor = src;
5850
5851    while !cursor.is_empty() {
5852        let info = ZSTD_findFrameSizeInfo(cursor, ZSTD_format_e::ZSTD_f_zstd1);
5853        let mut zfh = ZSTD_FrameHeader::default();
5854        let rc = ZSTD_getFrameHeader(&mut zfh, cursor);
5855        if ERR_isError(rc) {
5856            return rc;
5857        }
5858        if ERR_isError(info.compressedSize) || info.decompressedBound == ZSTD_CONTENTSIZE_ERROR {
5859            return ERROR(ErrorCode::CorruptionDetected);
5860        }
5861
5862        if zfh.frameType == ZSTD_FrameType_e::ZSTD_frame {
5863            margin += zfh.headerSize as usize;
5864            margin += if zfh.checksumFlag != 0 { 4 } else { 0 };
5865            margin += 3 * info.nbBlocks;
5866            if zfh.blockSizeMax > maxBlockSize {
5867                maxBlockSize = zfh.blockSizeMax;
5868            }
5869        } else {
5870            // Skippable: the whole frame counts.
5871            margin += info.compressedSize;
5872        }
5873
5874        cursor = &cursor[info.compressedSize..];
5875    }
5876
5877    margin + maxBlockSize as usize
5878}
5879
5880/// Port of `ZSTD_decompressBound`. Walks every frame in `src`,
5881/// summing the per-frame `decompressedBound`. Returns
5882/// `ZSTD_CONTENTSIZE_ERROR` on any parse error.
5883pub fn ZSTD_decompressBound(src: &[u8]) -> u64 {
5884    use crate::common::error::ERR_isError;
5885    let mut bound: u64 = 0;
5886    let mut cursor = src;
5887    while !cursor.is_empty() {
5888        let info = ZSTD_findFrameSizeInfo(cursor, ZSTD_format_e::ZSTD_f_zstd1);
5889        if ERR_isError(info.compressedSize) || info.decompressedBound == ZSTD_CONTENTSIZE_ERROR {
5890            return ZSTD_CONTENTSIZE_ERROR;
5891        }
5892        cursor = &cursor[info.compressedSize..];
5893        bound = bound.saturating_add(info.decompressedBound);
5894    }
5895    bound
5896}
5897
5898/// Port of `ZSTD_getDecompressedSize`. Deprecated: reads the frame
5899/// content size, returning 0 for "unknown / empty / error" — the
5900/// modern path is `ZSTD_getFrameContentSize` which distinguishes
5901/// those cases via sentinels.
5902pub fn ZSTD_getDecompressedSize(src: &[u8]) -> u64 {
5903    let ret = ZSTD_getFrameContentSize(src);
5904    if ret >= ZSTD_CONTENTSIZE_ERROR {
5905        0
5906    } else {
5907        ret
5908    }
5909}
5910
5911/// Port of `ZSTD_decodingBufferSize_internal` (`zstd_decompress.c:1970`).
5912/// Caller supplies `blockSizeMax` — the decoder's max expected block
5913/// size; `ZSTD_decodingBufferSize_min` defaults it to
5914/// `ZSTD_BLOCKSIZE_MAX`. Returns the ring-buffer size needed: window
5915/// plus twice the block size (one for output, one for split-lit
5916/// trailing bytes) plus two wildcopy-overlength slack regions.
5917pub fn ZSTD_decodingBufferSize_internal(
5918    windowSize: u64,
5919    frameContentSize: u64,
5920    blockSizeMax: usize,
5921) -> usize {
5922    use crate::common::error::{ErrorCode, ERROR};
5923    use crate::common::zstd_internal::WILDCOPY_OVERLENGTH;
5924    use crate::decompress::zstd_decompress_block::ZSTD_BLOCKSIZE_MAX;
5925    let blockSize = windowSize
5926        .min(ZSTD_BLOCKSIZE_MAX as u64)
5927        .min(blockSizeMax as u64) as usize;
5928    let neededRBSize: u64 = windowSize + (blockSize as u64) * 2 + (WILDCOPY_OVERLENGTH as u64) * 2;
5929    let neededSize = frameContentSize.min(neededRBSize);
5930    let minRBSize = neededSize as usize;
5931    if minRBSize as u64 != neededSize {
5932        return ERROR(ErrorCode::FrameParameterWindowTooLarge);
5933    }
5934    minRBSize
5935}
5936
5937/// Port of `ZSTD_decodingBufferSize_min`. Worst-case ring-buffer size
5938/// needed to decompress a frame with the given window and content
5939/// sizes — accounts for two-block wildcopy slack. Delegates to
5940/// `ZSTD_decodingBufferSize_internal` with `blockSizeMax =
5941/// ZSTD_BLOCKSIZE_MAX`.
5942pub fn ZSTD_decodingBufferSize_min(windowSize: u64, frameContentSize: u64) -> usize {
5943    use crate::decompress::zstd_decompress_block::ZSTD_BLOCKSIZE_MAX;
5944    ZSTD_decodingBufferSize_internal(windowSize, frameContentSize, ZSTD_BLOCKSIZE_MAX)
5945}
5946
5947/// Port of `ZSTD_createDCtx_advanced`.
5948pub fn ZSTD_createDCtx_advanced(
5949    customMem: crate::compress::zstd_compress::ZSTD_customMem,
5950) -> Option<Box<ZSTD_DCtx>> {
5951    if !crate::compress::zstd_compress::ZSTD_customMem_validate(customMem) {
5952        return None;
5953    }
5954    ZSTD_createDCtx_internal(customMem)
5955}
5956
5957/// Port of `ZSTD_createDStream_advanced`.
5958pub fn ZSTD_createDStream_advanced(
5959    customMem: crate::compress::zstd_compress::ZSTD_customMem,
5960) -> Option<Box<ZSTD_DStream>> {
5961    ZSTD_createDCtx_advanced(customMem)
5962}
5963
5964/// Port of `ZSTD_initStaticDCtx`. Places a `ZSTD_DCtx` header inside
5965/// the caller's workspace when alignment and size allow it.
5966pub fn ZSTD_initStaticDCtx(workspace: &mut [u8]) -> Option<&mut ZSTD_DCtx> {
5967    use core::mem::{align_of, size_of};
5968    use core::ptr;
5969
5970    if (workspace.as_mut_ptr() as usize) & (align_of::<u64>() - 1) != 0 {
5971        return None;
5972    }
5973    if workspace.len() < size_of::<ZSTD_DCtx>() {
5974        return None;
5975    }
5976
5977    let dctx = unsafe { &mut *(workspace.as_mut_ptr() as *mut ZSTD_DCtx) };
5978    unsafe {
5979        ptr::write(dctx, ZSTD_DCtx::default());
5980    }
5981    ZSTD_initDCtx_internal(dctx);
5982    Some(dctx)
5983}
5984
5985/// Port of `ZSTD_initStaticDStream`. Alias for `ZSTD_initStaticDCtx`.
5986pub fn ZSTD_initStaticDStream(workspace: &mut [u8]) -> Option<&mut ZSTD_DStream> {
5987    ZSTD_initStaticDCtx(workspace)
5988}
5989
5990/// Port of `ZSTD_initStaticDDict`. Places a `ZSTD_DDict` header in
5991/// caller workspace and references the caller's `dict` bytes.
5992pub fn ZSTD_initStaticDDict<'a>(
5993    workspace: &'a mut [u8],
5994    dict: &[u8],
5995) -> Option<&'a mut crate::decompress::zstd_ddict::ZSTD_DDict> {
5996    use crate::decompress::zstd_ddict::{ZSTD_DDict, ZSTD_dictContentType_e};
5997    use core::mem::{align_of, size_of};
5998    use core::ptr;
5999
6000    if (workspace.as_mut_ptr() as usize) & (align_of::<u64>() - 1) != 0 {
6001        return None;
6002    }
6003    if workspace.len() < size_of::<ZSTD_DDict>() {
6004        return None;
6005    }
6006
6007    let ddict = unsafe { &mut *(workspace.as_mut_ptr() as *mut ZSTD_DDict) };
6008    unsafe {
6009        ptr::write(
6010            ddict,
6011            ZSTD_DDict {
6012                dictBuffer: Vec::new(),
6013                dictContent: dict.as_ptr(),
6014                dictSize: dict.len(),
6015                dictID: 0,
6016                entropyPresent: 0,
6017            },
6018        );
6019    }
6020    let rc = crate::decompress::zstd_ddict::ZSTD_loadEntropy_intoDDict(
6021        ddict,
6022        ZSTD_dictContentType_e::ZSTD_dct_auto,
6023    );
6024    if crate::common::error::ERR_isError(rc) {
6025        return None;
6026    }
6027    Some(ddict)
6028}
6029
6030/// Port of `ZSTD_nextInputType_e`. Tells `ZSTD_decompressContinue`
6031/// callers what kind of chunk the decompressor expects next.
6032#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
6033pub enum ZSTD_nextInputType_e {
6034    #[default]
6035    ZSTDnit_frameHeader = 0,
6036    ZSTDnit_blockHeader = 1,
6037    ZSTDnit_block = 2,
6038    ZSTDnit_lastBlock = 3,
6039    ZSTDnit_checksum = 4,
6040    ZSTDnit_skippableFrame = 5,
6041}
6042
6043/// Port of `ZSTD_nextInputType`. Reports what kind of chunk the
6044/// block-level legacy decoder expects next from `decompressContinue`.
6045#[inline]
6046pub fn ZSTD_nextInputType(dctx: &ZSTD_DCtx) -> ZSTD_nextInputType_e {
6047    match dctx.stage {
6048        ZSTD_dStage::ZSTDds_getFrameHeaderSize | ZSTD_dStage::ZSTDds_decodeFrameHeader => {
6049            ZSTD_nextInputType_e::ZSTDnit_frameHeader
6050        }
6051        ZSTD_dStage::ZSTDds_decodeBlockHeader => ZSTD_nextInputType_e::ZSTDnit_blockHeader,
6052        ZSTD_dStage::ZSTDds_decompressBlock => ZSTD_nextInputType_e::ZSTDnit_block,
6053        ZSTD_dStage::ZSTDds_decompressLastBlock => ZSTD_nextInputType_e::ZSTDnit_lastBlock,
6054        ZSTD_dStage::ZSTDds_checkChecksum => ZSTD_nextInputType_e::ZSTDnit_checksum,
6055        ZSTD_dStage::ZSTDds_decodeSkippableHeader | ZSTD_dStage::ZSTDds_skipFrame => {
6056            ZSTD_nextInputType_e::ZSTDnit_skippableFrame
6057        }
6058    }
6059}
6060
6061/// Port of `ZSTD_nextSrcSizeToDecompressWithInputSize`. During raw
6062/// block streaming, upstream may consume a partial block bounded by
6063/// currently available input; all other stages require the exact
6064/// expected size.
6065#[inline]
6066pub fn ZSTD_nextSrcSizeToDecompressWithInputSize(dctx: &ZSTD_DCtx, inputSize: usize) -> usize {
6067    let in_block = matches!(
6068        dctx.stage,
6069        ZSTD_dStage::ZSTDds_decompressBlock | ZSTD_dStage::ZSTDds_decompressLastBlock
6070    );
6071    if !in_block || dctx.bType != crate::decompress::zstd_decompress_block::blockType_e::bt_raw {
6072        return dctx.expected;
6073    }
6074    if dctx.expected == 0 {
6075        return 0;
6076    }
6077    inputSize.clamp(1, dctx.expected)
6078}
6079
6080/// Port of `ZSTD_isSkipFrame`.
6081#[inline]
6082pub fn ZSTD_isSkipFrame(dctx: &ZSTD_DCtx) -> i32 {
6083    (dctx.stage == ZSTD_dStage::ZSTDds_skipFrame) as i32
6084}
6085
6086/// Port of `ZSTD_decompressBegin`. Legacy continue-style init — v0.1
6087/// doesn't drive a block-level state machine, so this is a no-op
6088/// returning 0.
6089#[inline]
6090pub fn ZSTD_decompressBegin(dctx: &mut ZSTD_DCtx) -> usize {
6091    use crate::common::xxhash::XXH64_reset;
6092    // Upstream (zstd_decompress.c:1560) resets per-frame state:
6093    // clear entropy flags, zero dictID, set stage to "awaiting
6094    // frame header", rebuild default FSE DTables so set_basic
6095    // branches in block-0 find valid defaults.
6096    dctx.litEntropy = 0;
6097    dctx.fseEntropy = 0;
6098    dctx.dictID = 0;
6099    dctx.isFrameDecompression = 1;
6100    dctx.previousDstEnd = None;
6101    dctx.prefixStart = None;
6102    dctx.virtualStart = None;
6103    dctx.dictEnd = None;
6104    // `repStartValue = {1, 4, 8}`.
6105    dctx.ddict_rep = [1, 4, 8];
6106    // Seed default FSE DTables so that `set_basic` blocks (the
6107    // very first sequence block of a fresh frame) have a valid
6108    // table to read from. Previously we relied on DCtx::new() to
6109    // have done this, but session resets (`reset_session_only`)
6110    // also need to re-seed without re-creating the whole DCtx.
6111    crate::decompress::zstd_decompress_block::ZSTD_buildDefaultSeqTables(dctx);
6112    dctx.expected = ZSTD_startingInputLength(dctx.format);
6113    dctx.stage = ZSTD_dStage::ZSTDds_getFrameHeaderSize;
6114    dctx.processedCSize = 0;
6115    dctx.decodedSize = 0;
6116    dctx.headerSize = 0;
6117    dctx.rleSize = 0;
6118    dctx.bType = crate::decompress::zstd_decompress_block::blockType_e::bt_raw;
6119    dctx.validateChecksum = 0;
6120    dctx.fParams = ZSTD_FrameHeader::default();
6121    dctx.headerBuffer.fill(0);
6122    dctx.historyBuffer.clear();
6123    XXH64_reset(&mut dctx.xxhState, 0);
6124    0
6125}
6126
6127/// Port of `ZSTD_decompressBegin_usingDict` (`zstd_decompress.c:1588`).
6128/// Calls `ZSTD_decompressBegin` first, then dispatches through
6129/// `ZSTD_decompress_insertDictionary` which handles magic-prefix vs
6130/// raw-content dicts.
6131pub fn ZSTD_decompressBegin_usingDict(dctx: &mut ZSTD_DCtx, dict: &[u8]) -> usize {
6132    use crate::common::error::{ERR_isError, ErrorCode, ERROR};
6133    let rc = ZSTD_decompressBegin(dctx);
6134    if ERR_isError(rc) {
6135        return rc;
6136    }
6137    if dict.is_empty() {
6138        return 0;
6139    }
6140    let rc = ZSTD_decompress_insertDictionary(dctx, dict);
6141    if ERR_isError(rc) {
6142        return ERROR(ErrorCode::DictionaryCorrupted);
6143    }
6144    0
6145}
6146
6147/// Port of `ZSTD_decompressBegin_usingDDict` (`zstd_decompress.c:1601`).
6148/// Uses the DDict's preloaded parameters and raw content.
6149pub fn ZSTD_decompressBegin_usingDDict(
6150    dctx: &mut ZSTD_DCtx,
6151    ddict: &crate::decompress::zstd_ddict::ZSTD_DDict,
6152) -> usize {
6153    use crate::common::error::ERR_isError;
6154    let rc = ZSTD_decompressBegin(dctx);
6155    if ERR_isError(rc) {
6156        return rc;
6157    }
6158    let content = crate::decompress::zstd_ddict::ZSTD_DDict_dictContent(ddict);
6159    if content.is_empty() {
6160        return 0;
6161    }
6162    crate::decompress::zstd_ddict::ZSTD_copyDDictParameters(dctx, ddict);
6163    0
6164}
6165
6166/// Port of `ZSTD_decompressContinue`. Legacy block-level decode —
6167/// callers must feed exactly the chunk kind and size reported by
6168/// `ZSTD_nextInputType()` / `ZSTD_nextSrcSizeToDecompress()`.
6169pub fn ZSTD_decompressContinue(dctx: &mut ZSTD_DCtx, dst: &mut [u8], src: &[u8]) -> usize {
6170    use crate::common::error::{ErrorCode, ERROR};
6171    use crate::common::mem::MEM_readLE32;
6172    use crate::common::xxhash::{XXH64_digest, XXH64_update};
6173    use crate::decompress::zstd_decompress_block::{
6174        blockProperties_t, blockType_e, streaming_operation, ZSTD_blockHeaderSize,
6175        ZSTD_decoder_entropy_rep, ZSTD_decompressBlock_internal, ZSTD_getcBlockSize,
6176    };
6177
6178    if src.len() != ZSTD_nextSrcSizeToDecompress(dctx) {
6179        return ERROR(ErrorCode::SrcSizeWrong);
6180    }
6181
6182    dctx.processedCSize = dctx.processedCSize.wrapping_add(src.len() as u64);
6183
6184    match dctx.stage {
6185        ZSTD_dStage::ZSTDds_getFrameHeaderSize => {
6186            if dctx.format == ZSTD_format_e::ZSTD_f_zstd1
6187                && src.len() >= ZSTD_FRAMEIDSIZE
6188                && (MEM_readLE32(src) & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START
6189            {
6190                dctx.headerBuffer[..src.len()].copy_from_slice(src);
6191                dctx.expected = ZSTD_SKIPPABLEHEADERSIZE - src.len();
6192                dctx.stage = ZSTD_dStage::ZSTDds_decodeSkippableHeader;
6193                return 0;
6194            }
6195            let headerSize = ZSTD_frameHeaderSize_internal(src, dctx.format);
6196            if crate::common::error::ERR_isError(headerSize) {
6197                return headerSize;
6198            }
6199            dctx.headerSize = headerSize;
6200            dctx.headerBuffer[..src.len()].copy_from_slice(src);
6201            dctx.expected = headerSize - src.len();
6202            dctx.stage = ZSTD_dStage::ZSTDds_decodeFrameHeader;
6203            0
6204        }
6205        ZSTD_dStage::ZSTDds_decodeFrameHeader => {
6206            let offset = dctx.headerSize - src.len();
6207            dctx.headerBuffer[offset..offset + src.len()].copy_from_slice(src);
6208            let header = dctx.headerBuffer[..dctx.headerSize].to_vec();
6209            let rc = ZSTD_decodeFrameHeader(dctx, &header, header.len());
6210            if crate::common::error::ERR_isError(rc) {
6211                return rc;
6212            }
6213            if dctx.fParams.frameType != ZSTD_FrameType_e::ZSTD_frame {
6214                return ERROR(ErrorCode::PrefixUnknown);
6215            }
6216            dctx.expected = ZSTD_blockHeaderSize;
6217            dctx.stage = ZSTD_dStage::ZSTDds_decodeBlockHeader;
6218            0
6219        }
6220        ZSTD_dStage::ZSTDds_decodeBlockHeader => {
6221            let mut bp = blockProperties_t {
6222                blockType: blockType_e::bt_raw,
6223                lastBlock: 0,
6224                origSize: 0,
6225            };
6226            let cBlockSize = ZSTD_getcBlockSize(src, &mut bp);
6227            if crate::common::error::ERR_isError(cBlockSize) {
6228                return cBlockSize;
6229            }
6230            if cBlockSize > dctx.fParams.blockSizeMax as usize {
6231                return ERROR(ErrorCode::CorruptionDetected);
6232            }
6233            dctx.expected = cBlockSize;
6234            dctx.bType = bp.blockType;
6235            dctx.rleSize = bp.origSize as usize;
6236            if cBlockSize != 0 {
6237                dctx.stage = if bp.lastBlock != 0 {
6238                    ZSTD_dStage::ZSTDds_decompressLastBlock
6239                } else {
6240                    ZSTD_dStage::ZSTDds_decompressBlock
6241                };
6242                return 0;
6243            }
6244            if bp.lastBlock != 0 {
6245                if dctx.fParams.checksumFlag != 0 {
6246                    dctx.expected = 4;
6247                    dctx.stage = ZSTD_dStage::ZSTDds_checkChecksum;
6248                } else {
6249                    dctx.expected = ZSTD_startingInputLength(dctx.format);
6250                    dctx.stage = ZSTD_dStage::ZSTDds_getFrameHeaderSize;
6251                }
6252            } else {
6253                dctx.expected = ZSTD_blockHeaderSize;
6254            }
6255            0
6256        }
6257        ZSTD_dStage::ZSTDds_decompressLastBlock | ZSTD_dStage::ZSTDds_decompressBlock => {
6258            let mut entropy_rep = ZSTD_decoder_entropy_rep {
6259                rep: dctx.ddict_rep,
6260            };
6261            let rSize = match dctx.bType {
6262                blockType_e::bt_compressed => {
6263                    let r = ZSTD_decompressBlock_internal(
6264                        dctx,
6265                        &mut entropy_rep,
6266                        dst,
6267                        0,
6268                        src,
6269                        streaming_operation::is_streaming,
6270                    );
6271                    dctx.expected = 0;
6272                    r
6273                }
6274                blockType_e::bt_raw => {
6275                    let r = ZSTD_copyRawBlock(dst, src);
6276                    if crate::common::error::ERR_isError(r) {
6277                        return r;
6278                    }
6279                    dctx.expected -= r;
6280                    r
6281                }
6282                blockType_e::bt_rle => {
6283                    let r = ZSTD_setRleBlock(dst, src[0], dctx.rleSize);
6284                    dctx.expected = 0;
6285                    r
6286                }
6287                blockType_e::bt_reserved => return ERROR(ErrorCode::CorruptionDetected),
6288            };
6289            if crate::common::error::ERR_isError(rSize) {
6290                return rSize;
6291            }
6292            if rSize > dctx.fParams.blockSizeMax as usize {
6293                return ERROR(ErrorCode::CorruptionDetected);
6294            }
6295            dctx.ddict_rep = entropy_rep.rep;
6296            dctx.decodedSize = dctx.decodedSize.wrapping_add(rSize as u64);
6297            if dctx.validateChecksum != 0 && rSize > 0 {
6298                XXH64_update(&mut dctx.xxhState, &dst[..rSize]);
6299            }
6300            // Append this block's output to the rolling history buffer
6301            // so subsequent blocks' back-references can resolve into it
6302            // via the ext-dict path. Cap at the frame's window size
6303            // (or the ZSTD_BLOCKSIZE_MAX fallback before a frame
6304            // header has been parsed).
6305            if rSize > 0 {
6306                let cap = if dctx.fParams.windowSize > 0 {
6307                    dctx.fParams.windowSize as usize
6308                } else {
6309                    crate::decompress::zstd_decompress_block::ZSTD_BLOCKSIZE_MAX
6310                };
6311                dctx.historyBuffer.extend_from_slice(&dst[..rSize]);
6312                if dctx.historyBuffer.len() > cap {
6313                    let drop = dctx.historyBuffer.len() - cap;
6314                    dctx.historyBuffer.drain(..drop);
6315                }
6316            }
6317            if dctx.expected > 0 {
6318                return rSize;
6319            }
6320            if dctx.stage == ZSTD_dStage::ZSTDds_decompressLastBlock {
6321                if dctx.fParams.frameContentSize != ZSTD_CONTENTSIZE_UNKNOWN
6322                    && dctx.decodedSize != dctx.fParams.frameContentSize
6323                {
6324                    return ERROR(ErrorCode::CorruptionDetected);
6325                }
6326                if dctx.fParams.checksumFlag != 0 {
6327                    dctx.expected = 4;
6328                    dctx.stage = ZSTD_dStage::ZSTDds_checkChecksum;
6329                } else {
6330                    dctx.expected = ZSTD_startingInputLength(dctx.format);
6331                    dctx.stage = ZSTD_dStage::ZSTDds_getFrameHeaderSize;
6332                }
6333                // Clear the rolling history at frame boundaries — the
6334                // next frame starts with a fresh window.
6335                dctx.historyBuffer.clear();
6336            } else {
6337                dctx.expected = ZSTD_blockHeaderSize;
6338                dctx.stage = ZSTD_dStage::ZSTDds_decodeBlockHeader;
6339            }
6340            rSize
6341        }
6342        ZSTD_dStage::ZSTDds_checkChecksum => {
6343            if dctx.validateChecksum != 0 {
6344                let h32 = XXH64_digest(&dctx.xxhState) as u32;
6345                let check32 = MEM_readLE32(src);
6346                if check32 != h32 {
6347                    return ERROR(ErrorCode::ChecksumWrong);
6348                }
6349            }
6350            dctx.expected = ZSTD_startingInputLength(dctx.format);
6351            dctx.stage = ZSTD_dStage::ZSTDds_getFrameHeaderSize;
6352            0
6353        }
6354        ZSTD_dStage::ZSTDds_decodeSkippableHeader => {
6355            let offset = ZSTD_SKIPPABLEHEADERSIZE - src.len();
6356            dctx.headerBuffer[offset..offset + src.len()].copy_from_slice(src);
6357            dctx.expected = MEM_readLE32(&dctx.headerBuffer[ZSTD_FRAMEIDSIZE..]) as usize;
6358            dctx.stage = ZSTD_dStage::ZSTDds_skipFrame;
6359            0
6360        }
6361        ZSTD_dStage::ZSTDds_skipFrame => {
6362            dctx.expected = ZSTD_startingInputLength(dctx.format);
6363            dctx.stage = ZSTD_dStage::ZSTDds_getFrameHeaderSize;
6364            0
6365        }
6366    }
6367}
6368
6369/// Port of `ZSTD_copyDCtx`. Deep-copies `src` into `dst`. Upstream
6370/// only copies the "header" portion of the struct to skip the large
6371/// `inBuff` workspace; the Rust port owns each field in a `Vec`, so
6372/// we delegate to `Clone::clone_from` — this copies everything,
6373/// including the scratch buffers (they're small and the semantics
6374/// are correct).
6375#[inline]
6376pub fn ZSTD_copyDCtx(dst: &mut ZSTD_DCtx, src: &ZSTD_DCtx) {
6377    dst.clone_from(src);
6378}
6379
6380/// Port of `ZSTD_initDStream_usingDDict`. Like `ZSTD_initDStream` but
6381/// attaches a pre-built `ZSTD_DDict` — the DDict's raw content is
6382/// copied onto `dctx.stream_dict` so every frame decoded in this
6383/// session sees it as history.
6384pub fn ZSTD_initDStream_usingDDict(
6385    zds: &mut ZSTD_DCtx,
6386    ddict: &crate::decompress::zstd_ddict::ZSTD_DDict,
6387) -> usize {
6388    let rc = ZSTD_initDStream(zds);
6389    if crate::common::error::ERR_isError(rc) {
6390        return rc;
6391    }
6392    // Route through `ZSTD_DCtx_refDDict` (which calls
6393    // `insertDictionary` under the hood) so a DDict built from a
6394    // magic-prefix zstd-format dict surfaces its dictID + entropy
6395    // tables on the dctx. Previously we wrote stream_dict directly,
6396    // bypassing the magic probe — sibling fix to the
6397    // `initDStream_usingDict` parity fix. Matches upstream's
6398    // `initDStream_usingDDict` → `refDDict` chain
6399    // (zstd_decompress.c:1753).
6400    let rc = ZSTD_DCtx_refDDict(zds, ddict);
6401    if crate::common::error::ERR_isError(rc) {
6402        return rc;
6403    }
6404    // Same `dctx.format`-aware hint as initDStream / resetDStream —
6405    // magicless-mode callers get 1, zstd1 callers get 5.
6406    ZSTD_startingInputLength(zds.format)
6407}
6408
6409/// Port of `ZSTD_nextSrcSizeToDecompress`. Returns the exact byte
6410/// count the legacy block-level decoder expects next.
6411#[inline]
6412pub fn ZSTD_nextSrcSizeToDecompress(dctx: &ZSTD_DCtx) -> usize {
6413    dctx.expected
6414}
6415
6416/// Port of `ZSTD_resetDStream`. Clears per-frame state (input/output
6417/// buffers) but preserves the configured dict for subsequent frames.
6418/// Returns a hint for the suggested next input size.
6419pub fn ZSTD_resetDStream(zds: &mut ZSTD_DCtx) -> usize {
6420    zds.stream_in_buffer.clear();
6421    zds.stream_out_buffer.clear();
6422    zds.stream_out_drained = 0;
6423    zds.oversizedDuration = 0;
6424    // Upstream (zstd_decompress.c:1772) returns
6425    // `ZSTD_startingInputLength(dctx->format)` — the number of bytes
6426    // needed to query the next frame header. Magicless-mode (`ZSTD_f_zstd1_magicless`)
6427    // callers need the shorter 2-byte hint instead of the 5-byte zstd1 hint.
6428    ZSTD_startingInputLength(zds.format)
6429}
6430
6431/// Port of `ZSTD_DCtx_isOverflow`.
6432pub fn ZSTD_DCtx_isOverflow(
6433    zds: &ZSTD_DCtx,
6434    neededInBuffSize: usize,
6435    neededOutBuffSize: usize,
6436) -> i32 {
6437    let retained = zds.stream_in_buffer.capacity() + zds.stream_out_buffer.capacity();
6438    let needed = (neededInBuffSize + neededOutBuffSize)
6439        * crate::common::zstd_internal::ZSTD_WORKSPACETOOLARGE_FACTOR;
6440    (retained >= needed) as i32
6441}
6442
6443/// Port of `ZSTD_DCtx_updateOversizedDuration`.
6444pub fn ZSTD_DCtx_updateOversizedDuration(
6445    zds: &mut ZSTD_DCtx,
6446    neededInBuffSize: usize,
6447    neededOutBuffSize: usize,
6448) {
6449    if ZSTD_DCtx_isOverflow(zds, neededInBuffSize, neededOutBuffSize) != 0 {
6450        zds.oversizedDuration += 1;
6451    } else {
6452        zds.oversizedDuration = 0;
6453    }
6454}
6455
6456/// Port of `ZSTD_checkOutBuffer`. The stable-output-buffer mode isn't
6457/// exposed by this Rust port yet, so every output buffer is accepted.
6458#[inline]
6459pub fn ZSTD_checkOutBuffer(_zds: &ZSTD_DCtx, _output: &[u8], _output_pos: usize) -> usize {
6460    0
6461}
6462
6463/// Port of `ZSTD_decompressContinueStream`, adapted to Rust slices.
6464/// It calls the legacy `ZSTD_decompressContinue` transition and
6465/// advances `output_pos` by the decoded byte count.
6466pub fn ZSTD_decompressContinueStream(
6467    zds: &mut ZSTD_DCtx,
6468    output: &mut [u8],
6469    output_pos: &mut usize,
6470    src: &[u8],
6471) -> usize {
6472    let dstSize = if ZSTD_isSkipFrame(zds) != 0 {
6473        0
6474    } else {
6475        output.len() - *output_pos
6476    };
6477    let decodedSize =
6478        ZSTD_decompressContinue(zds, &mut output[*output_pos..*output_pos + dstSize], src);
6479    if crate::common::error::ERR_isError(decodedSize) {
6480        return decodedSize;
6481    }
6482    *output_pos += decodedSize;
6483    0
6484}
6485
6486/// Port of `ZSTD_decompressStream`. Buffers `input[input_pos..]` into
6487/// the DCtx, detects when a complete frame has been received via
6488/// `ZSTD_findFrameCompressedSize`, decodes it into an internal output
6489/// buffer, and drains the result into `output[output_pos..]`.
6490///
6491/// Returns a hint for the next suggested input size: 0 when the
6492/// current frame is complete and fully drained, `ZSTD_blockHeaderSize`
6493/// as a conservative "need more" hint otherwise.
6494///
6495/// v0.1 scope: single-frame per call-sequence. Multi-frame streams
6496/// work if the caller invokes `ZSTD_initDStream` between frames.
6497pub fn ZSTD_decompressStream(
6498    zds: &mut ZSTD_DCtx,
6499    output: &mut [u8],
6500    output_pos: &mut usize,
6501    input: &[u8],
6502    input_pos: &mut usize,
6503) -> usize {
6504    use crate::common::error::ERR_isError;
6505    // Drain any already-decoded bytes first.
6506    let avail = output.len() - *output_pos;
6507    let pending = zds.stream_out_buffer.len() - zds.stream_out_drained;
6508    let n = avail.min(pending);
6509    if n > 0 {
6510        output[*output_pos..*output_pos + n].copy_from_slice(
6511            &zds.stream_out_buffer[zds.stream_out_drained..zds.stream_out_drained + n],
6512        );
6513        zds.stream_out_drained += n;
6514        *output_pos += n;
6515    }
6516
6517    // Ingest fresh input.
6518    zds.stream_in_buffer.extend_from_slice(&input[*input_pos..]);
6519    *input_pos = input.len();
6520
6521    // If nothing pending on either side, we're done.
6522    if zds.stream_out_drained == zds.stream_out_buffer.len() && zds.stream_in_buffer.is_empty() {
6523        return 0;
6524    }
6525    // If output fully drained AND fresh input available, try to probe
6526    // a new frame.
6527    if zds.stream_out_drained == zds.stream_out_buffer.len() {
6528        // Try to measure a full frame from the staged input. Thread
6529        // the dctx's stored format through so magicless-mode streams
6530        // decode without the 4-byte magic prefix.
6531        let frame_sz = ZSTD_findFrameCompressedSize_advanced(&zds.stream_in_buffer, zds.format);
6532        if ERR_isError(frame_sz) {
6533            // Could be "need more input" (SrcSizeWrong). Return a
6534            // non-zero hint so the caller keeps feeding.
6535            return 3; // ZSTD_blockHeaderSize
6536        }
6537        // Determine decoded size.
6538        let declared = ZSTD_getFrameContentSize(&zds.stream_in_buffer);
6539        let out_size = if declared == ZSTD_CONTENTSIZE_UNKNOWN || declared == ZSTD_CONTENTSIZE_ERROR
6540        {
6541            // Fall back to a generous bound: 32× compressed size.
6542            frame_sz * 32
6543        } else {
6544            declared as usize
6545        };
6546        let mut decoded = vec![0u8; out_size.max(1)];
6547        let d = if zds.stream_dict.is_empty() {
6548            // Route through `ZSTD_decompressDCtx` (not `ZSTD_decompress`)
6549            // so the stream's DCtx state — crucially `dctx.format` —
6550            // is honored. A magicless-mode streaming decoder would
6551            // previously fail here because `ZSTD_decompress` allocates
6552            // a fresh dctx fixed to `ZSTD_f_zstd1`.
6553            use crate::common::xxhash::XXH64_state_t;
6554            use crate::decompress::zstd_decompress_block::ZSTD_decoder_entropy_rep;
6555            let frame_bytes = zds.stream_in_buffer[..frame_sz].to_vec();
6556            let mut rep = ZSTD_decoder_entropy_rep::default();
6557            let mut xxh = XXH64_state_t::default();
6558            ZSTD_decompressDCtx(zds, &mut rep, &mut xxh, &mut decoded, &frame_bytes)
6559        } else {
6560            // Thread the stream's own DCtx through the dict-decode
6561            // path (previously we allocated a throwaway `ZSTD_DCtx`
6562            // per frame — faithful-translation gap now that
6563            // `ZSTD_decompress_usingDict` honors the caller's dctx).
6564            // Clone the frame bytes + dict so the per-frame decoder
6565            // call can borrow `zds` mutably without aliasing.
6566            //
6567            // Honor the `use_once` lifetime: if the dict was bound
6568            // via `refPrefix`, demote it before the decode and clear
6569            // it after so a subsequent frame on the same stream
6570            // doesn't silently re-apply the prefix. `loadDictionary`
6571            // / `refDDict` bindings (`use_indefinitely`) survive.
6572            let was_use_once = zds.dictUses == ZSTD_dictUses_e::ZSTD_use_once;
6573            if was_use_once {
6574                zds.dictUses = ZSTD_dictUses_e::ZSTD_dont_use;
6575            }
6576            let dict = zds.stream_dict.clone();
6577            let frame_bytes = zds.stream_in_buffer[..frame_sz].to_vec();
6578            let decoded_len = ZSTD_decompress_usingDict(zds, &mut decoded, &frame_bytes, &dict);
6579            if was_use_once && !ERR_isError(decoded_len) {
6580                ZSTD_clearDict(zds);
6581            }
6582            decoded_len
6583        };
6584        if ERR_isError(d) {
6585            return d;
6586        }
6587        decoded.truncate(d);
6588        zds.stream_out_buffer = decoded;
6589        zds.stream_out_drained = 0;
6590        // Remove the consumed frame from the input buffer so multi-
6591        // frame streams work when the caller re-inits between frames.
6592        zds.stream_in_buffer.drain(..frame_sz);
6593
6594        // Drain the freshly-decoded output.
6595        let avail = output.len() - *output_pos;
6596        let pending = zds.stream_out_buffer.len() - zds.stream_out_drained;
6597        let n = avail.min(pending);
6598        if n > 0 {
6599            output[*output_pos..*output_pos + n].copy_from_slice(
6600                &zds.stream_out_buffer[zds.stream_out_drained..zds.stream_out_drained + n],
6601            );
6602            zds.stream_out_drained += n;
6603            *output_pos += n;
6604        }
6605    }
6606
6607    let remaining = zds.stream_out_buffer.len() - zds.stream_out_drained;
6608    if remaining == 0 && zds.stream_in_buffer.is_empty() {
6609        0
6610    } else {
6611        remaining.max(3)
6612    }
6613}
6614
6615/// Port of `ZSTD_decompressStream_simpleArgs` (`zstd.h:2611`). Upstream
6616/// provides this as an FFI-friendly variant of `ZSTD_decompressStream`
6617/// that takes positions by pointer so dynamic-language binders don't
6618/// have to build a `ZSTD_inBuffer` / `ZSTD_outBuffer` struct. Our
6619/// Rust port's base signature already uses `&mut usize` cursors so
6620/// the simpleArgs version is just a thin named alias — kept for
6621/// parity with FFI callers ported from upstream headers.
6622#[allow(clippy::too_many_arguments)]
6623#[inline]
6624pub fn ZSTD_decompressStream_simpleArgs(
6625    dctx: &mut ZSTD_DCtx,
6626    dst: &mut [u8],
6627    dst_pos: &mut usize,
6628    src: &[u8],
6629    src_pos: &mut usize,
6630) -> usize {
6631    ZSTD_decompressStream(dctx, dst, dst_pos, src, src_pos)
6632}