Skip to content

Further optimization#6

Merged
youknowone merged 26 commits intomainfrom
trim
Apr 20, 2026
Merged

Further optimization#6
youknowone merged 26 commits intomainfrom
trim

Conversation

@youknowone
Copy link
Copy Markdown
Owner

@youknowone youknowone commented Apr 13, 2026

Summary by CodeRabbit

  • New Features

    • Added index-related FFI bindings and a new LZMA stream header size constant.
    • New trimmed performance comparison script and expanded benchmarking variants.
  • Bug Fixes

    • Operations after stream finish now return errors instead of panicking.
    • Stronger runtime checks ensure compressed input is fully consumed and output sizes match.
  • Chores

    • Raised minimum Rust toolchain to 1.85 and updated crates to edition 2024.
    • Separated bindgen feature control for the native bindings and added a lint allowance.
  • Documentation

    • Expanded performance workflow guidance and examples.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR modernizes the codebase: raises MSRV to Rust 1.85, updates editions, removes many extern "C" ABIs in favor of unsafe fn, renames in_0input, replaces numerous pointer ops with safer slice/pointer patterns, adds a threading module and common_types, tightens unsafe handling, and enhances benchmarking scripts and assertions.

Changes

Cohort / File(s) Summary
Manifests & CI
Cargo.toml, Cargo.lock.msrv, liblzma-sys/Cargo.toml, perf-probe/Cargo.toml, xz-sys/Cargo.toml, xz/Cargo.toml, .github/workflows/*
MSRV bumped 1.63→1.85, editions bumped to 2024, lockfile/dependency bumps, feature mapping change for liblzma-sys, CI matrix aligned, lint allow unsafe_op_in_unsafe_fn added.
FFI surface & bindings
liblzma-sys/src/manual.rs, liblzma-sys/src/bindgen.rs
New index-related FFI bindings and LZMA_STREAM_HEADER_SIZE; lzma_options_delta field renamed to raw r#type.
Core types & re-exports
xz/src/common/common_types.rs, xz/src/types.rs
Introduced common_types module with FFI-compatible structs and function-pointer aliases; many public types reworked (callback types, allocator, lz encoder/decoder, mf functions) and re-exported; read32le/write32le signatures updated.
Threading module
xz/src/common/threading.rs, xz/src/common/mod.rs, xz/src/types.rs
New threading module providing mythread_* wrappers (pthread/Win32), exported types/constants, and re-exports from types.rs.
ABI removal & callback typing
many xz/src/common/*, xz/src/simple/*, xz/src/lz/*, xz/src/lzma/*, xz/src/delta/*
Removed extern "C" from numerous internal/public(crate) functions and changed stored callback types to non-ABI unsafe fn variants; updated casts/assignments accordingly.
Parameter renames (in_0 → input)
large set under xz/src/common/*, xz/src/lzma/*, xz/src/simple/*, xz/src/lz/*, xz/src/delta/*
Systematic rename of input buffer parameter and all internal uses across many encoder/decoder APIs.
Unsafe handling & option checks
xz/src/common/*, src/write.rs, src/write/auto_finish.rs, src/stream.rs, others
Replaced many unwrap()s on optional callbacks with if let/match returning LZMA_PROG_ERROR or debug_assert! + unwrap_unchecked where appropriate; replaced panics with error propagation for finished streams.
Pointer → slice / safer access
xz/src/check/*.rs, xz/src/simple/riscv.rs, xz/src/delta/*.rs, xz/src/lzma/*
Converted pointer/offset helpers to slice-based APIs in hot paths; read32le/write32le now accept typed byte-array refs; many buffer helpers rewritten to use slices.
Static mut removal & consts
xz/src/lzma/fastpos_table.rs, xz/src/rangecoder/price_table.rs, xz/src/common/stream_flags_common.rs, xz/src/lzma/lzma_encoder_presets.rs
Changed several pub static mut tables to immutable pub static or const tables.
Index & stream state machines
xz/src/common/index_decoder.rs, xz/src/common/index.rs, xz/src/common/stream_decoder.rs
Refactored decoder state machines to structured match-based flow, added helpers (goto_out), prealloc normalization, and unit test for index prealloc behavior.
Allocation & allocator logic
xz/src/alloc.rs, xz/src/common/common.rs
Allocator helpers now fall back to internal alloc/free when callback absent; added lzma_alloc_object; adjusted rust_alloc_impl unsafe blocks and removed extern "C" on rust allocator entrypoints.
Filters & stream writer changes
src/stream.rs, src/write.rs, src/write/auto_finish.rs
Filters now stores boxed options in Vec<Box<...>>; added finished_error() and made finish/flush return io::Error instead of panicking.
Benchmarks & scripts
benches/backend_comparison.rs, perf-probe/src/main.rs, scripts/*.sh, docs/performance-workflow.md, scripts/compare_all_trimmed.sh
Enhanced assertions validating LZMA_OK, input/output consumption and sizes; added xz-sys backend; added trimmed-mean hyperfine orchestration script and updated workflow docs and build invocations.
Numerous architecture-specific refactors
xz/src/simple/* (arm/arm64/armthumb/ia64/powerpc/sparc/x86/riscv) and related
Rewrote many low-level coders to use pointer arithmetic or typed array views, removed extern "C" ABI markers for coder callbacks, and adjusted init wiring to new function-pointer types.
Minor tooling/test fixes
systest/Cargo.toml, systest/build.rs
Systest edition bumped to 2024, ctest version bumped, improved CARGO_TARGET_DIR resolution and removed redundant binding renames.

Sequence Diagram(s)

sequenceDiagram
    participant Main as Main Thread
    participant Lib as xz::threading::mythread_create
    participant OS as pthread/_beginthreadex
    participant Worker as Worker Thread

    rect rgba(100,150,240,0.5)
    Main->>Lib: mythread_create(func, arg)
    end

    rect rgba(120,200,80,0.5)
    Lib->>OS: create thread (mask adjust on POSIX)
    OS-->>Lib: thread handle / success
    end

    rect rgba(240,180,80,0.5)
    OS->>Worker: start trampoline -> worker(func,arg)
    Worker->>Main: signal via condvar/mutex (mythread_cond_signal)
    Main->>Lib: mythread_join(handle)
    Lib->>OS: join / WaitForSingleObject
    OS-->>Lib: join result
    Lib-->>Main: return
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–75 minutes

Possibly related PRs

Poem

🐰 I nibble bytes and hop through code,
From in_0 to input down the road.
Externs tucked and slices neat,
Threads and types now dance a beat.
From 1.63 we leapt to 1.85 — hooray! 🥕✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch trim

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Line 54: The dev-dependency entry for liblzma-sys still pulls in its default
features (re-enabling bindgen); update the liblzma-sys dependency declarations
so they set default-features = false (both the target-specific dev-dependency
and the liblzma-sys = ["dep:liblzma-sys"] mapping) to prevent the bindgen
feature from being activated for benches/examples/tests; look for the
liblzma-sys entries and add default-features = false (and adjust to a table form
if needed) to ensure the bindgen feature stays disabled.

In `@perf-probe/src/main.rs`:
- Around line 530-535: The assertion requiring full consumption
(assert_eq!(in_pos, compressed.len(), ...)) is too strict for
lzma_stream_buffer_decode because it may validly leave trailing bytes; update
the logic around the lzma_stream_buffer_decode result (the in_pos check) to skip
enforcing exact consumption for external/compressed inputs loaded by
load_compressed_input and only assert exact-consumption for synthesized
single-stream data (or gate it behind an opt-in flag); identify and modify the
assert that compares in_pos and compressed.len(), and adjust call sites and any
tests that rely on full-consumption so that trailing bytes or concatenated
streams are accepted for --compressed-input while preserving the strict check
where appropriate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7404981b-7d93-4b32-9119-42d94fa8d4f1

📥 Commits

Reviewing files that changed from the base of the PR and between 1fad07b and c6236a1.

📒 Files selected for processing (8)
  • Cargo.toml
  • benches/backend_comparison.rs
  • liblzma-sys/src/manual.rs
  • perf-probe/src/main.rs
  • scripts/compare_api_workloads.sh
  • scripts/compare_backends.sh
  • scripts/profile_backend.sh
  • xz/src/common/common.rs

Comment thread Cargo.toml
xz = ["dep:xz"]
xz-sys = ["dep:xz-sys"]
liblzma-sys = ["dep:liblzma-sys", "liblzma-sys/bindgen"]
liblzma-sys = ["dep:liblzma-sys"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Cargo, when the same crate appears in [dependencies]and[dev-dependencies], are features unified for benches/examples/tests, and do default features from the dev-dependency stay enabled?

💡 Result:

Yes, when the same crate appears in both [dependencies] and [dev-dependencies], Cargo unifies the features for builds that include dev-dependencies (benches, examples, tests). This uses the union of features specified in both sections. Default features from the dev-dependency declaration remain enabled unless explicitly disabled there (via default-features = false). For normal builds (cargo build on lib/bin), only [dependencies] features apply (dev-deps ignored). This behavior depends on the feature resolver version (default "2" for edition 2021+, controlled by top-level resolver field). Resolver v2 avoids unwanted unification but includes dev-dep features when building dev targets like tests/examples/benches. Example from docs: [dependencies] serde = {version = "1.0", default-features = false} [dev-dependencies] serde = {version = "1.0", features = ["std"]} - cargo build: serde without features (defaults off). - cargo test: serde with "std" (union, defaults from dev-dep enabled since not disabled).

Citations:


🏁 Script executed:

# Check the structure of Cargo.toml, particularly around the lines mentioned
sed -n '20,60p' Cargo.toml | cat -n

Repository: youknowone/xz-rs

Length of output: 1680


liblzma-sys still enables bindgen for dev targets.

Line 45 defines a target-specific dev-dependency liblzma-sys = { path = "liblzma-sys" } without default-features = false. When benches/examples/tests are built with the liblzma-sys feature, Cargo unifies features across dependencies and dev-dependencies, re-enabling the default bindgen feature from the dev-dependency. This defeats the goal of disabling it at Line 25 and keeps the libclang requirement around for those builds.

♻️ Proposed fix
 [target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
 xz-sys = { path = "xz-sys" }
-liblzma-sys = { path = "liblzma-sys" }
+liblzma-sys = { path = "liblzma-sys", default-features = false }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 54, The dev-dependency entry for liblzma-sys still pulls
in its default features (re-enabling bindgen); update the liblzma-sys dependency
declarations so they set default-features = false (both the target-specific
dev-dependency and the liblzma-sys = ["dep:liblzma-sys"] mapping) to prevent the
bindgen feature from being activated for benches/examples/tests; look for the
liblzma-sys entries and add default-features = false (and adjust to a table form
if needed) to ensure the bindgen feature stays disabled.

Comment thread perf-probe/src/main.rs
Comment on lines +530 to +535
assert_eq!(
in_pos,
compressed.len(),
"{BACKEND_NAME} decode left trailing input: consumed {in_pos} of {} bytes",
compressed.len()
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does liblzma's lzma_stream_buffer_decodeallow trailing bytes by returning success and advancingin_pos only to the first unused byte?

💡 Result:

Yes, liblzma's lzma_stream_buffer_decode allows trailing bytes by returning LZMA_OK (success) and advancing *in_pos to the position of the first unused byte after successfully decoding a complete .xz Stream or .lzma file from the input buffer. This behavior follows the single-call decoding pattern in liblzma, where like lzma_code, it consumes only the necessary bytes for one complete stream and updates *in_pos accordingly, leaving any trailing data unconsumed without error if the decoding succeeded. The official documentation states that *in_pos and *out_pos are updated only if decoding succeeds (LZMA_OK), confirming partial consumption on success. Analogous streaming decoders (e.g., Python's LZMADecompressor) explicitly provide unconsumed_tail for trailing bytes after LZMA_STREAM_END, supporting this design.

Citations:


🏁 Script executed:

cat -n perf-probe/src/main.rs | sed -n '525,540p'

Repository: youknowone/xz-rs

Length of output: 639


🏁 Script executed:

cat -n perf-probe/src/main.rs | sed -n '500,545p'

Repository: youknowone/xz-rs

Length of output: 1659


🏁 Script executed:

rg -n "backend_decode\(" perf-probe/src/main.rs

Repository: youknowone/xz-rs

Length of output: 281


🏁 Script executed:

cat -n perf-probe/src/main.rs | sed -n '355,385p'

Repository: youknowone/xz-rs

Length of output: 1357


🏁 Script executed:

rg -n "fn load_compressed_input" perf-probe/src/main.rs

Repository: youknowone/xz-rs

Length of output: 151


🏁 Script executed:

cat -n perf-probe/src/main.rs | sed -n '299,330p'

Repository: youknowone/xz-rs

Length of output: 1394


🏁 Script executed:

rg -n "compressed.input" perf-probe/src/main.rs | head -20

Repository: youknowone/xz-rs

Length of output: 1277


Don't require full input consumption for --compressed-input.

lzma_stream_buffer_decode can legitimately return success with in_pos < compressed.len() when the buffer contains trailing bytes or extra concatenated streams. The assertion at lines 530–535 enforces full input consumption, which rejects valid inputs the liblzma API itself accepts. This is a user-visible regression for externally supplied data via --compressed-input (see load_compressed_input, line 299–312). Keep the exact-consumption check for synthesized single-stream data only, or make it opt-in.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@perf-probe/src/main.rs` around lines 530 - 535, The assertion requiring full
consumption (assert_eq!(in_pos, compressed.len(), ...)) is too strict for
lzma_stream_buffer_decode because it may validly leave trailing bytes; update
the logic around the lzma_stream_buffer_decode result (the in_pos check) to skip
enforcing exact consumption for external/compressed inputs loaded by
load_compressed_input and only assert exact-consumption for synthesized
single-stream data (or gate it behind an opt-in flag); identify and modify the
assert that compares in_pos and compressed.len(), and adjust call sites and any
tests that rely on full-consumption so that trailing bytes or concatenated
streams are accepted for --compressed-input while preserving the strict check
where appropriate.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 28

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
src/write.rs (1)

103-123: ⚠️ Potential issue | 🟡 Minor

Stale “# Panics” docs on try_finish (and write).

With this change, operating on an already-finished stream no longer panics — dump() and flush() now return finished_error(), and write() propagates it through dump()?. The doc comment still claims “further calls to write may result in a panic” and carries a # Panics section, which is now incorrect and misleading to API consumers.

📝 Proposed doc update (apply to both encoder and decoder)
     /// Attempt to finish this output stream, writing out final chunks of data.
     ///
     /// Note that this function can only be used once data has finished being
-    /// written to the output stream. After this function is called then further
-    /// calls to `write` may result in a panic.
-    ///
-    /// # Panics
-    ///
-    /// Attempts to write data to this stream may result in a panic after this
-    /// function is called.
+    /// written to the output stream. After this function is called, further
+    /// calls to `write`, `flush`, or `finish` will return an `io::Error` with
+    /// kind `BrokenPipe`.

The same update applies to the corresponding try_finish doc on XzDecoder (lines 288–297) and arguably the type-level docs on XzEncoder/XzDecoder.

Also applies to: 288-320

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/write.rs` around lines 103 - 123, The doc comment for try_finish (and
similar docs on XzDecoder::try_finish and write) is stale: it claims further
calls to write may panic and includes a "# Panics" section, but dump()/flush()
now return finished_error() and write() propagates it instead of panicking.
Update the documentation on XzEncoder::try_finish, XzDecoder::try_finish and the
write() docs to remove the "# Panics" section and the sentence about panics, and
replace it with a concise note that subsequent calls will return an error (via
finished_error()) rather than panic; mention the relevant methods (try_finish,
write, dump, flush, finished_error) so readers can find the behavior in code.
xz/src/common/index_decoder.rs (1)

339-387: 🧹 Nitpick | 🔵 Trivial

Nit: ret_ / ret__0 naming is inconsistent with the rest of the PR.

microlzma_encoder.rs (and the new code in this file) normalized the C2Rust-style ret_ / ret__0 locals to a single shadowed ret. These two functions still carry the old names — minor inconsistency while you're in the area.

Suggested rename
-    let ret_: lzma_ret = lzma_strm_init(strm);
-    if ret_ != LZMA_OK {
-        return ret_;
-    }
-    let ret__0: lzma_ret = lzma_index_decoder_init(
+    let ret: lzma_ret = lzma_strm_init(strm);
+    if ret != LZMA_OK {
+        return ret;
+    }
+    let ret: lzma_ret = lzma_index_decoder_init(
         ::core::ptr::addr_of_mut!((*(*strm).internal).next),
         (*strm).allocator,
         i,
         memlimit,
     );
-    if ret__0 != LZMA_OK {
+    if ret != LZMA_OK {
         lzma_end(strm);
-        return ret__0;
+        return ret;
     }

And similarly for ret_ in lzma_index_buffer_decode (line 383).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/index_decoder.rs` around lines 339 - 387, Rename the local
return-value variables in index_decoder_reset and lzma_index_buffer_decode to
the normalized shadowed name "ret" (instead of "ret_" and "ret__0") to match the
rest of the PR; e.g., in index_decoder_reset replace the lzma_ret variables ret_
and ret__0 with a single let mut ret: lzma_ret = lzma_strm_init(strm); then
shadow/assign ret for the lzma_index_decoder_init call and for the subsequent
checks, and in lzma_index_buffer_decode replace ret_ with ret and use the same
shadowing pattern when calling index_decoder_reset, preserving all existing
return checks and behavior.
xz/src/common/block_decoder.rs (1)

56-68: ⚠️ Potential issue | 🟠 Major

Same unwrap_unchecked safety regression as in stream_encoder.rs.

Downgrading from .unwrap() to debug_assert! + unwrap_unchecked() makes a None callback UB in release instead of a panic, and is inconsistent with the match → LZMA_PROG_ERROR pattern used in microlzma_decoder.rs / alone_encoder.rs in this same PR. See the detailed rationale on stream_encoder.rs. Suggest aligning on the explicit-match pattern here as well.

🛡️ Proposed fix
-            debug_assert!((*coder).next.code.is_some());
-            let code = (*coder).next.code.unwrap_unchecked();
-            let ret: lzma_ret = code(
+            let code = match (*coder).next.code {
+                Some(code) => code,
+                None => return LZMA_PROG_ERROR,
+            };
+            let ret: lzma_ret = code(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/block_decoder.rs` around lines 56 - 68, The current use of
debug_assert! + unwrap_unchecked() on (*coder).next.code in block_decoder.rs
introduces UB in release builds; replace that with an explicit match on
(*coder).next.code as in microlzma_decoder.rs/alone_encoder.rs: if Some(code) =>
call code(...) and assign the lzma_ret result, else return LZMA_PROG_ERROR (or
set ret = LZMA_PROG_ERROR) before exiting the function. Update the handling
around the lzma_ret variable (and any early return) so the None branch mirrors
the established explicit-match error path instead of using unwrap_unchecked().
xz/src/lz/lz_decoder.rs (1)

112-131: ⚠️ Potential issue | 🟡 Minor

Hoist the callback once to retire unwrap_unchecked.

The invariant that (*coder).next.code is Some inside this loop holds today only because the early return at line 112 is reached before the loop starts and nothing in the loop rebinds next.code. A future refactor that reassigns next.code (or a new path into this loop) would silently become UB in release builds, where debug_assert! is a no-op.

Caching the pointer once before the loop makes the contract explicit and costs nothing:

🛡️ Suggested tightening
-    if (*coder).next.code.is_none() {
-        return decode_buffer(coder, input, in_pos, in_size, out, out_pos, out_size);
-    }
+    let Some(next_code) = (*coder).next.code else {
+        return decode_buffer(coder, input, in_pos, in_size, out, out_pos, out_size);
+    };
     while *out_pos < out_size {
         if !(*coder).next_finished && (*coder).temp.pos == (*coder).temp.size {
             (*coder).temp.pos = 0;
             (*coder).temp.size = 0;
-            debug_assert!((*coder).next.code.is_some());
-            let next_code = (*coder).next.code.unwrap_unchecked();
             let ret: lzma_ret = next_code(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/lz/lz_decoder.rs` around lines 112 - 131, The loop relies on the
invariant that (*coder).next.code is Some but still calls unwrap_unchecked
inside the loop, which is fragile; hoist and cache the callback once before
entering the while loop: after the early return that calls decode_buffer, assert
or expect that (*coder).next.code is Some and bind let next_code =
(*coder).next.code.unwrap() (or use expect with a clear message), then inside
the loop call next_code(...) instead of unwrapping each iteration and remove the
unwrap_unchecked/debug_assert there; this ensures the callback pointer is
captured (referencing coder.next.code, next_code, decode_buffer, and
next_finished) and avoids potential UB from future refactors.
xz/src/common/block_buffer_encoder.rs (1)

198-205: ⚠️ Potential issue | 🟡 Minor

Semantic change: null input now permitted when in_size == 0.

Operator precedence parses this as block.is_null() || (input.is_null() && in_size != 0) || ..., allowing callers to pass input = NULL, in_size = 0. This matches upstream liblzma's API contract and is consistently applied across lzma_block_buffer_encode, lzma_block_uncomp_encode, and the filter buffer encoder functions. Existing callers in the codebase already handle this correctly: stream_buffer_encoder guards with if in_size > 0 before calling, and stream_encoder_mt allocates the input buffer so it is never null.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/block_buffer_encoder.rs` around lines 198 - 205, The
null-input-permitted-when-in_size==0 behavior should be made explicit: in the
conditional inside the block_buffer_encoder (the if that checks block.is_null(),
input.is_null(), out.is_null(), out_pos, and *out_pos > out_size) group the
input check with parentheses as (input.is_null() && in_size != 0) so the
intent/matching upstream contract is clear and cannot be misread by future
maintainers; adjust the conditional expression in the function containing that
if accordingly and leave the rest of the checks unchanged.
xz/src/common/string_conversion.rs (1)

659-661: 🛠️ Refactor suggestion | 🟠 Major

Use the new array_size helper here too — hardcoded [filter_codec_def; 11] sizes will drift.

Lines 659, 999, and 1085 still embed the fixed length 11 via size_of::<[filter_codec_def; 11]>() / size_of::<filter_codec_def>(). If filter_name_map ever gains or loses entries, these three sites won't be updated automatically and the loops will silently iterate past the array or terminate early — a real correctness risk in a file that otherwise already routes through array_size(...) for bcj_optmap, delta_optmap, and lzma12_optmap.

Proposed fix (applied to each site)
-    while i < core::mem::size_of::<[filter_codec_def; 11]>()
-        / core::mem::size_of::<filter_codec_def>()
+    while i < array_size(::core::ptr::addr_of!(filter_name_map))

Also applies to: 999-1001, 1084-1087

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/string_conversion.rs` around lines 659 - 661, Replace the
hardcoded array-length expression size_of::<[filter_codec_def; 11]>() /
size_of::<filter_codec_def>() with a call to the existing helper
array_size(filter_name_map) so the loops over filter_codec_def use the actual
filter_name_map length; locate the three sites (the while loop using i <
size_of::<[filter_codec_def; 11]>() / size_of::<filter_codec_def>() and the two
other similar occurrences around the references to
filter_codec_def/filter_name_map) and change the bound to
array_size(filter_name_map) (or an equivalent array_size call that returns the
correct usize) to avoid drifting when filter_name_map changes.
xz/src/lzma/lzma2_decoder.rs (1)

92-99: ⚠️ Potential issue | 🟡 Minor

unwrap_unchecked() replaces previous panic with UB in release.

The debug_assert!(...is_some()) + unwrap_unchecked() pattern is fine if (*coder).lzma.reset / set_uncompressed are always wired up before lzma2_decode is invoked — which currently appears to be the case via lzma_lzma_decoder_create. However, the previous .unwrap() would panic in release; the new form is undefined behavior if the invariant is ever broken (e.g., by a future refactor or error path that skips full init). Consider documenting the invariant near the declarations of lzma.reset/set_uncompressed, or keeping a release-time None guard returning LZMA_PROG_ERROR instead.

Also applies to: 126-132, 153-158

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/lzma/lzma2_decoder.rs` around lines 92 - 99, The code replaces prior
runtime panics with unsafe UB by calling unwrap_unchecked() on
(*coder).lzma.reset (and similarly on set_uncompressed) after only a
debug_assert; instead restore a safe release-time guard: check if
(*coder).lzma.reset (and set_uncompressed) is None and return an error (e.g.,
LZMA_PROG_ERROR) from lzma2_decode instead of calling unwrap_unchecked(), or
document the invariant near the lzma.reset/set_uncompressed declarations and
keep unwrap() to preserve a release panic; locate uses in lzma2_decode and
initialization in lzma_lzma_decoder_create to implement the chosen safe guard.
xz/src/simple/simple_coder.rs (1)

14-40: 🧹 Nitpick | 🔵 Trivial

Redundant None handling in copy_or_code.

The else branch is only reached when (*coder).next.code.is_some(), yet the match on lines 20–23 still handles None. Either collapse the is_none() check + match into a single match, or use unwrap_unchecked() to avoid the dead arm (consistent with lz_encoder.rs::fill_window, which uses unreachable_unchecked()).

♻️ Proposed refactor
-    if (*coder).next.code.is_none() {
-        lzma_bufcpy(input, in_pos, in_size, out, out_pos, out_size);
-        if (*coder).is_encoder && action == LZMA_FINISH && *in_pos == in_size {
-            (*coder).end_was_reached = true;
-        }
-    } else {
-        let code = match (*coder).next.code {
-            Some(code) => code,
-            None => return LZMA_PROG_ERROR,
-        };
-        let ret: lzma_ret = code(
-            (*coder).next.coder,
-            allocator,
-            input,
-            in_pos,
-            in_size,
-            out,
-            out_pos,
-            out_size,
-            action,
-        );
-        if ret == LZMA_STREAM_END {
-            (*coder).end_was_reached = true;
-        } else if ret != LZMA_OK {
-            return ret;
-        }
-    }
+    match (*coder).next.code {
+        None => {
+            lzma_bufcpy(input, in_pos, in_size, out, out_pos, out_size);
+            if (*coder).is_encoder && action == LZMA_FINISH && *in_pos == in_size {
+                (*coder).end_was_reached = true;
+            }
+        }
+        Some(code) => {
+            let ret: lzma_ret = code(
+                (*coder).next.coder, allocator, input, in_pos, in_size,
+                out, out_pos, out_size, action,
+            );
+            if ret == LZMA_STREAM_END {
+                (*coder).end_was_reached = true;
+            } else if ret != LZMA_OK {
+                return ret;
+            }
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/simple/simple_coder.rs` around lines 14 - 40, The branch currently
checks (*coder).next.code.is_none() then later matches (*coder).next.code with a
redundant None arm; simplify copy_or_code by replacing the is_none() + separate
match with a single match on (*coder).next.code: handle None by calling
lzma_bufcpy and setting (*coder).end_was_reached when appropriate, and handle
Some(code) by invoking code((*coder).next.coder, ...) and processing ret
(setting end_was_reached on LZMA_STREAM_END or returning ret on non-OK). This
removes the dead None arm and mirrors the safe pattern used elsewhere (avoid
introducing unsafe unwraps/unreachable_unchecked unless explicitly desired).
xz/src/common/filter_encoder.rs (1)

289-316: 🧹 Nitpick | 🔵 Trivial

Redundant None check in lzma_filters_update.

Line 289 already returns LZMA_PROG_ERROR when next.update is None; the subsequent match on lines 313–316 re-handles the unreachable None case. Consolidate into a single match (same pattern seen in lz_encoder.rs::lz_encoder_update).

♻️ Proposed cleanup
-    if (*(*strm).internal).next.update.is_none() {
-        return LZMA_PROG_ERROR;
-    }
     if lzma_raw_encoder_memusage(filters) == UINT64_MAX {
         return LZMA_OPTIONS_ERROR;
     }
@@
-    let update = match (*(*strm).internal).next.update {
-        Some(update) => update,
+    let update = match (*(*strm).internal).next.update {
+        Some(update) => update,
         None => return LZMA_PROG_ERROR,
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/filter_encoder.rs` around lines 289 - 316, The code in
lzma_filters_update redundantly checks (*(*strm).internal).next.update for None
twice; remove the initial if that returns LZMA_PROG_ERROR and rely on the single
match later that binds update (i.e., keep the existing match block "let update =
match (*(*strm).internal).next.update { Some(update) => update, None => return
LZMA_PROG_ERROR }" and delete the earlier "if
(*(*strm).internal).next.update.is_none() { return LZMA_PROG_ERROR; }" so there
is only one canonical None-check as in lz_encoder.rs::lz_encoder_update).
xz/src/common/common.rs (2)

101-119: 🧹 Nitpick | 🔵 Trivial

Minor: redundant zero-fill after calloc fallback in lzma_alloc_zero.

When allocator is non-null but alloc is None, the fallback calloc(1, size) already returns zeroed memory, yet the code then calls core::ptr::write_bytes(ptr, 0, size) again. Not incorrect, but consider routing the fallback through the "no custom allocator" branch or gating the write_bytes on the custom alloc path (where the returned memory may not be zeroed).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/common.rs` around lines 101 - 119, In lzma_alloc_zero, avoid
double-zeroing memory returned by calloc: when allocator is non-null and
(*allocator).alloc is None, call the existing calloc path (or set ptr from
calloc) but do not call core::ptr::write_bytes again; only call write_bytes when
using a custom allocator (i.e., when alloc is Some and the returned ptr needs
zeroing). Update the logic around lzma_alloc_zero, lzma_allocator, alloc, calloc
and the write_bytes call so the calloc fallback is routed through the
zeroed-memory branch or the write_bytes call is gated to custom-alloc outcomes.

24-57: ⚠️ Potential issue | 🔴 Critical

Critical: wasm malloc/calloc will fail to compile — unsafe function calls outside an unsafe context.

The functions at lines 24 and 40 call std::alloc::alloc (line 29) and std::alloc::alloc_zeroed (line 49), which are unsafe fns, but do not wrap these calls in an unsafe { } block. This is a hard compile error and will prevent the wasm target from building.

Fix
 fn malloc(size: size_t) -> *mut c_void {
     let layout = match rust_alloc_layout(size as usize) {
         Some(layout) => layout,
         None => return core::ptr::null_mut(),
     };
-    let base = alloc(layout);
+    let base = unsafe { alloc(layout) };
     if base.is_null() {
         return core::ptr::null_mut();
     }
 fn calloc(count: size_t, size: size_t) -> *mut c_void {
     let size = match (count as usize).checked_mul(size as usize) {
         Some(size) => size,
         None => return core::ptr::null_mut(),
     };
     let layout = match rust_alloc_layout(size) {
         Some(layout) => layout,
         None => return core::ptr::null_mut(),
     };
-    let base = alloc_zeroed(layout);
+    let base = unsafe { alloc_zeroed(layout) };
     if base.is_null() {
         return core::ptr::null_mut();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/common.rs` around lines 24 - 57, The malloc and calloc
implementations call the unsafe functions alloc and alloc_zeroed without an
unsafe block; wrap the calls to alloc and alloc_zeroed (and any pointer
arithmetic/dereference like *(base as *mut usize) and base.add(...)) inside
unsafe { ... } so those operations occur within an unsafe context in fn malloc
and fn calloc, keeping the existing early-null checks and use of
rust_alloc_layout and LZMA_ALLOC_HEADER_SIZE intact.
♻️ Duplicate comments (1)
perf-probe/src/main.rs (1)

531-540: ⚠️ Potential issue | 🟠 Major

Asserting full input consumption rejects valid multi-stream / trailing-byte inputs.

lzma_stream_buffer_decode is documented to return LZMA_OK after decoding a single .xz stream and leaving *in_pos at the first unused byte; trailing bytes or concatenated streams are not errors. For --compressed-input (see load_compressed_input at lines 299–312), user-supplied files may legitimately have trailing data, and this assertion will panic on them. Consider gating the exact-consumption check to the synthesized single-stream path only (i.e., when config.compressed_input_path.is_none()), or making it opt-in.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@perf-probe/src/main.rs` around lines 531 - 540, The assertions that in_pos ==
compressed.len() and out_pos == out_size reject valid multi-stream/trailing-byte
inputs because lzma_stream_buffer_decode may stop at the first unused byte;
update the checks so exact-consumption is only enforced for the synthesized
single-stream path: guard the in_pos/out_pos equality assertions behind a
condition that the input was synthesized (e.g., when
config.compressed_input_path.is_none()), or make the check
optional/configurable; references: in_pos, compressed.len(), out_pos, out_size,
lzma_stream_buffer_decode, load_compressed_input, and
config.compressed_input_path.is_none().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@patch.diff`:
- Around line 173-181: The added runtime guards (checking *in_pos, *out_pos vs
in_size/out_size and null checks for in_0/out) make the following debug_assert!s
(the three debug_assert! lines immediately after) unreachable; remove the
redundancy by either deleting those debug_assert! lines
(debug_assert!(!in_0.is_null() || *in_pos == in_size);
debug_assert!(!out.is_null() || *out_pos == out_size); debug_assert!(*in_pos <=
in_size);) or, if you want a debug-time panic instead of a silent return, remove
the new early-return guards and keep the debug_assert!s; update the block around
the checks of in_pos, out_pos, in_size, out_size, in_0 and out accordingly so
only one style (runtime guard or debug_assert) remains.

In `@scripts/compare_all_trimmed.sh`:
- Around line 85-105: The TSV row is written to SUMMARY_TSV before running
hyperfine in the record_pair function, causing orphaned entries if hyperfine
fails; move the printf that appends "$stem","$label","$rust_name","$c_name" so
it executes only after the hyperfine command completes successfully (i.e., run
hyperfine first inside record_pair and then append the TSV row), keeping the
same field order and file target to preserve downstream tooling that reads
SUMMARY_TSV.
- Around line 282-292: The summary header currently uses the loop-bound variable
rust_name (via len(by_name[rust_name]['times'])) which can be undefined or
misleading; change the code that writes the header to compute the measured run
count explicitly from the collected data (e.g., derive it from rows or from
summary_tsv) before writing the file, and handle the empty-case: if
summary_tsv/rows is empty write "0" or emit a clear error; update references to
use that new local variable (e.g., measured_runs or runs_count) instead of
rust_name/by_name inside the md.open block so the header is accurate and robust.
- Around line 270-277: Guard the division-by-zero when computing ratio = c /
rust: detect if rust is zero (or effectively zero, e.g., math.isclose(rust,
0.0)) before dividing; if rust == 0 and c > 0 set ratio to float('inf'), if both
rust and c are zero set ratio to 1.0 (or another sentinel), then compute status
and append to rows and failed the same way; reference the variables trimmed_mean
results (rust, c), ratio, status, rows and failed to locate and update the
logic.

In `@scripts/compare_upstream_c_tests.sh`:
- Around line 62-79: The script is fragile when TESTS is empty under set -u and
constructs unsafe/oversized SUFFIX filenames; fix by guarding array expansions
and sanitizing/truncating/hashing the suffix: first, avoid unbound expansion by
using a guarded check like if [[ ${`#TESTS`[@]} -gt 0 ]] (or use the ${TESTS[@]+x}
pattern) before touching TESTS, and only append to XZ_CMD and C_CMD inside that
guarded branch (refer to TESTS, XZ_CMD, C_CMD, and the for loop); second, build
SUFFIX by first sanitizing test names (replace '/' and whitespace with '_' or
remove unsafe chars), compute a combined byte length and if it exceeds a
threshold (e.g., 200 bytes) or if there are more than N tests, replace the
full-list suffix with a short deterministic hash (e.g., sha1 of the joined
names) and optionally a safe readable prefix, ensuring the final SUFFIX is
capped to filesystem-safe length.

In `@scripts/compare_workloads.sh`:
- Around line 54-80: The script currently overwrites a user-provided
EXPECTED_SIZE when COMPRESSED_INPUT is not set by unconditionally assigning
EXPECTED_SIZE="$SIZE"; update the logic in the block that generates
COMPRESSED_INPUT (the code that calls "$C_BIN" --workload encode ...) to first
check if EXPECTED_SIZE is non-empty and, if so, verify it matches SIZE and error
out (or at least warn and exit) on mismatch; otherwise, only set
EXPECTED_SIZE="$SIZE" when EXPECTED_SIZE was not supplied. Reference the
variables EXPECTED_SIZE, COMPRESSED_INPUT, SIZE and the encode invocation using
"$C_BIN" and RAW_ARGS to locate the change.
- Around line 73-76: The encode bootstrap is forwarding unfiltered RAW_ARGS to
the encode subcommand and clobbering any user-specified expected size; change
the invocation that builds COMPRESSED_INPUT to pass PASSTHROUGH_ARGS instead of
RAW_ARGS to avoid forwarding script-only flags to encode (reference the command
using C_BIN and the encode mode), and make EXPECTED_SIZE respect a previously
captured value (do not unconditionally set EXPECTED_SIZE="$SIZE"—only set it to
SIZE when EXPECTED_SIZE is empty/undefined) so a user-provided --expected-size
is preserved.

In `@scripts/run_upstream_c_tests.sh`:
- Around line 230-233: The label "compiled" is misleading because ${`#TESTS`[@]}
is the total input list, not the number that actually compiled or ran; either
rename the output to "attempted" to reflect total tests (change the echo
referencing TESTS) or compute an explicit run/compiled count by adding a counter
(e.g., RUN_COUNT) incremented in the test execution loop when a test actually
proceeds to compile/run, and then echo that counter instead of ${`#TESTS`[@]};
keep the existing skips and failures variables (skips, failures) unchanged and
update the echo lines to use TESTS, skips, failures and the new RUN_COUNT as
appropriate.
- Around line 113-121: The current assignment to NATIVE_LIBS runs the cargo
rustc pipeline inside a subshell so failures are swallowed; instead run the
"${XZ_BUILD_ENV[@]}" CARGO_TARGET_DIR="$TARGET_DIR" cargo rustc ... pipeline
with its stdout/stderr redirected to a temporary file, check the cargo command's
exit status (or enable pipefail for the local context) and exit with a clear
message if non-zero, then read/parse the temp file into NATIVE_LIBS (or fall
back to DEFAULT_NATIVE_LIBS). Update the block that builds NATIVE_LIBS (the
command that calls cargo rustc and sed/tail) to explicitly capture output, test
the command status, fail-fast with a helpful error, and cleanup the temp file.
- Around line 124-138: The use of "ls -t ... | head -n 1" to set STATICLIB is
fragile (SC2012); replace those occurrences (the assignments to STATICLIB for
libxz_sys and liblzma-sys for both release and debug branches) with a portable
mtime-based file discovery using find/stat/sort so the newest matching lib file
under TARGET_DIR is chosen reliably; ensure you update the branches that check
PROFILE and SKIP_BUILD, reference the STATICLIB variable and the
liblzma-sys/libxz_sys filename patterns, and handle platform differences (GNU
stat vs BSD stat) so the script works on Linux and macOS.

In `@src/write.rs`:
- Around line 35-38: Extract the duplicate finished_error implementations into a
single module-private free function (e.g. fn finished_error() -> io::Error) and
remove the per-impl methods from XzEncoder and XzDecoder; update all call sites
(including where code currently uses XzEncoder::<W>::finished_error in
auto_finish.rs) to call the new free function directly (replace
Err(XzEncoder::finished_error()) / .ok_or_else(XzEncoder::finished_error) with
Err(finished_error()) / .ok_or_else(finished_error)) so the error kind/message
is unified across encoder, decoder, and auto-finish logic.
- Around line 193-197: The current finished-error uses io::ErrorKind::BrokenPipe
which is debatable for a locally-finished stream; update the error kind in the
finished-error helper (Self::finished_error()) to a more semantically
appropriate kind (e.g., io::ErrorKind::UnexpectedEof or io::ErrorKind::Other) so
flush(), dump(), and finish() propagate a consistent, semantically-correct
error; pick one kind, replace BrokenPipe with that kind in
Self::finished_error(), and run tests to ensure callers depending on ErrorKind
are considered.

In `@xz/src/common/common_types.rs`:
- Around line 1-87: The review notes that callback types (e.g.
lzma_end_function, lzma_code_function, lzma_init_function) were changed from
extern "C" fn to plain unsafe fn while structs like lzma_next_coder_s and
lzma_internal_s retain #[repr(C)], which can mislead readers into thinking these
are FFI-exported; add brief doc comments to lzma_next_coder_s,
lzma_filter_info_s, and lzma_internal_s (and optionally above the
lzma_*_function type aliases) stating these are internal-only runtime/state
structs and their callbacks use the Rust ABI and are never passed across an FFI
boundary, so #[repr(C)] is used only for layout compatibility, not for
C-callable function pointers.

In `@xz/src/common/common.rs`:
- Around line 144-153: The two debug_assert! lines checking input/out nullity
against in_pos/in_size are now unreachable due to the preceding early returns;
remove the dead assertions (debug_assert!(!input.is_null() || *in_pos ==
in_size) and debug_assert!(!out.is_null() || *out_pos == out_size)) from
common.rs, or if you prefer them as documentation, move those debug_assert!
checks above the early-return guards that inspect input.is_null()/out.is_null()
so they can actually fire; update only the debug_assert placements for the
variables input, out, in_pos, in_size accordingly.

In `@xz/src/common/filter_buffer_decoder.rs`:
- Line 33: The initial lzma_raw_decoder_init call stores its result in a
variable named ret which is later shadowed by a new let mut ret; rename the
first variable (e.g., to init_ret) to avoid shadowing and update any subsequent
references that expect that initial result; specifically change the binding
returned from lzma_raw_decoder_init (currently `ret`) to `init_ret` and leave
the later `let mut ret` as-is so uses of `init_ret` and `ret` are unambiguous
(refer to lzma_raw_decoder_init, the first `ret` declaration and the later `let
mut ret`).

In `@xz/src/common/index_decoder.rs`:
- Around line 162-183: Add a one-line comment inside the SEQ_CRC32 match arm
(the block that checks (*coder).crc32 and returns LZMA_OK or LZMA_STREAM_END)
explaining that the early return of LZMA_OK when *in_pos == in_size
intentionally bypasses the usual goto_out path because the CRC32 bytes must not
be reintegrated into (*coder).crc32/accumulator; clarify that pre-CRC bytes were
already folded into (*coder).crc32 before entering SEQ_CRC32 so the CRC bytes
are handled separately and must not be fed back.

In `@xz/src/common/stream_encoder.rs`:
- Around line 17-21: The new unsafe helper supported_action_slot introduces
unnecessary raw-pointer arithmetic and weaker safety; revert sites that use it
(e.g., the write to supported_actions[LZMA_RUN as usize]) to direct array
indexing (e.g., (*(*strm).internal).supported_actions[index] = ...), or if you
want a single choke point keep a safe signature like fn
supported_action_slot(actions: &mut [bool; 5], index: usize) -> &mut bool and
use checked indexing there; update all call sites (including the other
occurrences around the 424-443 region) to pass the array reference or use direct
indexing and remove the unsafe supported_action_slot function.
- Around line 127-139: The code currently uses debug_assert! plus
unwrap_unchecked() on option fields like (*coder).block_encoder.code,
(*coder).block_encoder.update and (*coder).index_encoder.code which risks UB in
release builds; change these sites to explicitly match the Option (e.g., match
(*coder).block_encoder.code { Some(code) => { ...call code... }, None => return
LZMA_PROG_ERROR }) or revert to using unwrap() so the code returns
LZMA_PROG_ERROR on missing callbacks consistent with microlzma_decoder.rs and
alone_encoder.rs; update the three invocation blocks (the call through
block_encoder.code, block_encoder.update, and index_encoder.code) to follow the
same match → return LZMA_PROG_ERROR pattern.

In `@xz/src/common/string_conversion.rs`:
- Around line 683-690: The None branch in the match over filter_name_map[i as
usize].parse is unreachable; replace the match with an explicit invariant check
like using .expect("filter_codec_def.parse must be Some") (or unreachable!()) on
filter_name_map[i as usize].parse to make the invariant explicit, and remove the
lzma_free/options/allocator cleanup and the crate::c_str!("Internal error")
return from that branch so the code simply binds parse and continues to call
parse(str, str_end, options); keep references to filter_name_map, parse,
lzma_free, options and allocator so you can find and update the match site.

In `@xz/src/common/threading.rs`:
- Around line 449-465: mythread_cond_timedwait currently returns 0 on success
and 1 on any failure on Windows, losing pthread-like semantics; modify
mythread_cond_timedwait to return 0 on success, ETIMEDOUT (60) when the wait
actually timed out (either when elapsed >= timeout or when
SleepConditionVariableCS fails and GetLastError() == ERROR_TIMEOUT), and a
distinct non-zero errno-like value for other failures (e.g., return the Windows
error mapped or a generic non-zero) by checking the result of
SleepConditionVariableCS and calling GetLastError(); use the existing
condtime.start/timeout values to detect elapsed timeout before calling
SleepConditionVariableCS and return ETIMEDOUT in that case as well.
- Around line 222-291: The Windows thread path currently discards the
pthread-style return value (mythread_start always returns 0 and mythread_join
ignores GetExitCodeThread), so update the Windows implementations to document
this intentional mismatch: add a concise comment above mythread_start, the
#[cfg(windows)] mythread_create, and the Windows branch of mythread_join
explaining that func: Option<unsafe extern "C" fn(*mut c_void) -> *mut c_void>
returns a pointer which is intentionally not propagated via
_beginthreadex/_endthreadex and therefore pthread-style return values are not
recoverable on Windows; mention that upstream xz-utils does not rely on those
return values and note the rationale to avoid surprising future maintainers.
- Around line 29-106: The file defines Darwin-specific thread and clock layouts
for all non-Windows targets causing memory corruption (e.g., sigset_t as u32) —
replace these hand-rolled types and constants with the platform-correct libc
definitions: stop using the local __darwin_* and _opaque_pthread_* typedefs and
instead import libc::sigset_t, libc::pthread_t, libc::pthread_attr_t, and libc
clock constants (CLOCK_MONOTONIC, CLOCK_THREAD_CPUTIME_ID, CLOCK_REALTIME) so
Linux gets the right sizes; update callers such as mythread_create (which
currently passes addr_of_mut!(all) to pthread_sigmask) to use libc::sigset_t and
related APIs, and if you must keep any Darwin-only layouts, guard them with
#[cfg(target_os = "macos")] only.

In `@xz/src/lz/lz_encoder.rs`:
- Around line 72-75: The code uses core::hint::unreachable_unchecked() on
(*coder).next.code which is fragile; instead make the invariant local by
matching on (*coder).next.code inside fill_window (or the containing function)
as a single match expression so the None case is handled explicitly (e.g.,
return an Err/early return or handle the empty-case the same way the outer
is_none() check intended) and the Some(code) arm binds code for subsequent use;
replace the unreachable_unchecked() usage with that explicit None branch and
keep the rest of logic using the bound code.
- Around line 365-371: The code in lz_encoder_update redundantly checks
(*coder).lz.options_update twice; remove the initial if-block and collapse to a
single match or use if let to obtain options_update (e.g., match
(*coder).lz.options_update { Some(options_update) => ..., None => return
LZMA_PROG_ERROR }), ensuring only one None branch exists and eliminating the
unreachable None arm currently duplicated around the match; update references to
options_update accordingly.

In `@xz/src/lzma/lzma_encoder_optimum_normal.rs`:
- Around line 782-822: The two duplicated blocks that compute "literal after
rep/match" tail pricing (around the code using update_long_rep_state and
update_match_state) should be extracted into a small helper function
(#[inline(always)]) that accepts: (1) a state-updater function or enum selector
(to handle update_long_rep_state vs update_match_state), (2) the base price/u32
starting value (price + get_len_price(...) or cur_and_len_price_2), and (3) the
back_prev_2 value to store (rep_index vs cur_back + REPS); the helper should
reproduce the shared sequence of computing pos_state_next,
cur_and_len_literal_price via get_literal_price/match_prob, next_rep_match_price
with is_rep_prob, advancing len_end and setting opts entries, computing
cur_and_len_price via get_rep_price, and updating (*opt).* fields (price,
pos_prev, back_prev, prev_1_is_literal, prev_2, pos_prev_2, back_prev_2) so
callers in the blocks that currently call
update_long_rep_state/update_match_state simply invoke the helper with the
appropriate updater, base price, and back_prev_2 value.
- Around line 783-784: The temporary binding state_2 is being initialized to
state and then immediately overwritten; replace the two-step pattern with a
single binding by calling the updater directly (e.g., change the sequence using
update_long_rep_state to let mut state_2 = update_long_rep_state(state); and
apply the same folding for the other occurrence by using let mut state_2 =
update_match_state(state);), removing the redundant initial assignment.

In `@xz/src/simple/ia64.rs`:
- Around line 8-13: The BRANCH_TABLE access uses raw-pointer arithmetic which
removes the compile-time bounds check; change the deref expression that uses
BRANCH_TABLE.as_ptr().add(instr_template as usize) to a normal array index
BRANCH_TABLE[instr_template as usize] (keeping the existing instr_template mask
to 5 bits) so the compiler retains the bounds check if BRANCH_TABLE size
changes—locate this in the loop around ptr/cur/instr_template and replace the
unsafe pointer dereference with the safe indexed access.

In `@xz/src/simple/x86.rs`:
- Around line 62-65: The unsafe pointer access to MASK_TO_BIT_NUMBER can be
replaced with safe indexing: use MASK_TO_BIT_NUMBER[(prev_mask >> 1) as usize]
to compute i_0 instead of unsafe { *MASK_TO_BIT_NUMBER.as_ptr().add(...) }
because the preceding guard (prev_mask >> 1 <= 4 && prev_mask >> 1 != 3)
guarantees the index is in-range; update the code that sets i_0 (and leaves
computation of b = (dest >> (24u32).wrapping_sub(i_0.wrapping_mul(8))) as u8) to
remove the unsafe block while keeping the same types and semantics.

---

Outside diff comments:
In `@src/write.rs`:
- Around line 103-123: The doc comment for try_finish (and similar docs on
XzDecoder::try_finish and write) is stale: it claims further calls to write may
panic and includes a "# Panics" section, but dump()/flush() now return
finished_error() and write() propagates it instead of panicking. Update the
documentation on XzEncoder::try_finish, XzDecoder::try_finish and the write()
docs to remove the "# Panics" section and the sentence about panics, and replace
it with a concise note that subsequent calls will return an error (via
finished_error()) rather than panic; mention the relevant methods (try_finish,
write, dump, flush, finished_error) so readers can find the behavior in code.

In `@xz/src/common/block_buffer_encoder.rs`:
- Around line 198-205: The null-input-permitted-when-in_size==0 behavior should
be made explicit: in the conditional inside the block_buffer_encoder (the if
that checks block.is_null(), input.is_null(), out.is_null(), out_pos, and
*out_pos > out_size) group the input check with parentheses as (input.is_null()
&& in_size != 0) so the intent/matching upstream contract is clear and cannot be
misread by future maintainers; adjust the conditional expression in the function
containing that if accordingly and leave the rest of the checks unchanged.

In `@xz/src/common/block_decoder.rs`:
- Around line 56-68: The current use of debug_assert! + unwrap_unchecked() on
(*coder).next.code in block_decoder.rs introduces UB in release builds; replace
that with an explicit match on (*coder).next.code as in
microlzma_decoder.rs/alone_encoder.rs: if Some(code) => call code(...) and
assign the lzma_ret result, else return LZMA_PROG_ERROR (or set ret =
LZMA_PROG_ERROR) before exiting the function. Update the handling around the
lzma_ret variable (and any early return) so the None branch mirrors the
established explicit-match error path instead of using unwrap_unchecked().

In `@xz/src/common/common.rs`:
- Around line 101-119: In lzma_alloc_zero, avoid double-zeroing memory returned
by calloc: when allocator is non-null and (*allocator).alloc is None, call the
existing calloc path (or set ptr from calloc) but do not call
core::ptr::write_bytes again; only call write_bytes when using a custom
allocator (i.e., when alloc is Some and the returned ptr needs zeroing). Update
the logic around lzma_alloc_zero, lzma_allocator, alloc, calloc and the
write_bytes call so the calloc fallback is routed through the zeroed-memory
branch or the write_bytes call is gated to custom-alloc outcomes.
- Around line 24-57: The malloc and calloc implementations call the unsafe
functions alloc and alloc_zeroed without an unsafe block; wrap the calls to
alloc and alloc_zeroed (and any pointer arithmetic/dereference like *(base as
*mut usize) and base.add(...)) inside unsafe { ... } so those operations occur
within an unsafe context in fn malloc and fn calloc, keeping the existing
early-null checks and use of rust_alloc_layout and LZMA_ALLOC_HEADER_SIZE
intact.

In `@xz/src/common/filter_encoder.rs`:
- Around line 289-316: The code in lzma_filters_update redundantly checks
(*(*strm).internal).next.update for None twice; remove the initial if that
returns LZMA_PROG_ERROR and rely on the single match later that binds update
(i.e., keep the existing match block "let update = match
(*(*strm).internal).next.update { Some(update) => update, None => return
LZMA_PROG_ERROR }" and delete the earlier "if
(*(*strm).internal).next.update.is_none() { return LZMA_PROG_ERROR; }" so there
is only one canonical None-check as in lz_encoder.rs::lz_encoder_update).

In `@xz/src/common/index_decoder.rs`:
- Around line 339-387: Rename the local return-value variables in
index_decoder_reset and lzma_index_buffer_decode to the normalized shadowed name
"ret" (instead of "ret_" and "ret__0") to match the rest of the PR; e.g., in
index_decoder_reset replace the lzma_ret variables ret_ and ret__0 with a single
let mut ret: lzma_ret = lzma_strm_init(strm); then shadow/assign ret for the
lzma_index_decoder_init call and for the subsequent checks, and in
lzma_index_buffer_decode replace ret_ with ret and use the same shadowing
pattern when calling index_decoder_reset, preserving all existing return checks
and behavior.

In `@xz/src/common/string_conversion.rs`:
- Around line 659-661: Replace the hardcoded array-length expression
size_of::<[filter_codec_def; 11]>() / size_of::<filter_codec_def>() with a call
to the existing helper array_size(filter_name_map) so the loops over
filter_codec_def use the actual filter_name_map length; locate the three sites
(the while loop using i < size_of::<[filter_codec_def; 11]>() /
size_of::<filter_codec_def>() and the two other similar occurrences around the
references to filter_codec_def/filter_name_map) and change the bound to
array_size(filter_name_map) (or an equivalent array_size call that returns the
correct usize) to avoid drifting when filter_name_map changes.

In `@xz/src/lz/lz_decoder.rs`:
- Around line 112-131: The loop relies on the invariant that (*coder).next.code
is Some but still calls unwrap_unchecked inside the loop, which is fragile;
hoist and cache the callback once before entering the while loop: after the
early return that calls decode_buffer, assert or expect that (*coder).next.code
is Some and bind let next_code = (*coder).next.code.unwrap() (or use expect with
a clear message), then inside the loop call next_code(...) instead of unwrapping
each iteration and remove the unwrap_unchecked/debug_assert there; this ensures
the callback pointer is captured (referencing coder.next.code, next_code,
decode_buffer, and next_finished) and avoids potential UB from future refactors.

In `@xz/src/lzma/lzma2_decoder.rs`:
- Around line 92-99: The code replaces prior runtime panics with unsafe UB by
calling unwrap_unchecked() on (*coder).lzma.reset (and similarly on
set_uncompressed) after only a debug_assert; instead restore a safe release-time
guard: check if (*coder).lzma.reset (and set_uncompressed) is None and return an
error (e.g., LZMA_PROG_ERROR) from lzma2_decode instead of calling
unwrap_unchecked(), or document the invariant near the
lzma.reset/set_uncompressed declarations and keep unwrap() to preserve a release
panic; locate uses in lzma2_decode and initialization in
lzma_lzma_decoder_create to implement the chosen safe guard.

In `@xz/src/simple/simple_coder.rs`:
- Around line 14-40: The branch currently checks (*coder).next.code.is_none()
then later matches (*coder).next.code with a redundant None arm; simplify
copy_or_code by replacing the is_none() + separate match with a single match on
(*coder).next.code: handle None by calling lzma_bufcpy and setting
(*coder).end_was_reached when appropriate, and handle Some(code) by invoking
code((*coder).next.coder, ...) and processing ret (setting end_was_reached on
LZMA_STREAM_END or returning ret on non-OK). This removes the dead None arm and
mirrors the safe pattern used elsewhere (avoid introducing unsafe
unwraps/unreachable_unchecked unless explicitly desired).

---

Duplicate comments:
In `@perf-probe/src/main.rs`:
- Around line 531-540: The assertions that in_pos == compressed.len() and
out_pos == out_size reject valid multi-stream/trailing-byte inputs because
lzma_stream_buffer_decode may stop at the first unused byte; update the checks
so exact-consumption is only enforced for the synthesized single-stream path:
guard the in_pos/out_pos equality assertions behind a condition that the input
was synthesized (e.g., when config.compressed_input_path.is_none()), or make the
check optional/configurable; references: in_pos, compressed.len(), out_pos,
out_size, lzma_stream_buffer_decode, load_compressed_input, and
config.compressed_input_path.is_none().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3b9a1481-9cb5-4094-ab10-e11e19333012

📥 Commits

Reviewing files that changed from the base of the PR and between c6236a1 and c4b047d.

📒 Files selected for processing (86)
  • docs/performance-workflow.md
  • patch.diff
  • perf-probe/src/main.rs
  • scripts/compare_all_trimmed.sh
  • scripts/compare_api_workloads.sh
  • scripts/compare_upstream_c_tests.sh
  • scripts/compare_workloads.sh
  • scripts/run_upstream_c_tests.sh
  • src/stream.rs
  • src/write.rs
  • src/write/auto_finish.rs
  • systest/Cargo.toml
  • systest/build.rs
  • xz-sys/src/lib.rs
  • xz/src/alloc.rs
  • xz/src/check/check.rs
  • xz/src/check/crc32_fast.rs
  • xz/src/check/crc64_fast.rs
  • xz/src/check/sha256.rs
  • xz/src/common/alone_decoder.rs
  • xz/src/common/alone_encoder.rs
  • xz/src/common/auto_decoder.rs
  • xz/src/common/block_buffer_decoder.rs
  • xz/src/common/block_buffer_encoder.rs
  • xz/src/common/block_decoder.rs
  • xz/src/common/block_encoder.rs
  • xz/src/common/block_header_decoder.rs
  • xz/src/common/block_header_encoder.rs
  • xz/src/common/common.rs
  • xz/src/common/common_types.rs
  • xz/src/common/easy_buffer_encoder.rs
  • xz/src/common/file_info.rs
  • xz/src/common/filter_buffer_decoder.rs
  • xz/src/common/filter_buffer_encoder.rs
  • xz/src/common/filter_common.rs
  • xz/src/common/filter_decoder.rs
  • xz/src/common/filter_encoder.rs
  • xz/src/common/filter_flags_decoder.rs
  • xz/src/common/index.rs
  • xz/src/common/index_decoder.rs
  • xz/src/common/index_encoder.rs
  • xz/src/common/index_hash.rs
  • xz/src/common/lzip_decoder.rs
  • xz/src/common/microlzma_decoder.rs
  • xz/src/common/microlzma_encoder.rs
  • xz/src/common/mod.rs
  • xz/src/common/outqueue.rs
  • xz/src/common/stream_buffer_decoder.rs
  • xz/src/common/stream_buffer_encoder.rs
  • xz/src/common/stream_decoder.rs
  • xz/src/common/stream_decoder_mt.rs
  • xz/src/common/stream_encoder.rs
  • xz/src/common/stream_encoder_mt.rs
  • xz/src/common/stream_flags_common.rs
  • xz/src/common/stream_flags_decoder.rs
  • xz/src/common/stream_flags_encoder.rs
  • xz/src/common/string_conversion.rs
  • xz/src/common/threading.rs
  • xz/src/common/vli_decoder.rs
  • xz/src/delta/delta_common.rs
  • xz/src/delta/delta_decoder.rs
  • xz/src/delta/delta_encoder.rs
  • xz/src/lz/lz_decoder.rs
  • xz/src/lz/lz_encoder.rs
  • xz/src/lz/lz_encoder_mf.rs
  • xz/src/lzma/fastpos_table.rs
  • xz/src/lzma/lzma2_decoder.rs
  • xz/src/lzma/lzma2_encoder.rs
  • xz/src/lzma/lzma_decoder.rs
  • xz/src/lzma/lzma_encoder.rs
  • xz/src/lzma/lzma_encoder_optimum_fast.rs
  • xz/src/lzma/lzma_encoder_optimum_normal.rs
  • xz/src/lzma/lzma_encoder_presets.rs
  • xz/src/rangecoder/price_table.rs
  • xz/src/simple/arm.rs
  • xz/src/simple/arm64.rs
  • xz/src/simple/armthumb.rs
  • xz/src/simple/ia64.rs
  • xz/src/simple/powerpc.rs
  • xz/src/simple/riscv.rs
  • xz/src/simple/simple_coder.rs
  • xz/src/simple/simple_decoder.rs
  • xz/src/simple/simple_encoder.rs
  • xz/src/simple/sparc.rs
  • xz/src/simple/x86.rs
  • xz/src/types.rs

Comment thread patch.diff Outdated
Comment on lines +173 to +181
+ if *in_pos > in_size || *out_pos > out_size {
+ return 0;
+ }
+ if (in_0.is_null() && *in_pos != in_size) || (out.is_null() && *out_pos != out_size) {
+ return 0;
+ }
debug_assert!(!in_0.is_null() || *in_pos == in_size);
debug_assert!(!out.is_null() || *out_pos == out_size);
debug_assert!(*in_pos <= in_size);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Runtime guards make subsequent debug_assert!s dead code.

The new early returns cover the exact same conditions as the debug_assert! on lines 179–181 that immediately follow. Those asserts can no longer fire in either debug or release. Either drop the redundant debug_asserts, or keep only the asserts and drop the new guards if silent fallthrough is not desired (the current change effectively converts a debug-time panic into a silent no-op in debug builds).

♻️ Proposed cleanup
     if *in_pos > in_size || *out_pos > out_size {
         return 0;
     }
     if (in_0.is_null() && *in_pos != in_size) || (out.is_null() && *out_pos != out_size) {
         return 0;
     }
-    debug_assert!(!in_0.is_null() || *in_pos == in_size);
-    debug_assert!(!out.is_null() || *out_pos == out_size);
-    debug_assert!(*in_pos <= in_size);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
+ if *in_pos > in_size || *out_pos > out_size {
+ return 0;
+ }
+ if (in_0.is_null() && *in_pos != in_size) || (out.is_null() && *out_pos != out_size) {
+ return 0;
+ }
debug_assert!(!in_0.is_null() || *in_pos == in_size);
debug_assert!(!out.is_null() || *out_pos == out_size);
debug_assert!(*in_pos <= in_size);
if *in_pos > in_size || *out_pos > out_size {
return 0;
}
if (in_0.is_null() && *in_pos != in_size) || (out.is_null() && *out_pos != out_size) {
return 0;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@patch.diff` around lines 173 - 181, The added runtime guards (checking
*in_pos, *out_pos vs in_size/out_size and null checks for in_0/out) make the
following debug_assert!s (the three debug_assert! lines immediately after)
unreachable; remove the redundancy by either deleting those debug_assert! lines
(debug_assert!(!in_0.is_null() || *in_pos == in_size);
debug_assert!(!out.is_null() || *out_pos == out_size); debug_assert!(*in_pos <=
in_size);) or, if you want a debug-time panic instead of a silent return, remove
the new early-return guards and keep the debug_assert!s; update the block around
the checks of in_pos, out_pos, in_size, out_size, in_0 and out accordingly so
only one style (runtime guard or debug_assert) remains.

Comment on lines +85 to +105
record_pair() {
local stem="$1"
local label="$2"
local rust_name="$3"
local rust_cmd="$4"
local c_name="$5"
local c_cmd="$6"

printf '%s\t%s\t%s\t%s\n' "$stem" "$label" "$rust_name" "$c_name" >>"$SUMMARY_TSV"

hyperfine \
--shell=none \
--runs "$RUNS" \
--warmup "$WARMUP" \
--export-json "$RESULTS_DIR/${stem}.json" \
--export-markdown "$RESULTS_DIR/${stem}.md" \
--command-name "$rust_name" \
"$rust_cmd" \
--command-name "$c_name" \
"$c_cmd"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider writing the TSV row only after hyperfine succeeds.

The TSV row is appended before the measurement runs, so a mid-run hyperfine failure (caught by set -e) leaves an orphan row pointing to a missing JSON. The next invocation truncates, so this is recoverable — but a partial manual inspection between runs would confuse tooling. Swap the order for cleaner state:

Proposed fix
-  printf '%s\t%s\t%s\t%s\n' "$stem" "$label" "$rust_name" "$c_name" >>"$SUMMARY_TSV"
-
   hyperfine \
     --shell=none \
     ...
     "$c_cmd"
+
+  printf '%s\t%s\t%s\t%s\n' "$stem" "$label" "$rust_name" "$c_name" >>"$SUMMARY_TSV"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
record_pair() {
local stem="$1"
local label="$2"
local rust_name="$3"
local rust_cmd="$4"
local c_name="$5"
local c_cmd="$6"
printf '%s\t%s\t%s\t%s\n' "$stem" "$label" "$rust_name" "$c_name" >>"$SUMMARY_TSV"
hyperfine \
--shell=none \
--runs "$RUNS" \
--warmup "$WARMUP" \
--export-json "$RESULTS_DIR/${stem}.json" \
--export-markdown "$RESULTS_DIR/${stem}.md" \
--command-name "$rust_name" \
"$rust_cmd" \
--command-name "$c_name" \
"$c_cmd"
}
record_pair() {
local stem="$1"
local label="$2"
local rust_name="$3"
local rust_cmd="$4"
local c_name="$5"
local c_cmd="$6"
hyperfine \
--shell=none \
--runs "$RUNS" \
--warmup "$WARMUP" \
--export-json "$RESULTS_DIR/${stem}.json" \
--export-markdown "$RESULTS_DIR/${stem}.md" \
--command-name "$rust_name" \
"$rust_cmd" \
--command-name "$c_name" \
"$c_cmd"
printf '%s\t%s\t%s\t%s\n' "$stem" "$label" "$rust_name" "$c_name" >>"$SUMMARY_TSV"
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/compare_all_trimmed.sh` around lines 85 - 105, The TSV row is written
to SUMMARY_TSV before running hyperfine in the record_pair function, causing
orphaned entries if hyperfine fails; move the printf that appends
"$stem","$label","$rust_name","$c_name" so it executes only after the hyperfine
command completes successfully (i.e., run hyperfine first inside record_pair and
then append the TSV row), keeping the same field order and file target to
preserve downstream tooling that reads SUMMARY_TSV.

Comment on lines +270 to +277
rust = trimmed_mean(by_name[rust_name]["times"])
c = trimmed_mean(by_name[c_name]["times"])
ratio = c / rust
allowed = c * (1.0 + tolerance)
status = "PASS" if rust <= allowed else "FAIL"
if status == "FAIL":
failed.append(label)
rows.append((label, rust, c, ratio, status))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Division-by-zero risk on ratio.

ratio = c / rust will raise ZeroDivisionError if a trimmed Rust mean is effectively zero (pathological but possible for extremely fast probes at small sizes). Guard it:

Proposed fix
-    ratio = c / rust
+    ratio = float("inf") if rust == 0 else c / rust
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rust = trimmed_mean(by_name[rust_name]["times"])
c = trimmed_mean(by_name[c_name]["times"])
ratio = c / rust
allowed = c * (1.0 + tolerance)
status = "PASS" if rust <= allowed else "FAIL"
if status == "FAIL":
failed.append(label)
rows.append((label, rust, c, ratio, status))
rust = trimmed_mean(by_name[rust_name]["times"])
c = trimmed_mean(by_name[c_name]["times"])
ratio = float("inf") if rust == 0 else c / rust
allowed = c * (1.0 + tolerance)
status = "PASS" if rust <= allowed else "FAIL"
if status == "FAIL":
failed.append(label)
rows.append((label, rust, c, ratio, status))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/compare_all_trimmed.sh` around lines 270 - 277, Guard the
division-by-zero when computing ratio = c / rust: detect if rust is zero (or
effectively zero, e.g., math.isclose(rust, 0.0)) before dividing; if rust == 0
and c > 0 set ratio to float('inf'), if both rust and c are zero set ratio to
1.0 (or another sentinel), then compute status and append to rows and failed the
same way; reference the variables trimmed_mean results (rust, c), ratio, status,
rows and failed to locate and update the logic.

Comment on lines +282 to +292
with md.open("w") as f:
f.write("# Trimmed Backend Comparison\n\n")
f.write(f"- measured runs per pair: {len(by_name[rust_name]['times'])}\n")
f.write("- aggregation: discard min and max, average remaining runs\n")
f.write(f"- allowed Rust slowdown tolerance: {tolerance:.3f}\n\n")
f.write("| workload | Rust trimmed mean | C trimmed mean | C/Rust ratio | status |\n")
f.write("|---|---:|---:|---:|---|\n")
for label, rust, c, ratio, status in rows:
f.write(
f"| {label} | {rust:.6f}s | {c:.6f}s | {ratio:.3f}x | {status} |\n"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Summary header uses loop-leaked variables; fragile if no pairs run.

len(by_name[rust_name]['times']) reads from the last loop iteration's binding rather than explicit state. If summary_tsv is empty, this raises NameError on rust_name rather than a clear error; if any future workload has a different run count, the header becomes misleading. Prefer computing from the first row or tracking explicitly.

Proposed fix
-with md.open("w") as f:
+measured_runs = len(by_name[rust_name]["times"]) if rows else 0
+with md.open("w") as f:
     f.write("# Trimmed Backend Comparison\n\n")
-    f.write(f"- measured runs per pair: {len(by_name[rust_name]['times'])}\n")
+    f.write(f"- measured runs per pair: {measured_runs}\n")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/compare_all_trimmed.sh` around lines 282 - 292, The summary header
currently uses the loop-bound variable rust_name (via
len(by_name[rust_name]['times'])) which can be undefined or misleading; change
the code that writes the header to compute the measured run count explicitly
from the collected data (e.g., derive it from rows or from summary_tsv) before
writing the file, and handle the empty-case: if summary_tsv/rows is empty write
"0" or emit a clear error; update references to use that new local variable
(e.g., measured_runs or runs_count) instead of rust_name/by_name inside the
md.open block so the header is accurate and robust.

Comment thread scripts/compare_upstream_c_tests.sh Outdated
Comment on lines +62 to +79
TESTS=("$@")

PROFILE="$PROFILE" LANE="$LANE" BACKEND=xz-sys scripts/run_upstream_c_tests.sh "${TESTS[@]}" >/dev/null
PROFILE="$PROFILE" LANE="$LANE" BACKEND=liblzma-sys scripts/run_upstream_c_tests.sh "${TESTS[@]}" >/dev/null

XZ_CMD="env PROFILE=$PROFILE LANE=$LANE BACKEND=xz-sys SKIP_BUILD=1 scripts/run_upstream_c_tests.sh"
C_CMD="env PROFILE=$PROFILE LANE=$LANE BACKEND=liblzma-sys SKIP_BUILD=1 scripts/run_upstream_c_tests.sh"

if [[ ${#TESTS[@]} -gt 0 ]]; then
for test_name in "${TESTS[@]}"; do
XZ_CMD+=" ${test_name}"
C_CMD+=" ${test_name}"
done
SUFFIX="$(printf '%s-' "${TESTS[@]}")"
SUFFIX="${SUFFIX%-}"
else
SUFFIX="all"
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Minor: "${TESTS[@]}" expansion under set -u, and SUFFIX can produce oversized/unsafe filenames.

Two small robustness concerns in this new script:

  1. Older bash (≤ 4.3, notably macOS default 3.2) treats "${empty_array[@]}" as unbound under set -u. If contributors run this on such a host with no positional tests, lines 64-65 will abort. Consider the "${arr[@]+...}" guard or documenting a bash 4.4+ requirement.
  2. SUFFIX="$(printf '%s-' "${TESTS[@]}")" pastes arbitrary test names into the report path. With many tests this overshoots typical filename limits (255 bytes on most filesystems), and a test name containing / or whitespace would silently produce bad paths. Consider capping length and hashing (e.g., sha1sum prefix) when more than N tests are passed.
Suggested hardening
-TESTS=("$@")
+TESTS=("$@")
+
+# Guard empty-array expansion for older bash under `set -u`.
+EMPTY_SAFE_TESTS=("${TESTS[@]+"${TESTS[@]}"}")
@@
-PROFILE="$PROFILE" LANE="$LANE" BACKEND=xz-sys scripts/run_upstream_c_tests.sh "${TESTS[@]}" >/dev/null
-PROFILE="$PROFILE" LANE="$LANE" BACKEND=liblzma-sys scripts/run_upstream_c_tests.sh "${TESTS[@]}" >/dev/null
+PROFILE="$PROFILE" LANE="$LANE" BACKEND=xz-sys scripts/run_upstream_c_tests.sh "${EMPTY_SAFE_TESTS[@]}" >/dev/null
+PROFILE="$PROFILE" LANE="$LANE" BACKEND=liblzma-sys scripts/run_upstream_c_tests.sh "${EMPTY_SAFE_TESTS[@]}" >/dev/null
@@
-  SUFFIX="$(printf '%s-' "${TESTS[@]}")"
-  SUFFIX="${SUFFIX%-}"
+  SUFFIX="$(printf '%s-' "${TESTS[@]}")"
+  SUFFIX="${SUFFIX%-}"
+  if (( ${`#SUFFIX`} > 80 )); then
+    SUFFIX="multi-$(printf '%s' "$SUFFIX" | sha1sum | cut -c1-10)"
+  fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
TESTS=("$@")
PROFILE="$PROFILE" LANE="$LANE" BACKEND=xz-sys scripts/run_upstream_c_tests.sh "${TESTS[@]}" >/dev/null
PROFILE="$PROFILE" LANE="$LANE" BACKEND=liblzma-sys scripts/run_upstream_c_tests.sh "${TESTS[@]}" >/dev/null
XZ_CMD="env PROFILE=$PROFILE LANE=$LANE BACKEND=xz-sys SKIP_BUILD=1 scripts/run_upstream_c_tests.sh"
C_CMD="env PROFILE=$PROFILE LANE=$LANE BACKEND=liblzma-sys SKIP_BUILD=1 scripts/run_upstream_c_tests.sh"
if [[ ${#TESTS[@]} -gt 0 ]]; then
for test_name in "${TESTS[@]}"; do
XZ_CMD+=" ${test_name}"
C_CMD+=" ${test_name}"
done
SUFFIX="$(printf '%s-' "${TESTS[@]}")"
SUFFIX="${SUFFIX%-}"
else
SUFFIX="all"
fi
TESTS=("$@")
# Guard empty-array expansion for older bash under `set -u`.
EMPTY_SAFE_TESTS=("${TESTS[@]+"${TESTS[@]}"}")
PROFILE="$PROFILE" LANE="$LANE" BACKEND=xz-sys scripts/run_upstream_c_tests.sh "${EMPTY_SAFE_TESTS[@]}" >/dev/null
PROFILE="$PROFILE" LANE="$LANE" BACKEND=liblzma-sys scripts/run_upstream_c_tests.sh "${EMPTY_SAFE_TESTS[@]}" >/dev/null
XZ_CMD="env PROFILE=$PROFILE LANE=$LANE BACKEND=xz-sys SKIP_BUILD=1 scripts/run_upstream_c_tests.sh"
C_CMD="env PROFILE=$PROFILE LANE=$LANE BACKEND=liblzma-sys SKIP_BUILD=1 scripts/run_upstream_c_tests.sh"
if [[ ${`#TESTS`[@]} -gt 0 ]]; then
for test_name in "${TESTS[@]}"; do
XZ_CMD+=" ${test_name}"
C_CMD+=" ${test_name}"
done
SUFFIX="$(printf '%s-' "${TESTS[@]}")"
SUFFIX="${SUFFIX%-}"
if (( ${`#SUFFIX`} > 80 )); then
SUFFIX="multi-$(printf '%s' "$SUFFIX" | sha1sum | cut -c1-10)"
fi
else
SUFFIX="all"
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/compare_upstream_c_tests.sh` around lines 62 - 79, The script is
fragile when TESTS is empty under set -u and constructs unsafe/oversized SUFFIX
filenames; fix by guarding array expansions and sanitizing/truncating/hashing
the suffix: first, avoid unbound expansion by using a guarded check like if [[
${`#TESTS`[@]} -gt 0 ]] (or use the ${TESTS[@]+x} pattern) before touching TESTS,
and only append to XZ_CMD and C_CMD inside that guarded branch (refer to TESTS,
XZ_CMD, C_CMD, and the for loop); second, build SUFFIX by first sanitizing test
names (replace '/' and whitespace with '_' or remove unsafe chars), compute a
combined byte length and if it exceeds a threshold (e.g., 200 bytes) or if there
are more than N tests, replace the full-list suffix with a short deterministic
hash (e.g., sha1 of the joined names) and optionally a safe readable prefix,
ensuring the final SUFFIX is capped to filesystem-safe length.

Comment thread xz/src/lz/lz_encoder.rs Outdated
Comment thread xz/src/lzma/lzma_encoder_optimum_normal.rs Outdated
Comment thread xz/src/lzma/lzma_encoder_optimum_normal.rs Outdated
Comment thread xz/src/simple/ia64.rs
Comment on lines +8 to +13
let ptr = buffer.as_mut_ptr();
let mut i: size_t = 0;
while i < size {
let instr_template: u32 = (buffer[i] & 0x1f) as u32;
let mask: u32 = BRANCH_TABLE[instr_template as usize];
let cur = unsafe { ptr.add(i) };
let instr_template: u32 = unsafe { (*cur & 0x1f) as u32 };
let mask: u32 = unsafe { *BRANCH_TABLE.as_ptr().add(instr_template as usize) };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Optional: keep BRANCH_TABLE access checked.

instr_template is masked to 5 bits so the index is always < 32, but switching to raw-pointer arithmetic removes the compile-time guarantee that BRANCH_TABLE stays at length 32. A normal array index (BRANCH_TABLE[instr_template as usize]) should optimize equivalently and keeps a bounds check in place if the table size is ever changed.

♻️ Suggested simplification
-        let instr_template: u32 = unsafe { (*cur & 0x1f) as u32 };
-        let mask: u32 = unsafe { *BRANCH_TABLE.as_ptr().add(instr_template as usize) };
+        let instr_template: u32 = unsafe { (*cur & 0x1f) as u32 };
+        let mask: u32 = BRANCH_TABLE[instr_template as usize];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let ptr = buffer.as_mut_ptr();
let mut i: size_t = 0;
while i < size {
let instr_template: u32 = (buffer[i] & 0x1f) as u32;
let mask: u32 = BRANCH_TABLE[instr_template as usize];
let cur = unsafe { ptr.add(i) };
let instr_template: u32 = unsafe { (*cur & 0x1f) as u32 };
let mask: u32 = unsafe { *BRANCH_TABLE.as_ptr().add(instr_template as usize) };
let ptr = buffer.as_mut_ptr();
let mut i: size_t = 0;
while i < size {
let cur = unsafe { ptr.add(i) };
let instr_template: u32 = unsafe { (*cur & 0x1f) as u32 };
let mask: u32 = BRANCH_TABLE[instr_template as usize];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/simple/ia64.rs` around lines 8 - 13, The BRANCH_TABLE access uses
raw-pointer arithmetic which removes the compile-time bounds check; change the
deref expression that uses BRANCH_TABLE.as_ptr().add(instr_template as usize) to
a normal array index BRANCH_TABLE[instr_template as usize] (keeping the existing
instr_template mask to 5 bits) so the compiler retains the bounds check if
BRANCH_TABLE size changes—locate this in the loop around ptr/cur/instr_template
and replace the unsafe pointer dereference with the safe indexed access.

Comment thread xz/src/simple/x86.rs
Comment on lines 62 to 65
}
let i_0: u32 = MASK_TO_BIT_NUMBER[(prev_mask >> 1) as usize];
let i_0: u32 =
unsafe { *MASK_TO_BIT_NUMBER.as_ptr().add((prev_mask >> 1) as usize) };
b = (dest >> (24u32).wrapping_sub(i_0.wrapping_mul(8))) as u8;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Nit: indexed access into MASK_TO_BIT_NUMBER can be safe.

The guard at line 47 (prev_mask >> 1 <= 4 && prev_mask >> 1 != 3) bounds the index to {0,1,2,4}, always in-range for [u32; 5]. A safe MASK_TO_BIT_NUMBER[(prev_mask >> 1) as usize] would remove one unnecessary unsafe block without perf impact.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/simple/x86.rs` around lines 62 - 65, The unsafe pointer access to
MASK_TO_BIT_NUMBER can be replaced with safe indexing: use
MASK_TO_BIT_NUMBER[(prev_mask >> 1) as usize] to compute i_0 instead of unsafe {
*MASK_TO_BIT_NUMBER.as_ptr().add(...) } because the preceding guard (prev_mask
>> 1 <= 4 && prev_mask >> 1 != 3) guarantees the index is in-range; update the
code that sets i_0 (and leaves computation of b = (dest >>
(24u32).wrapping_sub(i_0.wrapping_mul(8))) as u8) to remove the unsafe block
while keeping the same types and semantics.

@youknowone youknowone marked this pull request as draft April 17, 2026 08:48
@youknowone youknowone force-pushed the trim branch 2 times, most recently from 657742f to 4741f2e Compare April 17, 2026 14:54
@youknowone youknowone marked this pull request as ready for review April 17, 2026 14:58
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
xz/src/common/common.rs (1)

101-119: 🧹 Nitpick | 🔵 Trivial

Minor: double zero-write when allocator is non-null with no alloc callback.

At Line 110 the fallback path calls calloc(1, size) (which already returns zeroed memory), but then at Lines 112–114 write_bytes(ptr, 0, size) re-zeroes the same region. It's correctness-preserving, just wasted work. Moving the write_bytes inside the if let Some(alloc) arm (where the user's alloc is malloc-like and actually needs zeroing) is cleaner:

♻️ Proposed simplification
     if !allocator.is_null() {
         if let Some(alloc) = (*allocator).alloc {
             ptr = alloc((*allocator).opaque, 1, size);
+            if !ptr.is_null() {
+                core::ptr::write_bytes(ptr as *mut u8, 0, size);
+            }
         } else {
             ptr = calloc(1, size);
         }
-        if !ptr.is_null() {
-            core::ptr::write_bytes(ptr as *mut u8, 0, size);
-        }
     } else {
         ptr = calloc(1, size);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/common.rs` around lines 101 - 119, The function lzma_alloc_zero
currently calls write_bytes to zero memory even after using calloc, causing
redundant work; in lzma_alloc_zero, move the core::ptr::write_bytes(ptr as *mut
u8, 0, size) call into the branch where (*allocator).alloc is Some(alloc) so we
only manually zero memory returned by the allocator's alloc callback, leaving
the calloc fallback (and the else when allocator is null) to rely on calloc's
already-zeroed memory; update the ptr assignment flow around the alloc fallback
to preserve existing semantics.
♻️ Duplicate comments (2)
xz/src/common/common.rs (1)

150-153: ⚠️ Potential issue | 🟡 Minor

Debug-asserts are now unreachable after the new early returns.

Lines 147–149 already return 0 when input.is_null() && *in_pos != in_size (and the symmetric out case), making the debug_assert! at Lines 150–151 dead code in both debug and release builds. Either delete them, or move them above the null-handling early-returns so they can actually fire for documentation purposes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/common.rs` around lines 150 - 153, The debug assertions for
input/out pointer invariants (the debug_assert! checks referencing input,
in_pos, in_size, out, out_pos, out_size) are now unreachable due to the new
early return branches; either remove these redundant debug_assert! lines or move
them above the early-return null-handling checks so they can actually fire
(place the assertions before the null-check/return blocks that check
input.is_null() && *in_pos != in_size and the symmetric out case). Ensure the
chosen fix keeps the same runtime behavior (no panic in release) but restores
the assertions' value as debug-time documentation.
xz/src/common/threading.rs (1)

29-106: ⚠️ Potential issue | 🔴 Critical

Critical (unresolved): Darwin-specific type layouts and clock IDs apply to all non-Windows targets, including Linux.

The previous review flagged this and the code is unchanged: __darwin_sigset_t = u32 (4 bytes vs. glibc's ~128-byte sigset_t), the 8176-byte _opaque_pthread_t, and Darwin clock IDs (_CLOCK_MONOTONIC = 6, _CLOCK_THREAD_CPUTIME_ID = 16) are all gated only on #[cfg(not(windows))]. On Linux, mythread_create's call to pthread_sigmask with a 4-byte sigset_t will let the kernel write past the stack slot, and the clock ID mismatch silently picks the wrong clock for any future code that uses them beyond _CLOCK_REALTIME = 0.

The project already has libc = "0.2" in dependencies — please replace these hand-rolled types with libc::sigset_t, libc::pthread_*, and libc::CLOCK_* so each target gets the correct definitions automatically, or at minimum gate these Darwin definitions on #[cfg(target_os = "macos")].

Also applies to: 117-124

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/threading.rs` around lines 29 - 106, The file defines
Darwin-specific layouts and clock IDs for __darwin_sigset_t, _opaque_pthread_t
(and exported pthread types) and constants like
_CLOCK_MONOTONIC/_CLOCK_THREAD_CPUTIME_ID but gates them with
#[cfg(not(windows))], causing wrong layouts on Linux and crashing callers such
as mythread_create that calls pthread_sigmask; replace the hand-rolled types and
constants with the platform-safe libc equivalents (use libc::sigset_t,
libc::pthread_t / libc::pthread_attr_t / libc::pthread_mutex_t, and
libc::CLOCK_MONOTONIC / libc::CLOCK_THREAD_CPUTIME_ID) so each target gets
correct definitions, or if you must keep custom structs, restrict them to
#[cfg(target_os = "macos")] and update usages (e.g., pthread_sigmask and any
CLOCK_* references) to the corresponding libc types/constants to prevent size/ID
mismatches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@systest/build.rs`:
- Around line 192-195: The current string replace on generated_src
(fs::read_to_string -> generated_src.replace -> fs::write) is fragile to
whitespace/linebreaks and may incorrectly double-prefix or miss already-unsafe
blocks; replace it with a regex-based, idempotent transform: use the regex crate
to match extern "C" blocks with flexible whitespace and optional
attributes/comments (e.g. a pattern matching optional "unsafe" group and then
extern\s*"C"\s*\{), iterate matches and only insert the "unsafe " prefix when
the match does not already include "unsafe", then write the transformed
generated_src back with fs::write; update the code around the variables
generated and generated_src to perform this safer replacement.

In `@xz/src/common/string_conversion.rs`:
- Around line 168-182: The functions parse_bcj and parse_delta currently have
safe fn signatures but accept and dereference raw pointers; change their
signatures to unsafe fn parse_bcj(...) -> *const c_char and unsafe fn
parse_delta(...) -> *const c_char (matching the existing unsafe fn parse_lzma12)
so the raw-pointer contract is explicit; after changing them, update/remove any
safe→unsafe coercions in filter_name_map where they are cast as unsafe fn(...)
so the map uses the new unsafe fn types directly.

In `@xz/src/lzma/lzma_encoder_optimum_normal.rs`:
- Around line 222-232: The pointer expressions around dist_slot_prices and the
probability pointer passed into rc_bittree_price are overly wrapped with
addr_of_mut! and deref-offset patterns; simplify them to the same idiom used
later (use raw pointer arithmetic / .add(dist_state) casts) so that
dist_slot_prices is computed as a pointer derived from (*coder).dist_slot_prices
with .add(dist_state) as *mut u32, and the probability array pointer passed into
rc_bittree_price is similarly simplified to a plain .add(dist_state) as *mut
probability; update uses of dist_slot and the loop over (*coder).dist_table_size
to reference these simplified pointers.
- Line 579: The #[inline(always)] annotation on the large helper2 function (used
inside the hot loop in lzma_lzma_optimum_normal) should be changed to avoid
forced inlining: locate the helper2 function and replace #[inline(always)] with
either #[inline] or #[inline(never)] (prefer #[inline] to let the compiler
decide, or #[inline(never)] if you want to prevent code bloat), then run the
LZMA encoder benchmarks for the optimum-normal path to validate which choice
performs best before committing.

In `@xz/src/types.rs`:
- Line 374: Add a breaking-change note documenting the struct field rename from
`type_0` to `type_` on `lzma_options_delta` so downstream users know how to
migrate; update the README breaking changes section (0.4.x or later) and/or
CHANGELOG to include the exact old→new field mapping
(`lzma_options_delta::type_0` → `lzma_options_delta::type_`), a brief example of
the code change (access/construct with `type_` instead of `type_0`), and a short
migration note for consumers of any public APIs referencing
`lzma_options_delta`.

---

Outside diff comments:
In `@xz/src/common/common.rs`:
- Around line 101-119: The function lzma_alloc_zero currently calls write_bytes
to zero memory even after using calloc, causing redundant work; in
lzma_alloc_zero, move the core::ptr::write_bytes(ptr as *mut u8, 0, size) call
into the branch where (*allocator).alloc is Some(alloc) so we only manually zero
memory returned by the allocator's alloc callback, leaving the calloc fallback
(and the else when allocator is null) to rely on calloc's already-zeroed memory;
update the ptr assignment flow around the alloc fallback to preserve existing
semantics.

---

Duplicate comments:
In `@xz/src/common/common.rs`:
- Around line 150-153: The debug assertions for input/out pointer invariants
(the debug_assert! checks referencing input, in_pos, in_size, out, out_pos,
out_size) are now unreachable due to the new early return branches; either
remove these redundant debug_assert! lines or move them above the early-return
null-handling checks so they can actually fire (place the assertions before the
null-check/return blocks that check input.is_null() && *in_pos != in_size and
the symmetric out case). Ensure the chosen fix keeps the same runtime behavior
(no panic in release) but restores the assertions' value as debug-time
documentation.

In `@xz/src/common/threading.rs`:
- Around line 29-106: The file defines Darwin-specific layouts and clock IDs for
__darwin_sigset_t, _opaque_pthread_t (and exported pthread types) and constants
like _CLOCK_MONOTONIC/_CLOCK_THREAD_CPUTIME_ID but gates them with
#[cfg(not(windows))], causing wrong layouts on Linux and crashing callers such
as mythread_create that calls pthread_sigmask; replace the hand-rolled types and
constants with the platform-safe libc equivalents (use libc::sigset_t,
libc::pthread_t / libc::pthread_attr_t / libc::pthread_mutex_t, and
libc::CLOCK_MONOTONIC / libc::CLOCK_THREAD_CPUTIME_ID) so each target gets
correct definitions, or if you must keep custom structs, restrict them to
#[cfg(target_os = "macos")] and update usages (e.g., pthread_sigmask and any
CLOCK_* references) to the corresponding libc types/constants to prevent size/ID
mismatches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 94f13470-c37f-4ebb-8be3-6c1138ab4887

📥 Commits

Reviewing files that changed from the base of the PR and between c4b047d and 4741f2e.

📒 Files selected for processing (25)
  • .github/workflows/main.yml
  • .github/workflows/webassembly.yml
  • Cargo.lock.msrv
  • Cargo.toml
  • liblzma-sys/Cargo.toml
  • liblzma-sys/src/bindgen.rs
  • perf-probe/Cargo.toml
  • perf-probe/src/main.rs
  • systest/Cargo.toml
  • systest/build.rs
  • systest/src/main.rs
  • xz-sys/Cargo.toml
  • xz-sys/src/lib.rs
  • xz/Cargo.toml
  • xz/src/alloc.rs
  • xz/src/common/common.rs
  • xz/src/common/filter_decoder.rs
  • xz/src/common/filter_encoder.rs
  • xz/src/common/string_conversion.rs
  • xz/src/common/threading.rs
  • xz/src/lib.rs
  • xz/src/lzma/lzma_decoder.rs
  • xz/src/lzma/lzma_encoder.rs
  • xz/src/lzma/lzma_encoder_optimum_normal.rs
  • xz/src/types.rs

Comment thread systest/build.rs Outdated
Comment on lines +192 to +195
let generated = out_dir.join("all.rs");
let generated_src = fs::read_to_string(&generated).unwrap();
let generated_src = generated_src.replace("extern \"C\" {", "unsafe extern \"C\" {");
fs::write(generated, generated_src).unwrap();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check what ctest currently emits and whether newer versions emit `unsafe extern "C"`.
rg -nP 'extern\s+"C"\s*\{' -C2 | head -n 80
# Look for any pinned/constrained ctest version
rg -nP 'ctest\s*=' --type=toml

Repository: youknowone/xz-rs

Length of output: 4252


🏁 Script executed:

# Check if there's a generated all.rs or similar ctest output in the repo
find . -name "all.rs" -type f 2>/dev/null | head -20

# Check ctest dependency and version
cat systest/Cargo.toml | grep -A5 -B5 ctest

Repository: youknowone/xz-rs

Length of output: 337


🏁 Script executed:

# Search build.rs to understand the context better
cat -n systest/build.rs | sed -n '185,200p'

Repository: youknowone/xz-rs

Length of output: 593


🏁 Script executed:

# Check ctest crate documentation or look at what versions of ctest generate
rg -i "unsafe extern" systest/ 2>/dev/null || echo "No unsafe extern found in systest/"

# Check if there's a Cargo.lock that shows ctest dependencies
cat Cargo.lock | grep -A10 "name = \"ctest\"" | head -20

Repository: youknowone/xz-rs

Length of output: 425


🌐 Web query:

ctest 0.5.1 rust crate "unsafe extern" generated code

💡 Result:

The Rust crate ctest (latest version 0.5.0, released 2026-01-29) is a build-time tool for automated testing of FFI bindings in *-sys crates. It parses the Rust crate's lib.rs (which typically contains extern function declarations, constants, structs, etc.) and specified C headers to generate tests verifying that Rust bindings match C APIs in terms of function signatures, constant values, struct layouts/alignments, type sizes/alignments, etc. ctest generates two files during the build process: - A Rust file (e.g., all.rs) containing a main function with assertions comparing Rust and C sides. This is included in the systest binary's main.rs. - A C file compiled into the build, which queries the C headers for layout info, sizes, etc., at runtime. The generated Rust code exercises the extern fns from the parsed *-sys crate (e.g., mylib_sys::some_c_fn), calling them unsafely as needed since extern fns are unsafe to call without proper invariants. No sources explicitly show "unsafe extern" in generated code snippets. However, since ctest parses extern blocks (docs.rs shows Abi enum for extern block ABI like "C"), and Rust extern fns require unsafe { } to call, the generated tests use unsafe blocks around calls to these fns. In Rust 1.80+ (editions >=2024), extern blocks are unsafe extern, aligning with modern FFI practices (RFC 3484). Version 0.5.0 updates (beta releases 2025) likely incorporate syn 2.0+ parsing supporting unsafe extern syntax, but core functionality remains FFI validation via generated tests calling extern fns unsafely. No evidence of ctest 0.5.1; latest is 0.5.0. Repository: https://github.com/rust-lang/libc/tree/main/ctest. Usage example in README generates tests for extern fns without shown code, but tests validate signatures/layouts implying unsafe calls in generated main.

Citations:


String-replace post-processing of all.rs lacks defensive robustness.

The core concern—using a simple string replacement without accounting for whitespace variations or already-unsafe blocks—remains valid. However, the stated risk factors need correction:

  1. Incorrect premise: ctest is pinned to "0.5.1" in Cargo.toml, not floating. The claim about "unpinned version" is factually wrong.

  2. Unsubstantiated risk: No evidence that ctest 0.5.1 generates unsafe extern "C" { blocks. The concern about "double-unsafe" from ctest output is speculative.

  3. Valid fragility issue: The simple replace("extern \"C\" {", ...) is fragile to:

    • Whitespace variations (e.g., extern "C"\n{, extern "C" {)
    • Comments or conditional attributes near the block

    These cases would silently leave declarations as safe extern "C" without a build error, defeating the intended hardening.

Recommendation: Replace the string-based approach with a regex-based solution that handles whitespace robustly and idempotently checks for already-unsafe blocks. The fragility risk is real even with a pinned ctest version, as its output formatting may vary across minor versions or with different C header configurations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@systest/build.rs` around lines 192 - 195, The current string replace on
generated_src (fs::read_to_string -> generated_src.replace -> fs::write) is
fragile to whitespace/linebreaks and may incorrectly double-prefix or miss
already-unsafe blocks; replace it with a regex-based, idempotent transform: use
the regex crate to match extern "C" blocks with flexible whitespace and optional
attributes/comments (e.g. a pattern matching optional "unsafe" group and then
extern\s*"C"\s*\{), iterate matches and only insert the "unsafe " prefix when
the match does not already include "unsafe", then write the transformed
generated_src back with fs::write; update the code around the variables
generated and generated_src to perform this safer replacement.

Comment on lines +168 to 182
fn parse_bcj(
str: *mut *const c_char,
str_end: *const c_char,
filter_options: *mut c_void,
) -> *const c_char {
return unsafe {
unsafe {
parse_options(
str,
str_end,
filter_options,
::core::ptr::addr_of!(bcj_optmap) as *const option_map,
core::mem::size_of::<[option_map; 1]>() / core::mem::size_of::<option_map>(),
array_size(::core::ptr::addr_of!(bcj_optmap)),
)
};
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make parse_bcj and parse_delta unsafe fn to match their raw-pointer contract.

These two functions take raw pointers (*mut *const c_char, *const c_char, *mut c_void) and dereference them via internal unsafe { ... } blocks, but the outer signature dropped both extern "C" and unsafe, leaving them as plain safe fn. This is inconsistent with parse_lzma12 at Line 305 (unsafe fn parse_lzma12) and, more importantly, means any safe Rust caller could invoke them with arbitrary (even null/invalid) pointers — the only guard is the "no one else calls them" convention.

The casts at the filter_name_map entries (e.g., parse_bcj as unsafe fn(...)) happen to compile because safe-fn → unsafe-fn coercion is allowed, so the bug is latent, but the soundness invariant should be expressed in the signature.

🛡️ Proposed fix
-fn parse_bcj(
+unsafe fn parse_bcj(
     str: *mut *const c_char,
     str_end: *const c_char,
     filter_options: *mut c_void,
 ) -> *const c_char {
-    unsafe {
-        parse_options(
+    parse_options(
             str,
             str_end,
             filter_options,
             ::core::ptr::addr_of!(bcj_optmap) as *const option_map,
             array_size(::core::ptr::addr_of!(bcj_optmap)),
-        )
-    }
+    )
 }

Apply the analogous change to parse_delta, then the as unsafe fn(...) casts in filter_name_map no longer rely on safe→unsafe coercion.

Also applies to: 195-212

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/string_conversion.rs` around lines 168 - 182, The functions
parse_bcj and parse_delta currently have safe fn signatures but accept and
dereference raw pointers; change their signatures to unsafe fn parse_bcj(...) ->
*const c_char and unsafe fn parse_delta(...) -> *const c_char (matching the
existing unsafe fn parse_lzma12) so the raw-pointer contract is explicit; after
changing them, update/remove any safe→unsafe coercions in filter_name_map where
they are cast as unsafe fn(...) so the map uses the new unsafe fn types
directly.

Comment thread xz/src/lzma/lzma_encoder_optimum_normal.rs
Comment thread xz/src/lzma/lzma_encoder_optimum_normal.rs Outdated
Comment thread xz/src/types.rs
#[repr(C)]
pub struct lzma_options_delta {
pub type_0: lzma_delta_type,
pub type_: lzma_delta_type,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Catch any stragglers still referencing the old field name.
rg -nP --type=rust '\btype_0\b'

Repository: youknowone/xz-rs

Length of output: 42


🏁 Script executed:

# Examine the struct definition around line 374
sed -n '360,390p' xz/src/types.rs

Repository: youknowone/xz-rs

Length of output: 793


🏁 Script executed:

# Search for CHANGELOG or release notes
find . -type f \( -name "CHANGELOG*" -o -name "CHANGELOG*" -o -name "HISTORY*" -o -name "RELEASES*" -o -name "NEWS*" \) | head -20

Repository: youknowone/xz-rs

Length of output: 42


🏁 Script executed:

# Search for any remaining references to type_0 in all file types (docs, comments, etc.)
rg -n 'type_0' --no-heading

Repository: youknowone/xz-rs

Length of output: 42


🏁 Script executed:

# Check root directory for documentation files
ls -la | grep -iE 'readme|changelog|news|release'

Repository: youknowone/xz-rs

Length of output: 120


🏁 Script executed:

# Look for any documentation in top-level directories
git ls-files | head -30

Repository: youknowone/xz-rs

Length of output: 738


🏁 Script executed:

# Read README to check for breaking change documentation
cat README.md

Repository: youknowone/xz-rs

Length of output: 2078


🏁 Script executed:

# Check Cargo.toml for version and any breaking change notes
head -30 Cargo.toml

Repository: youknowone/xz-rs

Length of output: 1110


🏁 Script executed:

# Search for any documentation about the type_ field rename
rg -n 'type_' --type=rust -B 2 -A 2 xz/src/types.rs | head -50

Repository: youknowone/xz-rs

Length of output: 201


🏁 Script executed:

# Check xz-sys directory for any CHANGELOG or version notes
ls -la xz-sys/ | head -20

Repository: youknowone/xz-rs

Length of output: 283


🏁 Script executed:

# Check if there are any git tags or version history that document this change
git tag | tail -20

Repository: youknowone/xz-rs

Length of output: 42


🏁 Script executed:

# Look for any documentation about the type_ field or migration in xz-sys
rg -n 'type_' xz-sys/src/ -A 2 -B 2 2>/dev/null | head -50

Repository: youknowone/xz-rs

Length of output: 42


Document the breaking field rename in release notes.

The field rename type_0type_ in lzma_options_delta is confirmed and all in-repo usages have been migrated. However, this breaking change is not documented in the README's breaking changes section (0.4.x or later) or in any CHANGELOG file. Downstream consumers upgrading will encounter compiler errors without clear migration guidance. Add documentation of this field rename to the release notes or README breaking changes section.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/types.rs` at line 374, Add a breaking-change note documenting the
struct field rename from `type_0` to `type_` on `lzma_options_delta` so
downstream users know how to migrate; update the README breaking changes section
(0.4.x or later) and/or CHANGELOG to include the exact old→new field mapping
(`lzma_options_delta::type_0` → `lzma_options_delta::type_`), a brief example of
the code change (access/construct with `type_` instead of `type_0`), and a short
migration note for consumers of any public APIs referencing
`lzma_options_delta`.

@youknowone youknowone changed the title Remove default features from liblzma Further optimization Apr 18, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
xz-sys/src/lib.rs (1)

72-81: ⚠️ Potential issue | 🟠 Major

Document breaking API change: lzma_options_delta.type_r#type in release notes.

The field rename from type_ to r#type is a confirmed public API breaking change for xz-sys consumers. Any code accessing options.type_ will fail to compile. While the rename aligns with using Rust's raw identifier syntax (required since type is a reserved keyword), no CHANGELOG or release notes documenting this breaking change were found in the repository. Add an entry to the release documentation before shipping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz-sys/src/lib.rs` around lines 72 - 81, The public struct field in
lzma_options_delta was renamed from type_ to the raw identifier r#type, which is
a breaking API change for consumers; update the release notes / CHANGELOG to
document this breaking change, explicitly mention the struct lzma_options_delta
and that callers must replace any usage of options.type_ with options.r#type (or
migrate to a compatible accessor), include the rationale (use of Rust raw
identifier because "type" is reserved), and add a migration note and example to
the release entry before shipping.
xz/src/common/filter_encoder.rs (1)

289-316: 🧹 Nitpick | 🔵 Trivial

Redundant update.is_none() check at line 289 now that line 313 does the same match.

With the unwrap() elimination at 313-316, the early-return at 289-291 duplicates the None → LZMA_PROG_ERROR arm. Either drop the early check and let the match at 313-316 handle the missing callback, or keep the early check and replace the match with a single .unwrap() / unwrap_unchecked(). The current form reads both checks, which is harmless but noisy.

♻️ Suggested consolidation
 pub unsafe fn lzma_filters_update(strm: *mut lzma_stream, filters: *const lzma_filter) -> lzma_ret {
-    if (*(*strm).internal).next.update.is_none() {
-        return LZMA_PROG_ERROR;
-    }
-    if lzma_raw_encoder_memusage(filters) == UINT64_MAX {
+    let update = match (*(*strm).internal).next.update {
+        Some(update) => update,
+        None => return LZMA_PROG_ERROR,
+    };
+    if lzma_raw_encoder_memusage(filters) == UINT64_MAX {
         return LZMA_OPTIONS_ERROR;
     }
     ...
-    let update = match (*(*strm).internal).next.update {
-        Some(update) => update,
-        None => return LZMA_PROG_ERROR,
-    };
     update(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/filter_encoder.rs` around lines 289 - 316, Remove the redundant
pre-check that returns LZMA_PROG_ERROR when
(*(*strm).internal).next.update.is_none() and let the later match on
(*(*strm).internal).next.update handle the None case; specifically, delete the
early if block that tests next.update and returns LZMA_PROG_ERROR, keeping the
existing match that binds update (or returns LZMA_PROG_ERROR) so there is a
single, clear ownership/validation point for update in filter_encoder.rs.
♻️ Duplicate comments (7)
xz/src/common/common.rs (1)

144-153: ⚠️ Potential issue | 🟡 Minor

Dead debug_assert!s after the new early-return guards.

The asserts on Lines 150–151 can no longer fire because the explicit return 0 guards on Lines 147–149 already reject the exact conditions being asserted. Either drop them, or hoist them above the early-return guards if you want to keep them as documentation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/common.rs` around lines 144 - 153, The two debug_assert! checks
that verify input/out nullity (debug_assert!(!input.is_null() || *in_pos ==
in_size); and debug_assert!(!out.is_null() || *out_pos == out_size);) are
redundant because the preceding early-return guards already cover those
conditions; remove those two debug_assert! lines from common.rs (or
alternatively move them above the early-return checks if you prefer to keep them
as documentation) so the code no longer contains impossible assertions after the
early returns, leaving the remaining bounds debug_asserts for *in_pos <= in_size
and *out_pos <= out_size intact.
xz/src/common/string_conversion.rs (1)

168-212: ⚠️ Potential issue | 🟡 Minor

parse_bcj and parse_delta still safe fn despite a raw-pointer contract.

Both functions accept *mut *const c_char / *mut c_void and dereference them inside an internal unsafe { ... } block, but the outer signatures are plain fn. This matters because the filter_name_map entries now explicitly cast them as unsafe fn(*mut *const c_char, *const c_char, *mut c_void) -> *const c_char; the cast compiles only by virtue of the safe→unsafe coercion rule, leaving the invariant unexpressed in the signature. Contrast with parse_lzma12 at line 305 which is correctly declared unsafe fn. Promoting these two to unsafe fn removes the implicit coercion and lets the function body drop its outer unsafe { ... } wrapper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/string_conversion.rs` around lines 168 - 212, The functions
parse_bcj and parse_delta are declared as safe fn but require unsafe pointer
dereferences and are used via unsafe fn pointers (allowing implicit coercion);
change both signatures to unsafe fn parse_bcj(...) -> *const c_char and unsafe
fn parse_delta(...) -> *const c_char, remove the surrounding outer unsafe { ...
} blocks inside their bodies, and ensure their usages (e.g., entries in
filter_name_map) match the unsafe fn type so the raw-pointer contract is
explicit.
xz/src/common/threading.rs (2)

449-465: ⚠️ Potential issue | 🟡 Minor

Windows mythread_cond_timedwait still collapses ETIMEDOUT and generic failure into the same return value.

i32::from(ret == 0) returns 0 on success and 1 on any failure, whereas the non-Windows branch returns raw pthread_cond_timedwait semantics (0 / ETIMEDOUT / other errno). Any caller that switches on ret == ETIMEDOUT (upstream xz-utils' worker loop does this) will never take the timeout branch on Windows. Map ERROR_TIMEOUT/elapsed≥timeout to ETIMEDOUT (60) and other failures to a distinct non-zero value to preserve parity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/threading.rs` around lines 449 - 465, The Windows
mythread_cond_timedwait implementation collapses timeout and other failures into
the same non-zero return; change mythread_cond_timedwait so it returns 0 on
success, ETIMEDOUT (60) when the wait timed out (either elapsed >= timeout_ms
before calling or SleepConditionVariableCS returns 0 and GetLastError() ==
ERROR_TIMEOUT), and a distinct non-zero error code (e.g., 1) for other failures;
use the existing condtime.start/timeout fields, call SleepConditionVariableCS,
check GetLastError() for ERROR_TIMEOUT, and map that to ETIMEDOUT while
preserving success as 0 and other failures as a non-zero value.

29-124: ⚠️ Potential issue | 🔴 Critical

Darwin-only type layouts are still applied to every non-Windows target — this corrupts memory on Linux.

__darwin_sigset_t is u32 (4 bytes), _opaque_pthread_t is an 8176-byte struct, and _CLOCK_MONOTONIC = 6 / _CLOCK_THREAD_CPUTIME_ID = 16 are Darwin constants. Under cfg(not(windows)) these are also the definitions used on Linux, where:

  • glibc sigset_t is 128 bytes — in mythread_create (lines 238-245) the 4-byte on-stack all/old passed to pthread_sigmask causes a 128-byte read/write into adjacent stack memory.
  • pthread_t is an unsigned long handle, not an 8176-byte struct.
  • CLOCK_MONOTONIC = 1 and CLOCK_THREAD_CPUTIME_ID = 3 on Linux, so any use of these constants (e.g., a future change to mythread_cond_init picking _CLOCK_MONOTONIC) would silently pass the wrong clock id to clock_gettime. Only _CLOCK_REALTIME = 0 matches Linux by coincidence.

libc = "0.2" is already in the dependency graph — use libc::sigset_t, libc::pthread_t, libc::pthread_attr_t, libc::CLOCK_MONOTONIC, libc::CLOCK_THREAD_CPUTIME_ID, libc::CLOCK_REALTIME, and libc::pthread_sigmask so each OS gets correct layouts automatically. If the __darwin_* shims are kept, they must be gated behind #[cfg(target_os = "macos")], not #[cfg(not(windows))].

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/threading.rs` around lines 29 - 124, The review points out
Darwin-specific type/layouts (e.g., __darwin_sigset_t, _opaque_pthread_t,
__darwin_pthread_* types, and constants like
_CLOCK_MONOTONIC/_CLOCK_THREAD_CPUTIME_ID) are incorrectly used for all
non-Windows targets causing memory corruption; replace those usages with the
platform-correct libc types and constants (use libc::sigset_t, libc::pthread_t,
libc::pthread_attr_t, libc::CLOCK_MONOTONIC, libc::CLOCK_THREAD_CPUTIME_ID,
libc::CLOCK_REALTIME and call libc::pthread_sigmask) where referenced (notably
in mythread_create and mythread_cond_init), or if you keep the Darwin shims gate
them with #[cfg(target_os = "macos")] instead of #[cfg(not(windows))] so Linux
gets correct layouts.
scripts/compare_all_trimmed.sh (2)

85-105: ⚠️ Potential issue | 🟡 Minor

TSV row is appended before hyperfine runs, leaving orphan rows when a measurement fails.

set -euo pipefail will abort the whole script if hyperfine returns non-zero, but the TSV row pointing to the missing ${stem}.json has already been committed to disk. Move the printf below the measurement so the index only reflects completed runs:

-  printf '%s\t%s\t%s\t%s\n' "$stem" "$label" "$rust_name" "$c_name" >>"$SUMMARY_TSV"
-
   hyperfine \
     --shell=none \
     ...
     "$c_cmd"
+
+  printf '%s\t%s\t%s\t%s\n' "$stem" "$label" "$rust_name" "$c_name" >>"$SUMMARY_TSV"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/compare_all_trimmed.sh` around lines 85 - 105, The TSV row is written
before running hyperfine in record_pair(), which leaves orphan entries when
hyperfine fails; move the printf that appends to SUMMARY_TSV to after the
hyperfine invocation (or only append after hyperfine succeeds) so the
SUMMARY_TSV only contains rows for completed measurements, referencing the
existing record_pair function, SUMMARY_TSV, RESULTS_DIR and ${stem}.ensure the
hyperfine block remains unchanged except for relocating the append so failures
won't produce index rows.

270-292: ⚠️ Potential issue | 🟡 Minor

Embedded Python has two latent failure modes that still need guards.

  1. Line 272 — ratio = c / rust raises ZeroDivisionError if the trimmed Rust mean is effectively zero (plausible at very small input sizes with fast probes).
  2. Line 284 — len(by_name[rust_name]['times']) leaks the last loop-iteration's binding; if summary_tsv is empty the header raises NameError, and if a future pair has a different run count the header becomes misleading.
-    ratio = c / rust
+    ratio = float("inf") if rust == 0 else c / rust
-with md.open("w") as f:
+measured_runs = len(by_name[rust_name]["times"]) if rows else 0
+with md.open("w") as f:
     f.write("# Trimmed Backend Comparison\n\n")
-    f.write(f"- measured runs per pair: {len(by_name[rust_name]['times'])}\n")
+    f.write(f"- measured runs per pair: {measured_runs}\n")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/compare_all_trimmed.sh` around lines 270 - 292, Protect against
divide-by-zero when computing ratio by checking the trimmed Rust mean (rust)
before doing ratio = c / rust; if rust is zero (or nearly zero) set ratio =
float("inf") (or a sentinel) and decide status accordingly (e.g., mark "FAIL")
instead of letting ZeroDivisionError propagate; also avoid reading
len(by_name[rust_name]['times']) from a loop-bound name that may not
exist—compute a stable measured_runs value up front for the summary (for
example, measured_runs = len(next(iter(by_name.values()))['times']) if by_name
else 0) and use that in the header so the summary never raises NameError or
reports a misleading count.
scripts/compare_workloads.sh (1)

67-80: ⚠️ Potential issue | 🟡 Minor

User-supplied --expected-size still silently clobbered in the bootstrap branch.

Line 55 captures a user-provided --expected-size, but line 76 unconditionally overwrites EXPECTED_SIZE="$SIZE" whenever the script generates its own compressed input. Either gate the assignment on EXPECTED_SIZE being empty, or error out on inconsistency:

-    COMPRESSED_INPUT="$RESULTS_DIR/${WORKLOAD}-input-${SIZE}.xz"
-    "$C_BIN" \
-      --workload encode "${RAW_ARGS[@]}" --iters 1 --warmup 0 --save-output "$COMPRESSED_INPUT" >/dev/null
-    EXPECTED_SIZE="$SIZE"
+    COMPRESSED_INPUT="$RESULTS_DIR/${WORKLOAD}-input-${SIZE}.xz"
+    "$C_BIN" \
+      --workload encode --size "$SIZE" "${PASSTHROUGH_ARGS[@]}" \
+      --iters 1 --warmup 0 --save-output "$COMPRESSED_INPUT" >/dev/null
+    if [[ -z "$EXPECTED_SIZE" ]]; then
+      EXPECTED_SIZE="$SIZE"
+    elif [[ "$EXPECTED_SIZE" != "$SIZE" ]]; then
+      echo "--expected-size ($EXPECTED_SIZE) conflicts with --size ($SIZE) in bootstrap mode" >&2
+      exit 2
+    fi

Also swapping RAW_ARGSPASSTHROUGH_ARGS keeps the encode invocation clean of script-only flags (e.g., stray --expected-size or --input-kind that only matter to the measured workload).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/compare_workloads.sh` around lines 67 - 80, The script currently
overwrites a user-provided EXPECTED_SIZE when it generates compressed input: in
the branch that sets COMPRESSED_INPUT the line EXPECTED_SIZE="$SIZE" must only
set EXPECTED_SIZE if it is empty (or else verify it equals SIZE and error on
mismatch); also avoid passing script-only flags into the encoder by replacing
RAW_ARGS with PASSTHROUGH_ARGS in the encode invocation (the command using
"$C_BIN" --workload encode ... should use "${PASSTHROUGH_ARGS[@]}" instead of
"${RAW_ARGS[@]}"). Ensure you reference COMPRESSED_INPUT, SIZE, EXPECTED_SIZE,
RAW_ARGS/PASSTHROUGH_ARGS and the "$C_BIN" encode invocation when making these
changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@xz/src/common/common.rs`:
- Around line 432-437: The fallback branch in lzma_get_progress writes through
progress_in/progress_out unconditionally when next.get_progress is None, causing
possible null-pointer derefs; update the else branch to check for null pointers
before writing (i.e., only assign *progress_in = strm.total_in and *progress_out
= strm.total_out when progress_in and progress_out are non-null) and preserve
the original behavior that callers may pass NULL for either progress pointer;
locate the handling around (*(*strm).internal).next.get_progress and modify that
else path to guard dereferences of progress_in and progress_out.
- Around line 131-135: The debug_assert in lzma_alloc_object allows types with
alignment >16 to slip through in release builds; replace or enforce it so
misaligned allocations cannot occur: either add a compile-time trait bound that
only implements for types with core::mem::align_of::<T>() <= 16 (create a sealed
AlignLE16 trait and implement it for allowed types and require T: AlignLE16 on
lzma_alloc_object), or change the debug_assert! to a runtime assert! to enforce
the check in release, and update the function documentation to state the
alignment requirement; ensure any fix still calls lzma_alloc(size_of::<T>(),
allocator) and returns as *mut T.

In `@xz/src/lib.rs`:
- Line 7: Remove the crate-level allow for the lint "unsafe_op_in_unsafe_fn" in
lib.rs (the entry that reads unsafe_op_in_unsafe_fn) so the Rust 2024 linting
behavior is preserved; if you must keep the allow temporarily, replace it with a
clearly marked TODO/FIXME referencing a follow-up task or issue ID to audit and
remove the allow once the transpiled unsafe regions in this crate have been
reviewed (target the crate root attribute where unsafe_op_in_unsafe_fn is
listed).

---

Outside diff comments:
In `@xz-sys/src/lib.rs`:
- Around line 72-81: The public struct field in lzma_options_delta was renamed
from type_ to the raw identifier r#type, which is a breaking API change for
consumers; update the release notes / CHANGELOG to document this breaking
change, explicitly mention the struct lzma_options_delta and that callers must
replace any usage of options.type_ with options.r#type (or migrate to a
compatible accessor), include the rationale (use of Rust raw identifier because
"type" is reserved), and add a migration note and example to the release entry
before shipping.

In `@xz/src/common/filter_encoder.rs`:
- Around line 289-316: Remove the redundant pre-check that returns
LZMA_PROG_ERROR when (*(*strm).internal).next.update.is_none() and let the later
match on (*(*strm).internal).next.update handle the None case; specifically,
delete the early if block that tests next.update and returns LZMA_PROG_ERROR,
keeping the existing match that binds update (or returns LZMA_PROG_ERROR) so
there is a single, clear ownership/validation point for update in
filter_encoder.rs.

---

Duplicate comments:
In `@scripts/compare_all_trimmed.sh`:
- Around line 85-105: The TSV row is written before running hyperfine in
record_pair(), which leaves orphan entries when hyperfine fails; move the printf
that appends to SUMMARY_TSV to after the hyperfine invocation (or only append
after hyperfine succeeds) so the SUMMARY_TSV only contains rows for completed
measurements, referencing the existing record_pair function, SUMMARY_TSV,
RESULTS_DIR and ${stem}.ensure the hyperfine block remains unchanged except for
relocating the append so failures won't produce index rows.
- Around line 270-292: Protect against divide-by-zero when computing ratio by
checking the trimmed Rust mean (rust) before doing ratio = c / rust; if rust is
zero (or nearly zero) set ratio = float("inf") (or a sentinel) and decide status
accordingly (e.g., mark "FAIL") instead of letting ZeroDivisionError propagate;
also avoid reading len(by_name[rust_name]['times']) from a loop-bound name that
may not exist—compute a stable measured_runs value up front for the summary (for
example, measured_runs = len(next(iter(by_name.values()))['times']) if by_name
else 0) and use that in the header so the summary never raises NameError or
reports a misleading count.

In `@scripts/compare_workloads.sh`:
- Around line 67-80: The script currently overwrites a user-provided
EXPECTED_SIZE when it generates compressed input: in the branch that sets
COMPRESSED_INPUT the line EXPECTED_SIZE="$SIZE" must only set EXPECTED_SIZE if
it is empty (or else verify it equals SIZE and error on mismatch); also avoid
passing script-only flags into the encoder by replacing RAW_ARGS with
PASSTHROUGH_ARGS in the encode invocation (the command using "$C_BIN" --workload
encode ... should use "${PASSTHROUGH_ARGS[@]}" instead of "${RAW_ARGS[@]}").
Ensure you reference COMPRESSED_INPUT, SIZE, EXPECTED_SIZE,
RAW_ARGS/PASSTHROUGH_ARGS and the "$C_BIN" encode invocation when making these
changes.

In `@xz/src/common/common.rs`:
- Around line 144-153: The two debug_assert! checks that verify input/out
nullity (debug_assert!(!input.is_null() || *in_pos == in_size); and
debug_assert!(!out.is_null() || *out_pos == out_size);) are redundant because
the preceding early-return guards already cover those conditions; remove those
two debug_assert! lines from common.rs (or alternatively move them above the
early-return checks if you prefer to keep them as documentation) so the code no
longer contains impossible assertions after the early returns, leaving the
remaining bounds debug_asserts for *in_pos <= in_size and *out_pos <= out_size
intact.

In `@xz/src/common/string_conversion.rs`:
- Around line 168-212: The functions parse_bcj and parse_delta are declared as
safe fn but require unsafe pointer dereferences and are used via unsafe fn
pointers (allowing implicit coercion); change both signatures to unsafe fn
parse_bcj(...) -> *const c_char and unsafe fn parse_delta(...) -> *const c_char,
remove the surrounding outer unsafe { ... } blocks inside their bodies, and
ensure their usages (e.g., entries in filter_name_map) match the unsafe fn type
so the raw-pointer contract is explicit.

In `@xz/src/common/threading.rs`:
- Around line 449-465: The Windows mythread_cond_timedwait implementation
collapses timeout and other failures into the same non-zero return; change
mythread_cond_timedwait so it returns 0 on success, ETIMEDOUT (60) when the wait
timed out (either elapsed >= timeout_ms before calling or
SleepConditionVariableCS returns 0 and GetLastError() == ERROR_TIMEOUT), and a
distinct non-zero error code (e.g., 1) for other failures; use the existing
condtime.start/timeout fields, call SleepConditionVariableCS, check
GetLastError() for ERROR_TIMEOUT, and map that to ETIMEDOUT while preserving
success as 0 and other failures as a non-zero value.
- Around line 29-124: The review points out Darwin-specific type/layouts (e.g.,
__darwin_sigset_t, _opaque_pthread_t, __darwin_pthread_* types, and constants
like _CLOCK_MONOTONIC/_CLOCK_THREAD_CPUTIME_ID) are incorrectly used for all
non-Windows targets causing memory corruption; replace those usages with the
platform-correct libc types and constants (use libc::sigset_t, libc::pthread_t,
libc::pthread_attr_t, libc::CLOCK_MONOTONIC, libc::CLOCK_THREAD_CPUTIME_ID,
libc::CLOCK_REALTIME and call libc::pthread_sigmask) where referenced (notably
in mythread_create and mythread_cond_init), or if you keep the Darwin shims gate
them with #[cfg(target_os = "macos")] instead of #[cfg(not(windows))] so Linux
gets correct layouts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 19d56680-7940-47dd-92ce-7bc93121b473

📥 Commits

Reviewing files that changed from the base of the PR and between 4741f2e and dc080a9.

📒 Files selected for processing (25)
  • .github/workflows/main.yml
  • .github/workflows/webassembly.yml
  • Cargo.lock.msrv
  • Cargo.toml
  • docs/performance-workflow.md
  • liblzma-sys/Cargo.toml
  • perf-probe/Cargo.toml
  • perf-probe/src/main.rs
  • scripts/compare_all_trimmed.sh
  • scripts/compare_workloads.sh
  • systest/build.rs
  • xz-sys/Cargo.toml
  • xz-sys/src/lib.rs
  • xz/Cargo.toml
  • xz/src/alloc.rs
  • xz/src/common/common.rs
  • xz/src/common/filter_decoder.rs
  • xz/src/common/filter_encoder.rs
  • xz/src/common/string_conversion.rs
  • xz/src/common/threading.rs
  • xz/src/lib.rs
  • xz/src/lzma/lzma_decoder.rs
  • xz/src/lzma/lzma_encoder.rs
  • xz/src/lzma/lzma_encoder_optimum_normal.rs
  • xz/src/types.rs

Comment thread xz/src/common/common.rs
Comment on lines +131 to +135
#[inline]
pub unsafe fn lzma_alloc_object<T>(allocator: *const lzma_allocator) -> *mut T {
debug_assert!(core::mem::align_of::<T>() <= 16);
lzma_alloc(core::mem::size_of::<T>() as size_t, allocator) as *mut T
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🌐 Web query:

Is const { assert!(...) } (inline const block with assert referencing generic type parameters) stable in Rust 1.85?

💡 Result:

No, const { assert!(...) } with inline const blocks referencing generic type parameters is stable in Rust 1.85.

Citations:


Consider runtime or trait-based alignment validation instead of debug assertions.

debug_assert!(core::mem::align_of::<T>() <= 16) is stripped in release builds, so a type with alignment exceeding 16 bytes could silently allocate misaligned memory. While inline const assertions with generic type parameters are not yet stable in Rust, alternatives include:

  • Using a trait bound to enforce alignment at compile time (e.g., a custom trait that only implements for types with alignment ≤ 16)
  • Converting the debug assertion to a non-debug assertion if this is critical to safety
  • Adding documentation clarifying the alignment constraint and its enforcement responsibility
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/common.rs` around lines 131 - 135, The debug_assert in
lzma_alloc_object allows types with alignment >16 to slip through in release
builds; replace or enforce it so misaligned allocations cannot occur: either add
a compile-time trait bound that only implements for types with
core::mem::align_of::<T>() <= 16 (create a sealed AlignLE16 trait and implement
it for allowed types and require T: AlignLE16 on lzma_alloc_object), or change
the debug_assert! to a runtime assert! to enforce the check in release, and
update the function documentation to state the alignment requirement; ensure any
fix still calls lzma_alloc(size_of::<T>(), allocator) and returns as *mut T.

Comment thread xz/src/common/common.rs
Comment on lines +432 to 437
if let Some(get_progress) = (*(*strm).internal).next.get_progress {
get_progress((*(*strm).internal).next.coder, progress_in, progress_out);
} else {
*progress_in = (*strm).total_in;
*progress_out = (*strm).total_out;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Potential null-pointer deref in lzma_get_progress fallback path.

When strm/internal are non-null but next.get_progress is None, the else branch at Lines 435–436 unconditionally writes through progress_in and progress_out. The earlier guard at Lines 422–430 only tolerates null progress_* when strm/internal are null, so a caller that passes NULL for either progress pointer on a valid stream (documented to be allowed by liblzma) will segfault here. This is new behavior introduced by the unwrapif let Some rewrite.

🔒️ Proposed fix
     if let Some(get_progress) = (*(*strm).internal).next.get_progress {
         get_progress((*(*strm).internal).next.coder, progress_in, progress_out);
     } else {
-        *progress_in = (*strm).total_in;
-        *progress_out = (*strm).total_out;
+        if !progress_in.is_null() {
+            *progress_in = (*strm).total_in;
+        }
+        if !progress_out.is_null() {
+            *progress_out = (*strm).total_out;
+        }
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(get_progress) = (*(*strm).internal).next.get_progress {
get_progress((*(*strm).internal).next.coder, progress_in, progress_out);
} else {
*progress_in = (*strm).total_in;
*progress_out = (*strm).total_out;
};
if let Some(get_progress) = (*(*strm).internal).next.get_progress {
get_progress((*(*strm).internal).next.coder, progress_in, progress_out);
} else {
if !progress_in.is_null() {
*progress_in = (*strm).total_in;
}
if !progress_out.is_null() {
*progress_out = (*strm).total_out;
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/common.rs` around lines 432 - 437, The fallback branch in
lzma_get_progress writes through progress_in/progress_out unconditionally when
next.get_progress is None, causing possible null-pointer derefs; update the else
branch to check for null pointers before writing (i.e., only assign *progress_in
= strm.total_in and *progress_out = strm.total_out when progress_in and
progress_out are non-null) and preserve the original behavior that callers may
pass NULL for either progress pointer; locate the handling around
(*(*strm).internal).next.get_progress and modify that else path to guard
dereferences of progress_in and progress_out.

Comment thread xz/src/lib.rs
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
unsafe_op_in_unsafe_fn,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Suppressing unsafe_op_in_unsafe_fn weakens documentation of internal unsafety.

This lint, when enabled (as it is by default in Rust 2024 edition), forces callers of unsafe operations inside an unsafe fn to still wrap them in explicit unsafe { ... } blocks, localizing safety reasoning. Allowing it globally is a reasonable pragmatic choice for a c2rust-transpiled crate, but it undoes a key 2024-edition safety-ergonomics win. Consider tracking a follow-up to remove this allow once the transpiled unsafe regions have been auditable-tagged.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/lib.rs` at line 7, Remove the crate-level allow for the lint
"unsafe_op_in_unsafe_fn" in lib.rs (the entry that reads unsafe_op_in_unsafe_fn)
so the Rust 2024 linting behavior is preserved; if you must keep the allow
temporarily, replace it with a clearly marked TODO/FIXME referencing a follow-up
task or issue ID to audit and remove the allow once the transpiled unsafe
regions in this crate have been reviewed (target the crate root attribute where
unsafe_op_in_unsafe_fn is listed).

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
xz/src/common/filter_encoder.rs (1)

295-312: ⚠️ Potential issue | 🟡 Minor

Missing hard bound on count before indexing reversed_filters.

count is incremented until the LZMA_VLI_UNKNOWN sentinel with no check against LZMA_FILTERS_MAX / the length of reversed_filters (5). If an adversarial or malformed caller passes a filter chain without a sentinel within 5 entries, the subsequent reversed_filter_slot(..., count as usize) write at line 311 becomes OOB — caught by the debug_assert! only in debug builds. lzma_validate_chain enforces this in most ingress paths, but lzma_filters_update does not call it before this loop. Consider adding an explicit bound:

     let mut count: size_t = 1;
     while (*filters.offset(count as isize)).id != LZMA_VLI_UNKNOWN {
         count += 1;
+        if count >= LZMA_FILTERS_MAX as size_t {
+            return LZMA_OPTIONS_ERROR;
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/filter_encoder.rs` around lines 295 - 312, The loop that
increments count until LZMA_VLI_UNKNOWN can overflow the fixed array
reversed_filters (length 5) causing an OOB write; clamp or validate count
against LZMA_FILTERS_MAX (or the length of reversed_filters) before using it and
return an error if the sentinel isn't found within the bound. Modify the logic
in lzma_filters_update (the function containing count, reversed_filters, and
reversed_filter_slot) to check count < LZMA_FILTERS_MAX while scanning, or
call/inline lzma_validate_chain beforehand, and ensure the final write to
reversed_filter_slot(count as usize) only happens when count < LZMA_FILTERS_MAX
(otherwise handle as invalid input).
♻️ Duplicate comments (7)
xz/src/lz/lz_encoder.rs (2)

365-371: 🧹 Nitpick | 🔵 Trivial

Existing finding still applies: collapse the duplicate options_update check.

Line 365 and the None match arm handle the same case. A single match is enough.

♻️ Proposed cleanup
-    if (*coder).lz.options_update.is_none() {
-        return LZMA_PROG_ERROR;
-    }
     let options_update = match (*coder).lz.options_update {
         Some(options_update) => options_update,
         None => return LZMA_PROG_ERROR,
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/lz/lz_encoder.rs` around lines 365 - 371, The code checks
(*coder).lz.options_update twice; remove the initial if (*) check and collapse
to a single match or if let on (*coder).lz.options_update so you only handle the
None case once (returning LZMA_PROG_ERROR) and bind options_update for later
use; update the block around the options_update binding in lz_encoder.rs
(reference symbol: (*coder).lz.options_update and the LZMA_PROG_ERROR return) to
avoid the duplicate check.

72-75: 🧹 Nitpick | 🔵 Trivial

Existing finding still applies: avoid unreachable_unchecked() here.

This is still fragile because the None case is only excluded by the outer check. Prefer binding next.code with one match/if let so the invariant stays local.

#!/bin/bash
# Description: Confirm the current `unreachable_unchecked()` pattern in `fill_window`.
# Expected: no `unreachable_unchecked()` remains after refactoring to a single local match.

rg -n -C6 'unreachable_unchecked\(\)' xz/src/lz/lz_encoder.rs
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/lz/lz_encoder.rs` around lines 72 - 75, The pattern using
core::hint::unreachable_unchecked() when reading (*coder).next.code is fragile;
in fill_window (the surrounding function) bind next.code once with a safe match
or if let (e.g., if let Some(code) = (*coder).next.code { let code = code; ... }
) so the presence of the value is enforced locally and remove the
unreachable_unchecked() call; if the None branch should never occur keep an
explicit safe panic/log (or return an Err) instead of unreachable_unchecked(),
updating the code paths that use the local code binding (references to coder and
next.code) accordingly.
xz/src/common/common.rs (2)

429-434: ⚠️ Potential issue | 🔴 Critical

Guard nullable progress outputs in the fallback path.

When next.get_progress is absent, this branch unconditionally dereferences progress_in and progress_out, unlike the null-stream branch above.

🛡️ Proposed fix
     if let Some(get_progress) = (*(*strm).internal).next.get_progress {
         get_progress((*(*strm).internal).next.coder, progress_in, progress_out);
     } else {
-        *progress_in = (*strm).total_in;
-        *progress_out = (*strm).total_out;
+        if !progress_in.is_null() {
+            *progress_in = (*strm).total_in;
+        }
+        if !progress_out.is_null() {
+            *progress_out = (*strm).total_out;
+        }
     };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/common.rs` around lines 429 - 434, The fallback else branch for
when (*(*strm).internal).next.get_progress is None currently unconditionally
dereferences progress_in and progress_out; modify that branch to guard each
pointer (progress_in and progress_out) before writing: check if progress_in is
non-null and only then assign (*progress_in) = (*strm).total_in, and similarly
check progress_out before assigning (*progress_out) = (*strm).total_out so the
behavior matches the null-stream safe handling used elsewhere (referencing the
same (*strm).internal.next.get_progress, next.coder, total_in, total_out
symbols).

131-134: ⚠️ Potential issue | 🟠 Major

Enforce typed allocation alignment outside debug builds.

debug_assert! is removed in release, so a future over-aligned T can be returned from a 16-byte-aligned allocator as *mut T. Make this a real assertion or constrain T with an explicit allocation-safety trait.

🛡️ Minimal enforcement
 pub unsafe fn lzma_alloc_object<T>(allocator: *const lzma_allocator) -> *mut T {
-    debug_assert!(core::mem::align_of::<T>() <= 16);
+    assert!(
+        core::mem::align_of::<T>() <= 16,
+        "lzma_alloc_object only supports types with alignment <= 16"
+    );
     lzma_alloc(core::mem::size_of::<T>() as size_t, allocator) as *mut T
 }

Verification:

#!/bin/bash
# Locate all current typed allocations so their alignments can be audited.
rg -nP --type=rust -C2 'lzma_alloc_object::<[^>]+>' xz/src
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/common.rs` around lines 131 - 134, The current debug-only check
in lzma_alloc_object allows release builds to produce misaligned pointers for
over-aligned T; change the debug_assert! in lzma_alloc_object to a real runtime
assertion (assert!(core::mem::align_of::<T>() <= 16)) or alternatively introduce
an unsafe marker trait (e.g., trait AllocSafeForLzma {}) and add a bound T:
AllocSafeForLzma on lzma_alloc_object so only vetted types can be allocated;
update callers or implement the trait for existing safe types if you choose the
trait route and keep the call to lzma_alloc(core::mem::size_of::<T>() as size_t,
allocator) as *mut T.
xz/src/common/string_conversion.rs (1)

168-182: ⚠️ Potential issue | 🟠 Major

Make the raw-pointer parsers unsafe fn.

parse_bcj and parse_delta still expose safe signatures while requiring valid raw pointers. That leaves UB-callable code behind a safe function boundary, and the filter_name_map casts are relying on safe→unsafe coercion.

🛡️ Proposed fix
-fn parse_bcj(
+unsafe fn parse_bcj(
     str: *mut *const c_char,
     str_end: *const c_char,
     filter_options: *mut c_void,
 ) -> *const c_char {
@@
-fn parse_delta(
+unsafe fn parse_delta(
     str: *mut *const c_char,
     str_end: *const c_char,
     filter_options: *mut c_void,
 ) -> *const c_char {

Then remove the unnecessary safe→unsafe coercion reliance at the filter_name_map entries if desired.

Verification:

#!/bin/bash
# Verify the parser signatures and their function-pointer uses.
rg -nP --type=rust -C4 'fn\s+parse_(bcj|delta)\s*\(' xz/src/common/string_conversion.rs
rg -nP --type=rust -C3 'parse_(bcj|delta)\s+as\s+unsafe\s+fn' xz/src/common/string_conversion.rs

Also applies to: 195-212

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/string_conversion.rs` around lines 168 - 182, The functions
parse_bcj and parse_delta are currently declared as safe but take raw pointers,
so change their signatures to unsafe fn parse_bcj(...) -> *const c_char and
unsafe fn parse_delta(...) -> *const c_char, update any places where they are
referenced as function pointers (e.g., entries in filter_name_map) to use them
as unsafe fn (or remove any safe→unsafe coercions by matching the function
pointer type to unsafe fn), and ensure call sites that invoke these parsers call
them within unsafe blocks; also update any casts that used safe-to-unsafe
coercion for filter_name_map to match the new unsafe fn pointer type.
xz/src/common/threading.rs (2)

29-135: ⚠️ Potential issue | 🔴 Critical

Use platform libc pthread/sigset/clock types instead of Darwin layouts.

The non-Windows path still applies Darwin pthread and sigset_t layouts to every Unix target. On Linux, passing the 4-byte sigset_t at Lines 236-243 into pthread_sigmask can corrupt stack memory.

Prefer libc::sigset_t, libc::pthread_t, libc::pthread_mutex_t, libc::pthread_cond_t, libc::timespec, and libc::CLOCK_* under #[cfg(not(windows))], or guard these Darwin layouts with #[cfg(target_os = "macos")] only.

Verification:

#!/bin/bash
# Confirm the current hand-rolled ABI types and whether libc is available.
rg -nP --type=rust -C3 '(__darwin|_opaque_pthread|pub type sigset_t|_CLOCK_MONOTONIC|pthread_sigmask)' xz/src/common/threading.rs
rg -nP --type=toml -C2 '\blibc\s*='

Also applies to: 179-203, 229-257

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/threading.rs` around lines 29 - 135, The file defines
Darwin-specific ABI structs and type aliases (__darwin_pthread_handler_rec,
_opaque_pthread_*, __darwin_sigset_t, __darwin_time_t, timespec, and CLOCK_*
constants) under #[cfg(not(windows))], which incorrectly applies macOS layouts
to all Unix targets and can corrupt calls like pthread_sigmask; fix by switching
these aliases to use the platform libc types (libc::sigset_t, libc::pthread_t,
libc::pthread_mutex_t, libc::pthread_cond_t, libc::timespec, and libc::CLOCK_*
constants) when #[cfg(not(windows))], or alternatively narrow the existing
Darwin-specific definitions to #[cfg(target_os = "macos")] (keeping the
__darwin_* structs and aliases only for macOS) so Linux and other Unixes use
libc types instead; update the declarations for sigset_t, pthread_t,
pthread_mutex_t, pthread_cond_t, timespec, and the clock constants accordingly
and remove or guard the __darwin_*/_opaque_pthread_* definitions as needed.

