Skip to content

Lib/add hashed password#2932

Draft
ChanTsune wants to merge 3 commits intomainfrom
lib/add-hashed-password
Draft

Lib/add hashed password#2932
ChanTsune wants to merge 3 commits intomainfrom
lib/add-hashed-password

Conversation

@ChanTsune
Copy link
Copy Markdown
Owner

@ChanTsune ChanTsune commented Apr 7, 2026

Summary by CodeRabbit

  • New Features

    • Support for pre-hashed passwords for encrypted archives (Argon2id, PBKDF2-SHA256); new tests demonstrating hashed-password workflows and error cases.
  • Refactor

    • Archive-reading now reuses a mutable read-options object and caches derived decryption keys to improve performance across multiple encrypted entries; debug output redacts sensitive data.
  • Documentation

    • Examples and READMEs updated to reflect the new read-options usage.
  • Tests

    • Added security and compatibility tests for hashed passwords and debug-safety.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 7, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1a28cf13-2d7c-4943-8692-12f8240cd116

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactor to thread a single mutable ReadOptions through solid/normal entry iteration and readers; add KeyCache and HashedPassword support so decryption reuses derived keys and accepts pre-hashed passwords. Many call sites, tests, examples, and fuzz targets updated to borrow &mut ReadOptions.

Changes

Cohort / File(s) Summary
Core API updates
lib/src/entry.rs, lib/src/archive/read.rs, lib/src/archive/read/slice.rs
Changed entry/entries/reader APIs to accept &mut ReadOptions instead of password slices/owned options; threaded options through archive iterators.
Key cache & read-time changes
lib/src/entry/key_cache.rs, lib/src/entry/read.rs, lib/src/entry/options.rs
Added KeyCache LRU module; decrypt_reader now accepts &mut KeyCache and uses it to reuse derived keys; ReadOptions now carries a cache and debug redaction.
Hashed-password support (write path & hashing)
lib/src/entry/write.rs, lib/src/hash.rs, lib/src/entry/options.rs
Introduced HashedPassword and CipherPassword; write path accepts hashed passwords (uses stored key/phsf); added helper to generate hashed passwords and constants.
CLI & archive consumers
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
Refactored CLI flows to construct a single ReadOptions per operation and pass &mut read_options into entries(...)/reader(...) across solid/normal, fast-read, and stoppable variants.
Tests, fuzz targets & examples
cli/tests/..., lib/tests/..., fuzz/fuzz_targets/..., lib/examples/..., lib/README.md, pna/README.md
Updated tests/fuzz targets/examples/benchmarks to construct and reuse mutable ReadOptions; added tests for hashed-password and security/debug redaction.
Benchmarks & docs
lib/benches/create_extract.rs, lib/src/archive/write.rs
Benchmarks and test examples updated to pass &mut ReadOptions to readers/entries.
CI / config
.github/workflows/webassembly.yml, .cargo/config.toml
Removed certain RUSTFLAGS from workflow and added per-target rustflags for wasm32-unknown-emscripten (memory/growth/link args).

Sequence Diagram(s)

mermaid
sequenceDiagram
participant CLI as CLI (caller)
participant Archive as Archive
participant Entry as Entry
participant Decryptor as Decryptor
participant Cache as KeyCache
CLI->>Archive: create &mut ReadOptions
CLI->>Archive: Archive::entries_with_password(&mut ReadOptions)
Archive->>Entry: yield Solid/Normal entries (borrow ReadOptions)
Entry->>Decryptor: entry.reader(&mut ReadOptions)
Decryptor->>Cache: get(phsf)
alt cached
Cache-->>Decryptor: cached key (bytes)
else not cached
Decryptor->>Decryptor: derive key from password
Decryptor->>Cache: insert(phsf, key)
Cache-->>Decryptor: stored key
end
Decryptor-->>Entry: decrypted reader (stream)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

"I stash the keys inside my hat,
one hash for many — imagine that!
Pass options once, then read with speed,
cached keys hop in when they're freed.
Nibble bytes, decrypt, and clap — a rabbit's archived nap."

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title "Lib/add hashed password" directly corresponds to the main objective of adding hashed password support to the library, as evidenced by the new HashedPassword struct, hashed_password builder method, and comprehensive test coverage for encrypted archive operations with hashed passwords.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lib/add-hashed-password

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 and usage tips.

