Skip to content

perf: implement streaming decompression for history sync processing - #672

Merged
jlucaso1 merged 3 commits into
mainfrom
optimize-history-sync
Jun 1, 2026
Merged

jlucaso1 merged 3 commits into
mainfrom
optimize-history-sync

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

What

Cuts the memory cost of history-sync decode along two axes: peak (a new streaming decompress-and-parse path) and total allocations (the per-message and per-secret-record work). All in wacore::history_sync plus the pooled decompressor in wacore_binary::zlib_pool.

  1. Streaming decompression + incremental parse (peak). A new InflateReader decompresses zlib into a bounded 64 KB window and keeps an accumulation buffer that drops its consumed prefix, so a caller can read length-delimited records as they appear. process_history_sync_streaming walks the top-level HistorySync fields (conversations, pushnames, nctSalt) as their bytes arrive, extracts from each conversation, and discards it before pulling the next. process_history_sync now branches: when the caller does not need the full decompressed blob (retain_blob == false), it streams; when a consumer wants it (Event::HistorySync), the existing whole-blob path runs unchanged. Peak shifts from the entire decompressed blob to roughly the largest single conversation.

  2. Per-message partial decode (allocations). extract_conversation_fields now walks the conversation protobuf directly and prost-decodes each HistorySyncMsg one at a time, extracting its secret record and dropping it, instead of decoding the conversation into a Vec<HistorySyncMsgInternalFields> that heap-allocated one element per message. The forwarded / poll / bot detection stays in prost via the typed MessageInternalFields, so the security-relevant logic is untouched; only the outer wrapper is read by hand.

  3. Message-secret accumulator pre-sizing (allocations). The records vector is sized once from a single allocation-free count pass (count_history_sync_messages) and records are pushed straight into it, with no per-conversation temp Vec and no extend copy. This removes the one-element-at-a-time growth (grow_one) that dominated the decode's allocations.

  4. zlib output buffer sizing. The pooled decompressor's output buffer is now pre-sized from the compressed length (2x, bounded by the hard cap) instead of a fixed 64 KB clamp, which had forced roughly log2(size / 64 KB) doubling reallocations for every multi-MB chunk.

Why

The history-sync core only extracts message secrets, tctokens, pushname and nctSalt during the protobuf walk; it never needs the fully materialized proto, nor every message in memory at once. Holding the whole decompressed blob, plus one heap box per message, plus a doubling accumulator, was pure cost with no decode benefit. Only an external Event::HistorySync consumer actually needs the blob, and on that path nothing changes.

Results

Measured with dhat on synthetic history-sync blobs (varied per-message content, so they compress at a realistic ratio rather than collapsing to near-zero).

  • Total allocated (decode path, 500 conversations x 30 messages, secret on every message): 147 MB to 84 MB (about -43%), and fewer allocations than the baseline (393k to 375k) despite the same decode work. The grow_one churn from the message vector and the secret-record accumulator goes to zero; the 60k per-message boxes go to zero.
  • Peak (streaming, 2000 conversations x 30 messages, ~200 B body each, secret on every 10th): at the global heap maximum, 31.1 MB to 1.9 MB (about -94%, ~16x); total allocated 48.5 MB to 4.2 MB. This is the no-consumer case, where the blob never has to be materialized.

Compatibility

  • No breaking changes. InflateReader is new and additive. process_history_sync's signature is unchanged; the streaming path is selected by the existing retain_blob argument and returns the same secrets / tctokens / pushname / nctSalt as the full path (covered by a parity test).
  • Streaming runs only when there is no Event::HistorySync consumer (the caller passes retain_blob = has_handlers()). When a consumer is present, the blob is still produced via the full path, so peak there is unchanged. Broadening the gate (per-event-type interest, so a client with only unrelated handlers can still stream) is a possible follow-up.

Tests

New coverage:

  • Full-vs-streaming parity over a multi-conversation blob that includes a >64 KB conversation spanning multiple decompress windows, a group (no tctoken expected), pushname and nctSalt. Both paths return byte-identical secrets, tctokens, pushname and nctSalt; only the full path retains the blob.
  • InflateReader units: round-trip read back in tiny odd-sized steps across chunk boundaries, an ensure() larger than the 64 KB window, the decompressed-size cap, and one-shot vs streaming equivalence.
  • The existing extraction tests (top-level and context secrets, forwarded / nested-forwarded skipped) still pass through the new per-message path.

Validation:

  • cargo fmt --all
  • cargo clippy -p wacore -p wacore-binary --lib -- -D warnings clean
  • cargo test -p wacore -p wacore-binary (history-sync + zlib suites green)

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 42d3fb11-d0c7-47f1-a610-ff0e1ca4fe6d

📥 Commits

