Skip to content

perf: binary - #279

Merged
jlucaso1 merged 4 commits into
mainfrom
perf-binary
Feb 11, 2026
Merged

perf: binary#279
jlucaso1 merged 4 commits into
mainfrom
perf-binary

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Feb 11, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added auto/exact marshaling APIs and new helpers for marshalling into preallocated or reused buffers.
  • Refactor

    • Encoding/decoding reworked to support multiple writer backends and precomputed size plans with string/JID hinting and caching.
  • Performance

    • Safer, faster packed-data and string decoding with reduced bounds checks and optimized nibble/hex paths.
  • Tests / Benchmarks

    • Expanded tests and benchmarks covering auto/exact paths, large payloads, long strings, buffer reuse, and roundtrips.

@coderabbitai

coderabbitai Bot commented Feb 11, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a ByteWriter abstraction and JID/string-hint-aware encoder with precomputed marshaling plans and new marshal APIs (auto/exact/vec/ref variants); expands benchmarks; and optimizes decoder packed-string decoding with unchecked/SIMD paths and tag-dispatch entry points. Public API gains new marshal re-exports.

Changes

Cohort / File(s) Summary
Decoder optimizations
wacore/binary/src/decoder.rs
Replaced indexed reads with position-tracking + unchecked access, added tag-dispatch entry points (read_content_from_tag, read_value_as_string_from_tag), SIMD-packed decoding (decode_packed_hex, decode_packed_nibble), and new unpack helpers; packed-string path now uses from_utf8_unchecked.
Encoder core & ByteWriter
wacore/binary/src/encoder.rs
Introduced ByteWriter trait and backends (IoByteWriter, VecByteWriter, SliceByteWriter), made Encoder<'a, W: ByteWriter> generic, replaced direct writes with writer methods, and added constructors for Io/Vec/Slice writers and bytes_written accessor.
Hinting, JID & size planning
wacore/binary/src/encoder.rs
Added StrKey, StringHint, StringHintCache, MarshaledSizePlan, JID parsing helpers (parse_jid_meta, split_jid_from_meta, write_jid_*), and precompute/cached size planners (*_encoded_size_with_cache, build_marshaled_*_plan).
Marshal APIs & helpers
wacore/binary/src/marshal.rs, wacore/binary/src/lib.rs
Added public marshal helpers (marshal_auto, marshal_exact, marshal_ref_auto, marshal_ref_exact, marshal_to_vec, marshal_ref_to_vec) plus capacity heuristics, two-pass exact-size marshalling using Encoder::new_slice, and exports in lib.rs.
Benchmarks
wacore/binary/benches/binary_benchmark.rs
Expanded benchmark suite to exercise new marshal variants (auto/exact/vec/slice), long strings, huge bytes, many-children nodes, and round-trip benchmarks.
Tests & helpers
wacore/binary/src/encoder.rs, wacore/binary/src/marshal.rs
Moved/added tests for validate_nibble/validate_hex, JID parsing preservation, packing paths, size-plan parity, and stress tests for auto-reserve behavior.
Manifest
Cargo.toml
Updated dependencies/manifest lines to accommodate new modules, tests, and benches.

Sequence Diagram

sequenceDiagram
    participant Caller as Caller
    participant Marshal as marshal_auto / marshal_exact
    participant Planner as build_marshaled_node_plan
    participant Cache as StringHintCache
    participant Encoder as Encoder::new_slice / new_vec
    participant Writer as SliceByteWriter / VecByteWriter / IoByteWriter

    Caller->>Marshal: request marshal(node)
    alt exact-size path
        Marshal->>Planner: build_marshaled_node_plan(node)
        Planner->>Cache: classify_string_hint / collect hints
        Cache-->>Planner: hints
        Planner-->>Marshal: MarshaledSizePlan {size, hints}
        Marshal->>Encoder: Encoder::new_slice(buf, hints)
    else auto-reserve path
        Marshal->>Marshal: estimate_capacity_node / reserve Vec
        Marshal->>Encoder: Encoder::new_vec(buf)
    end
    Encoder->>Writer: write_u8 / write_bytes (use hints / write_jid_*)
    Writer-->>Encoder: bytes_written
    Encoder-->>Marshal: encoded bytes
    Marshal-->>Caller: payload
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 I hopped through bytes and planned the land,
Hints in my pouch, a marshaled strand,
Nibbles crunch in SIMD drum, unchecked strings hum,
Writers stamp and buffers bloom till done,
A joyful hop for every run!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'perf: binary' is vague and generic, using only 'perf' as a descriptor without specifying what performance improvements were made. Consider a more descriptive title that highlights the main performance improvement, such as 'perf: optimize binary encoding with ByteWriter abstraction and marshaling strategies' or 'perf: add marshaling size planning and ByteWriter backends'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 91.54% 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 docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch perf-binary

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
wacore/binary/src/decoder.rs (1)

