Skip to main content

wechat_protocol/
group_mutation.rs

1//! Ordinary Linux group mutations. Each call submits exactly once. The caller
2//! must journal request admission and retain raw replies before decoding them.
3use super::*;
4use std::collections::HashSet;
5
6#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(
8    tag = "action",
9    rename_all = "snake_case",
10    rename_all_fields = "camelCase",
11    deny_unknown_fields
12)]
13pub enum GroupMutation {
14    Create {
15        members: Vec<String>,
16        name: Option<String>,
17    },
18    Rename {
19        group_id: String,
20        name: String,
21    },
22    Leave {
23        group_id: String,
24    },
25    AddMembers {
26        group_id: String,
27        members: Vec<String>,
28    },
29    RemoveMembers {
30        group_id: String,
31        members: Vec<String>,
32    },
33    MyName {
34        group_id: String,
35        display_name: String,
36    },
37    ClearAnnouncement {
38        group_id: String,
39    },
40    Mute {
41        thread_id: String,
42        muted: bool,
43    },
44    Pin {
45        thread_id: String,
46        pinned: bool,
47    },
48}
49#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
50#[serde(rename_all = "camelCase")]
51pub struct GroupMemberResult {
52    pub username: String,
53    /// Delete replies have no member status. Never synthesize one.
54    pub status: Option<u32>,
55    pub nickname: Option<String>,
56}
57#[derive(Clone, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct GroupMutationReply {
60    pub service_code: i32,
61    pub command_results: Vec<i32>,
62    pub group_id: Option<String>,
63    pub name: Option<String>,
64    pub name_initials: Option<String>,
65    pub name_pinyin: Option<String>,
66    pub members: Vec<GroupMemberResult>,
67    pub declared_count: Option<u32>,
68    pub accepted: bool,
69    pub uncertain: bool,
70}
71#[derive(Clone, Serialize, Deserialize)]
72pub struct GroupRpc {
73    pub cgi: u16,
74    pub path: String,
75    pub opcode: Option<u32>,
76    pub body: Vec<u8>,
77}
78pub fn ordinary_identity(value: &str) -> Result<()> {
79    ensure!(
80        !value.is_empty()
81            && value.len() <= 256
82            && !value.chars().any(|c| c.is_control() || c.is_whitespace()),
83        "invalid_group_identity"
84    );
85    Ok(())
86}
87fn group(value: &str) -> Result<()> {
88    ordinary_identity(value)?;
89    ensure!(
90        value.ends_with("@chatroom") && value.len() > 9,
91        "invalid_group_identity"
92    );
93    Ok(())
94}
95fn title(value: &str, empty: bool) -> Result<()> {
96    ensure!(
97        (empty || !value.trim().is_empty())
98            && value.len() <= 512
99            && !value.chars().any(char::is_control),
100        "invalid_group_text"
101    );
102    Ok(())
103}
104impl GroupMutation {
105    pub fn target(&self) -> Option<&str> {
106        match self {
107            Self::Create { .. } => None,
108            Self::Mute { thread_id, .. } | Self::Pin { thread_id, .. } => Some(thread_id),
109            Self::Rename { group_id, .. }
110            | Self::Leave { group_id }
111            | Self::AddMembers { group_id, .. }
112            | Self::RemoveMembers { group_id, .. }
113            | Self::MyName { group_id, .. }
114            | Self::ClearAnnouncement { group_id } => Some(group_id),
115        }
116    }
117    pub fn validate(&self, own: &str) -> Result<()> {
118        ordinary_identity(own)?;
119        if let Some(id) = self.target() {
120            if matches!(self, Self::Mute { .. } | Self::Pin { .. }) && !id.ends_with("@chatroom") {
121                ordinary_identity(id)?;
122                ensure!(id != own && !id.contains('@'), "ordinary_contact_required");
123            } else {
124                group(id)?;
125            }
126        }
127        match self {
128            Self::Create { members, name } => {
129                validate_members(members, own)?;
130                ensure!(members.len() >= 2, "invalid_group_members");
131                if let Some(name) = name {
132                    title(name, true)?;
133                }
134            }
135            Self::AddMembers { members, .. } | Self::RemoveMembers { members, .. } => {
136                validate_members(members, own)?
137            }
138            Self::Rename { name, .. } => title(name, false)?,
139            Self::MyName { display_name, .. } => title(display_name, true)?,
140            _ => {}
141        }
142        Ok(())
143    }
144}
145fn validate_members(members: &[String], own: &str) -> Result<()> {
146    ensure!(
147        !members.is_empty() && members.len() <= 40,
148        "invalid_group_members"
149    );
150    let mut seen = HashSet::new();
151    for id in members {
152        ordinary_identity(id)?;
153        ensure!(
154            id != own && !id.contains('@') && seen.insert(id),
155            "invalid_group_members"
156        );
157    }
158    Ok(())
159}
160fn sk(value: &str) -> Vec<u8> {
161    p::bytes(1, value.as_bytes())
162}
163fn text_field(fields: &[p::Field], n: u32) -> Result<String> {
164    let raw = p::data(fields, n)?;
165    // The Linux MemberResp requires the outer wrapper, but accepts an empty
166    // SKString message. A missing outer field remains invalid.
167    if raw.is_empty() {
168        return Ok(String::new());
169    }
170    Ok(std::str::from_utf8(p::data(&p::decode(raw)?, 1)?)?.to_owned())
171}
172fn u32_field(fields: &[p::Field], n: u32) -> Result<u32> {
173    Ok(u32::try_from(p::scalar(fields, n)?)?)
174}
175fn signed(value: u64) -> Result<i32> {
176    let code = value as u32 as i32;
177    ensure!(
178        value <= u64::from(u32::MAX) || value == code as i64 as u64,
179        "invalid_group_response"
180    );
181    Ok(code)
182}
183fn oplog(opcode: u32, payload: Vec<u8>) -> Result<GroupRpc> {
184    let item = p::message(&[p::uint(1, opcode.into()), p::bytes(2, &p::buffer(&payload))])?;
185    Ok(GroupRpc {
186        cgi: 681,
187        path: "/cgi-bin/micromsg-bin/oplog".into(),
188        opcode: Some(opcode),
189        body: p::bytes(1, &p::message(&[p::uint(1, 1), p::bytes(2, &item)])?),
190    })
191}
192/// Direct contact writes use the official full-contact oplog shape. Fetch a
193/// current record first; change only the mute bit and preserve all other bytes.
194pub fn contact_notification_request(record: &ContactRecord, muted: bool) -> Result<GroupRpc> {
195    ordinary_identity(&record.username)?;
196    ensure!(!record.username.contains('@'), "ordinary_contact_required");
197    let fields = p::decode(&record.raw)?;
198    ensure!(
199        wire::decode_contact_record(&record.raw)?.username == record.username,
200        "contact_identity_mismatch"
201    );
202    for number in 1..=14 {
203        ensure!(
204            p::optional(&fields, number)?.is_some(),
205            "contact_snapshot_incomplete"
206        );
207    }
208    ensure!(
209        p::scalar(&fields, 7)? == u64::from(u32::MAX)
210            && !controlled_contact_fields(&record.raw)?.contains(&8),
211        "contact_flags_unresolved"
212    );
213    let flag = u32::try_from(p::scalar(&fields, 8)?)?;
214    ensure!(flag & 1 != 0, "ordinary_contact_required");
215    let flag = (flag & !0x200) | if muted { 0x200 } else { 0 };
216    let mut payload = Vec::new();
217    let mut at = 0;
218    while at < record.raw.len() {
219        let start = at;
220        let tag = p::read_varint(&record.raw, &mut at)?;
221        match tag & 7 {
222            0 => {
223                p::read_varint(&record.raw, &mut at)?;
224            }
225            1 => at += 8,
226            2 => {
227                let length = usize::try_from(p::read_varint(&record.raw, &mut at)?)?;
228                at += length;
229            }
230            5 => at += 4,
231            _ => bail!("invalid_contact_snapshot"),
232        }
233        ensure!(at <= record.raw.len(), "invalid_contact_snapshot");
234        if tag >> 3 == 8 {
235            payload.extend(p::uint(8, flag.into()));
236        } else {
237            payload.extend_from_slice(&record.raw[start..at]);
238        }
239    }
240    oplog(2, payload)
241}
242
243/// Ordinary Linux pin/unpin copies the complete contact, changes primary bit
244/// 0x800, and sends ModContact through oplog2. The secondary mask is a uint64.
245/// A folded group is removed from its message box in a preceding oplog74 step.
246pub fn contact_pinning_requests(record: &ContactRecord, pinned: bool) -> Result<Vec<GroupRpc>> {
247    ordinary_identity(&record.username)?;
248    let group = record.username.ends_with("@chatroom");
249    ensure!(
250        group || !record.username.contains('@'),
251        "ordinary_contact_required"
252    );
253    let fields = p::decode(&record.raw)?;
254    ensure!(
255        wire::decode_contact_record(&record.raw)?.username == record.username,
256        "contact_identity_mismatch"
257    );
258    for number in 1..=14 {
259        ensure!(
260            p::optional(&fields, number)?.is_some(),
261            "contact_snapshot_incomplete"
262        );
263    }
264    let controlled = controlled_contact_fields(&record.raw)?;
265    ensure!(
266        p::scalar(&fields, 7)? == u64::from(u32::MAX)
267            && !controlled.contains(&8)
268            && !controlled.contains(&83),
269        "contact_flags_unresolved"
270    );
271    let flag = u32::try_from(p::scalar(&fields, 8)?)?;
272    ensure!(
273        flag & if group { 2 } else { 1 } != 0,
274        "ordinary_contact_required"
275    );
276    let secondary_mask = p::scalar(&fields, 82)?;
277    ensure!(
278        secondary_mask == u64::MAX || secondary_mask == u64::from(u32::MAX),
279        "contact_flags_unresolved"
280    );
281    let secondary = u32::try_from(p::scalar(&fields, 83)?)?;
282    let folded = if group {
283        flag & 0x10000000 != 0
284    } else {
285        secondary & 0x80 != 0
286    };
287    let mut new_flag = (flag & !0x800) | if pinned { 0x800 } else { 0 };
288    if pinned && folded {
289        // This branch changes the primary flags only. The official producer
290        // restores the prior secondary value, unlike notification unmute.
291        new_flag &= !0x10000080;
292    }
293    let mut payload = Vec::new();
294    let mut at = 0;
295    while at < record.raw.len() {
296        let start = at;
297        let tag = p::read_varint(&record.raw, &mut at)?;
298        match tag & 7 {
299            0 => {
300                p::read_varint(&record.raw, &mut at)?;
301            }
302            1 => at += 8,
303            2 => {
304                let length = usize::try_from(p::read_varint(&record.raw, &mut at)?)?;
305                at += length;
306            }
307            5 => at += 4,
308            _ => bail!("invalid_contact_snapshot"),
309        }
310        ensure!(at <= record.raw.len(), "invalid_contact_snapshot");
311        match tag >> 3 {
312            7 => payload.extend(p::uint(7, u64::from(u32::MAX))),
313            8 => payload.extend(p::uint(8, new_flag.into())),
314            82 => payload.extend(p::uint(82, u64::MAX)),
315            83 => payload.extend(p::uint(83, secondary.into())),
316            _ => payload.extend_from_slice(&record.raw[start..at]),
317        }
318    }
319    let mut requests = Vec::new();
320    if group && pinned && folded {
321        requests.push(oplog(
322            74,
323            p::message(&[p::bytes(1, &sk(&record.username)), p::uint(2, 0)])?,
324        )?);
325    }
326    requests.push(oplog(2, payload)?);
327    Ok(requests)
328}
329/// Steps are separate requests. An uncertain step must stop the entire flow.
330/// A known cleanup rejection can be retained while a later quit is attempted.
331pub fn group_mutation_requests(
332    state: &AccountState,
333    action: &GroupMutation,
334    minimized: bool,
335) -> Result<Vec<GroupRpc>> {
336    state.validate()?;
337    action.validate(&state.own_username)?;
338    let target = action.target().unwrap_or_default();
339    let base = || wire::base_request(&state.device, state.uin, 0);
340    let rpc = |cgi, path: &str, body| GroupRpc {
341        cgi,
342        path: path.into(),
343        opcode: None,
344        body,
345    };
346    Ok(match action {
347        GroupMutation::Pin { .. } => bail!("contact_snapshot_required"),
348        GroupMutation::Create { members, name } => {
349            let mut fields = vec![
350                p::bytes(1, &base()?),
351                p::bytes(2, &sk(name.as_deref().unwrap_or_default())),
352                p::uint(3, members.len() as u64),
353            ];
354            fields.extend(members.iter().map(|m| p::bytes(4, &p::bytes(1, &sk(m)))));
355            vec![rpc(
356                119,
357                "/cgi-bin/micromsg-bin/createchatroom",
358                p::message(&fields)?,
359            )]
360        }
361        GroupMutation::AddMembers { members, .. }
362        | GroupMutation::RemoveMembers { members, .. } => {
363            let add = matches!(action, GroupMutation::AddMembers { .. });
364            let mut fields = vec![p::bytes(1, &base()?), p::uint(2, members.len() as u64)];
365            fields.extend(members.iter().map(|m| p::bytes(3, &p::bytes(1, &sk(m)))));
366            fields.push(p::bytes(
367                4,
368                &if add {
369                    sk(target)
370                } else {
371                    target.as_bytes().to_vec()
372                },
373            ));
374            if !add {
375                fields.push(p::uint(5, 0));
376            }
377            vec![rpc(
378                if add { 120 } else { 179 },
379                if add {
380                    "/cgi-bin/micromsg-bin/addchatroommember"
381                } else {
382                    "/cgi-bin/micromsg-bin/delchatroommember"
383                },
384                p::message(&fields)?,
385            )]
386        }
387        GroupMutation::Rename { name, .. } => vec![oplog(
388            27,
389            p::message(&[p::bytes(1, &sk(target)), p::bytes(2, &sk(name))])?,
390        )?],
391        GroupMutation::Leave { .. } => vec![
392            oplog(4, p::bytes(1, &sk(target)))?,
393            oplog(7, p::bytes(1, &sk(target)))?,
394            oplog(
395                16,
396                p::message(&[
397                    p::bytes(1, &sk(target)),
398                    p::bytes(2, &sk(&state.own_username)),
399                ])?,
400            )?,
401        ],
402        GroupMutation::MyName { display_name, .. } => vec![oplog(
403            48,
404            p::message(&[
405                p::bytes(1, target.as_bytes()),
406                p::bytes(2, state.own_username.as_bytes()),
407                p::bytes(3, display_name.as_bytes()),
408            ])?,
409        )?],
410        GroupMutation::ClearAnnouncement { .. } => vec![rpc(
411            993,
412            "/cgi-bin/micromsg-bin/setchatroomannouncement",
413            p::message(&[
414                p::bytes(1, &base()?),
415                p::bytes(2, target.as_bytes()),
416                p::bytes(3, b""),
417                p::bytes(5, b""),
418                p::uint(8, 1),
419            ])?,
420        )],
421        GroupMutation::Mute { .. } if !target.ends_with("@chatroom") => {
422            bail!("contact_snapshot_required")
423        }
424        GroupMutation::Mute { muted, .. } => {
425            let mut out = vec![oplog(
426                20,
427                p::message(&[p::bytes(1, &sk(target)), p::uint(2, u64::from(!muted))])?,
428            )?];
429            if !muted && minimized {
430                out.push(oplog(
431                    74,
432                    p::message(&[p::bytes(1, &sk(target)), p::uint(2, 0)])?,
433                )?);
434            }
435            out
436        }
437    })
438}
439/// Recover a valid create identity even when another success field is malformed.
440/// This is evidence for review, never permission to repeat the creation.
441pub fn observed_created_group(raw: &[u8]) -> Option<String> {
442    let fields = p::decode(raw).ok()?;
443    let id = text_field(&fields, 7).ok()?;
444    group(&id).ok()?;
445    Some(id)
446}
447pub fn decode_group_mutation(
448    rpc: &GroupRpc,
449    raw: &[u8],
450    requested: &[String],
451) -> Result<GroupMutationReply> {
452    let f = p::decode(raw)?;
453    let mut out = GroupMutationReply {
454        service_code: 0,
455        command_results: vec![],
456        group_id: None,
457        name: None,
458        name_initials: None,
459        name_pinyin: None,
460        members: vec![],
461        declared_count: None,
462        accepted: false,
463        uncertain: false,
464    };
465    if rpc.cgi == 681 {
466        out.service_code = signed(p::scalar(&f, 1)?)?;
467        if out.service_code != 0 {
468            out.uncertain = out.service_code == -433;
469            return Ok(out);
470        }
471        let ret = p::decode(p::data(&f, 2)?)?;
472        let count = u32_field(&ret, 1)?;
473        let mut values = vec![];
474        for field in ret.iter().filter(|f| f.number == 2) {
475            match &field.value {
476                p::Value::Bytes(bytes) => {
477                    let mut at = 0;
478                    while at < bytes.len() {
479                        values.push(signed(p::read_varint(bytes, &mut at)?)?);
480                    }
481                }
482                p::Value::Varint(value) => values.push(signed(*value)?),
483                _ => bail!("invalid_group_response"),
484            }
485        }
486        ensure!(
487            count == 1 && values.len() == 1,
488            "invalid_group_result_count"
489        );
490        out.accepted = values[0] == 0;
491        out.uncertain = values[0] == -433;
492        out.command_results = values;
493        return Ok(out);
494    }
495    out.service_code = p::base_code(&f)?;
496    if rpc.cgi == 119 {
497        out.group_id = observed_created_group(raw);
498    }
499    if out.service_code != 0 {
500        out.uncertain = out.service_code == -433;
501        return Ok(out);
502    }
503    if rpc.cgi == 993 {
504        out.accepted = true;
505        return Ok(out);
506    }
507    let create = rpc.cgi == 119;
508    let remove = rpc.cgi == 179;
509    ensure!(create || remove || rpc.cgi == 120, "invalid_group_rpc");
510    out.declared_count = Some(u32_field(&f, if create { 5 } else { 2 })?);
511    if create {
512        ensure!(out.group_id.is_some(), "missing_created_group");
513        out.name = Some(text_field(&f, 2)?);
514        out.name_initials = Some(text_field(&f, 3)?);
515        out.name_pinyin = Some(text_field(&f, 4)?);
516        p::unbuffer(p::data(&f, 8)?)?;
517    }
518    let mut seen = HashSet::new();
519    for field in f.iter().filter(|x| x.number == if create { 6 } else { 3 }) {
520        let p::Value::Bytes(bytes) = &field.value else {
521            bail!("invalid_group_member_reply")
522        };
523        let m = p::decode(bytes)?;
524        let username = text_field(&m, 1)?;
525        ensure!(
526            requested.contains(&username) && seen.insert(username.clone()),
527            "foreign_group_member_reply"
528        );
529        let (status, nickname) = if remove {
530            (None, None)
531        } else {
532            for n in [3, 4, 5, 9, 10, 11] {
533                text_field(&m, n)?;
534            }
535            signed(p::scalar(&m, 6)?)?;
536            u32_field(&m, 12)?;
537            (Some(u32_field(&m, 2)?), Some(text_field(&m, 3)?))
538        };
539        out.members.push(GroupMemberResult {
540            username,
541            status,
542            nickname,
543        });
544    }
545    // Acceptance and completeness remain distinct. Preserve unknown statuses,
546    // subsets and count-like values rather than inventing missing successes.
547    out.accepted = true;
548    Ok(out)
549}
550impl NativeClient {
551    pub fn group_mutation_raw(
552        &self,
553        state: &mut AccountState,
554        request: &GroupRpc,
555    ) -> Result<Vec<u8>> {
556        ensure!(
557            matches!(request.cgi, 119 | 120 | 179 | 681 | 993),
558            "invalid_group_rpc"
559        );
560        let expected = match request.cgi {
561            119 => "createchatroom",
562            120 => "addchatroommember",
563            179 => "delchatroommember",
564            681 => "oplog",
565            _ => "setchatroomannouncement",
566        };
567        ensure!(
568            request.path == format!("/cgi-bin/micromsg-bin/{expected}"),
569            "invalid_group_rpc"
570        );
571        self.authenticated_rpc(state, request.cgi, &request.path, &request.body)
572    }
573}
574#[cfg(test)]
575mod tests {
576    use super::*;
577    fn member(id: &str, status: u32) -> Vec<u8> {
578        let mut f = vec![
579            p::bytes(1, &sk(id)),
580            p::uint(2, status.into()),
581            p::int32(6, 0),
582            p::uint(12, 1),
583        ];
584        for n in [3, 4, 5, 9, 10, 11] {
585            f.push(p::bytes(n, &sk("")));
586        }
587        p::message(&f).unwrap()
588    }
589    #[test]
590    fn pinning_full_contact_preserves_unknown_fields_and_uses_u64_secondary_mask() {
591        fn record(name: &str, flag: u32, secondary: u32) -> ContactRecord {
592            let mut fields = vec![
593                p::bytes(1, &sk(name)),
594                p::uint(7, u32::MAX.into()),
595                p::uint(8, flag.into()),
596                p::uint(82, u32::MAX.into()),
597                p::uint(83, secondary.into()),
598                p::bytes(123, b"untouched"),
599            ];
600            for n in [2, 3, 4, 10, 11, 12] {
601                fields.push(p::bytes(n, &sk("")));
602            }
603            for n in [5, 9, 13, 14] {
604                fields.push(p::uint(n, 0));
605            }
606            fields.push(p::bytes(6, &p::buffer(b"")));
607            wire::decode_contact_record(&p::message(&fields).unwrap()).unwrap()
608        }
609        fn payload(rpc: &GroupRpc) -> Vec<p::Field> {
610            assert_eq!(rpc.cgi, 681);
611            assert_eq!(rpc.path, "/cgi-bin/micromsg-bin/oplog");
612            let outer = p::decode(&rpc.body).unwrap();
613            let list = p::decode(p::data(&outer, 1).unwrap()).unwrap();
614            assert_eq!(p::scalar(&list, 1).unwrap(), 1);
615            let item = p::decode(p::data(&list, 2).unwrap()).unwrap();
616            assert_eq!(p::scalar(&item, 1).unwrap(), u64::from(rpc.opcode.unwrap()));
617            p::decode(&p::unbuffer(p::data(&item, 2).unwrap()).unwrap()).unwrap()
618        }
619        for (name, baseline, secondary, pinned, expected, steps) in [
620            ("peer", 0x421, 0x400, true, 0xc21, 1),
621            ("peer", 0xe21, 0x400, false, 0x621, 1),
622            // Direct folded state is in the secondary flags. Pinning preserves
623            // those bytes while clearing these primary bits, as Linux does.
624            ("peer", 0x10000081, 0x480, true, 0x801, 1),
625            ("room@chatroom", 0x10000082, 0x400, true, 0x802, 2),
626            ("room@chatroom", 0x10000882, 0x400, false, 0x10000082, 1),
627        ] {
628            let source = record(name, baseline, secondary);
629            let requests = contact_pinning_requests(&source, pinned).unwrap();
630            assert_eq!(requests.len(), steps);
631            if steps == 2 {
632                assert_eq!(requests[0].opcode, Some(74));
633                let fields = payload(&requests[0]);
634                assert_eq!(text_field(&fields, 1).unwrap(), name);
635                assert_eq!(p::scalar(&fields, 2).unwrap(), 0);
636            }
637            let fields = payload(requests.last().unwrap());
638            assert_eq!(requests.last().unwrap().opcode, Some(2));
639            assert_eq!(p::scalar(&fields, 7).unwrap(), u64::from(u32::MAX));
640            assert_eq!(p::scalar(&fields, 8).unwrap(), expected);
641            assert_eq!(p::scalar(&fields, 82).unwrap(), u64::MAX);
642            assert_eq!(p::scalar(&fields, 83).unwrap(), u64::from(secondary));
643            assert_eq!(p::data(&fields, 123).unwrap(), b"untouched");
644        }
645        let mut missing = record("peer", 1, 0);
646        missing.raw = p::message(
647            &p::decode(&missing.raw)
648                .unwrap()
649                .into_iter()
650                .filter(|f| f.number != 83)
651                .map(|f| match f.value {
652                    p::Value::Varint(n) => p::uint(f.number, n),
653                    p::Value::Bytes(b) => p::bytes(f.number, &b),
654                    _ => unreachable!(),
655                })
656                .collect::<Vec<_>>(),
657        )
658        .unwrap();
659        assert!(contact_pinning_requests(&missing, true).is_err());
660        let mut mismatch = record("peer", 1, 0);
661        mismatch.username = "other".into();
662        assert!(contact_pinning_requests(&mismatch, true).is_err());
663        assert!(contact_pinning_requests(&record("peer", 0, 0), true).is_err());
664    }
665    #[test]
666    fn packed_signed_results_and_uncertain_special_code() {
667        let rpc = oplog(16, vec![]).unwrap();
668        for n in [0, -1, -433, i32::MIN] {
669            let raw = p::message(&[
670                p::int32(1, 0),
671                p::bytes(
672                    2,
673                    &p::message(&[
674                        p::uint(1, 1),
675                        p::bytes(2, &p::varint(n as i64 as u64)),
676                        p::bytes(3, b""),
677                    ])
678                    .unwrap(),
679                ),
680            ])
681            .unwrap();
682            let out = decode_group_mutation(&rpc, &raw, &[]).unwrap();
683            assert_eq!(out.command_results, [n]);
684            assert_eq!(out.accepted, n == 0);
685            assert_eq!(out.uncertain, n == -433);
686        }
687        assert!(decode_group_mutation(
688            &rpc,
689            &p::message(&[p::uint(1, 0), p::bytes(2, &p::uint(1, 1))]).unwrap(),
690            &[]
691        )
692        .is_err());
693    }
694    #[test]
695    fn create_preserves_partial_unknown_status_and_identity_on_malformed_member() {
696        let rpc = GroupRpc {
697            cgi: 119,
698            path: String::new(),
699            opcode: None,
700            body: vec![],
701        };
702        let mut f = vec![
703            p::bytes(1, &p::int32(1, 0)),
704            p::uint(5, 17),
705            p::bytes(7, &sk("synthetic@chatroom")),
706            p::bytes(8, &p::uint(1, 0)),
707        ];
708        for n in [2, 3, 4] {
709            f.push(p::bytes(n, &sk("")));
710        }
711        f.push(p::bytes(6, &member("peer", u32::MAX)));
712        let raw = p::message(&f).unwrap();
713        let out = decode_group_mutation(&rpc, &raw, &["peer".into()]).unwrap();
714        assert!(out.accepted);
715        assert_eq!(out.declared_count, Some(17));
716        assert_eq!(out.members[0].status, Some(u32::MAX));
717        f.push(p::bytes(6, &p::bytes(1, &sk("other"))));
718        let bad = p::message(&f).unwrap();
719        assert!(decode_group_mutation(&rpc, &bad, &["peer".into(), "other".into()]).is_err());
720        assert_eq!(
721            observed_created_group(&bad).as_deref(),
722            Some("synthetic@chatroom")
723        );
724    }
725    #[test]
726    fn required_outer_wrappers_can_be_empty_but_cannot_be_missing() {
727        let rpc = GroupRpc {
728            cgi: 119,
729            path: String::new(),
730            opcode: None,
731            body: vec![],
732        };
733        let mut member = vec![
734            p::bytes(1, &sk("peer")),
735            p::uint(2, 0),
736            p::uint(6, 0),
737            p::uint(12, 1),
738        ];
739        for n in [3, 4, 5, 9, 10, 11] {
740            member.push(p::bytes(n, b""));
741        }
742        let f = [
743            p::bytes(1, &p::int32(1, 0)),
744            p::bytes(2, b""),
745            p::bytes(3, b""),
746            p::bytes(4, b""),
747            p::uint(5, 1),
748            p::bytes(6, &p::message(&member).unwrap()),
749            p::bytes(7, &sk("room@chatroom")),
750            p::bytes(8, &p::uint(1, 0)),
751        ];
752        assert!(
753            decode_group_mutation(&rpc, &p::message(&f).unwrap(), &["peer".into()])
754                .unwrap()
755                .accepted
756        );
757        let missing = f
758            .into_iter()
759            .enumerate()
760            .filter_map(|(i, f)| (i != 1).then_some(f))
761            .collect::<Vec<_>>();
762        assert!(
763            decode_group_mutation(&rpc, &p::message(&missing).unwrap(), &["peer".into()]).is_err()
764        );
765    }
766}