Skip to content

feat!: sticker pack sending support - #454

Merged
jlucaso1 merged 6 commits into
mainfrom
feat/sticker-pack-send
Mar 28, 2026
Merged

jlucaso1 merged 6 commits into
mainfrom
feat/sticker-pack-send

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Mar 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add proto-level helpers for creating and sending sticker pack messages, following the same pattern as album messages (no dedicated send method)
  • New modules: wacore/src/zip.rs (store-only ZIP writer), wacore/src/webp.rs (animated detection), wacore/src/sticker_pack.rs (helpers + types)
  • Add StickerPackThumbnail MediaType and Downloadable impl for StickerPackMessage
  • Add UploadOptions with optional media_key for shared-key uploads (sticker pack thumbnail reuses ZIP's key)
  • Proto fields match WA Web's GenerateStickerPackMessageProto.js exactly

Breaking changes

  • Client::upload() now takes UploadOptions as third parameter (use Default::default() for existing behavior)
  • MediaType gains StickerPackThumbnail variant and #[non_exhaustive]

WA Web parity

All protocol details verified against captured WhatsApp Web JS:

Detail Value WA Web source
HKDF info (ZIP) "WhatsApp Sticker Pack Keys" CreateMediaKeys.js:62
HKDF info (thumbnail) "WhatsApp Sticker Pack Thumbnail Keys" CreateMediaKeys.js:63-64
MMS type "sticker-pack" / "thumbnail-sticker-pack" MmsMediaTypes.js:40-41
Upload path /mms/sticker-pack / /mms/thumbnail-sticker-pack ClientFormatHashUrl.js:42
Wire mediatype attr "sticker_pack" JobsCommon.js:220-221
Shared media key Thumbnail reuses ZIP's key Prep.js:271-274,308,334
Proto fields Exact match with GenerateStickerPackMessageProto.js No extra fields set

Usage

let stickers = vec![
    StickerInput::new(&webp_bytes).with_emojis(vec!["😀".into()]),
    StickerInput::new(&webp_bytes_2),
];
let zip_result = create_sticker_pack_zip("pack-id", &stickers, &cover_webp)?;

let zip_upload = client.upload(
    zip_result.zip_bytes.clone(), MediaType::StickerPack, Default::default()
).await?;
let thumb_upload = client.upload(
    thumbnail_jpeg, MediaType::StickerPackThumbnail,
    UploadOptions::new().with_media_key(zip_upload.media_key.clone()),
).await?;

let metadata = StickerPackMetadata::new(pack_id, "My Pack".into(), "Me".into());
let msg = build_sticker_pack_message(&zip_result, &zip_upload.into(), &thumb_upload.into(), metadata);
client.send_message(jid, msg).await?;

Test plan

  • Unit tests for ZIP writer (CRC-32, single/multi file, byte-level offset validation)
  • Unit tests for animated WebP detection (VP8X flag, ANIM chunk, edge cases)
  • Unit tests for sticker pack ZIP creation (basic, dedup, limits, emojis, pack_id validation)
  • Unit test for message proto builder (verifies exact field parity with WA Web)
  • cargo clippy --all --tests clean
  • cargo fmt --all --check clean
  • E2E test with real WhatsApp connection

Summary by CodeRabbit

  • New Features

    • Create and share sticker packs with metadata, cover images, deduplication, package generation, and downloadable package support.
    • Uploads accept optional upload options including a custom 32‑byte media key; upload API surface updated accordingly.
    • WebP animation detection added to improve media handling.
    • Thumbnail/sticker‑pack media type support for downloads.
  • Tests

    • End-to-end media upload tests updated to use the new upload signature (options parameter).

Add proto-level helpers for creating and sending sticker pack messages,
following the same pattern as album messages (no dedicated send method).

New modules:
- wacore/src/zip.rs: minimal store-only ZIP writer (no deps)
- wacore/src/webp.rs: animated WebP detection via RIFF/VP8X parsing
- wacore/src/sticker_pack.rs: create_sticker_pack_zip(), build_sticker_pack_message(), types

Breaking changes:
- Client::upload() now takes UploadOptions as third parameter
- MediaType enum gains StickerPackThumbnail variant and #[non_exhaustive]
@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fe5b4aca-6fc9-4064-9170-d0c518cad37a

📥 Commits

Reviewing files that changed from the base of the PR and between ece212c and 02572b2.

📒 Files selected for processing (1)
  • wacore/src/sticker_pack.rs

📝 Walkthrough

Walkthrough

Adds sticker-pack creation (ZIP + proto message), WebP animation detection, an in-memory ZIP writer, key-aware media encryption via new UploadOptions, re-exports wacore::sticker_pack and wacore::webp, updates Client::upload signature, and adjusts tests and call sites to the new upload API.

Changes

Cohort / File(s) Summary
Top-level exports & upload API
src/lib.rs, src/upload.rs
Re-exported wacore::sticker_pack and wacore::webp; added pub use upload::UploadOptions; Client::upload signature changed to accept UploadOptions.
Client upload & tests
src/upload.rs, tests/e2e/tests/media.rs, tests/e2e/tests/newsletter.rs
Introduced pub struct UploadOptions { pub media_key: Option<Vec<u8>> } with constructors; Client::upload takes options; tests updated to pass Default::default(); encryption uses provided 32-byte media_key when present and validates length.
wacore upload encryption
wacore/src/upload.rs
Added encrypt_media_with_key(…, Option<&[u8;32]>); encrypt_media delegates to it; in-memory encryption now uses MediaEncryptor::update/finalize.
Sticker pack module & message builder
wacore/src/sticker_pack.rs
New module building store-only ZIPs for sticker packs with SHA-256 deduplication, proto Sticker entries preservation, message builder (StickerPackMessage), media upload info type, validations, and unit tests.
ZIP writer utility
wacore/src/zip.rs
New pub(crate) ZipWriter that writes store-only ZIP in memory (local headers, central directory, EOCD) with CRC32 and tests.
WebP animation detection
wacore/src/webp.rs
New pub fn is_animated(data: &[u8]) -> bool parsing RIFF/WebP chunks (VP8X/ANIM/ANMF) with unit tests.
Media type & send/download updates
wacore/src/download.rs, wacore/src/send.rs
Added MediaType::StickerPackThumbnail, marked MediaType #[non_exhaustive], updated mappings; media_type_from_message now detects sticker_pack_message.
wacore module visibility
wacore/src/lib.rs
Exposed pub mod sticker_pack; and pub mod webp;, added internal mod zip;.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Pack as StickerPackModule
  participant Zip as ZipWriter
  participant CoreUpload as wacore::upload
  participant CDN as UploadService

  Client->>Pack: create_sticker_pack(stickers, metadata, cover)
  Pack->>Zip: add files / finish -> zip_bytes
  Zip-->>Pack: zip_bytes
  Pack->>CoreUpload: encrypt_media_with_key(zip_bytes, media_key?)
  CoreUpload->>CDN: upload encrypted payload
  CDN-->>CoreUpload: UploadResponse
  CoreUpload-->>Pack: MediaUploadInfo (From<UploadResponse>)
  Pack-->>Client: StickerPackMessage (with upload info)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I zipped the stickers, neat and bright,
deduped the bytes and locked them tight.
WebP blinks, keys snug in sight,
encrypted hops to CDN flight.
Pack delivered — what a delight! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat!: sticker pack sending support' directly and clearly describes the main objective of the pull request: adding sticker pack sending functionality. It accurately reflects the core feature addition across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 84.38% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sticker-pack-send

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.

@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Mar 28, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 1m 37s —— View job


Code Review: Sticker Pack Sending Support

  • Read all changed files
  • Review new modules (zip.rs, webp.rs, sticker_pack.rs)
  • Review changes to existing modules (upload, download, send, lib)
  • Review test changes
  • Post review findings

Overall this is a well-structured PR that follows the existing codebase patterns (builder pattern, proto-level helpers without dedicated send methods, #[non_exhaustive] on public types). The test coverage is good. A few items worth addressing:


Bug: Silent fallback on invalid media key length

src/upload.rs:296-298

let key_ref = options
    .media_key
    .as_ref()
    .and_then(|k| <&[u8; 32]>::try_from(k.as_slice()).ok());

If the caller passes a media_key with the wrong length (not 32 bytes), .ok() silently discards it and falls back to generating a random key. This is dangerous for sticker pack thumbnails — the whole point of UploadOptions::with_media_key is to reuse the ZIP's key for the thumbnail. A wrong-length key would silently produce a thumbnail that can't be decrypted by recipients, with no error indicating what went wrong. This should return an error instead.

Fix this →


Potential overflow in WebP chunk parser

wacore/src/webp.rs:35

offset += 8 + chunk_size + (chunk_size & 1);

chunk_size is a u32 cast to usize. On 32-bit platforms, if a malformed WebP has chunk_size near u32::MAX, this addition wraps around and causes an infinite loop. Consider using saturating_add or checked_add to break the loop on overflow:

offset = match offset.checked_add(8 + chunk_size + (chunk_size & 1)) {
    Some(next) => next,
    None => break,
};

Low severity since this is likely 64-bit only, but good defensive practice for a parser that handles untrusted input.

Fix this →


Minor: Unnecessary allocation in dedup hash set

wacore/src/sticker_pack.rs:150-156

let mut seen_hashes = std::collections::HashSet::new();
for input in stickers {
    let hash = Sha256::digest(input.data);
    // ...
    if seen_hashes.insert(hash.to_vec()) {

Sha256::digest returns GenericArray<u8, U32> which can be converted to [u8; 32]. Using HashSet<[u8; 32]> avoids a heap allocation per sticker:

let hash: [u8; 32] = Sha256::digest(input.data).into();

Verify: "sticker_pack" wire type string

wacore/src/send.rs:160

if msg.sticker_pack_message.is_some() {
    return Some("sticker_pack");
}

Other media type strings in this function (e.g. "image", "video", "sticker") match WA Web's wire constants. Worth double-checking that "sticker_pack" (with underscore) is correct — WA Web might use "stickerPack" or "sticker-pack". If this is wrong, sticker pack messages won't route correctly.


Design: Everything else looks good

  • zip.rs — Clean minimal store-only ZIP writer. CRC-32 implementation is correct (verified against the standard 0xCBF43926 test vector). The u32 casts for file sizes are fine since ZIP32 has an inherent 4 GiB limit, and sticker packs are well under that.
  • sticker_pack.rs — Good use of #[non_exhaustive], proper validation (empty/max sticker count), and smart dedup logic. Builder pattern for StickerInput and StickerPackMetadata is ergonomic. Tests are thorough.
  • wacore/src/download.rsMediaType additions (StickerPack, StickerPackThumbnail) with correct app_info, mms_type, and upload_path mappings. The impl_downloadable! for StickerPackMessage is consistent with existing types.
  • UploadOptions — Clean #[non_exhaustive] + builder pattern. Default::default() migration for existing callers is mechanical and correct.
  • From<UploadResponse> for MediaUploadInfo — Nice bridge between the high-level and core crates.
  • Test changes — All Default::default() additions to existing upload() calls are correct and mechanical.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: abd6d3d150

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/src/zip.rs Outdated
put_u32(&mut self.buf, e.size); // compressed
put_u32(&mut self.buf, e.size); // uncompressed
put_u16(&mut self.buf, e.name.len() as u16);
put_zero_u16s(&mut self.buf, 5); // extra len, comment len, disk start, internal attrs

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Emit only four u16 fields before external attrs

The central-directory record writes put_zero_u16s(..., 5) after the filename length, but ZIP format only has four 16-bit fields there (extra length, comment length, disk start, internal attrs). This extra zero shifts the encoded external attrs and relative offset fields by 2 bytes, so readers parse incorrect local-header offsets and treat the archive as corrupt, which breaks sticker-pack ZIP consumption for multi-entry archives.

Useful? React with 👍 / 👎.

Comment thread src/upload.rs Outdated
let key_ref = options
.media_key
.as_ref()
.and_then(|k| <&[u8; 32]>::try_from(k.as_slice()).ok());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject invalid media_key lengths in upload options

If UploadOptions.media_key is not exactly 32 bytes, try_from(...).ok() silently discards it and falls back to generating a fresh key. That makes callers believe key reuse succeeded when it did not; in sticker-pack flows this can produce thumbnail uploads encrypted with a different key than the one referenced in the final message, causing downstream decryption failures. This should return an explicit error instead of silently changing behavior.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Mar 28, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchfeat/sticker-pack-send
Testbedubuntu-latest

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()Instructions
instructions x 1e3
📈 plot
🚷 threshold
🚨 alert (🔔)
47.13 x 1e3
(+9.44%)Baseline: 43.06 x 1e3
45.22 x 1e3
(104.23%)

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,197.00
(-5.67%)Baseline: 6,569.51
6,897.99
(89.84%)
binary_benchmark::child_iteration_group::bench_get_children_by_tag📈 view plot
🚷 view threshold
524,304.00
(-27.54%)Baseline: 723,551.34
759,728.91
(69.01%)
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled()📈 view plot
🚷 view threshold
20,868.00
(-5.79%)Baseline: 22,150.88
23,258.42
(89.72%)
binary_benchmark::marshal_group::bench_marshal_allocating📈 view plot
🚷 view threshold
98,202.00
(-17.42%)Baseline: 118,916.94
124,862.79
(78.65%)
binary_benchmark::marshal_group::bench_marshal_auto_allocating📈 view plot
🚷 view threshold
98,230.00
(-10.36%)Baseline: 109,577.27
115,056.14
(85.38%)
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating📈 view plot
🚷 view threshold
532,948.00
(-0.11%)Baseline: 533,539.15
560,216.11
(95.13%)
binary_benchmark::marshal_group::bench_marshal_auto_long_string📈 view plot
🚷 view threshold
15,870.00
(-4.83%)Baseline: 16,675.27
17,509.03
(90.64%)
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating📈 view plot
🚷 view threshold
14,715,201.00
(-8.18%)Baseline: 16,026,206.72
16,827,517.06
(87.45%)
binary_benchmark::marshal_group::bench_marshal_exact_allocating📈 view plot
🚷 view threshold
118,358.00
(-21.01%)Baseline: 149,846.81
157,339.15
(75.22%)
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating📈 view plot
🚷 view threshold
534,378.00
(-0.11%)Baseline: 534,959.78
561,707.77
(95.13%)
binary_benchmark::marshal_group::bench_marshal_exact_long_string📈 view plot
🚷 view threshold
17,919.00
(-4.31%)Baseline: 18,726.63
19,662.96
(91.13%)
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating📈 view plot
🚷 view threshold
28,066,347.00
(-22.05%)Baseline: 36,007,770.55
37,808,159.07
(74.23%)
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating📈 view plot
🚷 view threshold
533,387.00
(-0.11%)Baseline: 533,978.15
560,677.06
(95.13%)
binary_benchmark::marshal_group::bench_marshal_long_string📈 view plot
🚷 view threshold
15,843.00
(-7.53%)Baseline: 17,132.24
17,988.85
(88.07%)
binary_benchmark::marshal_group::bench_marshal_many_children_allocating📈 view plot
🚷 view threshold
14,716,627.00
(-8.18%)Baseline: 16,027,319.98
16,828,685.97
(87.45%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer📈 view plot
🚷 view threshold
107,945.00
(-13.86%)Baseline: 125,315.47
131,581.24
(82.04%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer📈 view plot
🚷 view threshold
98,302.00
(-10.35%)Baseline: 109,649.27
115,131.74
(85.38%)
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled()📈 view plot
🚷 view threshold
90,974.00
(-5.45%)Baseline: 96,222.63
101,033.76
(90.04%)
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,378.00
(-3.51%)Baseline: 7,646.62
8,028.95
(91.89%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled()📈 view plot
🚷 view threshold
91,005.00
(-1.84%)Baseline: 92,708.53
97,343.95
(93.49%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,401.00
(+0.37%)Baseline: 7,373.64
7,742.32
(95.59%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled()📈 view plot
🚷 view threshold
106,790.00
(-1.57%)Baseline: 108,493.53
113,918.20
(93.74%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled()📈 view plot
🚷 view threshold
8,913.00
(+0.31%)Baseline: 8,885.64
9,329.92
(95.53%)
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled()📈 view plot
🚷 view threshold
41,989.00
(-8.20%)Baseline: 45,738.61
48,025.54
(87.43%)
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled()📈 view plot
🚷 view threshold
2,717.00
(-4.48%)Baseline: 2,844.28
2,986.50
(90.98%)
binary_benchmark::unpack_group::bench_unpack_compressed📈 view plot
🚷 view threshold
556,092.00
(+2.53%)Baseline: 542,350.93
569,468.48
(97.65%)
binary_benchmark::unpack_group::bench_unpack_uncompressed📈 view plot
🚷 view threshold
771.00
(-0.31%)Baseline: 773.42
812.09
(94.94%)
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data()📈 view plot
🚷 view threshold
27,738,277.00
(+0.13%)Baseline: 27,702,399.72
29,087,519.71
(95.36%)
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message()📈 view plot
🚷 view threshold
5,544,828.00
(-0.06%)Baseline: 5,547,957.67
5,825,355.55
(95.18%)
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session()📈 view plot
🚷 view threshold
175,061.00
(-1.38%)Baseline: 177,510.66
186,386.19
(93.92%)
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session()📈 view plot
🚷 view threshold
175,710.00
(-1.45%)Baseline: 178,291.63
187,206.21
(93.86%)
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users()📈 view plot
🚷 view threshold
17,311,610.00
(+0.18%)Baseline: 17,280,709.67
18,144,745.15
(95.41%)
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender()📈 view plot
🚷 view threshold
298,417.00
(+0.57%)Baseline: 296,711.85
311,547.44
(95.79%)
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message()📈 view plot
🚷 view threshold
12,698,722.00
(+0.83%)Baseline: 12,594,177.20
13,223,886.06
(96.03%)
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution()📈 view plot
🚷 view threshold
719,543.00
(+0.39%)Baseline: 716,768.45
752,606.87
(95.61%)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()📈 view plot
🚷 view threshold
🚨 view alert (🔔)
47,129.00
(+9.44%)Baseline: 43,061.92
45,215.01
(104.23%)

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,761.19
16,339,849.25
(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,378,778.00
(-1.90%)Baseline: 5,482,950.54
5,757,098.07
(93.43%)
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session()📈 view plot
🚷 view threshold
312,188.00
(-62.39%)Baseline: 829,965.17
871,463.43
(35.82%)
libsignal_benchmark::signature_group::bench_key_generation keygen📈 view plot
🚷 view threshold
2,830,547.00
(+0.19%)Baseline: 2,825,273.98
2,966,537.67
(95.42%)
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message()📈 view plot
🚷 view threshold
3,452,844.00
(-0.55%)Baseline: 3,471,898.30
3,645,493.21
(94.72%)
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message()📈 view plot
🚷 view threshold
124,686,468.00
(-0.52%)Baseline: 125,343,539.70
131,610,716.69
(94.74%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message()📈 view plot
🚷 view threshold
12,002.00
(+1.58%)Baseline: 11,815.37
12,406.14
(96.74%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message()📈 view plot
🚷 view threshold
3,930.00
(+2.57%)Baseline: 3,831.56
4,023.14
(97.68%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended()📈 view plot
🚷 view threshold
87,073.00
(-0.82%)Baseline: 87,796.93
92,186.78
(94.45%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple()📈 view plot
🚷 view threshold
79,022.00
(-1.02%)Baseline: 79,835.44
83,827.22
(94.27%)
reporting_token_benchmark::key_derivation_group::bench_key_derivation📈 view plot
🚷 view threshold
50,375.00
(-1.11%)Baseline: 50,940.29
53,487.30
(94.18%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message()📈 view plot
🚷 view threshold
5,956.00
(+3.30%)Baseline: 5,765.68
6,053.96
(98.38%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message()📈 view plot
🚷 view threshold
2,227.00
(+4.70%)Baseline: 2,126.97
2,233.32
(99.72%)
reporting_token_benchmark::token_calculation_group::bench_token_calculation📈 view plot
🚷 view threshold
21,920.00
(+0.01%)Baseline: 21,917.12
23,012.98
(95.25%)
🐰 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: 4

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

Inline comments:
In `@src/upload.rs`:
- Around line 294-298: The code silently drops an invalid-length
UploadOptions.media_key by using .ok(), causing unintended fallback to a new
key; instead, validate the media_key length and return an explicit error when it
isn't exactly 32 bytes so caller intent is preserved. In the block that builds
key_ref from UploadOptions.media_key (the variable key_ref and the conversion
using <&[u8; 32]>::try_from), check if media_key.is_some() and if try_from fails
then return Err(...) (or propagate a specific error type) rather than calling
.ok(), and only call wacore::upload::encrypt_media_with_key(&data, media_type,
key_ref) when key_ref is valid or when media_key was absent and a fresh key is
acceptable.

In `@wacore/src/sticker_pack.rs`:
- Around line 206-207: The code hardcodes thumbnail_height and thumbnail_width
to 252 in the sticker pack proto (in the
build_sticker_pack_message/MediaUploadInfo area), which can produce incorrect
metadata if the actual thumbnail size differs; update MediaUploadInfo (or the
build_sticker_pack_message function signature) to accept thumbnail_width and
thumbnail_height as parameters and populate the proto from those fields, or if
you prefer not to change the API add a clear doc/comment on
MediaUploadInfo/build_sticker_pack_message stating callers must supply a 252×252
thumbnail and that sizes will not be validated or adjusted.

In `@wacore/src/webp.rs`:
- Around line 34-36: The arithmetic offset += 8 + chunk_size + (chunk_size & 1)
can overflow for crafted large chunk_size; replace this with checked or
saturating arithmetic on offset and chunk_size (e.g., use checked_add or
saturating_add when summing 8, chunk_size, and (chunk_size & 1)) and return an
Err or handle the overflow if addition returns None, ensuring you update the
same offset variable safely (refer to offset and chunk_size in the parsing loop
/ function that computes chunk offsets).

In `@wacore/src/zip.rs`:
- Around line 45-46: Add a doc-comment to the ZipWriter type explaining that
sizes are stored as u32 (ZIP32) and that the current casts (data.len() as u32
and self.buf.len() as u32 in the write logic) will truncate values > 4GiB;
mention this is acceptable for small sticker WebP use but warn against reusing
ZipWriter for larger archives or suggest adding checks/assertions if larger
inputs may be expected. Ensure the comment references ZipWriter and the
size/offset casts so maintainers see the limitation when editing that code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 30acc5e5-1645-4f0c-9380-36c373c5fe9c

📥 Commits

Reviewing files that changed from the base of the PR and between 7514429 and abd6d3d.

📒 Files selected for processing (11)
  • src/lib.rs
  • src/upload.rs
  • tests/e2e/tests/media.rs
  • tests/e2e/tests/newsletter.rs
  • wacore/src/download.rs
  • wacore/src/lib.rs
  • wacore/src/send.rs
  • wacore/src/sticker_pack.rs
  • wacore/src/upload.rs
  • wacore/src/webp.rs
  • wacore/src/zip.rs

Comment thread src/upload.rs Outdated
Comment thread wacore/src/sticker_pack.rs Outdated
Comment thread wacore/src/webp.rs Outdated
Comment thread wacore/src/zip.rs
Comment on lines +45 to +46
let size = data.len() as u32;
let offset = self.buf.len() as u32;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider documenting ZIP32 size limitations.

The as u32 casts on data.len() and buf.len() will silently truncate values exceeding 4GB. While this is a ZIP32 format limitation and acceptable for the sticker pack use case (small WebP files), a brief doc comment on ZipWriter noting this constraint would help prevent misuse if the struct is later reused for other purposes.

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

In `@wacore/src/zip.rs` around lines 45 - 46, Add a doc-comment to the ZipWriter
type explaining that sizes are stored as u32 (ZIP32) and that the current casts
(data.len() as u32 and self.buf.len() as u32 in the write logic) will truncate
values > 4GiB; mention this is acceptable for small sticker WebP use but warn
against reusing ZipWriter for larger archives or suggest adding
checks/assertions if larger inputs may be expected. Ensure the comment
references ZipWriter and the size/offset casts so maintainers see the limitation
when editing that code.

- Error on invalid media_key length instead of silent fallback
- Use checked_add in WebP chunk parser to prevent overflow
- Use [u8; 32] instead of Vec<u8> in dedup HashSet to avoid allocations
- Document 252x252 thumbnail requirement on build_sticker_pack_message
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Mar 28, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 1m 27s —— View job


Code Review: Sticker Pack Sending Support

  • Read all changed files (post-fix commit)
  • Review new modules (zip.rs, webp.rs, sticker_pack.rs)
  • Review upload changes and UploadOptions
  • Review download/send integration
  • Review test changes
  • Post findings

Well-structured PR that follows existing codebase patterns nicely. The fix commit (b1a55be) addressed the previous review's top findings (media_key validation, WebP overflow). One remaining bug and a few minor items:


Bug: Central directory has extra u16 field (ZIP corruption)

wacore/src/zip.rs:81

put_zero_u16s(&mut self.buf, 5); // extra len, comment len, disk start, internal attrs

The comment lists 4 fields but writes 5 zero u16s. Per the ZIP spec (APPNOTE.TXT §4.3.12), after file_name_length there are exactly 4 u16 fields before the u32 external_file_attributes:

  1. extra field length
  2. file comment length
  3. disk number start
  4. internal file attributes

The extra 2 bytes shift external_file_attributes and relative_offset_of_local_header by 2 bytes, producing a malformed central directory. This isn't caught by the unit tests because they only check the EOCD signature position and file count — they don't parse via a real ZIP reader.

Should be:

put_zero_u16s(&mut self.buf, 4); // extra len, comment len, disk start, internal attrs

Fix this →


Verify: "sticker_pack" wire type string

wacore/src/send.rs:161

return Some("sticker_pack");

Other media type strings in media_type_from_message use camelCase or single words matching WA Web wire constants ("image", "video", "sticker", "livelocation"). Worth confirming that "sticker_pack" (with underscore) is the correct wire format — WA Web might use "stickerPack" or another variant. If wrong, sticker pack messages may not route correctly on the receiving end.


Minor: sticker_pack_size uses ZIP bytes length, not upload file length

wacore/src/sticker_pack.rs:210

sticker_pack_size: Some(zip_result.zip_bytes.len() as u64),

This records the pre-encryption ZIP size. If the protocol field should reflect the encrypted upload size (zip_upload.file_length), this would be incorrect. Worth checking against WA Web behavior — if it expects the unencrypted ZIP size, this is fine.


Everything else looks good

  • Previous review fixes (b1a55be): The media_key validation now properly returns an error for invalid lengths (src/upload.rs:294-300). The WebP parser uses checked_add for overflow safety (wacore/src/webp.rs:34). The dedup hash set uses [u8; 32] instead of Vec<u8> (wacore/src/sticker_pack.rs:153). All good.
  • zip.rs — Clean minimal ZIP writer. CRC-32 is correct (verified against 0xCBF43926). Store-only approach is appropriate for sticker packs.
  • webp.rs — Correct VP8X/ANIM/ANMF detection. Good test coverage.
  • sticker_pack.rs — Solid builder pattern with #[non_exhaustive], proper validation, smart dedup. Thorough tests.
  • wacore/src/download.rsStickerPack/StickerPackThumbnail media type additions with correct app_info, mms_type, and upload_path mappings. impl_downloadable! for StickerPackMessage is consistent.
  • UploadOptions — Clean design. Default::default() migration for existing callers is mechanical and correct.
  • From<UploadResponse> for MediaUploadInfo — Good bridge between crate layers.
  • E2E test changes — All Default::default() additions are correct and mechanical.

View job

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

♻️ Duplicate comments (1)
wacore/src/webp.rs (1)

34-36: ⚠️ Potential issue | 🟡 Minor

The chunk-advance expression can still overflow before checked_add runs.

offset.checked_add(8 + chunk_size + (chunk_size & 1)) only checks the final add. On 32-bit targets, the inner 8 + chunk_size + ... can still wrap or panic first on crafted input.

In Rust on 32-bit targets, can the subexpression in `offset.checked_add(8 + chunk_size + (chunk_size & 1))` overflow before `checked_add` is invoked, and what is the correct fully-checked pattern for this case?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/webp.rs` around lines 34 - 36, The expression offset.checked_add(8
+ chunk_size + (chunk_size & 1)) can overflow in the inner arithmetic on
narrower targets; replace it with a chain of checked_add calls to perform each
addition safely (e.g. call offset.checked_add(8) then .and_then(|v|
v.checked_add(chunk_size)) then .and_then(|v| v.checked_add(chunk_size & 1)))
and use the resulting Option to match Some(next) / None to break; reference the
variables offset, chunk_size and the checked_add usage to locate and update the
code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/upload.rs`:
- Around line 263-268: UploadOptions currently derives Debug and will leak the
raw media_key; remove Debug from the derive list on the UploadOptions struct and
add a manual impl std::fmt::Debug for UploadOptions that prints the other fields
normally but redacts media_key (e.g., show "Some(<redacted>)" or "None").
Reference: the UploadOptions struct and its media_key field—keep Default and
Clone derives, drop Debug, and implement Debug to avoid printing the raw key.

In `@wacore/src/sticker_pack.rs`:
- Around line 181-210: The function build_sticker_pack_message currently mixes
data from StickerPackMetadata, StickerPackZipResult, and MediaUploadInfo without
verification—fix by enforcing consistency before building the proto: in
build_sticker_pack_message compute/validate that zip_upload.file_length equals
zip_result.zip_bytes.len() and that zip_upload.file_sha256 matches the SHA-256
of zip_result.zip_bytes (or alternatively change the function signature to
accept a single aggregated struct that pairs a StickerPackZipResult with its
MediaUploadInfo); also validate thumb_upload.direct_path/sha256 against the
tray/thumbnail bytes in StickerPackZipResult (and confirm metadata.pack_id
matches the pack identity inside zip_result if present), and return an error (or
panic) if any check fails so mismatched sources cannot produce an inconsistent
Message.
- Around line 128-148: The code creates a ZIP entry name using pack_id directly
in create_sticker_pack_zip (tray_icon_file_name = format!("{pack_id}.webp")),
which allows path traversal or invalid entry names; fix it by validating and
sanitizing pack_id before using it: ensure pack_id is non-empty, within a
reasonable length, and contains only a safe character set (e.g., alphanumeric,
dash, underscore) or replace/encode any path separators, dots, or control
characters (reject or canonicalize values containing "..", '/', '\\', or ASCII
control codes) and then build tray_icon_file_name from the sanitized value;
apply the same validation when constructing any other ZIP entry names and return
an error (bail!) on invalid pack_id.

In `@wacore/src/webp.rs`:
- Around line 23-27: The VP8X branch reads data[offset + 8] without ensuring the
VP8X chunk actually contains the 10-byte payload; add a guard that the parsed
chunk_size for the VP8X chunk is at least 10 (VP8X payload length) and that
offset + 8 is within data bounds before accessing data[offset + 8]; update the
condition that checks fourcc == b"VP8X" to require chunk_size >= 10 and keep the
existing bounds check so the animation flag read (data[offset + 8]) only occurs
when the full VP8X payload is present.

---

Duplicate comments:
In `@wacore/src/webp.rs`:
- Around line 34-36: The expression offset.checked_add(8 + chunk_size +
(chunk_size & 1)) can overflow in the inner arithmetic on narrower targets;
replace it with a chain of checked_add calls to perform each addition safely
(e.g. call offset.checked_add(8) then .and_then(|v| v.checked_add(chunk_size))
then .and_then(|v| v.checked_add(chunk_size & 1))) and use the resulting Option
to match Some(next) / None to break; reference the variables offset, chunk_size
and the checked_add usage to locate and update the code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5efd666b-c061-4727-b134-4e5e184313bc

📥 Commits

Reviewing files that changed from the base of the PR and between abd6d3d and b1a55be.

📒 Files selected for processing (3)
  • src/upload.rs
  • wacore/src/sticker_pack.rs
  • wacore/src/webp.rs

Comment thread src/upload.rs
Comment thread wacore/src/sticker_pack.rs
Comment on lines +181 to +210
pub fn build_sticker_pack_message(
zip_result: &StickerPackZipResult,
zip_upload: &MediaUploadInfo,
thumb_upload: &MediaUploadInfo,
metadata: StickerPackMetadata,
) -> wa::Message {
use wa::message::sticker_pack_message::StickerPackOrigin;

let pack_msg = wa::message::StickerPackMessage {
sticker_pack_id: Some(metadata.pack_id),
name: Some(metadata.name),
publisher: Some(metadata.publisher),
stickers: zip_result.stickers.clone(),
file_length: Some(zip_upload.file_length),
file_sha256: Some(zip_upload.file_sha256.clone()),
file_enc_sha256: Some(zip_upload.file_enc_sha256.clone()),
media_key: Some(zip_upload.media_key.clone()),
direct_path: Some(zip_upload.direct_path.clone()),
caption: None,
context_info: None,
pack_description: metadata.description,
media_key_timestamp: Some(zip_upload.media_key_timestamp),
tray_icon_file_name: Some(zip_result.tray_icon_file_name.clone()),
thumbnail_direct_path: Some(thumb_upload.direct_path.clone()),
thumbnail_sha256: Some(thumb_upload.file_sha256.clone()),
thumbnail_enc_sha256: Some(thumb_upload.file_enc_sha256.clone()),
thumbnail_height: Some(252),
thumbnail_width: Some(252),
image_data_hash: None,
sticker_pack_size: Some(zip_result.zip_bytes.len() as u64),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid building the proto from three independent pack sources.

This function pulls pack identity from metadata, tray-icon/sticker data from zip_result, and CDN metadata from zip_upload without any consistency check. A mismatched call can emit a proto whose hashes/direct path point to one ZIP while the sticker list or size describe another.

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

In `@wacore/src/sticker_pack.rs` around lines 181 - 210, The function
build_sticker_pack_message currently mixes data from StickerPackMetadata,
StickerPackZipResult, and MediaUploadInfo without verification—fix by enforcing
consistency before building the proto: in build_sticker_pack_message
compute/validate that zip_upload.file_length equals zip_result.zip_bytes.len()
and that zip_upload.file_sha256 matches the SHA-256 of zip_result.zip_bytes (or
alternatively change the function signature to accept a single aggregated struct
that pairs a StickerPackZipResult with its MediaUploadInfo); also validate
thumb_upload.direct_path/sha256 against the tray/thumbnail bytes in
StickerPackZipResult (and confirm metadata.pack_id matches the pack identity
inside zip_result if present), and return an error (or panic) if any check fails
so mismatched sources cannot produce an inconsistent Message.

Comment thread wacore/src/webp.rs Outdated
put_zero_u16s wrote 5 zero u16s instead of 4 after file_name_length,
shifting external_file_attributes and local_header_offset by 2 bytes.

Added a test that validates byte-level offsets through the central
directory to prevent regressions.
- Redact media_key in UploadOptions Debug impl
- Validate pack_id to prevent path traversal in ZIP entry names
- Require VP8X chunk_size >= 10 before reading animation flag
- Chain checked_add calls to prevent inner arithmetic overflow
Remove fields WA Web doesn't set on outgoing sticker packs:
media_key_timestamp, thumbnail_height/width, sticker_pack_origin,
image_data_hash. Add caption field to StickerPackMetadata.

Verified against GenerateStickerPackMessageProto.js.
@jlucaso1
jlucaso1 merged commit 0973946 into main Mar 28, 2026
7 checks passed
@jlucaso1
jlucaso1 deleted the feat/sticker-pack-send branch March 28, 2026 16:37

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02572b26f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

file_length: Some(zip_upload.file_length),
file_sha256: Some(zip_upload.file_sha256.clone()),
file_enc_sha256: Some(zip_upload.file_enc_sha256.clone()),
media_key: Some(zip_upload.media_key.clone()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate thumbnail media key before building sticker-pack message

StickerPackMessage carries a single media_key, but this builder always takes it from zip_upload and never verifies thumb_upload.media_key is the same key. If callers upload the thumbnail with Default::default() (fresh key) instead of reusing the ZIP key, the message will include thumbnail hashes/direct path for bytes encrypted under a different key than recipients derive from media_key, causing thumbnail decryption failures. Please fail fast on key mismatch (or make this function return Result) to prevent silently constructing inconsistent messages.

Useful? React with 👍 / 👎.

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