346-363: Scalar fallback's second loop is unreachable — consider simplifying.

When the SIMD validity check fails (!(hi_valid & lo_valid).all()), the first scalar loop (Lines 350-355) validates every nibble and will ?-return on the first invalid value. Since the SIMD condition is equivalent to the scalar one, the second loop (Lines 356-361) that actually pushes bytes is unreachable in practice. This means the branch always errors out.

If the intent is purely a validation-then-decode pattern, the two loops could be collapsed into one that validates and pushes in a single pass.

♻️ Suggested simplification
             if !(hi_valid & lo_valid).all() {
-                // Validate first, then decode scalar as a conservative fallback.
-                for byte in *chunk {
-                    let high = (byte & 0xF0) >> 4;
-                    let low = byte & 0x0F;
-                    Self::unpack_nibble(high)?;
-                    Self::unpack_nibble(low)?;
-                }
-                for byte in *chunk {
-                    let high = (byte & 0xF0) >> 4;
-                    let low = byte & 0x0F;
-                    unpacked_bytes.push(Self::unpack_nibble(high)?);
-                    unpacked_bytes.push(Self::unpack_nibble(low)?);
-                }
+                // Scalar fallback: validate and decode in a single pass.
+                for byte in *chunk {
+                    let high = (byte & 0xF0) >> 4;
+                    let low = byte & 0x0F;
+                    unpacked_bytes.push(Self::unpack_nibble(high)?);
+                    unpacked_bytes.push(Self::unpack_nibble(low)?);
+                }
                 continue;
             }
wacore/binary/src/encoder.rs (3)

212-226: StrKey pointer-identity cache is fragile — the invariant should be enforced structurally.

StrKey relies on string pointer identity, meaning the cache is only valid while the same immutable Node/NodeRef backs both the planning and encoding passes. The comment on line 241-242 captures this, and the current call sites in marshal_exact/marshal_ref_exact are correct (borrow spans both passes).

However, nothing prevents a future caller from building a plan from one node and encoding a different node, which would silently degrade to cache misses (no incorrect results, but no benefit either). This is acceptable for now — just flagging for awareness.


809-861: SIMD nibble packing: the deinterleave trick is correct but non-obvious — the inline comment helps.

The deinterleave(nibbles, nibbles.rotate_elements_left::<1>()) pattern extracts even/odd elements into the first 8 lanes, which are then shift-or'd and written as 8 packed bytes from 16 input chars. The same pattern is used for hex packing.

One note: packed_bytes[..8] is correct because 16 input nibbles produce 8 packed bytes, but a brief comment explaining why only half the SIMD output is used would help future readers.


720-765: write_jid_ref and write_jid_owned are near-identical — consider extracting shared logic.

Both methods differ only in the Jid type accessed (&JidRef vs &Jid) but the encoding logic is the same. If these types share a trait or accessor pattern, a single generic helper could reduce the duplication.

