Skip to main content

wechat_protocol/
contact_sync.rs

1//! Linux 4.1.13.26 contact stream (CGI8674). This stream has its own
2//! `rs_config`/`sync_11` cursor; the message SyncKey is only request context.
3use crate::{protobuf as p, wire, AccountState, NativeClient};
4use anyhow::{bail, ensure, Result};
5use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7
8pub const CONTACT_SELECTOR: u64 = 0x20004;
9
10#[derive(Clone, Serialize, Deserialize)]
11pub struct ContactSyncBatch {
12    pub cursor: Vec<u8>,
13    pub continuation: Vec<u64>,
14    pub commands: Vec<wire::SyncCommand>,
15    pub raw: Vec<u8>,
16}
17
18/// SyncKeyBuffer: repeated field1 SyncKeyPair { key1:u32, value2:u64 }.
19/// Keep the supplied encoding intact, including unknown fields, for publication.
20pub fn validate_cursor(input: &[u8]) -> Result<()> {
21    ensure!(input.len() <= 65536, "Contact cursor exceeds limit");
22    let fields = p::decode(input)?;
23    let mut keys = HashSet::new();
24    for field in fields.iter().filter(|field| field.number == 1) {
25        let p::Value::Bytes(raw) = &field.value else {
26            bail!("Invalid contact cursor pair");
27        };
28        let pair = p::decode(raw)?;
29        let key = u32::try_from(p::scalar(&pair, 1)?)?;
30        p::scalar(&pair, 2)?;
31        ensure!(
32            keys.insert(key) && keys.len() <= 256,
33            "Invalid contact cursor keys"
34        );
35    }
36    Ok(())
37}
38
39pub fn request(
40    state: &AccountState,
41    cursor: &[u8],
42    message_cursor: &[u8],
43    continuation: bool,
44) -> Result<Vec<u8>> {
45    validate_cursor(cursor)?;
46    ensure!(
47        message_cursor.len() <= 65536,
48        "Message cursor exceeds limit"
49    );
50    ensure!(
51        !cursor.is_empty() || !message_cursor.is_empty(),
52        "Contact sync has no cursor context"
53    );
54    // The drain at 0x6202310 uses scene3 initially and scene4 after a response
55    // with nonzero continuation selectors. Request3 is repeated uint64.
56    let mut fields = vec![
57        p::bytes(1, &wire::base_request(&state.device, state.uin, 0)?),
58        p::uint(2, if continuation { 4 } else { 3 }),
59        p::uint(3, CONTACT_SELECTOR),
60        p::bytes(4, cursor),
61    ];
62    fields.push(p::bytes(5, message_cursor));
63    p::message(&fields)
64}
65
66pub fn decode(input: &[u8]) -> Result<ContactSyncBatch> {
67    ensure!(
68        input.len() <= 16 * 1024 * 1024,
69        "Contact response exceeds limit"
70    );
71    let fields = p::decode(input)?;
72    let code = p::base_code(&fields)?;
73    ensure!(code == 0, "Contact sync service returned {code}");
74    let cursor = p::data(&fields, 3)?.to_vec();
75    validate_cursor(&cursor)?;
76    let mut continuation = Vec::new();
77    let mut commands = Vec::new();
78    for field in fields {
79        match (field.number, field.value) {
80            (2, p::Value::Varint(value)) => {
81                continuation.push(value);
82                ensure!(
83                    continuation.len() <= 64,
84                    "Contact continuation exceeds limit"
85                );
86            }
87            (4, p::Value::Bytes(raw)) => {
88                let item = p::decode(&raw)?;
89                // This stream carries raw CmdItem payloads; NewSync uses SKBuffer.
90                commands.push(wire::SyncCommand {
91                    id: i32::try_from(p::scalar(&item, 1)?)?,
92                    payload: p::data(&item, 2)?.to_vec(),
93                });
94                ensure!(commands.len() <= 10000, "Contact command limit");
95            }
96            (2 | 4, _) => bail!("Invalid contact response field type"),
97            _ => {}
98        }
99    }
100    Ok(ContactSyncBatch {
101        cursor,
102        continuation,
103        commands,
104        raw: input.to_vec(),
105    })
106}
107
108impl NativeClient {
109    pub fn contact_sync(
110        &self,
111        state: &mut AccountState,
112        cursor: &[u8],
113        message_cursor: &[u8],
114        continuation: bool,
115    ) -> Result<ContactSyncBatch> {
116        let request = request(state, cursor, message_cursor, continuation)?;
117        decode(&self.authenticated_rpc(
118            state,
119            8674,
120            "/cgi-bin/micromsg-bin/contactsync",
121            &request,
122        )?)
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    fn pair(key: u64, value: u64) -> Vec<u8> {
130        p::bytes(
131            1,
132            &p::message(&[p::uint(1, key), p::uint(2, value)]).unwrap(),
133        )
134    }
135    #[test]
136    fn cursor_values_keep_u64_and_reject_duplicate_or_malformed_keys() {
137        validate_cursor(&pair(11, u64::MAX)).unwrap();
138        assert!(validate_cursor(&[pair(11, 1), pair(11, 2)].concat()).is_err());
139        assert!(validate_cursor(&p::uint(1, 11)).is_err());
140        assert!(validate_cursor(&pair(u32::MAX as u64 + 1, 0)).is_err());
141    }
142    #[test]
143    fn final_and_continuing_pages_retain_unknown_commands_and_raw_bytes() {
144        for flags in [vec![0], vec![0, CONTACT_SELECTOR]] {
145            let mut fields = vec![
146                p::bytes(1, &p::message(&[p::int32(1, 0)]).unwrap()),
147                p::bytes(3, &pair(11, 7)),
148                p::bytes(
149                    4,
150                    &p::message(&[p::uint(1, 912), p::bytes(2, b"unknown")]).unwrap(),
151                ),
152            ];
153            fields.extend(flags.iter().map(|value| p::uint(2, *value)));
154            let raw = p::message(&fields).unwrap();
155            let batch = decode(&raw).unwrap();
156            assert_eq!(batch.continuation, flags);
157            assert_eq!(batch.commands[0].id, 912);
158            assert_eq!(batch.commands[0].payload, b"unknown");
159            assert_eq!(batch.raw, raw);
160        }
161    }
162}