Reviewing files that changed from the base of the PR and between a05d721 and 69c0b1b.

📒 Files selected for processing (2)
  • wacore/binary/src/zlib_pool.rs
  • wacore/src/history_sync.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Streaming decompression for incremental reads with enforced maximum decompressed size.
    • Streaming history extraction path that can avoid retaining entire decompressed blobs.
  • Improvements

    • Lowered initial decompression preallocation to reduce memory use with clamped limits.
    • Conversation parsing now streams messages to handle very large records without full buffering.
  • Tests

    • Added tests verifying streaming vs eager extraction parity, large-record handling, and max-size enforcement.

Walkthrough

Adds InflateReader for incremental zlib decompression and a streaming extraction path in process_history_sync (used when retain_blob == false) that parses top-level protobuf fields and messages on-the-fly, replacing eager full decompression + prost conversation decoding for the streaming case.

Changes

Streaming Zlib Extraction for History Sync

Layer / File(s) Summary
InflateReader streaming decompression
wacore/binary/src/zlib_pool.rs
Implements InflateReader<'a> with rolling 64KB windows, ensure()/available()/consume() API, EOF/no-progress detection, and max-size enforcement. Updates decompress_zlib_pooled preallocation heuristic to use compressed.len() * 2 clamped appropriately. Includes unit tests for streaming behavior and pooled-vs-streaming equivalence.
process_history_sync routing and small eager changes
wacore/src/history_sync.rs
Imports InflateReader, routes process_history_sync to process_history_sync_streaming when retain_blob == false; keeps eager path when retain_blob == true and pre-sizes msg_secret_records. Adjusts per-conversation extraction calls to the new helper.
Streaming extraction implementation
wacore/src/history_sync.rs
Adds process_history_sync_streaming that incrementally reads the zlib-compressed protobuf stream using InflateReader, parses top-level length-delimited fields (conversations, pushname, nctSalt) and yields extraction with decompressed_bytes == None.
Best-effort capacity counting helpers
wacore/src/history_sync.rs
Adds scanners that count conversations/messages in decompressed bytes to pre-allocate msg_secret_records for the eager path without full prost decoding; tolerant of malformed tails.
Manual protobuf conversation field extraction
wacore/src/history_sync.rs
Removes ConversationInternalFields; adds extract_conversation_fields to walk conversation protobuf fields, decode HistorySyncMsg one-at-a-time via HistorySyncMsgInternalFields, extract per-message secret records inline, optionally form TcTokenCandidate, and stop best-effort on malformed tails. Adds PartialEq derives for testing.
Integration tests
wacore/binary/src/zlib_pool.rs, wacore/src/history_sync.rs
Adds unit tests for InflateReader (streaming roundtrip, compaction, >64KB window buffering, max enforcement) and an integration test asserting streaming vs full extraction equivalence across DM/group/pushname/nctSalt scenarios.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant process_history_sync
  participant process_history_sync_streaming
  participant InflateReader
  participant ProtobufParser

  Caller->>process_history_sync: call(compressed_blob, retain_blob=false)
  process_history_sync->>process_history_sync_streaming: delegate
  process_history_sync_streaming->>InflateReader: new(compressed, max)
  loop while not done
    process_history_sync_streaming->>InflateReader: ensure(need)
    InflateReader->>InflateReader: pump() decompress into buffer
    process_history_sync_streaming->>ProtobufParser: parse available bytes
    ProtobufParser-->>process_history_sync_streaming: (parsed_fields, parsed_len)
    process_history_sync_streaming->>InflateReader: consume(parsed_len)
  end
  process_history_sync_streaming-->>process_history_sync: HistorySyncExtraction (decompressed_bytes=None)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#362: Overlaps refactoring of history_sync decompression/parsing pipeline and prior changes to lazy/streaming-style extraction.
  • oxidezap/whatsapp-rust#533: Related prior work on retain_blob / tc_token_candidates / decompression retention that this change builds on.

Alright — this must work reliably. Pay close attention to InflateReader's pump no-progress detection, max-size enforcement, and the manual field walker’s boundary handling.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing streaming decompression for history sync processing, which is the primary focus of the changeset.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering streaming decompression, per-message partial decode, pre-sizing optimizations, and zlib buffer sizing with detailed rationale and results.
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.

