Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,13 @@ jobs:
if: needs.changes.outputs.code == 'true'
working-directory: zstd
run: cargo nextest run --profile ci -p structured-zstd --features hash,std,dict-builder,caller-workspace
- name: Test (without legacy)
# The default build decodes v0.5-v0.7 frames; without the feature the
# decoders and the command line refuse them as an unknown format. The
# feature changes only those two, so their tests run again here.
if: needs.changes.outputs.code == 'true'
working-directory: zstd
run: cargo nextest run --profile ci -p structured-zstd --no-default-features --features hash,std,dict-builder -E 'test(/^decoding::/) | binary_id(structured-zstd::bin/structured-zstd)'
- name: Doc tests
if: needs.changes.outputs.code == 'true'
run: cargo test --doc -p structured-zstd --features hash,std,dict-builder,caller-workspace
Expand Down Expand Up @@ -559,6 +566,11 @@ jobs:
- name: Clippy (no_std + caller-workspace)
working-directory: zstd
run: cargo clippy -p structured-zstd --no-default-features --features caller-workspace,hash -- -D warnings
# The v0.5-v0.7 decoders without `hash`, where their content checksum
# is not computed.
- name: Clippy (no_std + legacy)
working-directory: zstd
run: cargo clippy -p structured-zstd --no-default-features --features legacy -- -D warnings
# The clippy steps above compile for the host, which has SSE2 and a
# float unit. A kernel target has neither (x86_64-unknown-none is
# -sse,-sse2,...,+soft-float), so a vector type that
Expand All @@ -575,6 +587,7 @@ jobs:
cargo build -p structured-zstd --lib --profile "$profile" --target x86_64-unknown-none --no-default-features --features hash
cargo build -p structured-zstd --lib --profile "$profile" --target x86_64-unknown-none --no-default-features --features kernel-avx2,hash
cargo build -p structured-zstd --lib --profile "$profile" --target x86_64-unknown-none --no-default-features --features caller-workspace,hash
cargo build -p structured-zstd --lib --profile "$profile" --target x86_64-unknown-none --no-default-features --features legacy,hash
done

