1use super::*;
4
5pub(super) const LAST_OUT_OF_SERVICE_ALARM: &str = "LastOutOfServiceInformation";
6
7#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
9#[serde(deny_unknown_fields)]
10pub struct CiscoIpPhoneAlarmString {
11 #[serde(rename = "@name")]
12 pub name: String,
13 #[serde(rename = "$text", default)]
14 pub value: String,
16}
17
18impl fmt::Debug for CiscoIpPhoneAlarmString {
19 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20 formatter
21 .debug_struct("CiscoIpPhoneAlarmString")
22 .field("name", &self.name)
23 .field("value", &"<redacted>")
24 .finish()
25 }
26}
27
28#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
30#[serde(deny_unknown_fields)]
31pub struct CiscoIpPhoneAlarmEnum {
32 #[serde(rename = "@name")]
33 pub name: String,
34 #[serde(rename = "$text")]
35 pub value: i32,
36}
37
38impl fmt::Debug for CiscoIpPhoneAlarmEnum {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 formatter
41 .debug_struct("CiscoIpPhoneAlarmEnum")
42 .field("name", &self.name)
43 .field("value", &self.value)
44 .finish()
45 }
46}
47
48#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
50pub enum CiscoIpPhoneAlarmParameter {
51 #[serde(rename = "String")]
53 String(CiscoIpPhoneAlarmString),
54 #[serde(rename = "Enum")]
56 Enum(CiscoIpPhoneAlarmEnum),
57}
58
59impl fmt::Debug for CiscoIpPhoneAlarmParameter {
60 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 Self::String(value) => value.fmt(formatter),
63 Self::Enum(value) => value.fmt(formatter),
64 }
65 }
66}
67
68#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
70#[serde(deny_unknown_fields)]
71pub struct CiscoIpPhoneAlarmParameterList {
72 #[serde(rename = "$value", default)]
73 pub parameters: Vec<CiscoIpPhoneAlarmParameter>,
74}
75
76impl fmt::Debug for CiscoIpPhoneAlarmParameterList {
77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 formatter
79 .debug_struct("CiscoIpPhoneAlarmParameterList")
80 .field("parameter_count", &self.parameters.len())
81 .finish()
82 }
83}
84
85#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
87#[serde(deny_unknown_fields)]
88pub struct CiscoIpPhoneAlarmEntry {
89 #[serde(rename = "@Name")]
90 pub name: String,
91 #[serde(rename = "ParameterList")]
92 pub parameter_list: CiscoIpPhoneAlarmParameterList,
93}
94
95impl fmt::Debug for CiscoIpPhoneAlarmEntry {
96 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
97 formatter
98 .debug_struct("CiscoIpPhoneAlarmEntry")
99 .field("name", &self.name)
100 .field("parameter_count", &self.parameter_list.parameters.len())
101 .finish()
102 }
103}
104
105#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
107#[serde(rename = "x-cisco-alarm", deny_unknown_fields)]
108pub struct CiscoIpPhoneAlarm {
109 #[serde(rename = "Alarm")]
110 pub alarm: CiscoIpPhoneAlarmEntry,
111}
112
113impl fmt::Debug for CiscoIpPhoneAlarm {
114 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115 formatter
116 .debug_struct("CiscoIpPhoneAlarm")
117 .field("name", &self.alarm.name)
118 .field(
119 "parameter_count",
120 &self.alarm.parameter_list.parameters.len(),
121 )
122 .finish()
123 }
124}
125
126impl CiscoIpPhoneAlarm {
127 pub fn validate(&self) -> Result<(), PhoneXmlError> {
129 if self.alarm.name != LAST_OUT_OF_SERVICE_ALARM {
130 return Err(PhoneXmlError::InvalidField {
131 field: "phone alarm name",
132 expected: "LastOutOfServiceInformation",
133 });
134 }
135 let mut names = HashSet::new();
136 for parameter in &self.alarm.parameter_list.parameters {
137 let name = match parameter {
138 CiscoIpPhoneAlarmParameter::String(value) => {
139 validate_optional_text(
140 "phone alarm string name",
141 Some(&value.name),
142 1,
143 PHONE_ALARM_MAX_BYTES,
144 )?;
145 validate_optional_text(
146 "phone alarm string value",
147 Some(&value.value),
148 0,
149 PHONE_ALARM_MAX_BYTES,
150 )?;
151 &value.name
152 }
153 CiscoIpPhoneAlarmParameter::Enum(value) => {
154 validate_optional_text(
155 "phone alarm enumeration name",
156 Some(&value.name),
157 1,
158 PHONE_ALARM_MAX_BYTES,
159 )?;
160 &value.name
161 }
162 };
163 if !names.insert(name.as_str()) {
164 return Err(PhoneXmlError::InvalidField {
165 field: "phone alarm parameter names",
166 expected: "unique across string and enumeration parameters",
167 });
168 }
169 }
170 Ok(())
171 }
172
173 pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
175 #[derive(serde::Deserialize)]
176 enum AlarmEnvelope {
177 #[serde(rename = "x-cisco-alarm")]
178 Alarm(CiscoIpPhoneAlarm),
179 }
180 let AlarmEnvelope::Alarm(document) =
181 from_bytes(document, PHONE_ALARM_MAX_BYTES).map_err(redact_alarm_schema_error)?;
182 document.validate()?;
183 Ok(document)
184 }
185
186 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
188 self.validate()?;
189 to_string(self, PHONE_ALARM_MAX_BYTES)
190 }
191
192 pub fn string(&self, name: &str) -> Option<&str> {
194 self.alarm
195 .parameter_list
196 .parameters
197 .iter()
198 .find_map(|parameter| match parameter {
199 CiscoIpPhoneAlarmParameter::String(value) if value.name == name => {
200 Some(value.value.as_str())
201 }
202 _ => None,
203 })
204 }
205
206 pub fn enumeration(&self, name: &str) -> Option<i32> {
208 self.alarm
209 .parameter_list
210 .parameters
211 .iter()
212 .find_map(|parameter| match parameter {
213 CiscoIpPhoneAlarmParameter::Enum(value) if value.name == name => Some(value.value),
214 _ => None,
215 })
216 }
217
218 pub fn reason_for_out_of_service(&self) -> Option<i32> {
220 self.enumeration("ReasonForOutOfService")
221 }
222}
223
224#[derive(Clone, Eq, PartialEq)]
226pub struct OpaquePhoneAlarm(Vec<u8>);
227
228impl OpaquePhoneAlarm {
229 pub fn as_bytes(&self) -> &[u8] {
230 &self.0
231 }
232
233 pub fn into_bytes(self) -> Vec<u8> {
234 self.0
235 }
236}
237
238impl fmt::Debug for OpaquePhoneAlarm {
239 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
240 formatter
241 .debug_struct("OpaquePhoneAlarm")
242 .field("byte_count", &self.0.len())
243 .finish_non_exhaustive()
244 }
245}
246
247#[derive(Clone, Eq, PartialEq)]
249pub enum PhoneAlarmTelemetry {
250 LastOutOfService(CiscoIpPhoneAlarm),
252 Opaque(OpaquePhoneAlarm),
254}
255
256#[derive(Clone, Copy, Debug, Eq, PartialEq)]
258pub enum PhoneAlarmKind {
259 LastOutOfService,
260}
261
262#[derive(Clone, Copy, Debug, Eq, PartialEq)]
264pub struct PhoneAlarmSummary {
265 pub kind: PhoneAlarmKind,
266 pub reason_for_out_of_service: Option<i32>,
268}
269
270impl fmt::Debug for PhoneAlarmTelemetry {
271 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
272 match self {
273 Self::LastOutOfService(alarm) => alarm.fmt(formatter),
274 Self::Opaque(alarm) => alarm.fmt(formatter),
275 }
276 }
277}
278
279impl PhoneAlarmTelemetry {
280 pub fn summary(&self) -> Option<PhoneAlarmSummary> {
283 match self {
284 Self::LastOutOfService(alarm) => Some(PhoneAlarmSummary {
285 kind: PhoneAlarmKind::LastOutOfService,
286 reason_for_out_of_service: alarm.reason_for_out_of_service(),
287 }),
288 Self::Opaque(_) => None,
289 }
290 }
291
292 pub fn is_opaque(&self) -> bool {
293 matches!(self, Self::Opaque(_))
294 }
295}
296
297pub fn parse_phone_alarm(document: &[u8]) -> Result<PhoneAlarmTelemetry, PhoneXmlError> {
300 #[derive(Debug, serde::Deserialize)]
301 struct AlarmProbe {
302 #[serde(rename = "Alarm", default)]
303 alarms: Vec<AlarmNameProbe>,
304 }
305
306 #[derive(Debug, serde::Deserialize)]
307 struct AlarmNameProbe {
308 #[serde(rename = "@Name")]
309 name: String,
310 }
311
312 #[derive(serde::Deserialize)]
313 enum AlarmProbeEnvelope {
314 #[serde(rename = "x-cisco-alarm")]
315 Alarm(AlarmProbe),
316 #[serde(other)]
317 Unknown,
318 }
319
320 let supported = match from_bytes(document, PHONE_ALARM_MAX_BYTES)
321 .map_err(redact_alarm_schema_error)?
322 {
323 AlarmProbeEnvelope::Alarm(probe) => {
324 matches!(probe.alarms.as_slice(), [alarm] if alarm.name == LAST_OUT_OF_SERVICE_ALARM)
325 }
326 AlarmProbeEnvelope::Unknown => false,
327 };
328 if supported {
329 CiscoIpPhoneAlarm::from_xml(document).map(PhoneAlarmTelemetry::LastOutOfService)
330 } else {
331 Ok(PhoneAlarmTelemetry::Opaque(OpaquePhoneAlarm(
332 document.to_vec(),
333 )))
334 }
335}
336
337pub(super) fn redact_alarm_schema_error(error: PhoneXmlError) -> PhoneXmlError {
338 match error {
339 PhoneXmlError::Deserialize(_) => PhoneXmlError::InvalidAlarmSchema,
340 error => error,
341 }
342}
343
344#[derive(Clone, Copy, Eq, Hash, PartialEq)]
346pub struct PhoneBssid([u8; 6]);
347
348impl PhoneBssid {
349 pub const fn from_octets(octets: [u8; 6]) -> Self {
351 Self(octets)
352 }
353
354 pub const fn octets(self) -> [u8; 6] {
355 self.0
356 }
357
358 pub fn parse(value: &str) -> Result<Self, PhoneXmlError> {
360 parse_bssid(value).ok_or(PhoneXmlError::InvalidField {
361 field: "phone location BSSID",
362 expected: "six hexadecimal octets separated by colons",
363 })
364 }
365}
366
367impl fmt::Display for PhoneBssid {
368 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
369 write!(
370 formatter,
371 "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
372 self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
373 )
374 }
375}
376
377impl fmt::Debug for PhoneBssid {
378 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
379 formatter.write_str("PhoneBssid(<redacted>)")
380 }
381}
382
383impl Serialize for PhoneBssid {
384 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
385 where
386 S: serde::Serializer,
387 {
388 serializer.serialize_str(&self.to_string())
389 }
390}
391
392impl<'de> serde::Deserialize<'de> for PhoneBssid {
393 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
394 where
395 D: serde::Deserializer<'de>,
396 {
397 let value = String::deserialize(deserializer)?;
398 parse_bssid(&value).map_or_else(
399 || {
400 Err(serde::de::Error::custom(
401 "BSSID must contain six hexadecimal octets separated by colons",
402 ))
403 },
404 Ok,
405 )
406 }
407}
408
409pub(super) fn parse_bssid(value: &str) -> Option<PhoneBssid> {
410 let mut octets = [0u8; 6];
411 let mut components = value.split(':');
412 for octet in &mut octets {
413 let component = components.next()?;
414 if component.len() != 2 {
415 return None;
416 }
417 *octet = u8::from_str_radix(component, 16).ok()?;
418 }
419 components.next().is_none().then_some(PhoneBssid(octets))
420}
421
422#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
426#[serde(deny_unknown_fields)]
427pub struct CiscoIpPhoneWifiLocation {
428 #[serde(rename = "BSSID")]
429 pub bssid: PhoneBssid,
430 #[serde(rename = "SSID")]
431 pub ssid: String,
432 #[serde(rename = "APName")]
433 pub access_point_name: String,
434}
435
436impl fmt::Debug for CiscoIpPhoneWifiLocation {
437 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
438 formatter
439 .debug_struct("CiscoIpPhoneWifiLocation")
440 .field("bssid", &self.bssid)
441 .field("ssid_byte_count", &self.ssid.len())
442 .field(
443 "access_point_name_char_count",
444 &self.access_point_name.chars().count(),
445 )
446 .finish()
447 }
448}
449
450#[derive(Clone, Default, serde::Deserialize, Eq, PartialEq, Serialize)]
452#[serde(deny_unknown_fields)]
453pub struct CiscoIpPhoneOffPremises {
454 #[serde(rename = "$text", default, skip_serializing_if = "String::is_empty")]
455 marker: String,
456}
457
458impl fmt::Debug for CiscoIpPhoneOffPremises {
459 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
460 formatter.write_str("CiscoIpPhoneOffPremises")
461 }
462}
463
464impl CiscoIpPhoneOffPremises {
465 pub const fn new() -> Self {
467 Self {
468 marker: String::new(),
469 }
470 }
471
472 fn validate(&self) -> Result<(), PhoneXmlError> {
473 if self.marker.is_empty() {
474 Ok(())
475 } else {
476 Err(PhoneXmlError::InvalidField {
477 field: "phone off-premises marker",
478 expected: "an empty element",
479 })
480 }
481 }
482}
483
484#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
488#[serde(rename = "Interface1", deny_unknown_fields)]
489pub struct CiscoIpPhoneLocationInformation {
490 #[serde(rename = "wifi")]
491 pub wifi: CiscoIpPhoneWifiLocation,
492 #[serde(rename = "OffPrem", default, skip_serializing_if = "Option::is_none")]
493 pub off_premises: Option<CiscoIpPhoneOffPremises>,
494}
495
496impl fmt::Debug for CiscoIpPhoneLocationInformation {
497 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
498 formatter
499 .debug_struct("CiscoIpPhoneLocationInformation")
500 .field("wifi", &self.wifi)
501 .field("off_premises", &self.off_premises.is_some())
502 .finish()
503 }
504}
505
506impl CiscoIpPhoneLocationInformation {
507 pub fn validate(&self) -> Result<(), PhoneXmlError> {
509 validate_optional_text(
510 "phone location SSID",
511 Some(&self.wifi.ssid),
512 0,
513 PHONE_LOCATION_MAX_BYTES,
514 )?;
515 validate_optional_text(
516 "phone location access-point name",
517 Some(&self.wifi.access_point_name),
518 0,
519 PHONE_LOCATION_MAX_BYTES,
520 )?;
521 if self.wifi.ssid.len() > 32 {
522 return Err(PhoneXmlError::InvalidField {
523 field: "phone location SSID",
524 expected: "at most 32 bytes",
525 });
526 }
527 if let Some(off_premises) = &self.off_premises {
528 off_premises.validate()?;
529 }
530 Ok(())
531 }
532
533 pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
535 #[derive(serde::Deserialize)]
536 enum LocationEnvelope {
537 #[serde(rename = "Interface1")]
538 Location(CiscoIpPhoneLocationInformation),
539 }
540
541 let LocationEnvelope::Location(location) =
542 from_bytes(document, PHONE_LOCATION_MAX_BYTES).map_err(redact_location_schema_error)?;
543 location.validate()?;
544 Ok(location)
545 }
546
547 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
549 self.validate()?;
550 to_string(self, PHONE_LOCATION_MAX_BYTES)
551 }
552
553 pub const fn is_off_premises(&self) -> bool {
554 self.off_premises.is_some()
555 }
556}
557
558#[derive(Clone, Eq, PartialEq)]
560pub struct OpaquePhoneLocation(Vec<u8>);
561
562impl OpaquePhoneLocation {
563 pub fn as_bytes(&self) -> &[u8] {
564 &self.0
565 }
566
567 pub fn into_bytes(self) -> Vec<u8> {
568 self.0
569 }
570}
571
572impl fmt::Debug for OpaquePhoneLocation {
573 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
574 formatter
575 .debug_struct("OpaquePhoneLocation")
576 .field("byte_count", &self.0.len())
577 .finish_non_exhaustive()
578 }
579}
580
581#[derive(Clone, Eq, PartialEq)]
583pub enum PhoneLocationTelemetry {
584 WirelessInterface(CiscoIpPhoneLocationInformation),
586 Opaque(OpaquePhoneLocation),
588}
589
590impl fmt::Debug for PhoneLocationTelemetry {
591 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
592 match self {
593 Self::WirelessInterface(location) => location.fmt(formatter),
594 Self::Opaque(location) => location.fmt(formatter),
595 }
596 }
597}
598
599#[derive(Clone, Copy, Debug, Eq, PartialEq)]
601pub enum PhoneLocationKind {
602 WirelessInterface,
603}
604
605#[derive(Clone, Copy, Debug, Eq, PartialEq)]
607pub struct PhoneLocationSummary {
608 pub kind: PhoneLocationKind,
609 pub off_premises: bool,
611}
612
613impl PhoneLocationTelemetry {
614 pub fn summary(&self) -> Option<PhoneLocationSummary> {
616 match self {
617 Self::WirelessInterface(location) => Some(PhoneLocationSummary {
618 kind: PhoneLocationKind::WirelessInterface,
619 off_premises: location.is_off_premises(),
620 }),
621 Self::Opaque(_) => None,
622 }
623 }
624
625 pub fn is_opaque(&self) -> bool {
626 matches!(self, Self::Opaque(_))
627 }
628}
629
630pub fn parse_phone_location(document: &[u8]) -> Result<PhoneLocationTelemetry, PhoneXmlError> {
633 #[derive(Debug, serde::Deserialize)]
634 struct LocationProbe;
635
636 #[derive(serde::Deserialize)]
637 enum LocationProbeEnvelope {
638 #[serde(rename = "Interface1")]
639 Location(LocationProbe),
640 #[serde(other)]
641 Unknown,
642 }
643
644 let supported = matches!(
645 from_bytes(document, PHONE_LOCATION_MAX_BYTES).map_err(redact_location_schema_error)?,
646 LocationProbeEnvelope::Location(_)
647 );
648 if supported {
649 CiscoIpPhoneLocationInformation::from_xml(document)
650 .map(PhoneLocationTelemetry::WirelessInterface)
651 } else {
652 Ok(PhoneLocationTelemetry::Opaque(OpaquePhoneLocation(
653 document.to_vec(),
654 )))
655 }
656}
657
658pub(super) fn redact_location_schema_error(error: PhoneXmlError) -> PhoneXmlError {
659 match error {
660 PhoneXmlError::Deserialize(_) => PhoneXmlError::InvalidLocationSchema,
661 error => error,
662 }
663}