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        impl $document {
83            /// Parses and validates this document using its schema byte limit.
84            pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
85                <Self as PhoneXmlDocument>::parse_xml(document)
86            }
87
88            /// Parses and validates this document with a stricter byte limit.
89            pub fn from_xml_with_limit(
90                document: &[u8],
91                maximum_bytes: usize,
92            ) -> Result<Self, PhoneXmlError> {
93                <Self as PhoneXmlDocument>::parse_xml_with_limit(document, maximum_bytes)
94            }
95
96            /// Validates and serializes this document using its schema byte limit.
97            pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
98                <Self as PhoneXmlDocument>::serialize_xml(self)
99            }
100
101            /// Validates and serializes this document with a stricter byte limit.
102            pub fn to_xml_with_limit(&self, maximum_bytes: usize) -> Result<String, PhoneXmlError> {
103                <Self as PhoneXmlDocument>::serialize_xml_with_limit(self, maximum_bytes)
104            }
105        }
106    };
107}
108
109impl_phone_xml_document!(CiscoIpPhoneText, b"CiscoIPPhoneText", PHONE_TEXT_MAX_BYTES);
110impl_phone_xml_document!(
111    CiscoIpPhoneInput,
112    b"CiscoIPPhoneInput",
113    PHONE_INPUT_MAX_BYTES
114);
115impl_phone_xml_document!(
116    CiscoIpPhoneExecute,
117    b"CiscoIPPhoneExecute",
118    PHONE_EXECUTE_MAX_BYTES
119);
120impl_phone_xml_document!(
121    CiscoIpPhoneImageList,
122    b"CiscoIPPhoneImageList",
123    PHONE_BACKGROUND_LIST_MAX_BYTES
124);
125impl_phone_xml_document!(
126    CiscoIpPhoneStatus,
127    b"CiscoIPPhoneStatus",
128    PHONE_STATUS_MAX_BYTES
129);
130impl_phone_xml_document!(
131    CiscoIpPhoneStatusFile,
132    b"CiscoIPPhoneStatusFile",
133    PHONE_STATUS_MAX_BYTES
134);
135impl_phone_xml_document!(
136    CiscoIpPhoneDirectory,
137    b"CiscoIPPhoneDirectory",
138    PHONE_DIRECTORY_MAX_BYTES
139);
140impl_phone_xml_document!(CiscoIpPhoneMenu, b"CiscoIPPhoneMenu", PHONE_MENU_MAX_BYTES);
141impl_phone_xml_document!(
142    CiscoIpPhoneIconMenu,
143    b"CiscoIPPhoneIconMenu",
144    PHONE_MENU_MAX_BYTES
145);
146impl_phone_xml_document!(
147    CiscoIpPhoneIconFileMenu,
148    b"CiscoIPPhoneIconFileMenu",
149    PHONE_MENU_MAX_BYTES
150);
151
152fn validate_root(document: &[u8], expected: &[u8]) -> Result<(), PhoneXmlError> {
153    let mut reader = Reader::from_reader(decoding_reader(document));
154    let mut buffer = Vec::new();
155    loop {
156        match reader.read_event_into(&mut buffer) {
157            Ok(Event::Start(element) | Event::Empty(element)) => {
158                return if element.name().as_ref().as_bytes() == expected {
159                    Ok(())
160                } else {
161                    Err(PhoneXmlError::InvalidField {
162                        field: "phone XML document root",
163                        expected: "the schema root for this document type",
164                    })
165                };
166            }
167            Ok(Event::Eof) => {
168                return Err(PhoneXmlError::InvalidField {
169                    field: "phone XML document root",
170                    expected: "one schema root element",
171                });
172            }
173            Ok(_) => {}
174            Err(error) => return Err(PhoneXmlError::Malformed(error)),
175        }
176        buffer.clear();
177    }
178}