wasm:
Expand Down Expand Up @@ -1005,6 +1018,19 @@ jobs:
# feature, which the targets above must not get.
working-directory: zstd/fuzz
run: cargo fuzz run --target "$FUZZ_TARGET_TRIPLE" --features workspace decode_workspace -- -max_total_time=90 -timeout=30
- name: Fuzz decode_legacy (90s, seeded with the v0.5-v0.7 frames)
# Seeded from `zstd/legacy_frames`: blind mutation rarely gets past
# these formats' entropy headers to their blocks.
working-directory: zstd/fuzz
run: |
mkdir -p corpus/decode_legacy
# Crash inputs committed for this target replay first, as the
# other targets' do.
shopt -s dotglob
if [ -d artifacts/decode_legacy ] && [ -n "$(find artifacts/decode_legacy -mindepth 1 -maxdepth 1 -print -quit)" ]; then
cargo fuzz run --target "$FUZZ_TARGET_TRIPLE" decode_legacy artifacts/decode_legacy/*
fi
cargo fuzz run --target "$FUZZ_TARGET_TRIPLE" decode_legacy corpus/decode_legacy ../legacy_frames -- -max_total_time=90 -timeout=30
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Upload new crash artifacts on failure
if: failure()
uses: actions/upload-artifact@v7
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

## Highlights

- **Production-grade decoder** — complete [RFC 8878](https://www.rfc-editor.org/rfc/rfc8878) implementation: dictionary-backed streams, raw / RLE / compressed blocks, the full frame format, optional content checksums, runtime-dispatched SIMD kernels (SSE2 / BMI2 / AVX2 / NEON, opt-in AVX-512).
- **Production-grade decoder** — complete [RFC 8878](https://www.rfc-editor.org/rfc/rfc8878) implementation: dictionary-backed streams, raw / RLE / compressed blocks, the full frame format, optional content checksums, runtime-dispatched SIMD kernels (SSE2 / BMI2 / AVX2 / NEON, opt-in AVX-512). It also reads the v0.5-v0.7 frame formats, as every libzstd 1.5.7 build does by default.
- **Full-range encoder** — every C-zstd level (`-131072..=22`) produces valid frames decodable by this crate and by upstream C zstd; named presets, per-knob parameter overrides, long-distance matching, streaming via `std::io::Write`.
- **Dictionaries end to end** — compress and decompress with the same dictionary format C zstd consumes; reusable parsed handles; pure-Rust COVER / FastCOVER / legacy training behind the `dict-builder` feature.
- **Wire-compatible both ways** — frames interoperate with C zstd in either direction; interop is enforced in CI against the reference implementation.
Expand Down Expand Up @@ -332,7 +332,8 @@ in pure Rust:
| `kernel-simd128` | ✅ | WebAssembly SIMD kernel (needs `-C target-feature=+simd128`) |
| `kernel-vbmi2` | ❌ | AVX-512 decode kernel (see note below) |
| `kernel-scalar` | ✅ | Marker for the always-compiled scalar fallback |
| `dict-builder` | ❌ | Pure-Rust COVER / FastCOVER / legacy dictionary training |
| `dict-builder` | ✅ | Pure-Rust COVER / FastCOVER / legacy dictionary training |
| `legacy` | ✅ | Decoding of the v0.5, v0.6 and v0.7 frame formats; without it they are refused as an unknown format |
| `lsm` | ❌ | [Storage-format extensions](#storage-format-extensions) |
| `caller-workspace` | ❌ | [Decoding in caller-owned memory](#decoding-in-caller-owned-memory) |

Expand Down
11 changes: 7 additions & 4 deletions c-api/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use core::ffi::{c_uint, c_ulonglong};
use codec::decoding::errors::ReadFrameHeaderError;
use codec::decoding::{
FrameContentSize, FrameMagic, FrameSizeError, decompression_margin, find_decompressed_size,
find_frame_compressed_size, frame_decompressed_bound, frame_header_size, frame_magic,
find_frame_compressed_size, frame_decompressed_bound, frame_header_size, frame_magic, is_frame,
min_decoding_buffer_size, read_frame_header_info,
};
use codec::skippable::{
Expand Down Expand Up @@ -65,7 +65,9 @@ pub struct ZSTD_FrameHeader {
}

/// `size_t ZSTD_frameHeaderSize(const void* src, size_t srcSize)` — length of
/// the frame header (including the magic number), or an error code.
/// the frame header (including the magic number), or an error code. A
/// v0.5-v0.7 header, which `ZSTD_isFrame` accepts, is measured by its own
/// format's size fields.
///
/// # Safety
/// `src` must be valid for `src_size` bytes (or `NULL` with `src_size == 0`).
Expand Down Expand Up @@ -220,14 +222,15 @@ pub unsafe extern "C" fn ZSTD_findDecompressedSize(src: *const u8, src_size: usi
}

/// `unsigned ZSTD_isFrame(const void* buffer, size_t size)` — 1 when `buffer`
/// starts with a frame or skippable-frame magic number, else 0.
/// starts with a frame, skippable-frame or v0.5-v0.7 frame magic number,
/// else 0.
///
/// # Safety
/// `buffer` must be valid for `size` bytes (or `NULL` with `size == 0`).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ZSTD_isFrame(buffer: *const u8, size: usize) -> c_uint {
let buffer = unsafe { in_slice(buffer, size) };
c_uint::from(frame_magic(buffer).is_some())
c_uint::from(is_frame(buffer))
Comment thread
polaz marked this conversation as resolved.
}

/// `unsigned ZSTD_isSkippableFrame(const void* buffer, size_t size)` — 1 when
Expand Down
46 changes: 36 additions & 10 deletions c-api/src/streaming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,21 @@ fn enter_stream_frame(
let take = (FRAME_HEADER_MAX - held_len).min(available.len());
view[held_len..held_len + take].copy_from_slice(&available[..take]);
let view = &view[..held_len + take];
// A v0.5-v0.7 header is measured by its own size fields; once whole,
// the decoder starts it from the same reader (upstream's
// `ZSTD_decompressStream` hands such a frame to its legacy streams).
if !dctx.magicless
&& let Some(version) = codec::decoding::LegacyVersion::of(view)
{
if version
.header_size(view)
.is_some_and(|len| len <= view.len())
{
start_stream_frame(dctx, inp, src, &held[..held_len])?;
return Ok(FrameEntry::Started);
}
return Ok(hold_header(dctx, inp, view, take));
}
match codec::decoding::read_frame_header_info(view, dctx.magicless) {
Ok(_) => {
start_stream_frame(dctx, inp, src, &held[..held_len])?;
Expand All @@ -1105,16 +1120,7 @@ fn enter_stream_frame(
| ReadFrameHeaderError::DictionaryIdReadError(_)
| ReadFrameHeaderError::FrameContentSizeReadError(_),
) => {
debug_assert!(view.len() < FRAME_HEADER_MAX, "no header is longer");
inp.pos += take;
let mut bytes = [0; FRAME_HEADER_MAX];
bytes[..view.len()].copy_from_slice(view);
dctx.stream_entry = StreamEntry::Header {
bytes,
len: view.len(),
};
dctx.stream_frame_done = false;
return Ok(FrameEntry::WantsInput);
return Ok(hold_header(dctx, inp, view, take));
}
// A malformed header (bad magic, invalid descriptor, ...) is a
// decode error: the need-more-input hint with nothing consumed
Expand All @@ -1128,6 +1134,26 @@ fn enter_stream_frame(
}
}

/// Hold `view`, a frame header the input has not all brought yet: the `take`
/// bytes of it from the input are consumed, and the frame has begun.
fn hold_header(
dctx: &mut ZSTD_DCtx,
inp: &mut ZSTD_inBuffer,
view: &[u8],
take: usize,
) -> FrameEntry {
debug_assert!(view.len() < FRAME_HEADER_MAX, "no header is longer");
inp.pos += take;
let mut bytes = [0; FRAME_HEADER_MAX];
bytes[..view.len()].copy_from_slice(view);
dctx.stream_entry = StreamEntry::Header {
bytes,
len: view.len(),
};
dctx.stream_frame_done = false;
FrameEntry::WantsInput
}

/// Start the decoder on the frame whose header is `held` followed by the
/// input, taking from the input what the header still needed.
fn start_stream_frame(
Expand Down
1 change: 1 addition & 0 deletions c-api/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
mod bufferless;
mod cctx_params;
mod experimental;
mod legacy;
mod provided;
mod sequences;
mod streams;
Expand Down
Loading
Loading