Skip to main content

wechat_protocol/
wire.rs

1//! Typed numbered business messages for the supported native profile.
2use crate::{crypto::random, protobuf as p, AccountState, DeviceProfile};
3use anyhow::{bail, ensure, Result};
4use serde::{Deserialize, Serialize};
5use std::{
6    collections::{BTreeMap, HashSet},
7    time::{SystemTime, UNIX_EPOCH},
8};
9
10pub fn unix_time() -> Result<u32> {
11    Ok(u32::try_from(
12        SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
13    )?)
14}
15fn text(value: &str, max: usize, empty: bool) -> Result<&[u8]> {
16    ensure!(
17        (empty || !value.is_empty()) && !value.contains('\0') && value.len() <= max,
18        "Invalid text input"
19    );
20    Ok(value.as_bytes())
21}
22pub fn base_request(profile: &DeviceProfile, uin: u32, task: u32) -> Result<Vec<u8>> {
23    profile.validate()?;
24    p::message(&[
25        p::bytes(1, &[0]),
26        p::uint(2, uin as u64),
27        p::bytes(3, &profile.device_id),
28        p::int32(4, profile.client_version),
29        p::bytes(5, text(&profile.device_type, 1024, false)?),
30        p::uint(6, task as u64),
31    ])
32}
33pub fn qr_request(profile: &DeviceProfile, computer_name: &str) -> Result<Vec<u8>> {
34    p::message(&[
35        p::bytes(1, &base_request(profile, 0, 0)?),
36        p::bytes(2, &p::buffer(&random::<16>()?)),
37        p::uint(3, 0),
38        p::bytes(4, text(computer_name, 255, false)?),
39    ])
40}
41pub fn poll_request(profile: &DeviceProfile, identifier: &[u8]) -> Result<Vec<u8>> {
42    ensure!(
43        !identifier.is_empty() && identifier.len() <= 4096,
44        "Invalid login identifier"
45    );
46    let ms = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
47    let timestamp = ((ms & 0xffffffff) / 1000) as u64;
48    p::message(&[
49        p::bytes(1, &base_request(profile, 0, 0)?),
50        p::bytes(2, &p::buffer(&random::<16>()?)),
51        p::bytes(3, identifier),
52        p::uint(4, timestamp),
53        p::uint(5, 0),
54    ])
55}
56pub struct QrResponse {
57    pub image: Vec<u8>,
58    pub identifier: Vec<u8>,
59    pub notify_key: Vec<u8>,
60    pub width: u32,
61    pub height: u32,
62}
63pub fn decode_qr(input: &[u8]) -> Result<QrResponse> {
64    let f = p::decode(input)?;
65    let code = p::base_code(&f)?;
66    ensure!(code == 0, "QR service returned {code}");
67    let image = p::unbuffer(p::data(&f, 2)?)?;
68    let identifier = p::data(&f, 3)?.to_vec();
69    p::scalar(&f, 4)?;
70    let notify_key = p::unbuffer(p::data(&f, 5)?)?;
71    ensure!(
72        !identifier.is_empty()
73            && identifier.len() <= 4096
74            && !notify_key.is_empty()
75            && notify_key.len() <= 1024,
76        "Invalid QR state"
77    );
78    ensure!(
79        image.len() >= 33 && image[..8] == *b"\x89PNG\r\n\x1a\n" && image[12..16] == *b"IHDR",
80        "Invalid QR image"
81    );
82    let width = u32::from_be_bytes(image[16..20].try_into()?);
83    let height = u32::from_be_bytes(image[20..24].try_into()?);
84    ensure!(
85        (21..=4096).contains(&width) && (21..=4096).contains(&height),
86        "QR image dimensions invalid"
87    );
88    Ok(QrResponse {
89        image,
90        identifier,
91        notify_key,
92        width,
93        height,
94    })
95}
96pub struct PollResponse {
97    pub status: Option<u32>,
98    pub credential3: Option<Vec<u8>>,
99    pub credential4: Option<Vec<u8>>,
100    pub lan_challenge: bool,
101}
102pub fn decode_poll(input: &[u8], notify_key: &[u8]) -> Result<PollResponse> {
103    let f = p::decode(input)?;
104    let code = p::base_code(&f)?;
105    ensure!(code == 0, "Login poll returned {code}");
106    let Some(pkg) = p::optional_data(&f, 3)? else {
107        return Ok(PollResponse {
108            status: None,
109            credential3: None,
110            credential4: None,
111            lan_challenge: false,
112        });
113    };
114    let pkg = p::decode(pkg)?;
115    let ciphertext = p::unbuffer(p::data(&pkg, 1)?)?;
116    p::scalar(&pkg, 2)?;
117    let plain = crate::crypto::cbc(&ciphertext, notify_key, openssl::symm::Mode::Decrypt)?;
118    let notify = p::decode(&plain)?;
119    // LoginQRCodeNotifyData.f17 is an SKBuffer containing
120    // LoginQRCodeNonStoreExtNotifyData; its f1 is the risk state.
121    let lan_challenge = match p::optional_data(&notify, 17)? {
122        Some(buffer) => {
123            ensure!(buffer.len() <= 4096, "Login risk extension too large");
124            let extension = p::unbuffer(buffer)?;
125            let extension = p::decode(&extension)?;
126            match p::optional(&extension, 1)? {
127                Some(p::Value::Varint(state)) => *state == 11,
128                None => false,
129                _ => bail!("Invalid login risk state"),
130            }
131        }
132        None => false,
133    };
134    let status = u32::try_from(p::scalar(&notify, 2)?)?;
135    let credential3 = p::optional_data(&notify, 3)?.map(Vec::from);
136    let credential4 = p::optional_data(&notify, 4)?.map(Vec::from);
137    if status == 2 && !lan_challenge {
138        ensure!(
139            credential3
140                .as_ref()
141                .is_some_and(|b| !b.is_empty() && b.len() <= 65536)
142                && credential4
143                    .as_ref()
144                    .is_some_and(|b| !b.is_empty() && b.len() <= 65536),
145            "Approved login lacks credentials"
146        );
147    }
148    Ok(PollResponse {
149        status: Some(status),
150        credential3,
151        credential4,
152        lan_challenge,
153    })
154}
155pub fn manual_auth(
156    profile: &DeviceProfile,
157    computer_name: &str,
158    c3: &[u8],
159    c4: &[u8],
160    point: &[u8],
161    key_info: &[u8],
162) -> Result<Vec<u8>> {
163    ensure!(
164        !c3.is_empty()
165            && !c4.is_empty()
166            && c3.len() <= 65536
167            && c4.len() <= 65536
168            && point.len() == 57
169            && point[0] == 4
170            && key_info.len() <= 1024 * 1024,
171        "Invalid manual auth inputs"
172    );
173    let rsa = p::message(&[
174        p::bytes(1, &p::buffer(&random::<16>()?)),
175        p::bytes(
176            2,
177            &p::message(&[p::int32(1, 713), p::bytes(2, &p::buffer(point))])?,
178        ),
179        p::bytes(3, c3),
180        p::bytes(4, c4),
181        p::bytes(5, &[]),
182    ])?;
183    let wt = p::message(&[p::bytes(1, &[]), p::bytes(4, &p::buffer(&[0]))])?;
184    let mut base = vec![p::bytes(2, &wt)];
185    if !key_info.is_empty() {
186        base.push(p::bytes(5, &p::buffer(key_info)));
187    }
188    let aes = p::message(&[
189        p::bytes(1, &base_request(profile, 0, 1)?),
190        p::bytes(2, &p::message(&base)?),
191        p::uint(5, 0),
192        p::bytes(8, text(computer_name, 255, false)?),
193        p::bytes(9, profile.device_type.as_bytes()),
194        p::bytes(11, &[]),
195        p::int32(13, 1000),
196        p::bytes(19, b"0"),
197        p::uint(22, 2),
198    ])?;
199    p::message(&[p::bytes(1, &rsa), p::bytes(2, &aes)])
200}
201pub fn auto_auth(state: &AccountState, point: &[u8], computer_name: &str) -> Result<Vec<u8>> {
202    state.validate()?;
203    ensure!(point.len() == 57 && point[0] == 4, "Invalid P224 point");
204    let token = p::decode(&state.auto_auth_key)?;
205    let part1 = p::unbuffer(p::data(&token, 1)?)?;
206    let part2 = p::unbuffer(p::data(&token, 2)?)?;
207    ensure!(
208        part1.len() == 32 && !part2.is_empty(),
209        "Unsupported saved auto-auth token"
210    );
211    let wt = p::message(&[p::bytes(4, &p::buffer(&[0]))])?;
212    let mut base_auth = vec![p::bytes(2, &wt)];
213    if !state.key_info.is_empty() {
214        base_auth.push(p::bytes(5, &p::buffer(&state.key_info)));
215    }
216    let aes = p::message(&[
217        p::bytes(1, &base_request(&state.device, state.uin, 12)?),
218        p::bytes(2, &p::message(&base_auth)?),
219        p::bytes(3, &p::buffer(&state.auto_auth_key)),
220        p::uint(6, 0),
221        p::bytes(9, text(computer_name, 255, true)?),
222        p::bytes(10, state.device.device_type.as_bytes()),
223        p::bytes(11, b"en"),
224        p::bytes(12, &[]),
225        p::bytes(19, state.device.device_type.as_bytes()),
226    ])?;
227    let rsa = p::message(&[
228        p::bytes(2, &p::buffer(&part1)),
229        p::bytes(
230            3,
231            &p::message(&[p::int32(1, 713), p::bytes(2, &p::buffer(point))])?,
232        ),
233    ])?;
234    p::message(&[p::bytes(1, &rsa), p::bytes(2, &aes)])
235}
236pub fn push_login(
237    profile: &DeviceProfile,
238    username: &str,
239    token: &[u8],
240    public_key: &[u8],
241) -> Result<Vec<u8>> {
242    ensure!(
243        !token.is_empty() && token.len() <= 1024 * 1024,
244        "Missing saved token"
245    );
246    p::message(&[
247        p::bytes(1, &base_request(profile, 0, 0)?),
248        p::bytes(2, text(username, 128, false)?),
249        p::bytes(4, &random::<32>()?),
250        p::bytes(5, &p::buffer(&random::<16>()?)),
251        p::uint(6, 3),
252        p::bytes(8, &p::buffer(token)),
253        p::bytes(10, &p::buffer(public_key)),
254        p::uint(12, 1),
255        p::bytes(13, b"en"),
256        p::uint(14, 0),
257        p::uint(15, 0),
258    ])
259}
260pub fn decode_push(input: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
261    let f = p::decode(input)?;
262    let code = p::base_code(&f)?;
263    ensure!(code == 0, "Saved-device login returned {code}");
264    let id = p::data(&f, 2)?.to_vec();
265    let key = p::unbuffer(p::data(&f, 3)?)?;
266    ensure!(
267        id.len() == 20 && key.len() == 16,
268        "Unsupported saved-device login state"
269    );
270    Ok((id, key))
271}
272
273#[derive(Clone, Serialize, Deserialize)]
274pub struct SyncCommand {
275    pub id: i32,
276    pub payload: Vec<u8>,
277}
278#[derive(Clone, Serialize, Deserialize)]
279pub struct SyncBatch {
280    pub code: i32,
281    pub flags: u32,
282    pub cursor: Vec<u8>,
283    pub commands: Vec<SyncCommand>,
284    pub continuation: bool,
285}
286pub fn sync_request(cursor: &[u8], device_type: &str, scene: u32) -> Result<Vec<u8>> {
287    ensure!(
288        !cursor.is_empty() && cursor.len() <= 65536,
289        "Invalid sync cursor"
290    );
291    p::message(&[
292        p::bytes(1, &p::uint(1, 0)),
293        p::uint(2, 7),
294        p::bytes(3, &p::buffer(cursor)),
295        p::uint(4, scene as u64),
296        p::bytes(5, text(device_type, 1024, false)?),
297        p::uint(9, 0),
298    ])
299}
300pub fn decode_sync(input: &[u8], prior: &[u8]) -> Result<SyncBatch> {
301    let f = p::decode(input)?;
302    let code = p::scalar(&f, 1)? as u32 as i32;
303    ensure!(code == 0 || code == -2006, "Sync service returned {code}");
304    let list = p::decode(p::data(&f, 2)?)?;
305    let flags = u32::try_from(p::scalar(&f, 3)?)?;
306    let incoming = p::unbuffer(p::data(&f, 4)?)?;
307    if !incoming.is_empty() {
308        decode_cursor(&incoming)?;
309    }
310    let mut commands = vec![];
311    for field in list.iter().filter(|f| f.number == 2) {
312        let p::Value::Bytes(v) = &field.value else {
313            bail!("Invalid sync command");
314        };
315        let item = p::decode(v)?;
316        commands.push(SyncCommand {
317            id: p::scalar(&item, 1)? as u32 as i32,
318            payload: p::unbuffer(p::data(&item, 2)?)?,
319        });
320    }
321    ensure!(
322        p::scalar(&list, 1)? == commands.len() as u64,
323        "Sync command count mismatch"
324    );
325    let cursor = apply_cursor(prior, &incoming, code)?;
326    let sent = if prior.is_empty() { &[0][..] } else { prior };
327    let continuation = flags & 7 != 0 && incoming != sent;
328    Ok(SyncBatch {
329        code,
330        flags,
331        cursor,
332        commands,
333        continuation,
334    })
335}
336#[derive(Clone, Serialize, Deserialize)]
337pub struct AddMessage {
338    pub from: String,
339    pub to: String,
340    pub peer: String,
341    pub sender_id: Option<String>,
342    pub outgoing: bool,
343    pub server_message_id: Option<String>,
344    pub numbered1: i32,
345    pub message_type: i32,
346    pub content: Vec<u8>,
347    pub content_text: Option<String>,
348    pub numbered6: u32,
349    pub numbered7: u32,
350    pub inline_data: Vec<u8>,
351    pub created_at: u32,
352    pub msgsource: Option<Vec<u8>>,
353    pub opaque11: Option<Vec<u8>>,
354    pub numbered13: Option<u32>,
355    /// Present field 13 correlated to official server_seq on the supported
356    /// profile (319 exact joined records). Missing remains unsupported for
357    /// official-store projection, never a fabricated sequence zero.
358    pub server_sequence: Option<u32>,
359    pub raw: Vec<u8>,
360}
361/// Decode AddMsg command 5. Fields 6/7 remain numbered. Present field 13 has a
362/// bounded official server_seq correlation; this does not enable every message
363/// class for official writes. A room's actual sender requires separate, validated
364/// group-envelope/member resolution and is deliberately unresolved here.
365pub fn decode_add_message(input: &[u8], own_username: &str) -> Result<AddMessage> {
366    let f = p::decode(input)?;
367    let sk = |n| -> Result<Vec<u8>> {
368        let nested = p::decode(p::data(&f, n)?)?;
369        Ok(p::optional_data(&nested, 1)?.unwrap_or_default().to_vec())
370    };
371    let from = String::from_utf8(sk(2)?)?;
372    let to = String::from_utf8(sk(3)?)?;
373    ensure!(
374        !from.is_empty()
375            && !to.is_empty()
376            && from.len() <= 512
377            && to.len() <= 512
378            && !own_username.is_empty(),
379        "Invalid message identities"
380    );
381    let message_type = p::scalar(&f, 4)? as u32 as i32;
382    let peer = if message_type == 10000 {
383        from.clone()
384    } else if from == own_username {
385        to.clone()
386    } else {
387        from.clone()
388    };
389    ensure!(
390        from == own_username || to == own_username || peer.ends_with("@chatroom"),
391        "Message does not match account"
392    );
393    let content = sk(5)?;
394    let content_text = String::from_utf8(content.clone()).ok();
395    let server_message_id = match p::optional(&f, 12)? {
396        Some(p::Value::Varint(v)) => {
397            if *v == 0 {
398                None
399            } else {
400                Some(v.to_string())
401            }
402        }
403        None => None,
404        _ => bail!("Invalid message server ID"),
405    };
406    let numbered13 = match p::optional(&f, 13)? {
407        Some(p::Value::Varint(v)) => Some(u32::try_from(*v)?),
408        None => None,
409        _ => bail!("Invalid numbered field13"),
410    };
411    Ok(AddMessage {
412        sender_id: (!from.ends_with("@chatroom")).then(|| from.clone()),
413        outgoing: from == own_username,
414        from,
415        to,
416        peer,
417        server_message_id,
418        numbered1: p::scalar(&f, 1)? as u32 as i32,
419        message_type,
420        content,
421        content_text,
422        numbered6: u32::try_from(p::scalar(&f, 6)?)?,
423        numbered7: u32::try_from(p::scalar(&f, 7)?)?,
424        inline_data: p::unbuffer(p::data(&f, 8)?)?,
425        created_at: u32::try_from(p::scalar(&f, 9)?)?,
426        msgsource: p::optional_data(&f, 10)?.map(Vec::from),
427        opaque11: p::optional_data(&f, 11)?.map(Vec::from),
428        numbered13,
429        server_sequence: numbered13,
430        raw: input.to_vec(),
431    })
432}
433pub fn decode_cursor(input: &[u8]) -> Result<Vec<(u32, u32)>> {
434    let f = p::decode(input)?;
435    u32::try_from(p::scalar(&f, 1)?)?;
436    let mut out = vec![];
437    for item in f.iter().filter(|f| f.number == 2) {
438        let p::Value::Bytes(v) = &item.value else {
439            bail!("Invalid cursor entry");
440        };
441        let nested = p::decode(v)?;
442        out.push((
443            u32::try_from(p::scalar(&nested, 1)?)?,
444            u32::try_from(p::scalar(&nested, 2)?)?,
445        ));
446    }
447    Ok(out)
448}
449pub fn apply_cursor(prior: &[u8], incoming: &[u8], code: i32) -> Result<Vec<u8>> {
450    if code == -2006 {
451        return Ok(incoming.to_vec());
452    }
453    ensure!(code == 0, "Unsupported sync status");
454    if incoming.is_empty() {
455        return Ok(prior.to_vec());
456    }
457    if prior.is_empty() {
458        return Ok(incoming.to_vec());
459    }
460    let mut merged = BTreeMap::new();
461    for (k, v) in decode_cursor(prior)? {
462        merged.entry(k).or_insert(v);
463    }
464    for (k, v) in decode_cursor(incoming)? {
465        let value = merged.entry(k).or_insert(0);
466        *value = (*value).max(v);
467    }
468    let mut out = vec![p::uint(1, merged.len() as u64)];
469    for (k, v) in merged {
470        out.push(p::bytes(
471            2,
472            &p::message(&[p::uint(1, k as u64), p::uint(2, v as u64)])?,
473        ));
474    }
475    p::message(&out)
476}
477#[derive(Clone, Serialize, Deserialize, Debug)]
478pub struct Heartbeat {
479    pub next_interval: u32,
480    pub selector: u32,
481}
482pub fn heartbeat_request(state: &AccountState, cursor: &[u8]) -> Result<Vec<u8>> {
483    p::message(&[
484        p::bytes(1, &base_request(&state.device, state.uin, 0)?),
485        p::uint(2, unix_time()? as u64),
486        p::bytes(3, &p::buffer(cursor)),
487    ])
488}
489pub fn decode_heartbeat(input: &[u8]) -> Result<Heartbeat> {
490    let f = p::decode(input)?;
491    let code = p::base_code(&f)?;
492    ensure!(code == 0, "Heartbeat returned {code}");
493    Ok(Heartbeat {
494        next_interval: u32::try_from(p::scalar(&f, 2)?)?,
495        selector: u32::try_from(p::scalar(&f, 3)?)?,
496    })
497}
498
499pub fn text_send_request(
500    destination: &str,
501    content: &str,
502    created_at: u32,
503    client_id: u32,
504) -> Result<Vec<u8>> {
505    text_send_request_with_source(destination, content, created_at, client_id, None)
506}
507
508pub fn text_send_request_with_source(
509    destination: &str,
510    content: &str,
511    created_at: u32,
512    client_id: u32,
513    source: Option<&str>,
514) -> Result<Vec<u8>> {
515    ensure!(
516        created_at > 0 && client_id > 0,
517        "Invalid send timestamp/client ID"
518    );
519    let mut fields = vec![
520        p::bytes(1, &p::bytes(1, text(destination, 256, false)?)),
521        p::bytes(2, text(content, 64 * 1024, false)?),
522        p::uint(3, 1),
523        p::uint(4, created_at as u64),
524        p::uint(5, client_id as u64),
525    ];
526    if let Some(source) = source.filter(|value| !value.is_empty()) {
527        fields.push(p::bytes(6, text(source, 256 * 1024, false)?));
528    }
529    let item = p::message(&fields)?;
530    p::message(&[p::uint(1, 1), p::bytes(2, &item)])
531}
532/// Native SuperFastHash of the persisted task client-ID string. Tail bytes have
533/// signed int8 semantics in the supported AMD64 profile. This is not a hash of
534/// message content and is not a server idempotency guarantee.
535pub fn client_message_id(data: &[u8]) -> u32 {
536    if data.is_empty() || data.len() > i32::MAX as usize {
537        return 0;
538    }
539    let word = |i| u16::from_le_bytes([data[i], data[i + 1]]) as u32;
540    let signed = |i| data[i] as i8 as i32 as u32;
541    let mut h = data.len() as u32;
542    let mut p = 0;
543    for _ in 0..data.len() / 4 {
544        h = h.wrapping_add(word(p));
545        let tmp = (word(p + 2) << 11) ^ h;
546        h = (h << 16) ^ tmp;
547        p += 4;
548        h = h.wrapping_add(h >> 11);
549    }
550    match data.len() & 3 {
551        3 => {
552            h = h.wrapping_add(word(p));
553            h = (h << 16) ^ h ^ (signed(p + 2) << 18);
554            h = h.wrapping_add(h >> 11);
555        }
556        2 => {
557            h = h.wrapping_add(word(p));
558            h ^= h << 11;
559            h = h.wrapping_add(h >> 17);
560        }
561        1 => {
562            h = h.wrapping_add(signed(p));
563            h ^= h << 10;
564            h = h.wrapping_add(h >> 1);
565        }
566        _ => {}
567    }
568    h ^= h << 3;
569    h = h.wrapping_add(h >> 5);
570    h ^= h << 4;
571    h = h.wrapping_add(h >> 17);
572    h ^= h << 25;
573    h.wrapping_add(h >> 6)
574}
575#[derive(Clone, Serialize, Deserialize, Debug)]
576pub struct TextSendResult {
577    pub accepted: bool,
578    pub outer_code: i32,
579    pub item_code: Option<i32>,
580    pub server_message_id: Option<String>,
581    pub returned_destination_matches: bool,
582    pub returned_client_id_matches: bool,
583}
584pub fn decode_text_send(input: &[u8], destination: &str, client_id: u32) -> Result<TextSendResult> {
585    let f = p::decode(input)?;
586    let code = p::base_code(&f)?;
587    let declared = p::scalar(&f, 2)?;
588    if code != 0 {
589        return Ok(TextSendResult {
590            accepted: false,
591            outer_code: code,
592            item_code: None,
593            server_message_id: None,
594            returned_destination_matches: false,
595            returned_client_id_matches: false,
596        });
597    }
598    let list: Vec<_> = f.iter().filter(|f| f.number == 3).collect();
599    ensure!(
600        declared == 1 && list.len() == 1,
601        "Send response cardinality differs"
602    );
603    let p::Value::Bytes(raw) = &list[0].value else {
604        bail!("Invalid send response");
605    };
606    let item = p::decode(raw)?;
607    let item_code = p::scalar(&item, 1)? as u32 as i32;
608    let returned = p::decode(p::data(&item, 2)?)?;
609    let dest = p::data(&returned, 1)?;
610    for n in 3..=7 {
611        u32::try_from(p::scalar(&item, n)?)?;
612    }
613    let id = if item_code == 0 {
614        let id = p::scalar(&item, 8)?;
615        ensure!(id != 0, "Accepted send has no server ID");
616        Some(id.to_string())
617    } else {
618        None
619    };
620    Ok(TextSendResult {
621        accepted: item_code == 0,
622        outer_code: code,
623        item_code: Some(item_code),
624        server_message_id: id,
625        returned_destination_matches: dest == destination.as_bytes(),
626        returned_client_id_matches: p::scalar(&item, 4)? == client_id as u64,
627    })
628}
629
630#[derive(Clone, Serialize, Deserialize)]
631pub struct ContactRecord {
632    pub username: String,
633    pub nickname: Option<String>,
634    pub remark: Option<String>,
635    pub alias: Option<String>,
636    pub large_avatar_url: Option<String>,
637    pub small_avatar_url: Option<String>,
638    pub raw: Vec<u8>,
639}
640pub fn contact_request(state: &AccountState, usernames: &[String]) -> Result<Vec<u8>> {
641    ensure!(
642        !usernames.is_empty() && usernames.len() <= 2,
643        "Supported contact batch is 1 or 2 identities"
644    );
645    let mut seen = HashSet::new();
646    let mut fields = vec![
647        p::bytes(1, &base_request(&state.device, state.uin, 0)?),
648        p::uint(2, usernames.len() as u64),
649    ];
650    for user in usernames {
651        ensure!(seen.insert(user), "Duplicate requested contact");
652        fields.push(p::bytes(3, &p::bytes(1, text(user, 256, false)?)));
653    }
654    fields.extend([p::uint(4, 0), p::uint(6, 0)]);
655    p::message(&fields)
656}
657fn raw_text(bytes: &[u8]) -> Result<String> {
658    Ok(std::str::from_utf8(bytes)?.to_string())
659}
660fn skstring(f: &[p::Field], n: u32) -> Result<Option<String>> {
661    let Some(v) = p::optional_data(f, n)? else {
662        return Ok(None);
663    };
664    let nested = p::decode(v)?;
665    Ok(Some(raw_text(
666        p::optional_data(&nested, 1)?.unwrap_or_default(),
667    )?))
668}
669pub fn decode_contact_record(raw: &[u8]) -> Result<ContactRecord> {
670    let f = p::decode(raw)?;
671    let username = skstring(&f, 1)?.ok_or_else(|| anyhow::anyhow!("Contact has no identity"))?;
672    ensure!(!username.is_empty(), "Empty contact identity");
673    let direct =
674        |n| -> Result<Option<String>> { p::optional_data(&f, n)?.map(raw_text).transpose() };
675    Ok(ContactRecord {
676        username,
677        nickname: skstring(&f, 2)?,
678        remark: skstring(&f, 10)?,
679        alias: direct(30)?,
680        large_avatar_url: direct(39)?,
681        small_avatar_url: direct(40)?,
682        raw: raw.to_vec(),
683    })
684}
685#[derive(Clone, Serialize, Deserialize)]
686pub struct ContactLookup {
687    pub contacts: Vec<ContactRecord>,
688    pub results: Vec<i32>,
689}
690fn contact_result_code(value: u64) -> Result<i32> {
691    let signed = value as u32 as i32;
692    ensure!(
693        value <= u32::MAX as u64 || (signed < 0 && value == signed as i64 as u64),
694        "Invalid per-contact int32 result"
695    );
696    Ok(signed)
697}
698pub fn decode_contacts(input: &[u8], requested: &[String]) -> Result<ContactLookup> {
699    ensure!(
700        input.len() <= crate::MAX_MESSAGE_BYTES,
701        "Contact response exceeds bound"
702    );
703    let f = p::decode(input)?;
704    let code = p::base_code(&f)?;
705    ensure!(code == 0, "Contact service returned {code}");
706    let mut slots = vec![];
707    let mut results = vec![];
708    for field in &f {
709        match (field.number, &field.value) {
710            (3, p::Value::Bytes(raw)) => slots.push(raw.as_slice()),
711            (4, p::Value::Varint(n)) => results.push(contact_result_code(*n)?),
712            (4, p::Value::Bytes(raw)) => {
713                let mut offset = 0;
714                while offset < raw.len() {
715                    results.push(contact_result_code(p::read_varint(raw, &mut offset)?)?);
716                }
717            }
718            (3 | 4, _) => bail!("Invalid contact response field"),
719            _ => {}
720        }
721    }
722    // The declared count includes default ModContact placeholders for rejected
723    // slots. Count the wire slots before interpreting their contact identities.
724    ensure!(
725        p::scalar(&f, 2)? == slots.len() as u64 && slots.len() <= requested.len(),
726        "Contact count mismatch"
727    );
728    ensure!(
729        results.is_empty() || results.len() == requested.len(),
730        "Contact result count mismatch"
731    );
732    let aligned = slots.len() == requested.len() && results.len() == requested.len();
733    let mut contacts = vec![];
734    let mut seen = HashSet::new();
735    let mut omitted_unavailable = false;
736    for (index, raw) in slots.iter().enumerate() {
737        let fields = p::decode(raw)?;
738        let identity = p::optional_data(&fields, 1)?;
739        // Observed service code1 accompanies an explicitly present, empty
740        // SKString slot. It rejects this lookup; it does not prove deletion or
741        // permanent unavailability. Other empty/missing/unknown cases fail.
742        if identity == Some(&[][..]) && aligned && results[index] == 1 {
743            omitted_unavailable = true;
744            continue;
745        }
746        let record = decode_contact_record(raw)?;
747        ensure!(
748            requested.contains(&record.username) && seen.insert(record.username.clone()),
749            "Unrequested or duplicate contact"
750        );
751        contacts.push((index, record));
752    }
753    if omitted_unavailable {
754        ensure!(
755            contacts
756                .iter()
757                .all(|(index, record)| requested.get(*index) == Some(&record.username)),
758            "Unavailable contact slot alignment mismatch"
759        );
760    }
761    Ok(ContactLookup {
762        contacts: contacts.into_iter().map(|(_, record)| record).collect(),
763        results,
764    })
765}
766
767#[derive(Clone, Serialize, Deserialize)]
768pub struct RoomMember {
769    pub username: String,
770    pub nickname: Option<String>,
771    pub room_display_name: Option<String>,
772    pub raw: Vec<u8>,
773    #[serde(default)]
774    pub flags: u32,
775    #[serde(default)]
776    pub opaque: Option<Vec<u8>>,
777}
778#[derive(Clone, Serialize, Deserialize)]
779pub struct RoomRoster {
780    pub room_id: String,
781    pub version: u32,
782    pub members: Vec<RoomMember>,
783    #[serde(default)]
784    pub full_member_ids: Option<Vec<String>>,
785    #[serde(default)]
786    pub owner: Option<String>,
787    /// None preserves the cached list; Some replaces it, including an empty list.
788    #[serde(default)]
789    pub administrator_ids: Option<Vec<String>>,
790}
791pub fn room_request(state: &AccountState, room_id: &str, version: u32) -> Result<Vec<u8>> {
792    ensure!(room_id.ends_with("@chatroom"), "Invalid room identity");
793    p::message(&[
794        p::bytes(1, &base_request(&state.device, state.uin, 0)?),
795        p::bytes(2, text(room_id, 256, false)?),
796        p::uint(3, version as u64),
797    ])
798}
799pub fn decode_room(input: &[u8], room_id: &str) -> Result<RoomRoster> {
800    let f = p::decode(input)?;
801    let code = p::base_code(&f)?;
802    ensure!(code == 0, "Room service returned {code}");
803    if let Some(echo) = p::optional_data(&f, 2)? {
804        ensure!(echo == room_id.as_bytes(), "Foreign room reply");
805    }
806    let version = u32::try_from(p::scalar(&f, 3)?)?;
807    let member_data = p::decode(p::data(&f, 4)?)?;
808    let mut members = vec![];
809    let mut seen = HashSet::new();
810    for field in member_data.iter().filter(|f| f.number == 2) {
811        let p::Value::Bytes(raw) = &field.value else {
812            bail!("Invalid member field");
813        };
814        let m = p::decode(raw)?;
815        let username = raw_text(p::data(&m, 1)?)?;
816        ensure!(
817            !username.is_empty() && seen.insert(username.clone()),
818            "Invalid member identity"
819        );
820        members.push(RoomMember {
821            username,
822            nickname: p::optional_data(&m, 2)?.map(raw_text).transpose()?,
823            room_display_name: p::optional_data(&m, 3)?.map(raw_text).transpose()?,
824            raw: raw.clone(),
825            flags: u32::try_from(p::scalar(&m, 6)?)?,
826            opaque: p::optional_data(&m, 7)?.map(Vec::from),
827        });
828    }
829    match p::optional(&member_data, 1)? {
830        Some(p::Value::Varint(n)) => ensure!(*n == members.len() as u64, "Member count mismatch"),
831        None => {}
832        _ => bail!("Invalid member count"),
833    }
834    let mut full_ids = vec![];
835    let mut full_seen = HashSet::new();
836    for field in f.iter().filter(|v| v.number == 7) {
837        let p::Value::Bytes(raw) = &field.value else {
838            bail!("Invalid full member identity");
839        };
840        let name = raw_text(p::data(&p::decode(raw)?, 1)?)?;
841        ensure!(
842            !name.is_empty()
843                && name.len() <= 256
844                && !name.chars().any(|c| c.is_control() || c.is_whitespace())
845                && full_seen.insert(name.clone())
846                && full_ids.len() < 10000,
847            "Invalid full member identity"
848        );
849        full_ids.push(name);
850    }
851    let full_member_ids = match p::optional(&f, 6)? {
852        Some(p::Value::Varint(n)) => {
853            ensure!(
854                u32::try_from(*n)? as usize == full_ids.len(),
855                "Full member count mismatch"
856            );
857            Some(full_ids)
858        }
859        None if full_ids.is_empty() => None,
860        _ => bail!("Invalid full member count"),
861    };
862    let administrator_control = match p::optional(&f, 8)? {
863        None => 0,
864        Some(p::Value::Varint(value)) => u32::try_from(*value)?,
865        _ => bail!("Invalid administrator control"),
866    };
867    let mut administrator_ids = vec![];
868    let mut administrator_seen = HashSet::new();
869    for (index, field) in f.iter().filter(|v| v.number == 9).enumerate() {
870        ensure!(index < 10000, "Excessive administrator identities");
871        let p::Value::Bytes(raw) = &field.value else {
872            bail!("Invalid administrator identity");
873        };
874        let nested = p::decode(raw)?;
875        let name = raw_text(p::optional_data(&nested, 1)?.unwrap_or_default())?;
876        // Empty SKString entries are sentinels, not administrator identities.
877        if name.is_empty() {
878            continue;
879        }
880        ensure!(
881            name.len() <= 256
882                && !name.chars().any(|c| c.is_control() || c.is_whitespace())
883                && administrator_seen.insert(name.clone()),
884            "Invalid administrator identity"
885        );
886        administrator_ids.push(name);
887    }
888    // The supported native consumer uses field 8 as a nonzero replacement
889    // control, not as a count of the field 9 SKString entries.
890    let administrator_ids = (administrator_control != 0).then_some(administrator_ids);
891    let owner = p::optional_data(&f, 5)?
892        .map(raw_text)
893        .transpose()?
894        .filter(|s| !s.is_empty());
895    Ok(RoomRoster {
896        room_id: room_id.to_string(),
897        version,
898        members,
899        full_member_ids,
900        owner,
901        administrator_ids,
902    })
903}
904
905#[derive(Clone, Serialize, Deserialize)]
906pub struct InitContactPage {
907    pub contact_sequence: u32,
908    pub room_sequence: u32,
909    pub more: bool,
910    pub usernames: Vec<String>,
911}
912pub fn init_contacts_request(
913    username: &str,
914    contact_sequence: u32,
915    room_sequence: u32,
916) -> Result<Vec<u8>> {
917    p::message(&[
918        p::bytes(1, text(username, 256, false)?),
919        p::uint(2, contact_sequence as u64),
920        p::uint(3, room_sequence as u64),
921    ])
922}
923pub fn decode_init_contacts(input: &[u8]) -> Result<InitContactPage> {
924    let f = p::decode(input)?;
925    let code = p::base_code(&f)?;
926    ensure!(code == 0, "Contact bootstrap returned {code}");
927    let optional_scalar = |n| -> Result<u32> {
928        match p::optional(&f, n)? {
929            Some(p::Value::Varint(v)) => Ok(u32::try_from(*v)?),
930            None => Ok(0),
931            _ => bail!("Invalid contact cursor field"),
932        }
933    };
934    let mut seen = HashSet::new();
935    let mut usernames = vec![];
936    for field in f.iter().filter(|f| f.number == 5) {
937        let p::Value::Bytes(raw) = &field.value else {
938            bail!("Invalid contact identifier");
939        };
940        let user = raw_text(raw)?;
941        ensure!(
942            !user.is_empty()
943                && user.len() <= 512
944                && !user.chars().any(|c| c.is_control() || c.is_whitespace())
945                && seen.insert(user.clone())
946                && usernames.len() < 10000,
947            "Invalid contact bootstrap identity"
948        );
949        usernames.push(user);
950    }
951    Ok(InitContactPage {
952        contact_sequence: optional_scalar(2)?,
953        room_sequence: optional_scalar(3)?,
954        more: optional_scalar(4)? != 0,
955        usernames,
956    })
957}
958#[derive(Clone, Serialize, Deserialize)]
959pub struct AvatarBytes {
960    pub username: String,
961    pub bytes: Vec<u8>,
962}
963pub fn avatar_request(state: &AccountState, usernames: &[String]) -> Result<Vec<u8>> {
964    ensure!(
965        !usernames.is_empty() && usernames.len() <= 2,
966        "Supported avatar batch is 1 or 2 identities"
967    );
968    let mut parts = vec![
969        p::bytes(1, &base_request(&state.device, state.uin, 0)?),
970        p::uint(2, usernames.len() as u64),
971    ];
972    let mut seen = HashSet::new();
973    for user in usernames {
974        ensure!(seen.insert(user), "Duplicate avatar identity");
975        parts.push(p::bytes(3, &p::bytes(1, text(user, 256, false)?)));
976    }
977    p::message(&parts)
978}
979/// Returns exact bounded bytes. The service must fully decode the image before
980/// treating this response as a displayable avatar or storing an image mime type.
981pub fn decode_avatars(input: &[u8], requested: &[String]) -> Result<Vec<AvatarBytes>> {
982    let f = p::decode(input)?;
983    let code = p::base_code(&f)?;
984    ensure!(code == 0, "Avatar service returned {code}");
985    let mut out = vec![];
986    let mut seen = HashSet::new();
987    for field in f.iter().filter(|f| f.number == 3) {
988        let p::Value::Bytes(raw) = &field.value else {
989            bail!("Invalid avatar item");
990        };
991        let pair = p::decode(raw)?;
992        let username =
993            skstring(&pair, 2)?.ok_or_else(|| anyhow::anyhow!("Missing avatar identity"))?;
994        ensure!(
995            requested.contains(&username) && seen.insert(username.clone()),
996            "Foreign or duplicate avatar"
997        );
998        let bytes = p::unbuffer(p::data(&pair, 1)?)?;
999        ensure!(
1000            !bytes.is_empty() && bytes.len() <= 2 * 1024 * 1024,
1001            "Avatar bytes outside limit"
1002        );
1003        out.push(AvatarBytes { username, bytes });
1004    }
1005    ensure!(
1006        p::scalar(&f, 2)? == out.len() as u64 && out.len() <= requested.len(),
1007        "Avatar count mismatch"
1008    );
1009    Ok(out)
1010}
1011#[derive(Clone, Serialize, Deserialize)]
1012pub struct VoiceChunk {
1013    pub offset: u32,
1014    pub next_offset: u32,
1015    pub total_length: u32,
1016    pub complete: bool,
1017    pub bytes: Vec<u8>,
1018    pub echoed_server_id: Option<String>,
1019    pub offset_matches: bool,
1020}
1021fn voice_range(total: u32, offset: u32) -> Result<u32> {
1022    ensure!(
1023        total > 0 && total <= crate::MAX_MESSAGE_BYTES as u32 && offset < total,
1024        "Invalid voice range"
1025    );
1026    Ok((total - offset).min(32768))
1027}
1028pub fn voice_request(
1029    state: &AccountState,
1030    server_id: u64,
1031    total: u32,
1032    offset: u32,
1033) -> Result<Vec<u8>> {
1034    ensure!(server_id != 0, "Missing voice server ID");
1035    let length = voice_range(total, offset)?;
1036    p::message(&[
1037        p::uint(1, 0),
1038        p::uint(2, offset as u64),
1039        p::uint(3, length as u64),
1040        p::bytes(5, &base_request(&state.device, state.uin, 0)?),
1041        p::uint(6, server_id),
1042    ])
1043}
1044pub fn decode_voice(input: &[u8], server_id: u64, total: u32, offset: u32) -> Result<VoiceChunk> {
1045    let requested = voice_range(total, offset)?;
1046    let f = p::decode(input)?;
1047    let base = p::decode(p::data(&f, 9)?)?;
1048    let code = p::scalar(&base, 1)? as u32 as i32;
1049    ensure!(code == 0, "Voice service returned {code}");
1050    for n in [1, 2, 3, 5, 8] {
1051        u32::try_from(p::scalar(&f, n)?)?;
1052    }
1053    let bytes = p::unbuffer(p::data(&f, 7)?)?;
1054    ensure!(
1055        p::scalar(&f, 3)? == bytes.len() as u64
1056            && !bytes.is_empty()
1057            && bytes.len() <= requested as usize,
1058        "Voice response length invalid"
1059    );
1060    let echoed = match p::optional(&f, 11)? {
1061        Some(p::Value::Varint(id)) => Some(*id),
1062        None => None,
1063        _ => bail!("Invalid voice server ID echo"),
1064    };
1065    ensure!(
1066        echoed.is_none_or(|id| id == server_id),
1067        "Foreign voice server ID"
1068    );
1069    let next_offset = offset + bytes.len() as u32;
1070    ensure!(next_offset <= total, "Voice chunk overflow");
1071    Ok(VoiceChunk {
1072        offset,
1073        next_offset,
1074        total_length: total,
1075        complete: next_offset == total,
1076        bytes,
1077        echoed_server_id: echoed.map(|v| v.to_string()),
1078        offset_matches: p::scalar(&f, 2)? == offset as u64,
1079    })
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084    use super::*;
1085    #[test]
1086    fn room_delta_count_is_independent_of_complete_identity_list() {
1087        let member = p::message(&[
1088            p::bytes(1, b"changed"),
1089            p::uint(6, 0x2000),
1090            p::bytes(7, b"opaque"),
1091        ])
1092        .unwrap();
1093        let roster = |nested: Vec<u8>, extra: Vec<Vec<u8>>| {
1094            let mut fields = vec![
1095                p::bytes(1, &p::uint(1, 0)),
1096                p::uint(3, 7),
1097                p::bytes(4, &nested),
1098            ];
1099            fields.extend(extra);
1100            p::message(&fields).unwrap()
1101        };
1102        let nested = p::message(&[p::uint(1, 1), p::bytes(2, &member)]).unwrap();
1103        let full = vec![
1104            p::uint(6, 2),
1105            p::bytes(7, &p::bytes(1, b"changed")),
1106            p::bytes(7, &p::bytes(1, b"unchanged")),
1107        ];
1108        let decoded = decode_room(&roster(nested.clone(), full.clone()), "room@chatroom").unwrap();
1109        assert_eq!(decoded.members.len(), 1);
1110        assert_eq!(decoded.members[0].flags, 0x2000);
1111        assert_eq!(
1112            decoded.members[0].opaque.as_deref(),
1113            Some(b"opaque".as_slice())
1114        );
1115        assert_eq!(decoded.full_member_ids.unwrap(), ["changed", "unchanged"]);
1116        assert!(decode_room(
1117            &roster(nested.clone(), vec![p::uint(6, 2)]),
1118            "room@chatroom"
1119        )
1120        .is_err());
1121        assert!(decode_room(&roster(p::bytes(1, b"wrong type"), vec![]), "room@chatroom").is_err());
1122        assert!(decode_room(
1123            &roster(p::bytes(2, &p::bytes(1, b"missing-flags")), vec![]),
1124            "room@chatroom"
1125        )
1126        .is_err());
1127        let mut administrator_clear = full;
1128        administrator_clear.push(p::uint(8, 1));
1129        assert_eq!(
1130            decode_room(
1131                &roster(nested.clone(), administrator_clear),
1132                "room@chatroom"
1133            )
1134            .unwrap()
1135            .administrator_ids,
1136            Some(vec![])
1137        );
1138        assert!(decode_room(&roster(nested, vec![]), "room@chatroom")
1139            .unwrap()
1140            .full_member_ids
1141            .is_none());
1142    }
1143
1144    fn room_administrator_fixture(extra: &[u8]) -> Vec<u8> {
1145        // CGI551 response: BaseResponse{ret=0}, version=7,
1146        // ChatRoomMemberData{member_count=0}. These tags are independent of
1147        // the codec under test and retain an empty changed-member list.
1148        let mut raw = vec![0x0a, 0x02, 0x08, 0x00, 0x18, 0x07, 0x22, 0x02, 0x08, 0x00];
1149        raw.extend_from_slice(extra);
1150        raw
1151    }
1152
1153    #[test]
1154    fn room_administrator_control_replaces_list_without_count_semantics() {
1155        // Field 8=42; repeated field 9 SKString values: alice, empty, bob.
1156        // The native consumer filters empty strings and replaces the list
1157        // whenever field 8 is nonzero, without comparing it to a count.
1158        let raw = room_administrator_fixture(&[
1159            0x40, 0x2a, 0x4a, 0x07, 0x0a, 0x05, b'a', b'l', b'i', b'c', b'e', 0x4a, 0x00, 0x4a,
1160            0x05, 0x0a, 0x03, b'b', b'o', b'b',
1161        ]);
1162        let roster = decode_room(&raw, "room@chatroom").unwrap();
1163        assert_eq!(roster.administrator_ids.unwrap(), ["alice", "bob"]);
1164        assert!(roster.members.is_empty());
1165        assert!(roster.full_member_ids.is_none());
1166    }
1167
1168    #[test]
1169    fn room_administrator_absent_or_zero_control_preserves_list() {
1170        for extra in [
1171            vec![],
1172            vec![0x40, 0x00],
1173            vec![0x4a, 0x07, 0x0a, 0x05, b'a', b'l', b'i', b'c', b'e'],
1174            vec![
1175                0x40, 0x00, 0x4a, 0x07, 0x0a, 0x05, b'a', b'l', b'i', b'c', b'e',
1176            ],
1177        ] {
1178            assert!(
1179                decode_room(&room_administrator_fixture(&extra), "room@chatroom")
1180                    .unwrap()
1181                    .administrator_ids
1182                    .is_none()
1183            );
1184        }
1185        // Persisted metadata receipts from before this field was understood
1186        // preserve the cached administrator list when deserialized.
1187        let old: RoomRoster = serde_json::from_value(serde_json::json!({
1188            "room_id":"room@chatroom", "version":7, "members":[]
1189        }))
1190        .unwrap();
1191        assert!(old.administrator_ids.is_none());
1192    }
1193
1194    #[test]
1195    fn room_administrator_control_clears_absent_and_empty_entries() {
1196        for extra in [
1197            vec![0x40, 0x01],
1198            vec![0x40, 0x01, 0x4a, 0x00],
1199            vec![0x40, 0x01, 0x4a, 0x02, 0x0a, 0x00],
1200        ] {
1201            assert_eq!(
1202                decode_room(&room_administrator_fixture(&extra), "room@chatroom")
1203                    .unwrap()
1204                    .administrator_ids,
1205                Some(vec![])
1206            );
1207        }
1208    }
1209
1210    #[test]
1211    fn room_administrator_fields_reject_malformed_and_unbounded_values() {
1212        let string = |value: &[u8]| p::bytes(9, &p::bytes(1, value));
1213        let repeated = string(b"alice");
1214        for extra in [
1215            p::bytes(8, &[]),
1216            p::uint(8, u64::from(u32::MAX) + 1),
1217            [p::uint(8, 1), p::uint(8, 1)].concat(),
1218            p::uint(9, 1),
1219            p::bytes(9, &[0x0a, 0x01]),
1220            p::bytes(9, &p::uint(1, 1)),
1221            p::bytes(9, &[p::bytes(1, b"a"), p::bytes(1, b"b")].concat()),
1222            string(&[0xff]),
1223            string(b"alice bob"),
1224            string(b"alice\n"),
1225            string(&[b'a'; 257]),
1226            [repeated.clone(), repeated].concat(),
1227            p::bytes(9, &[]).repeat(10001),
1228        ] {
1229            assert!(decode_room(&room_administrator_fixture(&extra), "room@chatroom").is_err());
1230        }
1231    }
1232    fn cursor(k: u64, v: u64) -> Vec<u8> {
1233        p::message(&[
1234            p::uint(1, 1),
1235            p::bytes(2, &p::message(&[p::uint(1, k), p::uint(2, v)]).unwrap()),
1236        ])
1237        .unwrap()
1238    }
1239    #[test]
1240    fn explicit_code1_contact_slot_does_not_hide_successful_neighbor() {
1241        let requested = vec!["unavailable".into(), "available".into()];
1242        let empty = p::bytes(1, &[]);
1243        let available = p::bytes(1, &p::bytes(1, b"available"));
1244        for codes in [
1245            p::bytes(4, &[1, 0]),
1246            p::message(&[p::uint(4, 1), p::uint(4, 0)]).unwrap(),
1247        ] {
1248            let raw = p::message(&[
1249                p::bytes(1, &p::uint(1, 0)),
1250                p::uint(2, 2),
1251                p::bytes(3, &empty),
1252                p::bytes(3, &available),
1253                codes,
1254            ])
1255            .unwrap();
1256            let result = decode_contacts(&raw, &requested).unwrap();
1257            assert_eq!(result.results, vec![1, 0]);
1258            assert_eq!(result.contacts.len(), 1);
1259            assert_eq!(result.contacts[0].username, "available");
1260            assert!(decode_contact_record(&empty).is_err());
1261        }
1262    }
1263    #[test]
1264    fn contact_result_overflow_cannot_alias_success_or_unavailable() {
1265        let requested = vec!["unavailable".into(), "available".into()];
1266        let empty = p::bytes(1, &[]);
1267        let available = p::bytes(1, &p::bytes(1, b"available"));
1268        for overflow in [1u64 << 32, (1u64 << 32) + 1, (1u64 << 32) + u32::MAX as u64] {
1269            let packed = p::uint(1, overflow)[1..].to_vec();
1270            for codes in [
1271                p::message(&[p::uint(4, overflow), p::uint(4, 0)]).unwrap(),
1272                p::bytes(4, &[packed.as_slice(), &[0]].concat()),
1273            ] {
1274                let raw = p::message(&[
1275                    p::bytes(1, &p::uint(1, 0)),
1276                    p::uint(2, 2),
1277                    p::bytes(3, &empty),
1278                    p::bytes(3, &available),
1279                    codes,
1280                ])
1281                .unwrap();
1282                assert!(decode_contacts(&raw, &requested).is_err());
1283            }
1284        }
1285        assert_eq!(contact_result_code(0).unwrap(), 0);
1286        assert_eq!(contact_result_code(1).unwrap(), 1);
1287        assert_eq!(contact_result_code(u32::MAX as u64).unwrap(), -1);
1288        assert_eq!(contact_result_code(u64::MAX).unwrap(), -1);
1289        assert_eq!(
1290            contact_result_code(i32::MIN as i64 as u64).unwrap(),
1291            i32::MIN
1292        );
1293    }
1294    #[test]
1295    fn unexplained_empty_contact_slots_and_unproved_alignment_remain_errors() {
1296        let ids = vec!["unavailable".into(), "available".into()];
1297        let empty = p::bytes(1, &[]);
1298        let available = p::bytes(1, &p::bytes(1, b"available"));
1299        for codes in [vec![], p::bytes(4, &[0, 0]), p::bytes(4, &[2, 0])] {
1300            let raw = p::message(&[
1301                p::bytes(1, &p::uint(1, 0)),
1302                p::uint(2, 2),
1303                p::bytes(3, &empty),
1304                p::bytes(3, &available),
1305                codes,
1306            ])
1307            .unwrap();
1308            assert!(decode_contacts(&raw, &ids).is_err());
1309        }
1310        let partial = p::message(&[
1311            p::bytes(1, &p::uint(1, 0)),
1312            p::uint(2, 1),
1313            p::bytes(3, &empty),
1314            p::bytes(4, &[1, 0]),
1315        ])
1316        .unwrap();
1317        assert!(decode_contacts(&partial, &ids).is_err());
1318        let wrong = p::message(&[
1319            p::bytes(1, &p::uint(1, 0)),
1320            p::uint(2, 2),
1321            p::bytes(3, &empty),
1322            p::bytes(3, &p::bytes(1, &p::bytes(1, b"unavailable"))),
1323            p::bytes(4, &[1, 0]),
1324        ])
1325        .unwrap();
1326        assert!(decode_contacts(&wrong, &ids).is_err());
1327        let absent = p::message(&[
1328            p::bytes(1, &p::uint(1, 0)),
1329            p::uint(2, 2),
1330            p::bytes(3, &[]),
1331            p::bytes(3, &available),
1332            p::bytes(4, &[1, 0]),
1333        ])
1334        .unwrap();
1335        assert!(decode_contacts(&absent, &ids).is_err());
1336    }
1337    #[test]
1338    fn cursor_merge_and_reset() {
1339        assert_eq!(
1340            decode_cursor(&apply_cursor(&cursor(1, 9), &cursor(1, 2), 0).unwrap()).unwrap(),
1341            vec![(1, 9)]
1342        );
1343        assert_eq!(
1344            apply_cursor(&cursor(1, 9), &cursor(1, 2), -2006).unwrap(),
1345            cursor(1, 2)
1346        );
1347        assert!(apply_cursor(&[], &[], 3).is_err());
1348    }
1349    #[test]
1350    fn send_u64_ids_lossless() {
1351        let item = p::message(&[
1352            p::uint(1, 0),
1353            p::bytes(2, &p::bytes(1, b"filehelper")),
1354            p::uint(3, 0),
1355            p::uint(4, 99),
1356            p::uint(5, 0),
1357            p::uint(6, 0),
1358            p::uint(7, 0),
1359            p::uint(8, u64::MAX),
1360        ])
1361        .unwrap();
1362        let response = p::message(&[
1363            p::bytes(1, &p::uint(1, 0)),
1364            p::uint(2, 1),
1365            p::bytes(3, &item),
1366        ])
1367        .unwrap();
1368        assert_eq!(
1369            decode_text_send(&response, "filehelper", 99)
1370                .unwrap()
1371                .server_message_id,
1372            Some(u64::MAX.to_string())
1373        );
1374    }
1375    #[test]
1376    fn foreign_room_rejected() {
1377        let response = p::message(&[
1378            p::bytes(1, &p::uint(1, 0)),
1379            p::bytes(2, b"foreign@chatroom"),
1380            p::uint(3, 0),
1381            p::bytes(4, &p::uint(1, 0)),
1382        ])
1383        .unwrap();
1384        assert!(decode_room(&response, "known@chatroom").is_err());
1385    }
1386    #[test]
1387    fn native_client_id_vectors_include_signed_tail_bytes() {
1388        for (data, expected) in [
1389            (b"".to_vec(), 0),
1390            (b"a".to_vec(), 291415938),
1391            (b"abc".to_vec(), 3535673738),
1392            (b"native_test_0123456789".to_vec(), 483784200),
1393            (vec![255], 0),
1394            (vec![1, 2, 255], 2571803832),
1395            ((0..=255).collect(), 3840866583),
1396        ] {
1397            assert_eq!(client_message_id(&data), expected);
1398        }
1399    }
1400    #[test]
1401    fn text_source_is_optional_length_delimited_item_field_six() {
1402        let request = text_send_request_with_source("r", "x", 1, 2, Some("<msgsource/>")).unwrap();
1403        // One outer item; its nested destination is field 1, scalars 3/4/5
1404        // remain varints, and the source uses tag 0x32 (field 6, wire 2).
1405        assert_eq!(
1406            hex::encode(request),
1407            "0801121c0a030a0172120178180120012802320c3c6d7367736f757263652f3e"
1408        );
1409        let plain = text_send_request("r", "x", 1, 2).unwrap();
1410        assert_eq!(
1411            plain,
1412            text_send_request_with_source("r", "x", 1, 2, None).unwrap()
1413        );
1414        assert_eq!(
1415            plain,
1416            text_send_request_with_source("r", "x", 1, 2, Some("")).unwrap()
1417        );
1418        assert!(
1419            text_send_request_with_source("r", "x", 1, 2, Some(&"x".repeat(256 * 1024 + 1)))
1420                .is_err()
1421        );
1422    }
1423    #[test]
1424    fn addmsg_preserves_numbered_fields_and_opaque_source() {
1425        let payload = p::message(&[
1426            p::uint(1, 7),
1427            p::bytes(2, &p::bytes(1, b"room@chatroom")),
1428            p::bytes(3, &p::bytes(1, b"synthetic-self")),
1429            p::uint(4, 1),
1430            p::bytes(5, &p::bytes(1, b"synthetic content")),
1431            p::uint(6, 123),
1432            p::uint(7, 456),
1433            p::bytes(8, &p::buffer(&[])),
1434            p::uint(9, 999),
1435            p::bytes(10, b"<msgsource/>"),
1436            p::bytes(11, &[255, 0]),
1437            p::uint(12, u64::MAX),
1438            p::uint(13, 789),
1439        ])
1440        .unwrap();
1441        let decoded = decode_add_message(&payload, "synthetic-self").unwrap();
1442        assert_eq!(decoded.peer, "room@chatroom");
1443        assert!(decoded.sender_id.is_none());
1444        assert_eq!(decoded.server_message_id, Some(u64::MAX.to_string()));
1445        assert_eq!(decoded.numbered13, Some(789));
1446        assert_eq!(decoded.opaque11, Some(vec![255, 0]));
1447        assert_eq!(decoded.raw, payload);
1448    }
1449}