447-463: ⚠️ Potential issue | 🟠 Major

Preserve pthread-style timeout semantics on Windows.

mythread_cond_timedwait returns 1 for every Windows wait failure, so callers cannot distinguish timeout from other failures. Return the repo’s errno-compatible timeout value for actual timeouts and a distinct non-zero error for other failures.

Verification:

#!/bin/bash
# Locate timeout comparisons/constants to choose the correct errno-compatible timeout value.
rg -nP --type=rust -C3 '\bETIMEDOUT\b|mythread_cond_timedwait|SleepConditionVariableCS|GetLastError' xz/src
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@xz/src/common/filter_encoder.rs`:
- Around line 313-316: The code performs two redundant checks for
(*(*strm).internal).next.update: an early check that returns LZMA_PROG_ERROR (at
line ~289) and then a match that treats None as possible again; remove the
redundancy by choosing one approach—either drop the early is_none() check and
keep the match on update, or keep the early check and replace the match with an
unsafe direct extraction (e.g., using unwrap_unchecked()) for update to avoid
the dead None arm; apply the same safety caveat used in filter_buffer_decoder.rs
if you choose unwrap_unchecked().
- Around line 268-281: The function encoder_find unnecessarily uses pointer
arithmetic and multiple unsafe blocks; replace the manual size_of loop and
encoders_ptr.add(...) with safe indexing over the static encoders array (e.g.,
for i in 0..encoders.len() or while i < encoders.len()) using &encoders[i] to
get a safe reference and then convert it to a raw pointer with as *const
lzma_filter_encoder when returning; remove the extra unsafe blocks and size_of
math, keep the signature and null return (core::ptr::null()) unchanged, and
adjust the loop index type to usize to avoid casts.
- Around line 258-267: The helper functions reversed_filter_slot and
supported_action_slot use unsafe pointer arithmetic and remove release-mode
bounds checks; replace their uses with idiomatic indexing (e.g., &mut
filters[index] for the fixed-size [lzma_filter; 5] and &mut actions[index as
usize] for the bool array) and delete these two unsafe helpers; update call
sites to use direct indexing so the compiler retains standard bounds checking
semantics and keeps consistency with other files (e.g., stream_decoder_mt.rs /
stream_encoder_mt.rs).

In `@xz/src/common/microlzma_encoder.rs`:
- Around line 23-26: The current use of debug_assert! with unwrap_unchecked on
(*coder).lzma.set_out_limit and (*coder).lzma.code is unsafe in release builds;
change these to real runtime checks (e.g., test is_some()/is_none() or match)
and handle the missing-callback case instead of calling unwrap_unchecked: check
(*coder).lzma.set_out_limit and (*coder).lzma.code, early-return or propagate an
error if either is None, and then safely extract the callbacks into local
variables (set_out_limit and code) for use.

In `@xz/src/lzma/lzma2_decoder.rs`:
- Around line 92-94: Replace the unsafe debug_assert!/unwrap_unchecked() pattern
for optional callbacks with explicit pattern matching that returns
LZMA_PROG_ERROR when the callback is missing: for the reset call use `let
Some(reset) = (*coder).lzma.reset else { return LZMA_PROG_ERROR; }; reset(...)`,
for the set_uncompressed call use `let Some(set_uncompressed) =
(*coder).lzma.set_uncompressed else { return LZMA_PROG_ERROR; };
set_uncompressed(...)`, and similarly for the other reset occurrence; this
ensures no UB in release builds and fails gracefully when the callback is None.

---

Outside diff comments:
In `@xz/src/common/filter_encoder.rs`:
- Around line 295-312: The loop that increments count until LZMA_VLI_UNKNOWN can
overflow the fixed array reversed_filters (length 5) causing an OOB write; clamp
or validate count against LZMA_FILTERS_MAX (or the length of reversed_filters)
before using it and return an error if the sentinel isn't found within the
bound. Modify the logic in lzma_filters_update (the function containing count,
reversed_filters, and reversed_filter_slot) to check count < LZMA_FILTERS_MAX
while scanning, or call/inline lzma_validate_chain beforehand, and ensure the
final write to reversed_filter_slot(count as usize) only happens when count <
LZMA_FILTERS_MAX (otherwise handle as invalid input).

---

Duplicate comments:
In `@xz/src/common/common.rs`:
- Around line 429-434: The fallback else branch for when
(*(*strm).internal).next.get_progress is None currently unconditionally
dereferences progress_in and progress_out; modify that branch to guard each
pointer (progress_in and progress_out) before writing: check if progress_in is
non-null and only then assign (*progress_in) = (*strm).total_in, and similarly
check progress_out before assigning (*progress_out) = (*strm).total_out so the
behavior matches the null-stream safe handling used elsewhere (referencing the
same (*strm).internal.next.get_progress, next.coder, total_in, total_out
symbols).
- Around line 131-134: The current debug-only check in lzma_alloc_object allows
release builds to produce misaligned pointers for over-aligned T; change the
debug_assert! in lzma_alloc_object to a real runtime assertion
(assert!(core::mem::align_of::<T>() <= 16)) or alternatively introduce an unsafe
marker trait (e.g., trait AllocSafeForLzma {}) and add a bound T:
AllocSafeForLzma on lzma_alloc_object so only vetted types can be allocated;
update callers or implement the trait for existing safe types if you choose the
trait route and keep the call to lzma_alloc(core::mem::size_of::<T>() as size_t,
allocator) as *mut T.

In `@xz/src/common/string_conversion.rs`:
- Around line 168-182: The functions parse_bcj and parse_delta are currently
declared as safe but take raw pointers, so change their signatures to unsafe fn
parse_bcj(...) -> *const c_char and unsafe fn parse_delta(...) -> *const c_char,
update any places where they are referenced as function pointers (e.g., entries
in filter_name_map) to use them as unsafe fn (or remove any safe→unsafe
coercions by matching the function pointer type to unsafe fn), and ensure call
sites that invoke these parsers call them within unsafe blocks; also update any
casts that used safe-to-unsafe coercion for filter_name_map to match the new
unsafe fn pointer type.

In `@xz/src/common/threading.rs`:
- Around line 29-135: The file defines Darwin-specific ABI structs and type
aliases (__darwin_pthread_handler_rec, _opaque_pthread_*, __darwin_sigset_t,
__darwin_time_t, timespec, and CLOCK_* constants) under #[cfg(not(windows))],
which incorrectly applies macOS layouts to all Unix targets and can corrupt
calls like pthread_sigmask; fix by switching these aliases to use the platform
libc types (libc::sigset_t, libc::pthread_t, libc::pthread_mutex_t,
libc::pthread_cond_t, libc::timespec, and libc::CLOCK_* constants) when
#[cfg(not(windows))], or alternatively narrow the existing Darwin-specific
definitions to #[cfg(target_os = "macos")] (keeping the __darwin_* structs and
aliases only for macOS) so Linux and other Unixes use libc types instead; update
the declarations for sigset_t, pthread_t, pthread_mutex_t, pthread_cond_t,
timespec, and the clock constants accordingly and remove or guard the
__darwin_*/_opaque_pthread_* definitions as needed.

In `@xz/src/lz/lz_encoder.rs`:
- Around line 365-371: The code checks (*coder).lz.options_update twice; remove
the initial if (*) check and collapse to a single match or if let on
(*coder).lz.options_update so you only handle the None case once (returning
LZMA_PROG_ERROR) and bind options_update for later use; update the block around
the options_update binding in lz_encoder.rs (reference symbol:
(*coder).lz.options_update and the LZMA_PROG_ERROR return) to avoid the
duplicate check.
- Around line 72-75: The pattern using core::hint::unreachable_unchecked() when
reading (*coder).next.code is fragile; in fill_window (the surrounding function)
bind next.code once with a safe match or if let (e.g., if let Some(code) =
(*coder).next.code { let code = code; ... } ) so the presence of the value is
enforced locally and remove the unreachable_unchecked() call; if the None branch
should never occur keep an explicit safe panic/log (or return an Err) instead of
unreachable_unchecked(), updating the code paths that use the local code binding
(references to coder and next.code) accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cf85d7f9-d704-4545-8a44-63de1471c964

📥 Commits

Reviewing files that changed from the base of the PR and between dc080a9 and 146611d.

📒 Files selected for processing (22)
  • xz/src/common/auto_decoder.rs
  • xz/src/common/block_buffer_decoder.rs
  • xz/src/common/common.rs
  • xz/src/common/filter_buffer_decoder.rs
  • xz/src/common/filter_buffer_encoder.rs
  • xz/src/common/filter_common.rs
  • xz/src/common/filter_decoder.rs
  • xz/src/common/filter_encoder.rs
  • xz/src/common/index.rs
  • xz/src/common/microlzma_encoder.rs
  • xz/src/common/outqueue.rs
  • xz/src/common/stream_buffer_decoder.rs
  • xz/src/common/stream_decoder_mt.rs
  • xz/src/common/stream_encoder_mt.rs
  • xz/src/common/string_conversion.rs
  • xz/src/common/threading.rs
  • xz/src/lz/lz_decoder.rs
  • xz/src/lz/lz_encoder.rs
  • xz/src/lzma/lzma2_decoder.rs
  • xz/src/lzma/lzma2_encoder.rs
  • xz/src/lzma/lzma_decoder.rs
  • xz/src/lzma/lzma_encoder.rs

Comment on lines +258 to +267
#[inline(always)]
unsafe fn reversed_filter_slot(filters: *mut [lzma_filter; 5], index: usize) -> *mut lzma_filter {
debug_assert!(index < 5);
(filters as *mut lzma_filter).add(index)
}
#[inline(always)]
unsafe fn supported_action_slot(actions: *mut bool, index: u32) -> *mut bool {
debug_assert!((index as usize) < 5);
actions.add(index as usize)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Prefer idiomatic array indexing over pointer-arithmetic helpers.

reversed_filter_slot / supported_action_slot replace &mut arr[i] with raw-pointer arithmetic behind a debug_assert!. After inlining this should produce the same codegen as direct indexing on a fixed-size array (the bounds check folds away when the index is a constant like LZMA_RUN/LZMA_FINISH), while losing the release-mode bounds check and adding an unsafe API surface. Direct indexing would be clearer and strictly safer.

♻️ Suggested simplification
-#[inline(always)]
-unsafe fn reversed_filter_slot(filters: *mut [lzma_filter; 5], index: usize) -> *mut lzma_filter {
-    debug_assert!(index < 5);
-    (filters as *mut lzma_filter).add(index)
-}
-#[inline(always)]
-unsafe fn supported_action_slot(actions: *mut bool, index: u32) -> *mut bool {
-    debug_assert!((index as usize) < 5);
-    actions.add(index as usize)
-}

Then at the call sites:

-        *reversed_filter_slot(
-            ::core::ptr::addr_of_mut!(reversed_filters),
-            (count - i - 1) as usize,
-        ) = *filters.offset(i as isize);
+        reversed_filters[(count - i - 1) as usize] = *filters.offset(i as isize);
...
-    (*reversed_filter_slot(::core::ptr::addr_of_mut!(reversed_filters), count as usize)).id =
-        LZMA_VLI_UNKNOWN;
+    reversed_filters[count as usize].id = LZMA_VLI_UNKNOWN;
...
-    *supported_action_slot(
-        ::core::ptr::addr_of_mut!((*(*strm).internal).supported_actions) as *mut bool,
-        LZMA_RUN,
-    ) = true;
+    (*(*strm).internal).supported_actions[LZMA_RUN as usize] = true;

Note: other files (e.g., stream_decoder_mt.rs:1555-1556, stream_encoder_mt.rs:1320-1323) still use direct indexing for supported_actions, so the helper also introduces inconsistency across the crate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/filter_encoder.rs` around lines 258 - 267, The helper functions
reversed_filter_slot and supported_action_slot use unsafe pointer arithmetic and
remove release-mode bounds checks; replace their uses with idiomatic indexing
(e.g., &mut filters[index] for the fixed-size [lzma_filter; 5] and &mut
actions[index as usize] for the bool array) and delete these two unsafe helpers;
update call sites to use direct indexing so the compiler retains standard bounds
checking semantics and keeps consistency with other files (e.g.,
stream_decoder_mt.rs / stream_encoder_mt.rs).

