Skip to main content

sccp_protocol/phone/xml/
document.rs

1//! Shared workflow for complete, schema-checked phone XML documents.
2//!
3//! Application code can use [`PhoneXmlDocument::parse_xml`] and
4//! [`PhoneXmlDocument::serialize_xml`] uniformly across supported display
5//! roots. The `*_with_limit` methods add a stricter per-request resource limit
6//! without weakening a document type's validation rules.
7
8use quick_xml::events::Event;
9use quick_xml::reader::Reader;
10use serde::Serialize;
11use serde::de::DeserializeOwned;
12
13use super::{
14    CiscoIpPhoneDirectory, CiscoIpPhoneExecute, CiscoIpPhoneIconFileMenu, CiscoIpPhoneIconMenu,
15    CiscoIpPhoneImageList, CiscoIpPhoneInput, CiscoIpPhoneMenu, CiscoIpPhoneStatus,
16    CiscoIpPhoneStatusFile, CiscoIpPhoneText, PHONE_BACKGROUND_LIST_MAX_BYTES,
17    PHONE_DIRECTORY_MAX_BYTES, PHONE_EXECUTE_MAX_BYTES, PHONE_INPUT_MAX_BYTES,
18    PHONE_MENU_MAX_BYTES, PHONE_STATUS_MAX_BYTES, PHONE_TEXT_MAX_BYTES, PhoneXmlError,
19    decoding_reader, from_bytes, to_string,
20};
21
22mod sealed {
23    pub trait Sealed {}
24}
25
26/// Common bounded parsing and serialization contract for a complete phone XML
27/// document.
28///
29/// The trait is sealed because every supported root has its own schema and
30/// validation policy. It centralizes the security boundary without allowing a
31/// downstream type to opt into parsing with a guessed root or size limit.
32pub trait PhoneXmlDocument: sealed::Sealed + Sized + Serialize + DeserializeOwned {
33    /// Exact root element accepted for this document model.
34    const ROOT: &'static [u8];
35    /// Default maximum encoded document size, in bytes.
36    const MAXIMUM_BYTES: usize;
37
38    /// Checks schema invariants that cannot be expressed by Serde types alone.
39    fn validate_document(&self) -> Result<(), PhoneXmlError>;
40
41    /// Parses the exact schema root within [`Self::MAXIMUM_BYTES`].
42    fn parse_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
43        Self::parse_xml_with_limit(document, Self::MAXIMUM_BYTES)
44    }
45
46    /// Parses the exact schema root within a caller-selected byte limit.
47    ///
48    /// This is useful when a transport or device profile imposes a bound
49    /// smaller than [`Self::MAXIMUM_BYTES`].
50    fn parse_xml_with_limit(document: &[u8], maximum_bytes: usize) -> Result<Self, PhoneXmlError> {
51        let parsed: Self = from_bytes(document, maximum_bytes)?;
52        validate_root(document, Self::ROOT)?;
53        parsed.validate_document()?;
54        Ok(parsed)
55    }
56
57    /// Validates and serializes the document within [`Self::MAXIMUM_BYTES`].
58    fn serialize_xml(&self) -> Result<String, PhoneXmlError> {
59        self.serialize_xml_with_limit(Self::MAXIMUM_BYTES)
60    }
61
62    /// Validates and serializes the document within a caller-selected byte limit.
63    fn serialize_xml_with_limit(&self, maximum_bytes: usize) -> Result<String, PhoneXmlError> {
64        self.validate_document()?;
65        to_string(self, maximum_bytes)
66    }
67}
68
69macro_rules! impl_phone_xml_document {
70    ($document:ty, $root:literal, $maximum:expr) => {
71        impl sealed::Sealed for $document {}
72
73        impl PhoneXmlDocument for $document {
74            const ROOT: &'static [u8] = $root;
75            const MAXIMUM_BYTES: usize = $maximum;
76
77            fn validate_document(&self) -> Result<(), PhoneXmlError> {
78                <$document>::validate(self)
79            }
80        }
81    };
82}
83
84impl_phone_xml_document!(CiscoIpPhoneText, b"CiscoIPPhoneText", PHONE_TEXT_MAX_BYTES);
85impl_phone_xml_document!(
86    CiscoIpPhoneInput,
87    b"CiscoIPPhoneInput",
88    PHONE_INPUT_MAX_BYTES
89);
90impl_phone_xml_document!(
91    CiscoIpPhoneExecute,
92    b"CiscoIPPhoneExecute",
93    PHONE_EXECUTE_MAX_BYTES
94);
95impl_phone_xml_document!(
96    CiscoIpPhoneImageList,
97    b"CiscoIPPhoneImageList",
98    PHONE_BACKGROUND_LIST_MAX_BYTES
99);
100impl_phone_xml_document!(
101    CiscoIpPhoneStatus,
102    b"CiscoIPPhoneStatus",
103    PHONE_STATUS_MAX_BYTES
104);
105impl_phone_xml_document!(
106    CiscoIpPhoneStatusFile,
107    b"CiscoIPPhoneStatusFile",
108    PHONE_STATUS_MAX_BYTES
109);
110impl_phone_xml_document!(
111    CiscoIpPhoneDirectory,
112    b"CiscoIPPhoneDirectory",
113    PHONE_DIRECTORY_MAX_BYTES
114);
115impl_phone_xml_document!(CiscoIpPhoneMenu, b"CiscoIPPhoneMenu", PHONE_MENU_MAX_BYTES);
116impl_phone_xml_document!(
117    CiscoIpPhoneIconMenu,
118    b"CiscoIPPhoneIconMenu",
119    PHONE_MENU_MAX_BYTES
120);
121impl_phone_xml_document!(
122    CiscoIpPhoneIconFileMenu,
123    b"CiscoIPPhoneIconFileMenu",
124    PHONE_MENU_MAX_BYTES
125);
126
127fn validate_root(document: &[u8], expected: &[u8]) -> Result<(), PhoneXmlError> {
128    let mut reader = Reader::from_reader(decoding_reader(document));
129    let mut buffer = Vec::new();
130    loop {
131        match reader.read_event_into(&mut buffer) {
132            Ok(Event::Start(element) | Event::Empty(element)) => {
133                return if element.name().as_ref().as_bytes() == expected {
134                    Ok(())
135                } else {
136                    Err(PhoneXmlError::InvalidField {
137                        field: "phone XML document root",
138                        expected: "the schema root for this document type",
139                    })
140                };
141            }
142            Ok(Event::Eof) => {
143                return Err(PhoneXmlError::InvalidField {
144                    field: "phone XML document root",
145                    expected: "one schema root element",
146                });
147            }
148            Ok(_) => {}
149            Err(error) => return Err(PhoneXmlError::Malformed(error)),
150        }
151        buffer.clear();
152    }
153}