use super::*;
pub(super) const LAST_OUT_OF_SERVICE_ALARM: &str = "LastOutOfServiceInformation";
#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CiscoIpPhoneAlarmString {
#[serde(rename = "@name")]
pub name: String,
#[serde(rename = "$text", default)]
pub value: String,
}
impl fmt::Debug for CiscoIpPhoneAlarmString {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CiscoIpPhoneAlarmString")
.field("name", &self.name)
.field("value", &"<redacted>")
.finish()
}
}
#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CiscoIpPhoneAlarmEnum {
#[serde(rename = "@name")]
pub name: String,
#[serde(rename = "$text")]
pub value: i32,
}
impl fmt::Debug for CiscoIpPhoneAlarmEnum {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CiscoIpPhoneAlarmEnum")
.field("name", &self.name)
.field("value", &self.value)
.finish()
}
}
#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
pub enum CiscoIpPhoneAlarmParameter {
#[serde(rename = "String")]
String(CiscoIpPhoneAlarmString),
#[serde(rename = "Enum")]
Enum(CiscoIpPhoneAlarmEnum),
}
impl fmt::Debug for CiscoIpPhoneAlarmParameter {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::String(value) => value.fmt(formatter),
Self::Enum(value) => value.fmt(formatter),
}
}
}
#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CiscoIpPhoneAlarmParameterList {
#[serde(rename = "$value", default)]
pub parameters: Vec<CiscoIpPhoneAlarmParameter>,
}
impl fmt::Debug for CiscoIpPhoneAlarmParameterList {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CiscoIpPhoneAlarmParameterList")
.field("parameter_count", &self.parameters.len())
.finish()
}
}
#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CiscoIpPhoneAlarmEntry {
#[serde(rename = "@Name")]
pub name: String,
#[serde(rename = "ParameterList")]
pub parameter_list: CiscoIpPhoneAlarmParameterList,
}
impl fmt::Debug for CiscoIpPhoneAlarmEntry {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CiscoIpPhoneAlarmEntry")
.field("name", &self.name)
.field("parameter_count", &self.parameter_list.parameters.len())
.finish()
}
}
#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename = "x-cisco-alarm", deny_unknown_fields)]
pub struct CiscoIpPhoneAlarm {
#[serde(rename = "Alarm")]
pub alarm: CiscoIpPhoneAlarmEntry,
}
impl fmt::Debug for CiscoIpPhoneAlarm {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CiscoIpPhoneAlarm")
.field("name", &self.alarm.name)
.field(
"parameter_count",
&self.alarm.parameter_list.parameters.len(),
)
.finish()
}
}
impl CiscoIpPhoneAlarm {
pub fn validate(&self) -> Result<(), PhoneXmlError> {
if self.alarm.name != LAST_OUT_OF_SERVICE_ALARM {
return Err(PhoneXmlError::InvalidField {
field: "phone alarm name",
expected: "LastOutOfServiceInformation",
});
}
let mut names = HashSet::new();
for parameter in &self.alarm.parameter_list.parameters {
let name = match parameter {
CiscoIpPhoneAlarmParameter::String(value) => {
validate_optional_text(
"phone alarm string name",
Some(&value.name),
1,
PHONE_ALARM_MAX_BYTES,
)?;
validate_optional_text(
"phone alarm string value",
Some(&value.value),
0,
PHONE_ALARM_MAX_BYTES,
)?;
&value.name
}
CiscoIpPhoneAlarmParameter::Enum(value) => {
validate_optional_text(
"phone alarm enumeration name",
Some(&value.name),
1,
PHONE_ALARM_MAX_BYTES,
)?;
&value.name
}
};
if !names.insert(name.as_str()) {
return Err(PhoneXmlError::InvalidField {
field: "phone alarm parameter names",
expected: "unique across string and enumeration parameters",
});
}
}
Ok(())
}
pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
#[derive(serde::Deserialize)]
enum AlarmEnvelope {
#[serde(rename = "x-cisco-alarm")]
Alarm(CiscoIpPhoneAlarm),
}
let AlarmEnvelope::Alarm(document) =
from_bytes(document, PHONE_ALARM_MAX_BYTES).map_err(redact_alarm_schema_error)?;
document.validate()?;
Ok(document)
}
pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
self.validate()?;
to_string(self, PHONE_ALARM_MAX_BYTES)
}
pub fn string(&self, name: &str) -> Option<&str> {
self.alarm
.parameter_list
.parameters
.iter()
.find_map(|parameter| match parameter {
CiscoIpPhoneAlarmParameter::String(value) if value.name == name => {
Some(value.value.as_str())
}
_ => None,
})
}
pub fn enumeration(&self, name: &str) -> Option<i32> {
self.alarm
.parameter_list
.parameters
.iter()
.find_map(|parameter| match parameter {
CiscoIpPhoneAlarmParameter::Enum(value) if value.name == name => Some(value.value),
_ => None,
})
}
pub fn reason_for_out_of_service(&self) -> Option<i32> {
self.enumeration("ReasonForOutOfService")
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct OpaquePhoneAlarm(Vec<u8>);
impl OpaquePhoneAlarm {
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn into_bytes(self) -> Vec<u8> {
self.0
}
}
impl fmt::Debug for OpaquePhoneAlarm {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OpaquePhoneAlarm")
.field("byte_count", &self.0.len())
.finish_non_exhaustive()
}
}
#[derive(Clone, Eq, PartialEq)]
pub enum PhoneAlarmTelemetry {
LastOutOfService(CiscoIpPhoneAlarm),
Opaque(OpaquePhoneAlarm),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PhoneAlarmKind {
LastOutOfService,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PhoneAlarmSummary {
pub kind: PhoneAlarmKind,
pub reason_for_out_of_service: Option<i32>,
}
impl fmt::Debug for PhoneAlarmTelemetry {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::LastOutOfService(alarm) => alarm.fmt(formatter),
Self::Opaque(alarm) => alarm.fmt(formatter),
}
}
}
impl PhoneAlarmTelemetry {
pub fn summary(&self) -> Option<PhoneAlarmSummary> {
match self {
Self::LastOutOfService(alarm) => Some(PhoneAlarmSummary {
kind: PhoneAlarmKind::LastOutOfService,
reason_for_out_of_service: alarm.reason_for_out_of_service(),
}),
Self::Opaque(_) => None,
}
}
pub fn is_opaque(&self) -> bool {
matches!(self, Self::Opaque(_))
}
}
pub fn parse_phone_alarm(document: &[u8]) -> Result<PhoneAlarmTelemetry, PhoneXmlError> {
#[derive(Debug, serde::Deserialize)]
struct AlarmProbe {
#[serde(rename = "Alarm", default)]
alarms: Vec<AlarmNameProbe>,
}
#[derive(Debug, serde::Deserialize)]
struct AlarmNameProbe {
#[serde(rename = "@Name")]
name: String,
}
#[derive(serde::Deserialize)]
enum AlarmProbeEnvelope {
#[serde(rename = "x-cisco-alarm")]
Alarm(AlarmProbe),
#[serde(other)]
Unknown,
}
let supported = match from_bytes(document, PHONE_ALARM_MAX_BYTES)
.map_err(redact_alarm_schema_error)?
{
AlarmProbeEnvelope::Alarm(probe) => {
matches!(probe.alarms.as_slice(), [alarm] if alarm.name == LAST_OUT_OF_SERVICE_ALARM)
}
AlarmProbeEnvelope::Unknown => false,
};
if supported {
CiscoIpPhoneAlarm::from_xml(document).map(PhoneAlarmTelemetry::LastOutOfService)
} else {
Ok(PhoneAlarmTelemetry::Opaque(OpaquePhoneAlarm(
document.to_vec(),
)))
}
}
pub(super) fn redact_alarm_schema_error(error: PhoneXmlError) -> PhoneXmlError {
match error {
PhoneXmlError::Deserialize(_) => PhoneXmlError::InvalidAlarmSchema,
error => error,
}
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct PhoneBssid([u8; 6]);
impl PhoneBssid {
pub const fn from_octets(octets: [u8; 6]) -> Self {
Self(octets)
}
pub const fn octets(self) -> [u8; 6] {
self.0
}
pub fn parse(value: &str) -> Result<Self, PhoneXmlError> {
parse_bssid(value).ok_or(PhoneXmlError::InvalidField {
field: "phone location BSSID",
expected: "six hexadecimal octets separated by colons",
})
}
}
impl fmt::Display for PhoneBssid {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
)
}
}
impl fmt::Debug for PhoneBssid {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("PhoneBssid(<redacted>)")
}
}
impl Serialize for PhoneBssid {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> serde::Deserialize<'de> for PhoneBssid {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
parse_bssid(&value).map_or_else(
|| {
Err(serde::de::Error::custom(
"BSSID must contain six hexadecimal octets separated by colons",
))
},
Ok,
)
}
}
pub(super) fn parse_bssid(value: &str) -> Option<PhoneBssid> {
let mut octets = [0u8; 6];
let mut components = value.split(':');
for octet in &mut octets {
let component = components.next()?;
if component.len() != 2 {
return None;
}
*octet = u8::from_str_radix(component, 16).ok()?;
}
components.next().is_none().then_some(PhoneBssid(octets))
}
#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CiscoIpPhoneWifiLocation {
#[serde(rename = "BSSID")]
pub bssid: PhoneBssid,
#[serde(rename = "SSID")]
pub ssid: String,
#[serde(rename = "APName")]
pub access_point_name: String,
}
impl fmt::Debug for CiscoIpPhoneWifiLocation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CiscoIpPhoneWifiLocation")
.field("bssid", &self.bssid)
.field("ssid_byte_count", &self.ssid.len())
.field(
"access_point_name_char_count",
&self.access_point_name.chars().count(),
)
.finish()
}
}
#[derive(Clone, Default, serde::Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CiscoIpPhoneOffPremises {
#[serde(rename = "$text", default, skip_serializing_if = "String::is_empty")]
marker: String,
}
impl fmt::Debug for CiscoIpPhoneOffPremises {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("CiscoIpPhoneOffPremises")
}
}
impl CiscoIpPhoneOffPremises {
pub const fn new() -> Self {
Self {
marker: String::new(),
}
}
fn validate(&self) -> Result<(), PhoneXmlError> {
if self.marker.is_empty() {
Ok(())
} else {
Err(PhoneXmlError::InvalidField {
field: "phone off-premises marker",
expected: "an empty element",
})
}
}
}
#[derive(Clone, serde::Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename = "Interface1", deny_unknown_fields)]
pub struct CiscoIpPhoneLocationInformation {
#[serde(rename = "wifi")]
pub wifi: CiscoIpPhoneWifiLocation,
#[serde(rename = "OffPrem", default, skip_serializing_if = "Option::is_none")]
pub off_premises: Option<CiscoIpPhoneOffPremises>,
}
impl fmt::Debug for CiscoIpPhoneLocationInformation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CiscoIpPhoneLocationInformation")
.field("wifi", &self.wifi)
.field("off_premises", &self.off_premises.is_some())
.finish()
}
}
impl CiscoIpPhoneLocationInformation {
pub fn validate(&self) -> Result<(), PhoneXmlError> {
validate_optional_text(
"phone location SSID",
Some(&self.wifi.ssid),
0,
PHONE_LOCATION_MAX_BYTES,
)?;
validate_optional_text(
"phone location access-point name",
Some(&self.wifi.access_point_name),
0,
PHONE_LOCATION_MAX_BYTES,
)?;
if self.wifi.ssid.len() > 32 {
return Err(PhoneXmlError::InvalidField {
field: "phone location SSID",
expected: "at most 32 bytes",
});
}
if let Some(off_premises) = &self.off_premises {
off_premises.validate()?;
}
Ok(())
}
pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
#[derive(serde::Deserialize)]
enum LocationEnvelope {
#[serde(rename = "Interface1")]
Location(CiscoIpPhoneLocationInformation),
}
let LocationEnvelope::Location(location) =
from_bytes(document, PHONE_LOCATION_MAX_BYTES).map_err(redact_location_schema_error)?;
location.validate()?;
Ok(location)
}
pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
self.validate()?;
to_string(self, PHONE_LOCATION_MAX_BYTES)
}
pub const fn is_off_premises(&self) -> bool {
self.off_premises.is_some()
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct OpaquePhoneLocation(Vec<u8>);
impl OpaquePhoneLocation {
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn into_bytes(self) -> Vec<u8> {
self.0
}
}
impl fmt::Debug for OpaquePhoneLocation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OpaquePhoneLocation")
.field("byte_count", &self.0.len())
.finish_non_exhaustive()
}
}
#[derive(Clone, Eq, PartialEq)]
pub enum PhoneLocationTelemetry {
WirelessInterface(CiscoIpPhoneLocationInformation),
Opaque(OpaquePhoneLocation),
}
impl fmt::Debug for PhoneLocationTelemetry {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::WirelessInterface(location) => location.fmt(formatter),
Self::Opaque(location) => location.fmt(formatter),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PhoneLocationKind {
WirelessInterface,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PhoneLocationSummary {
pub kind: PhoneLocationKind,
pub off_premises: bool,
}
impl PhoneLocationTelemetry {
pub fn summary(&self) -> Option<PhoneLocationSummary> {
match self {
Self::WirelessInterface(location) => Some(PhoneLocationSummary {
kind: PhoneLocationKind::WirelessInterface,
off_premises: location.is_off_premises(),
}),
Self::Opaque(_) => None,
}
}
pub fn is_opaque(&self) -> bool {
matches!(self, Self::Opaque(_))
}
}
pub fn parse_phone_location(document: &[u8]) -> Result<PhoneLocationTelemetry, PhoneXmlError> {
#[derive(Debug, serde::Deserialize)]
struct LocationProbe;
#[derive(serde::Deserialize)]
enum LocationProbeEnvelope {
#[serde(rename = "Interface1")]
Location(LocationProbe),
#[serde(other)]
Unknown,
}
let supported = matches!(
from_bytes(document, PHONE_LOCATION_MAX_BYTES).map_err(redact_location_schema_error)?,
LocationProbeEnvelope::Location(_)
);
if supported {
CiscoIpPhoneLocationInformation::from_xml(document)
.map(PhoneLocationTelemetry::WirelessInterface)
} else {
Ok(PhoneLocationTelemetry::Opaque(OpaquePhoneLocation(
document.to_vec(),
)))
}
}
pub(super) fn redact_location_schema_error(error: PhoneXmlError) -> PhoneXmlError {
match error {
PhoneXmlError::Deserialize(_) => PhoneXmlError::InvalidLocationSchema,
error => error,
}
}