@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 Apr 7, 2026
Copy link
Copy Markdown
Contributor

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

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 to ReadOptions to optimize decryption performance by caching derived keys and adds a HashedPassword struct for efficient archive creation. The API has been updated to require a mutable reference to ReadOptions when reading entries. Review feedback identifies several locations in the CLI where ReadOptions is instantiated locally, which prevents cache reuse and leads to redundant, expensive hashing operations. Furthermore, the removal of standard trait derivations (such as Eq and Hash) from the options structs is flagged as a breaking change for library consumers that should be mitigated with manual implementations.

Comment thread cli/src/command/diff.rs
let data_kind = entry.header().data_kind();
let path = entry.header().path();
let path_str = path.as_str();
let mut read_options = ReadOptions::with_password(password);
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.

high

Creating a new ReadOptions instance inside compare_entry prevents the KeyCache from being reused across different entries in the archive. Since compare_entry is called for every entry during a diff operation, this results in redundant and expensive password hashing (e.g., Argon2) for every file if they share the same encryption parameters. This significantly impacts performance, especially for archives with many small encrypted files. Consider passing a mutable reference to a shared ReadOptions object from the caller.

return Ok(());
};

let mut read_options = ReadOptions::with_password(password);
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.

high

Instantiating ReadOptions within extract_entry defeats the purpose of the KeyCache for NormalEntry decryption across the archive. Because extract_entry is invoked per file, the cache is lost between calls, forcing a full key derivation (Argon2/PBKDF2) for every entry. This is a major performance bottleneck. For sequential extraction, a single ReadOptions should be reused. For parallel extraction (via Rayon), consider using thread-local ReadOptions or cloning a 'warmed' instance if applicable.

