Skip to main content

wechat_protocol/calls/
client.rs

1//! Stateful personal-voice signaling over the maintained native host transport.
2//! Account ownership, journal persistence, logout fences, cancellation and timeout
3//! supervision belong to the caller. These methods never retry an RPC.
4
5use super::{
6    app_frame::{self, AppFrameParameters},
7    app_session::{AppAuthority, AppSession, PendingAppAuth},
8    host_policy::CallHostRoute,
9    personal::{self, VoiceInputs, VoiceRoom},
10    relay::SelectedRelay,
11    signaling::{CallOperation, CommonRequest, CommonResponse},
12};
13use crate::{
14    protobuf::{self as p, Field, Value},
15    wire, AccountState, NativeClient,
16};
17use anyhow::{bail, ensure, Context, Result};
18use std::collections::{BTreeMap, BTreeSet};
19use zeroize::Zeroizing;
20
21/// A host RPC boundary for signaling. Implementations must authenticate transport
22/// replies, update accepted host cookies and perform exactly one submission.
23/// The default native implementation also enforces active per-CGI route policy.
24pub trait PersonalTransport {
25    fn call_rpc(
26        &self,
27        state: &mut AccountState,
28        route: CallHostRoute,
29        request: &[u8],
30    ) -> Result<Vec<u8>>;
31}
32/// A local transport stopped before handing business request bytes to its
33/// submission layer. An already-recorded prepared intent is preserved. Only
34/// this proven pre-submission case may use this marker;
35/// timeouts, server replies and other ambiguous outcomes must use other errors.
36#[derive(Debug)]
37pub struct CallRpcNotSubmitted;
38impl std::fmt::Display for CallRpcNotSubmitted {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.write_str("Call control request was not submitted")
41    }
42}
43impl std::error::Error for CallRpcNotSubmitted {}
44
45impl PersonalTransport for NativeClient {
46    fn call_rpc(
47        &self,
48        state: &mut AccountState,
49        route: CallHostRoute,
50        request: &[u8],
51    ) -> Result<Vec<u8>> {
52        self.experimental_call_rpc(state, route, request)
53    }
54}
55/// The room/control exchange is understood, but its selected media profile is
56/// outside this implementation. This is distinct from an ambiguous RPC outcome;
57/// the owner can end the known call once using its current control phase.
58#[derive(Debug)]
59pub struct UnsupportedCallMedia;
60impl std::fmt::Display for UnsupportedCallMedia {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.write_str("Selected call media is unsupported")
63    }
64}
65impl std::error::Error for UnsupportedCallMedia {}
66
67mod incoming;
68mod termination;
69pub use super::personal::RemoteExitReason;
70pub use incoming::{IncomingPhase, NativeIncomingSession};
71pub use termination::{LocalStopReason, RemoteTermination};
72
73type AvSnapshot = BTreeMap<u16, (u32, Option<Vec<u8>>)>;
74
75/// Metadata and advertised voice support supplied by the embedding client.
76/// Request buffers are not returned codec/format/channel selections. This is
77/// independent client metadata, never a copied official profile/session.
78#[derive(Clone)]
79pub struct VoiceParameters {
80    pub sdk: Vec<u8>,
81    pub group: Vec<u8>,
82    pub device: Vec<u8>,
83    pub engine_capability: Vec<u8>,
84    pub channel_capability: Vec<u8>,
85    pub network_type: u32,
86    pub sim_type: u32,
87    pub video_length: u32,
88    pub video_ratio: u32,
89    pub nics: Vec<Vec<u8>>,
90    pub cloud_device_id: Vec<u8>,
91    pub app_config_version: u32,
92}
93impl VoiceParameters {
94    pub fn inputs(&self) -> VoiceInputs<'_> {
95        VoiceInputs {
96            sdk: &self.sdk,
97            group: &self.group,
98            device: &self.device,
99            engine_capability: &self.engine_capability,
100            channel_capability: &self.channel_capability,
101            network_type: self.network_type,
102            sim_type: self.sim_type,
103            video_length: self.video_length,
104            video_ratio: self.video_ratio,
105            nics: &self.nics,
106        }
107    }
108    fn sdk_version(&self) -> Result<u32> {
109        u32::try_from(number(&fields(&self.sdk)?, 1, None)?).map_err(Into::into)
110    }
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub struct TaskIds {
115    pub sdk: u32,
116    pub host: u32,
117}
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub enum PersonalPhase {
120    Idle,
121    Inviting,
122    Ringing,
123    Joined,
124    Ending,
125    TerminalPending,
126    Uncertain,
127    Finished,
128}
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum SyncProgress {
131    Waiting,
132    MediaReady,
133    Terminal(RemoteTermination),
134}
135
136#[derive(Clone)]
137struct Member {
138    member: u16,
139    status: u32,
140    openid: Vec<u8>,
141}
142enum ControlOrigin {
143    Outgoing { invite_id: u64 },
144    Incoming,
145}
146struct PersonalControl {
147    identity: VoiceRoom,
148    peer: String,
149    origin: ControlOrigin,
150    peer_joined: Option<bool>,
151    terminal: Option<RemoteTermination>,
152    invitation_pending: bool,
153    pending_recipients: Vec<String>,
154}
155
156#[derive(Clone)]
157struct ActiveRoom {
158    identity: VoiceRoom,
159    peer: String,
160    live_id: u64,
161    cookies: Vec<u8>,
162    strategy: Zeroizing<Vec<u8>>,
163    members: BTreeMap<String, Member>,
164    original_members: BTreeMap<String, Member>,
165    unbound_peer_openid: Option<Vec<u8>>,
166    member_sequence: u32,
167    cap_sequence: u32,
168    engine: Vec<u8>,
169    channel: Vec<u8>,
170    av_sequence: u32,
171    av: AvSnapshot,
172    media_taken: bool,
173}
174
175pub struct NegotiatedMedia {
176    pub relay: SelectedRelay,
177    pub peer_member: u16,
178    pub frames_per_packet: u8,
179}
180
181/// One generic app session with at most one personal call. It is not Clone and
182/// never auto-renews authentication, redials, fabricates a callee Join for caller
183/// context, or retries an uncertain action. Callers persist state.cookie changes
184/// even when subsequent app parsing fails.
185pub struct NativePersonalSession {
186    app: AppSession,
187    authority: AppAuthority,
188    own_username: String,
189    parameters: VoiceParameters,
190    phase: PersonalPhase,
191    active: Option<ActiveRoom>,
192    control: Option<PersonalControl>,
193    host_tasks: BTreeSet<u32>,
194    sdk_tasks: BTreeSet<u32>,
195}
196impl NativePersonalSession {
197    /// Perform exactly one generic7994 request after the host has journaled its
198    /// original intent. `device32` is fresh own app identity from a secure RNG.
199    pub fn authenticate(
200        client: &impl PersonalTransport,
201        state: &mut AccountState,
202        authority: AppAuthority,
203        host_task: u32,
204        device32: [u8; 32],
205        parameters: VoiceParameters,
206    ) -> Result<Self> {
207        ensure!(host_task != 0, "Missing app auth task");
208        parameters.inputs().validate()?;
209        ensure!(
210            number(&fields(&parameters.group)?, 5, None)? == 1,
211            "Selected caller sync requires group mode1"
212        );
213        let pending = PendingAppAuth::new(authority, device32)?;
214        let request = common(state, host_task, &pending.request_body()?)?;
215        let reply = client.call_rpc(state, CallHostRoute::AppAuth, &request)?;
216        let app = pending.complete(&reply, authority)?;
217        Ok(Self {
218            app,
219            authority,
220            own_username: state.own_username.clone(),
221            parameters,
222            phase: PersonalPhase::Idle,
223            active: None,
224            control: None,
225            host_tasks: BTreeSet::from([host_task]),
226            sdk_tasks: BTreeSet::new(),
227        })
228    }
229    pub fn phase(&self) -> PersonalPhase {
230        self.phase
231    }
232    /// Current authenticated remote microphone state while joined. This is
233    /// independent of membership, codec selection and whether media is audible.
234    pub fn peer_microphone_muted(&self) -> Option<bool> {
235        if self.phase != PersonalPhase::Joined {
236            return None;
237        }
238        let room = self.active.as_ref()?;
239        let member = room.members.get(&room.peer)?;
240        if member.status != 1 {
241            return None;
242        }
243        match room.av.get(&member.member)?.0 {
244            0 => Some(true),
245            1 => Some(false),
246            _ => None,
247        }
248    }
249    /// Allocated control identity survives a later media-admission failure. It
250    /// does not by itself authorize Cancel or Exit; the current phase still must
251    /// prove pending versus joined membership.
252    pub fn has_allocated_control(&self) -> bool {
253        self.control.is_some()
254    }
255    pub fn media_ready(&self, state: &AccountState, current: AppAuthority) -> Result<bool> {
256        self.check(state, current)?;
257        if self.phase != PersonalPhase::Joined {
258            return Ok(false);
259        }
260        match self.active.as_ref() {
261            Some(active) => ready(active, &self.own_username),
262            None => Ok(false),
263        }
264    }
265    fn check(&self, state: &AccountState, current: AppAuthority) -> Result<()> {
266        self.app.validate_authority(current)?;
267        ensure!(
268            current == self.authority && state.own_username == self.own_username,
269            "Personal call account changed"
270        );
271        Ok(())
272    }
273    fn reserve(&mut self, tasks: TaskIds) -> Result<()> {
274        ensure!(
275            tasks.host != 0
276                && tasks.sdk != 0
277                && self.host_tasks.len() < 4096
278                && self.sdk_tasks.len() < 4096,
279            "Invalid or exhausted call task IDs"
280        );
281        ensure!(
282            !self.host_tasks.contains(&tasks.host) && !self.sdk_tasks.contains(&tasks.sdk),
283            "Call task already spent"
284        );
285        self.host_tasks.insert(tasks.host);
286        self.sdk_tasks.insert(tasks.sdk);
287        Ok(())
288    }
289    fn host(
290        &mut self,
291        client: &impl PersonalTransport,
292        state: &mut AccountState,
293        route: CallHostRoute,
294        body: &[u8],
295        host_task: u32,
296    ) -> Result<Vec<u8>> {
297        let request = common(state, host_task, body)?;
298        let raw = client.call_rpc(state, route, &request)?;
299        let reply = CommonResponse::decode(&raw)?;
300        ensure!(
301            reply.return_code()? == 0,
302            "Personal call host operation rejected"
303        );
304        reply
305            .body
306            .ok_or_else(|| anyhow::anyhow!("Missing personal call host reply"))
307    }
308    pub fn invite(
309        &mut self,
310        client: &impl PersonalTransport,
311        state: &mut AccountState,
312        current: AppAuthority,
313        peer: &str,
314        invite_id: u64,
315        tasks: TaskIds,
316    ) -> Result<()> {
317        self.check(state, current)?;
318        ensure!(
319            self.phase == PersonalPhase::Idle && peer != self.own_username,
320            "Personal session busy or invalid peer"
321        );
322        self.reserve(tasks)?;
323        self.phase = PersonalPhase::Inviting;
324        let result = self.invite_once(client, state, peer, invite_id, tasks);
325        let understood_media_error = result
326            .as_ref()
327            .is_err_and(|error| error.is::<UnsupportedCallMedia>())
328            && self
329                .control
330                .as_ref()
331                .is_some_and(|control| control.peer_joined.is_some())
332            && matches!(self.phase, PersonalPhase::Ringing | PersonalPhase::Joined);
333        if result.is_err() && !understood_media_error {
334            self.phase = PersonalPhase::Uncertain;
335        }
336        result
337    }
338    fn invite_once(
339        &mut self,
340        client: &impl PersonalTransport,
341        state: &mut AccountState,
342        peer: &str,
343        invite_id: u64,
344        tasks: TaskIds,
345    ) -> Result<()> {
346        let request =
347            personal::invite(&self.parameters.inputs(), peer, invite_id, tasks.sdk, None)?;
348        let body = self.host(
349            client,
350            state,
351            request.operation.into(),
352            request.body(),
353            tasks.host,
354        )?;
355        let wrapper = personal::InviteResponse::decode(&body)?;
356        ensure!(
357            wrapper.custom_auth.as_ref().is_none_or(|v| v.is_empty()),
358            "Additional custom call auth unsupported"
359        );
360        ensure!(
361            wrapper.invite_results.len() <= 1
362                && wrapper.invite_results.iter().all(|(name, _)| name == peer),
363            "Personal invitation result identity mismatch"
364        );
365        ensure!(
366            wrapper.unknown_fields.is_empty(),
367            "Unknown personal allocation fields"
368        );
369        let own_member = u16::try_from(wrapper.own_member)?;
370        self.control = Some(PersonalControl {
371            identity: VoiceRoom {
372                room: wrapper.room,
373                room_key: wrapper.room_key,
374                own_member: wrapper.own_member,
375            },
376            peer: peer.to_owned(),
377            origin: ControlOrigin::Outgoing { invite_id },
378            peer_joined: None,
379            terminal: None,
380            invitation_pending: false,
381            pending_recipients: vec![],
382        });
383        // The personal host wrapper's per-target status is not the overall
384        // allocation outcome. The source reduces the initial full roster before
385        // AV/codec setup, including busy/rejected peers and our own removal.
386        let initial = fields(&wrapper.client_response)?;
387        only(&initial, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10])?;
388        let raw_members = data(&initial, 2)?;
389        let list = fields(raw_members)?;
390        only(&list, &[1, 2, 3])?;
391        p::optional_data(&list, 3)?;
392        let member_sequence = u32::try_from(number(&list, 1, Some(0))?)?;
393        ensure!(
394            member_sequence != 0,
395            "Missing initial current member sequence"
396        );
397        let members = parse_roster_entries(repeated(&list, 2, 16)?, &self.own_username, peer)?;
398        ensure!(
399            members
400                .get(&self.own_username)
401                .is_none_or(|own| own.member == own_member)
402                && members
403                    .get(peer)
404                    .is_none_or(|peer| peer.member != own_member),
405            "Initial personal member identity mismatch"
406        );
407        let peer_joined = members.get(peer).is_some_and(|peer| peer.status == 1);
408        let pending = members
409            .get(peer)
410            .is_some_and(|peer| matches!(peer.status, 2 | 3));
411        let control = self.control.as_mut().expect("allocated control");
412        control.peer_joined = Some(peer_joined);
413        control.invitation_pending = pending;
414        if pending {
415            control.pending_recipients = vec![peer.to_owned()];
416        }
417        let terminal = termination::classify(
418            termination::Origin::Caller,
419            1,
420            members.get(&self.own_username).map(|own| own.status),
421            members.get(peer).map(|peer| peer.status),
422            pending,
423        )?;
424        if let Some(action @ RemoteTermination::LocalStop(_)) = terminal {
425            control.terminal = Some(action);
426            self.phase = PersonalPhase::Finished;
427            return Ok(());
428        }
429        let live_id = number(&initial, 5, None)?;
430        ensure!(live_id != 0, "Missing initial live identity");
431        self.parameters.group = replace_group(&self.parameters.group, live_id, &wrapper.group_id)?;
432        if let Some(action) = terminal {
433            self.control.as_mut().expect("allocated control").terminal = Some(action);
434            self.phase = PersonalPhase::TerminalPending;
435            return Ok(());
436        }
437        ensure!(
438            members.len() == 2 && members[&self.own_username].status == 1,
439            "Incomplete initial personal roster"
440        );
441        self.phase = if peer_joined {
442            PersonalPhase::Joined
443        } else {
444            PersonalPhase::Ringing
445        };
446        let client_response = personal::JoinClientResponse::decode(&wrapper.client_response)?;
447        let strategy = fields(&client_response.strategy)?;
448        ensure!(
449            number(&strategy, 14, None)? & (1 << 36) == 0,
450            "Alternate caller sync unsupported"
451        );
452        let (cap_sequence, engine, channel) = cap(data(&strategy, 13)?)?;
453        let (av_sequence, av) = av(
454            client_response
455                .av_status
456                .as_deref()
457                .ok_or_else(|| anyhow::anyhow!("Missing initial AV snapshot"))?,
458            own_member,
459            members[peer].member,
460        )?;
461        let active = ActiveRoom {
462            identity: VoiceRoom {
463                room: wrapper.room,
464                room_key: wrapper.room_key,
465                own_member: wrapper.own_member,
466            },
467            peer: peer.to_owned(),
468            live_id: client_response.live_id,
469            cookies: client_response.cookies,
470            strategy: Zeroizing::new(client_response.strategy),
471            original_members: members.clone(),
472            unbound_peer_openid: None,
473            members,
474            member_sequence,
475            cap_sequence,
476            engine,
477            channel,
478            av_sequence,
479            av,
480            media_taken: false,
481        };
482        self.active = Some(active);
483        let _ = ready(
484            self.active.as_ref().expect("accepted room"),
485            &self.own_username,
486        )?;
487        Ok(())
488    }
489    /// One caller LiveSync with source-selected timeout. Install all supported
490    /// effects atomically, then cookies. Unknown or post-media reconfiguration
491    /// stops with the old cursor retained and no automatic next poll.
492    pub fn poll(
493        &mut self,
494        client: &impl PersonalTransport,
495        state: &mut AccountState,
496        current: AppAuthority,
497        tasks: TaskIds,
498    ) -> Result<SyncProgress> {
499        self.check(state, current)?;
500        ensure!(
501            matches!(self.phase, PersonalPhase::Ringing | PersonalPhase::Joined),
502            "Personal caller sync unavailable"
503        );
504        self.reserve(tasks)?;
505        let result = self.poll_once(client, state, tasks);
506        if result.as_ref().is_err_and(|error| {
507            !error.is::<CallRpcNotSubmitted>() && !error.is::<UnsupportedCallMedia>()
508        }) {
509            self.phase = PersonalPhase::Uncertain;
510        }
511        result
512    }
513    fn poll_once(
514        &mut self,
515        client: &impl PersonalTransport,
516        state: &mut AccountState,
517        tasks: TaskIds,
518    ) -> Result<SyncProgress> {
519        let active = self
520            .active
521            .as_ref()
522            .ok_or_else(|| anyhow::anyhow!("Missing personal room"))?;
523        let strategy = fields(&active.strategy)?;
524        let policies = strategy
525            .iter()
526            .filter(|v| v.number == 11)
527            .map(|v| match v.value {
528                Value::Varint(n) => u32::try_from(n).map_err(Into::into),
529                _ => bail!("Invalid call policy wire type"),
530            })
531            .collect::<Result<Vec<u32>>>()?;
532        let timeout = if policies.len() < 83 {
533            6
534        } else if (3..=50).contains(&policies[82]) {
535            policies[82]
536        } else {
537            10
538        };
539        let request = personal::live_sync(active.live_id, &active.cookies, timeout, tasks.sdk)?;
540        let body = self.host(
541            client,
542            state,
543            CallHostRoute::LiveSync,
544            request.body(),
545            tasks.host,
546        )?;
547        let response = personal::LiveSyncResponse::decode(&body)?;
548        ensure!(
549            response.unknown_fields.is_empty(),
550            "Unknown caller sync response field"
551        );
552        let control = self
553            .control
554            .as_ref()
555            .ok_or_else(|| anyhow::anyhow!("Missing personal control"))?;
556        let origin = match control.origin {
557            ControlOrigin::Outgoing { .. } => termination::Origin::Caller,
558            ControlOrigin::Incoming => termination::Origin::Callee,
559        };
560        let local_status = if matches!(control.origin, ControlOrigin::Incoming)
561            && self.phase != PersonalPhase::Joined
562        {
563            3
564        } else {
565            1
566        };
567        let original_invite =
568            matches!(control.origin,ControlOrigin::Outgoing{invite_id}if invite_id!=0);
569        let mut invitation_pending = control.invitation_pending;
570        let mut pending_recipients = control.pending_recipients.clone();
571        let mut terminal = None;
572        let mut next = self.active.as_ref().expect("active room").clone();
573        'notifications: for item in response.items {
574            ensure!(
575                item.kind == 1 && item.unknown_fields.is_empty(),
576                "Unsupported caller sync item"
577            );
578            let message = fields(&item.body)?;
579            only(&message, &[1, 2])?;
580            ensure!(
581                number(&message, 1, None)? == 10002,
582                "Unsupported caller sync command"
583            );
584            let notify = fields(data(&message, 2)?)?;
585            only(&notify, &[1, 2, 3, 4, 5])?;
586            ensure!(
587                number(&notify, 3, None)? == next.identity.room
588                    && matches_room_key(&notify, 4, next.identity.room_key)?,
589                "Caller notification room mismatch"
590            );
591            for raw in repeated(&notify, 5, 64)? {
592                let item = fields(raw)?;
593                only(&item, &[1, 2])?;
594                let kind = number(&item, 1, None)?;
595                let bytes = data(&item, 2)?;
596                match kind {
597                    10 => {
598                        let update = fields(bytes)?;
599                        only(&update, &[1, 2, 3, 4])?;
600                        ensure!(
601                            matches_room_key(&update, 1, next.identity.room)?
602                                && matches_room_key(&update, 2, next.identity.room_key)?,
603                            "Caller roster room mismatch"
604                        );
605                        let sequence = u32::try_from(number(&update, 3, Some(0))?)?;
606                        if sequence <= next.member_sequence {
607                            continue;
608                        }
609                        let members = parse_roster_entries(
610                            repeated(&update, 4, 16)?,
611                            &self.own_username,
612                            &next.peer,
613                        )?;
614                        for (name, member) in &members {
615                            if let Some(old) = next.original_members.get(name) {
616                                ensure!(
617                                    old.member == member.member && old.openid == member.openid,
618                                    "Caller member identity changed"
619                                );
620                            } else {
621                                ensure!(
622                                    matches!(origin, termination::Origin::Callee)
623                                        && name == &next.peer
624                                        && next.unbound_peer_openid.as_deref()
625                                            == Some(member.openid.as_slice())
626                                        && u32::from(member.member) != next.identity.own_member,
627                                    "Unbound caller member identity mismatch"
628                                );
629                                next.original_members.insert(name.clone(), member.clone());
630                                next.unbound_peer_openid = None;
631                            }
632                        }
633                        if members
634                            .get(&next.peer)
635                            .is_some_and(|member| member.status == 1)
636                        {
637                            invitation_pending = false;
638                            pending_recipients.clear();
639                        }
640                        let action = termination::classify(
641                            origin,
642                            local_status,
643                            members.get(&self.own_username).map(|m| m.status),
644                            members.get(&next.peer).map(|m| m.status),
645                            original_invite && invitation_pending && !pending_recipients.is_empty(),
646                        )?;
647                        // A later authoritative own-removal proof suppresses
648                        // an unsubmitted teardown; no later row revives us.
649                        if !matches!(terminal, Some(RemoteTermination::LocalStop(_)))
650                            && (terminal.is_none()
651                                || matches!(action, Some(RemoteTermination::LocalStop(_))))
652                        {
653                            terminal = action;
654                        }
655                        next.members = members;
656                        next.member_sequence = sequence;
657                        if matches!(terminal, Some(RemoteTermination::LocalStop(_))) {
658                            // Verified loss/handling of this local participant
659                            // ends its reducer. Do not revive it or turn later
660                            // ancillary items into a reason to stay on the call.
661                            // The transport retains the complete received reply.
662                            break 'notifications;
663                        }
664                    }
665                    11 => {
666                        let (sequence, engine, channel) = cap(bytes)?;
667                        if sequence <= next.cap_sequence {
668                            continue;
669                        }
670                        ensure!(
671                            !next.media_taken || terminal.is_some(),
672                            "Post-start call capability reconfiguration unsupported"
673                        );
674                        next.cap_sequence = sequence;
675                        next.engine = engine;
676                        next.channel = channel;
677                    }
678                    12 => {
679                        let (sequence, av) = av(
680                            bytes,
681                            next.identity.own_member as u16,
682                            next.original_members
683                                .get(&next.peer)
684                                .context("Call AV arrived before caller member binding")?
685                                .member,
686                        )?;
687                        if sequence <= next.av_sequence {
688                            continue;
689                        }
690                        ensure!(
691                            !next.media_taken
692                                || terminal.is_some()
693                                || microphone_update(&next, &av, &self.own_username),
694                            "Post-start call AV reconfiguration unsupported"
695                        );
696                        next.av_sequence = sequence;
697                        next.av = av;
698                    }
699                    _ => bail!("Unsupported caller notification kind"),
700                }
701            }
702        }
703        if let Some(action) = terminal {
704            next.cookies = response.cookies.unwrap_or_default();
705            let control = self.control.as_mut().expect("personal control");
706            control.peer_joined = Some(
707                next.members
708                    .get(&next.peer)
709                    .is_some_and(|member| member.status == 1),
710            );
711            control.invitation_pending = invitation_pending;
712            control.pending_recipients = pending_recipients;
713            control.terminal = Some(action);
714            self.phase = if matches!(action, RemoteTermination::LocalStop(_)) {
715                PersonalPhase::Finished
716            } else {
717                PersonalPhase::TerminalPending
718            };
719            self.active = Some(next);
720            return Ok(SyncProgress::Terminal(action));
721        }
722        let media_result = match if local_status == 3 {
723            Ok(false)
724        } else {
725            ready(&next, &self.own_username)
726        } {
727            Err(error) if !error.is::<UnsupportedCallMedia>() => return Err(error),
728            result => result,
729        };
730        // A supported full batch can publish its control state. Unsupported
731        // media retains the previous cursor and permits no further media work.
732        if media_result.is_ok() {
733            next.cookies = response.cookies.unwrap_or_default();
734        }
735        let joined = next
736            .members
737            .get(&next.peer)
738            .is_some_and(|member| member.status == 1);
739        self.control
740            .as_mut()
741            .ok_or_else(|| anyhow::anyhow!("Missing allocated control"))?
742            .peer_joined = Some(joined);
743        let control = self.control.as_mut().expect("personal control");
744        control.invitation_pending = invitation_pending;
745        control.pending_recipients = pending_recipients;
746        self.phase = if (joined || !invitation_pending) && local_status == 1 {
747            PersonalPhase::Joined
748        } else {
749            PersonalPhase::Ringing
750        };
751        self.active = Some(next);
752        Ok(if media_result? {
753            SyncProgress::MediaReady
754        } else {
755            SyncProgress::Waiting
756        })
757    }
758    /// Move one admitted media context out only after current roster/capability/AV
759    /// state agrees. This never constructs a synthetic callee Join response.
760    pub fn take_media(
761        &mut self,
762        state: &AccountState,
763        current: AppAuthority,
764        connection_index: usize,
765        udp_index: usize,
766    ) -> Result<NegotiatedMedia> {
767        self.check(state, current)?;
768        let active = self
769            .active
770            .as_mut()
771            .ok_or_else(|| anyhow::anyhow!("Missing personal room"))?;
772        ensure!(
773            self.phase == PersonalPhase::Joined
774                && !active.media_taken
775                && ready(active, &self.own_username)?,
776            "Personal media not ready or already taken"
777        );
778        let relay = SelectedRelay::from_strategy(
779            active.identity.room,
780            active.identity.room_key,
781            active.identity.own_member as u16,
782            &active.strategy,
783            connection_index,
784            udp_index,
785        )?;
786        active.media_taken = true;
787        Ok(NegotiatedMedia {
788            relay,
789            peer_member: active.members[&active.peer].member,
790            frames_per_packet: (profile_shape(&active.engine, &active.channel)?[1] >> 8) as u8,
791        })
792    }
793    /// Cancel the original unanswered personal invitation once. This selected
794    /// client owns one original pending peer; no unrelated recipients are added.
795    pub fn cancel(
796        &mut self,
797        client: &impl PersonalTransport,
798        state: &mut AccountState,
799        current: AppAuthority,
800        tasks: TaskIds,
801    ) -> Result<()> {
802        self.check(state, current)?;
803        ensure!(
804            self.phase == PersonalPhase::Ringing,
805            "Personal invitation no longer cancellable"
806        );
807        self.reserve(tasks)?;
808        self.phase = PersonalPhase::Ending;
809        let result = (|| {
810            let control = self
811                .control
812                .as_ref()
813                .ok_or_else(|| anyhow::anyhow!("Missing allocated control"))?;
814            let room = control.identity;
815            let ControlOrigin::Outgoing { invite_id } = control.origin else {
816                bail!("Incoming calls cannot use caller Cancel")
817            };
818            ensure!(
819                control.peer_joined == Some(false)
820                    && control.invitation_pending
821                    && !control.pending_recipients.is_empty(),
822                "Pending invitation provenance missing or already joined"
823            );
824            let request = personal::cancel(
825                &self.parameters.inputs(),
826                room.room,
827                room.room_key,
828                invite_id,
829                &control.pending_recipients,
830                personal::CancelReason::Local,
831                tasks.sdk,
832            )?;
833            let body = self.host(
834                client,
835                state,
836                CallHostRoute::Cancel,
837                request.body(),
838                tasks.host,
839            )?;
840            let outer = fields(&body)?;
841            let cancelled = fields(data(&outer, 1)?)?;
842            ensure!(
843                number(&cancelled, 1, None)? == room.room
844                    && number(&cancelled, 2, None)? == room.room_key,
845                "Personal cancellation room mismatch"
846            );
847            Ok(())
848        })();
849        self.phase = if result.is_ok() {
850            PersonalPhase::Finished
851        } else {
852            PersonalPhase::Uncertain
853        };
854        result
855    }
856    /// Complete one source-classified remote termination. This is separate from
857    /// local Hangup: the peer may already be absent or nonjoined. The embedding
858    /// owner must stop media before calling. Failed replies remain uncertain.
859    pub fn finish_remote(
860        &mut self,
861        client: &impl PersonalTransport,
862        state: &mut AccountState,
863        current: AppAuthority,
864        tasks: TaskIds,
865        talk_success: bool,
866    ) -> Result<()> {
867        self.check(state, current)?;
868        ensure!(
869            self.phase == PersonalPhase::TerminalPending,
870            "No pending remote teardown"
871        );
872        self.reserve(tasks)?;
873        self.phase = PersonalPhase::Ending;
874        let result = (|| {
875            let control = self
876                .control
877                .as_mut()
878                .ok_or_else(|| anyhow::anyhow!("Missing remote termination control"))?;
879            let action = control
880                .terminal
881                .take()
882                .ok_or_else(|| anyhow::anyhow!("Remote termination already spent"))?;
883            let identity = control.identity;
884            match action {
885                RemoteTermination::Exit(reason) => {
886                    let request = personal::exit_remote(
887                        &self.parameters.inputs(),
888                        identity,
889                        reason,
890                        talk_success,
891                        tasks.sdk,
892                    )?;
893                    let parameters = AppFrameParameters {
894                        sdk_version: self.parameters.sdk_version()?,
895                        cloud_device_id: &self.parameters.cloud_device_id,
896                        host_client_version: state.device.client_version as u32,
897                        app_config_version: self.parameters.app_config_version,
898                    };
899                    let body = app_frame::prepare_transfer(
900                        &self.app,
901                        current,
902                        CallOperation::Exit,
903                        request.body.as_deref().expect("prepared remote Exit"),
904                        &parameters,
905                    )?;
906                    let reply =
907                        self.host(client, state, CallHostRoute::Transfer, &body, tasks.host)?;
908                    let exit = fields(&app_frame::decode_transfer_reply(
909                        &self.app,
910                        current,
911                        CallOperation::Exit,
912                        &reply,
913                    )?)?;
914                    ensure!(
915                        number(&exit, 1, None)? == identity.room
916                            && number(&exit, 2, None)? == identity.room_key
917                            && number(&exit, 3, None)? == u64::from(identity.own_member),
918                        "Remote Exit identity mismatch"
919                    );
920                }
921                RemoteTermination::NoResponseCancel => {
922                    let ControlOrigin::Outgoing { invite_id } = control.origin else {
923                        bail!("Callee cannot cancel an original invitation")
924                    };
925                    ensure!(
926                        control.invitation_pending
927                            && !control.pending_recipients.is_empty()
928                            && control
929                                .pending_recipients
930                                .iter()
931                                .all(|recipient| recipient == &control.peer),
932                        "Missing current pending-recipient provenance"
933                    );
934                    control.invitation_pending = false;
935                    let recipients = std::mem::take(&mut control.pending_recipients);
936                    let request = personal::cancel(
937                        &self.parameters.inputs(),
938                        identity.room,
939                        identity.room_key,
940                        invite_id,
941                        &recipients,
942                        personal::CancelReason::NoResponse,
943                        tasks.sdk,
944                    )?;
945                    let body = self.host(
946                        client,
947                        state,
948                        CallHostRoute::Cancel,
949                        request.body(),
950                        tasks.host,
951                    )?;
952                    let outer = fields(&body)?;
953                    let cancelled = fields(data(&outer, 1)?)?;
954                    ensure!(
955                        number(&cancelled, 1, None)? == identity.room
956                            && number(&cancelled, 2, None)? == identity.room_key,
957                        "Remote cancellation room mismatch"
958                    );
959                }
960                RemoteTermination::LocalStop(_) => {
961                    bail!("Local-only stop must not submit a teardown")
962                }
963            }
964            Ok(())
965        })();
966        self.phase = if result.is_ok() {
967            PersonalPhase::Finished
968        } else {
969            PersonalPhase::Uncertain
970        };
971        result
972    }
973
974    /// Submit joined Exit once. `talk_success` is the independently established
975    /// source predicate, not inferred here from a phone timer. Reliable peer
976    /// notice and canonical history remain distinct from accepted server Exit.
977    pub fn exit(
978        &mut self,
979        client: &impl PersonalTransport,
980        state: &mut AccountState,
981        current: AppAuthority,
982        tasks: TaskIds,
983        talk_success: bool,
984    ) -> Result<()> {
985        self.check(state, current)?;
986        ensure!(
987            self.phase == PersonalPhase::Joined,
988            "Personal call not joined"
989        );
990        self.reserve(tasks)?;
991        self.phase = PersonalPhase::Ending;
992        let result = (|| {
993            let control = self
994                .control
995                .as_ref()
996                .ok_or_else(|| anyhow::anyhow!("Missing allocated control"))?;
997            ensure!(
998                matches!(control.origin, ControlOrigin::Incoming) || !control.invitation_pending,
999                "Joined control state unknown"
1000            );
1001            let identity = control.identity;
1002            let request = personal::exit(
1003                &self.parameters.inputs(),
1004                identity,
1005                1,
1006                talk_success,
1007                tasks.sdk,
1008            )?;
1009            let parameters = AppFrameParameters {
1010                sdk_version: self.parameters.sdk_version()?,
1011                cloud_device_id: &self.parameters.cloud_device_id,
1012                host_client_version: state.device.client_version as u32,
1013                app_config_version: self.parameters.app_config_version,
1014            };
1015            let body = app_frame::prepare_transfer(
1016                &self.app,
1017                current,
1018                CallOperation::Exit,
1019                request.body.as_deref().expect("prepared Exit body"),
1020                &parameters,
1021            )?;
1022            let delegate = self.host(client, state, CallHostRoute::Transfer, &body, tasks.host)?;
1023            let exit = fields(&app_frame::decode_transfer_reply(
1024                &self.app,
1025                current,
1026                CallOperation::Exit,
1027                &delegate,
1028            )?)?;
1029            ensure!(
1030                number(&exit, 1, None)? == identity.room
1031                    && number(&exit, 2, None)? == identity.room_key
1032                    && number(&exit, 3, None)? == u64::from(identity.own_member),
1033                "Personal Exit identity mismatch"
1034            );
1035            Ok(())
1036        })();
1037        self.phase = if result.is_ok() {
1038            PersonalPhase::Finished
1039        } else {
1040            PersonalPhase::Uncertain
1041        };
1042        result
1043    }
1044}
1045
1046fn common(state: &AccountState, host_task: u32, body: &[u8]) -> Result<Vec<u8>> {
1047    CommonRequest {
1048        base_request: Some(wire::base_request(&state.device, state.uin, host_task)?),
1049        body: Some(body.to_vec()),
1050        unknown_fields: vec![],
1051    }
1052    .encode()
1053}
1054fn fields(raw: &[u8]) -> Result<Vec<Field>> {
1055    ensure!(raw.len() <= 64 * 1024, "Personal signaling body too large");
1056    super::signaling::decode_fields(raw)
1057}
1058fn data(values: &[Field], n: u32) -> Result<&[u8]> {
1059    p::optional_data(values, n)?.ok_or_else(|| anyhow::anyhow!("Missing personal bytes field {n}"))
1060}
1061fn number(values: &[Field], n: u32, default: Option<u64>) -> Result<u64> {
1062    match p::optional(values, n)? {
1063        Some(Value::Varint(v)) => Ok(*v),
1064        None => default.ok_or_else(|| anyhow::anyhow!("Missing personal integer {n}")),
1065        _ => bail!("Invalid personal integer {n}"),
1066    }
1067}
1068fn repeated(values: &[Field], n: u32, limit: usize) -> Result<Vec<&[u8]>> {
1069    let mut out = Vec::new();
1070    for f in values.iter().filter(|v| v.number == n) {
1071        ensure!(out.len() < limit, "Too many personal entries");
1072        if let Value::Bytes(raw) = &f.value {
1073            out.push(raw.as_slice());
1074        } else {
1075            bail!("Invalid personal entry wire type");
1076        }
1077    }
1078    Ok(out)
1079}
1080fn only(values: &[Field], allowed: &[u32]) -> Result<()> {
1081    ensure!(
1082        values.iter().all(|f| allowed.contains(&f.number)),
1083        "Unknown personal signaling field"
1084    );
1085    Ok(())
1086}
1087fn matches_room_key(values: &[Field], n: u32, expected: u64) -> Result<bool> {
1088    let value = number(values, n, Some(0))?;
1089    Ok(value == 0 || value == expected)
1090}
1091fn roster(raw: &[u8], own: &str, peer: &str, own_member: u16) -> Result<BTreeMap<String, Member>> {
1092    let values = fields(raw)?;
1093    roster_entries(repeated(&values, 2, 16)?, own, peer, own_member)
1094}
1095fn roster_entries(
1096    entries: Vec<&[u8]>,
1097    own: &str,
1098    peer: &str,
1099    own_member: u16,
1100) -> Result<BTreeMap<String, Member>> {
1101    let members = parse_roster_entries(entries, own, peer)?;
1102    ensure!(
1103        members.len() == 2
1104            && members
1105                .get(own)
1106                .is_some_and(|m| m.member == own_member && m.status == 1)
1107            && members
1108                .get(peer)
1109                .is_some_and(|m| (1..=3).contains(&m.status)),
1110        "Incomplete or unsupported personal roster"
1111    );
1112    Ok(members)
1113}
1114fn parse_roster_entries(
1115    entries: Vec<&[u8]>,
1116    own: &str,
1117    peer: &str,
1118) -> Result<BTreeMap<String, Member>> {
1119    ensure!(entries.len() <= 2, "Unsupported direct roster size");
1120    let mut members = BTreeMap::new();
1121    let mut ids = BTreeSet::new();
1122    for raw in entries {
1123        let values = fields(raw)?;
1124        only(&values, &[1, 2, 3, 4, 5, 6, 7, 8, 9])?;
1125        for tag in [4, 5, 9] {
1126            if let Some(Value::Varint(value)) = p::optional(&values, tag)? {
1127                u32::try_from(*value)?;
1128            } else {
1129                ensure!(
1130                    p::optional(&values, tag)?.is_none(),
1131                    "Invalid member scalar wire type"
1132                );
1133            }
1134        }
1135        for tag in [6, 8] {
1136            if let Some(value) = p::optional_data(&values, tag)? {
1137                std::str::from_utf8(value)?;
1138            }
1139        }
1140        let name = std::str::from_utf8(data(&values, 7)?)?;
1141        ensure!(
1142            (name == own || name == peer) && !members.contains_key(name),
1143            "Unexpected personal roster identity"
1144        );
1145        let member = u16::try_from(number(&values, 2, None)?)?;
1146        let status = u32::try_from(number(&values, 3, None)?)?;
1147        let openid = data(&values, 1)?.to_vec();
1148        ensure!(
1149            !openid.is_empty() && ids.insert(member),
1150            "Incomplete or duplicate personal member"
1151        );
1152        members.insert(
1153            name.to_owned(),
1154            Member {
1155                member,
1156                status,
1157                openid,
1158            },
1159        );
1160    }
1161    Ok(members)
1162}
1163fn cap(raw: &[u8]) -> Result<(u32, Vec<u8>, Vec<u8>)> {
1164    let values = fields(raw)?;
1165    only(&values, &[1, 2])?;
1166    let sequence = u32::try_from(number(&values, 1, Some(0))?)?;
1167    ensure!(sequence != 0, "Missing current call capability sequence");
1168    let content = fields(data(&values, 2)?)?;
1169    only(&content, &[1, 2])?;
1170    let engine = data(&content, 1)?.to_vec();
1171    let channel = data(&content, 2)?.to_vec();
1172    ensure!(
1173        !engine.is_empty() && !channel.is_empty(),
1174        "Partial call capabilities unsupported"
1175    );
1176    profile_shape(&engine, &channel)?;
1177    Ok((sequence, engine, channel))
1178}
1179fn av(raw: &[u8], own: u16, peer: u16) -> Result<(u32, AvSnapshot)> {
1180    let values = fields(raw)?;
1181    only(&values, &[1, 3, 4])?;
1182    let sequence = u32::try_from(number(&values, 3, Some(0))?)?;
1183    let mut members = BTreeMap::new();
1184    for raw in repeated(&values, 4, 2)? {
1185        let values = fields(raw)?;
1186        only(&values, &[1, 2, 3])?;
1187        let member = u16::try_from(number(&values, 1, None)?)?;
1188        ensure!(
1189            [own, peer].contains(&member) && !members.contains_key(&member),
1190            "Unexpected AV member"
1191        );
1192        members.insert(
1193            member,
1194            (
1195                u32::try_from(number(&values, 2, None)?)?,
1196                p::optional_data(&values, 3)?
1197                    .map(|info| {
1198                        ensure!(info.len() <= 1024, "Microphone metadata too large");
1199                        Ok(info.to_vec())
1200                    })
1201                    .transpose()?,
1202            ),
1203        );
1204    }
1205    Ok((sequence, members))
1206}
1207fn profile_shape(engine: &[u8], channel: &[u8]) -> Result<[u64; 6]> {
1208    let ef = fields(engine)?;
1209    let cf = fields(channel)?;
1210    let mut seen = BTreeSet::new();
1211    for field in &ef {
1212        ensure!(
1213            (1..=42).contains(&field.number) && field.number != 41 && seen.insert(field.number),
1214            "Unsupported engine capability shape"
1215        );
1216        let Value::Varint(value) = field.value else {
1217            bail!("Invalid engine capability wire type")
1218        };
1219        if [1, 2, 3, 4, 25, 26, 27].contains(&field.number) {
1220            i32::try_from(value as i64)?;
1221        } else {
1222            u32::try_from(value)?;
1223        }
1224    }
1225    only(&cf, &[1, 2])?;
1226    Ok([
1227        number(&ef, 5, Some(0))?,
1228        number(&ef, 6, Some(0))?,
1229        number(&ef, 7, Some(0))?,
1230        number(&ef, 39, Some(0))?,
1231        u64::from(u32::try_from(number(&cf, 1, Some(0))?)?),
1232        u64::from(u32::try_from(number(&cf, 2, Some(0))?)?),
1233    ])
1234}
1235fn profile(engine: &[u8], channel: &[u8]) -> Result<()> {
1236    if !matches!(
1237        profile_shape(engine, channel)?,
1238        [17, 0x106 | 0x206, 0x800000, 0, 2, 3]
1239    ) {
1240        return Err(UnsupportedCallMedia.into());
1241    }
1242    Ok(())
1243}
1244/// A microphone change does not replace the already admitted media context.
1245/// Keep microphone metadata opaque: it cannot reset codec, crypto or counters.
1246fn microphone_update(room: &ActiveRoom, av: &AvSnapshot, own: &str) -> bool {
1247    let Some(local) = room.members.get(own) else {
1248        return false;
1249    };
1250    let Some(peer) = room.members.get(&room.peer) else {
1251        return false;
1252    };
1253    av.len() == 2
1254        && av
1255            .get(&local.member)
1256            .is_some_and(|(status, info)| *status == 1 && info.is_none())
1257        && av.get(&peer.member).is_some_and(|(status, _)| *status <= 1)
1258}
1259fn ready(room: &ActiveRoom, own: &str) -> Result<bool> {
1260    // Current capability bytes are validated even while a peer/AV stream is
1261    // pending. Codec0 is then unselected data, never decoder permission.
1262    profile_shape(&room.engine, &room.channel)?;
1263    let own_id = room
1264        .members
1265        .get(own)
1266        .context("Missing current own member")?
1267        .member;
1268    let peer = room
1269        .members
1270        .get(&room.peer)
1271        .context("Missing current peer member")?;
1272    if peer.status != 1
1273        || room.av.len() != 2
1274        || !(room.media_taken && microphone_update(room, &room.av, own)
1275            || [own_id, peer.member].iter().all(|id| {
1276                room.av
1277                    .get(id)
1278                    .is_some_and(|(status, info)| *status == 1 && info.is_none())
1279            }))
1280    {
1281        return Ok(false);
1282    }
1283    profile(&room.engine, &room.channel)?;
1284    Ok(true)
1285}
1286fn replace_group(raw: &[u8], live_id: u64, group: &str) -> Result<Vec<u8>> {
1287    let values = fields(raw)?;
1288    let mut encoded = Vec::new();
1289    for field in values {
1290        let value = match field.number {
1291            2 => p::uint(2, live_id),
1292            4 => p::bytes(4, group.as_bytes()),
1293            _ => match field.value {
1294                Value::Varint(v) => p::uint(field.number, v),
1295                Value::Bytes(v) => p::bytes(field.number, &v),
1296                Value::Fixed64(_) | Value::Fixed32(_) => bail!("Unsupported group field wire type"),
1297            },
1298        };
1299        encoded.push(value);
1300    }
1301    p::message(&encoded)
1302}
1303
1304#[cfg(test)]
1305mod tests {
1306    use super::*;
1307
1308    pub(super) struct FakeTransport(
1309        pub(super) std::cell::RefCell<std::collections::VecDeque<(CallHostRoute, Vec<u8>)>>,
1310    );
1311    impl PersonalTransport for FakeTransport {
1312        fn call_rpc(
1313            &self,
1314            state: &mut AccountState,
1315            route: CallHostRoute,
1316            request: &[u8],
1317        ) -> Result<Vec<u8>> {
1318            let (expected, reply) = self
1319                .0
1320                .borrow_mut()
1321                .pop_front()
1322                .ok_or_else(|| anyhow::anyhow!("Unexpected fake call RPC"))?;
1323            ensure!(route == expected, "Unexpected fake call route");
1324            let request = CommonRequest::decode(request)?;
1325            ensure!(
1326                request.base_request.is_some() && request.body.is_some(),
1327                "Missing host common wrapper"
1328            );
1329            source_request(route, &request)?;
1330            state.cookie = vec![route.command() as u8];
1331            Ok(reply)
1332        }
1333    }
1334    /// Independent selected request contract at the actual session/transport
1335    /// boundary. Literal field numbers and command/path values are not obtained
1336    /// by serializing another production request or reading its enum accessors.
1337    fn source_request(route: CallHostRoute, request: &CommonRequest) -> Result<()> {
1338        let base = fields(
1339            request
1340                .base_request
1341                .as_deref()
1342                .context("Missing host base")?,
1343        )?;
1344        assert_eq!(data(&base, 1)?, &[0]);
1345        assert_eq!(number(&base, 2, None)?, 1);
1346        assert!(number(&base, 6, None)? > 0);
1347        let body = fields(request.body.as_deref().context("Missing host body")?)?;
1348        match route {
1349            CallHostRoute::AppAuth => {
1350                let device = data(&fields(data(&body, 1)?)?, 1)?.to_vec();
1351                assert_eq!(device.len(), 32);
1352                let raw = request.body.as_deref().unwrap();
1353                assert_eq!(&raw[..4], &[10, 34, 10, 32]);
1354                assert_eq!(raw.len(), 36);
1355            }
1356            CallHostRoute::Invite => {
1357                assert_eq!(data(&body, 1)?, b"peer");
1358                assert_eq!(number(&body, 2, None)?, 2);
1359                assert!(number(&body, 5, None)? > 0);
1360                assert!(p::optional(&body, 6)?.is_none());
1361                assert_eq!(data(&body, 7)?, b"voip-2p");
1362                let client = fields(data(&body, 4)?)?;
1363                assert_eq!(number(&client, 1, None)?, 0x050000cb);
1364                assert_eq!(data(&client, 2)?, b"test");
1365                assert_eq!(number(&client, 3, None)?, 0);
1366                assert_eq!(number(&client, 4, None)?, 1);
1367                let caps = fields(data(&client, 5)?)?;
1368                assert_eq!(number(&fields(data(&caps, 1)?)?, 5, None)?, 0x40000);
1369                assert_eq!(data(&caps, 2)?.len(), 12);
1370            }
1371            CallHostRoute::LiveSync => {
1372                assert_eq!(number(&body, 1, None)?, 99);
1373                data(&body, 2)?;
1374                assert_eq!(data(&body, 4)?, b"voip-2p");
1375                assert_eq!(number(&body, 5, None)?, 6);
1376            }
1377            CallHostRoute::Cancel => {
1378                let inner = fields(data(&body, 1)?)?;
1379                assert_eq!(number(&inner, 3, None)?, 7);
1380                assert_eq!(number(&inner, 4, None)?, 9);
1381                assert_eq!(number(&inner, 5, None)?, number(&body, 2, None)?);
1382                assert!([1, 2].contains(&number(&inner, 6, None)?));
1383                assert_eq!(number(&body, 3, None)?, 7);
1384                assert_eq!(data(&body, 4)?, b"peer");
1385                assert_eq!(data(&body, 5)?, b"voip-2p");
1386            }
1387            CallHostRoute::RingingAck => {
1388                let inner = fields(data(&body, 1)?)?;
1389                assert_eq!(number(&inner, 4, None)?, 1);
1390                assert_eq!(number(&inner, 5, None)?, 7);
1391                assert_eq!(number(&inner, 6, None)?, 9);
1392                assert_eq!(number(&inner, 7, None)?, 0);
1393                assert_eq!(number(&inner, 13, None)?, 1);
1394                let caps = fields(data(&inner, 12)?)?;
1395                assert_eq!(number(&fields(data(&caps, 1)?)?, 5, None)?, 0x40000);
1396                assert_eq!(data(&caps, 2)?.len(), 12);
1397                assert_eq!(data(&body, 3)?, b"voip-2p");
1398            }
1399            CallHostRoute::Transfer => {
1400                assert_eq!(number(&body, 1, None)?, 1901);
1401                let command = number(&body, 2, None)? as u32;
1402                let path: &[u8] = match command {
1403                    3361 => b"/cloudim/cloud-bin/cloudvoipmtjoin",
1404                    3095 => b"/cloudim/cloud-bin/cloudvoipmtreject",
1405                    3380 => b"/cloudim/cloud-bin/cloudvoipmtexit",
1406                    _ => bail!("Unexpected synthetic Cloud command"),
1407                };
1408                assert_eq!(data(&body, 3)?, b"wechat");
1409                assert_eq!(data(&body, 4)?, path);
1410                let frame = app_frame::TaskFrame::decode(data(&body, 5)?, command)?;
1411                assert_eq!(frame.integers[&2], frame.integers[&22]);
1412                assert!(frame.integers[&2] > 0);
1413                assert_eq!(frame.strings[&24], b"wechat");
1414                let clear = app_frame::lz4_decode(&frame.payload, 64 * 1024)?;
1415                let cloud = fields(&clear)?;
1416                let app_base = fields(data(&cloud, 1)?)?;
1417                assert_eq!(number(&app_base, 1, None)?, frame.integers[&22]);
1418                assert_eq!(data(&app_base, 2)?, b"Ldeviceid");
1419                let inner = fields(data(&cloud, 2)?)?;
1420                if command == 3361 {
1421                    assert_eq!(number(&inner, 5, None)?, 2);
1422                    assert_eq!(number(&inner, 6, None)?, 1);
1423                    assert_eq!(number(&inner, 8, None)?, 0);
1424                    assert_eq!(number(&inner, 11, None)?, 7);
1425                    assert_eq!(number(&inner, 12, None)?, 9);
1426                } else {
1427                    assert_eq!(number(&inner, 3, None)?, 7);
1428                    assert_eq!(number(&inner, 4, None)?, 9);
1429                    assert_eq!(number(&inner, 5, None)?, 0);
1430                    let reason = number(&inner, 6, None)?;
1431                    if command == 3095 {
1432                        assert_eq!(reason, 1);
1433                    } else {
1434                        assert!([1, 5, 6, 7].contains(&reason));
1435                    }
1436                }
1437            }
1438        }
1439        Ok(())
1440    }
1441    pub(super) fn host_reply(body: &[u8]) -> Vec<u8> {
1442        p::message(&[p::bytes(1, &[8, 0]), p::bytes(2, body)]).unwrap()
1443    }
1444    pub(super) fn state() -> AccountState {
1445        AccountState {
1446            schema: 1,
1447            device: crate::DeviceProfile {
1448                device_id: vec![b'x'; 16],
1449                device_type: "Linux".to_owned(),
1450                client_version: -227271398,
1451            },
1452            uin: 1,
1453            own_username: "own".to_owned(),
1454            cookie: vec![],
1455            session_key: vec![1; 16],
1456            request_key: vec![],
1457            response_key: vec![],
1458            auto_auth_key: vec![],
1459            key_info: vec![],
1460            database_password: vec![],
1461            auth_snapshot: crate::AuthSnapshot::default(),
1462            long_host: "localhost".to_owned(),
1463            short_host: "localhost".to_owned(),
1464        }
1465    }
1466    pub(super) fn authority() -> AppAuthority {
1467        AppAuthority {
1468            account_generation: 1,
1469            logout_epoch: 0,
1470            credential_digest: [4; 32],
1471        }
1472    }
1473
1474    pub(super) fn capabilities_for_codec(codec: u64) -> (Vec<u8>, Vec<u8>) {
1475        (
1476            p::message(&[p::uint(5, codec), p::uint(6, 0x206), p::uint(7, 0x800000)]).unwrap(),
1477            p::message(&[p::uint(1, 2), p::uint(2, 3)]).unwrap(),
1478        )
1479    }
1480    pub(super) fn parameters() -> VoiceParameters {
1481        let offer =
1482            super::super::voice_offer::VoiceOffer::from_owned_random(0x01000000, [5; 32]).unwrap();
1483        VoiceParameters {
1484            sdk: p::message(&[p::uint(1, 0x050000cb), p::bytes(2, b"test")]).unwrap(),
1485            group: p::message(&[
1486                p::bytes(1, b"wechat"),
1487                p::uint(2, 0),
1488                p::bytes(3, b"voip-2p"),
1489                p::bytes(4, &[]),
1490                p::uint(5, 1),
1491                p::bytes(6, &[]),
1492                p::uint(7, 0),
1493            ])
1494            .unwrap(),
1495            device: vec![],
1496            engine_capability: offer.engine_advertisement().to_vec(),
1497            channel_capability: offer.advertised_channel_identity().to_vec(),
1498            network_type: 0,
1499            sim_type: 0,
1500            video_length: 0,
1501            video_ratio: 0,
1502            nics: vec![],
1503            cloud_device_id: b"Ldeviceid".to_vec(),
1504            app_config_version: 1,
1505        }
1506    }
1507    pub(super) fn member(name: &str, id: u16, status: u32) -> Vec<u8> {
1508        p::message(&[
1509            p::bytes(1, name.as_bytes()),
1510            p::uint(2, u64::from(id)),
1511            p::uint(3, u64::from(status)),
1512            p::bytes(7, name.as_bytes()),
1513        ])
1514        .unwrap()
1515    }
1516    pub(super) fn av_status(sequence: u32, peer_status: u32) -> Vec<u8> {
1517        let own = p::message(&[p::uint(1, 0), p::uint(2, 1)]).unwrap();
1518        let peer = p::message(&[p::uint(1, 1), p::uint(2, u64::from(peer_status))]).unwrap();
1519        p::message(&[
1520            p::uint(3, u64::from(sequence)),
1521            p::bytes(4, &own),
1522            p::bytes(4, &peer),
1523        ])
1524        .unwrap()
1525    }
1526    pub(super) fn auth_reply() -> Vec<u8> {
1527        auth_reply_uin(17)
1528    }
1529    fn auth_reply_uin(uin: u64) -> Vec<u8> {
1530        let auth = p::message(&[p::bytes(9, b"server"), p::uint(11, 1)]).unwrap();
1531        let unify = p::message(&[p::bytes(2, &auth), p::bytes(3, &p::uint(1, uin))]).unwrap();
1532        host_reply(&p::message(&[p::bytes(1, &[8, 0]), p::bytes(2, &unify)]).unwrap())
1533    }
1534    fn invite_reply() -> Vec<u8> {
1535        invite_reply_profile(17, 2)
1536    }
1537    fn invite_reply_profile(codec: u64, peer_status: u32) -> Vec<u8> {
1538        invite_reply_full(codec, peer_status, 0)
1539    }
1540    fn invite_reply_full(codec: u64, peer_status: u32, peer_av: u32) -> Vec<u8> {
1541        let (engine, channel) = capabilities_for_codec(codec);
1542        // Scalar0 may be absent in a current, unselected capability message.
1543        let engine = if codec == 0 {
1544            p::message(&[p::uint(6, 0x206), p::uint(7, 0x800000)]).unwrap()
1545        } else {
1546            engine
1547        };
1548        let cap = p::message(&[
1549            p::uint(1, 1),
1550            p::bytes(
1551                2,
1552                &p::message(&[p::bytes(1, &engine), p::bytes(2, &channel)]).unwrap(),
1553            ),
1554        ])
1555        .unwrap();
1556        let address = p::message(&[p::bytes(1, b"127.0.0.1"), p::uint(2, 6000)]).unwrap();
1557        let connection = p::message(&[p::uint(1, 1), p::bytes(4, &address)]).unwrap();
1558        let strategy = p::message(&[
1559            p::bytes(10, &[7; 32]),
1560            p::bytes(13, &cap),
1561            p::uint(14, 0),
1562            p::bytes(15, &connection),
1563        ])
1564        .unwrap();
1565        let roster = p::message(&[
1566            p::uint(1, 1),
1567            p::bytes(2, &member("own", 0, 1)),
1568            p::bytes(2, &member("peer", 1, peer_status)),
1569        ])
1570        .unwrap();
1571        let client = p::message(&[
1572            p::bytes(1, &strategy),
1573            p::bytes(2, &roster),
1574            p::bytes(3, &av_status(1, peer_av)),
1575            p::uint(5, 99),
1576            p::bytes(6, b"old"),
1577        ])
1578        .unwrap();
1579        host_reply(
1580            &p::message(&[
1581                p::uint(1, 7),
1582                p::uint(2, 9),
1583                p::uint(3, 0),
1584                p::bytes(4, b"group"),
1585                p::bytes(5, &client),
1586            ])
1587            .unwrap(),
1588        )
1589    }
1590    fn with_result(reply: &[u8], recipient: &str, code: u64) -> Vec<u8> {
1591        let common = fields(reply).unwrap();
1592        let mut body = data(&common, 2).unwrap().to_vec();
1593        body.extend(p::bytes(
1594            8,
1595            &p::message(&[p::bytes(1, recipient.as_bytes()), p::uint(2, code)]).unwrap(),
1596        ));
1597        host_reply(&body)
1598    }
1599    fn initial_roster_reply(sequence: u32, own: Option<u32>, peer: Option<u32>) -> Vec<u8> {
1600        let mut list = vec![p::uint(1, u64::from(sequence))];
1601        if let Some(status) = own {
1602            list.push(p::bytes(2, &member("own", 0, status)));
1603        }
1604        if let Some(status) = peer {
1605            list.push(p::bytes(2, &member("peer", 1, status)));
1606        }
1607        // No AV/codec data is needed for the source's earlier terminal reducer.
1608        let client = p::message(&[
1609            p::bytes(2, &p::message(&list).unwrap()),
1610            p::uint(5, 99),
1611            p::bytes(6, b"initial"),
1612        ])
1613        .unwrap();
1614        with_result(
1615            &host_reply(
1616                &p::message(&[
1617                    p::uint(1, 7),
1618                    p::uint(2, 9),
1619                    p::uint(3, 0),
1620                    p::bytes(4, b"group"),
1621                    p::bytes(5, &client),
1622                ])
1623                .unwrap(),
1624            ),
1625            "peer",
1626            23,
1627        )
1628    }
1629    fn capability_sync(engine: &[u8], channel: &[u8]) -> Vec<u8> {
1630        let content = p::message(&[p::bytes(1, engine), p::bytes(2, channel)]).unwrap();
1631        let cap = p::message(&[p::uint(1, 2), p::bytes(2, &content)]).unwrap();
1632        let notify = p::message(&[
1633            p::uint(3, 7),
1634            p::uint(4, 9),
1635            p::bytes(
1636                5,
1637                &p::message(&[p::uint(1, 11), p::bytes(2, &cap)]).unwrap(),
1638            ),
1639        ])
1640        .unwrap();
1641        let message = p::message(&[p::uint(1, 10002), p::bytes(2, &notify)]).unwrap();
1642        host_reply(
1643            &p::message(&[
1644                p::bytes(
1645                    1,
1646                    &p::message(&[p::uint(1, 1), p::bytes(2, &message)]).unwrap(),
1647                ),
1648                p::bytes(2, b"bad-cursor"),
1649            ])
1650            .unwrap(),
1651        )
1652    }
1653    fn microphone_sync(sequence: u32, status: u32, info: Option<&[u8]>) -> Vec<u8> {
1654        let own = p::message(&[p::uint(1, 0), p::uint(2, 1)]).unwrap();
1655        let mut peer = vec![p::uint(1, 1), p::uint(2, u64::from(status))];
1656        if let Some(info) = info {
1657            peer.push(p::bytes(3, info));
1658        }
1659        let av = p::message(&[
1660            p::uint(3, u64::from(sequence)),
1661            p::bytes(4, &own),
1662            p::bytes(4, &p::message(&peer).unwrap()),
1663        ])
1664        .unwrap();
1665        let notify = p::message(&[
1666            p::uint(3, 7),
1667            p::uint(4, 9),
1668            p::bytes(5, &p::message(&[p::uint(1, 12), p::bytes(2, &av)]).unwrap()),
1669        ])
1670        .unwrap();
1671        let message = p::message(&[p::uint(1, 10002), p::bytes(2, &notify)]).unwrap();
1672        host_reply(
1673            &p::message(&[
1674                p::bytes(
1675                    1,
1676                    &p::message(&[p::uint(1, 1), p::bytes(2, &message)]).unwrap(),
1677                ),
1678                p::bytes(2, b"mic-update"),
1679            ])
1680            .unwrap(),
1681        )
1682    }
1683    fn joined_sync(extra_unknown: bool) -> Vec<u8> {
1684        joined_sync_profile(extra_unknown, None)
1685    }
1686    fn joined_sync_profile(extra_unknown: bool, codec: Option<u64>) -> Vec<u8> {
1687        let members = p::message(&[
1688            p::uint(1, 7),
1689            p::uint(2, 9),
1690            p::uint(3, 2),
1691            p::bytes(4, &member("own", 0, 1)),
1692            p::bytes(4, &member("peer", 1, 1)),
1693        ])
1694        .unwrap();
1695        let mut notify = vec![
1696            p::uint(3, 7),
1697            p::uint(4, 9),
1698            p::bytes(
1699                5,
1700                &p::message(&[p::uint(1, 10), p::bytes(2, &members)]).unwrap(),
1701            ),
1702            p::bytes(
1703                5,
1704                &p::message(&[p::uint(1, 12), p::bytes(2, &av_status(2, 1))]).unwrap(),
1705            ),
1706        ];
1707        if let Some(codec) = codec {
1708            let (engine, channel) = capabilities_for_codec(codec);
1709            let content = p::message(&[p::bytes(1, &engine), p::bytes(2, &channel)]).unwrap();
1710            let cap = p::message(&[p::uint(1, 2), p::bytes(2, &content)]).unwrap();
1711            notify.push(p::bytes(
1712                5,
1713                &p::message(&[p::uint(1, 11), p::bytes(2, &cap)]).unwrap(),
1714            ));
1715        }
1716        if extra_unknown {
1717            notify.push(p::bytes(
1718                5,
1719                &p::message(&[p::uint(1, 999), p::bytes(2, &[])]).unwrap(),
1720            ));
1721        }
1722        let message = p::message(&[
1723            p::uint(1, 10002),
1724            p::bytes(2, &p::message(&notify).unwrap()),
1725        ])
1726        .unwrap();
1727        host_reply(
1728            &p::message(&[
1729                p::bytes(
1730                    1,
1731                    &p::message(&[p::uint(1, 1), p::bytes(2, &message)]).unwrap(),
1732                ),
1733                p::bytes(2, b"new"),
1734            ])
1735            .unwrap(),
1736        )
1737    }
1738    pub(super) fn exit_reply() -> Vec<u8> {
1739        exit_reply_uin(17)
1740    }
1741    fn exit_reply_uin(uin: u64) -> Vec<u8> {
1742        let exit = p::message(&[p::uint(1, 7), p::uint(2, 9), p::uint(3, 0)]).unwrap();
1743        let cloud =
1744            p::message(&[p::bytes(1, &[8, 0]), p::uint(2, uin), p::bytes(3, &exit)]).unwrap();
1745        let frame = app_frame::TaskFrame {
1746            command: 3380,
1747            integers: BTreeMap::from([
1748                (4, 0),
1749                (7, 0),
1750                (8, 0),
1751                (21, 0),
1752                (22, uin),
1753                (23, 0),
1754                (25, 0),
1755                (26, 0),
1756            ]),
1757            strings: BTreeMap::new(),
1758            payload: cloud,
1759        }
1760        .encode()
1761        .unwrap();
1762        host_reply(&p::message(&[p::uint(1, 0), p::bytes(2, &frame)]).unwrap())
1763    }
1764    #[test]
1765    fn fake_caller_auth_invite_full_sync_media_and_exit() {
1766        let transport = RecordingTransport {
1767            inner: FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
1768                (CallHostRoute::AppAuth, auth_reply_uin(u64::MAX)),
1769                (CallHostRoute::Invite, invite_reply()),
1770                (CallHostRoute::LiveSync, joined_sync(false)),
1771                (
1772                    CallHostRoute::LiveSync,
1773                    microphone_sync(3, 0, Some(&[8, 2])),
1774                ),
1775                (
1776                    CallHostRoute::LiveSync,
1777                    microphone_sync(4, 1, Some(&[8, 3])),
1778                ),
1779                (CallHostRoute::Transfer, exit_reply_uin(u64::MAX)),
1780            ]))),
1781            requests: Default::default(),
1782        };
1783        let mut state = state();
1784        let mut session = NativePersonalSession::authenticate(
1785            &transport,
1786            &mut state,
1787            authority(),
1788            1,
1789            [9; 32],
1790            parameters(),
1791        )
1792        .unwrap();
1793        session
1794            .invite(
1795                &transport,
1796                &mut state,
1797                authority(),
1798                "peer",
1799                123,
1800                TaskIds { sdk: 1, host: 2 },
1801            )
1802            .unwrap();
1803        assert_eq!(session.phase(), PersonalPhase::Ringing);
1804        assert_eq!(
1805            session
1806                .poll(
1807                    &transport,
1808                    &mut state,
1809                    authority(),
1810                    TaskIds { sdk: 2, host: 3 }
1811                )
1812                .unwrap(),
1813            SyncProgress::MediaReady
1814        );
1815        let media = session.take_media(&state, authority(), 0, 0).unwrap();
1816        assert_eq!(session.peer_microphone_muted(), Some(false));
1817        assert_eq!(media.peer_member, 1);
1818        assert_eq!(media.relay.room_id, 7);
1819        assert!(session.take_media(&state, authority(), 0, 0).is_err());
1820        for (sdk, host, status) in [(3, 4, 0), (4, 5, 1)] {
1821            assert_eq!(
1822                session
1823                    .poll(&transport, &mut state, authority(), TaskIds { sdk, host })
1824                    .unwrap(),
1825                SyncProgress::MediaReady
1826            );
1827            assert_eq!(session.phase(), PersonalPhase::Joined);
1828            assert_eq!(session.peer_microphone_muted(), Some(status == 0));
1829            let active = session.active.as_ref().unwrap();
1830            assert_eq!(active.av.get(&1).unwrap().0, status);
1831            assert!(active.media_taken);
1832            assert!(session.take_media(&state, authority(), 0, 0).is_err());
1833        }
1834        let active = session.active.as_ref().unwrap();
1835        let mut unsupported = active.av.clone();
1836        unsupported.get_mut(&1).unwrap().0 = 4;
1837        assert!(!microphone_update(
1838            active,
1839            &unsupported,
1840            &state.own_username
1841        ));
1842        session
1843            .exit(
1844                &transport,
1845                &mut state,
1846                authority(),
1847                TaskIds { sdk: 5, host: 6 },
1848                false,
1849            )
1850            .unwrap();
1851        assert_eq!(session.phase(), PersonalPhase::Finished);
1852        assert_eq!(session.peer_microphone_muted(), None);
1853        assert!(transport.inner.0.borrow().is_empty());
1854        let requests = transport.requests.borrow();
1855        let host_tasks = requests
1856            .iter()
1857            .map(|(_, raw)| {
1858                let common = CommonRequest::decode(raw).unwrap();
1859                number(
1860                    &fields(common.base_request.as_deref().unwrap()).unwrap(),
1861                    6,
1862                    None,
1863                )
1864                .unwrap()
1865            })
1866            .collect::<Vec<_>>();
1867        assert_eq!(host_tasks, [1, 2, 3, 4, 5, 6]);
1868        let common = CommonRequest::decode(&requests.last().unwrap().1).unwrap();
1869        let delegate = fields(common.body.as_deref().unwrap()).unwrap();
1870        let frame = app_frame::TaskFrame::decode(data(&delegate, 5).unwrap(), 3380).unwrap();
1871        assert_eq!(frame.integers[&2], u64::MAX);
1872        assert_eq!(frame.integers[&22], u64::MAX);
1873        let common = fields(&session.app.common_session(authority()).unwrap()).unwrap();
1874        assert_eq!(number(&common, 4, None).unwrap(), u64::MAX);
1875        assert_eq!(data(&common, 5).unwrap(), b"server");
1876        assert_eq!(number(&common, 6, None).unwrap(), 1);
1877        assert!(p::optional(&common, 1).unwrap().is_none());
1878    }
1879    #[test]
1880    fn later_unknown_sync_item_prevents_cookie_and_partial_member_install() {
1881        let transport = FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
1882            (CallHostRoute::AppAuth, auth_reply()),
1883            (CallHostRoute::Invite, invite_reply()),
1884            (CallHostRoute::LiveSync, joined_sync(true)),
1885        ])));
1886        let mut state = state();
1887        let mut session = NativePersonalSession::authenticate(
1888            &transport,
1889            &mut state,
1890            authority(),
1891            1,
1892            [9; 32],
1893            parameters(),
1894        )
1895        .unwrap();
1896        session
1897            .invite(
1898                &transport,
1899                &mut state,
1900                authority(),
1901                "peer",
1902                123,
1903                TaskIds { sdk: 1, host: 2 },
1904            )
1905            .unwrap();
1906        assert!(session
1907            .poll(
1908                &transport,
1909                &mut state,
1910                authority(),
1911                TaskIds { sdk: 2, host: 3 }
1912            )
1913            .is_err());
1914        assert_eq!(session.phase(), PersonalPhase::Uncertain);
1915        let active = session.active.as_ref().unwrap();
1916        assert_eq!(active.cookies, b"old");
1917        assert_eq!(active.members["peer"].status, 2);
1918        assert!(session
1919            .poll(
1920                &transport,
1921                &mut state,
1922                authority(),
1923                TaskIds { sdk: 3, host: 4 }
1924            )
1925            .is_err());
1926    }
1927
1928    #[test]
1929    fn only_proven_unsent_poll_retains_known_room_for_exit() {
1930        struct StopTransport<'a> {
1931            inner: &'a FakeTransport,
1932            unsent: bool,
1933        }
1934        impl PersonalTransport for StopTransport<'_> {
1935            fn call_rpc(
1936                &self,
1937                state: &mut AccountState,
1938                route: CallHostRoute,
1939                request: &[u8],
1940            ) -> Result<Vec<u8>> {
1941                if route == CallHostRoute::LiveSync {
1942                    if self.unsent {
1943                        return Err(CallRpcNotSubmitted.into());
1944                    }
1945                    bail!("Ambiguous transport failure")
1946                }
1947                self.inner.call_rpc(state, route, request)
1948            }
1949        }
1950        for unsent in [true, false] {
1951            let transport =
1952                FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
1953                    (CallHostRoute::AppAuth, auth_reply()),
1954                    (CallHostRoute::Invite, invite_reply()),
1955                    (CallHostRoute::LiveSync, joined_sync(false)),
1956                    (CallHostRoute::Transfer, exit_reply()),
1957                ])));
1958            let mut state = state();
1959            let mut session = NativePersonalSession::authenticate(
1960                &transport,
1961                &mut state,
1962                authority(),
1963                1,
1964                [9; 32],
1965                parameters(),
1966            )
1967            .unwrap();
1968            session
1969                .invite(
1970                    &transport,
1971                    &mut state,
1972                    authority(),
1973                    "peer",
1974                    123,
1975                    TaskIds { sdk: 1, host: 2 },
1976                )
1977                .unwrap();
1978            session
1979                .poll(
1980                    &transport,
1981                    &mut state,
1982                    authority(),
1983                    TaskIds { sdk: 2, host: 3 },
1984                )
1985                .unwrap();
1986            let cookie = state.cookie.clone();
1987            let stop = StopTransport {
1988                inner: &transport,
1989                unsent,
1990            };
1991            assert!(session
1992                .poll(&stop, &mut state, authority(), TaskIds { sdk: 3, host: 4 })
1993                .is_err());
1994            assert_eq!(state.cookie, cookie);
1995            if unsent {
1996                assert_eq!(session.phase(), PersonalPhase::Joined);
1997                session
1998                    .exit(
1999                        &transport,
2000                        &mut state,
2001                        authority(),
2002                        TaskIds { sdk: 4, host: 5 },
2003                        false,
2004                    )
2005                    .unwrap();
2006                assert_eq!(session.phase(), PersonalPhase::Finished);
2007            } else {
2008                assert_eq!(session.phase(), PersonalPhase::Uncertain);
2009            }
2010        }
2011    }
2012
2013    #[test]
2014    fn pending_codec_zero_continues_sync_until_both_av_and_selected_codec_are_ready() {
2015        let transport = FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2016            (CallHostRoute::AppAuth, auth_reply()),
2017            (
2018                CallHostRoute::Invite,
2019                with_result(&invite_reply_profile(0, 2), "peer", 23),
2020            ),
2021            (
2022                CallHostRoute::LiveSync,
2023                host_reply(&p::bytes(2, b"waiting")),
2024            ),
2025            (
2026                CallHostRoute::LiveSync,
2027                joined_sync_profile(false, Some(17)),
2028            ),
2029        ])));
2030        let mut state = state();
2031        let mut session = NativePersonalSession::authenticate(
2032            &transport,
2033            &mut state,
2034            authority(),
2035            1,
2036            [9; 32],
2037            parameters(),
2038        )
2039        .unwrap();
2040        session
2041            .invite(
2042                &transport,
2043                &mut state,
2044                authority(),
2045                "peer",
2046                123,
2047                TaskIds { sdk: 1, host: 2 },
2048            )
2049            .unwrap();
2050        assert_eq!(session.phase(), PersonalPhase::Ringing);
2051        assert!(!session.media_ready(&state, authority()).unwrap());
2052        assert!(session.take_media(&state, authority(), 0, 0).is_err());
2053        assert!(p::optional(
2054            &fields(&session.active.as_ref().unwrap().engine).unwrap(),
2055            5
2056        )
2057        .unwrap()
2058        .is_none());
2059        assert_eq!(
2060            session
2061                .poll(
2062                    &transport,
2063                    &mut state,
2064                    authority(),
2065                    TaskIds { sdk: 2, host: 3 }
2066                )
2067                .unwrap(),
2068            SyncProgress::Waiting
2069        );
2070        assert_eq!(session.active.as_ref().unwrap().cookies, b"waiting");
2071        assert_eq!(
2072            session
2073                .poll(
2074                    &transport,
2075                    &mut state,
2076                    authority(),
2077                    TaskIds { sdk: 3, host: 4 }
2078                )
2079                .unwrap(),
2080            SyncProgress::MediaReady
2081        );
2082        assert!(session.media_ready(&state, authority()).unwrap());
2083        assert_eq!(
2084            session
2085                .take_media(&state, authority(), 0, 0)
2086                .unwrap()
2087                .peer_member,
2088            1
2089        );
2090    }
2091    #[test]
2092    fn pending_capability_shapes_are_checked_before_cursor_installation() {
2093        let (engine, channel) = capabilities_for_codec(0);
2094        for (bad_engine, bad_channel) in [
2095            ([engine.clone(), p::uint(99, 0)].concat(), channel.clone()),
2096            ([engine.clone(), p::uint(6, 0)].concat(), channel.clone()),
2097            (p::bytes(6, b"wrong-wire"), channel.clone()),
2098            (
2099                engine.clone(),
2100                p::message(&[p::uint(1, u64::MAX), p::uint(2, 3)]).unwrap(),
2101            ),
2102        ] {
2103            let transport =
2104                FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2105                    (CallHostRoute::AppAuth, auth_reply()),
2106                    (CallHostRoute::Invite, invite_reply_profile(0, 2)),
2107                    (
2108                        CallHostRoute::LiveSync,
2109                        capability_sync(&bad_engine, &bad_channel),
2110                    ),
2111                ])));
2112            let mut state = state();
2113            let mut session = NativePersonalSession::authenticate(
2114                &transport,
2115                &mut state,
2116                authority(),
2117                1,
2118                [9; 32],
2119                parameters(),
2120            )
2121            .unwrap();
2122            session
2123                .invite(
2124                    &transport,
2125                    &mut state,
2126                    authority(),
2127                    "peer",
2128                    123,
2129                    TaskIds { sdk: 1, host: 2 },
2130                )
2131                .unwrap();
2132            assert!(session
2133                .poll(
2134                    &transport,
2135                    &mut state,
2136                    authority(),
2137                    TaskIds { sdk: 2, host: 3 }
2138                )
2139                .is_err());
2140            assert_eq!(session.phase(), PersonalPhase::Uncertain);
2141            let active = session.active.as_ref().unwrap();
2142            assert_eq!(active.cookies, b"old");
2143            assert_eq!(active.cap_sequence, 1);
2144            assert_eq!(active.members["peer"].status, 2);
2145            assert!(session.take_media(&state, authority(), 0, 0).is_err());
2146        }
2147    }
2148    #[test]
2149    fn initial_full_roster_terminates_before_profile_and_recipient_result_is_not_overall_error() {
2150        for (own, peer, action) in [
2151            (
2152                Some(1),
2153                Some(6),
2154                RemoteTermination::Exit(personal::RemoteExitReason::PeerBusy),
2155            ),
2156            (
2157                Some(1),
2158                Some(7),
2159                RemoteTermination::Exit(personal::RemoteExitReason::PeerRejected),
2160            ),
2161            (
2162                Some(1),
2163                None,
2164                RemoteTermination::Exit(personal::RemoteExitReason::LastPeerLeft),
2165            ),
2166            (
2167                None,
2168                Some(1),
2169                RemoteTermination::LocalStop(LocalStopReason::SelfRemoved),
2170            ),
2171            (
2172                Some(2),
2173                Some(1),
2174                RemoteTermination::LocalStop(LocalStopReason::SelfRemoved),
2175            ),
2176        ] {
2177            let local = matches!(action, RemoteTermination::LocalStop(_));
2178            let mut replies = std::collections::VecDeque::from([
2179                (CallHostRoute::AppAuth, auth_reply()),
2180                (CallHostRoute::Invite, initial_roster_reply(1, own, peer)),
2181            ]);
2182            if !local {
2183                replies.push_back((CallHostRoute::Transfer, exit_reply()));
2184            }
2185            let transport = RecordingTransport {
2186                inner: FakeTransport(std::cell::RefCell::new(replies)),
2187                requests: Default::default(),
2188            };
2189            let mut state = state();
2190            let mut session = NativePersonalSession::authenticate(
2191                &transport,
2192                &mut state,
2193                authority(),
2194                1,
2195                [9; 32],
2196                parameters(),
2197            )
2198            .unwrap();
2199            session
2200                .invite(
2201                    &transport,
2202                    &mut state,
2203                    authority(),
2204                    "peer",
2205                    123,
2206                    TaskIds { sdk: 1, host: 2 },
2207                )
2208                .unwrap();
2209            assert_eq!(session.control.as_ref().unwrap().terminal, Some(action));
2210            assert!(session.active.is_none());
2211            assert!(!session.media_ready(&state, authority()).unwrap());
2212            if local {
2213                assert_eq!(session.phase(), PersonalPhase::Finished);
2214            } else {
2215                assert_eq!(session.phase(), PersonalPhase::TerminalPending);
2216                session
2217                    .finish_remote(
2218                        &transport,
2219                        &mut state,
2220                        authority(),
2221                        TaskIds { sdk: 2, host: 3 },
2222                        false,
2223                    )
2224                    .unwrap();
2225                let expected = match action {
2226                    RemoteTermination::Exit(reason) => reason.wire(),
2227                    _ => unreachable!(),
2228                };
2229                assert_eq!(
2230                    recorded_exit_reason(&transport.requests.borrow().last().unwrap().1),
2231                    expected
2232                );
2233            }
2234            assert_eq!(session.phase(), PersonalPhase::Finished);
2235            assert!(transport.inner.0.borrow().is_empty());
2236        }
2237    }
2238    #[test]
2239    fn initial_zero_sequence_or_unknown_membership_is_not_terminal_proof() {
2240        for (sequence, own, peer) in [
2241            (0, None, Some(1)),
2242            (1, Some(1), Some(99)),
2243            (1, Some(1), Some(8)),
2244        ] {
2245            let transport =
2246                FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2247                    (CallHostRoute::AppAuth, auth_reply()),
2248                    (
2249                        CallHostRoute::Invite,
2250                        initial_roster_reply(sequence, own, peer),
2251                    ),
2252                ])));
2253            let mut state = state();
2254            let mut session = NativePersonalSession::authenticate(
2255                &transport,
2256                &mut state,
2257                authority(),
2258                1,
2259                [9; 32],
2260                parameters(),
2261            )
2262            .unwrap();
2263            assert!(session
2264                .invite(
2265                    &transport,
2266                    &mut state,
2267                    authority(),
2268                    "peer",
2269                    123,
2270                    TaskIds { sdk: 1, host: 2 }
2271                )
2272                .is_err());
2273            assert_eq!(session.phase(), PersonalPhase::Uncertain);
2274            assert!(session.control.as_ref().unwrap().terminal.is_none());
2275            assert!(session
2276                .finish_remote(
2277                    &transport,
2278                    &mut state,
2279                    authority(),
2280                    TaskIds { sdk: 2, host: 3 },
2281                    false
2282                )
2283                .is_err());
2284            assert!(transport.0.borrow().is_empty());
2285        }
2286    }
2287    #[test]
2288    fn pending_unselected_control_is_not_media_admission_and_cancels_once() {
2289        let cancel = host_reply(
2290            &p::message(&[p::bytes(
2291                1,
2292                &p::message(&[p::uint(1, 7), p::uint(2, 9)]).unwrap(),
2293            )])
2294            .unwrap(),
2295        );
2296        let transport = FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2297            (CallHostRoute::AppAuth, auth_reply()),
2298            (CallHostRoute::Invite, invite_reply_profile(0, 2)),
2299            (CallHostRoute::Cancel, cancel),
2300        ])));
2301        let mut state = state();
2302        let mut session = NativePersonalSession::authenticate(
2303            &transport,
2304            &mut state,
2305            authority(),
2306            1,
2307            [9; 32],
2308            parameters(),
2309        )
2310        .unwrap();
2311        session
2312            .invite(
2313                &transport,
2314                &mut state,
2315                authority(),
2316                "peer",
2317                123,
2318                TaskIds { sdk: 1, host: 2 },
2319            )
2320            .unwrap();
2321        assert!(!session.media_ready(&state, authority()).unwrap());
2322        assert!(session.has_allocated_control());
2323        assert_eq!(session.phase(), PersonalPhase::Ringing);
2324        assert!(session.take_media(&state, authority(), 0, 0).is_err());
2325        struct CheckCancel<'a>(&'a FakeTransport);
2326        impl PersonalTransport for CheckCancel<'_> {
2327            fn call_rpc(
2328                &self,
2329                state: &mut AccountState,
2330                route: CallHostRoute,
2331                request: &[u8],
2332            ) -> Result<Vec<u8>> {
2333                assert_eq!(route, CallHostRoute::Cancel);
2334                let common = CommonRequest::decode(request)?;
2335                let outer = fields(common.body.as_deref().unwrap())?;
2336                let inner = fields(data(&outer, 1)?)?;
2337                assert_eq!(number(&inner, 3, None)?, 7);
2338                assert_eq!(number(&inner, 4, None)?, 9);
2339                assert_eq!(number(&inner, 5, None)?, 123);
2340                assert_eq!(data(&outer, 4)?, b"peer");
2341                self.0.call_rpc(state, route, request)
2342            }
2343        }
2344        session
2345            .cancel(
2346                &CheckCancel(&transport),
2347                &mut state,
2348                authority(),
2349                TaskIds { sdk: 2, host: 3 },
2350            )
2351            .unwrap();
2352        assert_eq!(session.phase(), PersonalPhase::Finished);
2353        assert!(transport.0.borrow().is_empty());
2354        assert!(session
2355            .cancel(
2356                &transport,
2357                &mut state,
2358                authority(),
2359                TaskIds { sdk: 3, host: 4 }
2360            )
2361            .is_err());
2362    }
2363    #[test]
2364    fn missing_cleanup_reply_stays_uncertain_and_is_not_retried() {
2365        let transport = FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2366            (CallHostRoute::AppAuth, auth_reply()),
2367            (CallHostRoute::Invite, invite_reply_profile(0, 2)),
2368        ])));
2369        let mut state = state();
2370        let mut session = NativePersonalSession::authenticate(
2371            &transport,
2372            &mut state,
2373            authority(),
2374            1,
2375            [9; 32],
2376            parameters(),
2377        )
2378        .unwrap();
2379        session
2380            .invite(
2381                &transport,
2382                &mut state,
2383                authority(),
2384                "peer",
2385                123,
2386                TaskIds { sdk: 1, host: 2 },
2387            )
2388            .unwrap();
2389        assert!(session
2390            .cancel(
2391                &transport,
2392                &mut state,
2393                authority(),
2394                TaskIds { sdk: 2, host: 3 }
2395            )
2396            .is_err());
2397        assert_eq!(session.phase(), PersonalPhase::Uncertain);
2398        assert!(session
2399            .cancel(
2400                &transport,
2401                &mut state,
2402                authority(),
2403                TaskIds { sdk: 3, host: 4 }
2404            )
2405            .is_err());
2406        assert!(transport.0.borrow().is_empty());
2407    }
2408    pub(super) fn roster_sync(
2409        sequence: u32,
2410        own: Option<u32>,
2411        peer: Option<u32>,
2412        unknown_after: bool,
2413    ) -> Vec<u8> {
2414        let mut roster = vec![
2415            p::uint(1, 7),
2416            p::uint(2, 9),
2417            p::uint(3, u64::from(sequence)),
2418        ];
2419        if let Some(status) = own {
2420            roster.push(p::bytes(4, &member("own", 0, status)));
2421        }
2422        if let Some(status) = peer {
2423            roster.push(p::bytes(4, &member("peer", 1, status)));
2424        }
2425        let mut notify = vec![
2426            p::uint(3, 7),
2427            p::uint(4, 9),
2428            p::bytes(
2429                5,
2430                &p::message(&[p::uint(1, 10), p::bytes(2, &p::message(&roster).unwrap())]).unwrap(),
2431            ),
2432        ];
2433        if unknown_after {
2434            notify.push(p::bytes(
2435                5,
2436                &p::message(&[p::uint(1, 999), p::bytes(2, &[])]).unwrap(),
2437            ));
2438        }
2439        let message = p::message(&[
2440            p::uint(1, 10002),
2441            p::bytes(2, &p::message(&notify).unwrap()),
2442        ])
2443        .unwrap();
2444        host_reply(
2445            &p::message(&[
2446                p::bytes(
2447                    1,
2448                    &p::message(&[p::uint(1, 1), p::bytes(2, &message)]).unwrap(),
2449                ),
2450                p::bytes(2, b"terminal-cursor"),
2451            ])
2452            .unwrap(),
2453        )
2454    }
2455    pub(super) struct RecordingTransport {
2456        pub(super) inner: FakeTransport,
2457        pub(super) requests: std::cell::RefCell<Vec<(CallHostRoute, Vec<u8>)>>,
2458    }
2459    impl PersonalTransport for RecordingTransport {
2460        fn call_rpc(
2461            &self,
2462            state: &mut AccountState,
2463            route: CallHostRoute,
2464            request: &[u8],
2465        ) -> Result<Vec<u8>> {
2466            self.requests.borrow_mut().push((route, request.to_vec()));
2467            self.inner.call_rpc(state, route, request)
2468        }
2469    }
2470    fn caller_with(
2471        transport: &impl PersonalTransport,
2472        state: &mut AccountState,
2473    ) -> NativePersonalSession {
2474        let mut session = NativePersonalSession::authenticate(
2475            transport,
2476            state,
2477            authority(),
2478            1,
2479            [9; 32],
2480            parameters(),
2481        )
2482        .unwrap();
2483        session
2484            .invite(
2485                transport,
2486                state,
2487                authority(),
2488                "peer",
2489                123,
2490                TaskIds { sdk: 1, host: 2 },
2491            )
2492            .unwrap();
2493        session
2494    }
2495    pub(super) fn recorded_exit_reason(request: &[u8]) -> u64 {
2496        let common = CommonRequest::decode(request).unwrap();
2497        let delegate = fields(common.body.as_deref().unwrap()).unwrap();
2498        assert_eq!(number(&delegate, 2, None).unwrap(), 3380);
2499        let frame = app_frame::TaskFrame::decode(data(&delegate, 5).unwrap(), 3380).unwrap();
2500        let cloud = fields(&app_frame::lz4_decode(&frame.payload, 64 * 1024).unwrap()).unwrap();
2501        number(&fields(data(&cloud, 2).unwrap()).unwrap(), 6, None).unwrap()
2502    }
2503    #[test]
2504    fn caller_remote_last_peer_uses_one_exit_with_distinct_reason_and_no_local_cancel() {
2505        for (peer, wire) in [(4, 5), (6, 7), (7, 6)] {
2506            let transport = RecordingTransport {
2507                inner: FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2508                    (CallHostRoute::AppAuth, auth_reply()),
2509                    (CallHostRoute::Invite, invite_reply()),
2510                    (
2511                        CallHostRoute::LiveSync,
2512                        roster_sync(2, Some(1), Some(peer), false),
2513                    ),
2514                    (CallHostRoute::Transfer, exit_reply()),
2515                ]))),
2516                requests: Default::default(),
2517            };
2518            let mut state = state();
2519            let mut session = caller_with(&transport, &mut state);
2520            assert!(matches!(
2521                session
2522                    .poll(
2523                        &transport,
2524                        &mut state,
2525                        authority(),
2526                        TaskIds { sdk: 2, host: 3 }
2527                    )
2528                    .unwrap(),
2529                SyncProgress::Terminal(RemoteTermination::Exit(_))
2530            ));
2531            assert_eq!(session.phase(), PersonalPhase::TerminalPending);
2532            assert!(session.take_media(&state, authority(), 0, 0).is_err());
2533            session
2534                .finish_remote(
2535                    &transport,
2536                    &mut state,
2537                    authority(),
2538                    TaskIds { sdk: 3, host: 4 },
2539                    false,
2540                )
2541                .unwrap();
2542            let requests = transport.requests.borrow();
2543            let last = requests.last().unwrap();
2544            assert_eq!(last.0, CallHostRoute::Transfer);
2545            assert_eq!(recorded_exit_reason(&last.1), wire);
2546            assert_eq!(session.phase(), PersonalPhase::Finished);
2547            assert!(session
2548                .finish_remote(
2549                    &transport,
2550                    &mut state,
2551                    authority(),
2552                    TaskIds { sdk: 4, host: 5 },
2553                    false
2554                )
2555                .is_err());
2556            assert!(transport.inner.0.borrow().is_empty());
2557        }
2558    }
2559    #[test]
2560    fn no_response_cancel_uses_original_id_and_direct_invite_timeout_wire_reason() {
2561        let cancel = host_reply(&p::bytes(
2562            1,
2563            &p::message(&[p::uint(1, 7), p::uint(2, 9)]).unwrap(),
2564        ));
2565        let transport = RecordingTransport {
2566            inner: FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2567                (CallHostRoute::AppAuth, auth_reply()),
2568                (CallHostRoute::Invite, invite_reply()),
2569                (
2570                    CallHostRoute::LiveSync,
2571                    roster_sync(2, Some(1), Some(8), false),
2572                ),
2573                (CallHostRoute::Cancel, cancel),
2574            ]))),
2575            requests: Default::default(),
2576        };
2577        let mut state = state();
2578        let mut session = caller_with(&transport, &mut state);
2579        assert_eq!(
2580            session
2581                .poll(
2582                    &transport,
2583                    &mut state,
2584                    authority(),
2585                    TaskIds { sdk: 2, host: 3 }
2586                )
2587                .unwrap(),
2588            SyncProgress::Terminal(RemoteTermination::NoResponseCancel)
2589        );
2590        session
2591            .finish_remote(
2592                &transport,
2593                &mut state,
2594                authority(),
2595                TaskIds { sdk: 3, host: 4 },
2596                false,
2597            )
2598            .unwrap();
2599        let requests = transport.requests.borrow();
2600        let last = requests.last().unwrap();
2601        assert_eq!(last.0, CallHostRoute::Cancel);
2602        let common = CommonRequest::decode(&last.1).unwrap();
2603        let outer = fields(common.body.as_deref().unwrap()).unwrap();
2604        assert_eq!(number(&outer, 2, None).unwrap(), 123);
2605        assert_eq!(repeated(&outer, 4, 16).unwrap(), vec![b"peer".as_slice()]);
2606
2607        let inner = fields(data(&outer, 1).unwrap()).unwrap();
2608        assert_eq!(number(&inner, 5, None).unwrap(), 123);
2609        assert_eq!(number(&inner, 6, None).unwrap(), 2);
2610    }
2611    #[test]
2612    fn previously_joined_peer_permanently_disarms_original_pending_cancel() {
2613        let transport = FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2614            (CallHostRoute::AppAuth, auth_reply()),
2615            (CallHostRoute::Invite, invite_reply()),
2616            (CallHostRoute::LiveSync, joined_sync(false)),
2617            (
2618                CallHostRoute::LiveSync,
2619                roster_sync(3, Some(1), Some(8), false),
2620            ),
2621        ])));
2622        let mut state = state();
2623        let mut session = caller_with(&transport, &mut state);
2624        assert_eq!(
2625            session
2626                .poll(
2627                    &transport,
2628                    &mut state,
2629                    authority(),
2630                    TaskIds { sdk: 2, host: 3 }
2631                )
2632                .unwrap(),
2633            SyncProgress::MediaReady
2634        );
2635        assert!(!session.control.as_ref().unwrap().invitation_pending);
2636        assert!(session
2637            .control
2638            .as_ref()
2639            .unwrap()
2640            .pending_recipients
2641            .is_empty());
2642        assert!(session
2643            .poll(
2644                &transport,
2645                &mut state,
2646                authority(),
2647                TaskIds { sdk: 3, host: 4 }
2648            )
2649            .is_err());
2650        assert_eq!(session.phase(), PersonalPhase::Uncertain);
2651        assert_eq!(session.active.as_ref().unwrap().cookies, b"new");
2652        assert!(session
2653            .finish_remote(
2654                &transport,
2655                &mut state,
2656                authority(),
2657                TaskIds { sdk: 4, host: 5 },
2658                false
2659            )
2660            .is_err());
2661        assert!(transport.0.borrow().is_empty());
2662    }
2663}