1use super::*;
4
5#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct PhoneBitmapData(Vec<u8>);
12
13impl PhoneBitmapData {
14 pub fn new(bytes: impl Into<Vec<u8>>) -> Result<Self, PhoneXmlError> {
16 let bytes = bytes.into();
17 validate_count(
18 "bitmap image data bytes",
19 bytes.len(),
20 PHONE_IMAGE_BITMAP_MAX_BYTES,
21 )?;
22 Ok(Self(bytes))
23 }
24
25 pub fn as_bytes(&self) -> &[u8] {
26 &self.0
27 }
28
29 pub fn into_bytes(self) -> Vec<u8> {
30 self.0
31 }
32}
33
34impl Serialize for PhoneBitmapData {
35 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
36 where
37 S: serde::Serializer,
38 {
39 const HEX: &[u8; 16] = b"0123456789ABCDEF";
40 let mut encoded = String::with_capacity(self.0.len().saturating_mul(2));
41 for byte in &self.0 {
42 encoded.push(char::from(HEX[usize::from(byte >> 4)]));
43 encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
44 }
45 serializer.serialize_str(&encoded)
46 }
47}
48
49impl<'de> serde::Deserialize<'de> for PhoneBitmapData {
50 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51 where
52 D: serde::Deserializer<'de>,
53 {
54 let encoded = <String as serde::Deserialize>::deserialize(deserializer)?;
55 let digits = encoded
56 .bytes()
57 .filter(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
58 .count();
59 let digit_count = encoded.len().saturating_sub(digits);
60 if digit_count % 2 != 0 {
61 return Err(serde::de::Error::custom(
62 "bitmap data must contain complete hexadecimal bytes",
63 ));
64 }
65 let mut decoded = Vec::with_capacity(digit_count / 2);
66 let mut high = None;
67 for byte in encoded
68 .bytes()
69 .filter(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
70 {
71 let value = match byte {
72 b'0'..=b'9' => byte - b'0',
73 b'a'..=b'f' => byte - b'a' + 10,
74 b'A'..=b'F' => byte - b'A' + 10,
75 _ => {
76 return Err(serde::de::Error::custom("bitmap data must be hexadecimal"));
77 }
78 };
79 if let Some(high) = high.take() {
80 decoded.push((high << 4) | value);
81 } else {
82 high = Some(value);
83 }
84 }
85 Self::new(decoded).map_err(serde::de::Error::custom)
86 }
87}
88
89#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
91#[serde(transparent)]
92pub struct PhoneImageUrl(String);
93
94impl PhoneImageUrl {
95 pub fn new(value: impl Into<String>) -> Result<Self, PhoneXmlError> {
97 let value = value.into();
98 validate_optional_text("phone image URL", Some(&value), 1, PHONE_XML_URL_MAX_CHARS)?;
99 Ok(Self(value))
100 }
101
102 pub fn as_str(&self) -> &str {
103 &self.0
104 }
105}
106
107impl<'de> serde::Deserialize<'de> for PhoneImageUrl {
108 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
109 where
110 D: serde::Deserializer<'de>,
111 {
112 let value = <String as serde::Deserialize>::deserialize(deserializer)?;
113 Self::new(value).map_err(serde::de::Error::custom)
114 }
115}
116
117#[derive(Clone, Eq, Hash, PartialEq)]
123pub struct PhoneBackgroundTftpUrl(String);
124
125impl PhoneBackgroundTftpUrl {
126 pub fn new(value: impl Into<String>) -> Result<Self, PhoneXmlError> {
128 let value = value.into();
129 validate_optional_text(
130 "background image TFTP URI",
131 Some(&value),
132 1,
133 PHONE_XML_URL_MAX_CHARS,
134 )?;
135 let parsed = url::Url::parse(&value).map_err(|_| PhoneXmlError::InvalidField {
136 field: "background image TFTP URI",
137 expected: "a TFTP:path URI to a PNG image",
138 })?;
139 let has_tftp_prefix = value
140 .get(..5)
141 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("tftp:"));
142 let path = value.get(5..).unwrap_or_default();
143 let is_png = path
144 .rsplit_once('.')
145 .is_some_and(|(_, extension)| extension.eq_ignore_ascii_case("png"));
146 if !has_tftp_prefix
147 || parsed.scheme() != "tftp"
148 || !parsed.cannot_be_a_base()
149 || parsed.host_str().is_some()
150 || parsed.query().is_some()
151 || parsed.fragment().is_some()
152 || !valid_background_tftp_path(path)
153 || !is_png
154 {
155 return Err(PhoneXmlError::InvalidField {
156 field: "background image TFTP URI",
157 expected: "a TFTP:path URI to a PNG image",
158 });
159 }
160 Ok(Self(value))
161 }
162
163 pub fn as_str(&self) -> &str {
164 &self.0
165 }
166
167 pub fn into_string(self) -> String {
168 self.0
169 }
170}
171
172impl fmt::Debug for PhoneBackgroundTftpUrl {
173 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
174 formatter.write_str("PhoneBackgroundTftpUrl(<redacted>)")
175 }
176}
177
178impl Serialize for PhoneBackgroundTftpUrl {
179 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
180 where
181 S: serde::Serializer,
182 {
183 serializer.serialize_str(&self.0)
184 }
185}
186
187impl<'de> serde::Deserialize<'de> for PhoneBackgroundTftpUrl {
188 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
189 where
190 D: serde::Deserializer<'de>,
191 {
192 let value = String::deserialize(deserializer)?;
193 Self::new(value).map_err(serde::de::Error::custom)
194 }
195}
196
197pub(super) fn valid_background_tftp_path(path: &str) -> bool {
198 !path.is_empty()
199 && !path.starts_with('/')
200 && !path.contains(['?', '#', '\\'])
201 && path.split('/').all(|component| {
202 valid_percent_encoding(component)
203 && percent_encoding::percent_decode_str(component)
204 .decode_utf8()
205 .is_ok_and(|decoded| {
206 !decoded.is_empty()
207 && decoded != "."
208 && decoded != ".."
209 && !decoded.contains(['/', '\\'])
210 && !decoded.chars().any(char::is_control)
211 })
212 })
213}
214
215pub(super) fn valid_percent_encoding(value: &str) -> bool {
216 let bytes = value.as_bytes();
217 let mut index = 0;
218 while index < bytes.len() {
219 if bytes[index] == b'%' {
220 if index + 2 >= bytes.len()
221 || !bytes[index + 1].is_ascii_hexdigit()
222 || !bytes[index + 2].is_ascii_hexdigit()
223 {
224 return false;
225 }
226 index += 3;
227 } else {
228 index += 1;
229 }
230 }
231 true
232}
233
234#[derive(Clone, Eq, Hash, PartialEq)]
236pub struct PhoneBackgroundHttpUrl(String);
237
238impl PhoneBackgroundHttpUrl {
239 pub fn new(value: impl Into<String>) -> Result<Self, PhoneXmlError> {
241 let value = value.into();
242 validate_http_resource_url(
243 "background image HTTP URL",
244 "an absolute HTTP URL without credentials or a fragment",
245 &value,
246 )?;
247 Ok(Self(value))
248 }
249
250 pub fn as_str(&self) -> &str {
251 &self.0
252 }
253
254 pub fn into_string(self) -> String {
255 self.0
256 }
257}
258
259pub(super) fn validate_http_resource_url(
260 field: &'static str,
261 expected: &'static str,
262 value: &str,
263) -> Result<(), PhoneXmlError> {
264 validate_optional_text(field, Some(value), 1, PHONE_XML_URL_MAX_CHARS)?;
265 if value
266 .chars()
267 .any(|character| character.is_ascii_whitespace() || character.is_ascii_control())
268 || value.contains('\\')
269 || !valid_percent_encoding(value)
270 {
271 return Err(PhoneXmlError::InvalidField { field, expected });
272 }
273 let parsed =
274 url::Url::parse(value).map_err(|_| PhoneXmlError::InvalidField { field, expected })?;
275 if parsed.scheme() != "http"
276 || parsed.host_str().is_none()
277 || !parsed.username().is_empty()
278 || parsed.password().is_some()
279 || parsed.fragment().is_some()
280 {
281 return Err(PhoneXmlError::InvalidField { field, expected });
282 }
283 Ok(())
284}
285
286impl fmt::Debug for PhoneBackgroundHttpUrl {
287 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
288 formatter.write_str("PhoneBackgroundHttpUrl(<redacted>)")
289 }
290}
291
292impl Serialize for PhoneBackgroundHttpUrl {
293 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
294 where
295 S: serde::Serializer,
296 {
297 serializer.serialize_str(&self.0)
298 }
299}
300
301impl<'de> serde::Deserialize<'de> for PhoneBackgroundHttpUrl {
302 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
303 where
304 D: serde::Deserializer<'de>,
305 {
306 let value = String::deserialize(deserializer)?;
307 Self::new(value).map_err(serde::de::Error::custom)
308 }
309}
310
311#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
313#[serde(deny_unknown_fields)]
314pub struct CiscoIpPhoneImageListItem {
315 #[serde(rename = "@Image")]
316 pub thumbnail_url: PhoneBackgroundTftpUrl,
317 #[serde(rename = "@URL")]
318 pub image_url: PhoneBackgroundTftpUrl,
319}
320
321#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
323#[serde(rename = "CiscoIPPhoneImageList", deny_unknown_fields)]
324pub struct CiscoIpPhoneImageList {
325 #[serde(rename = "ImageItem", default)]
326 pub items: Vec<CiscoIpPhoneImageListItem>,
327}
328
329impl CiscoIpPhoneImageList {
330 pub fn new(items: Vec<CiscoIpPhoneImageListItem>) -> Result<Self, PhoneXmlError> {
332 let document = Self { items };
333 document.validate()?;
334 Ok(document)
335 }
336
337 pub fn validate(&self) -> Result<(), PhoneXmlError> {
339 validate_count(
340 "background image choices",
341 self.items.len(),
342 PHONE_BACKGROUND_LIST_MAX_ITEMS,
343 )
344 }
345}
346
347#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
349#[serde(deny_unknown_fields)]
350pub struct CiscoIpPhoneBackground {
351 #[serde(rename = "image")]
352 pub image_url: PhoneBackgroundHttpUrl,
353 #[serde(rename = "icon")]
354 pub thumbnail_url: PhoneBackgroundHttpUrl,
355}
356
357#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
359#[serde(rename = "setBackground", deny_unknown_fields)]
360pub struct CiscoIpPhoneSetBackground {
361 #[serde(rename = "background")]
362 pub background: CiscoIpPhoneBackground,
363}
364
365impl CiscoIpPhoneSetBackground {
366 pub fn new(image_url: PhoneBackgroundHttpUrl, thumbnail_url: PhoneBackgroundHttpUrl) -> Self {
368 Self {
369 background: CiscoIpPhoneBackground {
370 image_url,
371 thumbnail_url,
372 },
373 }
374 }
375
376 pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
378 #[derive(serde::Deserialize)]
379 enum SetBackgroundEnvelope {
380 #[serde(rename = "setBackground")]
381 SetBackground(CiscoIpPhoneSetBackground),
382 }
383
384 let SetBackgroundEnvelope::SetBackground(document) =
385 from_bytes(document, PHONE_BACKGROUND_CONTROL_MAX_BYTES)?;
386 Ok(document)
387 }
388
389 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
391 to_string(self, PHONE_BACKGROUND_CONTROL_MAX_BYTES)
392 }
393}
394
395#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
397#[serde(rename = "setBackgroundPreview", deny_unknown_fields)]
398pub struct CiscoIpPhoneSetBackgroundPreview {
399 #[serde(rename = "image")]
400 pub image_url: PhoneBackgroundHttpUrl,
401}
402
403impl CiscoIpPhoneSetBackgroundPreview {
404 pub const fn new(image_url: PhoneBackgroundHttpUrl) -> Self {
406 Self { image_url }
407 }
408
409 pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
411 #[derive(serde::Deserialize)]
412 enum PreviewEnvelope {
413 #[serde(rename = "setBackgroundPreview")]
414 Preview(CiscoIpPhoneSetBackgroundPreview),
415 }
416
417 let PreviewEnvelope::Preview(document) =
418 from_bytes(document, PHONE_BACKGROUND_CONTROL_MAX_BYTES)?;
419 Ok(document)
420 }
421
422 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
424 to_string(self, PHONE_BACKGROUND_CONTROL_MAX_BYTES)
425 }
426}
427
428#[derive(Clone, Debug, Eq, PartialEq)]
430pub enum PhoneBackgroundControlDocument {
431 Set(CiscoIpPhoneSetBackground),
432 Preview(CiscoIpPhoneSetBackgroundPreview),
433}
434
435impl PhoneBackgroundControlDocument {
436 pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
438 #[derive(serde::Deserialize)]
439 enum BackgroundEnvelope {
440 #[serde(rename = "setBackground")]
441 Set(CiscoIpPhoneSetBackground),
442 #[serde(rename = "setBackgroundPreview")]
443 Preview(CiscoIpPhoneSetBackgroundPreview),
444 }
445
446 Ok(
447 match from_bytes(document, PHONE_BACKGROUND_CONTROL_MAX_BYTES)? {
448 BackgroundEnvelope::Set(document) => Self::Set(document),
449 BackgroundEnvelope::Preview(document) => Self::Preview(document),
450 },
451 )
452 }
453
454 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
456 self.to_xml_with_limit(PHONE_BACKGROUND_CONTROL_MAX_BYTES)
457 }
458
459 pub fn to_xml_with_limit(&self, maximum_bytes: usize) -> Result<String, PhoneXmlError> {
461 match self {
462 Self::Set(document) => to_string(document, maximum_bytes),
463 Self::Preview(document) => to_string(document, maximum_bytes),
464 }
465 }
466}
467
468#[derive(Clone, Eq, Hash, PartialEq)]
470pub struct PhoneRingtoneUrl(String);
471
472impl PhoneRingtoneUrl {
473 pub fn new(value: impl Into<String>) -> Result<Self, PhoneXmlError> {
475 let value = value.into();
476 if !value.starts_with("http://") {
477 return Err(PhoneXmlError::InvalidField {
478 field: "ringtone HTTP URL",
479 expected: "an absolute lowercase HTTP URL without credentials or a fragment",
480 });
481 }
482 validate_http_resource_url(
483 "ringtone HTTP URL",
484 "an absolute lowercase HTTP URL without credentials or a fragment",
485 &value,
486 )?;
487 Ok(Self(value))
488 }
489
490 pub fn as_str(&self) -> &str {
491 &self.0
492 }
493
494 pub fn into_string(self) -> String {
495 self.0
496 }
497}
498
499impl fmt::Debug for PhoneRingtoneUrl {
500 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
501 formatter.write_str("PhoneRingtoneUrl(<redacted>)")
502 }
503}
504
505impl Serialize for PhoneRingtoneUrl {
506 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
507 where
508 S: serde::Serializer,
509 {
510 serializer.serialize_str(&self.0)
511 }
512}
513
514impl<'de> serde::Deserialize<'de> for PhoneRingtoneUrl {
515 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
516 where
517 D: serde::Deserializer<'de>,
518 {
519 let value = String::deserialize(deserializer)?;
520 Self::new(value).map_err(serde::de::Error::custom)
521 }
522}
523
524#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
526#[serde(rename = "setRingTone", deny_unknown_fields)]
527pub struct CiscoIpPhoneSetRingTone {
528 #[serde(rename = "ringTone")]
529 pub ringtone_url: PhoneRingtoneUrl,
530}
531
532impl CiscoIpPhoneSetRingTone {
533 pub const fn new(ringtone_url: PhoneRingtoneUrl) -> Self {
535 Self { ringtone_url }
536 }
537
538 pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
540 #[derive(serde::Deserialize)]
541 enum RingToneEnvelope {
542 #[serde(rename = "setRingTone")]
543 RingTone(CiscoIpPhoneSetRingTone),
544 }
545
546 let RingToneEnvelope::RingTone(document) = from_bytes(document, PHONE_RINGTONE_MAX_BYTES)?;
547 Ok(document)
548 }
549
550 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
552 self.to_xml_with_limit(PHONE_RINGTONE_MAX_BYTES)
553 }
554
555 pub fn to_xml_with_limit(&self, maximum_bytes: usize) -> Result<String, PhoneXmlError> {
557 to_string(self, maximum_bytes)
558 }
559}
560
561#[derive(Clone, Copy, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
563#[serde(deny_unknown_fields)]
564pub struct PhoneTouchArea {
565 #[serde(rename = "@X1")]
566 pub x1: u16,
567 #[serde(rename = "@Y1")]
568 pub y1: u16,
569 #[serde(rename = "@X2")]
570 pub x2: u16,
571 #[serde(rename = "@Y2")]
572 pub y2: u16,
573}
574
575#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
577#[serde(deny_unknown_fields)]
578pub struct CiscoIpPhoneTouchAreaMenuItem {
579 #[serde(rename = "Name", default, skip_serializing_if = "Option::is_none")]
580 pub name: Option<String>,
581 #[serde(rename = "URL", default, skip_serializing_if = "Option::is_none")]
582 pub url: Option<String>,
583 #[serde(rename = "TouchArea", default, skip_serializing_if = "Option::is_none")]
584 pub touch_area: Option<PhoneTouchArea>,
585}
586
587#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
589#[serde(rename = "CiscoIPPhoneImage", deny_unknown_fields)]
590pub struct CiscoIpPhoneImage {
591 #[serde(
592 rename = "@keypadTarget",
593 default,
594 skip_serializing_if = "Option::is_none"
595 )]
596 pub keypad_target: Option<PhoneKeypadTarget>,
597 #[serde(rename = "@appId", default, skip_serializing_if = "Option::is_none")]
598 pub application_id: Option<String>,
599 #[serde(
600 rename = "@onAppFocusLost",
601 default,
602 skip_serializing_if = "Option::is_none"
603 )]
604 pub on_focus_lost: Option<String>,
605 #[serde(
606 rename = "@onAppFocusGained",
607 default,
608 skip_serializing_if = "Option::is_none"
609 )]
610 pub on_focus_gained: Option<String>,
611 #[serde(
612 rename = "@onAppMinimized",
613 default,
614 skip_serializing_if = "Option::is_none"
615 )]
616 pub on_minimized: Option<String>,
617 #[serde(
618 rename = "@onAppClosed",
619 default,
620 skip_serializing_if = "Option::is_none"
621 )]
622 pub on_closed: Option<String>,
623 #[serde(rename = "Title", default, skip_serializing_if = "Option::is_none")]
624 pub title: Option<String>,
625 #[serde(rename = "Prompt", default, skip_serializing_if = "Option::is_none")]
626 pub prompt: Option<String>,
627 #[serde(rename = "SoftKeyItem", default)]
628 pub soft_keys: Vec<CiscoIpPhoneSoftKeyItem>,
629 #[serde(rename = "KeyItem", default)]
630 pub key_items: Vec<CiscoIpPhoneKeyItem>,
631 #[serde(rename = "LocationX", default, skip_serializing_if = "Option::is_none")]
632 pub location_x: Option<i16>,
634 #[serde(rename = "LocationY", default, skip_serializing_if = "Option::is_none")]
635 pub location_y: Option<i16>,
637 #[serde(rename = "Width")]
638 pub width: u16,
640 #[serde(rename = "Height")]
641 pub height: u16,
643 #[serde(rename = "Depth")]
644 pub depth: u16,
646 #[serde(rename = "Data", default, skip_serializing_if = "Option::is_none")]
647 pub data: Option<PhoneBitmapData>,
648}
649
650impl CiscoIpPhoneImage {
651 pub fn validate(&self) -> Result<(), PhoneXmlError> {
653 validate_image_display(
654 self.title.as_deref(),
655 self.prompt.as_deref(),
656 self.application_id.as_deref(),
657 [
658 self.on_focus_lost.as_deref(),
659 self.on_focus_gained.as_deref(),
660 self.on_minimized.as_deref(),
661 self.on_closed.as_deref(),
662 ],
663 &self.soft_keys,
664 &self.key_items,
665 )?;
666 validate_bitmap_image(
667 self.location_x,
668 self.location_y,
669 self.width,
670 self.height,
671 self.depth,
672 self.data.as_ref(),
673 )
674 }
675
676 pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
678 #[derive(serde::Deserialize)]
679 enum ImageEnvelope {
680 #[serde(rename = "CiscoIPPhoneImage")]
681 Image(CiscoIpPhoneImage),
682 }
683 let ImageEnvelope::Image(document) = from_bytes(document, PHONE_IMAGE_MAX_BYTES)?;
684 document.validate()?;
685 Ok(document)
686 }
687
688 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
690 self.validate()?;
691 to_string(self, PHONE_IMAGE_MAX_BYTES)
692 }
693}
694
695#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
697#[serde(rename = "CiscoIPPhoneImageFile", deny_unknown_fields)]
698pub struct CiscoIpPhoneImageFile {
699 #[serde(
700 rename = "@keypadTarget",
701 default,
702 skip_serializing_if = "Option::is_none"
703 )]
704 pub keypad_target: Option<PhoneKeypadTarget>,
705 #[serde(rename = "@appId", default, skip_serializing_if = "Option::is_none")]
706 pub application_id: Option<String>,
707 #[serde(
708 rename = "@onAppFocusLost",
709 default,
710 skip_serializing_if = "Option::is_none"
711 )]
712 pub on_focus_lost: Option<String>,
713 #[serde(
714 rename = "@onAppFocusGained",
715 default,
716 skip_serializing_if = "Option::is_none"
717 )]
718 pub on_focus_gained: Option<String>,
719 #[serde(
720 rename = "@onAppMinimized",
721 default,
722 skip_serializing_if = "Option::is_none"
723 )]
724 pub on_minimized: Option<String>,
725 #[serde(
726 rename = "@onAppClosed",
727 default,
728 skip_serializing_if = "Option::is_none"
729 )]
730 pub on_closed: Option<String>,
731 #[serde(rename = "Title", default, skip_serializing_if = "Option::is_none")]
732 pub title: Option<String>,
733 #[serde(rename = "Prompt", default, skip_serializing_if = "Option::is_none")]
734 pub prompt: Option<String>,
735 #[serde(rename = "SoftKeyItem", default)]
736 pub soft_keys: Vec<CiscoIpPhoneSoftKeyItem>,
737 #[serde(rename = "KeyItem", default)]
738 pub key_items: Vec<CiscoIpPhoneKeyItem>,
739 #[serde(rename = "LocationX", default, skip_serializing_if = "Option::is_none")]
740 pub location_x: Option<i16>,
742 #[serde(rename = "LocationY", default, skip_serializing_if = "Option::is_none")]
743 pub location_y: Option<i16>,
745 #[serde(rename = "URL")]
746 pub url: PhoneImageUrl,
747}
748
749impl CiscoIpPhoneImageFile {
750 pub fn validate(&self) -> Result<(), PhoneXmlError> {
752 validate_image_display(
753 self.title.as_deref(),
754 self.prompt.as_deref(),
755 self.application_id.as_deref(),
756 [
757 self.on_focus_lost.as_deref(),
758 self.on_focus_gained.as_deref(),
759 self.on_minimized.as_deref(),
760 self.on_closed.as_deref(),
761 ],
762 &self.soft_keys,
763 &self.key_items,
764 )?;
765 validate_file_image_location(self.location_x, self.location_y)
766 }
767
768 pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
770 #[derive(serde::Deserialize)]
771 enum ImageFileEnvelope {
772 #[serde(rename = "CiscoIPPhoneImageFile")]
773 ImageFile(CiscoIpPhoneImageFile),
774 }
775 let ImageFileEnvelope::ImageFile(document) = from_bytes(document, PHONE_IMAGE_MAX_BYTES)?;
776 document.validate()?;
777 Ok(document)
778 }
779
780 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
782 self.validate()?;
783 to_string(self, PHONE_IMAGE_MAX_BYTES)
784 }
785}
786
787#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
789#[serde(rename = "CiscoIPPhoneGraphicMenu", deny_unknown_fields)]
790pub struct CiscoIpPhoneGraphicMenu {
791 #[serde(
792 rename = "@keypadTarget",
793 default,
794 skip_serializing_if = "Option::is_none"
795 )]
796 pub keypad_target: Option<PhoneKeypadTarget>,
797 #[serde(rename = "@appId", default, skip_serializing_if = "Option::is_none")]
798 pub application_id: Option<String>,
799 #[serde(
800 rename = "@onAppFocusLost",
801 default,
802 skip_serializing_if = "Option::is_none"
803 )]
804 pub on_focus_lost: Option<String>,
805 #[serde(
806 rename = "@onAppFocusGained",
807 default,
808 skip_serializing_if = "Option::is_none"
809 )]
810 pub on_focus_gained: Option<String>,
811 #[serde(
812 rename = "@onAppMinimized",
813 default,
814 skip_serializing_if = "Option::is_none"
815 )]
816 pub on_minimized: Option<String>,
817 #[serde(
818 rename = "@onAppClosed",
819 default,
820 skip_serializing_if = "Option::is_none"
821 )]
822 pub on_closed: Option<String>,
823 #[serde(rename = "Title", default, skip_serializing_if = "Option::is_none")]
824 pub title: Option<String>,
825 #[serde(rename = "Prompt", default, skip_serializing_if = "Option::is_none")]
826 pub prompt: Option<String>,
827 #[serde(rename = "SoftKeyItem", default)]
828 pub soft_keys: Vec<CiscoIpPhoneSoftKeyItem>,
829 #[serde(rename = "KeyItem", default)]
830 pub key_items: Vec<CiscoIpPhoneKeyItem>,
831 #[serde(rename = "LocationX", default, skip_serializing_if = "Option::is_none")]
832 pub location_x: Option<i16>,
834 #[serde(rename = "LocationY", default, skip_serializing_if = "Option::is_none")]
835 pub location_y: Option<i16>,
837 #[serde(rename = "Width")]
838 pub width: u16,
840 #[serde(rename = "Height")]
841 pub height: u16,
843 #[serde(rename = "Depth")]
844 pub depth: u16,
846 #[serde(rename = "Data", default, skip_serializing_if = "Option::is_none")]
847 pub data: Option<PhoneBitmapData>,
848 #[serde(rename = "MenuItem", default)]
849 pub items: Vec<CiscoIpPhoneMenuItem>,
850}
851
852impl CiscoIpPhoneGraphicMenu {
853 pub fn validate(&self) -> Result<(), PhoneXmlError> {
855 validate_image_display(
856 self.title.as_deref(),
857 self.prompt.as_deref(),
858 self.application_id.as_deref(),
859 [
860 self.on_focus_lost.as_deref(),
861 self.on_focus_gained.as_deref(),
862 self.on_minimized.as_deref(),
863 self.on_closed.as_deref(),
864 ],
865 &self.soft_keys,
866 &self.key_items,
867 )?;
868 validate_bitmap_image(
869 self.location_x,
870 self.location_y,
871 self.width,
872 self.height,
873 self.depth,
874 self.data.as_ref(),
875 )?;
876 validate_count(
877 "graphic menu items",
878 self.items.len(),
879 PHONE_GRAPHIC_MENU_MAX_ITEMS,
880 )?;
881 for item in &self.items {
882 validate_optional_text("graphic menu item name", item.name.as_deref(), 0, 64)?;
883 validate_optional_text(
884 "graphic menu item URL",
885 item.url.as_deref(),
886 0,
887 PHONE_XML_URL_MAX_CHARS,
888 )?;
889 }
890 Ok(())
891 }
892
893 pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
895 #[derive(serde::Deserialize)]
896 enum GraphicMenuEnvelope {
897 #[serde(rename = "CiscoIPPhoneGraphicMenu")]
898 GraphicMenu(CiscoIpPhoneGraphicMenu),
899 }
900 let GraphicMenuEnvelope::GraphicMenu(document) =
901 from_bytes(document, PHONE_IMAGE_MAX_BYTES)?;
902 document.validate()?;
903 Ok(document)
904 }
905
906 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
908 self.validate()?;
909 to_string(self, PHONE_IMAGE_MAX_BYTES)
910 }
911}
912
913#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
915#[serde(rename = "CiscoIPPhoneGraphicFileMenu", deny_unknown_fields)]
916pub struct CiscoIpPhoneGraphicFileMenu {
917 #[serde(
918 rename = "@keypadTarget",
919 default,
920 skip_serializing_if = "Option::is_none"
921 )]
922 pub keypad_target: Option<PhoneKeypadTarget>,
923 #[serde(rename = "@appId", default, skip_serializing_if = "Option::is_none")]
924 pub application_id: Option<String>,
925 #[serde(
926 rename = "@onAppFocusLost",
927 default,
928 skip_serializing_if = "Option::is_none"
929 )]
930 pub on_focus_lost: Option<String>,
931 #[serde(
932 rename = "@onAppFocusGained",
933 default,
934 skip_serializing_if = "Option::is_none"
935 )]
936 pub on_focus_gained: Option<String>,
937 #[serde(
938 rename = "@onAppMinimized",
939 default,
940 skip_serializing_if = "Option::is_none"
941 )]
942 pub on_minimized: Option<String>,
943 #[serde(
944 rename = "@onAppClosed",
945 default,
946 skip_serializing_if = "Option::is_none"
947 )]
948 pub on_closed: Option<String>,
949 #[serde(rename = "Title", default, skip_serializing_if = "Option::is_none")]
950 pub title: Option<String>,
951 #[serde(rename = "Prompt", default, skip_serializing_if = "Option::is_none")]
952 pub prompt: Option<String>,
953 #[serde(rename = "SoftKeyItem", default)]
954 pub soft_keys: Vec<CiscoIpPhoneSoftKeyItem>,
955 #[serde(rename = "KeyItem", default)]
956 pub key_items: Vec<CiscoIpPhoneKeyItem>,
957 #[serde(rename = "LocationX", default, skip_serializing_if = "Option::is_none")]
958 pub location_x: Option<i16>,
960 #[serde(rename = "LocationY", default, skip_serializing_if = "Option::is_none")]
961 pub location_y: Option<i16>,
963 #[serde(rename = "URL")]
964 pub url: PhoneImageUrl,
965 #[serde(rename = "MenuItem", default)]
966 pub items: Vec<CiscoIpPhoneTouchAreaMenuItem>,
967}
968
969impl CiscoIpPhoneGraphicFileMenu {
970 pub fn validate(&self) -> Result<(), PhoneXmlError> {
972 validate_image_display(
973 self.title.as_deref(),
974 self.prompt.as_deref(),
975 self.application_id.as_deref(),
976 [
977 self.on_focus_lost.as_deref(),
978 self.on_focus_gained.as_deref(),
979 self.on_minimized.as_deref(),
980 self.on_closed.as_deref(),
981 ],
982 &self.soft_keys,
983 &self.key_items,
984 )?;
985 validate_file_image_location(self.location_x, self.location_y)?;
986 validate_count(
987 "graphic-file menu items",
988 self.items.len(),
989 PHONE_GRAPHIC_FILE_MENU_MAX_ITEMS,
990 )?;
991 for item in &self.items {
992 validate_optional_text("graphic-file menu item name", item.name.as_deref(), 0, 32)?;
993 validate_optional_text(
994 "graphic-file menu item URL",
995 item.url.as_deref(),
996 0,
997 PHONE_XML_URL_MAX_CHARS,
998 )?;
999 }
1000 Ok(())
1001 }
1002
1003 pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
1005 #[derive(serde::Deserialize)]
1006 enum GraphicFileMenuEnvelope {
1007 #[serde(rename = "CiscoIPPhoneGraphicFileMenu")]
1008 GraphicFileMenu(CiscoIpPhoneGraphicFileMenu),
1009 }
1010 let GraphicFileMenuEnvelope::GraphicFileMenu(document) =
1011 from_bytes(document, PHONE_IMAGE_MAX_BYTES)?;
1012 document.validate()?;
1013 Ok(document)
1014 }
1015
1016 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
1018 self.validate()?;
1019 to_string(self, PHONE_IMAGE_MAX_BYTES)
1020 }
1021}
1022
1023#[derive(Clone, Debug, Eq, PartialEq)]
1025pub enum PhoneImageDocument {
1026 Image(CiscoIpPhoneImage),
1027 ImageFile(CiscoIpPhoneImageFile),
1028 GraphicMenu(CiscoIpPhoneGraphicMenu),
1029 GraphicFileMenu(CiscoIpPhoneGraphicFileMenu),
1030}
1031
1032impl PhoneImageDocument {
1033 pub fn validate(&self) -> Result<(), PhoneXmlError> {
1035 match self {
1036 Self::Image(document) => document.validate(),
1037 Self::ImageFile(document) => document.validate(),
1038 Self::GraphicMenu(document) => document.validate(),
1039 Self::GraphicFileMenu(document) => document.validate(),
1040 }
1041 }
1042
1043 pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
1045 #[derive(serde::Deserialize)]
1046 enum ImageDocumentEnvelope {
1047 #[serde(rename = "CiscoIPPhoneImage")]
1048 Image(CiscoIpPhoneImage),
1049 #[serde(rename = "CiscoIPPhoneImageFile")]
1050 ImageFile(CiscoIpPhoneImageFile),
1051 #[serde(rename = "CiscoIPPhoneGraphicMenu")]
1052 GraphicMenu(CiscoIpPhoneGraphicMenu),
1053 #[serde(rename = "CiscoIPPhoneGraphicFileMenu")]
1054 GraphicFileMenu(CiscoIpPhoneGraphicFileMenu),
1055 }
1056 let document = match from_bytes(document, PHONE_IMAGE_MAX_BYTES)? {
1057 ImageDocumentEnvelope::Image(document) => Self::Image(document),
1058 ImageDocumentEnvelope::ImageFile(document) => Self::ImageFile(document),
1059 ImageDocumentEnvelope::GraphicMenu(document) => Self::GraphicMenu(document),
1060 ImageDocumentEnvelope::GraphicFileMenu(document) => Self::GraphicFileMenu(document),
1061 };
1062 document.validate()?;
1063 Ok(document)
1064 }
1065
1066 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
1068 self.to_xml_with_limit(PHONE_IMAGE_MAX_BYTES)
1069 }
1070
1071 pub fn to_xml_with_limit(&self, maximum_bytes: usize) -> Result<String, PhoneXmlError> {
1073 self.validate()?;
1074 match self {
1075 Self::Image(document) => to_string(document, maximum_bytes),
1076 Self::ImageFile(document) => to_string(document, maximum_bytes),
1077 Self::GraphicMenu(document) => to_string(document, maximum_bytes),
1078 Self::GraphicFileMenu(document) => to_string(document, maximum_bytes),
1079 }
1080 }
1081}