use std::borrow::Borrow;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::num::NonZeroU64;
use std::ops::Deref;
use std::str::FromStr;
use crate::message::values::{
CallType, Codec, DeviceType, EchoCancellation, KeyMode, ProtocolVersion, SilenceSuppression,
SoftKey, Tone,
};
use crate::message::wire::CodecError;
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DateTemplate(String);
impl DateTemplate {
pub fn new(value: impl Into<String>) -> Result<Self, CodecError> {
let value = value.into();
let date = value.strip_suffix('A').unwrap_or(&value);
let mut fields = date.split(|character: char| !character.is_ascii_alphabetic());
let parsed = [fields.next(), fields.next(), fields.next()];
let separators = date
.bytes()
.filter(|byte| !byte.is_ascii_alphabetic())
.collect::<Vec<_>>();
let valid_fields = matches!(parsed, [Some(_), Some(_), Some(_)])
&& fields.next().is_none()
&& parsed
.into_iter()
.flatten()
.all(|field| matches!(field, "D" | "M" | "Y" | "YY"))
&& parsed
.into_iter()
.flatten()
.filter(|field| *field == "D")
.count()
== 1
&& parsed
.into_iter()
.flatten()
.filter(|field| *field == "M")
.count()
== 1
&& parsed
.into_iter()
.flatten()
.filter(|field| matches!(*field, "Y" | "YY"))
.count()
== 1;
if value.len() > 7
|| separators.len() != 2
|| !separators
.iter()
.all(|byte| matches!(byte, b'/' | b'.' | b'-' | b' '))
|| !valid_fields
{
return Err(CodecError::InvalidDefinition(
"date template must contain D, M, and Y/YY once, two supported separators, and an optional trailing A for 12-hour time"
.into(),
));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn uses_twelve_hour_clock(&self) -> bool {
self.0.ends_with('A')
}
}
impl AsRef<str> for DateTemplate {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for DateTemplate {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for DateTemplate {
type Err = CodecError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<String> for DateTemplate {
type Error = CodecError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl Default for DateTemplate {
fn default() -> Self {
Self("D/M/Y".into())
}
}
impl fmt::Debug for DateTemplate {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("DateTemplate")
.field(&self.0)
.finish()
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DeviceId(String);
impl DeviceId {
pub fn new(value: impl Into<String>) -> Result<Self, CodecError> {
let value = value.into().trim().to_ascii_uppercase();
if value.is_empty() || value.len() > 15 || !value.bytes().all(|b| b.is_ascii_alphanumeric())
{
return Err(CodecError::InvalidDeviceId(value));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for DeviceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for DeviceId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Borrow<str> for DeviceId {
fn borrow(&self) -> &str {
self.as_str()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SessionGeneration(NonZeroU64);
impl SessionGeneration {
pub const fn new(value: u64) -> Option<Self> {
match NonZeroU64::new(value) {
Some(value) => Some(Self(value)),
None => None,
}
}
pub const fn get(self) -> u64 {
self.0.get()
}
}
impl From<SessionGeneration> for u64 {
fn from(value: SessionGeneration) -> Self {
value.get()
}
}
impl fmt::Display for SessionGeneration {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
impl FromStr for DeviceId {
type Err = CodecError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl TryFrom<String> for DeviceId {
type Error = CodecError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
macro_rules! id_newtype {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $name(pub u32);
impl $name {
pub const fn new(value: u32) -> Self {
Self(value)
}
pub const fn get(self) -> u32 {
self.0
}
}
impl From<u32> for $name {
fn from(value: u32) -> Self {
Self(value)
}
}
impl From<$name> for u32 {
fn from(value: $name) -> Self {
value.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
};
}
id_newtype!( LineInstance);
id_newtype!( CallReference);
id_newtype!( PassthroughPartyId);
id_newtype!( AppearanceId);
id_newtype!( ConferenceId);
id_newtype!( ParticipantId);
id_newtype!( ApplicationId);
id_newtype!( TransactionId);
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct MediaTrafficClass(u8);
impl MediaTrafficClass {
pub const fn from_wire(value: u8) -> Self {
Self(value)
}
pub const fn from_dscp(dscp: u8) -> Option<Self> {
if dscp <= 63 {
Some(Self(dscp << 2))
} else {
None
}
}
pub const fn get(self) -> u8 {
self.0
}
}
impl From<MediaTrafficClass> for u8 {
fn from(value: MediaTrafficClass) -> Self {
value.get()
}
}
impl From<MediaTrafficClass> for u32 {
fn from(value: MediaTrafficClass) -> Self {
u32::from(value.get())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CallId(pub u64);
impl CallId {
pub const fn new(value: u64) -> Self {
Self(value)
}
pub const fn get(self) -> u64 {
self.0
}
}
impl From<u64> for CallId {
fn from(value: u64) -> Self {
Self::new(value)
}
}
impl From<CallId> for u64 {
fn from(value: CallId) -> Self {
value.get()
}
}
impl fmt::Display for CallId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LineDefinition {
pub number: String,
pub display_name: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CallerIdOverride {
pub name: Option<String>,
pub number: Option<String>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum AppearanceRingMode {
#[default]
Normal,
Silent,
Disabled,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LineAppearance {
pub id: AppearanceId,
pub instance: u32,
pub line: LineDefinition,
pub label: Option<String>,
pub caller_id: CallerIdOverride,
pub ring_mode: AppearanceRingMode,
pub initial_tone: Tone,
pub subscription_identity: Option<String>,
pub privacy: bool,
}
impl LineAppearance {
pub fn new(instance: u32, line: LineDefinition) -> Self {
Self {
id: AppearanceId::new(instance),
instance,
line,
label: None,
caller_id: CallerIdOverride::default(),
ring_mode: AppearanceRingMode::Normal,
initial_tone: Tone::InsideDial,
subscription_identity: None,
privacy: false,
}
}
pub fn display_label(&self) -> &str {
self.label.as_deref().unwrap_or(&self.line.display_name)
}
}
impl Deref for LineAppearance {
type Target = LineDefinition;
fn deref(&self) -> &Self::Target {
&self.line
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SpeedDialDefinition {
pub instance: u32,
pub number: String,
pub display_name: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BlfSpeedDialDefinition {
pub instance: u32,
pub number: String,
pub display_name: String,
pub hint: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BlfState {
Idle,
Ringing,
Busy,
Held,
Unavailable,
Unknown,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct BlfCallerInfo {
pub name: String,
pub number: String,
}
impl BlfCallerInfo {
pub fn display(&self) -> String {
match (self.name.trim(), self.number.trim()) {
("", "") => String::new(),
("", number) => number.to_owned(),
(name, "") => name.to_owned(),
(name, number) => format!("{name} ({number})"),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FeatureDefinition {
pub instance: u32,
pub label: String,
pub feature: crate::message::values::ButtonType,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ServiceDefinition {
pub instance: u32,
pub label: String,
pub url: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AddonModuleDefinition {
pub slot: u32,
pub device_type: crate::message::values::DeviceType,
}
impl AddonModuleDefinition {
pub const fn button_capacity(&self) -> Option<usize> {
use crate::message::values::DeviceType;
match self.device_type {
DeviceType::CiscoAddon7914 => Some(14),
DeviceType::CiscoAddon7915_12 | DeviceType::CiscoAddon7916_12 => Some(12),
DeviceType::CiscoAddon7915_24 | DeviceType::CiscoAddon7916_24 => Some(24),
DeviceType::AddonSpa500s | DeviceType::AddonSpa500ds | DeviceType::AddonSpa932ds => {
Some(32)
}
_ => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ButtonDefinition {
Line(LineAppearance),
SpeedDial(SpeedDialDefinition),
BlfSpeedDial(BlfSpeedDialDefinition),
Feature(FeatureDefinition),
Service(ServiceDefinition),
AddonModule(AddonModuleDefinition),
Unused,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SoftKeyProfile {
sets: HashMap<KeyMode, Vec<SoftKey>>,
}
impl SoftKeyProfile {
pub const MAX_KEYS_PER_MODE: usize = 16;
pub fn new(
sets: impl IntoIterator<Item = (KeyMode, Vec<SoftKey>)>,
) -> Result<Self, CodecError> {
let profile = Self {
sets: sets.into_iter().collect(),
};
profile.validate()?;
Ok(profile)
}
pub fn empty() -> Self {
Self {
sets: KeyMode::ALL_KNOWN
.iter()
.copied()
.map(|mode| (mode, Vec::new()))
.collect(),
}
}
pub fn built_in() -> Self {
let mut profile = Self::empty();
profile.sets.extend([
(KeyMode::OnHook, vec![SoftKey::NewCall]),
(
KeyMode::Connected,
vec![SoftKey::Hold, SoftKey::EndCall, SoftKey::Transfer],
),
(
KeyMode::OnHold,
vec![SoftKey::Resume, SoftKey::NewCall, SoftKey::EndCall],
),
(KeyMode::RingIn, vec![SoftKey::Answer, SoftKey::EndCall]),
(KeyMode::OffHook, vec![SoftKey::EndCall]),
(
KeyMode::ConnectedTransfer,
vec![SoftKey::Hold, SoftKey::EndCall, SoftKey::Transfer],
),
(
KeyMode::DigitsFollowing,
vec![SoftKey::Backspace, SoftKey::EndCall, SoftKey::Dial],
),
(
KeyMode::ConnectedConference,
vec![SoftKey::Hold, SoftKey::EndCall],
),
(KeyMode::RingOut, vec![SoftKey::EndCall]),
(
KeyMode::OffHookFeature,
vec![SoftKey::Resume, SoftKey::NewCall, SoftKey::EndCall],
),
(
KeyMode::OnHookStealable,
vec![SoftKey::Intercept, SoftKey::NewCall],
),
(
KeyMode::HoldConference,
vec![SoftKey::Resume, SoftKey::NewCall, SoftKey::EndCall],
),
]);
profile
}
pub fn actions(&self, mode: KeyMode) -> &[SoftKey] {
self.sets.get(&mode).map_or(&[], Vec::as_slice)
}
pub fn allows(&self, mode: KeyMode, action: SoftKey) -> bool {
action.is_known() && self.actions(mode).contains(&action)
}
pub fn valid_mask(&self, mode: KeyMode) -> u32 {
let count = self.actions(mode).len();
if count == 0 { 0 } else { (1_u32 << count) - 1 }
}
pub fn template_actions(&self) -> Vec<SoftKey> {
if self == &Self::built_in() {
return SoftKey::ALL_KNOWN.to_vec();
}
let configured: HashSet<_> = KeyMode::ALL_KNOWN
.iter()
.flat_map(|mode| self.actions(*mode).iter().copied())
.collect();
SoftKey::ALL_KNOWN
.iter()
.copied()
.filter(|action| configured.contains(action))
.collect()
}
pub fn validate(&self) -> Result<(), CodecError> {
if self.sets.len() != KeyMode::ALL_KNOWN.len()
|| KeyMode::ALL_KNOWN
.iter()
.any(|mode| !self.sets.contains_key(mode))
{
return Err(CodecError::InvalidDefinition(
"soft-key profile must define every known key mode".into(),
));
}
for (&mode, actions) in &self.sets {
if !mode.is_known() {
return Err(CodecError::InvalidDefinition(format!(
"soft-key profile contains unknown key mode {}",
mode.wire_value()
)));
}
if actions.len() > Self::MAX_KEYS_PER_MODE {
return Err(CodecError::InvalidDefinition(format!(
"soft-key mode {} contains {} actions; the protocol limit is {}",
mode.wire_value(),
actions.len(),
Self::MAX_KEYS_PER_MODE
)));
}
let mut seen = HashSet::new();
for &action in actions {
if !action.is_known() {
return Err(CodecError::InvalidDefinition(format!(
"soft-key mode {} contains unknown action {}",
mode.wire_value(),
action.wire_value()
)));
}
if !seen.insert(action) {
return Err(CodecError::InvalidDefinition(format!(
"soft-key mode {} repeats action {}",
mode.wire_value(),
action.wire_value()
)));
}
}
}
Ok(())
}
}
impl Default for SoftKeyProfile {
fn default() -> Self {
Self::built_in()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeviceDefinition {
pub id: DeviceId,
pub description: String,
pub transport: StationTransportRequirement,
pub signaling_qos: Option<SignalingQos>,
pub buttons: Vec<ButtonDefinition>,
pub soft_keys: SoftKeyProfile,
pub ui: StationUiPolicy,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum StationTransportRequirement {
Clear,
Secure,
#[default]
Either,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum StationTransport {
Clear,
Secure,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct SignalingQos {
pub dscp: u8,
pub cos: u8,
}
impl SignalingQos {
pub const fn new(dscp: u8, cos: u8) -> Self {
Self { dscp, cos }
}
pub(crate) fn validate(self) -> Result<(), CodecError> {
if self.dscp > 63 {
return Err(CodecError::InvalidDefinition(format!(
"signaling DSCP {} is outside 0..=63",
self.dscp
)));
}
if self.cos > 7 {
return Err(CodecError::InvalidDefinition(format!(
"signaling COS {} is outside 0..=7",
self.cos
)));
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StationUiPolicy {
pub placed_calls_redial_menu: bool,
pub hinted_ringing_notification: bool,
pub mwi_lamp_mode: crate::message::values::LampMode,
pub mwi_on_call: bool,
pub legacy_code_page: LegacyCodePage,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum LegacyCodePage {
#[default]
Iso8859_1,
Ascii,
}
impl Default for StationUiPolicy {
fn default() -> Self {
Self {
placed_calls_redial_menu: false,
hinted_ringing_notification: false,
mwi_lamp_mode: crate::message::values::LampMode::On,
mwi_on_call: false,
legacy_code_page: LegacyCodePage::Iso8859_1,
}
}
}
impl DeviceDefinition {
pub fn validate(&self) -> Result<(), CodecError> {
const MAX_BUTTONS: usize = 42;
self.soft_keys.validate()?;
if let Some(signaling_qos) = self.signaling_qos {
signaling_qos.validate()?;
}
if self.buttons.len() > MAX_BUTTONS {
return Err(CodecError::InvalidDefinition(format!(
"device {} has {} buttons; the protocol limit is {MAX_BUTTONS}",
self.id,
self.buttons.len()
)));
}
let mut expanded_buttons = 0_usize;
let mut addon_buttons_remaining = None;
for button in &self.buttons {
if let ButtonDefinition::AddonModule(addon) = button {
expanded_buttons += addon_buttons_remaining.take().unwrap_or_default();
addon_buttons_remaining = Some(addon.button_capacity().ok_or_else(|| {
CodecError::InvalidDefinition(format!(
"device {} has unsupported addon-module type {}",
self.id,
addon.device_type.wire_value()
))
})?);
continue;
}
expanded_buttons += 1;
if let Some(remaining) = &mut addon_buttons_remaining {
if *remaining == 0 {
return Err(CodecError::InvalidDefinition(format!(
"device {} configures more buttons than its addon module provides",
self.id
)));
}
*remaining -= 1;
}
}
expanded_buttons += addon_buttons_remaining.unwrap_or_default();
if expanded_buttons > MAX_BUTTONS {
return Err(CodecError::InvalidDefinition(format!(
"device {} expands to {expanded_buttons} buttons; the protocol limit is {MAX_BUTTONS}",
self.id
)));
}
let mut instances = HashSet::new();
let mut appearance_ids = HashSet::new();
for button in &self.buttons {
let Some((kind, instance)) = button.instance_key() else {
continue;
};
if instance == 0 {
return Err(CodecError::InvalidDefinition(format!(
"device {} has a {kind} button with instance zero",
self.id
)));
}
if !instances.insert((kind, instance)) {
return Err(CodecError::InvalidDefinition(format!(
"device {} repeats {kind} button instance {instance}",
self.id
)));
}
if let ButtonDefinition::Line(appearance) = button {
if appearance.id.get() == 0 {
return Err(CodecError::InvalidDefinition(format!(
"device {} has a line appearance with identifier zero",
self.id
)));
}
if !appearance_ids.insert(appearance.id) {
return Err(CodecError::InvalidDefinition(format!(
"device {} repeats line appearance identifier {}",
self.id, appearance.id
)));
}
}
if let ButtonDefinition::Service(service) = button {
validate_service_definition(&self.id, service)?;
}
}
let lines: Vec<_> = self.lines().collect();
if lines.is_empty() {
return Err(CodecError::InvalidDefinition(format!(
"device {} has no lines",
self.id
)));
}
let permits_sparse_lines = self.buttons.iter().any(|button| {
matches!(
button,
ButtonDefinition::Feature(feature)
if feature.feature == crate::message::values::ButtonType::Mobility
)
});
for (expected, line) in (1_u32..).zip(lines) {
if !permits_sparse_lines && line.instance != expected {
return Err(CodecError::InvalidDefinition(format!(
"device {} line instances must be contiguous from 1",
self.id
)));
}
if line.number.is_empty() || line.number.len() > 24 {
return Err(CodecError::InvalidDefinition(format!(
"device {} has an invalid line number",
self.id
)));
}
}
Ok(())
}
pub fn lines(&self) -> impl Iterator<Item = &LineAppearance> {
self.buttons.iter().filter_map(|button| match button {
ButtonDefinition::Line(line) => Some(line),
_ => None,
})
}
pub fn line(&self, instance: u32) -> Option<&LineAppearance> {
self.lines().find(|line| line.instance == instance)
}
pub fn first_line(&self) -> Option<&LineAppearance> {
self.lines().next()
}
pub fn line_count(&self) -> usize {
self.lines().count()
}
}
fn validate_service_definition(
device: &DeviceId,
service: &ServiceDefinition,
) -> Result<(), CodecError> {
const MAX_SERVICE_URL_BYTES: usize = 255;
const MAX_SERVICE_LABEL_BYTES: usize = 39;
const MAX_SERVICE_PARAMETERS: usize = 32;
const MAX_SERVICE_PARAMETER_BYTES: usize = 128;
if service.label.is_empty()
|| service.label.len() > MAX_SERVICE_LABEL_BYTES
|| service.label.chars().any(char::is_control)
{
return Err(CodecError::InvalidDefinition(format!(
"device {device} has an invalid service label"
)));
}
if service.url.is_empty()
|| service.url.len() > MAX_SERVICE_URL_BYTES
|| service.url.chars().any(char::is_control)
{
return Err(CodecError::InvalidDefinition(format!(
"device {device} has an invalid service URL"
)));
}
let url = url::Url::parse(&service.url).map_err(|_| {
CodecError::InvalidDefinition(format!("device {device} has a malformed service URL"))
})?;
if !matches!(url.scheme(), "http" | "https")
|| url.host_str().is_none()
|| url.fragment().is_some()
{
return Err(CodecError::InvalidDefinition(format!(
"device {device} service URL must be HTTP(S) without a fragment"
)));
}
let parameters = url.query_pairs().collect::<Vec<_>>();
if parameters.len() > MAX_SERVICE_PARAMETERS
|| parameters.iter().any(|(name, value)| {
name.is_empty()
|| name.len() > MAX_SERVICE_PARAMETER_BYTES
|| value.len() > MAX_SERVICE_PARAMETER_BYTES
|| name.chars().chain(value.chars()).any(char::is_control)
})
{
return Err(CodecError::InvalidDefinition(format!(
"device {device} service URL has invalid or excessive query parameters"
)));
}
Ok(())
}
impl ButtonDefinition {
fn instance_key(&self) -> Option<(&'static str, u32)> {
match self {
Self::Line(definition) => Some(("line", definition.instance)),
Self::SpeedDial(definition) => Some(("speed dial", definition.instance)),
Self::BlfSpeedDial(definition) => Some(("BLF speed dial", definition.instance)),
Self::Feature(definition) => Some(("feature", definition.instance)),
Self::Service(definition) => Some(("service URL", definition.instance)),
Self::AddonModule(definition) => Some(("addon module", definition.slot)),
Self::Unused => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeviceRegistration {
pub id: DeviceId,
pub peer: SocketAddr,
pub transport: StationTransport,
pub reported_address: Option<Ipv4Addr>,
pub reported_ipv6_address: Option<Ipv6Addr>,
pub device_type: DeviceType,
pub protocol: ProtocolVersion,
pub firmware: String,
}
impl DeviceRegistration {
pub fn reported_address_for_peer(&self) -> Option<IpAddr> {
match self.peer.ip() {
IpAddr::V4(_) => self.reported_address.map(IpAddr::V4),
IpAddr::V6(peer) => peer.to_ipv4_mapped().map_or_else(
|| self.reported_ipv6_address.map(IpAddr::V6),
|_| self.reported_address.map(IpAddr::V4),
),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CallDirection {
Inbound,
Outbound,
}
impl From<CallDirection> for CallType {
fn from(value: CallDirection) -> Self {
match value {
CallDirection::Inbound => Self::Inbound,
CallDirection::Outbound => Self::Outbound,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CallInfo {
pub direction: CallDirection,
pub calling_name: String,
pub calling_number: String,
pub called_name: String,
pub called_number: String,
pub original_called_name: String,
pub original_called_number: String,
pub last_redirecting_name: String,
pub last_redirecting_number: String,
pub original_redirect_reason: u32,
pub last_redirect_reason: u32,
pub party_restrictions: u32,
}
impl Default for CallInfo {
fn default() -> Self {
Self {
direction: CallDirection::Outbound,
calling_name: String::new(),
calling_number: String::new(),
called_name: String::new(),
called_number: String::new(),
original_called_name: String::new(),
original_called_number: String::new(),
last_redirecting_name: String::new(),
last_redirecting_number: String::new(),
original_redirect_reason: 0,
last_redirect_reason: 0,
party_restrictions: 0,
}
}
}
pub const DEFAULT_AUDIO_PACKET_MS: u32 = 20;
pub const DEFAULT_AUDIO_MAX_FRAMES_PER_PACKET: u32 = 1;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AudioProcessingPolicy {
pub echo_cancellation: EchoCancellation,
pub silence_suppression: SilenceSuppression,
}
impl Default for AudioProcessingPolicy {
fn default() -> Self {
Self {
echo_cancellation: EchoCancellation::On,
silence_suppression: SilenceSuppression::Off,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MediaEndpoint {
pub address: IpAddr,
pub rtp_port: u16,
pub rtcp_port: u16,
pub codec: Codec,
pub packet_ms: u32,
pub max_frames_per_packet: u32,
pub telephone_event_payload: u8,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identifiers_are_explicit_and_lossless() {
let reference = CallReference::new(42);
assert_eq!(reference.get(), 42);
assert_eq!(u32::from(reference), 42);
let appearance = AppearanceId::new(7);
assert_eq!(appearance.get(), 7);
assert_eq!(u32::from(appearance), 7);
let conference = ConferenceId::new(9);
assert_eq!(conference.get(), 9);
let participant = ParticipantId::new(11);
assert_eq!(participant.get(), 11);
}
#[test]
fn device_id_is_canonicalized() {
let id: DeviceId = " sep001122334455 ".parse().unwrap();
assert_eq!(id.as_str(), "SEP001122334455");
assert_eq!(id.as_ref(), "SEP001122334455");
let mut devices = HashMap::new();
devices.insert(id, "desk");
assert_eq!(devices.get("SEP001122334455"), Some(&"desk"));
}
#[test]
fn registration_selects_the_report_matching_the_effective_peer_family() {
let registration = DeviceRegistration {
id: DeviceId::new("SEP001122334455").unwrap(),
peer: "[2001:db8::20]:2000".parse().unwrap(),
transport: StationTransport::Clear,
reported_address: Some("192.0.2.20".parse().unwrap()),
reported_ipv6_address: Some("2001:db8::20".parse().unwrap()),
device_type: DeviceType::Cisco7962,
protocol: ProtocolVersion::V22,
firmware: "test".into(),
};
assert_eq!(
registration.reported_address_for_peer(),
Some("2001:db8::20".parse().unwrap())
);
let mapped = DeviceRegistration {
peer: "[::ffff:192.0.2.20]:2000".parse().unwrap(),
..registration
};
assert_eq!(
mapped.reported_address_for_peer(),
Some("192.0.2.20".parse().unwrap())
);
}
fn line_button(instance: u32, number: &str) -> ButtonDefinition {
ButtonDefinition::Line(LineAppearance::new(
instance,
LineDefinition {
number: number.into(),
display_name: number.into(),
},
))
}
#[test]
fn station_definition_accepts_non_line_buttons_between_lines() {
let definition = DeviceDefinition {
id: DeviceId::new("SEP001122334455").unwrap(),
description: "Desk".into(),
transport: StationTransportRequirement::Either,
signaling_qos: None,
buttons: vec![
line_button(1, "1001"),
ButtonDefinition::Unused,
ButtonDefinition::SpeedDial(SpeedDialDefinition {
instance: 1,
number: "2001".into(),
display_name: "Warehouse".into(),
}),
ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
instance: 1,
number: "2002".into(),
display_name: "Dispatch".into(),
hint: "2002@internal".into(),
}),
ButtonDefinition::Feature(FeatureDefinition {
instance: 1,
label: "DND".into(),
feature: crate::message::values::ButtonType::DoNotDisturb,
}),
ButtonDefinition::Service(ServiceDefinition {
instance: 1,
label: "Directory".into(),
url: "http://pbx.test/directory".into(),
}),
ButtonDefinition::AddonModule(AddonModuleDefinition {
slot: 1,
device_type: crate::message::values::DeviceType::CiscoAddon7914,
}),
line_button(2, "1002"),
],
soft_keys: SoftKeyProfile::default(),
ui: StationUiPolicy::default(),
};
definition.validate().unwrap();
assert_eq!(definition.line_count(), 2);
assert_eq!(definition.line(2).unwrap().number, "1002");
}
#[test]
fn station_definition_rejects_invalid_signaling_markings() {
let mut definition = DeviceDefinition {
id: DeviceId::new("SEP001122334455").unwrap(),
description: "Desk".into(),
transport: StationTransportRequirement::Either,
signaling_qos: Some(SignalingQos::new(64, 0)),
buttons: vec![line_button(1, "1001")],
soft_keys: SoftKeyProfile::default(),
ui: StationUiPolicy::default(),
};
assert!(matches!(
definition.validate(),
Err(CodecError::InvalidDefinition(message)) if message.contains("DSCP 64")
));
definition.signaling_qos = Some(SignalingQos::new(26, 8));
assert!(matches!(
definition.validate(),
Err(CodecError::InvalidDefinition(message)) if message.contains("COS 8")
));
}
#[test]
fn line_appearance_keeps_logical_and_device_specific_state_separate() {
let logical = LineDefinition {
number: "1001".into(),
display_name: "Reception".into(),
};
let mut appearance = LineAppearance::new(2, logical.clone());
appearance.label = Some("Private key".into());
appearance.caller_id = CallerIdOverride {
name: Some("Private desk".into()),
number: None,
};
appearance.ring_mode = AppearanceRingMode::Silent;
appearance.subscription_identity = Some("1001@internal".into());
appearance.privacy = true;
assert_eq!(appearance.line, logical);
assert_eq!(appearance.display_label(), "Private key");
assert_eq!(appearance.number, "1001");
assert_eq!(appearance.id, AppearanceId::new(2));
}
#[test]
fn station_definition_rejects_zero_and_duplicate_typed_instances() {
let definition = DeviceDefinition {
id: DeviceId::new("SEP001122334455").unwrap(),
description: "Desk".into(),
transport: StationTransportRequirement::Either,
signaling_qos: None,
buttons: vec![line_button(1, "1001"), line_button(1, "1002")],
soft_keys: SoftKeyProfile::default(),
ui: StationUiPolicy::default(),
};
assert!(matches!(
definition.validate(),
Err(CodecError::InvalidDefinition(message))
if message.contains("repeats line button instance 1")
));
let definition = DeviceDefinition {
id: DeviceId::new("SEP001122334455").unwrap(),
description: "Desk".into(),
transport: StationTransportRequirement::Either,
signaling_qos: None,
buttons: vec![
line_button(1, "1001"),
ButtonDefinition::Feature(FeatureDefinition {
instance: 0,
label: "DND".into(),
feature: crate::message::values::ButtonType::DoNotDisturb,
}),
],
soft_keys: SoftKeyProfile::default(),
ui: StationUiPolicy::default(),
};
assert!(matches!(
definition.validate(),
Err(CodecError::InvalidDefinition(message))
if message.contains("feature button with instance zero")
));
}
#[test]
fn station_definition_enforces_protocol_button_limit() {
let definition = DeviceDefinition {
id: DeviceId::new("SEP001122334455").unwrap(),
description: "Desk".into(),
transport: StationTransportRequirement::Either,
signaling_qos: None,
buttons: std::iter::once(line_button(1, "1001"))
.chain(std::iter::repeat_n(ButtonDefinition::Unused, 42))
.collect(),
soft_keys: SoftKeyProfile::default(),
ui: StationUiPolicy::default(),
};
assert!(matches!(
definition.validate(),
Err(CodecError::InvalidDefinition(message))
if message.contains("protocol limit is 42")
));
}
#[test]
fn service_urls_require_bounded_http_parameters() {
let service_device = |url: &str| DeviceDefinition {
id: DeviceId::new("SEP001122334455").unwrap(),
description: "Desk".into(),
transport: StationTransportRequirement::Either,
signaling_qos: None,
buttons: vec![
line_button(1, "1001"),
ButtonDefinition::Service(ServiceDefinition {
instance: 1,
label: "Directory".into(),
url: url.into(),
}),
],
soft_keys: SoftKeyProfile::default(),
ui: StationUiPolicy::default(),
};
service_device("https://pbx.example/sccp/directory?q=Fran%C3%A7ois&page=2")
.validate()
.unwrap();
service_device("https://user:secret@pbx.example/service")
.validate()
.unwrap();
for invalid in [
"file:///etc/passwd",
"https://pbx.example/service#private",
"https://pbx.example/service?=missing-name",
"not a URL",
] {
let error = service_device(invalid).validate().unwrap_err().to_string();
assert!(!error.contains(invalid));
}
let excessive = format!(
"https://pbx.example/service?{}",
(0..33)
.map(|index| format!("p{index}=v"))
.collect::<Vec<_>>()
.join("&")
);
assert!(service_device(&excessive).validate().is_err());
}
#[test]
fn soft_key_profiles_require_every_mode_and_unique_known_actions() {
assert!(matches!(
SoftKeyProfile::new([(KeyMode::OnHook, vec![SoftKey::NewCall])]),
Err(CodecError::InvalidDefinition(message))
if message.contains("every known key mode")
));
let duplicate = SoftKeyProfile::new(KeyMode::ALL_KNOWN.iter().copied().map(|mode| {
(
mode,
if mode == KeyMode::Connected {
vec![SoftKey::Hold, SoftKey::Hold]
} else {
Vec::new()
},
)
}));
assert!(matches!(
duplicate,
Err(CodecError::InvalidDefinition(message)) if message.contains("repeats action")
));
}
}