Comment on lines 268 to 281
fn encoder_find(id: lzma_vli) -> *const lzma_filter_encoder {
let encoders_ptr = encoders.as_ptr();
let mut i: size_t = 0;
while i < core::mem::size_of::<[lzma_filter_encoder; 12]>()
/ core::mem::size_of::<lzma_filter_encoder>()
{
if encoders[i as usize].id == id {
return &encoders[i as usize];
let encoder = unsafe { encoders_ptr.add(i as usize) };
if unsafe { (*encoder).id } == id {
return encoder;
}
i += 1;
}
core::ptr::null()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Pointer arithmetic in encoder_find is a wash vs. safe indexing.

Switching from &encoders[i as usize] (bounds-checked, but the check folds away given the loop bound) to encoders_ptr.add(i as usize) with two unsafe {} blocks doesn't buy a measurable win here and slightly broadens the unsafe surface. If the goal is purely optimization, direct indexing on a static array of known length already gives the optimizer what it needs; otherwise consider keeping this as safe code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/filter_encoder.rs` around lines 268 - 281, The function
encoder_find unnecessarily uses pointer arithmetic and multiple unsafe blocks;
replace the manual size_of loop and encoders_ptr.add(...) with safe indexing
over the static encoders array (e.g., for i in 0..encoders.len() or while i <
encoders.len()) using &encoders[i] to get a safe reference and then convert it
to a raw pointer with as *const lzma_filter_encoder when returning; remove the
extra unsafe blocks and size_of math, keep the signature and null return
(core::ptr::null()) unchanged, and adjust the loop index type to usize to avoid
casts.

Comment thread xz/src/common/filter_encoder.rs Outdated
Comment on lines +23 to +26
debug_assert!((*coder).lzma.set_out_limit.is_some());
let set_out_limit = (*coder).lzma.set_out_limit.unwrap_unchecked();
debug_assert!((*coder).lzma.code.is_some());
let code = (*coder).lzma.code.unwrap_unchecked();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep these callback checks in release builds.

debug_assert! is compiled out in release, so unwrap_unchecked() turns a missing nested callback into undefined behavior. Since lzma_next_coder_s.code and set_out_limit are Option fields and are initialized as None before lzma_next_filter_init, preserve the checked error path here.

🛡️ Proposed fix
-    debug_assert!((*coder).lzma.set_out_limit.is_some());
-    let set_out_limit = (*coder).lzma.set_out_limit.unwrap_unchecked();
-    debug_assert!((*coder).lzma.code.is_some());
-    let code = (*coder).lzma.code.unwrap_unchecked();
+    let Some(set_out_limit) = (*coder).lzma.set_out_limit else {
+        return LZMA_PROG_ERROR;
+    };
+    let Some(code) = (*coder).lzma.code else {
+        return LZMA_PROG_ERROR;
+    };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
debug_assert!((*coder).lzma.set_out_limit.is_some());
let set_out_limit = (*coder).lzma.set_out_limit.unwrap_unchecked();
debug_assert!((*coder).lzma.code.is_some());
let code = (*coder).lzma.code.unwrap_unchecked();
let Some(set_out_limit) = (*coder).lzma.set_out_limit else {
return LZMA_PROG_ERROR;
};
let Some(code) = (*coder).lzma.code else {
return LZMA_PROG_ERROR;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/common/microlzma_encoder.rs` around lines 23 - 26, The current use of
debug_assert! with unwrap_unchecked on (*coder).lzma.set_out_limit and
(*coder).lzma.code is unsafe in release builds; change these to real runtime
checks (e.g., test is_some()/is_none() or match) and handle the missing-callback
case instead of calling unwrap_unchecked: check (*coder).lzma.set_out_limit and
(*coder).lzma.code, early-return or propagate an error if either is None, and
then safely extract the callbacks into local variables (set_out_limit and code)
for use.

Comment on lines +92 to +94
debug_assert!((*coder).lzma.reset.is_some());
let reset = (*coder).lzma.reset.unwrap_unchecked();
reset(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify all release-mode unchecked optional callback unwraps in this decoder.
# Expected: each `unwrap_unchecked()` should either be guarded by a runtime branch
# that returns an error, or be replaced with a defined failure path.

rg -n -C4 'unwrap_unchecked\(\)' xz/src/lzma/lzma2_decoder.rs

Repository: youknowone/xz-rs

Length of output: 1550


🏁 Script executed:

# Check where lzma.reset and lzma.set_uncompressed are initialized
rg -n 'reset\s*=' xz/src/lzma/lzma2_decoder.rs | head -20

Repository: youknowone/xz-rs

Length of output: 415


🏁 Script executed:

# Check the function signature and return type
rg -n 'fn lzma2_decoder' xz/src/lzma/lzma2_decoder.rs

Repository: youknowone/xz-rs

Length of output: 185


🏁 Script executed:

# Search for LZMA_PROG_ERROR usage in the file to verify it's a valid return
rg -n 'LZMA_PROG_ERROR' xz/src/lzma/lzma2_decoder.rs

Repository: youknowone/xz-rs

Length of output: 104


🏁 Script executed:

# Check initialization of these callbacks in lzma_lzma_decoder_create
fd -e 'rs' -e 'c' xz/src/lzma | xargs rg -l 'lzma_lzma_decoder_create'

Repository: youknowone/xz-rs

Length of output: 483


🏁 Script executed:

# Check lzma_decoder.rs for the decoder creation function
rg -n 'lzma_lzma_decoder_create' xz/src/lzma/lzma_decoder.rs -A 30

Repository: youknowone/xz-rs

Length of output: 2431


🏁 Script executed:

# Search for where reset and set_uncompressed are assigned in lzma_decoder
rg -n '\.reset\s*=' xz/src/lzma/lzma_decoder.rs

Repository: youknowone/xz-rs

Length of output: 159


🏁 Script executed:

# Check the structure definition of lzma_decoder_s to understand the callbacks
rg -n 'reset\s*:' xz/src/lzma/ -A 1 -B 1

Repository: youknowone/xz-rs

Length of output: 1034


🏁 Script executed:

# Check the full lzma2_decoder_init function
rg -n 'lzma2_decoder_init' xz/src/lzma/lzma2_decoder.rs -A 15

Repository: youknowone/xz-rs

Length of output: 1685


🏁 Script executed:

# Check if lzma_lzma_decoder_create is called in lzma2_decoder
rg -n 'lzma_lzma_decoder_create' xz/src/lzma/lzma2_decoder.rs -B 2 -A 2

Repository: youknowone/xz-rs

Length of output: 473


🏁 Script executed:

# Get more context around the lzma_lzma_decoder_create call at line 224
sed -n '220,235p' xz/src/lzma/lzma2_decoder.rs

Repository: youknowone/xz-rs

Length of output: 562


🏁 Script executed:

# Check the entire sequence of initialization and unwrap_unchecked calls
sed -n '201,230p' xz/src/lzma/lzma2_decoder.rs

Repository: youknowone/xz-rs

Length of output: 1192


Replace debug_assert + unwrap_unchecked with explicit error handling for optional callbacks.

debug_assert! is stripped in release builds, so unwrap_unchecked() becomes undefined behavior if callbacks are ever None. While lzma_lzma_decoder_create sets these callbacks to Some, defensive programming in unsafe code requires explicit error handling rather than assertions. Use let Some(...) = ... else { return LZMA_PROG_ERROR; } instead.

This applies to lines 92–94 (reset), 126–128 (set_uncompressed), and 153–155 (reset).

🛡️ Proposed fix
-                            debug_assert!((*coder).lzma.reset.is_some());
-                            let reset = (*coder).lzma.reset.unwrap_unchecked();
+                            let Some(reset) = (*coder).lzma.reset else {
+                                return LZMA_PROG_ERROR;
+                            };
                             reset(
                                 (*coder).lzma.coder,
                                 ::core::ptr::addr_of_mut!((*coder).options) as *const c_void,
                             );
@@
-                debug_assert!((*coder).lzma.set_uncompressed.is_some());
-                let set_uncompressed = (*coder).lzma.set_uncompressed.unwrap_unchecked();
+                let Some(set_uncompressed) = (*coder).lzma.set_uncompressed else {
+                    return LZMA_PROG_ERROR;
+                };
                 set_uncompressed(
                     (*coder).lzma.coder,
                     (*coder).uncompressed_size as lzma_vli,
@@
-                debug_assert!((*coder).lzma.reset.is_some());
-                let reset = (*coder).lzma.reset.unwrap_unchecked();
+                let Some(reset) = (*coder).lzma.reset else {
+                    return LZMA_PROG_ERROR;
+                };
                 reset(
                     (*coder).lzma.coder,
                     ::core::ptr::addr_of_mut!((*coder).options) as *const c_void,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
debug_assert!((*coder).lzma.reset.is_some());
let reset = (*coder).lzma.reset.unwrap_unchecked();
reset(
let Some(reset) = (*coder).lzma.reset else {
return LZMA_PROG_ERROR;
};
reset(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xz/src/lzma/lzma2_decoder.rs` around lines 92 - 94, Replace the unsafe
debug_assert!/unwrap_unchecked() pattern for optional callbacks with explicit
pattern matching that returns LZMA_PROG_ERROR when the callback is missing: for
the reset call use `let Some(reset) = (*coder).lzma.reset else { return
LZMA_PROG_ERROR; }; reset(...)`, for the set_uncompressed call use `let
Some(set_uncompressed) = (*coder).lzma.set_uncompressed else { return
LZMA_PROG_ERROR; }; set_uncompressed(...)`, and similarly for the other reset
occurrence; this ensures no UB in release builds and fails gracefully when the
callback is None.

@youknowone youknowone merged commit 877fbe1 into main Apr 20, 2026
47 checks passed
This was referenced Apr 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant