Skip to main content

sipx_call/
snapshot.rs

1//! Bounded, versioned persistence for confirmed-dialog protocol state.
2//!
3//! A snapshot is not a serialized [`crate::Call`]. It deliberately excludes every socket,
4//! transaction, task, clock instant, credential and media key. The host supplies fresh runtime
5//! resources through [`DialogRestoreContext`] and this module validates the complete attachment
6//! before [`crate::Call::restore_dialog`] publishes a call.
7
8use std::fmt;
9use std::net::SocketAddr;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::time::Duration;
13
14use bytes::Bytes;
15use sipx_media::{Codec, MediaSession};
16use sipx_sdp::{Direction, RtcpMode};
17use sipx_sip::Uri;
18use sipx_sip::headers::Address;
19use sipx_transport::{Handle, Target};
20use tokio::time::Instant;
21
22use crate::dialog::{DialogId, Role};
23use crate::{Codecs, Keying, MediaAddress, MediaPolicy, MediaProfile, NegotiatedKeying};
24
25const MAGIC: &[u8; 4] = b"SXD1";
26const LEGACY_VERSION: u16 = 1;
27const VERSION: u16 = 2;
28const FLAG_CALLEE: u16 = 1 << 0;
29const FLAG_PROTECTED: u16 = 1 << 1;
30const FLAG_SESSION: u16 = 1 << 2;
31const FLAG_PEER_UPDATE: u16 = 1 << 3;
32const KNOWN_FLAGS: u16 = FLAG_CALLEE | FLAG_PROTECTED | FLAG_SESSION | FLAG_PEER_UPDATE;
33
34/// The maximum accepted encoded snapshot size.
35pub const MAX_SNAPSHOT_BYTES: usize = 262_144;
36/// The maximum sum of all variable-length fields.
37pub const MAX_VARIABLE_BYTES: usize = 131_072;
38/// The maximum Call-ID or tag size.
39pub const MAX_ID_BYTES: usize = 1_024;
40/// The maximum party, target, or route size.
41pub const MAX_FIELD_BYTES: usize = 8_192;
42/// The maximum number of routes retained by one dialog.
43pub const MAX_ROUTES: usize = 64;
44
45/// Why a live call cannot be captured without losing active protocol work.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
47#[non_exhaustive]
48pub enum DialogNotQuiescent {
49    /// The call has already ended.
50    #[error("the call has ended")]
51    Ended,
52    /// The dialog-forming 2xx is still awaiting its ACK.
53    #[error("the dialog-forming response is still awaiting ACK")]
54    AwaitingAck,
55    /// An offer, answer, or UPDATE transaction remains outstanding.
56    #[error("an offer, answer, or UPDATE remains outstanding")]
57    OfferAnswer,
58    /// An interrupted media replacement still owns a previous session awaiting cleanup.
59    #[error("a replaced media session is still awaiting cleanup")]
60    MediaCleanup,
61    /// A received or originated transfer usage remains attached.
62    #[error("a transfer usage remains attached")]
63    Transfer,
64    /// Version one cannot preserve a live ICE generation or its credentials.
65    #[error("a live ICE generation cannot be snapshotted")]
66    Ice,
67}
68
69/// The explicit timer input a host must drive before restoration can continue.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71#[non_exhaustive]
72pub enum DialogSessionAction {
73    /// This side must originate the negotiated refresh.
74    Refresh,
75    /// The peer's refresh did not arrive and the session must expire.
76    Expire,
77}
78
79/// A typed refusal from snapshot capture, decoding, or runtime attachment.
80#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
81#[non_exhaustive]
82pub enum DialogPersistenceError {
83    /// The fixed format marker was absent.
84    #[error("dialog snapshot has an invalid magic value")]
85    InvalidMagic,
86    /// The schema version is not implemented by this build.
87    #[error("dialog snapshot schema version {0} is unsupported")]
88    UnsupportedVersion(u16),
89    /// An encoded flag that the selected schema does not assign was set.
90    #[error("dialog snapshot has reserved flags set: {0:#06x}")]
91    ReservedFlags(u16),
92    /// The input ended before one complete field was available.
93    #[error("dialog snapshot is truncated in {field}")]
94    Truncated {
95        /// The field being read.
96        field: &'static str,
97    },
98    /// One field exceeded its independently enforced limit.
99    #[error("dialog snapshot field {field} is {len} bytes; the limit is {max}")]
100    FieldTooLarge {
101        /// The field being read.
102        field: &'static str,
103        /// The declared or observed size.
104        len: usize,
105        /// The accepted size.
106        max: usize,
107    },
108    /// The complete input exceeded the format ceiling.
109    #[error("dialog snapshot is {len} bytes; the limit is {max}")]
110    InputTooLarge {
111        /// The observed input size.
112        len: usize,
113        /// The accepted size.
114        max: usize,
115    },
116    /// Individually bounded fields exceeded their aggregate ceiling.
117    #[error("dialog snapshot variable fields exceed {MAX_VARIABLE_BYTES} bytes")]
118    VariableDataTooLarge,
119    /// A text field was not UTF-8.
120    #[error("dialog snapshot field {field} is not UTF-8")]
121    InvalidUtf8 {
122        /// The field being read.
123        field: &'static str,
124    },
125    /// A field did not satisfy its named protocol invariant.
126    #[error("dialog snapshot has an invalid {field}")]
127    InvalidValue {
128        /// The field that failed validation.
129        field: &'static str,
130    },
131    /// An optional marker had a non-canonical value other than zero or one.
132    #[error("dialog snapshot has a non-canonical presence marker for {field}")]
133    NonCanonicalPresence {
134        /// The optional field being read.
135        field: &'static str,
136    },
137    /// The two independently generated dialog tags are identical.
138    #[error("dialog snapshot repeats the same local and remote tag")]
139    DuplicateTags,
140    /// The route count exceeded the fixed collection bound.
141    #[error("dialog snapshot has {count} routes; the limit is {MAX_ROUTES}")]
142    TooManyRoutes {
143        /// The declared route count.
144        count: usize,
145    },
146    /// Bytes remained after the last schema field.
147    #[error("dialog snapshot has trailing bytes")]
148    TrailingBytes,
149    /// A local request cannot advance the retained sequence number.
150    #[error("dialog snapshot local CSeq is exhausted")]
151    CseqExhausted,
152    /// A live call still owns runtime-only protocol work.
153    #[error("dialog is not quiescent: {0}")]
154    NotQuiescent(DialogNotQuiescent),
155    /// The captured or restored session deadline is already due.
156    #[error("dialog session action is already due: {0:?}")]
157    SessionActionDue(DialogSessionAction),
158    /// Session duration values contradict their negotiated interval.
159    #[error("dialog snapshot session timer values are contradictory")]
160    SessionContradiction,
161    /// A positive remaining duration cannot be represented by the injected clock.
162    #[error("dialog session deadline overflows the injected clock")]
163    ClockOverflow,
164    /// Protected dialog state was attached to a clear signalling target.
165    #[error("protected dialog state cannot be restored through clear signalling")]
166    SecurityDowngrade,
167    /// The fresh media keying differs from the retained negotiated class.
168    #[error("fresh media security does not match the dialog snapshot")]
169    MediaSecurityMismatch,
170    /// One non-secret media wire fact differs from the snapshot.
171    #[error("fresh media does not match the snapshot field {field}")]
172    MediaContractMismatch {
173        /// The mismatching fact.
174        field: &'static str,
175    },
176    /// An RTP payload type cannot be represented by the seven-bit wire field.
177    #[error("dialog snapshot {field} {value} exceeds the RTP payload type range 0..=127")]
178    PayloadTypeOutOfRange {
179        /// The audio or DTMF payload field.
180        field: &'static str,
181        /// The refused unmasked value.
182        value: u8,
183    },
184    /// This fresh runtime attachment was already consumed by a successful restore.
185    #[error("dialog restore context is already attached to a call")]
186    ContextAlreadyAttached,
187    /// This build cannot run a codec represented by these bytes.
188    #[error("dialog snapshot codec id {0} is unavailable in this build")]
189    UnsupportedCodec(u8),
190    /// The fresh media address would be unusable in SDP.
191    #[error("the fresh advertised media address must not be unspecified")]
192    UnspecifiedMediaAddress,
193}
194
195#[derive(Clone, Copy, PartialEq, Eq)]
196pub(crate) struct SessionSnapshot {
197    pub(crate) interval: Duration,
198    pub(crate) we_refresh: bool,
199    pub(crate) remaining: Duration,
200}
201
202/// Immutable version-two facts needed to continue a confirmed dialog.
203#[derive(Clone)]
204pub struct DialogSnapshot {
205    role: Role,
206    id: DialogId,
207    local_party: String,
208    remote_party: String,
209    remote_target: Uri,
210    route_set: Vec<String>,
211    local_cseq: u32,
212    remote_cseq: Option<u32>,
213    protected_signalling: bool,
214    media_keying: NegotiatedKeying,
215    media_profile: MediaProfile,
216    codecs: Codecs,
217    codec: Codec,
218    /// RTP clock rate of the exact negotiated codec format.
219    clock_rate: u32,
220    /// Payload number from the peer's description, used to transmit.
221    payload_type: u8,
222    /// Payload number from our description, used to receive.
223    receive_payload_type: u8,
224    dtmf_payload_type: Option<u8>,
225    rtcp_mode: RtcpMode,
226    hold: Direction,
227    peer_allows_update: bool,
228    session: Option<SessionSnapshot>,
229}
230
231impl fmt::Debug for DialogSnapshot {
232    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
233        formatter
234            .debug_struct("DialogSnapshot")
235            .field("version", &VERSION)
236            .field("role", &self.role)
237            .field("call_id_bytes", &self.id.call_id.len())
238            .field("local_tag_bytes", &self.id.local_tag.len())
239            .field("remote_tag_bytes", &self.id.remote_tag.len())
240            .field("local_party_bytes", &self.local_party.len())
241            .field("remote_party_bytes", &self.remote_party.len())
242            .field("remote_target_bytes", &self.remote_target.to_bytes().len())
243            .field("routes", &self.route_set.len())
244            .field("local_cseq", &self.local_cseq)
245            .field("remote_cseq", &self.remote_cseq)
246            .field("protected_signalling", &self.protected_signalling)
247            .field("media_keying", &self.media_keying)
248            .field("media_profile", &self.media_profile)
249            .field("codecs", &self.codecs)
250            .field("codec", &self.codec)
251            .field("clock_rate", &self.clock_rate)
252            .field("payload_type", &self.payload_type)
253            .field("receive_payload_type", &self.receive_payload_type)
254            .field("dtmf_payload_type", &self.dtmf_payload_type)
255            .field("rtcp_mode", &self.rtcp_mode)
256            .field("hold", &self.hold)
257            .field("peer_allows_update", &self.peer_allows_update)
258            .field("has_session_timer", &self.session.is_some())
259            .finish()
260    }
261}
262
263impl DialogSnapshot {
264    /// The schema version emitted by this build.
265    #[must_use]
266    pub const fn version(&self) -> u16 {
267        VERSION
268    }
269
270    /// Which dialog role was captured.
271    #[must_use]
272    pub const fn role(&self) -> Role {
273        self.role
274    }
275
276    /// The retained dialog identifier.
277    #[must_use]
278    pub const fn dialog_id(&self) -> &DialogId {
279        &self.id
280    }
281
282    /// The validated local party value, without the separately retained dialog tag.
283    #[must_use]
284    pub fn local_party(&self) -> &str {
285        &self.local_party
286    }
287
288    /// The validated remote party value, without the separately retained dialog tag.
289    #[must_use]
290    pub fn remote_party(&self) -> &str {
291        &self.remote_party
292    }
293
294    /// The last locally used in-dialog sequence number.
295    #[must_use]
296    pub const fn local_cseq(&self) -> u32 {
297        self.local_cseq
298    }
299
300    /// The greatest accepted remote sequence number.
301    #[must_use]
302    pub const fn remote_cseq(&self) -> Option<u32> {
303        self.remote_cseq
304    }
305
306    /// The retained remote target from the latest target refresh.
307    #[must_use]
308    pub const fn remote_target(&self) -> &Uri {
309        &self.remote_target
310    }
311
312    /// The route set in send order.
313    #[must_use]
314    pub fn route_set(&self) -> &[String] {
315        &self.route_set
316    }
317
318    /// Whether the captured signalling path was protected.
319    #[must_use]
320    pub const fn protected_signalling(&self) -> bool {
321        self.protected_signalling
322    }
323
324    /// The negotiated media keying class. No key bytes are retained.
325    #[must_use]
326    pub const fn media_keying(&self) -> NegotiatedKeying {
327        self.media_keying
328    }
329
330    /// The retained named media profile.
331    #[must_use]
332    pub const fn media_profile(&self) -> MediaProfile {
333        self.media_profile
334    }
335
336    /// The exact codec policy used for later offers.
337    #[must_use]
338    pub const fn codecs(&self) -> Codecs {
339        self.codecs
340    }
341
342    /// The negotiated audio codec.
343    #[must_use]
344    pub const fn codec(&self) -> Codec {
345        self.codec
346    }
347
348    /// The RTP clock rate of the negotiated audio format.
349    #[must_use]
350    pub const fn clock_rate(&self) -> u32 {
351        self.clock_rate
352    }
353
354    /// The RTP payload type used to send the negotiated codec.
355    #[must_use]
356    pub const fn payload_type(&self) -> u8 {
357        self.payload_type
358    }
359
360    /// The RTP payload type accepted for the negotiated codec.
361    #[must_use]
362    pub const fn receive_payload_type(&self) -> u8 {
363        self.receive_payload_type
364    }
365
366    /// The dynamic RTP payload type carrying telephone events, when enabled.
367    #[must_use]
368    pub const fn dtmf_payload_type(&self) -> Option<u8> {
369        self.dtmf_payload_type
370    }
371
372    /// The negotiated RTCP socket shape.
373    #[must_use]
374    pub const fn rtcp_mode(&self) -> RtcpMode {
375        self.rtcp_mode
376    }
377
378    /// The most recently negotiated media direction.
379    #[must_use]
380    pub const fn direction(&self) -> Direction {
381        self.hold
382    }
383
384    /// Whether the peer advertised support for UPDATE.
385    #[must_use]
386    pub const fn peer_allows_update(&self) -> bool {
387        self.peer_allows_update
388    }
389
390    /// Negotiated interval, local refresher role, and remaining duration at capture.
391    #[must_use]
392    pub const fn session_timer(&self) -> Option<(Duration, bool, Duration)> {
393        match self.session {
394            Some(session) => Some((session.interval, session.we_refresh, session.remaining)),
395            None => None,
396        }
397    }
398
399    /// Encode this value in the deterministic version-two binary format.
400    ///
401    /// Values can only be obtained from validated capture or decoding, so this operation is
402    /// infallible and never writes a partial result to a caller-owned buffer.
403    #[must_use]
404    pub fn encode(&self) -> Vec<u8> {
405        let mut encoded = Vec::with_capacity(self.encoded_len().min(MAX_SNAPSHOT_BYTES));
406        encoded.extend_from_slice(MAGIC);
407        put_u16(&mut encoded, VERSION);
408        let mut flags = 0u16;
409        if self.role == Role::Callee {
410            flags |= FLAG_CALLEE;
411        }
412        if self.protected_signalling {
413            flags |= FLAG_PROTECTED;
414        }
415        if self.session.is_some() {
416            flags |= FLAG_SESSION;
417        }
418        if self.peer_allows_update {
419            flags |= FLAG_PEER_UPDATE;
420        }
421        put_u16(&mut encoded, flags);
422        put_bytes(&mut encoded, &self.id.call_id);
423        put_bytes(&mut encoded, &self.id.local_tag);
424        put_bytes(&mut encoded, &self.id.remote_tag);
425        put_bytes(&mut encoded, self.local_party.as_bytes());
426        put_bytes(&mut encoded, self.remote_party.as_bytes());
427        put_bytes(&mut encoded, &self.remote_target.to_bytes());
428        put_u16(
429            &mut encoded,
430            u16::try_from(self.route_set.len()).unwrap_or(u16::MAX),
431        );
432        for route in &self.route_set {
433            put_bytes(&mut encoded, route.as_bytes());
434        }
435        put_u32(&mut encoded, self.local_cseq);
436        put_optional_u32(&mut encoded, self.remote_cseq);
437        encoded.push(keying_id(self.media_keying));
438        encoded.push(profile_id(self.media_profile));
439        let preferences: Vec<_> = self.codecs.preferences().collect();
440        encoded.push(u8::try_from(preferences.len()).unwrap_or(u8::MAX));
441        for preference in preferences {
442            encoded.push(preference_id(preference));
443        }
444        encoded.push(codec_id(self.codec));
445        put_u32(&mut encoded, self.clock_rate);
446        encoded.push(self.payload_type);
447        encoded.push(self.receive_payload_type);
448        put_optional_u8(&mut encoded, self.dtmf_payload_type);
449        encoded.push(rtcp_id(self.rtcp_mode));
450        encoded.push(direction_id(self.hold));
451        // Version two only admits an idle offer state. Writing the state explicitly makes a
452        // future state additive without letting an old decoder mistake it for idle.
453        encoded.push(0);
454        if let Some(session) = self.session {
455            put_u64(&mut encoded, session.interval.as_secs());
456            encoded.push(u8::from(session.we_refresh));
457            let nanos = u64::try_from(session.remaining.as_nanos()).unwrap_or(u64::MAX);
458            put_u64(&mut encoded, nanos);
459        }
460        encoded
461    }
462
463    /// Decode and validate one complete snapshot.
464    ///
465    /// The total input bound is applied before any field is read. Per-field and aggregate bounds
466    /// are then checked before each allocation, so hostile length prefixes cannot request their
467    /// declared amount of memory.
468    #[allow(
469        clippy::too_many_lines,
470        reason = "one ordered read keeps the canonical field order and pre-allocation checks auditable"
471    )]
472    pub fn decode(input: &[u8]) -> Result<Self, DialogPersistenceError> {
473        if input.len() > MAX_SNAPSHOT_BYTES {
474            return Err(DialogPersistenceError::InputTooLarge {
475                len: input.len(),
476                max: MAX_SNAPSHOT_BYTES,
477            });
478        }
479        let mut reader = Reader::new(input);
480        if reader.exact(4, "magic")? != MAGIC {
481            return Err(DialogPersistenceError::InvalidMagic);
482        }
483        let version = reader.u16("version")?;
484        if !matches!(version, LEGACY_VERSION | VERSION) {
485            return Err(DialogPersistenceError::UnsupportedVersion(version));
486        }
487        let flags = reader.u16("flags")?;
488        if flags & !KNOWN_FLAGS != 0 {
489            return Err(DialogPersistenceError::ReservedFlags(flags & !KNOWN_FLAGS));
490        }
491        let role = if flags & FLAG_CALLEE == 0 {
492            Role::Caller
493        } else {
494            Role::Callee
495        };
496        let call_id = reader.bytes("Call-ID", MAX_ID_BYTES)?;
497        let local_tag = reader.bytes("local tag", MAX_ID_BYTES)?;
498        let remote_tag = reader.bytes("remote tag", MAX_ID_BYTES)?;
499        let local_party = reader.string("local party", MAX_FIELD_BYTES)?;
500        let remote_party = reader.string("remote party", MAX_FIELD_BYTES)?;
501        let target_bytes = reader.bytes("remote target", MAX_FIELD_BYTES)?;
502        let remote_target = Uri::parse(Bytes::from(target_bytes)).map_err(|_| {
503            DialogPersistenceError::InvalidValue {
504                field: "remote target",
505            }
506        })?;
507        let route_count = usize::from(reader.u16("route count")?);
508        if route_count > MAX_ROUTES {
509            return Err(DialogPersistenceError::TooManyRoutes { count: route_count });
510        }
511        let mut route_set = Vec::with_capacity(route_count);
512        for _ in 0..route_count {
513            route_set.push(reader.string("route", MAX_FIELD_BYTES)?);
514        }
515        let local_cseq = reader.u32("local CSeq")?;
516        let remote_cseq = reader.optional_u32("remote CSeq")?;
517        let media_keying = decode_keying(reader.u8("media keying")?)?;
518        let media_profile = decode_profile(reader.u8("media profile")?)?;
519        let preference_count = usize::from(reader.u8("codec preference count")?);
520        if preference_count == 0 || preference_count > 4 {
521            return Err(DialogPersistenceError::InvalidValue {
522                field: "codec preference count",
523            });
524        }
525        let mut preferences = Vec::with_capacity(preference_count);
526        for _ in 0..preference_count {
527            preferences.push(decode_preference(reader.u8("codec preference")?)?);
528        }
529        let codecs =
530            Codecs::ordered(&preferences).map_err(|_| DialogPersistenceError::InvalidValue {
531                field: "codec preferences",
532            })?;
533        let codec = decode_codec(reader.u8("codec")?)?;
534        let clock_rate = if version == LEGACY_VERSION {
535            codec.clock_rate()
536        } else {
537            reader.u32("media clock rate")?
538        };
539        let payload_type = reader.u8("payload type")?;
540        let receive_payload_type = if version == LEGACY_VERSION {
541            payload_type
542        } else {
543            reader.u8("receive payload type")?
544        };
545        let dtmf_payload_type = reader.optional_u8("DTMF payload type")?;
546        let rtcp_mode = decode_rtcp(reader.u8("RTCP mode")?)?;
547        let hold = decode_direction(reader.u8("hold direction")?)?;
548        if reader.u8("offer state")? != 0 {
549            return Err(DialogPersistenceError::InvalidValue {
550                field: "offer state",
551            });
552        }
553        let session = if flags & FLAG_SESSION == 0 {
554            None
555        } else {
556            let interval = Duration::from_secs(reader.u64("session interval")?);
557            let we_refresh = match reader.u8("session refresher")? {
558                0 => false,
559                1 => true,
560                _ => {
561                    return Err(DialogPersistenceError::NonCanonicalPresence {
562                        field: "session refresher",
563                    });
564                }
565            };
566            let remaining = Duration::from_nanos(reader.u64("session remaining")?);
567            Some(SessionSnapshot {
568                interval,
569                we_refresh,
570                remaining,
571            })
572        };
573        if !reader.is_empty() {
574            return Err(DialogPersistenceError::TrailingBytes);
575        }
576        let snapshot = Self {
577            role,
578            id: DialogId {
579                call_id,
580                local_tag,
581                remote_tag,
582            },
583            local_party,
584            remote_party,
585            remote_target,
586            route_set,
587            local_cseq,
588            remote_cseq,
589            protected_signalling: flags & FLAG_PROTECTED != 0,
590            media_keying,
591            media_profile,
592            codecs,
593            codec,
594            clock_rate,
595            payload_type,
596            receive_payload_type,
597            dtmf_payload_type,
598            rtcp_mode,
599            hold,
600            peer_allows_update: flags & FLAG_PEER_UPDATE != 0,
601            session,
602        };
603        snapshot.validate()?;
604        Ok(snapshot)
605    }
606
607    pub(crate) fn from_parts(parts: SnapshotParts) -> Result<Self, DialogPersistenceError> {
608        let snapshot = Self {
609            role: parts.role,
610            id: parts.id,
611            local_party: parts.local_party,
612            remote_party: parts.remote_party,
613            remote_target: parts.remote_target,
614            route_set: parts.route_set,
615            local_cseq: parts.local_cseq,
616            remote_cseq: parts.remote_cseq,
617            protected_signalling: parts.protected_signalling,
618            media_keying: parts.media_keying,
619            media_profile: parts.media_profile,
620            codecs: parts.codecs,
621            codec: parts.codec,
622            clock_rate: parts.clock_rate,
623            payload_type: parts.payload_type,
624            receive_payload_type: parts.receive_payload_type,
625            dtmf_payload_type: parts.dtmf_payload_type,
626            rtcp_mode: parts.rtcp_mode,
627            hold: parts.hold,
628            peer_allows_update: parts.peer_allows_update,
629            session: parts.session,
630        };
631        snapshot.validate()?;
632        Ok(snapshot)
633    }
634
635    pub(crate) fn validate_restore(
636        &self,
637        context: &DialogRestoreContext,
638    ) -> Result<Option<(Duration, bool, Instant)>, DialogPersistenceError> {
639        self.validate()?;
640        if self.protected_signalling && !context.target.transport.is_secure() {
641            return Err(DialogPersistenceError::SecurityDowngrade);
642        }
643        if requires_secure_route(&self.remote_target, &self.route_set)?
644            && !context.target.transport.is_secure()
645        {
646            return Err(DialogPersistenceError::SecurityDowngrade);
647        }
648        validate_media(self, context)?;
649        match self.session {
650            None => Ok(None),
651            Some(session) if session.remaining.is_zero() => {
652                Err(DialogPersistenceError::SessionActionDue(session.action()))
653            }
654            Some(session) => {
655                let remaining = session
656                    .remaining
657                    .checked_sub(context.elapsed_since_capture)
658                    .filter(|remaining| !remaining.is_zero())
659                    .ok_or_else(|| DialogPersistenceError::SessionActionDue(session.action()))?;
660                let deadline = context
661                    .now
662                    .checked_add(remaining)
663                    .ok_or(DialogPersistenceError::ClockOverflow)?;
664                Ok(Some((session.interval, session.we_refresh, deadline)))
665            }
666        }
667    }
668
669    pub(crate) fn dialog(&self) -> crate::Dialog {
670        crate::Dialog {
671            role: self.role,
672            id: self.id.clone(),
673            local_uri: self.local_party.clone(),
674            remote_uri: self.remote_party.clone(),
675            remote_target: self.remote_target.clone(),
676            local_cseq: self.local_cseq,
677            remote_cseq: self.remote_cseq,
678            route_set: self.route_set.clone(),
679        }
680    }
681
682    pub(crate) const fn media_profile_value(&self) -> MediaProfile {
683        self.media_profile
684    }
685
686    pub(crate) const fn codecs_value(&self) -> Codecs {
687        self.codecs
688    }
689
690    pub(crate) const fn negotiated(&self, remote: SocketAddr) -> crate::call::Negotiated {
691        crate::call::Negotiated {
692            remote,
693            codec: self.codec,
694            clock_rate: self.clock_rate,
695            payload_type: Some(self.payload_type),
696            receive_payload_type: Some(self.receive_payload_type),
697            dtmf: self.dtmf_payload_type,
698            rtcp_mode: self.rtcp_mode,
699        }
700    }
701
702    pub(crate) const fn peer_allows_update_value(&self) -> bool {
703        self.peer_allows_update
704    }
705
706    fn encoded_len(&self) -> usize {
707        64usize
708            .saturating_add(self.variable_len())
709            .saturating_add(self.route_set.len().saturating_mul(4))
710    }
711
712    fn variable_len(&self) -> usize {
713        self.id
714            .call_id
715            .len()
716            .saturating_add(self.id.local_tag.len())
717            .saturating_add(self.id.remote_tag.len())
718            .saturating_add(self.local_party.len())
719            .saturating_add(self.remote_party.len())
720            .saturating_add(self.remote_target.to_bytes().len())
721            .saturating_add(
722                self.route_set
723                    .iter()
724                    .fold(0usize, |sum, route| sum.saturating_add(route.len())),
725            )
726    }
727
728    fn validate(&self) -> Result<(), DialogPersistenceError> {
729        validate_bytes("Call-ID", &self.id.call_id, MAX_ID_BYTES, false)?;
730        if self.id.call_id.iter().any(|byte| *byte <= 0x20) {
731            return Err(DialogPersistenceError::InvalidValue { field: "Call-ID" });
732        }
733        validate_token("local tag", &self.id.local_tag)?;
734        validate_token("remote tag", &self.id.remote_tag)?;
735        if self.id.local_tag == self.id.remote_tag {
736            return Err(DialogPersistenceError::DuplicateTags);
737        }
738        validate_party("local party", &self.local_party)?;
739        validate_party("remote party", &self.remote_party)?;
740        validate_uri("remote target", &self.remote_target)?;
741        if self.route_set.len() > MAX_ROUTES {
742            return Err(DialogPersistenceError::TooManyRoutes {
743                count: self.route_set.len(),
744            });
745        }
746        for route in &self.route_set {
747            validate_route(route)?;
748        }
749        if self.variable_len() > MAX_VARIABLE_BYTES {
750            return Err(DialogPersistenceError::VariableDataTooLarge);
751        }
752        if self.local_cseq == u32::MAX {
753            return Err(DialogPersistenceError::CseqExhausted);
754        }
755        if requires_secure_route(&self.remote_target, &self.route_set)?
756            && !self.protected_signalling
757        {
758            return Err(DialogPersistenceError::InvalidValue {
759                field: "SIPS security state",
760            });
761        }
762        if self.media_keying == NegotiatedKeying::Sdes && !self.protected_signalling {
763            return Err(DialogPersistenceError::InvalidValue {
764                field: "SDES signalling security",
765            });
766        }
767        if !self.codecs.carries(self.codec) {
768            return Err(DialogPersistenceError::InvalidValue {
769                field: "negotiated codec selection",
770            });
771        }
772        if self.clock_rate == 0 || self.clock_rate > sipx_audio::pcm::MAX_SAMPLE_RATE {
773            return Err(DialogPersistenceError::InvalidValue {
774                field: "media clock rate",
775            });
776        }
777        validate_payload_type("payload type", self.payload_type)?;
778        validate_payload_type("receive payload type", self.receive_payload_type)?;
779        if let Some(payload_type) = self.dtmf_payload_type {
780            validate_payload_type("DTMF payload type", payload_type)?;
781        }
782        if [self.payload_type, self.receive_payload_type]
783            .into_iter()
784            .any(|payload_type| self.dtmf_payload_type == Some(payload_type))
785        {
786            return Err(DialogPersistenceError::InvalidValue {
787                field: "DTMF payload type",
788            });
789        }
790        if self.rtcp_mode == RtcpMode::Mux
791            && [
792                Some(self.payload_type),
793                Some(self.receive_payload_type),
794                self.dtmf_payload_type,
795            ]
796            .into_iter()
797            .flatten()
798            .any(|payload| (64..=95).contains(&payload))
799        {
800            return Err(DialogPersistenceError::InvalidValue {
801                field: "RTCP-mux payload type",
802            });
803        }
804        if self.media_profile != MediaProfile::Standard {
805            return Err(DialogPersistenceError::InvalidValue {
806                field: "restorable media profile",
807            });
808        }
809        if let Some(session) = self.session
810            && (session.interval < sipx_sip::session::ABSOLUTE_MIN_INTERVAL
811                || session.remaining > session.interval)
812        {
813            return Err(DialogPersistenceError::SessionContradiction);
814        }
815        if self.encoded_len() > MAX_SNAPSHOT_BYTES {
816            return Err(DialogPersistenceError::InputTooLarge {
817                len: self.encoded_len(),
818                max: MAX_SNAPSHOT_BYTES,
819            });
820        }
821        Ok(())
822    }
823}
824
825impl SessionSnapshot {
826    pub(crate) const fn action(self) -> DialogSessionAction {
827        if self.we_refresh {
828            DialogSessionAction::Refresh
829        } else {
830            DialogSessionAction::Expire
831        }
832    }
833}
834
835pub(crate) struct SnapshotParts {
836    pub(crate) role: Role,
837    pub(crate) id: DialogId,
838    pub(crate) local_party: String,
839    pub(crate) remote_party: String,
840    pub(crate) remote_target: Uri,
841    pub(crate) route_set: Vec<String>,
842    pub(crate) local_cseq: u32,
843    pub(crate) remote_cseq: Option<u32>,
844    pub(crate) protected_signalling: bool,
845    pub(crate) media_keying: NegotiatedKeying,
846    pub(crate) media_profile: MediaProfile,
847    pub(crate) codecs: Codecs,
848    pub(crate) codec: Codec,
849    pub(crate) clock_rate: u32,
850    pub(crate) payload_type: u8,
851    pub(crate) receive_payload_type: u8,
852    pub(crate) dtmf_payload_type: Option<u8>,
853    pub(crate) rtcp_mode: RtcpMode,
854    pub(crate) hold: Direction,
855    pub(crate) peer_allows_update: bool,
856    pub(crate) session: Option<SessionSnapshot>,
857}
858
859/// Fresh runtime resources and policy required to attach a decoded dialog.
860///
861/// The context is borrowed during restoration. A refusal therefore does not consume or stop its
862/// media session, mutate its endpoint, or create a transaction. `elapsed_since_capture` and `now`
863/// are explicit so no persisted process-local instant is ever interpreted in a new process and
864/// downtime cannot silently renew a retained session timer.
865pub struct DialogRestoreContext {
866    pub(crate) endpoint: Handle,
867    pub(crate) target: Target,
868    pub(crate) media: Arc<MediaSession>,
869    pub(crate) media_address: MediaAddress,
870    pub(crate) remote_media: SocketAddr,
871    pub(crate) policy: MediaPolicy,
872    pub(crate) direction: Direction,
873    pub(crate) elapsed_since_capture: Duration,
874    pub(crate) now: Instant,
875    claimed: AtomicBool,
876}
877
878impl DialogRestoreContext {
879    /// Describe already-created runtime resources without starting any new work.
880    #[must_use]
881    #[allow(clippy::too_many_arguments)]
882    pub fn new(
883        endpoint: Handle,
884        target: Target,
885        media: Arc<MediaSession>,
886        media_address: MediaAddress,
887        remote_media: SocketAddr,
888        policy: MediaPolicy,
889        direction: Direction,
890        elapsed_since_capture: Duration,
891        now: Instant,
892    ) -> Self {
893        Self {
894            endpoint,
895            target,
896            media,
897            media_address,
898            remote_media,
899            policy,
900            direction,
901            elapsed_since_capture,
902            now,
903            claimed: AtomicBool::new(false),
904        }
905    }
906
907    pub(crate) fn claim(&self) -> Result<(), DialogPersistenceError> {
908        self.claimed
909            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
910            .map(|_| ())
911            .map_err(|_| DialogPersistenceError::ContextAlreadyAttached)
912    }
913}
914
915impl fmt::Debug for DialogRestoreContext {
916    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
917        formatter
918            .debug_struct("DialogRestoreContext")
919            .field("endpoint_local", &self.endpoint.local_addr())
920            .field("target_addr", &self.target.addr)
921            .field("target_transport", &self.target.transport)
922            .field("target_has_path", &self.target.path.is_some())
923            .field("media_local", &self.media.local_addr())
924            .field("media_advertised", &self.media_address.advertised())
925            .field("media_bind", &self.media_address.bind())
926            .field("remote_media", &self.remote_media)
927            .field("policy", &self.policy)
928            .field("direction", &self.direction)
929            .field("elapsed_since_capture", &self.elapsed_since_capture)
930            .field("now", &self.now)
931            .finish_non_exhaustive()
932    }
933}
934
935fn validate_media(
936    snapshot: &DialogSnapshot,
937    context: &DialogRestoreContext,
938) -> Result<(), DialogPersistenceError> {
939    if context.media_address.advertised().is_unspecified() {
940        return Err(DialogPersistenceError::UnspecifiedMediaAddress);
941    }
942    if context.remote_media.ip().is_unspecified() || context.remote_media.port() == 0 {
943        return Err(DialogPersistenceError::MediaContractMismatch {
944            field: "remote media address",
945        });
946    }
947    if context.media.local_addr().ip() != context.media_address.bind() {
948        return Err(DialogPersistenceError::MediaContractMismatch {
949            field: "media bind address",
950        });
951    }
952    if context.media.runs_ice() || context.policy.ice != crate::IcePolicy::Disabled {
953        return Err(DialogPersistenceError::MediaContractMismatch { field: "ICE state" });
954    }
955    if context.policy.profile != snapshot.media_profile {
956        return Err(DialogPersistenceError::MediaContractMismatch {
957            field: "media profile",
958        });
959    }
960    if context.policy.codecs != snapshot.codecs {
961        return Err(DialogPersistenceError::MediaContractMismatch {
962            field: "codec policy",
963        });
964    }
965    if context.direction != snapshot.hold {
966        return Err(DialogPersistenceError::MediaContractMismatch { field: "direction" });
967    }
968    let expected_keying = match context.policy.keying {
969        Keying::Plain => NegotiatedKeying::Plain,
970        Keying::Sdes => NegotiatedKeying::Sdes,
971        Keying::DtlsSrtp => NegotiatedKeying::DtlsSrtp,
972        Keying::Auto => return Err(DialogPersistenceError::MediaSecurityMismatch),
973    };
974    if expected_keying != snapshot.media_keying
975        || context.media.is_encrypted() != (snapshot.media_keying != NegotiatedKeying::Plain)
976    {
977        return Err(DialogPersistenceError::MediaSecurityMismatch);
978    }
979    if snapshot.media_keying == NegotiatedKeying::Sdes && !context.target.transport.is_secure() {
980        return Err(DialogPersistenceError::SecurityDowngrade);
981    }
982    if context.media.codec() != snapshot.codec {
983        return Err(DialogPersistenceError::MediaContractMismatch { field: "codec" });
984    }
985    if context.media.clock_rate() != snapshot.clock_rate {
986        return Err(DialogPersistenceError::MediaContractMismatch {
987            field: "media clock rate",
988        });
989    }
990    if context.media.wire_payload_type() != snapshot.payload_type {
991        return Err(DialogPersistenceError::MediaContractMismatch {
992            field: "payload type",
993        });
994    }
995    if context.media.receive_payload_type() != snapshot.receive_payload_type {
996        return Err(DialogPersistenceError::MediaContractMismatch {
997            field: "receive payload type",
998        });
999    }
1000    if context.media.dtmf_payload_type() != snapshot.dtmf_payload_type {
1001        return Err(DialogPersistenceError::MediaContractMismatch {
1002            field: "DTMF payload type",
1003        });
1004    }
1005    if context.media.rtcp_mode() != snapshot.rtcp_mode {
1006        return Err(DialogPersistenceError::MediaContractMismatch { field: "RTCP mode" });
1007    }
1008    Ok(())
1009}
1010
1011fn validate_payload_type(field: &'static str, value: u8) -> Result<(), DialogPersistenceError> {
1012    if value > 0x7f {
1013        return Err(DialogPersistenceError::PayloadTypeOutOfRange { field, value });
1014    }
1015    Ok(())
1016}
1017
1018fn validate_bytes(
1019    field: &'static str,
1020    value: &[u8],
1021    max: usize,
1022    allow_empty: bool,
1023) -> Result<(), DialogPersistenceError> {
1024    if value.len() > max {
1025        return Err(DialogPersistenceError::FieldTooLarge {
1026            field,
1027            len: value.len(),
1028            max,
1029        });
1030    }
1031    if !allow_empty && value.is_empty() {
1032        return Err(DialogPersistenceError::InvalidValue { field });
1033    }
1034    if !value.iter().all(|byte| (0x20..=0x7e).contains(byte)) {
1035        return Err(DialogPersistenceError::InvalidValue { field });
1036    }
1037    Ok(())
1038}
1039
1040fn validate_token(field: &'static str, value: &[u8]) -> Result<(), DialogPersistenceError> {
1041    validate_bytes(field, value, MAX_ID_BYTES, false)?;
1042    if !value.iter().copied().all(is_token_char) {
1043        return Err(DialogPersistenceError::InvalidValue { field });
1044    }
1045    Ok(())
1046}
1047
1048fn is_token_char(byte: u8) -> bool {
1049    byte.is_ascii_alphanumeric()
1050        || matches!(
1051            byte,
1052            b'-' | b'.' | b'!' | b'%' | b'*' | b'_' | b'+' | b'`' | b'\'' | b'~'
1053        )
1054}
1055
1056fn validate_party(field: &'static str, value: &str) -> Result<(), DialogPersistenceError> {
1057    validate_bytes(field, value.as_bytes(), MAX_FIELD_BYTES, false)?;
1058    let address = Address::parse(value.as_bytes(), field)
1059        .map_err(|_| DialogPersistenceError::InvalidValue { field })?;
1060    if address.tag().is_some() || address.uri.password().is_some() || !address.uri.scheme().is_sip()
1061    {
1062        return Err(DialogPersistenceError::InvalidValue { field });
1063    }
1064    Ok(())
1065}
1066
1067fn validate_uri(field: &'static str, value: &Uri) -> Result<(), DialogPersistenceError> {
1068    let bytes = value.to_bytes();
1069    validate_bytes(field, &bytes, MAX_FIELD_BYTES, false)?;
1070    if !value.scheme().is_sip() || value.password().is_some() {
1071        return Err(DialogPersistenceError::InvalidValue { field });
1072    }
1073    Ok(())
1074}
1075
1076fn validate_route(value: &str) -> Result<(), DialogPersistenceError> {
1077    validate_bytes("route", value.as_bytes(), MAX_FIELD_BYTES, false)?;
1078    let route = Address::parse(value.as_bytes(), "Route")
1079        .map_err(|_| DialogPersistenceError::InvalidValue { field: "route" })?;
1080    if !route.uri.scheme().is_sip() || route.uri.password().is_some() {
1081        return Err(DialogPersistenceError::InvalidValue { field: "route" });
1082    }
1083    Ok(())
1084}
1085
1086fn requires_secure_route(target: &Uri, routes: &[String]) -> Result<bool, DialogPersistenceError> {
1087    if target.scheme().is_secure() {
1088        return Ok(true);
1089    }
1090    for route in routes {
1091        let address = Address::parse(route.as_bytes(), "Route")
1092            .map_err(|_| DialogPersistenceError::InvalidValue { field: "route" })?;
1093        if address.uri.scheme().is_secure() {
1094            return Ok(true);
1095        }
1096    }
1097    Ok(false)
1098}
1099
1100struct Reader<'a> {
1101    remaining: &'a [u8],
1102    variable: usize,
1103}
1104
1105impl<'a> Reader<'a> {
1106    const fn new(input: &'a [u8]) -> Self {
1107        Self {
1108            remaining: input,
1109            variable: 0,
1110        }
1111    }
1112
1113    fn exact(
1114        &mut self,
1115        len: usize,
1116        field: &'static str,
1117    ) -> Result<&'a [u8], DialogPersistenceError> {
1118        let Some(value) = self.remaining.get(..len) else {
1119            return Err(DialogPersistenceError::Truncated { field });
1120        };
1121        self.remaining = self.remaining.get(len..).unwrap_or_default();
1122        Ok(value)
1123    }
1124
1125    fn u8(&mut self, field: &'static str) -> Result<u8, DialogPersistenceError> {
1126        self.exact(1, field)?
1127            .first()
1128            .copied()
1129            .ok_or(DialogPersistenceError::Truncated { field })
1130    }
1131
1132    fn u16(&mut self, field: &'static str) -> Result<u16, DialogPersistenceError> {
1133        let value = self.exact(2, field)?;
1134        let octets: [u8; 2] = value
1135            .try_into()
1136            .map_err(|_| DialogPersistenceError::Truncated { field })?;
1137        Ok(u16::from_be_bytes(octets))
1138    }
1139
1140    fn u32(&mut self, field: &'static str) -> Result<u32, DialogPersistenceError> {
1141        let value = self.exact(4, field)?;
1142        let octets: [u8; 4] = value
1143            .try_into()
1144            .map_err(|_| DialogPersistenceError::Truncated { field })?;
1145        Ok(u32::from_be_bytes(octets))
1146    }
1147
1148    fn u64(&mut self, field: &'static str) -> Result<u64, DialogPersistenceError> {
1149        let value = self.exact(8, field)?;
1150        let octets: [u8; 8] = value
1151            .try_into()
1152            .map_err(|_| DialogPersistenceError::Truncated { field })?;
1153        Ok(u64::from_be_bytes(octets))
1154    }
1155
1156    fn bytes(
1157        &mut self,
1158        field: &'static str,
1159        max: usize,
1160    ) -> Result<Vec<u8>, DialogPersistenceError> {
1161        let declared = usize::try_from(self.u32(field)?).map_err(|_| {
1162            DialogPersistenceError::FieldTooLarge {
1163                field,
1164                len: usize::MAX,
1165                max,
1166            }
1167        })?;
1168        if declared > max {
1169            return Err(DialogPersistenceError::FieldTooLarge {
1170                field,
1171                len: declared,
1172                max,
1173            });
1174        }
1175        let next_total = self
1176            .variable
1177            .checked_add(declared)
1178            .ok_or(DialogPersistenceError::VariableDataTooLarge)?;
1179        if next_total > MAX_VARIABLE_BYTES {
1180            return Err(DialogPersistenceError::VariableDataTooLarge);
1181        }
1182        let value = self.exact(declared, field)?.to_vec();
1183        self.variable = next_total;
1184        Ok(value)
1185    }
1186
1187    fn string(
1188        &mut self,
1189        field: &'static str,
1190        max: usize,
1191    ) -> Result<String, DialogPersistenceError> {
1192        String::from_utf8(self.bytes(field, max)?)
1193            .map_err(|_| DialogPersistenceError::InvalidUtf8 { field })
1194    }
1195
1196    fn optional_u8(&mut self, field: &'static str) -> Result<Option<u8>, DialogPersistenceError> {
1197        match self.u8(field)? {
1198            0 => Ok(None),
1199            1 => self.u8(field).map(Some),
1200            _ => Err(DialogPersistenceError::NonCanonicalPresence { field }),
1201        }
1202    }
1203
1204    fn optional_u32(&mut self, field: &'static str) -> Result<Option<u32>, DialogPersistenceError> {
1205        match self.u8(field)? {
1206            0 => Ok(None),
1207            1 => self.u32(field).map(Some),
1208            _ => Err(DialogPersistenceError::NonCanonicalPresence { field }),
1209        }
1210    }
1211
1212    const fn is_empty(&self) -> bool {
1213        self.remaining.is_empty()
1214    }
1215}
1216
1217fn put_u16(out: &mut Vec<u8>, value: u16) {
1218    out.extend_from_slice(&value.to_be_bytes());
1219}
1220
1221fn put_u32(out: &mut Vec<u8>, value: u32) {
1222    out.extend_from_slice(&value.to_be_bytes());
1223}
1224
1225fn put_u64(out: &mut Vec<u8>, value: u64) {
1226    out.extend_from_slice(&value.to_be_bytes());
1227}
1228
1229fn put_bytes(out: &mut Vec<u8>, value: &[u8]) {
1230    put_u32(out, u32::try_from(value.len()).unwrap_or(u32::MAX));
1231    out.extend_from_slice(value);
1232}
1233
1234fn put_optional_u8(out: &mut Vec<u8>, value: Option<u8>) {
1235    match value {
1236        None => out.push(0),
1237        Some(value) => {
1238            out.push(1);
1239            out.push(value);
1240        }
1241    }
1242}
1243
1244fn put_optional_u32(out: &mut Vec<u8>, value: Option<u32>) {
1245    match value {
1246        None => out.push(0),
1247        Some(value) => {
1248            out.push(1);
1249            put_u32(out, value);
1250        }
1251    }
1252}
1253
1254const fn keying_id(value: NegotiatedKeying) -> u8 {
1255    match value {
1256        NegotiatedKeying::Plain => 0,
1257        NegotiatedKeying::Sdes => 1,
1258        NegotiatedKeying::DtlsSrtp => 2,
1259    }
1260}
1261
1262fn decode_keying(value: u8) -> Result<NegotiatedKeying, DialogPersistenceError> {
1263    match value {
1264        0 => Ok(NegotiatedKeying::Plain),
1265        1 => Ok(NegotiatedKeying::Sdes),
1266        2 => Ok(NegotiatedKeying::DtlsSrtp),
1267        _ => Err(DialogPersistenceError::InvalidValue {
1268            field: "media keying",
1269        }),
1270    }
1271}
1272
1273const fn profile_id(value: MediaProfile) -> u8 {
1274    match value {
1275        MediaProfile::Standard => 0,
1276        MediaProfile::BrowserAudio => 1,
1277    }
1278}
1279
1280fn decode_profile(value: u8) -> Result<MediaProfile, DialogPersistenceError> {
1281    match value {
1282        0 => Ok(MediaProfile::Standard),
1283        1 => Ok(MediaProfile::BrowserAudio),
1284        _ => Err(DialogPersistenceError::InvalidValue {
1285            field: "media profile",
1286        }),
1287    }
1288}
1289
1290const fn preference_id(value: crate::CodecPreference) -> u8 {
1291    match value {
1292        crate::CodecPreference::Pcmu => 0,
1293        crate::CodecPreference::Pcma => 1,
1294        crate::CodecPreference::Opus => 2,
1295        crate::CodecPreference::L16 => 3,
1296    }
1297}
1298
1299fn decode_preference(value: u8) -> Result<crate::CodecPreference, DialogPersistenceError> {
1300    match value {
1301        0 => Ok(crate::CodecPreference::Pcmu),
1302        1 => Ok(crate::CodecPreference::Pcma),
1303        2 => Ok(crate::CodecPreference::Opus),
1304        3 => Ok(crate::CodecPreference::L16),
1305        _ => Err(DialogPersistenceError::InvalidValue {
1306            field: "codec preference",
1307        }),
1308    }
1309}
1310
1311const fn codec_id(value: Codec) -> u8 {
1312    match value {
1313        Codec::Pcmu => 0,
1314        Codec::Pcma => 1,
1315        #[cfg(feature = "opus")]
1316        Codec::Opus => 2,
1317        Codec::L16 => 3,
1318    }
1319}
1320
1321fn decode_codec(value: u8) -> Result<Codec, DialogPersistenceError> {
1322    match value {
1323        0 => Ok(Codec::Pcmu),
1324        1 => Ok(Codec::Pcma),
1325        #[cfg(feature = "opus")]
1326        2 => Ok(Codec::Opus),
1327        #[cfg(not(feature = "opus"))]
1328        2 => Err(DialogPersistenceError::UnsupportedCodec(2)),
1329        3 => Ok(Codec::L16),
1330        other => Err(DialogPersistenceError::UnsupportedCodec(other)),
1331    }
1332}
1333
1334const fn rtcp_id(value: RtcpMode) -> u8 {
1335    match value {
1336        RtcpMode::Separate => 0,
1337        RtcpMode::Mux => 1,
1338    }
1339}
1340
1341fn decode_rtcp(value: u8) -> Result<RtcpMode, DialogPersistenceError> {
1342    match value {
1343        0 => Ok(RtcpMode::Separate),
1344        1 => Ok(RtcpMode::Mux),
1345        _ => Err(DialogPersistenceError::InvalidValue { field: "RTCP mode" }),
1346    }
1347}
1348
1349const fn direction_id(value: Direction) -> u8 {
1350    match value {
1351        Direction::SendRecv => 0,
1352        Direction::SendOnly => 1,
1353        Direction::RecvOnly => 2,
1354        Direction::Inactive => 3,
1355    }
1356}
1357
1358fn decode_direction(value: u8) -> Result<Direction, DialogPersistenceError> {
1359    match value {
1360        0 => Ok(Direction::SendRecv),
1361        1 => Ok(Direction::SendOnly),
1362        2 => Ok(Direction::RecvOnly),
1363        3 => Ok(Direction::Inactive),
1364        _ => Err(DialogPersistenceError::InvalidValue {
1365            field: "hold direction",
1366        }),
1367    }
1368}
1369
1370#[cfg(test)]
1371#[allow(
1372    clippy::unwrap_used,
1373    clippy::expect_used,
1374    clippy::panic,
1375    clippy::indexing_slicing
1376)]
1377mod tests {
1378    use super::*;
1379
1380    fn uri(value: &str) -> Uri {
1381        Uri::parse(Bytes::copy_from_slice(value.as_bytes())).expect("fixture URI")
1382    }
1383
1384    fn parts() -> SnapshotParts {
1385        SnapshotParts {
1386            role: Role::Caller,
1387            id: DialogId {
1388                call_id: b"persist-1@example.net".to_vec(),
1389                local_tag: b"lt".to_vec(),
1390                remote_tag: b"rt".to_vec(),
1391            },
1392            local_party: "Alice <sip:alice@example.net>".to_owned(),
1393            remote_party: "Bob <sip:bob@example.org>".to_owned(),
1394            remote_target: uri("sip:refreshed@192.0.2.20:5070"),
1395            route_set: vec![
1396                "<sip:first.example;lr>".to_owned(),
1397                "<sip:second.example;lr>".to_owned(),
1398            ],
1399            local_cseq: 41,
1400            remote_cseq: Some(9),
1401            protected_signalling: false,
1402            media_keying: NegotiatedKeying::Plain,
1403            media_profile: MediaProfile::Standard,
1404            codecs: Codecs::G711,
1405            codec: Codec::Pcmu,
1406            clock_rate: 8_000,
1407            payload_type: 0,
1408            receive_payload_type: 0,
1409            dtmf_payload_type: Some(101),
1410            rtcp_mode: RtcpMode::Mux,
1411            hold: Direction::SendRecv,
1412            peer_allows_update: true,
1413            session: None,
1414        }
1415    }
1416
1417    fn fixture() -> DialogSnapshot {
1418        DialogSnapshot::from_parts(parts()).expect("valid fixture")
1419    }
1420
1421    /// Return the payload range of one of the six leading length-prefixed values.
1422    fn leading_field(bytes: &[u8], ordinal: usize) -> std::ops::Range<usize> {
1423        let mut at = 8usize;
1424        for index in 0..=ordinal {
1425            let len = u32::from_be_bytes(bytes[at..at + 4].try_into().expect("length")) as usize;
1426            let range = at + 4..at + 4 + len;
1427            if index == ordinal {
1428                return range;
1429            }
1430            at = range.end;
1431        }
1432        panic!("field ordinal exists")
1433    }
1434
1435    fn after_routes(bytes: &[u8]) -> usize {
1436        let target = leading_field(bytes, 5);
1437        let mut at = target.end;
1438        let count = u16::from_be_bytes(bytes[at..at + 2].try_into().expect("route count"));
1439        at += 2;
1440        for _ in 0..count {
1441            let len = u32::from_be_bytes(bytes[at..at + 4].try_into().expect("route length"));
1442            at += 4 + usize::try_from(len).expect("route length fits");
1443        }
1444        at
1445    }
1446
1447    fn media_offsets(bytes: &[u8]) -> (usize, usize, usize, usize) {
1448        let mut at = after_routes(bytes) + 4;
1449        let remote_cseq_present = bytes[at];
1450        at += 1 + if remote_cseq_present == 1 { 4 } else { 0 };
1451        at += 2;
1452        let preference_count = usize::from(bytes[at]);
1453        at += 1 + preference_count;
1454        at += 1;
1455        let clock_rate = at;
1456        at += 4;
1457        let payload_type = at;
1458        let receive_payload_type = payload_type + 1;
1459        let dtmf_marker = receive_payload_type + 1;
1460        assert_eq!(bytes[dtmf_marker], 1, "fixture carries a DTMF payload");
1461        (
1462            clock_rate,
1463            payload_type,
1464            receive_payload_type,
1465            dtmf_marker + 1,
1466        )
1467    }
1468
1469    #[test]
1470    fn dp1_is_canonical_and_preserves_the_complete_dialog_order() {
1471        let snapshot = fixture();
1472        let bytes = snapshot.encode();
1473        let decoded = DialogSnapshot::decode(&bytes).expect("decodes");
1474        assert_eq!(decoded.encode(), bytes);
1475        assert_eq!(decoded.role(), Role::Caller);
1476        assert_eq!(decoded.local_cseq(), 41);
1477        assert_eq!(decoded.remote_cseq(), Some(9));
1478        assert_eq!(
1479            decoded.remote_target().to_bytes(),
1480            snapshot.remote_target().to_bytes()
1481        );
1482        assert_eq!(
1483            decoded.route_set(),
1484            ["<sip:first.example;lr>", "<sip:second.example;lr>",]
1485        );
1486    }
1487
1488    #[test]
1489    fn dp2_rejects_an_unknown_version_before_reading_variable_fields() {
1490        let mut bytes = fixture().encode();
1491        bytes[4..6].copy_from_slice(&3u16.to_be_bytes());
1492        bytes.truncate(6);
1493        assert_eq!(
1494            DialogSnapshot::decode(&bytes).unwrap_err(),
1495            DialogPersistenceError::UnsupportedVersion(3)
1496        );
1497    }
1498
1499    /// S-36/M-43: version one stored one symmetric payload number and implied the codec's fixed
1500    /// clock. Version two keeps accepting those bytes and expands both old facts explicitly.
1501    #[test]
1502    fn version_one_snapshots_decode_with_a_symmetric_payload_assignment() {
1503        let mut legacy = fixture().encode();
1504        legacy[4..6].copy_from_slice(&LEGACY_VERSION.to_be_bytes());
1505        let (clock_rate, _, receive_payload_type, _) = media_offsets(&legacy);
1506        legacy.remove(receive_payload_type);
1507        legacy.drain(clock_rate..clock_rate + 4);
1508
1509        let decoded = DialogSnapshot::decode(&legacy).expect("version one remains readable");
1510        assert_eq!(decoded.payload_type(), 0);
1511        assert_eq!(decoded.receive_payload_type(), 0);
1512        assert_eq!(decoded.clock_rate(), 8_000);
1513        assert_eq!(
1514            decoded.version(),
1515            VERSION,
1516            "re-encoding upgrades the schema"
1517        );
1518    }
1519
1520    #[test]
1521    fn dp3_checks_declared_field_bounds_before_allocation_or_copy() {
1522        let mut oversized = fixture().encode();
1523        oversized[8..12].copy_from_slice(&u32::try_from(MAX_ID_BYTES + 1).unwrap().to_be_bytes());
1524        assert_eq!(
1525            DialogSnapshot::decode(&oversized).unwrap_err(),
1526            DialogPersistenceError::FieldTooLarge {
1527                field: "Call-ID",
1528                len: MAX_ID_BYTES + 1,
1529                max: MAX_ID_BYTES,
1530            }
1531        );
1532
1533        let mut truncated = fixture().encode();
1534        truncated[8..12].copy_from_slice(&1000u32.to_be_bytes());
1535        assert_eq!(
1536            DialogSnapshot::decode(&truncated).unwrap_err(),
1537            DialogPersistenceError::Truncated { field: "Call-ID" }
1538        );
1539
1540        let huge = vec![0u8; MAX_SNAPSHOT_BYTES + 1];
1541        assert_eq!(
1542            DialogSnapshot::decode(&huge).unwrap_err(),
1543            DialogPersistenceError::InputTooLarge {
1544                len: MAX_SNAPSHOT_BYTES + 1,
1545                max: MAX_SNAPSHOT_BYTES,
1546            }
1547        );
1548    }
1549
1550    #[test]
1551    fn dp4_rejects_empty_duplicate_and_malformed_identity_fields() {
1552        let bytes = fixture().encode();
1553
1554        let mut empty_tag = bytes.clone();
1555        let local = leading_field(&empty_tag, 1);
1556        empty_tag[local.start - 4..local.start].copy_from_slice(&0u32.to_be_bytes());
1557        empty_tag.drain(local);
1558        assert_eq!(
1559            DialogSnapshot::decode(&empty_tag).unwrap_err(),
1560            DialogPersistenceError::InvalidValue { field: "local tag" }
1561        );
1562
1563        let mut duplicate = bytes.clone();
1564        let local = leading_field(&duplicate, 1);
1565        let remote = leading_field(&duplicate, 2);
1566        let local_value = duplicate[local].to_vec();
1567        duplicate[remote].copy_from_slice(&local_value);
1568        assert_eq!(
1569            DialogSnapshot::decode(&duplicate).unwrap_err(),
1570            DialogPersistenceError::DuplicateTags
1571        );
1572
1573        let mut malformed_target = bytes;
1574        let target = leading_field(&malformed_target, 5);
1575        malformed_target[target.clone()].fill(b'x');
1576        assert_eq!(
1577            DialogSnapshot::decode(&malformed_target).unwrap_err(),
1578            DialogPersistenceError::InvalidValue {
1579                field: "remote target"
1580            }
1581        );
1582    }
1583
1584    #[test]
1585    fn dp4_rejects_route_flag_presence_and_trailing_noncanonical_forms() {
1586        let bytes = fixture().encode();
1587
1588        let mut too_many_routes = bytes.clone();
1589        let route_count = leading_field(&too_many_routes, 5).end;
1590        too_many_routes[route_count..route_count + 2]
1591            .copy_from_slice(&u16::try_from(MAX_ROUTES + 1).unwrap().to_be_bytes());
1592        assert_eq!(
1593            DialogSnapshot::decode(&too_many_routes).unwrap_err(),
1594            DialogPersistenceError::TooManyRoutes {
1595                count: MAX_ROUTES + 1
1596            }
1597        );
1598
1599        let mut reserved = bytes.clone();
1600        reserved[6..8].copy_from_slice(&(1u16 << 15).to_be_bytes());
1601        assert_eq!(
1602            DialogSnapshot::decode(&reserved).unwrap_err(),
1603            DialogPersistenceError::ReservedFlags(1u16 << 15)
1604        );
1605
1606        let mut noncanonical = bytes.clone();
1607        let remote_marker = after_routes(&noncanonical) + 4;
1608        noncanonical[remote_marker] = 2;
1609        assert_eq!(
1610            DialogSnapshot::decode(&noncanonical).unwrap_err(),
1611            DialogPersistenceError::NonCanonicalPresence {
1612                field: "remote CSeq"
1613            }
1614        );
1615
1616        let mut trailing = bytes;
1617        trailing.push(0);
1618        assert_eq!(
1619            DialogSnapshot::decode(&trailing).unwrap_err(),
1620            DialogPersistenceError::TrailingBytes
1621        );
1622    }
1623
1624    #[test]
1625    fn cseq_exhaustion_and_session_contradictions_are_typed() {
1626        let mut exhausted = fixture().encode();
1627        let cseq = after_routes(&exhausted);
1628        exhausted[cseq..cseq + 4].copy_from_slice(&u32::MAX.to_be_bytes());
1629        assert_eq!(
1630            DialogSnapshot::decode(&exhausted).unwrap_err(),
1631            DialogPersistenceError::CseqExhausted
1632        );
1633
1634        let mut contradictory = parts();
1635        contradictory.session = Some(SessionSnapshot {
1636            interval: Duration::from_secs(90),
1637            we_refresh: true,
1638            remaining: Duration::from_secs(91),
1639        });
1640        assert!(matches!(
1641            DialogSnapshot::from_parts(contradictory),
1642            Err(DialogPersistenceError::SessionContradiction)
1643        ));
1644    }
1645
1646    #[test]
1647    fn hostile_payload_types_outside_the_rtp_header_range_are_typed_refusals() {
1648        let canonical = fixture().encode();
1649        let (_, payload_type, receive_payload_type, dtmf_payload_type) = media_offsets(&canonical);
1650        for (offset, field) in [
1651            (payload_type, "payload type"),
1652            (receive_payload_type, "receive payload type"),
1653            (dtmf_payload_type, "DTMF payload type"),
1654        ] {
1655            for value in [128, u8::MAX] {
1656                let mut hostile = canonical.clone();
1657                hostile[offset] = value;
1658                assert_eq!(
1659                    DialogSnapshot::decode(&hostile).unwrap_err(),
1660                    DialogPersistenceError::PayloadTypeOutOfRange { field, value }
1661                );
1662            }
1663        }
1664    }
1665
1666    #[test]
1667    fn every_hostile_prefix_is_a_value_and_never_a_panic() {
1668        let canonical = fixture().encode();
1669        for length in 0..canonical.len() {
1670            assert!(DialogSnapshot::decode(&canonical[..length]).is_err());
1671        }
1672        for offset in 0..canonical.len() {
1673            let mut hostile = canonical.clone();
1674            hostile[offset] ^= 0xff;
1675            let _ = DialogSnapshot::decode(&hostile);
1676        }
1677        for length in [0usize, 1, 7, 31, 255, 4096] {
1678            let hostile = vec![0xff; length];
1679            let _ = DialogSnapshot::decode(&hostile);
1680        }
1681    }
1682
1683    #[test]
1684    fn debug_output_redacts_party_and_identifier_values() {
1685        let snapshot = fixture();
1686        let rendered = format!("{snapshot:?}");
1687        for secret in ["Alice", "Bob", "persist-1", "refreshed", "first.example"] {
1688            assert!(
1689                !rendered.contains(secret),
1690                "debug leaked {secret}: {rendered}"
1691            );
1692        }
1693        assert!(rendered.contains("call_id_bytes"));
1694    }
1695
1696    #[test]
1697    fn password_bearing_uris_are_never_accepted_as_durable_protocol_facts() {
1698        let mut target = parts();
1699        target.remote_target = uri("sip:alice:credential@example.net");
1700        assert!(matches!(
1701            DialogSnapshot::from_parts(target),
1702            Err(DialogPersistenceError::InvalidValue {
1703                field: "remote target"
1704            })
1705        ));
1706
1707        let mut party = parts();
1708        party.local_party = "<sip:alice:credential@example.net>".to_owned();
1709        assert!(matches!(
1710            DialogSnapshot::from_parts(party),
1711            Err(DialogPersistenceError::InvalidValue {
1712                field: "local party"
1713            })
1714        ));
1715    }
1716
1717    #[tokio::test]
1718    async fn dp6_refuses_mismatched_keying_without_consuming_the_fresh_media() {
1719        let mut secure = parts();
1720        secure.protected_signalling = true;
1721        secure.media_keying = NegotiatedKeying::Sdes;
1722        let snapshot = DialogSnapshot::from_parts(secure).expect("secure snapshot");
1723
1724        let (endpoint, _incoming) = sipx_transport::bind(sipx_transport::Config::new(
1725            "127.0.0.1:0".parse().expect("endpoint address"),
1726        ))
1727        .await
1728        .expect("endpoint binds");
1729        let remote: SocketAddr = "127.0.0.1:40000".parse().expect("media remote");
1730        let mut media_config = sipx_media::Config::new(remote, Codec::Pcmu);
1731        media_config.rtcp_mode = RtcpMode::Mux;
1732        let media = Arc::new(
1733            MediaSession::start("127.0.0.1:0".parse().expect("media bind"), media_config)
1734                .await
1735                .expect("plain media starts"),
1736        );
1737        let context = DialogRestoreContext::new(
1738            endpoint.clone(),
1739            Target::new(endpoint.local_addr(), sipx_transport::TransportKind::Tls),
1740            Arc::clone(&media),
1741            MediaAddress::new("127.0.0.1".parse().expect("media address")),
1742            remote,
1743            MediaPolicy::default().with_keying(Keying::Sdes),
1744            snapshot.direction(),
1745            Duration::ZERO,
1746            Instant::now(),
1747        );
1748        assert_eq!(endpoint.outstanding().await.expect("outstanding"), 0);
1749        assert_eq!(
1750            snapshot.validate_restore(&context).unwrap_err(),
1751            DialogPersistenceError::MediaSecurityMismatch
1752        );
1753        assert_eq!(endpoint.outstanding().await.expect("outstanding"), 0);
1754        assert_eq!(Arc::strong_count(&media), 2);
1755
1756        drop(context);
1757        drop(media);
1758        endpoint.shutdown().await;
1759    }
1760
1761    #[tokio::test]
1762    async fn restore_context_debug_omits_a_secret_bearing_websocket_target_path() {
1763        let (endpoint, _incoming) = sipx_transport::bind(sipx_transport::Config::new(
1764            "127.0.0.1:0".parse().expect("endpoint address"),
1765        ))
1766        .await
1767        .expect("endpoint binds");
1768        let remote: SocketAddr = "127.0.0.1:40000".parse().expect("media remote");
1769        let mut media_config = sipx_media::Config::new(remote, Codec::Pcmu);
1770        media_config.rtcp_mode = RtcpMode::Mux;
1771        let media = Arc::new(
1772            MediaSession::start("127.0.0.1:0".parse().expect("media bind"), media_config)
1773                .await
1774                .expect("media starts"),
1775        );
1776        let secret = "never-print-this-restore-token";
1777        let context = DialogRestoreContext::new(
1778            endpoint.clone(),
1779            Target::new(endpoint.local_addr(), sipx_transport::TransportKind::Wss)
1780                .verifying("internal-signalling.example")
1781                .at_path(format!("/calls?access_token={secret}")),
1782            Arc::clone(&media),
1783            MediaAddress::new("127.0.0.1".parse().expect("media address")),
1784            remote,
1785            MediaPolicy::default(),
1786            Direction::SendRecv,
1787            Duration::ZERO,
1788            Instant::now(),
1789        );
1790
1791        let context_debug = format!("{context:?}");
1792        assert!(!context_debug.contains(secret));
1793        assert!(!context_debug.contains("/calls"));
1794        assert!(!context_debug.contains("internal-signalling.example"));
1795        assert!(context_debug.contains("Wss"));
1796        assert!(context_debug.contains("target_has_path: true"));
1797
1798        drop(context);
1799        drop(media);
1800        endpoint.shutdown().await;
1801    }
1802}