Skip to content

Lib/read options key cache#3189

Merged
ChanTsune merged 6 commits into
mainfrom
lib/read-options-key-cache
Jul 15, 2026
Merged

Lib/read options key cache#3189
ChanTsune merged 6 commits into
mainfrom
lib/read-options-key-cache

Conversation

@ChanTsune

@ChanTsune ChanTsune commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • Improvements
    • Improved reading of password-protected archives across listing, extraction, verification, comparison, sorting, updating, and ACL/metadata operations.
    • Added key reuse during encrypted entry reads, reducing repeated key derivation and improving performance when processing multiple entries or solid archive blocks.
    • Standardized archive reading options for more consistent encrypted and unencrypted archive handling.
  • Tests
    • Expanded coverage for encrypted archive reading and key-cache behavior.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The archive library and CLI now pass shared ReadOptions through entry iteration, solid extraction, transformation, verification, and metadata operations. ReadOptions also provides bounded derived-key caching, with tests covering cache reuse, eviction, cloning, and updated reading APIs.

Changes

ReadOptions and key-cache migration

Layer / File(s) Summary
Key caching and decryption
lib/src/entry/options.rs, lib/src/entry/read.rs, lib/src/entry.rs
ReadOptions now owns a bounded shared key cache, and entry decryption resolves derived keys through that cache. Solid-entry APIs use read options instead of raw passwords.
Archive reading APIs
lib/src/archive/read.rs, lib/src/archive/read/slice.rs
Archive and slice readers now expose options-based solid-entry iteration and store cloned ReadOptions during expansion.
CLI archive processing pipeline
cli/src/command/core.rs, cli/src/command/core/archive_source.rs, cli/src/command/update.rs
Archive runners accept shared ReadOptions; transformation strategies receive TransformContext containing password and read options.
CLI command consumers
cli/src/command/{acl,diff,extract,list,sort,verify,xattr}.rs
CLI commands construct shared read options and pass them through archive traversal, entry readers, extraction, listing, comparison, sorting, verification, and metadata paths.
Tests and integration updates
lib/src/archive.rs, lib/src/archive/write.rs, lib/tests/*, cli/tests/*, fuzz/*, lib/examples/*
Tests, compatibility coverage, examples, and fuzzing use explicit ReadOptions; encryption tests validate derived-key cache behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main library change: adding a key cache to read options.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lib/read-options-key-cache

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a KeyCache mechanism to ReadOptions, allowing derived encryption keys to be cached and reused across entries, which significantly optimizes key derivation for encrypted archives. The changes propagate this cache through the CLI and library components. The review feedback points out a breaking change regarding the removal of equality and ordering traits from ReadOptions and suggests optimizing the cache implementation using a Vec with a FIFO eviction policy instead of a HashMap to improve performance and cache management.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread lib/src/entry/options.rs
Comment thread lib/src/entry/options.rs
Comment thread lib/src/entry/options.rs
@github-actions github-actions Bot added cli This issue is about cli application lib This issue is about lib crate break API braking change labels Jul 10, 2026
Replace password-only solid entry APIs with ReadOptions-based variants so normal and solid reads share the same configuration and derived-key cache. Update CLI callers, examples, fuzz coverage, and tests for the unified API.
@ChanTsune
ChanTsune force-pushed the lib/read-options-key-cache branch from c01f56f to 6af0638 Compare July 11, 2026 01:04
@ChanTsune
ChanTsune marked this pull request as ready for review July 15, 2026 02:57
@ChanTsune
ChanTsune merged commit 3439572 into main Jul 15, 2026
109 of 111 checks passed
@ChanTsune
ChanTsune deleted the lib/read-options-key-cache branch July 15, 2026 03:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
cli/src/command/extract.rs (1)

1295-1308: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the explicit 'a lifetime from the read_options parameter to resolve a compilation error.

The parameter read_options: &'a ReadOptions ties the lifetime of the options reference to the 'a lifetime of OutputOption<'a>. Because read_options is instantiated as a local stack variable in the calling function (run_extract_archive_reader and run_extract_archive), it cannot satisfy the outer 'a lifetime boundary, resulting in a "borrowed value does not live long enough" compiler error.

  • cli/src/command/extract.rs#L1295-L1308: change read_options: &'a ReadOptions to an elided read_options: &ReadOptions.
  • cli/src/command/extract.rs#L1363-L1379: change read_options: &'a ReadOptions to an elided read_options: &ReadOptions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/command/extract.rs` around lines 1295 - 1308, Remove the explicit 'a
lifetime from the read_options parameter in extract_file_entry and the
corresponding function at cli/src/command/extract.rs lines 1295-1308 and
1363-1379, changing both to an elided reference lifetime (&ReadOptions) so local
ReadOptions values can be passed without requiring the OutputOption lifetime.
🧹 Nitpick comments (4)
lib/examples/async_io.rs (1)

41-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share ReadOptions to leverage the derived-key cache.

Creating a new ReadOptions instance for every entry defeats its internal caching mechanism, which is designed to prevent re-deriving keys for every encrypted entry. Since this is an example, it's best to demonstrate the recommended pattern by instantiating ReadOptions once and sharing it across the extraction loop.

♻️ Proposed refactor
 async fn extract(path: String) -> io::Result<()> {
     let file = tokio::fs::File::open(path).await?.compat();
     let mut archive = Archive::read_header_async(file).await?;
+    let read_options = ReadOptions::builder().build();
     while let Some(entry) = archive.read_entry_async().await? {
         match entry {
             ReadEntry::Solid(solid_entry) => {
-                for entry in solid_entry.entries(ReadOptions::builder().build())? {
+                for entry in solid_entry.entries(&read_options)? {
                     let entry = entry?;
                     let mut file = io::Cursor::new(Vec::new());
-                    let mut reader = entry.reader(ReadOptions::builder().build())?.compat();
+                    let mut reader = entry.reader(&read_options)?.compat();
                     tokio::io::copy(&mut reader, &mut file).await?;
                 }
             }
             ReadEntry::Normal(entry) => {
                 let mut file = io::Cursor::new(Vec::new());
-                let mut reader = entry.reader(ReadOptions::builder().build())?.compat();
+                let mut reader = entry.reader(&read_options)?.compat();
                 tokio::io::copy(&mut reader, &mut file).await?;
             }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/examples/async_io.rs` at line 41, Instantiate a single ReadOptions value
before the extraction loop and pass a shared reference to it when calling
solid_entry.entries. Update the loop around solid_entry.entries so all entries
reuse the same ReadOptions instance and its derived-key cache.
cli/src/command/verify.rs (2)

89-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the redundant password variable.

Since ReadOptions implements the ReadOption trait, you can query the password directly via read_options.password(). You can completely eliminate the password variable and argument from verify_solid and verify_entry to simplify the code.

♻️ Proposed refactor
                         ReadEntry::Solid(solid) => {
                             solid_blocks += 1;
-                            if solid.encryption() != Encryption::No && password.is_none() {
+                            if solid.encryption() != Encryption::No && read_options.password().is_none() {
                                 println!("<solid block #{solid_blocks}>: skipped (encrypted)");
                                 report.skipped += 1;
                                 return Ok(());
                             }
                             if let Err(err) =
-                                verify_solid(&solid, password, &read_options, fast, &mut report)
+                                verify_solid(&solid, &read_options, fast, &mut report)
                             {
                                 println!("<solid block #{solid_blocks}>: FAILED ({err})");
                                 report.failed += 1;
                                 report.encrypted_failure |= solid.encryption() != Encryption::No;
                             }
                         }
                         ReadEntry::Normal(entry) => {
-                            verify_entry(&entry, password, &read_options, fast, &mut report)
+                            verify_entry(&entry, &read_options, fast, &mut report)
                         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/command/verify.rs` around lines 89 - 98, Remove the redundant
password variable from the verification flow and stop passing it to verify_solid
and verify_entry. Within those functions, retrieve the password through the
existing read_options.password() method, preserving the current verification
behavior.

126-139: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the redundant password parameter from function signatures.

As noted above, password is already held within read_options. Removing it simplifies the interface.

♻️ Proposed refactor
 fn verify_solid(
     solid: &SolidEntry,
-    password: Option<&[u8]>,
     read_options: &ReadOptions,
     fast: bool,
     report: &mut VerifyReport,
 ) -> io::Result<()> {
     for entry in solid.entries(read_options)? {
-        verify_entry(&entry?, password, read_options, fast, report);
+        verify_entry(&entry?, read_options, fast, report);
     }
     Ok(())
 }
 
 fn verify_entry(
     entry: &NormalEntry,
-    password: Option<&[u8]>,
     read_options: &ReadOptions,
     fast: bool,
     report: &mut VerifyReport,
 ) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/command/verify.rs` around lines 126 - 139, Remove the redundant
password parameter from the verification function signatures and update all
corresponding call sites, including the call from the loop over solid.entries in
the surrounding verification function. Use read_options as the sole source of
the password while preserving the existing verify_entry behavior.
lib/src/entry/read.rs (1)

22-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer and_then to avoid edition-specific syntax.

The if let ... && let ... syntax (let_chains) relies on the 2024 edition. You can achieve the exact same behavior in a fully backward-compatible and idiomatic way using and_then.

♻️ Proposed refactor
 fn resolve_key(phsf: &str, password: &[u8], key_cache: Option<&KeyCache>) -> io::Result<Output> {
-    if let Some(cache) = key_cache
-        && let Some(key) = cache.get(phsf)
-    {
+    if let Some(key) = key_cache.and_then(|c| c.get(phsf)) {
         return Ok(key);
     }
     let password_hash = derive_password_hash(phsf, password)?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/entry/read.rs` around lines 22 - 36, Update resolve_key to replace
the edition-specific chained if-let condition with an and_then-based lookup that
preserves the existing cache-hit early return and fallback password-hash
derivation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/src/entry.rs`:
- Around line 478-488: Update the return type of entries to remove O from the
use captures bound, changing use<'a, T, O> to capture only the lifetimes and
types required by the returned iterator. Keep options consumed synchronously for
decrypt_reader initialization without tying the iterator’s lifetime to the
options type.

In `@lib/tests/extract_solid_compatibility.rs`:
- Around line 53-54: Update the assert_entry helper to accept &ReadOptions
instead of a raw password, clone those options when constructing the entry
reader, and change every assert_entry call in extract_all—including the
extraction loop—to pass the shared &read_options so key-cache reuse is
exercised.

---

Outside diff comments:
In `@cli/src/command/extract.rs`:
- Around line 1295-1308: Remove the explicit 'a lifetime from the read_options
parameter in extract_file_entry and the corresponding function at
cli/src/command/extract.rs lines 1295-1308 and 1363-1379, changing both to an
elided reference lifetime (&ReadOptions) so local ReadOptions values can be
passed without requiring the OutputOption lifetime.

---

Nitpick comments:
In `@cli/src/command/verify.rs`:
- Around line 89-98: Remove the redundant password variable from the
verification flow and stop passing it to verify_solid and verify_entry. Within
those functions, retrieve the password through the existing
read_options.password() method, preserving the current verification behavior.
- Around line 126-139: Remove the redundant password parameter from the
verification function signatures and update all corresponding call sites,
including the call from the loop over solid.entries in the surrounding
verification function. Use read_options as the sole source of the password while
preserving the existing verify_entry behavior.

In `@lib/examples/async_io.rs`:
- Line 41: Instantiate a single ReadOptions value before the extraction loop and
pass a shared reference to it when calling solid_entry.entries. Update the loop
around solid_entry.entries so all entries reuse the same ReadOptions instance
and its derived-key cache.

In `@lib/src/entry/read.rs`:
- Around line 22-36: Update resolve_key to replace the edition-specific chained
if-let condition with an and_then-based lookup that preserves the existing
cache-hit early return and fallback password-hash derivation behavior.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4ddeb9ee-e11f-424a-89fa-7037280ca6fc

📥 Commits

Reviewing files that changed from the base of the PR and between 97f1780 and 52ac06b.

📒 Files selected for processing (24)
  • cli/src/command/acl.rs
  • cli/src/command/core.rs
  • cli/src/command/core/archive_source.rs
  • cli/src/command/diff.rs
  • cli/src/command/extract.rs
  • cli/src/command/list.rs
  • cli/src/command/sort.rs
  • cli/src/command/update.rs
  • cli/src/command/verify.rs
  • cli/src/command/xattr.rs
  • cli/tests/cli/stdio/option_ignore_zeros.rs
  • cli/tests/cli/stdio/option_mac_metadata.rs
  • cli/tests/cli/utils/archive.rs
  • fuzz/fuzz_targets/split_archive.rs
  • lib/examples/async_io.rs
  • lib/src/archive.rs
  • lib/src/archive/read.rs
  • lib/src/archive/read/slice.rs
  • lib/src/archive/write.rs
  • lib/src/entry.rs
  • lib/src/entry/builder.rs
  • lib/src/entry/options.rs
  • lib/src/entry/read.rs
  • lib/tests/extract_solid_compatibility.rs

Comment thread lib/src/entry.rs
Comment on lines +478 to +488
pub fn entries<'a, O: ReadOption>(
&'a self,
options: O,
) -> io::Result<impl Iterator<Item = io::Result<NormalEntry>> + use<'a, T, O>> {
let reader = decrypt_reader(
self.encoded_reader(),
self.header.encryption,
self.header.cipher_mode,
self.phsf.as_deref(),
password,
options.password(),
options.key_cache(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove O from the use<...> captures bound.

The options argument is consumed synchronously to initialize the DecryptReader, and the returned iterator does not capture or store it. Including O in the use<'a, T, O> bound incorrectly ties the iterator's lifetime to O's lifetime. If a caller passes a reference (e.g., &ReadOptions), this artificial capture bound will prevent them from mutating or moving the options while the iterator lives, leading to unnecessary borrow-checker friction.

♻️ Proposed fix
     pub fn entries<'a, O: ReadOption>(
         &'a self,
         options: O,
-    ) -> io::Result<impl Iterator<Item = io::Result<NormalEntry>> + use<'a, T, O>> {
+    ) -> io::Result<impl Iterator<Item = io::Result<NormalEntry>> + use<'a, T>> {
         let reader = decrypt_reader(
             self.encoded_reader(),
             self.header.encryption,
📝 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
pub fn entries<'a, O: ReadOption>(
&'a self,
options: O,
) -> io::Result<impl Iterator<Item = io::Result<NormalEntry>> + use<'a, T, O>> {
let reader = decrypt_reader(
self.encoded_reader(),
self.header.encryption,
self.header.cipher_mode,
self.phsf.as_deref(),
password,
options.password(),
options.key_cache(),
pub fn entries<'a, O: ReadOption>(
&'a self,
options: O,
) -> io::Result<impl Iterator<Item = io::Result<NormalEntry>> + use<'a, T>> {
let reader = decrypt_reader(
self.encoded_reader(),
self.header.encryption,
self.header.cipher_mode,
self.phsf.as_deref(),
options.password(),
options.key_cache(),
🧰 Tools
🪛 GitHub Actions: semver-checks / 0_Semver checks.txt

[error] 478-478: cargo semver checks failed: method_requires_different_generic_type_params. Failed: libpna::SolidEntry::entries now takes 1 generic type parameter instead of 0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/entry.rs` around lines 478 - 488, Update the return type of entries
to remove O from the use captures bound, changing use<'a, T, O> to capture only
the lifetimes and types required by the returned iterator. Keep options consumed
synchronously for decrypt_reader initialization without tying the iterator’s
lifetime to the options type.

Comment on lines +53 to +54
let read_options = ReadOptions::with_password(password);
for entry in archive_reader.entries_with_options(&read_options) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Share ReadOptions to exercise the key cache.

Currently, assert_entry is called with the raw password, causing it to internally instantiate a new ReadOptions (and therefore an empty key cache) for every single entry. This means the Key Derivation Function (KDF) is executed repeatedly, and the cache reuse logic—a primary objective of this PR—is never actually tested.

Update the unchanged assert_entry helper to accept options: &ReadOptions, use options.clone() when creating the reader, and pass the shared &read_options to it from this extraction loop.

🛠️ Proposed changes

For the unchanged assert_entry helper at the top of the file:

-fn assert_entry(item: NormalEntry, password: Option<&[u8]>) {
+fn assert_entry(item: NormalEntry, options: &ReadOptions) {
     let path = item.header().path().as_str();
     let mut dist = Vec::new();
-    let mut reader = item.reader(ReadOptions::with_password(password)).unwrap();
+    let mut reader = item.reader(options.clone()).unwrap();

And update the assert_entry call sites throughout extract_all:

     let read_options = ReadOptions::with_password(password);
     for entry in archive_reader.entries_with_options(&read_options) {
         let item = entry.unwrap();
         if item.header().data_kind() == DataKind::Directory {
             continue;
         }
         n += 1;
-        assert_entry(item, password);
+        assert_entry(item, &read_options);
     }
     
     // ...
     
     for entry in archive_reader.entries() {
         let item = entry.unwrap();
         match item {
             ReadEntry::Solid(item) => {
                 for item in item.entries(&read_options).unwrap() {
                     let item = item.unwrap();
                     if item.header().data_kind() == DataKind::Directory {
                         continue;
                     }
                     n += 1;
-                    assert_entry(item, password);
+                    assert_entry(item, &read_options);
                 }
             }
             ReadEntry::Normal(item) => {
                 if item.header().data_kind() == DataKind::Directory {
                     continue;
                 }
                 n += 1;
-                assert_entry(item, password);
+                assert_entry(item, &read_options);
             }
         }
     }
📝 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 read_options = ReadOptions::with_password(password);
for entry in archive_reader.entries_with_options(&read_options) {
fn assert_entry(item: NormalEntry, options: &ReadOptions) {
let path = item.header().path().as_str();
let mut dist = Vec::new();
let mut reader = item.reader(options.clone()).unwrap();
assert_entry(item, &read_options);
assert_entry(item, &read_options);
assert_entry(item, &read_options);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/tests/extract_solid_compatibility.rs` around lines 53 - 54, Update the
assert_entry helper to accept &ReadOptions instead of a raw password, clone
those options when constructing the entry reader, and change every assert_entry
call in extract_all—including the extraction loop—to pass the shared
&read_options so key-cache reuse is exercised.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

break API braking change cli This issue is about cli application lib This issue is about lib crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant