Skip to main content

wechat_protocol/calls/
personal.rs

1//! Personal-call request codecs. Preparation performs no transport or account I/O.
2//!
3//! Host requests and Cloud requests use different envelopes. SDK task identifiers
4//! are not host BaseRequest identifiers. The caller supplies current authenticated
5//! context, advertised capabilities and device metadata; this module invents none.
6
7use super::signaling::{self, CallOperation, CloudRequest, CommonRequest};
8use crate::protobuf::{self as p, Field, Value};
9use anyhow::{bail, ensure, Result};
10
11const MAX_BODY: usize = 64 * 1024;
12const MAX_NESTED: usize = 16 * 1024;
13const MAX_MEMBERS: usize = 16;
14
15/// Host operation identity. Inner Cloud command IDs are a different namespace.
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum HostOperation {
18    RingingAck,
19    Invite,
20    Cancel,
21    LiveSync,
22}
23
24impl HostOperation {
25    pub const fn command(self) -> u16 {
26        match self {
27            Self::RingingAck => 8069,
28            Self::Invite => 10342,
29            Self::Cancel => 9332,
30            Self::LiveSync => 9262,
31        }
32    }
33
34    pub const fn path(self) -> &'static str {
35        match self {
36            Self::RingingAck => "/cgi-bin/micromsg-bin/voipilinkack",
37            Self::Invite => "/cgi-bin/micromsg-bin/voipilinkinvite",
38            Self::Cancel => "/cgi-bin/micromsg-bin/voipilinkcancel",
39            Self::LiveSync => "/cgi-bin/micromsg-bin/voipilinklivesync",
40        }
41    }
42}
43
44/// Encoded inner host operation. It does not imply authorization to dispatch.
45/// Bodies may contain private identities and are deliberately not Debug.
46pub struct HostRequest {
47    pub operation: HostOperation,
48    pub sdk_task: u32,
49    body: Vec<u8>,
50    pub longpoll_seconds: Option<u32>,
51}
52
53impl HostRequest {
54    pub fn body(&self) -> &[u8] {
55        &self.body
56    }
57
58    /// Wrap with a separately prepared host BaseRequest. This never rewrites its
59    /// task field using `sdk_task`.
60    pub fn common(&self, host_base: Vec<u8>) -> Result<Vec<u8>> {
61        CommonRequest {
62            base_request: Some(host_base),
63            body: Some(self.body.clone()),
64            unknown_fields: vec![],
65        }
66        .encode()
67    }
68}
69
70/// Caller-supplied, selected direct-voice request settings. Nested protobufs are
71/// checked for their known wire types while unknown fields remain byte-identical.
72pub struct VoiceInputs<'a> {
73    pub sdk: &'a [u8],
74    pub group: &'a [u8],
75    pub device: &'a [u8],
76    pub engine_capability: &'a [u8],
77    pub channel_capability: &'a [u8],
78    pub network_type: u32,
79    pub sim_type: u32,
80    pub video_length: u32,
81    pub video_ratio: u32,
82    pub nics: &'a [Vec<u8>],
83}
84
85impl VoiceInputs<'_> {
86    pub fn validate(&self) -> Result<()> {
87        self.checked().map(|_| ())
88    }
89
90    fn checked(&self) -> Result<(u32, Vec<u8>, Vec<u8>)> {
91        ensure!(
92            self.network_type < 8 && self.nics.len() <= 8,
93            "Unsupported voice input configuration"
94        );
95        let sdk = fields(self.sdk)?;
96        let version = required_u32(&sdk, 1)?;
97        let owner_version = required_bytes(&sdk, 2)?;
98        text(owner_version, 128, true)?;
99        let group = fields(self.group)?;
100        ensure!(
101            required_bytes(&group, 1)? == b"wechat",
102            "Unsupported call application"
103        );
104        required_u64(&group, 2)?;
105        ensure!(
106            required_bytes(&group, 3)? == b"voip-2p",
107            "Unsupported call target kind"
108        );
109        text(required_bytes(&group, 4)?, 1024, false)?;
110        ensure!(
111            required_u32(&group, 5)? <= 1,
112            "Unsupported personal call mode"
113        );
114        let owner_group = required_bytes(&group, 6)?;
115        text(owner_group, 1024, false)?;
116        required_u32(&group, 7)?;
117        let device = fields(self.device)?;
118        for n in [1, 2, 3, 4, 5, 9] {
119            if let Some(v) = p::optional_data(&device, n)? {
120                text(v, 1024, false)?;
121            }
122        }
123        for n in [7, 8] {
124            signaling::optional_u32(&device, n)?;
125        }
126        p::optional_data(&device, 6)?;
127        for value in [self.engine_capability, self.channel_capability] {
128            ensure!(
129                !value.is_empty() && value.len() <= MAX_NESTED,
130                "Invalid call capability size"
131            );
132        }
133        for nic in self.nics {
134            ensure!(nic.len() <= 2048, "Call NIC description too large");
135            fields(nic)?;
136        }
137        Ok((version, owner_version.to_vec(), owner_group.to_vec()))
138    }
139
140    fn client_info(&self) -> Result<Vec<u8>> {
141        let (version, owner_version, _) = self.checked()?;
142        let capabilities = message(&[
143            p::bytes(1, self.engine_capability),
144            p::bytes(2, self.channel_capability),
145        ])?;
146        let mut values = vec![
147            p::uint(1, u64::from(version)),
148            p::bytes(2, &owner_version),
149            p::uint(3, u64::from(self.network_type)),
150            p::uint(4, 1),
151            p::bytes(5, &capabilities),
152            p::bytes(6, self.device),
153            p::uint(7, u64::from(self.video_length)),
154            p::uint(8, u64::from(self.video_ratio)),
155        ];
156        values.extend(self.nics.iter().map(|nic| p::bytes(9, nic)));
157        values.push(p::uint(10, u64::from(self.sim_type)));
158        message(&values)
159    }
160}
161
162/// Prepare one direct voice invitation. `invite_id` identifies the original
163/// intent and is never allocated from a wall clock by this codec. An absent
164/// custom auth value stays absent, distinct from an invalid empty value.
165pub fn invite(
166    inputs: &VoiceInputs<'_>,
167    peer: &str,
168    invite_id: u64,
169    sdk_task: u32,
170    custom_auth: Option<&[u8]>,
171) -> Result<HostRequest> {
172    text(peer.as_bytes(), 128, true)?;
173    ensure!(
174        invite_id != 0 && sdk_task != 0,
175        "Missing call intent or task"
176    );
177    let (_, _, owner_group) = inputs.checked()?;
178    let mut values = vec![
179        p::bytes(1, peer.as_bytes()),
180        p::uint(2, 2),
181        p::bytes(3, &owner_group),
182        p::bytes(4, &inputs.client_info()?),
183        p::uint(5, invite_id),
184    ];
185    if let Some(auth) = custom_auth {
186        ensure!(
187            !auth.is_empty() && auth.len() <= 8192,
188            "Invalid custom call authorization"
189        );
190        values.push(p::bytes(6, auth));
191    }
192    values.push(p::bytes(7, b"voip-2p"));
193    Ok(HostRequest {
194        operation: HostOperation::Invite,
195        sdk_task,
196        body: message(&values)?,
197        longpoll_seconds: None,
198    })
199}
200
201/// Cancel only the still-pending recipients of this original invitation.
202/// Preserve source traversal order and duplicates; membership/role/timer checks
203/// are the session owner's responsibility before constructing this request.
204pub fn cancel(
205    inputs: &VoiceInputs<'_>,
206    room: u64,
207    room_key: u64,
208    invite_id: u64,
209    pending_recipients: &[String],
210    reason: CancelReason,
211    sdk_task: u32,
212) -> Result<HostRequest> {
213    inputs.checked()?;
214    ensure!(
215        invite_id != 0 && sdk_task != 0 && (room == 0) == (room_key == 0),
216        "Invalid cancellation context"
217    );
218    ensure!(
219        !pending_recipients.is_empty() && pending_recipients.len() <= 8,
220        "Invalid pending recipient count"
221    );
222    for peer in pending_recipients {
223        text(peer.as_bytes(), 128, true)?;
224    }
225    let inner = message(&[
226        p::bytes(1, inputs.sdk),
227        p::bytes(2, inputs.group),
228        p::uint(3, room),
229        p::uint(4, room_key),
230        p::uint(5, invite_id),
231        p::uint(6, reason.wire()),
232    ])?;
233    let mut outer = vec![p::bytes(1, &inner), p::uint(2, invite_id), p::uint(3, room)];
234    outer.extend(
235        pending_recipients
236            .iter()
237            .map(|name| p::bytes(4, name.as_bytes())),
238    );
239    outer.push(p::bytes(5, b"voip-2p"));
240    Ok(HostRequest {
241        operation: HostOperation::Cancel,
242        sdk_task,
243        body: message(&outer)?,
244        longpoll_seconds: None,
245    })
246}
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
249pub enum CancelReason {
250    Local,
251    Timeout,
252    /// Direct InviteTimeout sends this lower-layer wire reason, bypassing the
253    /// ordinary SDK OnCancel mapping.
254    NoResponse,
255    Other,
256}
257
258impl CancelReason {
259    const fn wire(self) -> u64 {
260        match self {
261            Self::Local => 1,
262            Self::Timeout => 4,
263            Self::NoResponse => 2,
264            Self::Other => 0,
265        }
266    }
267}
268
269/// Prepare one caller LiveSync. `timeout_seconds` is source-selected (3..50),
270/// distinct from the outer request deadline. Cookies remain uncommitted until
271/// the owner has handled the entire response batch.
272pub fn live_sync(
273    live_id: u64,
274    cookies: &[u8],
275    timeout_seconds: u32,
276    sdk_task: u32,
277) -> Result<HostRequest> {
278    ensure!(
279        live_id != 0 && sdk_task != 0 && (3..=50).contains(&timeout_seconds),
280        "Invalid call sync context"
281    );
282    ensure!(cookies.len() <= MAX_NESTED, "Call sync cookie too large");
283    Ok(HostRequest {
284        operation: HostOperation::LiveSync,
285        sdk_task,
286        body: message(&[
287            p::uint(1, live_id),
288            p::bytes(2, cookies),
289            p::bytes(4, b"voip-2p"),
290            p::uint(5, u64::from(timeout_seconds)),
291        ])?,
292        longpoll_seconds: Some(timeout_seconds + 5),
293    })
294}
295
296/// Owned room correlation supplied only after the matching Join/Invite reply.
297/// Member zero is valid. Room/key are private and deliberately not Debug.
298#[derive(Clone, Copy, PartialEq, Eq)]
299pub struct VoiceRoom {
300    pub room: u64,
301    pub room_key: u64,
302    pub own_member: u32,
303}
304impl VoiceRoom {
305    fn validate(self) -> Result<()> {
306        ensure!(
307            self.room != 0 && self.room_key != 0,
308            "Unallocated call room"
309        );
310        Ok(())
311    }
312}
313
314/// Ringing ACK type1 for the selected incoming voice role. This is not Join.
315pub fn acknowledge_voice(
316    inputs: &VoiceInputs<'_>,
317    room: VoiceRoom,
318    sdk_task: u32,
319) -> Result<HostRequest> {
320    inputs.checked()?;
321    room.validate()?;
322    ensure!(sdk_task != 0, "Missing incoming ACK task");
323    let caps = message(&[
324        p::bytes(1, inputs.engine_capability),
325        p::bytes(2, inputs.channel_capability),
326    ])?;
327    let mut values = vec![
328        p::bytes(1, inputs.sdk),
329        p::bytes(2, inputs.group),
330        p::uint(3, u64::from(inputs.network_type)),
331        p::uint(4, 1),
332        p::uint(5, room.room),
333        p::uint(6, room.room_key),
334        p::uint(7, u64::from(room.own_member)),
335    ];
336    values.extend(inputs.nics.iter().map(|nic| p::bytes(8, nic)));
337    values.extend([
338        p::uint(9, u64::from(inputs.sim_type)),
339        p::bytes(11, inputs.device),
340        p::bytes(12, &caps),
341        p::uint(13, 1),
342        p::uint(14, u64::from(inputs.video_length)),
343        p::uint(15, u64::from(inputs.video_ratio)),
344    ]);
345    Ok(HostRequest {
346        operation: HostOperation::RingingAck,
347        sdk_task,
348        body: message(&[p::bytes(1, &message(&values)?), p::bytes(3, b"voip-2p")])?,
349        longpoll_seconds: None,
350    })
351}
352/// Decline an already-ACKed callee with source Reject reason1. Pre-ACK busy uses
353/// a different operation and is not implied by this builder.
354pub fn reject(inputs: &VoiceInputs<'_>, room: VoiceRoom, sdk_task: u32) -> Result<CloudRequest> {
355    inputs.checked()?;
356    room.validate()?;
357    ensure!(sdk_task != 0, "Missing Reject task");
358    Ok(CloudRequest::for_operation(
359        sdk_task,
360        CallOperation::Reject,
361        message(&[
362            p::bytes(1, inputs.sdk),
363            p::bytes(2, inputs.group),
364            p::uint(3, room.room),
365            p::uint(4, room.room_key),
366            p::uint(5, u64::from(room.own_member)),
367            p::uint(6, 1),
368        ])?,
369    ))
370}
371
372/// Prepare callee Cloud Join3361. The caller obtains its context from Invite and
373/// must not manufacture a callee Join. Timestamp is an independently selected
374/// wire value; this codec does not assume its clock or unit.
375pub fn join(
376    inputs: &VoiceInputs<'_>,
377    room: VoiceRoom,
378    timestamp: u64,
379    sdk_task: u32,
380) -> Result<CloudRequest> {
381    inputs.checked()?;
382    room.validate()?;
383    ensure!(sdk_task != 0, "Missing call task");
384    let capabilities = message(&[
385        p::bytes(1, inputs.engine_capability),
386        p::bytes(2, inputs.channel_capability),
387    ])?;
388    let mut values = vec![
389        p::bytes(1, inputs.sdk),
390        p::bytes(2, inputs.group),
391        p::uint(3, u64::from(inputs.network_type)),
392        p::bytes(4, &capabilities),
393        p::uint(5, 2),
394        p::uint(6, 1),
395        p::bytes(7, inputs.device),
396        p::uint(8, timestamp),
397        p::uint(9, u64::from(inputs.video_length)),
398        p::uint(10, u64::from(inputs.video_ratio)),
399        p::uint(11, room.room),
400        p::uint(12, room.room_key),
401    ];
402    values.extend(inputs.nics.iter().map(|nic| p::bytes(13, nic)));
403    values.push(p::uint(14, u64::from(inputs.sim_type)));
404    Ok(CloudRequest::for_operation(
405        sdk_task,
406        CallOperation::Join,
407        message(&values)?,
408    ))
409}
410
411/// Remote-last-member cleanup uses a distinct source table from local Hangup.
412#[derive(Clone, Copy, Debug, PartialEq, Eq)]
413pub enum RemoteExitReason {
414    LastPeerLeft,
415    PeerBusy,
416    PeerRejected,
417}
418impl RemoteExitReason {
419    pub const fn wire(self) -> u64 {
420        match self {
421            Self::LastPeerLeft => 5,
422            Self::PeerBusy => 7,
423            Self::PeerRejected => 6,
424        }
425    }
426}
427
428/// Prepare joined Cloud Exit3380. Source reason numbers belong to the selected
429/// SDK, not local HTTP/CLI enums. The caller must establish native talk-success
430/// state separately; a joined roster or decoded frame is insufficient evidence.
431/// Required reliable peer notices, when selected, are a separate operation.
432pub fn exit(
433    inputs: &VoiceInputs<'_>,
434    room: VoiceRoom,
435    source_reason: u32,
436    talk_success: bool,
437    sdk_task: u32,
438) -> Result<CloudRequest> {
439    inputs.checked()?;
440    room.validate()?;
441    ensure!(sdk_task != 0, "Missing call task");
442    let mapped = [1, 0, 0, 0, 0, 8, 9, 10, 0, 5]
443        .get(source_reason.wrapping_sub(1) as usize)
444        .copied()
445        .unwrap_or(0);
446    exit_with_wire(inputs, room, mapped, talk_success, sdk_task)
447}
448/// Clean up an admitted remote-terminal roster once. The owner must stop media
449/// first and retain uncertainty if the correlated reply is lost.
450pub fn exit_remote(
451    inputs: &VoiceInputs<'_>,
452    room: VoiceRoom,
453    reason: RemoteExitReason,
454    talk_success: bool,
455    sdk_task: u32,
456) -> Result<CloudRequest> {
457    inputs.checked()?;
458    room.validate()?;
459    ensure!(sdk_task != 0, "Missing call task");
460    exit_with_wire(inputs, room, reason.wire(), talk_success, sdk_task)
461}
462fn exit_with_wire(
463    inputs: &VoiceInputs<'_>,
464    room: VoiceRoom,
465    mapped: u64,
466    talk_success: bool,
467    sdk_task: u32,
468) -> Result<CloudRequest> {
469    let body = message(&[
470        p::bytes(1, inputs.sdk),
471        p::bytes(2, inputs.group),
472        p::uint(3, room.room),
473        p::uint(4, room.room_key),
474        p::uint(5, u64::from(room.own_member)),
475        p::uint(6, mapped),
476        p::uint(7, u64::from(inputs.network_type)),
477        p::uint(8, u64::from(!talk_success)),
478    ])?;
479    Ok(CloudRequest::for_operation(
480        sdk_task,
481        CallOperation::Exit,
482        body,
483    ))
484}
485
486/// Wrap an inner Cloud request using its separate app BaseRequest. This returns
487/// IlinkRequestInfo bytes, not host CommonRequest or a packed app-task frame.
488pub fn cloud_request_info(request: &CloudRequest, cloud_base: &[u8]) -> Result<Vec<u8>> {
489    let operation = request.operation()?;
490    let base = fields(cloud_base)?;
491    required_u64(&base, 1)?;
492    ensure!(
493        required_bytes(&base, 2)?.len() <= 128,
494        "Cloud device identity too large"
495    );
496    let signed = required_u64(&base, 3)? as i64;
497    i32::try_from(signed)?;
498    let body = request
499        .body
500        .as_deref()
501        .ok_or_else(|| anyhow::anyhow!("Missing Cloud call body"))?;
502    let cloud = message(&[p::bytes(1, cloud_base), p::bytes(2, body)])?;
503    message(&[
504        p::uint(1, u64::from(operation.command_id())),
505        p::bytes(2, operation.path().as_bytes()),
506        p::bytes(3, &cloud),
507        p::uint(5, 1),
508        p::uint(6, 1),
509        p::uint(7, 1),
510    ])
511}
512
513/// Raw caller response, distinct from the callee Join response. Decode after
514/// host CommonResponse authentication and success; it does not install a room.
515/// Secret room values and original fields deliberately have no Debug output.
516pub struct InviteResponse {
517    pub room: u64,
518    pub room_key: u64,
519    pub own_member: u32,
520    pub group_id: String,
521    pub client_response: Vec<u8>,
522    pub custom_auth: Option<Vec<u8>>,
523    pub invite_results: Vec<(String, u32)>,
524    pub original: Vec<u8>,
525    pub unknown_fields: Vec<Field>,
526}
527
528impl InviteResponse {
529    pub fn decode(input: &[u8]) -> Result<Self> {
530        let values = fields(input)?;
531        let room = required_u64(&values, 1)?;
532        let room_key = required_u64(&values, 2)?;
533        ensure!(room != 0 && room_key != 0, "Unallocated caller room");
534        let group = required_bytes(&values, 4)?;
535        text(group, 1024, true)?;
536        let client_response = required_bytes(&values, 5)?;
537        ensure!(
538            !client_response.is_empty(),
539            "Missing caller client response"
540        );
541        let mut results = Vec::new();
542        for value in values.iter().filter(|value| value.number == 8) {
543            ensure!(
544                results.len() < MAX_MEMBERS,
545                "Too many call invitation results"
546            );
547            let nested = fields(field_bytes(value)?)?;
548            let name = required_bytes(&nested, 1)?;
549            text(name, 128, true)?;
550            results.push((
551                String::from_utf8(name.to_vec())?,
552                signaling::optional_u32(&nested, 2)?.unwrap_or(0),
553            ));
554        }
555        let users = values
556            .iter()
557            .filter(|value| value.number == 6)
558            .collect::<Vec<_>>();
559        ensure!(users.len() <= MAX_MEMBERS, "Too many call user mappings");
560        for user in users {
561            fields(field_bytes(user)?)?;
562        }
563        if let Some(url) = p::optional_data(&values, 9)? {
564            text(url, 8192, false)?;
565        }
566        signaling::optional_u32(&values, 10)?;
567        Ok(Self {
568            room,
569            room_key,
570            own_member: required_u32(&values, 3)?,
571            group_id: String::from_utf8(group.to_vec())?,
572            client_response: client_response.to_vec(),
573            custom_auth: p::optional_data(&values, 7)?.map(Vec::from),
574            invite_results: results,
575            original: input.to_vec(),
576            unknown_fields: signaling::unknown_fields(values, 10),
577        })
578    }
579}
580
581/// A complete caller-sync reply. A decoder preserves unfamiliar items; the
582/// account owner must handle all supported effects before committing `cookies`.
583/// An unknown item is not evidence that advancing the cursor is safe.
584pub struct LiveSyncResponse {
585    pub items: Vec<LiveSyncItem>,
586    pub cookies: Option<Vec<u8>>,
587    pub original: Vec<u8>,
588    pub unknown_fields: Vec<Field>,
589}
590
591pub struct LiveSyncItem {
592    pub kind: i32,
593    pub body: Vec<u8>,
594    pub original: Vec<u8>,
595    pub unknown_fields: Vec<Field>,
596}
597
598impl LiveSyncResponse {
599    pub fn decode(input: &[u8]) -> Result<Self> {
600        let values = fields(input)?;
601        let mut items = Vec::new();
602        for value in values.iter().filter(|v| v.number == 1) {
603            ensure!(items.len() < 256, "Too many call sync items");
604            let raw = field_bytes(value)?;
605            let nested = fields(raw)?;
606            let kind = i32::try_from(required_u64(&nested, 1)? as i64)?;
607            let body = required_bytes(&nested, 2)?.to_vec();
608            items.push(LiveSyncItem {
609                kind,
610                body,
611                original: raw.to_vec(),
612                unknown_fields: signaling::unknown_fields(nested, 2),
613            });
614        }
615        Ok(Self {
616            items,
617            cookies: p::optional_data(&values, 2)?.map(Vec::from),
618            original: input.to_vec(),
619            unknown_fields: signaling::unknown_fields(values, 2),
620        })
621    }
622}
623
624/// Caller client context retains original Strategy separately from later current
625/// capability/AV snapshots. Retry seconds are not IM mode or a sync deadline.
626pub struct JoinClientResponse {
627    pub strategy: Vec<u8>,
628    pub members: Vec<u8>,
629    pub av_status: Option<Vec<u8>>,
630    pub live_id: u64,
631    pub cookies: Vec<u8>,
632    pub retry_seconds: Option<u32>,
633    pub original: Vec<u8>,
634}
635
636impl JoinClientResponse {
637    pub fn decode(input: &[u8]) -> Result<Self> {
638        let values = fields(input)?;
639        let strategy = required_bytes(&values, 1)?;
640        let members = required_bytes(&values, 2)?;
641        ensure!(
642            !strategy.is_empty() && !members.is_empty(),
643            "Missing caller room negotiation"
644        );
645        fields(strategy)?;
646        fields(members)?;
647        for n in [3, 4] {
648            if let Some(raw) = p::optional_data(&values, n)? {
649                fields(raw)?;
650            }
651        }
652        for n in [7, 8, 10] {
653            signaling::optional_u32(&values, n)?;
654        }
655        for value in values.iter().filter(|value| value.number == 9) {
656            fields(field_bytes(value)?)?;
657        }
658        let live_id = required_u64(&values, 5)?;
659        ensure!(live_id != 0, "Missing caller live identity");
660        Ok(Self {
661            strategy: strategy.to_vec(),
662            members: members.to_vec(),
663            av_status: p::optional_data(&values, 3)?.map(Vec::from),
664            live_id,
665            cookies: required_bytes(&values, 6)?.to_vec(),
666            retry_seconds: signaling::optional_u32(&values, 7)?,
667            original: input.to_vec(),
668        })
669    }
670}
671
672fn message(parts: &[Vec<u8>]) -> Result<Vec<u8>> {
673    let value = p::message(parts)?;
674    ensure!(value.len() <= MAX_BODY, "Personal call body too large");
675    Ok(value)
676}
677fn fields(input: &[u8]) -> Result<Vec<Field>> {
678    ensure!(input.len() <= MAX_BODY, "Personal call body too large");
679    signaling::decode_fields(input)
680}
681fn required_bytes(values: &[Field], number: u32) -> Result<&[u8]> {
682    p::optional_data(values, number)?
683        .ok_or_else(|| anyhow::anyhow!("Missing call bytes field {number}"))
684}
685fn required_u64(values: &[Field], number: u32) -> Result<u64> {
686    match p::optional(values, number)? {
687        Some(Value::Varint(v)) => Ok(*v),
688        _ => bail!("Missing or invalid call integer field {number}"),
689    }
690}
691fn required_u32(values: &[Field], number: u32) -> Result<u32> {
692    Ok(u32::try_from(required_u64(values, number)?)?)
693}
694fn field_bytes(field: &Field) -> Result<&[u8]> {
695    match &field.value {
696        Value::Bytes(value) => Ok(value),
697        _ => bail!("Invalid call bytes field {}", field.number),
698    }
699}
700fn text(value: &[u8], maximum: usize, nonempty: bool) -> Result<()> {
701    ensure!(
702        value.len() <= maximum && (!nonempty || !value.is_empty()) && !value.contains(&0),
703        "Invalid call text"
704    );
705    std::str::from_utf8(value)?;
706    Ok(())
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712
713    #[test]
714    fn full_sync_preserves_later_unknown_item_and_cookie_presence() {
715        let first = message(&[p::uint(1, 1), p::bytes(2, b"a")]).unwrap();
716        let later = message(&[p::int32(1, -1), p::bytes(2, b"b"), p::uint(99, 7)]).unwrap();
717        let wire = message(&[p::bytes(1, &first), p::bytes(1, &later), p::bytes(2, &[])]).unwrap();
718        let result = LiveSyncResponse::decode(&wire).unwrap();
719        assert_eq!(result.items.len(), 2);
720        assert_eq!(result.items[1].kind, -1);
721        assert_eq!(result.items[1].unknown_fields.len(), 1);
722        assert_eq!(result.cookies, Some(vec![]));
723        assert!(LiveSyncResponse::decode(&[]).unwrap().cookies.is_none());
724        let bad = message(&[p::bytes(1, &[8, 1, 8, 2, 18, 0])]).unwrap();
725        assert!(LiveSyncResponse::decode(&bad).is_err());
726    }
727
728    #[test]
729    fn caller_response_keeps_retry_separate_and_requires_full_widths() {
730        let client = message(&[
731            p::bytes(1, &[8, 1]),
732            p::bytes(2, &[8, 0]),
733            p::uint(5, 9),
734            p::bytes(6, &[]),
735            p::uint(7, 3),
736        ])
737        .unwrap();
738        let result = JoinClientResponse::decode(&client).unwrap();
739        assert_eq!(result.live_id, 9);
740        assert_eq!(result.retry_seconds, Some(3));
741        let mut bad = client.clone();
742        bad.extend(p::uint(7, 4));
743        assert!(JoinClientResponse::decode(&bad).is_err());
744        let mut response = message(&[
745            p::uint(1, 10),
746            p::uint(2, u64::MAX),
747            p::uint(3, 0),
748            p::bytes(4, b"g"),
749            p::bytes(5, &client),
750            p::bytes(7, b"auth"),
751            p::uint(100, 1),
752        ])
753        .unwrap();
754        let result = InviteResponse::decode(&response).unwrap();
755        assert_eq!(result.own_member, 0);
756        assert_eq!(result.room_key, u64::MAX);
757        assert_eq!(result.unknown_fields.len(), 1);
758        assert_eq!(result.original, response);
759        response.extend(p::uint(3, u64::MAX));
760        assert!(InviteResponse::decode(&response).is_err());
761        for (nested, expected) in [
762            (p::bytes(1, b"peer"), 0),
763            (message(&[p::bytes(1, b"peer"), p::uint(2, 7)]).unwrap(), 7),
764        ] {
765            let wire = message(&[
766                p::uint(1, 7),
767                p::uint(2, 9),
768                p::uint(3, 0),
769                p::bytes(4, b"group"),
770                p::bytes(5, &[8, 1]),
771                p::bytes(8, &nested),
772            ])
773            .unwrap();
774            assert_eq!(
775                InviteResponse::decode(&wire).unwrap().invite_results[0].1,
776                expected
777            );
778        }
779        let bad = message(&[
780            p::uint(1, 7),
781            p::uint(2, 9),
782            p::uint(3, 0),
783            p::bytes(4, b"group"),
784            p::bytes(5, &[8, 1]),
785            p::bytes(
786                8,
787                &message(&[p::bytes(1, b"peer"), p::bytes(2, &[])]).unwrap(),
788            ),
789        ])
790        .unwrap();
791        assert!(InviteResponse::decode(&bad).is_err());
792    }
793}