✏️ 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 optimize-history-sync

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@wacore/src/history_sync.rs`:
- Around line 222-229: The match arm that tries to set result.own_pushname
contains a redundant own_user.is_some() check; remove that redundant boolean
guard and rely solely on the let-chain pattern (let Some(own) = own_user && let
Some(name) = extract_own_pushname(value, own)) so the binding happens once and
then assign result.own_pushname = Some(name) as before; update the arm that
currently checks "7 => { ... }" to use only the let-chain with
extract_own_pushname and drop own_user.is_some().
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9ca4508e-e883-4cb9-a40c-3fdd55005587

📥 Commits

Reviewing files that changed from the base of the PR and between 3019778 and 133cf0d.

📒 Files selected for processing (2)
  • wacore/binary/src/zlib_pool.rs
  • wacore/src/history_sync.rs

Comment thread wacore/src/history_sync.rs
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

Benchmark Results

67 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 3,933 3,933 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 12,038 12,038 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 43,514 43,514 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 19,365 19,365 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 68,579 68,579 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 76,679 76,679 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 2,230 2,230 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 5,988 5,988 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 183,237 182,776 +0.3%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 2,205,397 2,205,397 +0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 888,655 888,024 +0.1%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 1,027,971 1,028,165 -0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 1,760,294 1,760,319 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 1,131,728 1,131,734 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 2,128,968 2,137,138 -0.4%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 7,310,463 7,297,659 +0.2%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 12,685,043 12,685,296 -0.0%
binary_benchmark::marshal_group::bench_marshal_allocating 71,296 71,326 -0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 71,349 71,379 -0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 98,416 98,446 -0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 78,796 78,826 -0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 71,396 71,426 -0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 7,599 7,593 +0.1%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 7,642 7,636 +0.1%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 9,354 9,348 +0.1%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 530,581 530,583 -0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 530,149 530,151 -0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 531,504 531,506 -0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 8,506,104 8,506,239 -0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 8,450,452 8,450,491 -0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 19,677,941 19,678,026 -0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 2,468 2,468 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 33,558 33,558 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 787 787 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 526,830 526,732 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 4,990 4,986 +0.1%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 5,315 5,315 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 61,874 61,874 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 5,347 5,347 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 61,942 61,942 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 6,734 6,734 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 85,585 85,585 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 477,570 477,570 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 11,563 11,563 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u32 396 396 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u32 120 120 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u64 439 439 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u64 153 153 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_i64 499 499 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_i64 162 162 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_loop_100_u64 44,624 44,624 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_loop_100_u64 16,424 16,424 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 17,253,412 17,449,638 -1.1%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 157,179 157,179 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 5,513,975 5,513,975 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 157,539 157,539 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 296,699 296,767 -0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 706,282 706,282 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 12,434,844 12,506,476 -0.6%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 27,383,843 27,485,497 -0.4%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 3,467,011 3,467,011 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 124,730,243 125,717,653 -0.8%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,830,452 2,830,452 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 46,566 46,566 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 5,197,012 5,197,012 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 360,648 360,648 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 14,255,917 14,255,917 +0.0%
No significant changes detected.

@jlucaso1

jlucaso1 commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Jun 1, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 3m 6s —— View job


Code Review

  • Read changed files
  • Analyze InflateReader correctness
  • Analyze manual protobuf walker
  • Analyze streaming top-level walker
  • Validate parity / counting helpers
  • Post review findings

Overall this is a well-scoped, well-tested optimization. The streaming peak/total numbers (≈94% peak, ≈43% total) are credible because the changes target exactly the dominant cost centers: the full-blob materialization, the per-HistorySyncMsg box, the grow_one churn on the secret accumulator, and the under-sized zlib output buffer. The parity test (streaming_and_full_paths_produce_identical_results) exercises the right axes: a >64 KB conversation that spans InflateReader's 64 KB chunk boundary, both DM and group (the group must be ignored for tc-token), pushname, and nctSalt. Findings below — none are blockers.


1. pump() lacks a no-progress guard — theoretical infinite loop on a degenerate stream

wacore/binary/src/zlib_pool.rs:76-115. The pump only treats "no progress" as EOF when self.in_pos >= self.input.len(). If flate2::Decompress::decompress ever returned Status::Ok / Status::BufError with produced == 0 while input remained (corrupt zlib stream that absorbs bytes without emitting output, or a pathological state), ensure() would spin forever because Self::CHUNK (64 KB) output is always available, so it's never an "output exhausted" condition that would naturally back off.

The one-shot path at lines 184–189 already enforces this:

if decompressor.total_in() == prev_in && decompressor.total_out() == prev_out {
    return Err(io::Error::new(io::ErrorKind::InvalidData, "zlib stream truncated (no progress)"));
}

Worth mirroring in InflateReader::pump: track prev_in/prev_out (which you already capture), and if both are equal post-call and not at StreamEnd, error out instead of looping. In practice flate2 with 64 KB out and any remaining input does make progress, but the streaming path is now the default for non-consumer callers, so a hardened invariant here is cheap insurance.

Fix this →

2. Inconsistent handling of stream end mid-field in process_history_sync_streaming

wacore/src/history_sync.rs:189-269. Three wire types, three different behaviors on truncated trailing input:

  • LENGTH_DELIMITED (line 201): ensure(len) false ⇒ MalformedProtobuf("length-delimited field truncated") — explicit error.
  • VARINT (line 240): no return-value check on ensure(10); whatever bytes are available are fed to read_varint, which produces "unexpected end of data in varint" if the high bit never clears. Different error message for the same root cause.
  • FIXED64 / FIXED32 (lines 247–263): ensure(...) false ⇒ silent break. A truncated tail of these is treated as a clean end-of-stream.

Pick one policy. Either error on truncation for everything (consistent with the LENGTH_DELIMITED branch and the comment "stop cleanly when the stream ends" being reserved for the initial ensure(1) only), or treat all mid-field truncation as best-effort clean stop. Today a truncated int64 tail succeeds while a truncated bytes tail errors — that's surprising for callers diffing this against the full path's behavior.

Fix this →

3. Dead-since-the-routing branch retained in the full path

wacore/src/history_sync.rs:78. Because the !retain_blob branch returns at line 60, control only reaches line 78 when retain_blob == true, so the conditional is always Some(buf.clone()). Minor cleanup: drop the ternary. Same nit at the doc comment block (// The full decompressed protobuf blob, only retained when event listeners exist) — still accurate, just worth noting the structural redundancy.

decompressed_bytes: Some(buf.clone()),

4. Redundant own_user.is_some() in the streaming pushname arm (already flagged by CodeRabbit)

wacore/src/history_sync.rs:222-230. The if let Some(own) = own_user binding in the let-chain already discriminates on presence, so the leading own_user.is_some() adds nothing. Note the full path at lines 110-112 does need an Option::is_some() in the match guard because match guards historically don't accept if let patterns (the comment at lines 105-109 explains this is held for compatibility with Rust < 1.94). But the streaming arm at line 223 is inside the arm body, not in a guard, so the let-chain is fully sufficient — drop the leading own_user.is_some() && .

5. chat_id UTF-8 fallback can attach secrets to an empty chat id

wacore/src/history_sync.rs:784. chat_id = std::str::from_utf8(&data[pos..end]).unwrap_or(""). If the id ever fails UTF-8 (it shouldn't for real input), every subsequent secret in the conversation is pushed with chat_id: "". The tc-token short-circuit at line 833 then returns None on the empty id (since parse_jid_fast("") won't match g.us/newsletter/bot), but the empty-id secrets still land in msg_secret_records. Either keep the current best-effort posture and add a brief comment acknowledging it, or skip the entire conversation when the id is non-UTF8 (else break;) to avoid orphan records.

6. Minor: count_history_sync_messages correctly handles malformed tails

wacore/src/history_sync.rs:350-400. The bail-on-first-error pattern is fine — pre-sizing is purely a hint and an undercount only costs a couple of late re-grows. The PR description's claim that this removes the grow_one accumulator churn is consistent with what I read. No action needed; flagging only because CodeRabbit's preamble asked you to verify the counters genuinely don't silently undercount — they can undercount on malformed input by design, and that's the right trade-off.


What's good

  • InflateReader API is clean: ensure / available / consume is exactly the right shape for length-delimited record parsing.
  • Compaction policy (drop the consumed prefix once cursor ≥ CHUNK or buffer fully drained) keeps peak proportional to one record, which is what the streaming claim hinges on.
  • The full-vs-streaming parity test is the right shape — it spans a multi-window conversation, mixes 1:1 and group, and exercises pushname + nctSalt. The fact that group's tc_token is set but not extracted is a load-bearing assertion and is correctly tested.
  • Per-message prost decode keeps the forwarded/poll/bot detection inside the typed path, so the security-sensitive logic is untouched. Right call.
  • decompress_zlib_pooled pre-sizing change (compressed.len() * 2) is well-justified — zlib here compresses ~2-5x so this rarely overshoots, and the clamp(4096, cap) caps the damage when it does.

The findings above are quality-of-implementation concerns, not blockers. The optimization is sound and the test coverage is appropriate for the scope.
| Branch: optimize-history-sync

…g pushname arm

The `let Some(own) = own_user` let-chain already short-circuits when own_user
is None, so the leading own_user.is_some() check was redundant.
- InflateReader::pump now errors on a stalled stream (no output produced, no
  input consumed, input remaining) instead of looping forever, mirroring the
  one-shot decompress_zlib_pooled no-progress guard.
- process_history_sync_streaming errors on a truncated FIXED32/FIXED64 tail for
  parity with the LENGTH_DELIMITED branch (was a silent clean break).
- extract_conversation_fields skips a conversation whose id is not valid UTF-8
  rather than attaching its secrets to an empty chat id.
- Drop the always-true `if retain_blob` ternary on the full path (the
  !retain_blob case returns earlier into the streaming variant).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant