Skip to main content

sccp_protocol/phone/xml/
status.rs

1//! Status phone XML document family.
2
3use super::*;
4
5/// A bitmap-backed status item displayed in the phone's status area.
6#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
7#[serde(rename = "CiscoIPPhoneStatus", deny_unknown_fields)]
8pub struct CiscoIpPhoneStatus {
9    #[serde(rename = "Text", default, skip_serializing_if = "Option::is_none")]
10    pub text: Option<String>,
11    #[serde(rename = "Timer", default, skip_serializing_if = "Option::is_none")]
12    /// Duration in seconds; omission leaves display lifetime device-defined.
13    pub timer_seconds: Option<u16>,
14    #[serde(rename = "LocationX", default, skip_serializing_if = "Option::is_none")]
15    /// Horizontal origin in `-1..=105`; `-1` requests automatic placement.
16    pub location_x: Option<i16>,
17    #[serde(rename = "LocationY", default, skip_serializing_if = "Option::is_none")]
18    /// Vertical origin in `-1..=20`; `-1` requests automatic placement.
19    pub location_y: Option<i16>,
20    #[serde(rename = "Width")]
21    /// Bitmap width in pixels, constrained to `1..=106`.
22    pub width: u16,
23    #[serde(rename = "Height")]
24    /// Bitmap height in pixels, constrained to `1..=21`.
25    pub height: u16,
26    #[serde(rename = "Depth")]
27    /// Bitmap bit depth, constrained to `1..=2`.
28    pub depth: u16,
29    #[serde(rename = "Data", default, skip_serializing_if = "Option::is_none")]
30    pub data: Option<PhoneBitmapData>,
31}
32
33impl CiscoIpPhoneStatus {
34    /// Validates text, display geometry, and the status-specific bitmap bound.
35    pub fn validate(&self) -> Result<(), PhoneXmlError> {
36        validate_optional_text("phone status text", self.text.as_deref(), 0, 32)?;
37        if self
38            .location_x
39            .is_some_and(|value| !(-1..=105).contains(&value))
40        {
41            return Err(PhoneXmlError::InvalidField {
42                field: "phone status horizontal location",
43                expected: "between -1 and 105",
44            });
45        }
46        if self
47            .location_y
48            .is_some_and(|value| !(-1..=20).contains(&value))
49        {
50            return Err(PhoneXmlError::InvalidField {
51                field: "phone status vertical location",
52                expected: "between -1 and 20",
53            });
54        }
55        if !(1..=106).contains(&self.width) {
56            return Err(PhoneXmlError::InvalidField {
57                field: "phone status width",
58                expected: "between 1 and 106",
59            });
60        }
61        if !(1..=21).contains(&self.height) {
62            return Err(PhoneXmlError::InvalidField {
63                field: "phone status height",
64                expected: "between 1 and 21",
65            });
66        }
67        if !(1..=2).contains(&self.depth) {
68            return Err(PhoneXmlError::InvalidField {
69                field: "phone status depth",
70                expected: "between 1 and 2",
71            });
72        }
73        if let Some(data) = &self.data {
74            validate_count(
75                "phone status bitmap bytes",
76                data.as_bytes().len(),
77                PHONE_STATUS_BITMAP_MAX_BYTES,
78            )?;
79        }
80        Ok(())
81    }
82}
83
84/// A URL-backed status item displayed in the phone's status area.
85#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
86#[serde(rename = "CiscoIPPhoneStatusFile", deny_unknown_fields)]
87pub struct CiscoIpPhoneStatusFile {
88    #[serde(rename = "Text", default, skip_serializing_if = "Option::is_none")]
89    pub text: Option<String>,
90    #[serde(rename = "Timer", default, skip_serializing_if = "Option::is_none")]
91    /// Duration in seconds; omission leaves display lifetime device-defined.
92    pub timer_seconds: Option<u16>,
93    #[serde(rename = "LocationX", default, skip_serializing_if = "Option::is_none")]
94    /// Horizontal origin in `-1..=261`; `-1` requests automatic placement.
95    pub location_x: Option<i16>,
96    #[serde(rename = "LocationY", default, skip_serializing_if = "Option::is_none")]
97    /// Vertical origin in `-1..=49`; `-1` requests automatic placement.
98    pub location_y: Option<i16>,
99    #[serde(rename = "URL")]
100    pub url: PhoneImageUrl,
101}
102
103impl CiscoIpPhoneStatusFile {
104    /// Validates text and the optional referenced-image origin.
105    pub fn validate(&self) -> Result<(), PhoneXmlError> {
106        validate_optional_text("phone status text", self.text.as_deref(), 0, 32)?;
107        if self
108            .location_x
109            .is_some_and(|value| !(-1..=261).contains(&value))
110        {
111            return Err(PhoneXmlError::InvalidField {
112                field: "phone status-file horizontal location",
113                expected: "between -1 and 261",
114            });
115        }
116        if self
117            .location_y
118            .is_some_and(|value| !(-1..=49).contains(&value))
119        {
120            return Err(PhoneXmlError::InvalidField {
121                field: "phone status-file vertical location",
122                expected: "between -1 and 49",
123            });
124        }
125        Ok(())
126    }
127}
128
129/// Either accepted status-service document family.
130#[derive(Clone, Debug, Eq, PartialEq)]
131pub enum PhoneStatusDocument {
132    Bitmap(CiscoIpPhoneStatus),
133    File(CiscoIpPhoneStatusFile),
134}
135
136impl PhoneStatusDocument {
137    /// Applies the invariants for the selected status family.
138    pub fn validate(&self) -> Result<(), PhoneXmlError> {
139        match self {
140            Self::Bitmap(document) => document.validate(),
141            Self::File(document) => document.validate(),
142        }
143    }
144
145    /// Detects the root and parses either supported status family.
146    pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
147        #[derive(serde::Deserialize)]
148        enum StatusDocumentEnvelope {
149            #[serde(rename = "CiscoIPPhoneStatus")]
150            Bitmap(CiscoIpPhoneStatus),
151            #[serde(rename = "CiscoIPPhoneStatusFile")]
152            File(CiscoIpPhoneStatusFile),
153        }
154        let document = match from_bytes(document, PHONE_STATUS_MAX_BYTES)? {
155            StatusDocumentEnvelope::Bitmap(document) => Self::Bitmap(document),
156            StatusDocumentEnvelope::File(document) => Self::File(document),
157        };
158        document.validate()?;
159        Ok(document)
160    }
161
162    /// Validates and serializes the selected family using [`PHONE_STATUS_MAX_BYTES`].
163    pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
164        self.to_xml_with_limit(PHONE_STATUS_MAX_BYTES)
165    }
166
167    /// Validates and serializes the selected family within a caller-selected limit.
168    pub fn to_xml_with_limit(&self, maximum_bytes: usize) -> Result<String, PhoneXmlError> {
169        self.validate()?;
170        match self {
171            Self::Bitmap(document) => to_string(document, maximum_bytes),
172            Self::File(document) => to_string(document, maximum_bytes),
173        }
174    }
175}