♻️ Sketch
// If both Jid and JidRef implement a common trait:
fn write_jid_impl(&mut self, user: &str, server: &str, device: u16, agent: u8) -> Result<()> {
    if device > 0 {
        let device_u8 = u8::try_from(device).map_err(|_| {
            BinaryError::AttrParse(format!("AD_JID device id out of range: {}", device))
        })?;
        self.write_u8(token::AD_JID)?;
        self.write_u8(agent)?;
        self.write_u8(device_u8)?;
        self.write_string(user)?;
    } else {
        self.write_u8(token::JID_PAIR)?;
        if user.is_empty() {
            self.write_u8(token::LIST_EMPTY)?;
        } else {
            self.write_string(user)?;
        }
        self.write_string(server)?;
    }
    Ok(())
}
wacore/binary/src/marshal.rs (1)

166-236: Capacity estimation only samples first 32 children — acceptable trade-off but worth documenting.

estimate_capacity_node and estimate_capacity_node_ref iterate only AUTO_CHILD_SAMPLE_LIMIT (32) children for cost estimation, then add children.len() * AUTO_CHILD_ESTIMATE for the rest. If later children are significantly larger than the sampled ones, the estimate underestimates — but the Vec will grow as needed, so this is a performance trade-off rather than a correctness issue. A brief doc comment on AUTO_CHILD_SAMPLE_LIMIT explaining the rationale would be helpful.


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 commented Feb 11, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchperf-binary
Testbedubuntu-latest

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
binary_benchmark::unpack_group::bench_unpack_compressedInstructions
instructions x 1e3
📈 plot
🚷 threshold
🚨 alert (🔔)
555.99 x 1e3
(+7.53%)Baseline: 517.06 x 1e3
542.91 x 1e3
(102.41%)

