1use 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 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 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}
192pub 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
243pub 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 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}
329pub 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}
439pub 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 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 ("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}