Skip to main content

sccp_protocol/phone/
service.rs

1//! Typed payloads returned by phone-hosted service applications.
2//!
3//! The SCCP application envelope carries routing identifiers separately from
4//! its payload. Execute responses use documented XML schemas, while interactive
5//! input and menu callbacks use a relative route followed by a standard URL
6//! query string.
7
8use std::fmt;
9
10use percent_encoding::percent_decode_str;
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14use super::xml::{self as phone_xml, PhoneXmlError};
15use crate::types::{ApplicationId, CallReference, LineInstance, TransactionId};
16
17/// Maximum application payload accepted from an envelope, in bytes.
18pub const MAX_PHONE_SERVICE_DATA_BYTES: usize = 2_000;
19/// Maximum number of result items in an execute response.
20pub const MAX_PHONE_SERVICE_RESPONSE_ITEMS: usize = 3;
21/// Maximum number of decoded components in a submission route.
22pub const MAX_PHONE_SERVICE_ROUTE_SEGMENTS: usize = 32;
23/// Maximum UTF-8 byte length of one decoded route component.
24pub const MAX_PHONE_SERVICE_ROUTE_COMPONENT_BYTES: usize = 1_024;
25/// Maximum number of ordered name/value pairs retained from a submission.
26pub const MAX_PHONE_SERVICE_SUBMITTED_VALUES: usize = 32;
27/// Maximum UTF-8 byte length of a decoded submission parameter name.
28pub const MAX_PHONE_SERVICE_PARAMETER_NAME_BYTES: usize = 128;
29/// Maximum UTF-8 byte length of a decoded submission parameter value.
30pub const MAX_PHONE_SERVICE_PARAMETER_VALUE_BYTES: usize = 1_024;
31/// Maximum character count of the data field in an execute result item.
32pub const MAX_PHONE_SERVICE_RESPONSE_DATA_CHARS: usize = 256;
33/// Maximum character count of the URL field in an execute result item.
34pub const MAX_PHONE_SERVICE_RESPONSE_URL_CHARS: usize = 256;
35/// Maximum character count of a structured application error message.
36pub const MAX_PHONE_SERVICE_ERROR_MESSAGE_CHARS: usize = 256;
37
38/// Envelope direction used to choose the permitted payload grammar.
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum PhoneServiceMessageKind {
41    /// An application-data envelope, which may contain an interactive submission.
42    Data,
43    /// A response envelope, which accepts only structured responses or opaque data.
44    Response,
45}
46
47/// Identifiers copied from the SCCP application envelope rather than inferred
48/// from the submitted payload.
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub struct PhoneServiceRouting {
51    pub application_id: ApplicationId,
52    pub line_instance: LineInstance,
53    pub call_reference: CallReference,
54    pub transaction_id: TransactionId,
55}
56
57/// Additional selectors carried only by the extended application envelope.
58#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
59pub struct PhoneServiceExtendedRouting {
60    /// Sender-defined continuation marker retained without interpretation.
61    pub sequence_flag: u32,
62    /// Sender-defined display ordering hint retained without interpretation.
63    pub display_priority: u32,
64    /// Conference association from the extended envelope, or zero when absent.
65    pub conference_id: u32,
66    /// Instance discriminator for applications with concurrent executions.
67    pub application_instance_id: u32,
68    /// Sender-defined routing selector retained without interpretation.
69    pub routing: u32,
70}
71
72/// One decoded application envelope and its typed or preserved payload.
73#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct PhoneServiceEvent {
75    /// Determines which payload grammar was accepted.
76    pub kind: PhoneServiceMessageKind,
77    pub routing: PhoneServiceRouting,
78    /// Extended fields when the envelope used the extended message form.
79    pub extended: Option<PhoneServiceExtendedRouting>,
80    /// Decoded payload or exact bounded bytes for an unsupported payload.
81    pub payload: PhoneServicePayload,
82}
83
84/// Supported application payloads after envelope decoding.
85#[derive(Clone, Debug, Eq, PartialEq)]
86pub enum PhoneServicePayload {
87    ExecuteResponse(CiscoIpPhoneResponse),
88    Error(CiscoIpPhoneError),
89    Submission(PhoneServiceSubmission),
90    /// A syntactically valid but unsupported XML schema, a non-XML response,
91    /// or binary application data. The protocol boundary already limits it.
92    Opaque(Vec<u8>),
93}
94
95/// A bounded collection of results for previously requested execute actions.
96#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
97#[serde(rename = "CiscoIPPhoneResponse")]
98pub struct CiscoIpPhoneResponse {
99    #[serde(rename = "ResponseItem", default)]
100    pub items: Vec<CiscoIpPhoneResponseItem>,
101}
102
103impl CiscoIpPhoneResponse {
104    /// Enforces the response item count and text bounds before serialization.
105    pub fn validate(&self) -> Result<(), PhoneXmlError> {
106        if self.items.len() > MAX_PHONE_SERVICE_RESPONSE_ITEMS {
107            return Err(PhoneXmlError::LimitExceeded {
108                kind: "phone-service response items",
109                actual: self.items.len(),
110                maximum: MAX_PHONE_SERVICE_RESPONSE_ITEMS,
111            });
112        }
113        for item in &self.items {
114            validate_response_text(
115                "phone-service response data",
116                &item.data,
117                MAX_PHONE_SERVICE_RESPONSE_DATA_CHARS,
118            )?;
119            validate_response_text(
120                "phone-service response URL",
121                &item.url,
122                MAX_PHONE_SERVICE_RESPONSE_URL_CHARS,
123            )?;
124        }
125        Ok(())
126    }
127}
128
129/// Extensible execute result code that preserves unknown numeric values.
130#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
131#[serde(transparent)]
132pub struct PhoneExecuteStatus(pub u32);
133
134impl PhoneExecuteStatus {
135    pub const OK: Self = Self(0);
136    pub const ERROR: Self = Self(1);
137    pub const URI_NOT_FOUND: Self = Self(4);
138    pub const NO_ACTIVE_CALL: Self = Self(6);
139
140    pub const fn get(self) -> u32 {
141        self.0
142    }
143}
144
145/// Result metadata for one requested execute action.
146#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
147pub struct CiscoIpPhoneResponseItem {
148    #[serde(rename = "@Status", alias = "Status")]
149    pub status: PhoneExecuteStatus,
150    #[serde(rename = "@Data", alias = "Data")]
151    /// Result text constrained to [`MAX_PHONE_SERVICE_RESPONSE_DATA_CHARS`] characters.
152    pub data: String,
153    #[serde(rename = "@URL", alias = "URL")]
154    /// Result URL constrained to [`MAX_PHONE_SERVICE_RESPONSE_URL_CHARS`] characters.
155    pub url: String,
156}
157
158/// Extensible structured-error number that preserves unknown numeric values.
159#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
160#[serde(transparent)]
161pub struct PhoneServiceErrorCode(pub u32);
162
163impl PhoneServiceErrorCode {
164    pub const PARSING: Self = Self(1);
165    pub const FRAMING: Self = Self(2);
166    pub const INTERNAL_FILE: Self = Self(3);
167    pub const AUTHENTICATION: Self = Self(4);
168
169    pub const fn get(self) -> u32 {
170        self.0
171    }
172}
173
174/// Structured application error suitable for XML serialization.
175#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
176#[serde(rename = "CiscoIPPhoneError")]
177pub struct CiscoIpPhoneError {
178    #[serde(rename = "@Number", alias = "Number")]
179    pub number: PhoneServiceErrorCode,
180    #[serde(rename = "$text", default, skip_serializing_if = "String::is_empty")]
181    pub message: String,
182}
183
184impl CiscoIpPhoneError {
185    /// Enforces the error-message character bound before serialization.
186    pub fn validate(&self) -> Result<(), PhoneXmlError> {
187        validate_response_text(
188            "phone-service error message",
189            &self.message,
190            MAX_PHONE_SERVICE_ERROR_MESSAGE_CHARS,
191        )
192    }
193}
194
195/// Percent-decoded interactive callback submitted by a phone application.
196#[derive(Clone, Debug, Eq, PartialEq)]
197pub struct PhoneServiceSubmission {
198    /// Percent-decoded route components in their original order.
199    pub route: Vec<String>,
200    /// Ordered form values. Duplicate names remain distinct.
201    pub values: Vec<PhoneServiceSubmittedValue>,
202}
203
204impl PhoneServiceSubmission {
205    /// Iterates matching values in submission order, including duplicates.
206    pub fn values_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
207        self.values
208            .iter()
209            .filter(move |value| value.name == name)
210            .map(|value| value.value.as_str())
211    }
212}
213
214/// One submitted name/value pair whose value is redacted from diagnostics.
215#[derive(Clone, Eq, PartialEq)]
216pub struct PhoneServiceSubmittedValue {
217    pub name: String,
218    /// Percent-decoded value redacted from [`Debug`](std::fmt::Debug) output.
219    pub value: String,
220}
221
222impl fmt::Debug for PhoneServiceSubmittedValue {
223    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
224        formatter
225            .debug_struct("PhoneServiceSubmittedValue")
226            .field("name", &self.name)
227            .field("value", &"<redacted>")
228            .finish()
229    }
230}
231
232/// Failures while validating or decoding an application payload.
233#[derive(Debug, Error)]
234pub enum PhoneServiceError {
235    /// A structured XML payload was malformed or violated its schema bounds.
236    #[error(transparent)]
237    Xml(#[from] PhoneXmlError),
238    #[error("phone-service submission is not valid UTF-8")]
239    InvalidUtf8,
240    #[error("phone-service submission contains invalid percent encoding")]
241    InvalidPercentEncoding,
242    #[error("phone-service submission contains a forbidden control character")]
243    ControlCharacter,
244    /// The route exceeded [`MAX_PHONE_SERVICE_ROUTE_SEGMENTS`].
245    #[error("phone-service submission route has {actual} components; maximum is {maximum}")]
246    TooManyRouteSegments { actual: usize, maximum: usize },
247    /// The query exceeded [`MAX_PHONE_SERVICE_SUBMITTED_VALUES`].
248    #[error("phone-service submission has {actual} values; maximum is {maximum}")]
249    TooManyValues { actual: usize, maximum: usize },
250    #[error("phone-service submission has an empty parameter name")]
251    EmptyParameterName,
252    #[error("phone-service submission has an empty route component")]
253    EmptyRouteComponent,
254    /// A decoded route, name, or value exceeded its corresponding byte limit.
255    #[error("phone-service submission {kind} has {actual} bytes; maximum is {maximum}")]
256    ComponentTooLong {
257        kind: &'static str,
258        actual: usize,
259        maximum: usize,
260    },
261}
262
263#[derive(Deserialize)]
264enum PhoneServiceXmlSchema {
265    #[serde(rename = "CiscoIPPhoneResponse")]
266    Response {
267        #[serde(rename = "ResponseItem", default)]
268        items: Vec<CiscoIpPhoneResponseItem>,
269    },
270    #[serde(rename = "CiscoIPPhoneError")]
271    Error {
272        #[serde(rename = "@Number", alias = "Number")]
273        number: PhoneServiceErrorCode,
274        #[serde(rename = "$text", default)]
275        message: String,
276    },
277    #[serde(other)]
278    Unknown,
279}
280
281/// Parse application data according to its message kind. Response envelopes
282/// accept the two documented XML schemas; data envelopes additionally accept
283/// interactive route/query submissions.
284pub fn parse_phone_service_payload(
285    data: &[u8],
286    kind: PhoneServiceMessageKind,
287) -> Result<PhoneServicePayload, PhoneServiceError> {
288    if data.len() > MAX_PHONE_SERVICE_DATA_BYTES {
289        return Err(PhoneXmlError::LimitExceeded {
290            kind: "phone-service data",
291            actual: data.len(),
292            maximum: MAX_PHONE_SERVICE_DATA_BYTES,
293        }
294        .into());
295    }
296    let trimmed = trim_protocol_padding(data);
297    if trimmed.is_empty() {
298        return Ok(PhoneServicePayload::Opaque(data.to_vec()));
299    }
300
301    if trimmed.first() == Some(&b'<') {
302        return match phone_xml::from_bytes::<PhoneServiceXmlSchema>(
303            trimmed,
304            MAX_PHONE_SERVICE_DATA_BYTES,
305        )? {
306            PhoneServiceXmlSchema::Response { items } => {
307                let response = CiscoIpPhoneResponse { items };
308                response.validate()?;
309                Ok(PhoneServicePayload::ExecuteResponse(response))
310            }
311            PhoneServiceXmlSchema::Error { number, message } => {
312                let error = CiscoIpPhoneError { number, message };
313                error.validate()?;
314                Ok(PhoneServicePayload::Error(error))
315            }
316            PhoneServiceXmlSchema::Unknown => Ok(PhoneServicePayload::Opaque(data.to_vec())),
317        };
318    }
319
320    if kind == PhoneServiceMessageKind::Response {
321        return Ok(PhoneServicePayload::Opaque(data.to_vec()));
322    }
323    let Ok(text) = std::str::from_utf8(trimmed) else {
324        return Ok(PhoneServicePayload::Opaque(data.to_vec()));
325    };
326    parse_submission(text).map(PhoneServicePayload::Submission)
327}
328
329fn validate_response_text(
330    field: &'static str,
331    value: &str,
332    maximum: usize,
333) -> Result<(), PhoneXmlError> {
334    if value.chars().count() > maximum {
335        Err(PhoneXmlError::InvalidField {
336            field,
337            expected: "within the documented phone-service text bound",
338        })
339    } else {
340        Ok(())
341    }
342}
343
344fn parse_submission(text: &str) -> Result<PhoneServiceSubmission, PhoneServiceError> {
345    if text.chars().any(char::is_control) || text.contains('#') {
346        return Err(PhoneServiceError::ControlCharacter);
347    }
348    let (route, query) = text
349        .split_once('?')
350        .map_or((text, None), |(route, query)| (route, Some(query)));
351    let route = if route.is_empty() {
352        Vec::new()
353    } else {
354        route
355            .split('/')
356            .map(|component| {
357                decode_component(
358                    component,
359                    "route component",
360                    MAX_PHONE_SERVICE_ROUTE_COMPONENT_BYTES,
361                )
362            })
363            .collect::<Result<Vec<_>, _>>()?
364    };
365    if route.len() > MAX_PHONE_SERVICE_ROUTE_SEGMENTS {
366        return Err(PhoneServiceError::TooManyRouteSegments {
367            actual: route.len(),
368            maximum: MAX_PHONE_SERVICE_ROUTE_SEGMENTS,
369        });
370    }
371    let values = query.map_or_else(|| Ok(Vec::new()), parse_form_values)?;
372    Ok(PhoneServiceSubmission { route, values })
373}
374
375fn parse_form_values(query: &str) -> Result<Vec<PhoneServiceSubmittedValue>, PhoneServiceError> {
376    if query.is_empty() {
377        return Ok(Vec::new());
378    }
379    for field in query.split('&') {
380        if field.is_empty() {
381            return Err(PhoneServiceError::EmptyParameterName);
382        }
383        let (name, value) = field.split_once('=').unwrap_or((field, ""));
384        validate_encoded_component(name)?;
385        validate_encoded_component(value)?;
386        percent_decode_str(name)
387            .decode_utf8()
388            .map_err(|_| PhoneServiceError::InvalidUtf8)?;
389        percent_decode_str(value)
390            .decode_utf8()
391            .map_err(|_| PhoneServiceError::InvalidUtf8)?;
392    }
393
394    let values = form_urlencoded::parse(query.as_bytes())
395        .map(|(name, value)| {
396            if name.is_empty() {
397                return Err(PhoneServiceError::EmptyParameterName);
398            }
399            validate_component_length(
400                "parameter name",
401                &name,
402                MAX_PHONE_SERVICE_PARAMETER_NAME_BYTES,
403            )?;
404            validate_component_length(
405                "parameter value",
406                &value,
407                MAX_PHONE_SERVICE_PARAMETER_VALUE_BYTES,
408            )?;
409            reject_decoded_controls(&name)?;
410            reject_decoded_controls(&value)?;
411            Ok(PhoneServiceSubmittedValue {
412                name: name.into_owned(),
413                value: value.into_owned(),
414            })
415        })
416        .collect::<Result<Vec<_>, _>>()?;
417    if values.len() > MAX_PHONE_SERVICE_SUBMITTED_VALUES {
418        return Err(PhoneServiceError::TooManyValues {
419            actual: values.len(),
420            maximum: MAX_PHONE_SERVICE_SUBMITTED_VALUES,
421        });
422    }
423    Ok(values)
424}
425
426fn decode_component(
427    encoded: &str,
428    kind: &'static str,
429    maximum: usize,
430) -> Result<String, PhoneServiceError> {
431    if encoded.is_empty() {
432        return Err(PhoneServiceError::EmptyRouteComponent);
433    }
434    validate_encoded_component(encoded)?;
435    let decoded = percent_decode_str(encoded)
436        .decode_utf8()
437        .map_err(|_| PhoneServiceError::InvalidUtf8)?;
438    validate_component_length(kind, &decoded, maximum)?;
439    reject_decoded_controls(&decoded)?;
440    Ok(decoded.into_owned())
441}
442
443fn validate_encoded_component(encoded: &str) -> Result<(), PhoneServiceError> {
444    let bytes = encoded.as_bytes();
445    let mut index = 0;
446    while index < bytes.len() {
447        if bytes[index] == b'%' {
448            let Some(pair) = bytes.get(index + 1..index + 3) else {
449                return Err(PhoneServiceError::InvalidPercentEncoding);
450            };
451            if !pair.iter().all(u8::is_ascii_hexdigit) {
452                return Err(PhoneServiceError::InvalidPercentEncoding);
453            }
454            index += 3;
455        } else {
456            index += 1;
457        }
458    }
459    Ok(())
460}
461
462fn validate_component_length(
463    kind: &'static str,
464    component: &str,
465    maximum: usize,
466) -> Result<(), PhoneServiceError> {
467    if component.len() > maximum {
468        return Err(PhoneServiceError::ComponentTooLong {
469            kind,
470            actual: component.len(),
471            maximum,
472        });
473    }
474    Ok(())
475}
476
477fn reject_decoded_controls(component: &str) -> Result<(), PhoneServiceError> {
478    if component.chars().any(char::is_control) {
479        Err(PhoneServiceError::ControlCharacter)
480    } else {
481        Ok(())
482    }
483}
484
485fn trim_protocol_padding(mut data: &[u8]) -> &[u8] {
486    while data.first().is_some_and(|byte| byte.is_ascii_whitespace()) {
487        data = &data[1..];
488    }
489    while data
490        .last()
491        .is_some_and(|byte| *byte == 0 || byte.is_ascii_whitespace())
492    {
493        data = &data[..data.len() - 1];
494    }
495    data
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    #[test]
503    fn typed_execute_responses_and_errors_use_the_xml_boundary() {
504        let payload = parse_phone_service_payload(
505            br#"<CiscoIPPhoneResponse><ResponseItem Status="0" Data="Taylor &amp; Co" URL="Play:chime.raw"/><ResponseItem Status="6" Data="No Active Call" URL="SendDigits:12"/></CiscoIPPhoneResponse>"#,
506            PhoneServiceMessageKind::Response,
507        )
508        .unwrap();
509        let PhoneServicePayload::ExecuteResponse(response) = payload else {
510            panic!("expected typed execute response");
511        };
512        assert_eq!(response.items.len(), 2);
513        assert_eq!(response.items[0].status, PhoneExecuteStatus::OK);
514        assert_eq!(response.items[0].data, "Taylor & Co");
515        assert_eq!(response.items[1].status, PhoneExecuteStatus::NO_ACTIVE_CALL);
516
517        assert_eq!(
518            parse_phone_service_payload(
519                br#"<CiscoIPPhoneError Number="4">Authentication failed</CiscoIPPhoneError>"#,
520                PhoneServiceMessageKind::Response,
521            )
522            .unwrap(),
523            PhoneServicePayload::Error(CiscoIpPhoneError {
524                number: PhoneServiceErrorCode::AUTHENTICATION,
525                message: "Authentication failed".into(),
526            })
527        );
528    }
529
530    #[test]
531    fn submitted_route_and_form_values_are_decoded_in_order() {
532        let payload = parse_phone_service_payload(
533            b"invite/desk%20one?NUMBER=555%2A12&NAME=Fran%C3%A7ois&NOTE=a+b&NOTE=second",
534            PhoneServiceMessageKind::Data,
535        )
536        .unwrap();
537        let PhoneServicePayload::Submission(submission) = payload else {
538            panic!("expected typed submission");
539        };
540        assert_eq!(submission.route, ["invite", "desk one"]);
541        assert_eq!(
542            submission
543                .values
544                .iter()
545                .map(|value| (value.name.as_str(), value.value.as_str()))
546                .collect::<Vec<_>>(),
547            [
548                ("NUMBER", "555*12"),
549                ("NAME", "François"),
550                ("NOTE", "a b"),
551                ("NOTE", "second"),
552            ]
553        );
554        assert_eq!(
555            submission.values_named("NOTE").collect::<Vec<_>>(),
556            ["a b", "second"]
557        );
558        let debug = format!("{:?}", submission.values[0]);
559        assert!(debug.contains("<redacted>"));
560        assert!(!debug.contains("555"));
561    }
562
563    #[test]
564    fn opaque_payloads_are_reserved_for_unknown_schemas() {
565        let unknown = b"<VendorPhoneResult><Value>one</Value></VendorPhoneResult>";
566        assert_eq!(
567            parse_phone_service_payload(unknown, PhoneServiceMessageKind::Response).unwrap(),
568            PhoneServicePayload::Opaque(unknown.to_vec())
569        );
570        assert_eq!(
571            parse_phone_service_payload(b"Success", PhoneServiceMessageKind::Response).unwrap(),
572            PhoneServicePayload::Opaque(b"Success".to_vec())
573        );
574        assert_eq!(
575            parse_phone_service_payload(&[0xff, 0x00], PhoneServiceMessageKind::Data).unwrap(),
576            PhoneServicePayload::Opaque(vec![0xff, 0x00])
577        );
578    }
579
580    #[test]
581    fn malformed_xml_encoding_and_bounds_fail_closed_without_values_in_errors() {
582        for malformed in [
583            b"<CiscoIPPhoneResponse>".as_slice(),
584            b"<!DOCTYPE x><CiscoIPPhoneResponse/>".as_slice(),
585            b"<VendorPhoneResult>".as_slice(),
586        ] {
587            assert!(
588                parse_phone_service_payload(malformed, PhoneServiceMessageKind::Response).is_err()
589            );
590        }
591
592        for malformed in [
593            "invite?PIN=secret%",
594            "invite?PIN=secret%GG",
595            "invite?PIN=%FF",
596            "invite?PIN=secret%0Avalue",
597            "invite?=secret",
598            "invite//desk?PIN=secret",
599            "invite?PIN=secret#fragment",
600        ] {
601            let error =
602                parse_phone_service_payload(malformed.as_bytes(), PhoneServiceMessageKind::Data)
603                    .unwrap_err()
604                    .to_string();
605            assert!(!error.contains("secret"), "{error}");
606        }
607
608        assert!(
609            parse_phone_service_payload(
610                &vec![b'x'; MAX_PHONE_SERVICE_DATA_BYTES + 1],
611                PhoneServiceMessageKind::Data,
612            )
613            .is_err()
614        );
615        let too_many = format!(
616            "route?{}",
617            (0..=MAX_PHONE_SERVICE_SUBMITTED_VALUES)
618                .map(|index| format!("p{index}=x"))
619                .collect::<Vec<_>>()
620                .join("&")
621        );
622        assert!(
623            parse_phone_service_payload(too_many.as_bytes(), PhoneServiceMessageKind::Data,)
624                .is_err()
625        );
626        let too_many_items = format!(
627            "<CiscoIPPhoneResponse>{}</CiscoIPPhoneResponse>",
628            r#"<ResponseItem Status="0" Data="ok" URL="Init:Services"/>"#
629                .repeat(MAX_PHONE_SERVICE_RESPONSE_ITEMS + 1)
630        );
631        assert!(
632            parse_phone_service_payload(
633                too_many_items.as_bytes(),
634                PhoneServiceMessageKind::Response,
635            )
636            .is_err()
637        );
638    }
639
640    #[test]
641    fn every_submission_collection_and_component_bound_is_enforced() {
642        let too_many_route_segments =
643            std::iter::repeat_n("x", MAX_PHONE_SERVICE_ROUTE_SEGMENTS + 1)
644                .collect::<Vec<_>>()
645                .join("/");
646        assert!(matches!(
647            parse_phone_service_payload(
648                too_many_route_segments.as_bytes(),
649                PhoneServiceMessageKind::Data,
650            ),
651            Err(PhoneServiceError::TooManyRouteSegments { .. })
652        ));
653
654        let long_route = "x".repeat(MAX_PHONE_SERVICE_ROUTE_COMPONENT_BYTES + 1);
655        assert!(matches!(
656            parse_phone_service_payload(long_route.as_bytes(), PhoneServiceMessageKind::Data),
657            Err(PhoneServiceError::ComponentTooLong {
658                kind: "route component",
659                ..
660            })
661        ));
662
663        let long_name = format!(
664            "route?{}=value",
665            "n".repeat(MAX_PHONE_SERVICE_PARAMETER_NAME_BYTES + 1)
666        );
667        assert!(matches!(
668            parse_phone_service_payload(long_name.as_bytes(), PhoneServiceMessageKind::Data),
669            Err(PhoneServiceError::ComponentTooLong {
670                kind: "parameter name",
671                ..
672            })
673        ));
674
675        let long_value = format!(
676            "route?name={}",
677            "v".repeat(MAX_PHONE_SERVICE_PARAMETER_VALUE_BYTES + 1)
678        );
679        assert!(matches!(
680            parse_phone_service_payload(long_value.as_bytes(), PhoneServiceMessageKind::Data),
681            Err(PhoneServiceError::ComponentTooLong {
682                kind: "parameter value",
683                ..
684            })
685        ));
686    }
687}