1use std::borrow::Borrow;
10use std::collections::{HashMap, HashSet};
11use std::fmt;
12use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
13use std::num::NonZeroU64;
14use std::ops::Deref;
15use std::str::FromStr;
16
17use crate::message::values::{
18 CallType, Codec, DeviceType, EchoCancellation, KeyMode, ProtocolVersion, SilenceSuppression,
19 SoftKey, Tone,
20};
21use crate::message::wire::CodecError;
22
23pub(crate) const MAX_STATION_BUTTON_INSTANCE: u32 = u8::MAX as u32;
24
25#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
30pub struct DateTemplate(String);
31
32impl DateTemplate {
33 pub fn new(value: impl Into<String>) -> Result<Self, CodecError> {
39 let value = value.into();
40 let date = value.strip_suffix('A').unwrap_or(&value);
41 let mut fields = date.split(|character: char| !character.is_ascii_alphabetic());
42 let parsed = [fields.next(), fields.next(), fields.next()];
43 let separators = date
44 .bytes()
45 .filter(|byte| !byte.is_ascii_alphabetic())
46 .collect::<Vec<_>>();
47 let valid_fields = matches!(parsed, [Some(_), Some(_), Some(_)])
48 && fields.next().is_none()
49 && parsed
50 .into_iter()
51 .flatten()
52 .all(|field| matches!(field, "D" | "M" | "Y" | "YY"))
53 && parsed
54 .into_iter()
55 .flatten()
56 .filter(|field| *field == "D")
57 .count()
58 == 1
59 && parsed
60 .into_iter()
61 .flatten()
62 .filter(|field| *field == "M")
63 .count()
64 == 1
65 && parsed
66 .into_iter()
67 .flatten()
68 .filter(|field| matches!(*field, "Y" | "YY"))
69 .count()
70 == 1;
71 if value.len() > 7
72 || separators.len() != 2
73 || !separators
74 .iter()
75 .all(|byte| matches!(byte, b'/' | b'.' | b'-' | b' '))
76 || !valid_fields
77 {
78 return Err(CodecError::InvalidDefinition(
79 "date template must contain D, M, and Y/YY once, two supported separators, and an optional trailing A for 12-hour time"
80 .into(),
81 ));
82 }
83 Ok(Self(value))
84 }
85
86 pub fn as_str(&self) -> &str {
87 &self.0
88 }
89
90 pub fn uses_twelve_hour_clock(&self) -> bool {
91 self.0.ends_with('A')
92 }
93}
94
95impl AsRef<str> for DateTemplate {
96 fn as_ref(&self) -> &str {
97 self.as_str()
98 }
99}
100
101impl fmt::Display for DateTemplate {
102 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103 formatter.write_str(self.as_str())
104 }
105}
106
107impl FromStr for DateTemplate {
108 type Err = CodecError;
109
110 fn from_str(value: &str) -> Result<Self, Self::Err> {
111 Self::new(value)
112 }
113}
114
115impl TryFrom<String> for DateTemplate {
116 type Error = CodecError;
117
118 fn try_from(value: String) -> Result<Self, Self::Error> {
119 Self::new(value)
120 }
121}
122
123impl Default for DateTemplate {
124 fn default() -> Self {
125 Self("D/M/Y".into())
126 }
127}
128
129impl fmt::Debug for DateTemplate {
130 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131 formatter
132 .debug_tuple("DateTemplate")
133 .field(&self.0)
134 .finish()
135 }
136}
137
138#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
140pub struct DeviceId(String);
141
142impl DeviceId {
143 pub fn new(value: impl Into<String>) -> Result<Self, CodecError> {
149 let value = value.into().trim().to_ascii_uppercase();
150 if value.is_empty() || value.len() > 15 || !value.bytes().all(|b| b.is_ascii_alphanumeric())
151 {
152 return Err(CodecError::InvalidDeviceId(value));
153 }
154 Ok(Self(value))
155 }
156
157 pub fn as_str(&self) -> &str {
158 &self.0
159 }
160}
161
162impl fmt::Display for DeviceId {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 f.write_str(&self.0)
165 }
166}
167
168impl AsRef<str> for DeviceId {
169 fn as_ref(&self) -> &str {
170 self.as_str()
171 }
172}
173
174impl Borrow<str> for DeviceId {
175 fn borrow(&self) -> &str {
176 self.as_str()
177 }
178}
179
180#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
185pub struct SessionGeneration(NonZeroU64);
186
187impl SessionGeneration {
188 pub const fn new(value: u64) -> Option<Self> {
191 match NonZeroU64::new(value) {
192 Some(value) => Some(Self(value)),
193 None => None,
194 }
195 }
196
197 pub const fn get(self) -> u64 {
198 self.0.get()
199 }
200}
201
202impl From<SessionGeneration> for u64 {
203 fn from(value: SessionGeneration) -> Self {
204 value.get()
205 }
206}
207
208impl fmt::Display for SessionGeneration {
209 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
210 self.0.fmt(formatter)
211 }
212}
213
214impl FromStr for DeviceId {
215 type Err = CodecError;
216
217 fn from_str(s: &str) -> Result<Self, Self::Err> {
218 Self::new(s)
219 }
220}
221
222impl TryFrom<String> for DeviceId {
223 type Error = CodecError;
224
225 fn try_from(value: String) -> Result<Self, Self::Error> {
226 Self::new(value)
227 }
228}
229
230macro_rules! id_newtype {
231 ($(#[$meta:meta])* $name:ident) => {
232 $(#[$meta])*
233 #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
234 pub struct $name(pub u32);
235
236 impl $name {
237 pub const fn new(value: u32) -> Self {
238 Self(value)
239 }
240
241 pub const fn get(self) -> u32 {
242 self.0
243 }
244 }
245
246 impl From<u32> for $name {
247 fn from(value: u32) -> Self {
248 Self(value)
249 }
250 }
251
252 impl From<$name> for u32 {
253 fn from(value: $name) -> Self {
254 value.0
255 }
256 }
257
258 impl fmt::Display for $name {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 self.0.fmt(f)
261 }
262 }
263 };
264}
265
266id_newtype!(LineInstance);
268id_newtype!(CallReference);
270id_newtype!(PassthroughPartyId);
272id_newtype!(AppearanceId);
274id_newtype!(ConferenceId);
276id_newtype!(ParticipantId);
278id_newtype!(ApplicationId);
280id_newtype!(TransactionId);
282
283#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
285pub struct MediaTrafficClass(u8);
286
287impl MediaTrafficClass {
288 pub const fn from_wire(value: u8) -> Self {
289 Self(value)
290 }
291
292 pub const fn from_dscp(dscp: u8) -> Option<Self> {
293 if dscp <= 63 {
294 Some(Self(dscp << 2))
295 } else {
296 None
297 }
298 }
299
300 pub const fn get(self) -> u8 {
301 self.0
302 }
303}
304
305impl From<MediaTrafficClass> for u8 {
306 fn from(value: MediaTrafficClass) -> Self {
307 value.get()
308 }
309}
310
311impl From<MediaTrafficClass> for u32 {
312 fn from(value: MediaTrafficClass) -> Self {
313 u32::from(value.get())
314 }
315}
316
317#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
323pub struct CallId(pub u64);
324
325impl CallId {
326 pub const fn new(value: u64) -> Self {
327 Self(value)
328 }
329
330 pub const fn get(self) -> u64 {
331 self.0
332 }
333}
334
335impl From<u64> for CallId {
336 fn from(value: u64) -> Self {
337 Self::new(value)
338 }
339}
340
341impl From<CallId> for u64 {
342 fn from(value: CallId) -> Self {
343 value.get()
344 }
345}
346
347impl fmt::Display for CallId {
348 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
349 self.0.fmt(formatter)
350 }
351}
352
353#[derive(Clone, Debug, Eq, PartialEq)]
358pub struct LineDefinition {
359 pub number: String,
362 pub display_name: String,
363}
364
365#[derive(Clone, Debug, Default, Eq, PartialEq)]
370pub struct CallerIdOverride {
371 pub name: Option<String>,
372 pub number: Option<String>,
373}
374
375#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
377pub enum AppearanceRingMode {
378 #[default]
379 Normal,
380 Silent,
381 Disabled,
382}
383
384#[derive(Clone, Debug, Eq, PartialEq)]
386pub struct LineAppearance {
387 pub id: AppearanceId,
389 pub instance: u32,
391 pub line: LineDefinition,
392 pub label: Option<String>,
394 pub caller_id: CallerIdOverride,
395 pub ring_mode: AppearanceRingMode,
396 pub initial_tone: Tone,
398 pub subscription_identity: Option<String>,
400 pub privacy: bool,
401}
402
403impl LineAppearance {
404 pub fn new(instance: u32, line: LineDefinition) -> Self {
406 Self {
407 id: AppearanceId::new(instance),
408 instance,
409 line,
410 label: None,
411 caller_id: CallerIdOverride::default(),
412 ring_mode: AppearanceRingMode::Normal,
413 initial_tone: Tone::InsideDial,
414 subscription_identity: None,
415 privacy: false,
416 }
417 }
418
419 pub fn display_label(&self) -> &str {
421 self.label.as_deref().unwrap_or(&self.line.display_name)
422 }
423}
424
425impl Deref for LineAppearance {
426 type Target = LineDefinition;
427
428 fn deref(&self) -> &Self::Target {
429 &self.line
430 }
431}
432
433#[derive(Clone, Debug, Eq, PartialEq)]
435pub struct SpeedDialDefinition {
436 pub instance: u32,
438 pub number: String,
439 pub display_name: String,
440}
441
442#[derive(Clone, Debug, Eq, PartialEq)]
444pub struct BlfSpeedDialDefinition {
445 pub instance: u32,
447 pub number: String,
448 pub display_name: String,
449}
450
451#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
453pub enum BlfState {
454 Idle,
455 Ringing,
456 Busy,
457 Held,
458 DoNotDisturb,
459 Unavailable,
460 #[default]
461 Unknown,
462}
463
464#[derive(Clone, Debug, Default, Eq, PartialEq)]
466pub struct BlfCallerInfo {
467 pub name: String,
468 pub number: String,
469}
470
471impl BlfCallerInfo {
472 pub fn display(&self) -> String {
474 match (self.name.trim(), self.number.trim()) {
475 ("", "") => String::new(),
476 ("", number) => number.to_owned(),
477 (name, "") => name.to_owned(),
478 (name, number) => format!("{name} ({number})"),
479 }
480 }
481}
482
483#[derive(Clone, Debug, Eq, PartialEq)]
485pub struct FeatureDefinition {
486 pub instance: u32,
488 pub label: String,
489 pub feature: crate::message::values::ButtonType,
490}
491
492#[derive(Clone, Debug, Eq, PartialEq)]
494pub struct ServiceDefinition {
495 pub instance: u32,
497 pub label: String,
498 pub url: String,
503}
504
505#[derive(Clone, Debug, Eq, PartialEq)]
507pub struct AddonModuleDefinition {
508 pub slot: u32,
510 pub device_type: crate::message::values::DeviceType,
511}
512
513impl AddonModuleDefinition {
514 pub const fn button_capacity(&self) -> Option<usize> {
516 use crate::message::values::DeviceType;
517
518 match self.device_type {
519 DeviceType::CiscoAddon7914 => Some(14),
520 DeviceType::CiscoAddon7915_12 | DeviceType::CiscoAddon7916_12 => Some(12),
521 DeviceType::CiscoAddon7915_24 | DeviceType::CiscoAddon7916_24 => Some(24),
522 DeviceType::AddonSpa500s | DeviceType::AddonSpa500ds | DeviceType::AddonSpa932ds => {
523 Some(32)
524 }
525 _ => None,
526 }
527 }
528}
529
530#[derive(Clone, Debug, Eq, PartialEq)]
535pub enum ButtonDefinition {
536 Line(LineAppearance),
537 SpeedDial(SpeedDialDefinition),
538 BlfSpeedDial(BlfSpeedDialDefinition),
539 Feature(FeatureDefinition),
540 Service(ServiceDefinition),
541 AddonModule(AddonModuleDefinition),
542 Unused,
543}
544
545#[derive(Clone, Debug, Eq, PartialEq)]
550pub struct SoftKeyProfile {
551 sets: HashMap<KeyMode, Vec<SoftKey>>,
552}
553
554impl SoftKeyProfile {
555 pub const MAX_KEYS_PER_MODE: usize = 16;
557
558 pub fn new(
563 sets: impl IntoIterator<Item = (KeyMode, Vec<SoftKey>)>,
564 ) -> Result<Self, CodecError> {
565 let profile = Self {
566 sets: sets.into_iter().collect(),
567 };
568 profile.validate()?;
569 Ok(profile)
570 }
571
572 pub fn empty() -> Self {
573 Self {
574 sets: KeyMode::ALL_KNOWN
575 .iter()
576 .copied()
577 .map(|mode| (mode, Vec::new()))
578 .collect(),
579 }
580 }
581
582 pub fn built_in() -> Self {
585 let mut profile = Self::empty();
586 profile.sets.extend([
587 (KeyMode::OnHook, vec![SoftKey::NewCall]),
588 (
589 KeyMode::Connected,
590 vec![SoftKey::Hold, SoftKey::EndCall, SoftKey::Transfer],
591 ),
592 (
593 KeyMode::OnHold,
594 vec![SoftKey::Resume, SoftKey::NewCall, SoftKey::EndCall],
595 ),
596 (KeyMode::RingIn, vec![SoftKey::Answer, SoftKey::EndCall]),
597 (KeyMode::OffHook, vec![SoftKey::EndCall]),
598 (
599 KeyMode::ConnectedTransfer,
600 vec![SoftKey::Hold, SoftKey::EndCall, SoftKey::Transfer],
601 ),
602 (
603 KeyMode::DigitsFollowing,
604 vec![SoftKey::Backspace, SoftKey::EndCall, SoftKey::Dial],
605 ),
606 (
607 KeyMode::ConnectedConference,
608 vec![SoftKey::Hold, SoftKey::EndCall],
609 ),
610 (KeyMode::RingOut, vec![SoftKey::EndCall]),
611 (
612 KeyMode::OffHookFeature,
613 vec![SoftKey::Resume, SoftKey::NewCall, SoftKey::EndCall],
614 ),
615 (
616 KeyMode::OnHookStealable,
617 vec![SoftKey::Intercept, SoftKey::NewCall],
618 ),
619 (
620 KeyMode::HoldConference,
621 vec![SoftKey::Resume, SoftKey::NewCall, SoftKey::EndCall],
622 ),
623 ]);
624 profile
625 }
626
627 pub fn actions(&self, mode: KeyMode) -> &[SoftKey] {
628 self.sets.get(&mode).map_or(&[], Vec::as_slice)
629 }
630
631 pub fn allows(&self, mode: KeyMode, action: SoftKey) -> bool {
632 action.is_known() && self.actions(mode).contains(&action)
633 }
634
635 pub fn valid_mask(&self, mode: KeyMode) -> u32 {
637 let count = self.actions(mode).len();
638 if count == 0 { 0 } else { (1_u32 << count) - 1 }
639 }
640
641 pub fn template_actions(&self) -> Vec<SoftKey> {
644 if self == &Self::built_in() {
645 return SoftKey::ALL_KNOWN.to_vec();
646 }
647 let configured: HashSet<_> = KeyMode::ALL_KNOWN
648 .iter()
649 .flat_map(|mode| self.actions(*mode).iter().copied())
650 .collect();
651 SoftKey::ALL_KNOWN
652 .iter()
653 .copied()
654 .filter(|action| configured.contains(action))
655 .collect()
656 }
657
658 pub fn validate(&self) -> Result<(), CodecError> {
660 if self.sets.len() != KeyMode::ALL_KNOWN.len()
661 || KeyMode::ALL_KNOWN
662 .iter()
663 .any(|mode| !self.sets.contains_key(mode))
664 {
665 return Err(CodecError::InvalidDefinition(
666 "soft-key profile must define every known key mode".into(),
667 ));
668 }
669 for (&mode, actions) in &self.sets {
670 if !mode.is_known() {
671 return Err(CodecError::InvalidDefinition(format!(
672 "soft-key profile contains unknown key mode {}",
673 mode.wire_value()
674 )));
675 }
676 if actions.len() > Self::MAX_KEYS_PER_MODE {
677 return Err(CodecError::InvalidDefinition(format!(
678 "soft-key mode {} contains {} actions; the protocol limit is {}",
679 mode.wire_value(),
680 actions.len(),
681 Self::MAX_KEYS_PER_MODE
682 )));
683 }
684 let mut seen = HashSet::new();
685 for &action in actions {
686 if !action.is_known() {
687 return Err(CodecError::InvalidDefinition(format!(
688 "soft-key mode {} contains unknown action {}",
689 mode.wire_value(),
690 action.wire_value()
691 )));
692 }
693 if !seen.insert(action) {
694 return Err(CodecError::InvalidDefinition(format!(
695 "soft-key mode {} repeats action {}",
696 mode.wire_value(),
697 action.wire_value()
698 )));
699 }
700 }
701 }
702 Ok(())
703 }
704}
705
706impl Default for SoftKeyProfile {
707 fn default() -> Self {
708 Self::built_in()
709 }
710}
711
712#[derive(Clone, Debug, Eq, PartialEq)]
719pub struct DeviceDefinition {
720 pub id: DeviceId,
721 pub description: String,
722 pub transport: StationTransportRequirement,
723 pub signaling_qos: Option<SignalingQos>,
726 pub buttons: Vec<ButtonDefinition>,
732 pub soft_keys: SoftKeyProfile,
734 pub ui: StationUiPolicy,
736}
737
738#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
740pub enum StationTransportRequirement {
741 Clear,
742 Secure,
743 #[default]
744 Either,
745}
746
747#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
749pub enum StationTransport {
750 Clear,
751 Secure,
752}
753
754#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
761pub struct SignalingQos {
762 pub dscp: u8,
763 pub cos: u8,
764}
765
766impl SignalingQos {
767 pub const fn new(dscp: u8, cos: u8) -> Self {
768 Self { dscp, cos }
769 }
770
771 pub(crate) fn validate(self) -> Result<(), CodecError> {
772 if self.dscp > 63 {
773 return Err(CodecError::InvalidDefinition(format!(
774 "signaling DSCP {} is outside 0..=63",
775 self.dscp
776 )));
777 }
778 if self.cos > 7 {
779 return Err(CodecError::InvalidDefinition(format!(
780 "signaling COS {} is outside 0..=7",
781 self.cos
782 )));
783 }
784 Ok(())
785 }
786}
787
788#[derive(Clone, Copy, Debug, Eq, PartialEq)]
791pub struct StationUiPolicy {
792 pub placed_calls_redial_menu: bool,
795 pub hinted_ringing_notification: bool,
798 pub speed_dial_await_further_digits: bool,
801 pub mwi_lamp_mode: crate::message::values::LampMode,
803 pub mwi_on_call: bool,
805 pub legacy_code_page: LegacyCodePage,
808}
809
810#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
813pub enum LegacyCodePage {
814 #[default]
815 Iso8859_1,
816 Ascii,
817}
818
819impl Default for StationUiPolicy {
820 fn default() -> Self {
821 Self {
822 placed_calls_redial_menu: false,
823 hinted_ringing_notification: false,
824 speed_dial_await_further_digits: false,
825 mwi_lamp_mode: crate::message::values::LampMode::On,
826 mwi_on_call: false,
827 legacy_code_page: LegacyCodePage::Iso8859_1,
828 }
829 }
830}
831
832impl DeviceDefinition {
833 pub fn validate(&self) -> Result<(), CodecError> {
835 const MAX_BUTTONS: usize = 256;
839
840 self.soft_keys.validate()?;
841 if let Some(signaling_qos) = self.signaling_qos {
842 signaling_qos.validate()?;
843 }
844
845 if self.buttons.len() > MAX_BUTTONS {
846 return Err(CodecError::InvalidDefinition(format!(
847 "device {} has {} buttons; the logical layout limit is {MAX_BUTTONS}",
848 self.id,
849 self.buttons.len()
850 )));
851 }
852
853 let mut expanded_buttons = 0_usize;
854 let mut addon_buttons_remaining = None;
855 for button in &self.buttons {
856 if let ButtonDefinition::AddonModule(addon) = button {
857 expanded_buttons += addon_buttons_remaining.take().unwrap_or_default();
858 addon_buttons_remaining = Some(addon.button_capacity().ok_or_else(|| {
859 CodecError::InvalidDefinition(format!(
860 "device {} has unsupported addon-module type {}",
861 self.id,
862 addon.device_type.wire_value()
863 ))
864 })?);
865 continue;
866 }
867 expanded_buttons += 1;
868 if let Some(remaining) = &mut addon_buttons_remaining {
869 if *remaining == 0 {
870 return Err(CodecError::InvalidDefinition(format!(
871 "device {} configures more buttons than its addon module provides",
872 self.id
873 )));
874 }
875 *remaining -= 1;
876 }
877 }
878 expanded_buttons += addon_buttons_remaining.unwrap_or_default();
879 if expanded_buttons > MAX_BUTTONS {
880 return Err(CodecError::InvalidDefinition(format!(
881 "device {} expands to {expanded_buttons} buttons; the logical layout limit is {MAX_BUTTONS}",
882 self.id
883 )));
884 }
885
886 let mut instances = HashSet::new();
887 let mut appearance_ids = HashSet::new();
888 for button in &self.buttons {
889 let Some((kind, instance)) = button.instance_key() else {
890 continue;
891 };
892 if instance == 0 {
893 return Err(CodecError::InvalidDefinition(format!(
894 "device {} has a {kind} button with instance zero",
895 self.id
896 )));
897 }
898 if kind != ButtonNamespace::AddonModule && instance > MAX_STATION_BUTTON_INSTANCE {
903 return Err(CodecError::InvalidDefinition(format!(
904 "device {} has a {kind} button with instance {instance}; maximum wire instance is {}",
905 self.id, MAX_STATION_BUTTON_INSTANCE
906 )));
907 }
908 if !instances.insert((kind, instance)) {
909 return Err(CodecError::InvalidDefinition(format!(
910 "device {} repeats {kind} button instance {instance}",
911 self.id
912 )));
913 }
914 if let ButtonDefinition::Line(appearance) = button {
915 if appearance.id.get() == 0 {
916 return Err(CodecError::InvalidDefinition(format!(
917 "device {} has a line appearance with identifier zero",
918 self.id
919 )));
920 }
921 if !appearance_ids.insert(appearance.id) {
922 return Err(CodecError::InvalidDefinition(format!(
923 "device {} repeats line appearance identifier {}",
924 self.id, appearance.id
925 )));
926 }
927 }
928 if let ButtonDefinition::Service(service) = button {
929 validate_service_definition(&self.id, service)?;
930 }
931 }
932
933 let lines: Vec<_> = self.lines().collect();
934 if lines.is_empty() {
935 return Err(CodecError::InvalidDefinition(format!(
936 "device {} has no lines",
937 self.id
938 )));
939 }
940 let permits_sparse_lines = self.buttons.iter().any(|button| {
944 matches!(
945 button,
946 ButtonDefinition::Feature(feature)
947 if feature.feature == crate::message::values::ButtonType::Mobility
948 )
949 });
950 for (expected, line) in (1_u32..).zip(lines) {
951 if !permits_sparse_lines && line.instance != expected {
952 return Err(CodecError::InvalidDefinition(format!(
953 "device {} line instances must be contiguous from 1",
954 self.id
955 )));
956 }
957 if line.number.is_empty() || line.number.len() > 24 {
958 return Err(CodecError::InvalidDefinition(format!(
959 "device {} has an invalid line number",
960 self.id
961 )));
962 }
963 }
964 Ok(())
965 }
966
967 pub fn lines(&self) -> impl Iterator<Item = &LineAppearance> {
968 self.buttons.iter().filter_map(|button| match button {
969 ButtonDefinition::Line(line) => Some(line),
970 _ => None,
971 })
972 }
973
974 pub fn line(&self, instance: u32) -> Option<&LineAppearance> {
975 self.lines().find(|line| line.instance == instance)
976 }
977
978 pub fn first_line(&self) -> Option<&LineAppearance> {
979 self.lines().next()
980 }
981
982 pub fn line_count(&self) -> usize {
983 self.lines().count()
984 }
985
986 pub(crate) fn feature_button(&self, instance: u32) -> Option<&FeatureDefinition> {
987 self.buttons.iter().find_map(|button| match button {
988 ButtonDefinition::Feature(feature) if feature.instance == instance => Some(feature),
989 _ => None,
990 })
991 }
992
993 pub(crate) fn blf_button(&self, instance: u32) -> Option<&BlfSpeedDialDefinition> {
994 self.buttons.iter().find_map(|button| match button {
995 ButtonDefinition::BlfSpeedDial(blf) if blf.instance == instance => Some(blf),
996 _ => None,
997 })
998 }
999}
1000
1001fn validate_service_definition(
1002 device: &DeviceId,
1003 service: &ServiceDefinition,
1004) -> Result<(), CodecError> {
1005 const MAX_SERVICE_URL_BYTES: usize = 255;
1006 const MAX_SERVICE_LABEL_BYTES: usize = 39;
1007 const MAX_SERVICE_PARAMETERS: usize = 32;
1008 const MAX_SERVICE_PARAMETER_BYTES: usize = 128;
1009
1010 if service.label.is_empty()
1011 || service.label.len() > MAX_SERVICE_LABEL_BYTES
1012 || service.label.chars().any(char::is_control)
1013 {
1014 return Err(CodecError::InvalidDefinition(format!(
1015 "device {device} has an invalid service label"
1016 )));
1017 }
1018 if service.url.is_empty()
1019 || service.url.len() > MAX_SERVICE_URL_BYTES
1020 || service.url.chars().any(char::is_control)
1021 {
1022 return Err(CodecError::InvalidDefinition(format!(
1023 "device {device} has an invalid service URL"
1024 )));
1025 }
1026 let url = url::Url::parse(&service.url).map_err(|_| {
1027 CodecError::InvalidDefinition(format!("device {device} has a malformed service URL"))
1028 })?;
1029 if !matches!(url.scheme(), "http" | "https")
1030 || url.host_str().is_none()
1031 || url.fragment().is_some()
1032 {
1033 return Err(CodecError::InvalidDefinition(format!(
1034 "device {device} service URL must be HTTP(S) without a fragment"
1035 )));
1036 }
1037 let parameters = url.query_pairs().collect::<Vec<_>>();
1038 if parameters.len() > MAX_SERVICE_PARAMETERS
1039 || parameters.iter().any(|(name, value)| {
1040 name.is_empty()
1041 || name.len() > MAX_SERVICE_PARAMETER_BYTES
1042 || value.len() > MAX_SERVICE_PARAMETER_BYTES
1043 || name.chars().chain(value.chars()).any(char::is_control)
1044 })
1045 {
1046 return Err(CodecError::InvalidDefinition(format!(
1047 "device {device} service URL has invalid or excessive query parameters"
1048 )));
1049 }
1050 Ok(())
1051}
1052
1053impl ButtonDefinition {
1054 fn instance_key(&self) -> Option<(ButtonNamespace, u32)> {
1055 match self {
1056 Self::Line(definition) => Some((ButtonNamespace::Line, definition.instance)),
1057 Self::SpeedDial(definition) => Some((ButtonNamespace::SpeedDial, definition.instance)),
1058 Self::BlfSpeedDial(definition) => Some((ButtonNamespace::Feature, definition.instance)),
1059 Self::Feature(definition) => Some((ButtonNamespace::Feature, definition.instance)),
1060 Self::Service(definition) => Some((ButtonNamespace::Service, definition.instance)),
1061 Self::AddonModule(definition) => Some((ButtonNamespace::AddonModule, definition.slot)),
1062 Self::Unused => None,
1063 }
1064 }
1065}
1066
1067#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1068enum ButtonNamespace {
1069 Line,
1070 SpeedDial,
1071 Feature,
1072 Service,
1073 AddonModule,
1074}
1075
1076impl fmt::Display for ButtonNamespace {
1077 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1078 formatter.write_str(match self {
1079 Self::Line => "line",
1080 Self::SpeedDial => "speed dial",
1081 Self::Feature => "feature",
1082 Self::Service => "service URL",
1083 Self::AddonModule => "addon module",
1084 })
1085 }
1086}
1087
1088#[derive(Clone, Debug, Eq, PartialEq)]
1093pub struct DeviceRegistration {
1094 pub id: DeviceId,
1095 pub peer: SocketAddr,
1096 pub transport: StationTransport,
1097 pub reported_address: Option<Ipv4Addr>,
1098 pub reported_ipv6_address: Option<Ipv6Addr>,
1099 pub device_type: DeviceType,
1100 pub protocol: ProtocolVersion,
1101 pub firmware: String,
1102}
1103
1104impl DeviceRegistration {
1105 pub fn reported_address_for_peer(&self) -> Option<IpAddr> {
1108 match self.peer.ip() {
1109 IpAddr::V4(_) => self.reported_address.map(IpAddr::V4),
1110 IpAddr::V6(peer) => peer.to_ipv4_mapped().map_or_else(
1111 || self.reported_ipv6_address.map(IpAddr::V6),
1112 |_| self.reported_address.map(IpAddr::V4),
1113 ),
1114 }
1115 }
1116}
1117
1118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1120pub enum CallDirection {
1121 Inbound,
1122 Outbound,
1123}
1124
1125impl From<CallDirection> for CallType {
1126 fn from(value: CallDirection) -> Self {
1127 match value {
1128 CallDirection::Inbound => Self::Inbound,
1129 CallDirection::Outbound => Self::Outbound,
1130 }
1131 }
1132}
1133
1134#[derive(Clone, Debug, Eq, PartialEq)]
1140pub struct CallInfo {
1141 pub direction: CallDirection,
1142 pub calling_name: String,
1143 pub calling_number: String,
1144 pub called_name: String,
1145 pub called_number: String,
1146 pub original_called_name: String,
1147 pub original_called_number: String,
1148 pub last_redirecting_name: String,
1149 pub last_redirecting_number: String,
1150 pub original_redirect_reason: u32,
1151 pub last_redirect_reason: u32,
1152 pub party_restrictions: u32,
1154}
1155
1156impl Default for CallInfo {
1157 fn default() -> Self {
1158 Self {
1159 direction: CallDirection::Outbound,
1160 calling_name: String::new(),
1161 calling_number: String::new(),
1162 called_name: String::new(),
1163 called_number: String::new(),
1164 original_called_name: String::new(),
1165 original_called_number: String::new(),
1166 last_redirecting_name: String::new(),
1167 last_redirecting_number: String::new(),
1168 original_redirect_reason: 0,
1169 last_redirect_reason: 0,
1170 party_restrictions: 0,
1171 }
1172 }
1173}
1174
1175pub const DEFAULT_AUDIO_PACKET_MS: u32 = 20;
1177pub const DEFAULT_AUDIO_MAX_FRAMES_PER_PACKET: u32 = 1;
1179
1180#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1183pub struct AudioProcessingPolicy {
1184 pub echo_cancellation: EchoCancellation,
1185 pub silence_suppression: SilenceSuppression,
1186}
1187
1188impl Default for AudioProcessingPolicy {
1189 fn default() -> Self {
1190 Self {
1191 echo_cancellation: EchoCancellation::On,
1192 silence_suppression: SilenceSuppression::Off,
1193 }
1194 }
1195}
1196
1197#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1202pub struct MediaEndpoint {
1203 pub address: IpAddr,
1204 pub rtp_port: u16,
1205 pub rtcp_port: u16,
1206 pub codec: Codec,
1207 pub packet_ms: u32,
1208 pub max_frames_per_packet: u32,
1209 pub telephone_event_payload: u8,
1211}
1212
1213#[cfg(test)]
1214mod tests {
1215 use super::*;
1216
1217 #[test]
1218 fn identifiers_are_explicit_and_lossless() {
1219 let reference = CallReference::new(42);
1220 assert_eq!(reference.get(), 42);
1221 assert_eq!(u32::from(reference), 42);
1222
1223 let appearance = AppearanceId::new(7);
1224 assert_eq!(appearance.get(), 7);
1225 assert_eq!(u32::from(appearance), 7);
1226
1227 let conference = ConferenceId::new(9);
1228 assert_eq!(conference.get(), 9);
1229
1230 let participant = ParticipantId::new(11);
1231 assert_eq!(participant.get(), 11);
1232 }
1233
1234 #[test]
1235 fn device_id_is_canonicalized() {
1236 let id: DeviceId = " sep001122334455 ".parse().unwrap();
1237 assert_eq!(id.as_str(), "SEP001122334455");
1238 assert_eq!(id.as_ref(), "SEP001122334455");
1239
1240 let mut devices = HashMap::new();
1241 devices.insert(id, "desk");
1242 assert_eq!(devices.get("SEP001122334455"), Some(&"desk"));
1243 }
1244
1245 #[test]
1246 fn registration_selects_the_report_matching_the_effective_peer_family() {
1247 let registration = DeviceRegistration {
1248 id: DeviceId::new("SEP001122334455").unwrap(),
1249 peer: "[2001:db8::20]:2000".parse().unwrap(),
1250 transport: StationTransport::Clear,
1251 reported_address: Some("192.0.2.20".parse().unwrap()),
1252 reported_ipv6_address: Some("2001:db8::20".parse().unwrap()),
1253 device_type: DeviceType::Cisco7962,
1254 protocol: ProtocolVersion::V22,
1255 firmware: "test".into(),
1256 };
1257 assert_eq!(
1258 registration.reported_address_for_peer(),
1259 Some("2001:db8::20".parse().unwrap())
1260 );
1261
1262 let mapped = DeviceRegistration {
1263 peer: "[::ffff:192.0.2.20]:2000".parse().unwrap(),
1264 ..registration
1265 };
1266 assert_eq!(
1267 mapped.reported_address_for_peer(),
1268 Some("192.0.2.20".parse().unwrap())
1269 );
1270 }
1271
1272 fn line_button(instance: u32, number: &str) -> ButtonDefinition {
1273 ButtonDefinition::Line(LineAppearance::new(
1274 instance,
1275 LineDefinition {
1276 number: number.into(),
1277 display_name: number.into(),
1278 },
1279 ))
1280 }
1281
1282 #[test]
1283 fn station_definition_accepts_non_line_buttons_between_lines() {
1284 let definition = DeviceDefinition {
1285 id: DeviceId::new("SEP001122334455").unwrap(),
1286 description: "Desk".into(),
1287 transport: StationTransportRequirement::Either,
1288 signaling_qos: None,
1289 buttons: vec![
1290 line_button(1, "1001"),
1291 ButtonDefinition::Unused,
1292 ButtonDefinition::SpeedDial(SpeedDialDefinition {
1293 instance: 1,
1294 number: "2001".into(),
1295 display_name: "Warehouse".into(),
1296 }),
1297 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1298 instance: 1,
1299 number: "2002".into(),
1300 display_name: "Dispatch".into(),
1301 }),
1302 ButtonDefinition::Feature(FeatureDefinition {
1303 instance: 2,
1304 label: "DND".into(),
1305 feature: crate::message::values::ButtonType::DoNotDisturb,
1306 }),
1307 ButtonDefinition::Service(ServiceDefinition {
1308 instance: 1,
1309 label: "Directory".into(),
1310 url: "http://pbx.test/directory".into(),
1311 }),
1312 ButtonDefinition::AddonModule(AddonModuleDefinition {
1313 slot: 1,
1314 device_type: crate::message::values::DeviceType::CiscoAddon7914,
1315 }),
1316 line_button(2, "1002"),
1317 ],
1318 soft_keys: SoftKeyProfile::default(),
1319 ui: StationUiPolicy::default(),
1320 };
1321
1322 definition.validate().unwrap();
1323 assert_eq!(definition.line_count(), 2);
1324 assert_eq!(definition.line(2).unwrap().number, "1002");
1325 }
1326
1327 #[test]
1328 fn station_definition_rejects_invalid_signaling_markings() {
1329 let mut definition = DeviceDefinition {
1330 id: DeviceId::new("SEP001122334455").unwrap(),
1331 description: "Desk".into(),
1332 transport: StationTransportRequirement::Either,
1333 signaling_qos: Some(SignalingQos::new(64, 0)),
1334 buttons: vec![line_button(1, "1001")],
1335 soft_keys: SoftKeyProfile::default(),
1336 ui: StationUiPolicy::default(),
1337 };
1338
1339 assert!(matches!(
1340 definition.validate(),
1341 Err(CodecError::InvalidDefinition(message)) if message.contains("DSCP 64")
1342 ));
1343
1344 definition.signaling_qos = Some(SignalingQos::new(26, 8));
1345 assert!(matches!(
1346 definition.validate(),
1347 Err(CodecError::InvalidDefinition(message)) if message.contains("COS 8")
1348 ));
1349 }
1350
1351 #[test]
1352 fn line_appearance_keeps_logical_and_device_specific_state_separate() {
1353 let logical = LineDefinition {
1354 number: "1001".into(),
1355 display_name: "Reception".into(),
1356 };
1357 let mut appearance = LineAppearance::new(2, logical.clone());
1358 appearance.label = Some("Private key".into());
1359 appearance.caller_id = CallerIdOverride {
1360 name: Some("Private desk".into()),
1361 number: None,
1362 };
1363 appearance.ring_mode = AppearanceRingMode::Silent;
1364 appearance.subscription_identity = Some("1001@internal".into());
1365 appearance.privacy = true;
1366
1367 assert_eq!(appearance.line, logical);
1368 assert_eq!(appearance.display_label(), "Private key");
1369 assert_eq!(appearance.number, "1001");
1370 assert_eq!(appearance.id, AppearanceId::new(2));
1371 }
1372
1373 #[test]
1374 fn station_definition_rejects_zero_and_duplicate_typed_instances() {
1375 let definition = DeviceDefinition {
1376 id: DeviceId::new("SEP001122334455").unwrap(),
1377 description: "Desk".into(),
1378 transport: StationTransportRequirement::Either,
1379 signaling_qos: None,
1380 buttons: vec![line_button(1, "1001"), line_button(1, "1002")],
1381 soft_keys: SoftKeyProfile::default(),
1382 ui: StationUiPolicy::default(),
1383 };
1384 assert!(matches!(
1385 definition.validate(),
1386 Err(CodecError::InvalidDefinition(message))
1387 if message.contains("repeats line button instance 1")
1388 ));
1389
1390 let definition = DeviceDefinition {
1391 id: DeviceId::new("SEP001122334455").unwrap(),
1392 description: "Desk".into(),
1393 transport: StationTransportRequirement::Either,
1394 signaling_qos: None,
1395 buttons: vec![
1396 line_button(1, "1001"),
1397 ButtonDefinition::Feature(FeatureDefinition {
1398 instance: 0,
1399 label: "DND".into(),
1400 feature: crate::message::values::ButtonType::DoNotDisturb,
1401 }),
1402 ],
1403 soft_keys: SoftKeyProfile::default(),
1404 ui: StationUiPolicy::default(),
1405 };
1406 assert!(matches!(
1407 definition.validate(),
1408 Err(CodecError::InvalidDefinition(message))
1409 if message.contains("feature button with instance zero")
1410 ));
1411
1412 let definition = DeviceDefinition {
1413 id: DeviceId::new("SEP001122334455").unwrap(),
1414 description: "Desk".into(),
1415 transport: StationTransportRequirement::Either,
1416 signaling_qos: None,
1417 buttons: vec![
1418 line_button(1, "1001"),
1419 ButtonDefinition::Feature(FeatureDefinition {
1420 instance: 1,
1421 label: "DND".into(),
1422 feature: crate::message::values::ButtonType::DoNotDisturb,
1423 }),
1424 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1425 instance: 1,
1426 number: "2001".into(),
1427 display_name: "Warehouse".into(),
1428 }),
1429 ],
1430 soft_keys: SoftKeyProfile::default(),
1431 ui: StationUiPolicy::default(),
1432 };
1433 assert!(matches!(
1434 definition.validate(),
1435 Err(CodecError::InvalidDefinition(message))
1436 if message.contains("repeats feature button instance 1")
1437 ));
1438
1439 let distinct_namespaces = DeviceDefinition {
1440 id: DeviceId::new("SEP001122334455").unwrap(),
1441 description: "Desk".into(),
1442 transport: StationTransportRequirement::Either,
1443 signaling_qos: None,
1444 buttons: vec![
1445 line_button(1, "1001"),
1446 ButtonDefinition::SpeedDial(SpeedDialDefinition {
1447 instance: 7,
1448 number: "2001".into(),
1449 display_name: "Warehouse".into(),
1450 }),
1451 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1452 instance: 7,
1453 number: "2002".into(),
1454 display_name: "Dispatch".into(),
1455 }),
1456 ],
1457 soft_keys: SoftKeyProfile::default(),
1458 ui: StationUiPolicy::default(),
1459 };
1460 distinct_namespaces.validate().unwrap();
1461 }
1462
1463 #[test]
1464 fn station_definition_enforces_one_byte_wire_instances_for_each_button_family() {
1465 let definition_with = |button| DeviceDefinition {
1466 id: DeviceId::new("SEP001122334455").unwrap(),
1467 description: "Desk".into(),
1468 transport: StationTransportRequirement::Either,
1469 signaling_qos: None,
1470 buttons: vec![line_button(1, "1001"), button],
1471 soft_keys: SoftKeyProfile::default(),
1472 ui: StationUiPolicy::default(),
1473 };
1474 let buttons = |instance| {
1475 [
1476 ButtonDefinition::SpeedDial(SpeedDialDefinition {
1477 instance,
1478 number: "2001".into(),
1479 display_name: "Speed".into(),
1480 }),
1481 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1482 instance,
1483 number: "2002".into(),
1484 display_name: "BLF".into(),
1485 }),
1486 ButtonDefinition::Feature(FeatureDefinition {
1487 instance,
1488 label: "DND".into(),
1489 feature: crate::message::values::ButtonType::DoNotDisturb,
1490 }),
1491 ButtonDefinition::Service(ServiceDefinition {
1492 instance,
1493 label: "Directory".into(),
1494 url: "https://pbx.example/directory".into(),
1495 }),
1496 ]
1497 };
1498
1499 for button in buttons(255) {
1500 definition_with(button).validate().unwrap();
1501 }
1502 for button in buttons(256) {
1503 assert!(matches!(
1504 definition_with(button).validate(),
1505 Err(CodecError::InvalidDefinition(message))
1506 if message.contains("maximum wire instance is 255")
1507 ));
1508 }
1509
1510 let line_255 = DeviceDefinition {
1511 buttons: vec![line_button(255, "1001")],
1512 ..definition_with(ButtonDefinition::Unused)
1513 };
1514 let mut line_255 = line_255;
1516 line_255.buttons.insert(
1517 0,
1518 ButtonDefinition::Feature(FeatureDefinition {
1519 instance: 1,
1520 label: "Mobility".into(),
1521 feature: crate::message::values::ButtonType::Mobility,
1522 }),
1523 );
1524 line_255.validate().unwrap();
1525 line_255.buttons[1] = line_button(256, "1001");
1526 assert!(matches!(
1527 line_255.validate(),
1528 Err(CodecError::InvalidDefinition(message))
1529 if message.contains("maximum wire instance is 255")
1530 ));
1531 }
1532
1533 #[test]
1534 fn blf_defaults_to_unknown() {
1535 assert_eq!(BlfState::default(), BlfState::Unknown);
1536 }
1537
1538 #[test]
1539 fn station_definition_enforces_bounded_logical_button_limit() {
1540 let definition = DeviceDefinition {
1541 id: DeviceId::new("SEP001122334455").unwrap(),
1542 description: "Desk".into(),
1543 transport: StationTransportRequirement::Either,
1544 signaling_qos: None,
1545 buttons: std::iter::once(line_button(1, "1001"))
1546 .chain(std::iter::repeat_n(ButtonDefinition::Unused, 256))
1547 .collect(),
1548 soft_keys: SoftKeyProfile::default(),
1549 ui: StationUiPolicy::default(),
1550 };
1551 assert!(matches!(
1552 definition.validate(),
1553 Err(CodecError::InvalidDefinition(message))
1554 if message.contains("logical layout limit is 256")
1555 ));
1556 }
1557
1558 #[test]
1559 fn service_urls_require_bounded_http_parameters() {
1560 let service_device = |url: &str| DeviceDefinition {
1561 id: DeviceId::new("SEP001122334455").unwrap(),
1562 description: "Desk".into(),
1563 transport: StationTransportRequirement::Either,
1564 signaling_qos: None,
1565 buttons: vec![
1566 line_button(1, "1001"),
1567 ButtonDefinition::Service(ServiceDefinition {
1568 instance: 1,
1569 label: "Directory".into(),
1570 url: url.into(),
1571 }),
1572 ],
1573 soft_keys: SoftKeyProfile::default(),
1574 ui: StationUiPolicy::default(),
1575 };
1576
1577 service_device("https://pbx.example/sccp/directory?q=Fran%C3%A7ois&page=2")
1578 .validate()
1579 .unwrap();
1580 service_device("https://user:secret@pbx.example/service")
1581 .validate()
1582 .unwrap();
1583 for invalid in [
1584 "file:///etc/passwd",
1585 "https://pbx.example/service#private",
1586 "https://pbx.example/service?=missing-name",
1587 "not a URL",
1588 ] {
1589 let error = service_device(invalid).validate().unwrap_err().to_string();
1590 assert!(!error.contains(invalid));
1591 }
1592 let excessive = format!(
1593 "https://pbx.example/service?{}",
1594 (0..33)
1595 .map(|index| format!("p{index}=v"))
1596 .collect::<Vec<_>>()
1597 .join("&")
1598 );
1599 assert!(service_device(&excessive).validate().is_err());
1600 }
1601
1602 #[test]
1603 fn soft_key_profiles_require_every_mode_and_unique_known_actions() {
1604 assert!(matches!(
1605 SoftKeyProfile::new([(KeyMode::OnHook, vec![SoftKey::NewCall])]),
1606 Err(CodecError::InvalidDefinition(message))
1607 if message.contains("every known key mode")
1608 ));
1609
1610 let duplicate = SoftKeyProfile::new(KeyMode::ALL_KNOWN.iter().copied().map(|mode| {
1611 (
1612 mode,
1613 if mode == KeyMode::Connected {
1614 vec![SoftKey::Hold, SoftKey::Hold]
1615 } else {
1616 Vec::new()
1617 },
1618 )
1619 }));
1620 assert!(matches!(
1621 duplicate,
1622 Err(CodecError::InvalidDefinition(message)) if message.contains("repeats action")
1623 ));
1624 }
1625}