Skip to main content

sipx_sip/headers/
history.rs

1//! Diversion history and call reasons (RFC 7044 and RFC 3326).
2
3use bytes::Bytes;
4
5use crate::error::HeaderError;
6use crate::escape;
7use crate::headers::address::Address;
8use crate::headers::grammar::{self, HeaderParam, is_token_char, trim};
9use crate::message::TypedHeader;
10use crate::name::HeaderName;
11use crate::params::Param;
12use crate::uri::Uri;
13
14const REASON: &str = "Reason";
15const HISTORY_INFO: &str = "History-Info";
16
17/// One RFC 3326 Reason value.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ReasonValue {
20    protocol: Vec<u8>,
21    cause: u16,
22    text: Option<Vec<u8>>,
23    extensions: Vec<HeaderParam>,
24}
25
26impl ReasonValue {
27    /// A SIP response code used as a reason.
28    #[must_use]
29    pub fn sip(cause: crate::message::StatusCode, text: Option<Vec<u8>>) -> Self {
30        Self {
31            protocol: b"SIP".to_vec(),
32            cause: cause.code(),
33            text,
34            extensions: Vec::new(),
35        }
36    }
37
38    /// A Q.850 cause value.
39    #[must_use]
40    pub fn q850(cause: u8, text: Option<Vec<u8>>) -> Self {
41        Self {
42            protocol: b"Q.850".to_vec(),
43            cause: u16::from(cause),
44            text,
45            extensions: Vec::new(),
46        }
47    }
48
49    /// The protocol token, preserving an extension protocol's spelling.
50    #[must_use]
51    pub fn protocol(&self) -> &[u8] {
52        &self.protocol
53    }
54
55    /// The decimal cause.
56    #[must_use]
57    pub fn cause(&self) -> u16 {
58        self.cause
59    }
60
61    /// The human-readable text, without quotes.
62    #[must_use]
63    pub fn text(&self) -> Option<&[u8]> {
64        self.text.as_deref()
65    }
66
67    /// Serialize one reason value.
68    #[must_use]
69    pub fn to_bytes(&self) -> Bytes {
70        let mut out = self.protocol.clone();
71        out.extend_from_slice(b";cause=");
72        out.extend_from_slice(self.cause.to_string().as_bytes());
73        if let Some(text) = &self.text {
74            out.extend_from_slice(b";text=\"");
75            write_quoted(text, &mut out);
76            out.push(b'"');
77        }
78        for parameter in &self.extensions {
79            write_parameter(parameter, &mut out);
80        }
81        Bytes::from(out)
82    }
83
84    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
85        let value = trim(value);
86        let semi = value
87            .iter()
88            .position(|&b| b == b';')
89            .ok_or(HeaderError::Syntax { header: REASON })?;
90        let protocol = trim(value.get(..semi).unwrap_or(&[]));
91        if protocol.is_empty() || !protocol.iter().all(|&b| is_token_char(b)) {
92            return Err(HeaderError::Syntax { header: REASON });
93        }
94        let params = grammar::parse_params(value.get(semi..).unwrap_or(&[]), REASON)?;
95        let causes: Vec<_> = params.iter().filter(|p| p.is("cause")).collect();
96        let texts: Vec<_> = params.iter().filter(|p| p.is("text")).collect();
97        if causes.len() != 1 || texts.len() > 1 {
98            return Err(HeaderError::Syntax { header: REASON });
99        }
100        let cause_bytes = causes
101            .first()
102            .and_then(|p| p.value.as_deref())
103            .ok_or(HeaderError::Syntax { header: REASON })?;
104        let cause_u64 = grammar::parse_u64(cause_bytes, REASON)?;
105        let cause =
106            u16::try_from(cause_u64).map_err(|_| HeaderError::OutOfRange { header: REASON })?;
107        if protocol.eq_ignore_ascii_case(b"SIP") && !(100..=699).contains(&cause) {
108            return Err(HeaderError::OutOfRange { header: REASON });
109        }
110        if protocol.eq_ignore_ascii_case(b"Q.850") && cause > 127 {
111            return Err(HeaderError::OutOfRange { header: REASON });
112        }
113        let text = texts.first().and_then(|p| p.value.clone());
114        let extensions = params
115            .into_iter()
116            .filter(|p| !p.is("cause") && !p.is("text"))
117            .collect();
118        Ok(Self {
119            protocol: protocol.to_vec(),
120            cause,
121            text,
122            extensions,
123        })
124    }
125}
126
127/// A comma-separated Reason header value.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct Reason(pub Vec<ReasonValue>);
130
131impl Reason {
132    /// Serialize the list for a header field.
133    #[must_use]
134    pub fn to_bytes(&self) -> Bytes {
135        join(self.0.iter().map(ReasonValue::to_bytes))
136    }
137}
138
139impl From<ReasonValue> for Reason {
140    fn from(value: ReasonValue) -> Self {
141        Self(vec![value])
142    }
143}
144
145impl TypedHeader for Reason {
146    const NAME: HeaderName = HeaderName::Reason;
147
148    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
149        grammar::split_list(value, REASON)?
150            .into_iter()
151            .map(ReasonValue::decode)
152            .collect::<Result<Vec<_>, _>>()
153            .and_then(|values| {
154                (!values.is_empty())
155                    .then_some(Self(values))
156                    .ok_or(HeaderError::Syntax { header: REASON })
157            })
158    }
159}
160
161/// A hierarchical RFC 7044 history index.
162#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
163pub struct HistoryIndex(Vec<u32>);
164
165impl HistoryIndex {
166    /// The mandatory first index.
167    #[must_use]
168    pub fn first() -> Self {
169        Self(vec![1])
170    }
171
172    /// Append a component used for a forwarding action.
173    #[must_use]
174    pub fn forwarded(&self) -> Self {
175        let mut components = self.0.clone();
176        components.push(1);
177        Self(components)
178    }
179
180    /// Append the visible zero which records a missing hop.
181    #[must_use]
182    pub fn gap(&self) -> Self {
183        let mut components = self.0.clone();
184        components.push(0);
185        Self(components)
186    }
187
188    /// Serialize the dotted decimal form.
189    #[must_use]
190    pub fn to_bytes(&self) -> Bytes {
191        let mut out = Vec::new();
192        for (position, component) in self.0.iter().enumerate() {
193            if position != 0 {
194                out.push(b'.');
195            }
196            out.extend_from_slice(component.to_string().as_bytes());
197        }
198        Bytes::from(out)
199    }
200
201    fn parse(value: &[u8]) -> Result<Self, HeaderError> {
202        let mut components = Vec::new();
203        for component in value.split(|&b| b == b'.') {
204            if component.is_empty() || (component.len() > 1 && component.first() == Some(&b'0')) {
205                return Err(HeaderError::Syntax {
206                    header: HISTORY_INFO,
207                });
208            }
209            let number = grammar::parse_u64(component, HISTORY_INFO)?;
210            components.push(u32::try_from(number).map_err(|_| HeaderError::OutOfRange {
211                header: HISTORY_INFO,
212            })?);
213        }
214        (!components.is_empty())
215            .then_some(Self(components))
216            .ok_or(HeaderError::Syntax {
217                header: HISTORY_INFO,
218            })
219    }
220}
221
222/// Why the target represented by a History-Info entry differs from its predecessor.
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub enum TargetChange {
225    /// Request-URI change for the same target user.
226    Rc(HistoryIndex),
227    /// Request-URI change to a different target user.
228    Mp(HistoryIndex),
229    /// No Request-URI change.
230    Np(HistoryIndex),
231}
232
233impl TargetChange {
234    fn index(&self) -> &HistoryIndex {
235        match self {
236            Self::Rc(index) | Self::Mp(index) | Self::Np(index) => index,
237        }
238    }
239
240    fn name(&self) -> &'static [u8] {
241        match self {
242            Self::Rc(_) => b"rc",
243            Self::Mp(_) => b"mp",
244            Self::Np(_) => b"np",
245        }
246    }
247}
248
249/// Target-change semantics selected when extending a history.
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub enum TargetChangeKind {
252    /// Same target user, changed Request-URI.
253    Rc,
254    /// Different target user.
255    Mp,
256    /// Unchanged Request-URI.
257    Np,
258}
259
260impl TargetChangeKind {
261    fn with(self, index: HistoryIndex) -> TargetChange {
262        match self {
263            Self::Rc => TargetChange::Rc(index),
264            Self::Mp => TargetChange::Mp(index),
265            Self::Np => TargetChange::Np(index),
266        }
267    }
268}
269
270/// One History-Info entry.
271#[derive(Debug, Clone)]
272pub struct HistoryEntry {
273    /// The URI targeted at this hop.
274    pub target: Uri,
275    /// The hierarchical position of this hop.
276    pub index: HistoryIndex,
277    /// The preceding target and the semantics of the change.
278    pub change: Option<TargetChange>,
279    extensions: Vec<HeaderParam>,
280}
281
282impl HistoryEntry {
283    /// Construct an entry without extensions.
284    #[must_use]
285    pub fn new(target: Uri, index: HistoryIndex, change: Option<TargetChange>) -> Self {
286        Self {
287            target,
288            index,
289            change,
290            extensions: Vec::new(),
291        }
292    }
293
294    /// Reasons embedded in this entry's targeted-to URI.
295    pub fn reasons(&self) -> Result<Vec<ReasonValue>, HeaderError> {
296        let Some(headers) = self.target.headers() else {
297            return Ok(Vec::new());
298        };
299        let mut reasons = Vec::new();
300        for header in headers.iter().filter(|p| p.has_name("Reason")) {
301            let encoded = header
302                .value()
303                .ok_or(HeaderError::Syntax { header: REASON })?;
304            let decoded = escape::decode(encoded).ok_or(HeaderError::Syntax { header: REASON })?;
305            reasons.extend(Reason::decode(&decoded)?.0);
306        }
307        Ok(reasons)
308    }
309
310    fn embed_reason(&mut self, reason: &ReasonValue) {
311        let encoded = percent_encode(&reason.to_bytes());
312        let _ = self.target.push_header(Param::new(
313            Bytes::from_static(b"Reason"),
314            Bytes::from(encoded),
315        ));
316    }
317
318    fn wants_privacy(&self) -> Result<bool, HeaderError> {
319        let Some(headers) = self.target.headers() else {
320            return Ok(false);
321        };
322        for header in headers.iter().filter(|p| p.has_name("Privacy")) {
323            let encoded = header.value().ok_or(HeaderError::Syntax {
324                header: HISTORY_INFO,
325            })?;
326            let decoded = escape::decode(encoded).ok_or(HeaderError::Syntax {
327                header: HISTORY_INFO,
328            })?;
329            if decoded.eq_ignore_ascii_case(b"history") {
330                return Ok(true);
331            }
332        }
333        Ok(false)
334    }
335
336    fn anonymize(&mut self) -> Result<(), HeaderError> {
337        self.target = Uri::parse(Bytes::from_static(b"sip:anonymous@anonymous.invalid")).map_err(
338            |source| HeaderError::Uri {
339                header: HISTORY_INFO,
340                source,
341            },
342        )?;
343        self.extensions.clear();
344        Ok(())
345    }
346
347    /// Serialize one entry.
348    #[must_use]
349    pub fn to_bytes(&self) -> Bytes {
350        let mut out = Vec::new();
351        out.push(b'<');
352        out.extend_from_slice(&self.target.to_bytes());
353        out.extend_from_slice(b">;index=");
354        out.extend_from_slice(&self.index.to_bytes());
355        if let Some(change) = &self.change {
356            out.push(b';');
357            out.extend_from_slice(change.name());
358            out.push(b'=');
359            out.extend_from_slice(&change.index().to_bytes());
360        }
361        for parameter in &self.extensions {
362            write_parameter(parameter, &mut out);
363        }
364        Bytes::from(out)
365    }
366
367    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
368        let address = Address::parse(value, HISTORY_INFO)?;
369        let indices: Vec<_> = address.params.iter().filter(|p| p.is("index")).collect();
370        if indices.len() != 1 {
371            return Err(HeaderError::Syntax {
372                header: HISTORY_INFO,
373            });
374        }
375        let index = HistoryIndex::parse(indices.first().and_then(|p| p.value.as_deref()).ok_or(
376            HeaderError::Syntax {
377                header: HISTORY_INFO,
378            },
379        )?)?;
380        let changes: Vec<_> = address
381            .params
382            .iter()
383            .filter(|p| p.is("rc") || p.is("mp") || p.is("np"))
384            .collect();
385        if changes.len() > 1 {
386            return Err(HeaderError::Syntax {
387                header: HISTORY_INFO,
388            });
389        }
390        let change = changes
391            .first()
392            .map(|parameter| {
393                let referenced = HistoryIndex::parse(parameter.value.as_deref().ok_or(
394                    HeaderError::Syntax {
395                        header: HISTORY_INFO,
396                    },
397                )?)?;
398                Ok(if parameter.is("rc") {
399                    TargetChange::Rc(referenced)
400                } else if parameter.is("mp") {
401                    TargetChange::Mp(referenced)
402                } else {
403                    TargetChange::Np(referenced)
404                })
405            })
406            .transpose()?;
407        let extensions = address
408            .params
409            .into_iter()
410            .filter(|p| !p.is("index") && !p.is("rc") && !p.is("mp") && !p.is("np"))
411            .collect();
412        Ok(Self {
413            target: address.uri,
414            index,
415            change,
416            extensions,
417        })
418    }
419}
420
421/// A complete History-Info list in wire order.
422#[derive(Debug, Clone, Default)]
423pub struct HistoryInfo(pub Vec<HistoryEntry>);
424
425impl HistoryInfo {
426    /// Parse every History-Info row as one ordered cache.
427    ///
428    /// RFC 3261 ยง7.3 makes repeated list-header rows equivalent to one comma-joined row. The
429    /// history indices must therefore be validated across the joined value: decoding a later row
430    /// by itself would incorrectly reject its first `1.1` entry for not beginning at `1`.
431    pub fn from_headers(headers: &crate::message::Headers) -> Option<Result<Self, HeaderError>> {
432        let mut joined = Vec::new();
433        for header in headers.get_all(&HeaderName::HistoryInfo) {
434            if !joined.is_empty() {
435                joined.extend_from_slice(b", ");
436            }
437            joined.extend_from_slice(&header.value());
438        }
439        (!joined.is_empty()).then(|| Self::decode(&joined))
440    }
441
442    /// Start a history at the mandatory first index.
443    #[must_use]
444    pub fn initial(target: Uri) -> Self {
445        Self(vec![HistoryEntry::new(target, HistoryIndex::first(), None)])
446    }
447
448    /// Extend this UA's history for a retargeting action.
449    ///
450    /// If the received cache omitted the actual previous Request-URI, the inserted `.0` entry
451    /// makes that gap visible before the new `.1` entry is appended.
452    #[must_use]
453    pub fn retargeted(
454        mut self,
455        previous: Uri,
456        next: Uri,
457        reason: &ReasonValue,
458        kind: TargetChangeKind,
459    ) -> Self {
460        if self.0.is_empty() {
461            self = Self::initial(previous.clone());
462        }
463        let last_matches = self
464            .0
465            .last()
466            .is_some_and(|entry| entry.target.equivalent(&previous));
467        if !last_matches {
468            let gap_index = self
469                .0
470                .last()
471                .map_or_else(HistoryIndex::first, |entry| entry.index.gap());
472            self.0.push(HistoryEntry::new(previous, gap_index, None));
473        }
474        let previous_index = self
475            .0
476            .last()
477            .map_or_else(HistoryIndex::first, |entry| entry.index.clone());
478        if let Some(entry) = self.0.last_mut() {
479            entry.embed_reason(reason);
480        }
481        self.0.push(HistoryEntry::new(
482            next,
483            previous_index.forwarded(),
484            Some(kind.with(previous_index)),
485        ));
486        self
487    }
488
489    /// Apply RFC 7044 history privacy before emitting the cache.
490    pub fn apply_privacy(&mut self, message_privacy: bool) -> Result<(), HeaderError> {
491        for entry in &mut self.0 {
492            if message_privacy || entry.wants_privacy()? {
493                entry.anonymize()?;
494            }
495        }
496        Ok(())
497    }
498
499    /// Apply message-level `Privacy: history` or `Privacy: header`, plus any entry-level
500    /// privacy marker, before emitting this cache.
501    pub fn apply_message_privacy(
502        &mut self,
503        headers: &crate::message::Headers,
504    ) -> Result<(), HeaderError> {
505        self.apply_privacy(message_requests_history_privacy(headers)?)
506    }
507
508    /// Serialize the complete comma-separated list.
509    #[must_use]
510    pub fn to_bytes(&self) -> Bytes {
511        join(self.0.iter().map(HistoryEntry::to_bytes))
512    }
513}
514
515/// Build the history a UAS returns in a non-100 response.
516///
517/// A malformed typed history is omitted: reflecting its opaque bytes could leak a target after
518/// a privacy request, while response construction itself must remain infallible for a syntactically
519/// valid request.
520pub(crate) fn for_response(
521    request: &crate::message::Request,
522    status: crate::message::StatusCode,
523) -> Option<Bytes> {
524    if status.code() == 100 {
525        return None;
526    }
527    let mut history = if let Some(parsed) = HistoryInfo::from_headers(&request.headers) {
528        parsed.ok()?
529    } else {
530        let supported = request
531            .headers
532            .typed_all::<crate::headers::Supported>()
533            .filter_map(Result::ok)
534            .any(|tags| tags.contains("histinfo"));
535        if !supported {
536            return None;
537        }
538        HistoryInfo::initial(request.uri.clone())
539    };
540    let message_privacy = message_requests_history_privacy(&request.headers).ok()?;
541    history.apply_privacy(message_privacy).ok()?;
542    Some(history.to_bytes())
543}
544
545fn message_requests_history_privacy(
546    headers: &crate::message::Headers,
547) -> Result<bool, HeaderError> {
548    let mut requested = false;
549    for privacy in headers.typed_all::<crate::headers::Privacy>() {
550        let privacy = privacy?;
551        requested |= privacy.is(&crate::headers::PrivacyValue::History)
552            || privacy.is(&crate::headers::PrivacyValue::Header);
553    }
554    Ok(requested)
555}
556
557impl TypedHeader for HistoryInfo {
558    const NAME: HeaderName = HeaderName::HistoryInfo;
559
560    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
561        let entries = grammar::split_list(value, HISTORY_INFO)?
562            .into_iter()
563            .map(HistoryEntry::decode)
564            .collect::<Result<Vec<_>, _>>()?;
565        if entries.is_empty() {
566            return Err(HeaderError::Syntax {
567                header: HISTORY_INFO,
568            });
569        }
570        if entries
571            .first()
572            .is_none_or(|entry| entry.index != HistoryIndex::first())
573        {
574            return Err(HeaderError::Syntax {
575                header: HISTORY_INFO,
576            });
577        }
578        for (position, entry) in entries.iter().enumerate() {
579            if let Some(change) = &entry.change {
580                let prior = entries
581                    .get(..position)
582                    .unwrap_or(&[])
583                    .iter()
584                    .any(|candidate| candidate.index == *change.index());
585                if !prior {
586                    return Err(HeaderError::Syntax {
587                        header: HISTORY_INFO,
588                    });
589                }
590            }
591        }
592        Ok(Self(entries))
593    }
594}
595
596fn join(values: impl Iterator<Item = Bytes>) -> Bytes {
597    let mut out = Vec::new();
598    for (position, value) in values.enumerate() {
599        if position != 0 {
600            out.extend_from_slice(b", ");
601        }
602        out.extend_from_slice(&value);
603    }
604    Bytes::from(out)
605}
606
607fn write_parameter(parameter: &HeaderParam, out: &mut Vec<u8>) {
608    out.push(b';');
609    out.extend_from_slice(&parameter.name);
610    if let Some(value) = &parameter.value {
611        out.push(b'=');
612        if value.iter().all(|&b| is_token_char(b)) {
613            out.extend_from_slice(value);
614        } else {
615            out.push(b'"');
616            write_quoted(value, out);
617            out.push(b'"');
618        }
619    }
620}
621
622fn write_quoted(value: &[u8], out: &mut Vec<u8>) {
623    for &byte in value {
624        if matches!(byte, b'\\' | b'"') {
625            out.push(b'\\');
626        }
627        out.push(byte);
628    }
629}
630
631fn percent_encode(value: &[u8]) -> Vec<u8> {
632    const HEX: &[u8; 16] = b"0123456789ABCDEF";
633    let mut out = Vec::with_capacity(value.len());
634    for &byte in value {
635        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
636            out.push(byte);
637        } else {
638            out.push(b'%');
639            out.push(*HEX.get(usize::from(byte >> 4)).unwrap_or(&b'0'));
640            out.push(*HEX.get(usize::from(byte & 0x0f)).unwrap_or(&b'0'));
641        }
642    }
643    out
644}
645
646#[cfg(test)]
647#[allow(
648    clippy::unwrap_used,
649    clippy::expect_used,
650    clippy::panic,
651    clippy::indexing_slicing
652)]
653mod tests {
654    use super::*;
655    use crate::message::StatusCode;
656
657    fn uri(value: &'static [u8]) -> Uri {
658        Uri::parse(Bytes::from_static(value)).unwrap()
659    }
660
661    #[test]
662    fn a_retargeted_request_carries_the_previous_target_and_the_reason_it_moved() {
663        let history = HistoryInfo::initial(uri(b"sip:alice@example.test")).retargeted(
664            uri(b"sip:alice@example.test"),
665            uri(b"sip:bob@example.test"),
666            &ReasonValue::sip(StatusCode::new(302).unwrap(), None),
667            TargetChangeKind::Mp,
668        );
669        assert_eq!(
670            history.to_bytes(),
671            Bytes::from_static(
672                b"<sip:alice@example.test?Reason=SIP%3Bcause%3D302>;index=1, <sip:bob@example.test>;index=1.1;mp=1"
673            )
674        );
675        assert_eq!(history.0[0].reasons().unwrap()[0].cause(), 302);
676    }
677
678    #[test]
679    fn a_missing_previous_target_gets_a_visible_zero_index() {
680        let history = HistoryInfo::initial(uri(b"sip:first@example.test")).retargeted(
681            uri(b"sip:hidden@example.test"),
682            uri(b"sip:last@example.test"),
683            &ReasonValue::sip(StatusCode::new(302).unwrap(), None),
684            TargetChangeKind::Mp,
685        );
686        assert_eq!(history.0[1].index.to_bytes(), Bytes::from_static(b"1.0"));
687        assert_eq!(history.0[2].index.to_bytes(), Bytes::from_static(b"1.0.1"));
688    }
689
690    #[test]
691    fn history_privacy_keeps_indices_and_hides_targets() {
692        let mut history = HistoryInfo::initial(uri(b"sip:alice@example.test")).retargeted(
693            uri(b"sip:alice@example.test"),
694            uri(b"sip:bob@example.test"),
695            &ReasonValue::sip(StatusCode::new(302).unwrap(), None),
696            TargetChangeKind::Mp,
697        );
698        history.apply_privacy(true).unwrap();
699        assert_eq!(
700            history.to_bytes(),
701            Bytes::from_static(
702                b"<sip:anonymous@anonymous.invalid>;index=1, <sip:anonymous@anonymous.invalid>;index=1.1;mp=1"
703            )
704        );
705    }
706
707    #[test]
708    fn typed_history_rejects_a_forward_target_reference() {
709        assert!(HistoryInfo::decode(b"<sip:a@b>;index=1;mp=1.1, <sip:c@d>;index=1.1").is_err());
710    }
711
712    #[test]
713    fn typed_history_requires_the_first_index_to_be_one() {
714        assert!(HistoryInfo::decode(b"<sip:a@b>;index=2").is_err());
715        assert!(HistoryInfo::decode(b"<sip:a@b>;index=1").is_ok());
716    }
717
718    #[test]
719    fn a_tel_target_does_not_receive_a_uri_reason_component() {
720        let history = HistoryInfo::initial(uri(b"tel:+12015550123")).retargeted(
721            uri(b"tel:+12015550123"),
722            uri(b"sip:bob@example.test"),
723            &ReasonValue::sip(StatusCode::new(302).unwrap(), None),
724            TargetChangeKind::Mp,
725        );
726        assert_eq!(
727            history.0[0].target.to_bytes(),
728            Bytes::from_static(b"tel:+12015550123")
729        );
730        assert!(history.0[0].reasons().unwrap().is_empty());
731    }
732
733    #[test]
734    fn reason_validates_protocol_specific_ranges() {
735        assert!(Reason::decode(b"SIP;cause=99").is_err());
736        assert!(Reason::decode(b"Q.850;cause=128").is_err());
737        assert_eq!(
738            Reason::decode(b"SIP;cause=486;text=Busy").unwrap().0[0].cause(),
739            486
740        );
741    }
742}