Comment thread cli/src/command/list.rs
if collect.link_target {
entry
.reader(ReadOptions::with_password(password))
.reader(&mut ReadOptions::with_password(password))
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.

high

ReadOptions is instantiated here (and again at line 442) for every symbolic or hard link encountered during a long listing. This triggers expensive key derivation twice per link entry. TableRow::from_entry should be refactored to accept a &mut ReadOptions from the caller (e.g., list_archive or run_list_archive), where a persistent options object already exists.

Comment thread lib/src/entry/options.rs
/// The compression algorithm and cipher mode are stored in the archive metadata, so you
/// only need to provide the password.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
#[derive(Clone, Debug)]
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.

medium

The removal of Eq, PartialEq, Ord, PartialOrd, and Hash from WriteOptions (and similarly from ReadOptions at line 867) is a breaking change for library consumers who may rely on these traits for comparing configurations or using them as keys in collections. While KeyCache makes automatic derivation difficult for ReadOptions, HashedPassword and WriteOptions should still implement these traits. For ReadOptions, consider a manual implementation of PartialEq and Hash that ignores the internal cache state.

/// Moves the hit entry to front (MRU) on success.
pub(crate) fn get(&mut self, phsf: &str) -> Option<Output> {
if let Some(pos) = self.entries.iter().position(|(k, _)| k == phsf) {
let entry = self.entries.remove(pos);
if let Some(pos) = self.entries.iter().position(|(k, _)| k == phsf) {
let entry = self.entries.remove(pos);
let output = entry.1;
self.entries.insert(0, entry);
if self.entries.len() >= Self::CAPACITY {
self.entries.pop();
}
self.entries.insert(0, (phsf, key));
Comment thread lib/src/entry/options.rs

#[test]
fn write_options_round_trip_hashed_password() {
let hashed = HashedPassword::new(b"secret", HashAlgorithm::argon2id()).unwrap();
Comment thread lib/src/entry/options.rs

#[test]
fn hashed_password_last_wins_over_raw() {
let hashed = HashedPassword::new(b"secret", HashAlgorithm::argon2id()).unwrap();

#[test]
fn round_trip_with_hashed_password_cbc() -> io::Result<()> {
let password = b"test_password";

#[test]
fn round_trip_with_hashed_password_pbkdf2() -> io::Result<()> {
let password = b"test_password";

#[test]
fn wrong_password_returns_error() -> io::Result<()> {
let hashed = HashedPassword::new(b"correct_password", HashAlgorithm::argon2id())?;
Comment thread lib/tests/security.rs

#[test]
fn hashed_password_debug_does_not_leak_key() {
let hp = HashedPassword::new(b"secret", HashAlgorithm::argon2id()).unwrap();
Comment thread lib/tests/security.rs

#[test]
fn write_options_hashed_round_trip_no_panic() {
let hashed = HashedPassword::new(b"password", HashAlgorithm::argon2id()).unwrap();
Copy link
Copy Markdown
Contributor

@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

🧹 Nitpick comments (3)
lib/benches/create_extract.rs (1)

70-78: Keep ReadOptions setup out of the benchmarked path.

item.reader(&mut ReadOptions::with_password(...)) makes this benchmark measure ReadOptions construction as well as slice decoding, unlike bench_read_archive above. Hoist a local let mut read_options = ...; once per b.iter iteration and pass that into reader(...).

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

In `@lib/benches/create_extract.rs` around lines 70 - 78, The benchmark currently
constructs ReadOptions for each entry via item.reader(&mut
ReadOptions::with_password(Some("password"))) which pollutes the timing; inside
the b.iter(|| { ... }) closure, hoist a single let mut read_options =
ReadOptions::with_password(Some("password")); immediately after creating the
reader (before the for item in reader.entries_slice() loop) and replace the
inline construction with item.reader(&mut read_options). This keeps ReadOptions
setup out of the per-entry measured path while leaving the rest of the
bench_read_archive logic unchanged.
lib/src/entry/write.rs (1)

38-62: Define precedence between hashed_password() and hash_algorithm().

In the CipherPassword::Hashed branch, cipher.hash_algorithm is ignored completely. That means a caller can configure one hash algorithm in WriteOptions while the embedded phsf came from another, and the archive will silently use the embedded hash. Either reject that mixed configuration or document that hashed_password() wins.

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

In `@lib/src/entry/write.rs` around lines 38 - 62, The CipherPassword::Hashed
branch currently ignores cipher.hash_algorithm, allowing mismatched hash
algorithms between the provided WriteOptions and the embedded phsf; update the
CipherPassword::Hashed arm in the match to enforce a clear precedence: either
(preferred) validate that the embedded phsf's algorithm equals
cipher.hash_algorithm and return an io::Error if they differ, or explicitly
document and accept that hashed_password() (the embedded phsf) wins and override
cipher.hash_algorithm accordingly; locate the check in the
CipherPassword::Hashed match arm (symbols: CipherPassword::Hashed, phsf,
cipher.hash_algorithm, hashed_password()) and implement the validation and error
return (or the override) consistently.
cli/src/command/list.rs (1)

428-448: Per-entry ReadOptions creation loses cache benefit for link targets.

Each symlink/hardlink target read creates a fresh ReadOptions, so the KeyCache won't be reused across entries. If archives contain many encrypted symlinks/hardlinks, this could cause redundant password derivation.

Consider passing an existing &mut ReadOptions into from_entry if this becomes a performance concern for encrypted archives with many links.

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

In `@cli/src/command/list.rs` around lines 428 - 448, Per-entry creation of
ReadOptions via ReadOptions::with_password inside the entry.reader calls (used
in the DataKind::SymLink and DataKind::HardLink branches) defeats KeyCache reuse
and forces repeated password derivation; instead create and reuse a single
mutable ReadOptions (e.g. let mut read_opts =
ReadOptions::with_password(password)) and pass &mut read_opts into
entry.reader/from_entry so all link target reads share the same
ReadOptions/KeyCache; update the call sites that currently call
ReadOptions::with_password(password) to use the shared &mut ReadOptions and
ensure from_entry signatures accept &mut ReadOptions if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/src/command/core.rs`:
- Around line 1191-1192: The loop currently constructs a new ReadOptions inside
the s.entries(&mut read_options)? call which causes ReadEntry::Solid branches to
lose KeyCache between solid blocks; instead thread a single mutable ReadOptions
from run_transform_entry() into TransformStrategy::transform() and stop
rebuilding it per-entry—modify run_transform_entry() to create and pass &mut
ReadOptions into calls that invoke TransformStrategy::transform(), update
TransformStrategy::transform() signature to accept &mut ReadOptions, and remove
the local ReadOptions::with_password(password) construction inside the s.entries
loop so the same ReadOptions (and its KeyCache) is reused across solid
transforms.

In `@cli/src/command/core/archive_source.rs`:
- Line 5: The unused-import warning comes from importing ReadOptions
unconditionally; make that import conditional on the memmap feature by removing
ReadOptions from the combined use and adding a separate feature-gated import
like #[cfg(feature = "memmap")] use pna::ReadOptions; so NormalEntry and
ReadEntry remain imported normally and ReadOptions is only imported when the
memmap-related code (which references ReadOptions) is compiled.

In `@lib/src/entry.rs`:
- Around line 435-446: Public read APIs were changed to accept &mut ReadOptions
which breaks source compatibility; revert the signatures of SolidEntry::entries
and NormalEntry::reader back to take &ReadOptions (immutable borrow) and instead
give ReadOptions an interior-mutable cache (e.g., RefCell/Mutex/Atomic/Cell
wrapper for the existing cache type). Update places that previously mutated
option.cache inside entries/reader to use the interior-mutable API
(borrow_mut()/lock()/get()/etc.) so the public method signatures stay &self,
&ReadOptions while still allowing cache updates. Ensure all call sites use the
immutable &ReadOptions and that the cache field name and type in ReadOptions
carry the interior-mutable wrapper so no external API changes are required.

In `@lib/src/entry/options.rs`:
- Around line 175-177: The hashed-password branch desynchronizes the builder's
hash_algorithm because hashed_password() swaps only the password variant without
updating self.hash_algorithm; fix by ensuring the HashedPassword value carries
its algorithm (or can derive it) and update the builder's hash_algorithm when
you replace the password variant. Concretely: extend HashedPassword (or the
result of crate::hash::new_hashed_password / HashedPassword::new) to include the
actual HashAlgorithm, have hashed_password() set self.hash_algorithm =
hashed.algorithm (or call a getter) when switching to the hashed branch, and
make into_builder()/password() use that algorithm so round-trips preserve the
KDF; apply the same change at the other occurrences noted (around the other
hashed_password/password conversions).

---

Nitpick comments:
In `@cli/src/command/list.rs`:
- Around line 428-448: Per-entry creation of ReadOptions via
ReadOptions::with_password inside the entry.reader calls (used in the
DataKind::SymLink and DataKind::HardLink branches) defeats KeyCache reuse and
forces repeated password derivation; instead create and reuse a single mutable
ReadOptions (e.g. let mut read_opts = ReadOptions::with_password(password)) and
pass &mut read_opts into entry.reader/from_entry so all link target reads share
the same ReadOptions/KeyCache; update the call sites that currently call
ReadOptions::with_password(password) to use the shared &mut ReadOptions and
ensure from_entry signatures accept &mut ReadOptions if needed.

In `@lib/benches/create_extract.rs`:
- Around line 70-78: The benchmark currently constructs ReadOptions for each
entry via item.reader(&mut ReadOptions::with_password(Some("password"))) which
pollutes the timing; inside the b.iter(|| { ... }) closure, hoist a single let
mut read_options = ReadOptions::with_password(Some("password")); immediately
after creating the reader (before the for item in reader.entries_slice() loop)
and replace the inline construction with item.reader(&mut read_options). This
keeps ReadOptions setup out of the per-entry measured path while leaving the
rest of the bench_read_archive logic unchanged.

In `@lib/src/entry/write.rs`:
- Around line 38-62: The CipherPassword::Hashed branch currently ignores
cipher.hash_algorithm, allowing mismatched hash algorithms between the provided
WriteOptions and the embedded phsf; update the CipherPassword::Hashed arm in the
match to enforce a clear precedence: either (preferred) validate that the
embedded phsf's algorithm equals cipher.hash_algorithm and return an io::Error
if they differ, or explicitly document and accept that hashed_password() (the
embedded phsf) wins and override cipher.hash_algorithm accordingly; locate the
check in the CipherPassword::Hashed match arm (symbols: CipherPassword::Hashed,
phsf, cipher.hash_algorithm, hashed_password()) and implement the validation and
error return (or the override) consistently.
🪄 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: 5a863a80-dcb0-4ea0-9df6-ee230cbf7941

📥 Commits

Reviewing files that changed from the base of the PR and between b1d6244 and 160aa3f.

📒 Files selected for processing (35)
  • 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/tests/cli/stdio/option_ignore_zeros.rs
  • cli/tests/cli/update/no_timestamp_archive.rs
  • cli/tests/cli/utils/archive.rs
  • fuzz/fuzz_targets/aes_cbc.rs
  • fuzz/fuzz_targets/aes_ctr.rs
  • fuzz/fuzz_targets/camellia_cbc.rs
  • fuzz/fuzz_targets/camellia_ctr.rs
  • fuzz/fuzz_targets/split_archive.rs
  • lib/README.md
  • lib/benches/create_extract.rs
  • lib/examples/async_io.rs
  • lib/examples/change_compression_method.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/key_cache.rs
  • lib/src/entry/options.rs
  • lib/src/entry/read.rs
  • lib/src/entry/write.rs
  • lib/src/hash.rs
  • lib/src/lib.rs
  • lib/tests/extract_compatibility.rs
  • lib/tests/extract_multipart_compatibility.rs
  • lib/tests/extract_solid_compatibility.rs
  • lib/tests/hashed_password.rs
  • lib/tests/security.rs
  • pna/README.md

Comment thread cli/src/command/core.rs
Comment on lines +1191 to +1192
let mut read_options = ReadOptions::with_password(password);
for n in s.entries(&mut 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.

⚠️ Potential issue | 🟠 Major

Reuse one ReadOptions across solid transforms.

These branches still create a fresh ReadOptions per ReadEntry::Solid, so unsolid/keep-solid rewrites lose the new KeyCache between solid blocks and re-run the KDF for every block. Thread a shared &mut ReadOptions from run_transform_entry() into TransformStrategy::transform() instead of rebuilding it here.

♻️ Minimal sketch
 trait TransformStrategy {
-    fn transform<W, T, F>(archive: &mut Archive<W>, password: Option<&[u8]>, read_entry: io::Result<ReadEntry<T>>, transformer: F) -> io::Result<()>
+    fn transform<W, T, F>(archive: &mut Archive<W>, read_options: &mut ReadOptions, read_entry: io::Result<ReadEntry<T>>, transformer: F) -> io::Result<()>
 }
 ...
- let password = password_provider();
+ let mut read_options = ReadOptions::with_password(password_provider());
 ...
- |entry| Transform::transform(&mut out_archive, password, entry, &mut processor),
+ |entry| Transform::transform(&mut out_archive, &mut read_options, entry, &mut processor),

Also applies to: 1236-1237

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

In `@cli/src/command/core.rs` around lines 1191 - 1192, The loop currently
constructs a new ReadOptions inside the s.entries(&mut read_options)? call which
causes ReadEntry::Solid branches to lose KeyCache between solid blocks; instead
thread a single mutable ReadOptions from run_transform_entry() into
TransformStrategy::transform() and stop rebuilding it per-entry—modify
run_transform_entry() to create and pass &mut ReadOptions into calls that invoke
TransformStrategy::transform(), update TransformStrategy::transform() signature
to accept &mut ReadOptions, and remove the local
ReadOptions::with_password(password) construction inside the s.entries loop so
the same ReadOptions (and its KeyCache) is reused across solid transforms.

Comment thread cli/src/command/core/archive_source.rs Outdated
use std::{fs, io};

use pna::{NormalEntry, ReadEntry};
use pna::{NormalEntry, ReadEntry, ReadOptions};
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.

⚠️ Potential issue | 🟡 Minor

Feature-gate the ReadOptions import to fix unused import warning.

The ReadOptions import is only used within the #[cfg(feature = "memmap")] block at lines 93-98. On WebAssembly targets (which don't use memmap), this causes an unused import warning that appears in CI.

🔧 Proposed fix
-use pna::{NormalEntry, ReadEntry, ReadOptions};
+use pna::{NormalEntry, ReadEntry};
+#[cfg(feature = "memmap")]
+use pna::ReadOptions;
🧰 Tools
🪛 GitHub Actions: Build Webassembly

[warning] 5-5: unused import: ReadOptions (#[warn(unused_imports)]).

🪛 GitHub Check: Test WebAssembly (nightly, wasm32-unknown-emscripten)

[warning] 5-5:
unused import: ReadOptions

🪛 GitHub Check: Test WebAssembly (nightly, wasm32-wasip1)

[warning] 5-5:
unused import: ReadOptions

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

In `@cli/src/command/core/archive_source.rs` at line 5, The unused-import warning
comes from importing ReadOptions unconditionally; make that import conditional
on the memmap feature by removing ReadOptions from the combined use and adding a
separate feature-gated import like #[cfg(feature = "memmap")] use
pna::ReadOptions; so NormalEntry and ReadEntry remain imported normally and
ReadOptions is only imported when the memmap-related code (which references
ReadOptions) is compiled.

Comment thread lib/src/entry.rs
Comment on lines 435 to 446
pub fn entries(
&self,
password: Option<&[u8]>,
option: &mut ReadOptions,
) -> io::Result<impl Iterator<Item = io::Result<NormalEntry>> + '_> {
let reader = decrypt_reader(
ChainReader::new(self.data.iter().map(AsRef::as_ref as fn(&T) -> &[u8])),
self.header.encryption,
self.header.cipher_mode,
self.phsf.as_deref(),
password,
option.password.as_deref(),
&mut option.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.

⚠️ Potential issue | 🟠 Major

Keep the public read APIs source-compatible.

Switching SolidEntry::entries and NormalEntry::reader to &mut ReadOptions is a breaking change for existing callers and forces exclusive borrowing through the whole read path. If this release is meant to stay semver-compatible, the cache needs interior mutability so these public signatures can remain read-only.

Also applies to: 1038-1052

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

In `@lib/src/entry.rs` around lines 435 - 446, Public read APIs were changed to
accept &mut ReadOptions which breaks source compatibility; revert the signatures
of SolidEntry::entries and NormalEntry::reader back to take &ReadOptions
(immutable borrow) and instead give ReadOptions an interior-mutable cache (e.g.,
RefCell/Mutex/Atomic/Cell wrapper for the existing cache type). Update places
that previously mutated option.cache inside entries/reader to use the
interior-mutable API (borrow_mut()/lock()/get()/etc.) so the public method
signatures stay &self, &ReadOptions while still allowing cache updates. Ensure
all call sites use the immutable &ReadOptions and that the cache field name and
type in ReadOptions carry the interior-mutable wrapper so no external API
changes are required.

Comment thread lib/src/entry/options.rs
Comment on lines +175 to 177
pub fn new(password: impl AsRef<[u8]>, hash_algorithm: HashAlgorithm) -> io::Result<Self> {
crate::hash::new_hashed_password(password.as_ref(), hash_algorithm)
}
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.

⚠️ Potential issue | 🟡 Minor

hashed_password() can desynchronize the builder’s hash algorithm.

HashedPassword::new accepts any HashAlgorithm, but hashed_password() only swaps the password variant and leaves self.hash_algorithm unchanged. With a non-default hashed password, into_builder() will therefore round-trip the wrong algorithm, and switching back to .password(...) later can silently change the archive’s KDF. The hashed branch should carry or derive the algorithm it actually represents.

Also applies to: 723-723, 803-805, 842-849

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

In `@lib/src/entry/options.rs` around lines 175 - 177, The hashed-password branch
desynchronizes the builder's hash_algorithm because hashed_password() swaps only
the password variant without updating self.hash_algorithm; fix by ensuring the
HashedPassword value carries its algorithm (or can derive it) and update the
builder's hash_algorithm when you replace the password variant. Concretely:
extend HashedPassword (or the result of crate::hash::new_hashed_password /
HashedPassword::new) to include the actual HashAlgorithm, have hashed_password()
set self.hash_algorithm = hashed.algorithm (or call a getter) when switching to
the hashed branch, and make into_builder()/password() use that algorithm so
round-trips preserve the KDF; apply the same change at the other occurrences
noted (around the other hashed_password/password conversions).

Comment thread lib/src/entry/options.rs
Comment on lines +635 to 636
#[derive(Clone, Debug)]
pub struct WriteOptions {
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.

⚠️ Potential issue | 🟠 Major

Restore the removed trait impls on the public option types.

cargo-semver-checks is already failing because downstream code can no longer compare, order, or hash WriteOptions, WriteOptionsBuilder, ReadOptions, and ReadOptionsBuilder. If these types need to remain semver-compatible, please keep those impls and treat runtime-only state like ReadOptions.cache as non-semantic for equality/hash.

Also applies to: 691-692, 867-871, 936-937

🧰 Tools
🪛 GitHub Actions: semver-checks

[error] 636-636: cargo-semver-checks failed: derive_trait_impl_removed — type WriteOptions no longer derives Eq, PartialEq, Ord, PartialOrd, and Hash.

Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (1)
.github/workflows/webassembly.yml (1)

93-93: Optional: centralize duplicated RUSTFLAGS for emscripten steps.

Line 93 and Line 100 repeat the same flag string. Consider moving it to a shared env scope (job-level or YAML anchor) to prevent future drift.

Also applies to: 100-100

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

In @.github/workflows/webassembly.yml at line 93, Extract the duplicated
RUSTFLAGS value used in the emscripten steps and centralize it (e.g., define
RUSTFLAGS at the job-level env or create a YAML anchor) so both occurrences
reference the single definition; update the steps that currently set RUSTFLAGS
to instead inherit the job env or reference the anchor to avoid duplication and
future drift (look for the RUSTFLAGS string "-C link-arg=-sINITIAL_MEMORY=512MB
-C link-arg=-sTOTAL_STACK=16MB -C link-arg=-sALLOW_MEMORY_GROWTH=1" and the
emscripten-related steps that currently set it).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.github/workflows/webassembly.yml:
- Line 93: Extract the duplicated RUSTFLAGS value used in the emscripten steps
and centralize it (e.g., define RUSTFLAGS at the job-level env or create a YAML
anchor) so both occurrences reference the single definition; update the steps
that currently set RUSTFLAGS to instead inherit the job env or reference the
anchor to avoid duplication and future drift (look for the RUSTFLAGS string "-C
link-arg=-sINITIAL_MEMORY=512MB -C link-arg=-sTOTAL_STACK=16MB -C
link-arg=-sALLOW_MEMORY_GROWTH=1" and the emscripten-related steps that
currently set it).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: db14ae4b-ccb9-4f20-a5f1-8c9630660658

📥 Commits

Reviewing files that changed from the base of the PR and between 160aa3f and a28ae05.

📒 Files selected for processing (1)
  • .github/workflows/webassembly.yml

@github-actions github-actions Bot added the CI Pull requests that update CI config label Apr 7, 2026
@ChanTsune ChanTsune force-pushed the lib/add-hashed-password branch from a28ae05 to 556c0ac Compare April 7, 2026 08:47
@ChanTsune ChanTsune marked this pull request as draft April 7, 2026 08:57
@ChanTsune ChanTsune force-pushed the lib/add-hashed-password branch 3 times, most recently from cb8595c to 160aa3f Compare April 8, 2026 00:58
@ChanTsune ChanTsune force-pushed the lib/add-hashed-password branch 2 times, most recently from 0c0f25a to e9c68a0 Compare April 27, 2026 01:47
@ChanTsune ChanTsune force-pushed the lib/add-hashed-password branch from e9c68a0 to 2d510ae Compare April 27, 2026 02:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

break API braking change CI Pull requests that update CI config 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.

2 participants