Click to view all benchmark results
BenchmarkInstructionsBenchmark Result
instructions
(Result Δ%)
Upper Boundary
instructions
(Limit %)
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled()📈 view plot
🚷 view threshold
6,124.00
(-15.22%)Baseline: 7,223.70
7,584.88
(80.74%)
binary_benchmark::child_iteration_group::bench_get_children_by_tag📈 view plot
🚷 view threshold
850,819.00
(+0.01%)Baseline: 850,745.00
893,282.25
(95.25%)
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled()📈 view plot
🚷 view threshold
22,212.00
(-10.33%)Baseline: 24,769.67
26,008.15
(85.40%)
binary_benchmark::marshal_group::bench_marshal_allocating📈 view plot
🚷 view threshold
119,220.00
(-12.44%)Baseline: 136,162.87
142,971.01
(83.39%)
binary_benchmark::marshal_group::bench_marshal_auto_allocating📈 view plot
🚷 view threshold
119,248.00
(0.00%)Baseline: 119,248.00
125,210.40
(95.24%)
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating📈 view plot
🚷 view threshold
534,027.00
(0.00%)Baseline: 534,027.00
560,728.35
(95.24%)
binary_benchmark::marshal_group::bench_marshal_auto_long_string📈 view plot
🚷 view threshold
17,350.00
(0.00%)Baseline: 17,350.00
18,217.50
(95.24%)
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating📈 view plot
🚷 view threshold
17,136,927.00
(0.00%)Baseline: 17,136,927.00
17,993,773.35
(95.24%)
binary_benchmark::marshal_group::bench_marshal_exact_allocating📈 view plot
🚷 view threshold
176,721.00
(0.00%)Baseline: 176,721.00
185,557.05
(95.24%)
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating📈 view plot
🚷 view threshold
535,440.00
(0.00%)Baseline: 535,440.00
562,212.00
(95.24%)
binary_benchmark::marshal_group::bench_marshal_exact_long_string📈 view plot
🚷 view threshold
19,404.00
(0.00%)Baseline: 19,404.00
20,374.20
(95.24%)
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating📈 view plot
🚷 view threshold
42,772,453.00
(0.00%)Baseline: 42,772,453.00
44,911,075.65
(95.24%)
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating📈 view plot
🚷 view threshold
534,466.00
(0.00%)Baseline: 534,466.00
561,189.30
(95.24%)
binary_benchmark::marshal_group::bench_marshal_long_string📈 view plot
🚷 view threshold
17,323.00
(-11.10%)Baseline: 19,486.53
20,460.86
(84.66%)
binary_benchmark::marshal_group::bench_marshal_many_children_allocating📈 view plot
🚷 view threshold
17,137,747.00
(0.00%)Baseline: 17,137,747.00
17,994,634.35
(95.24%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer📈 view plot
🚷 view threshold
129,121.00
(-5.32%)Baseline: 136,369.93
143,188.43
(90.18%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer📈 view plot
🚷 view threshold
119,320.00
(0.00%)Baseline: 119,320.00
125,286.00
(95.24%)
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled()📈 view plot
🚷 view threshold
93,707.00
(-11.68%)Baseline: 106,094.25
111,398.97
(84.12%)
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,249.00
(-14.39%)Baseline: 8,467.64
8,891.03
(81.53%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled()📈 view plot
🚷 view threshold
93,738.00
(0.00%)Baseline: 93,738.00
98,424.90
(95.24%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,272.00
(0.00%)Baseline: 7,272.00
7,635.60
(95.24%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled()📈 view plot
🚷 view threshold
109,523.00
(0.00%)Baseline: 109,523.00
114,999.15
(95.24%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled()📈 view plot
🚷 view threshold
8,784.00
(0.00%)Baseline: 8,784.00
9,223.20
(95.24%)
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled()📈 view plot
🚷 view threshold
44,794.00
(-9.28%)Baseline: 49,374.75
51,843.49
(86.40%)
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled()📈 view plot
🚷 view threshold
2,588.00
(-17.30%)Baseline: 3,129.28
3,285.75
(78.76%)
binary_benchmark::unpack_group::bench_unpack_compressed📈 view plot
🚷 view threshold
🚨 view alert (🔔)
555,986.00
(+7.53%)Baseline: 517,058.38
542,911.30
(102.41%)

binary_benchmark::unpack_group::bench_unpack_uncompressed📈 view plot
🚷 view threshold
771.00
(-0.87%)Baseline: 777.79
816.68
(94.41%)
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data()📈 view plot
🚷 view threshold
27,611,126.00
(-0.52%)Baseline: 27,754,348.08
29,142,065.49
(94.75%)
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message()📈 view plot
🚷 view threshold
5,540,334.00
(-0.43%)Baseline: 5,564,051.41
5,842,253.98
(94.83%)
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session()📈 view plot
🚷 view threshold
178,106.00
(+0.08%)Baseline: 177,967.98
186,866.38
(95.31%)
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session()📈 view plot
🚷 view threshold
178,917.00
(+0.08%)Baseline: 178,779.80
187,718.79
(95.31%)
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users()📈 view plot
🚷 view threshold
17,190,791.00
(-0.55%)Baseline: 17,285,860.21
18,150,153.22
(94.71%)
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender()📈 view plot
🚷 view threshold
295,894.00
(+0.03%)Baseline: 295,801.28
310,591.34
(95.27%)
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message()📈 view plot
🚷 view threshold
12,393,771.00
(-1.54%)Baseline: 12,587,882.34
13,217,276.46
(93.77%)
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution()📈 view plot
🚷 view threshold
715,619.00
(-0.03%)Baseline: 715,866.18
751,659.49
(95.21%)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()📈 view plot
🚷 view threshold
41,833.00
(+0.11%)Baseline: 41,787.31
43,876.68
(95.34%)
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction()📈 view plot
🚷 view threshold
15,561,842.00
(+0.00%)Baseline: 15,561,359.58
16,339,427.55
(95.24%)
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages()📈 view plot
🚷 view threshold
5,504,889.00
(-0.35%)Baseline: 5,524,315.26
5,800,531.03
(94.90%)
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session()📈 view plot
🚷 view threshold
956,786.00
(-0.69%)Baseline: 963,447.83
1,011,620.22
(94.58%)
libsignal_benchmark::signature_group::bench_key_generation keygen📈 view plot
🚷 view threshold
2,822,769.00
(-0.02%)Baseline: 2,823,198.16
2,964,358.07
(95.22%)
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message()📈 view plot
🚷 view threshold
3,444,364.00
(-3.36%)Baseline: 3,564,094.23
3,742,298.94
(92.04%)
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message()📈 view plot
🚷 view threshold
125,822,725.00
(+0.31%)Baseline: 125,427,766.81
131,699,155.15
(95.54%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message()📈 view plot
🚷 view threshold
11,665.00
(-1.08%)Baseline: 11,792.05
12,381.66
(94.21%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message()📈 view plot
🚷 view threshold
3,774.00
(-1.31%)Baseline: 3,824.17
4,015.38
(93.99%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended()📈 view plot
🚷 view threshold
87,777.00
(-0.23%)Baseline: 87,980.75
92,379.79
(95.02%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple()📈 view plot
🚷 view threshold
79,909.00
(-0.16%)Baseline: 80,035.87
84,037.66
(95.09%)
reporting_token_benchmark::key_derivation_group::bench_key_derivation📈 view plot
🚷 view threshold
51,010.00
(-0.04%)Baseline: 51,031.47
53,583.04
(95.20%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message()📈 view plot
🚷 view threshold
5,749.00
(+0.26%)Baseline: 5,734.37
6,021.09
(95.48%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message()📈 view plot
🚷 view threshold
2,109.00
(-0.30%)Baseline: 2,115.38
2,221.15
(94.95%)
reporting_token_benchmark::token_calculation_group::bench_token_calculation📈 view plot
🚷 view threshold
21,920.00
(+0.04%)Baseline: 21,911.40
23,006.97
(95.28%)
🐰 View full continuous benchmarking report in Bencher

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
wacore/binary/src/encoder.rs (1)

264-314: ⚠️ Potential issue | 🔴 Critical

parse_jid_meta silently drops text after the first : or _ even when the suffix is not a valid number.

When find(':') matches but device_part.parse::<u16>() fails, ua (the part before :) is still used for subsequent underscore scanning and determines user_end. Likewise, when find('_') matches but the agent suffix is not a valid u16, user_end is set to underscore_idx, truncating the user.

Concrete example: the string "foo:bar@baz.com" (≤ 48 chars, contains @) will be classified as StringHint::Jid with user_end = 3. write_jid_from_meta then encodes user "foo" + server "baz.com" via JID_PAIR, and the decoder reconstructs "foo@baz.com" — silently dropping ":bar".

The same data-loss applies to underscores: "hello_world@s.whatsapp.net" → encoded as user "hello", decoded as "hello@s.whatsapp.net".

When the suffix doesn't parse as a number, fall back to the full user_combined so no data is lost:

Proposed fix
 fn parse_jid_meta(input: &str) -> Option<ParsedJidMeta> {
     let sep_idx = input.find('@')?;
     let server_start = sep_idx + 1;
     let server = &input[server_start..];
     let user_combined = &input[..sep_idx];
 
-    let (user_agent, device) = if let Some(colon_idx) = user_combined.find(':') {
-        let ua = &user_combined[..colon_idx];
-        let device_part = &user_combined[colon_idx + 1..];
-        let parsed_device = if device_part.is_empty() {
-            None
-        } else {
-            device_part.parse::<u16>().ok()
-        };
-        (ua, parsed_device)
+    let (user_agent, device) = if let Some(colon_idx) = user_combined.find(':')
+        && let Ok(d) = user_combined[colon_idx + 1..].parse::<u16>()
+    {
+        (&user_combined[..colon_idx], Some(d))
     } else {
         (user_combined, None)
     };
 
-    let (user_end, agent_override) = if let Some(underscore_idx) = user_agent.find('_') {
-        let agent_part = &user_agent[underscore_idx + 1..];
-        (
-            underscore_idx,
-            if agent_part.is_empty() {
-                None
-            } else {
-                agent_part.parse::<u16>().ok()
-            },
-        )
+    let (user_end, agent_override) = if let Some(underscore_idx) = user_agent.find('_')
+        && let Ok(a) = user_agent[underscore_idx + 1..].parse::<u16>()
+    {
+        (underscore_idx, Some(a))
     } else {
         (user_agent.len(), None)
     };
🤖 Fix all issues with AI agents
In `@wacore/binary/src/marshal.rs`:
- Around line 45-51: marshal_ref currently allocates payload using plan.size and
returns it without truncating trailing zeros, mirroring the same trailing-zeros
risk as marshal; update marshal_ref so after writing with Encoder::new_slice and
encoder.write_node it slices/truncates payload to encoder.bytes_written() before
returning (use build_marshaled_node_ref_plan, plan.size,
encoder.bytes_written(), Encoder::new_slice and encoder.write_node to locate the
logic) to ensure the returned Vec<u8> contains only the actual encoded bytes.
- Around line 27-32: The vector payload is allocated with plan.size which may
overestimate the actual encoded length and leave trailing zeros; after calling
Encoder::write_node (using Encoder::new_slice with plan.hints) retrieve the
actual number of bytes via encoder.bytes_written() and truncate the payload to
that length (e.g., payload.truncate(bytes_written)) before returning so the
returned Vec<u8> contains only the real encoded bytes; update the return path in
the same function that calls build_marshaled_node_plan and uses
Encoder::new_slice/encoder.write_node to perform this truncation.
🧹 Nitpick comments (4)
wacore/binary/src/decoder.rs (1)

306-310: Unsafe from_utf8_unchecked is sound but removes a safety net.

The SAFETY argument is correct: both HEX_LOOKUP and NIBBLE_LOOKUP produce only ASCII bytes (including \x00 padding), so the output is always valid UTF-8. The SIMD validation for NIBBLE_8 (lines 271-284) and the scalar fallback via unpack_byte (lines 295-300) further ensure only expected nibble values reach the lookup.

That said, from_utf8_unchecked turns future lookup-table bugs into UB rather than a clean InvalidUtf8 error. Given the bytes are ASCII, from_utf8 should already be near-zero cost (fast-path short-circuit). Consider whether the marginal speedup justifies losing that defense layer — if it does, this is fine as-is.

wacore/binary/src/encoder.rs (3)

633-669: write_string_uncached duplicates write_string_with_hint — delegate instead.

The match arms are identical. write_string_uncached can just classify and delegate:

Proposed simplification
     fn write_string_uncached(&mut self, s: &str) -> Result<()> {
-        match classify_string_hint(s) {
-            StringHint::Empty => {
-                self.write_u8(token::BINARY_8)?;
-                self.write_u8(0)?;
-            }
-            StringHint::SingleToken(token) => self.write_u8(token)?,
-            StringHint::DoubleToken { dict, token } => {
-                self.write_u8(token::DICTIONARY_0 + dict)?;
-                self.write_u8(token)?;
-            }
-            StringHint::PackedNibble => self.write_packed_bytes(s, token::NIBBLE_8)?,
-            StringHint::PackedHex => self.write_packed_bytes(s, token::HEX_8)?,
-            StringHint::Jid(meta) => self.write_jid_from_meta(s, meta)?,
-            StringHint::RawBytes => self.write_bytes_with_len(s.as_bytes())?,
-        }
-        Ok(())
+        self.write_string_with_hint(s, classify_string_hint(s))
     }

188-202: StrKey relies on pointer identity — sound here but fragile across allocations.

StrKey caches based on (ptr, len), so it only matches the exact same string allocation. This is fine when the cache is built and consumed during a single immutable borrow of the same node, but a stale StringHintCache reused with a different node could silently miss every entry (benign) or, after allocator reuse, hit the wrong hint (UB-adjacent correctness bug). Consider adding a doc comment to StringHintCache noting that it must not outlive the strings it was built from.


692-732: device as u8 truncates silently for devices > 255.

Lines 676, 697, and 719 all cast device (a u16) to u8. If a device value exceeds 255, the high byte is silently dropped. The protocol's AD_JID format encodes only one byte for device, so this is likely a known protocol constraint — but a debug assertion or comment documenting this invariant would prevent confusion.

Comment thread wacore/binary/src/marshal.rs
Comment thread wacore/binary/src/marshal.rs

@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

🤖 Fix all issues with AI agents
In `@wacore/binary/src/encoder.rs`:
- Around line 346-366: The comment points out a mismatch between the JID length
heuristic and the test comment: in classify_string_hint the JID gate uses
is_likely_jid = s.len() <= 48, not 256; update the test comment to reflect the
actual 48-byte threshold (or change the heuristic if intended) so they match.
Locate classify_string_hint and the test around the comment (test line
referencing "256 bytes or less") and either adjust the test comment to say "48
bytes or less" or change the heuristic to use 256 if that was the original
intent; ensure any related tests still pass after the change.
🧹 Nitpick comments (6)
wacore/binary/src/decoder.rs (2)

113-121: Prefer token constants over magic numbers for consistency.

Lines 117–118 use raw 248 / 249 while the rest of the file (and the encoder) consistently references token::LIST_8 / token::LIST_16. Using the named constants improves readability and reduces the risk of silent divergence if those values ever change.

Suggested fix
     fn read_list_size(&mut self, tag: u8) -> Result<usize> {
         match tag {
             token::LIST_EMPTY => Ok(0),
-            248 => self.read_u8().map(|v| v as usize),
-            249 => self.read_u16_be().map(|v| v as usize),
+            token::LIST_8 => self.read_u8().map(|v| v as usize),
+            token::LIST_16 => self.read_u16_be().map(|v| v as usize),
             _ => Err(BinaryError::InvalidToken(tag)),
         }
     }

331-373: SIMD nibble validation fallback uses unreachable! — will panic on hypothetical SIMD mismatch instead of returning an error.

If the SIMD validation (simd_le/simd_eq) ever disagrees with the scalar unpack_nibble logic (e.g., due to a portable_simd bug on an unusual target), line 355 panics in release builds. The current validation logic is correct, so this is unlikely, but replacing unreachable! with unsafe { core::hint::unreachable_unchecked() } (to match the perf-oriented unsafe style elsewhere) or simply removing it and falling through to the scalar output would be more resilient.

That said, the SIMD mask (<= 11 || == 15) provably matches the scalar match arms, so this is a very low-risk concern.

wacore/binary/src/marshal.rs (1)

162-232: Capacity estimation functions are near-identical for Node and NodeRef — consider a shared helper.

estimate_capacity_node and estimate_capacity_node_ref have the same structure and constants, differing only in how they access content (&node.content vs node.content.as_deref()). A trait or closure-based approach could eliminate the duplication, but given the structural match differences, this is acceptable as-is.

wacore/binary/src/encoder.rs (3)

212-226: StrKey uses pointer identity — relies on node immutability between planning and encoding passes.

This is safe in the current marshal_exact flow where build_marshaled_node_plan and Encoder::new_slice operate on the same node in the same function scope. If this cache were ever reused across different node instances or after mutation/reallocation, lookups would silently miss.

A brief // SAFETY: comment documenting this invariant (same node, no mutation between passes) would help future maintainers.


676-714: write_string_uncached and write_string_with_hint share identical match arms.

Both functions dispatch on the same StringHint variants with the same writing logic. The only difference is that write_string_with_hint receives a pre-computed hint while write_string_uncached computes it via classify_string_hint. Consider extracting the shared match body into a single write_string_from_hint and having both callers resolve the hint first.

Suggested consolidation
-    fn write_string_uncached(&mut self, s: &str) -> Result<()> {
-        match classify_string_hint(s) {
-            StringHint::Empty => { ... }
-            // ... identical arms ...
-        }
-        Ok(())
-    }
-
-    fn write_string_with_hint(&mut self, s: &str, hint: StringHint) -> Result<()> {
-        match hint {
-            StringHint::Empty => { ... }
-            // ... identical arms ...
-        }
-        Ok(())
-    }
+    fn write_string_uncached(&mut self, s: &str) -> Result<()> {
+        let hint = classify_string_hint(s);
+        self.write_string_with_hint(s, hint)
+    }

252-280: Linear scan in StringHintCache is appropriate for bounded size but could become a bottleneck for deeply nested nodes.

hint_for and hint_or_insert do O(n) scans on every string lookup, capped at MAX_HINT_ENTRIES = 96. For the current workloads this is fine and likely faster than a HashMap. Just noting that if MAX_HINT_ENTRIES is ever raised significantly, switching to a HashMap (or a fixed-size hash table) would be warranted.

Comment thread wacore/binary/src/encoder.rs
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