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 = 0;
1178
1179#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1182pub struct AudioProcessingPolicy {
1183 pub echo_cancellation: EchoCancellation,
1184 pub silence_suppression: SilenceSuppression,
1185}
1186
1187impl Default for AudioProcessingPolicy {
1188 fn default() -> Self {
1189 Self {
1190 echo_cancellation: EchoCancellation::On,
1191 silence_suppression: SilenceSuppression::Off,
1192 }
1193 }
1194}
1195
1196#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1201pub struct MediaEndpoint {
1202 pub address: IpAddr,
1203 pub rtp_port: u16,
1204 pub rtcp_port: u16,
1205 pub codec: Codec,
1206 pub packet_ms: u32,
1207 pub max_frames_per_packet: u32,
1208 pub telephone_event_payload: u8,
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214 use super::*;
1215
1216 #[test]
1217 fn identifiers_are_explicit_and_lossless() {
1218 let reference = CallReference::new(42);
1219 assert_eq!(reference.get(), 42);
1220 assert_eq!(u32::from(reference), 42);
1221
1222 let appearance = AppearanceId::new(7);
1223 assert_eq!(appearance.get(), 7);
1224 assert_eq!(u32::from(appearance), 7);
1225
1226 let conference = ConferenceId::new(9);
1227 assert_eq!(conference.get(), 9);
1228
1229 let participant = ParticipantId::new(11);
1230 assert_eq!(participant.get(), 11);
1231 }
1232
1233 #[test]
1234 fn device_id_is_canonicalized() {
1235 let id: DeviceId = " sep001122334455 ".parse().unwrap();
1236 assert_eq!(id.as_str(), "SEP001122334455");
1237 assert_eq!(id.as_ref(), "SEP001122334455");
1238
1239 let mut devices = HashMap::new();
1240 devices.insert(id, "desk");
1241 assert_eq!(devices.get("SEP001122334455"), Some(&"desk"));
1242 }
1243
1244 #[test]
1245 fn registration_selects_the_report_matching_the_effective_peer_family() {
1246 let registration = DeviceRegistration {
1247 id: DeviceId::new("SEP001122334455").unwrap(),
1248 peer: "[2001:db8::20]:2000".parse().unwrap(),
1249 transport: StationTransport::Clear,
1250 reported_address: Some("192.0.2.20".parse().unwrap()),
1251 reported_ipv6_address: Some("2001:db8::20".parse().unwrap()),
1252 device_type: DeviceType::Cisco7962,
1253 protocol: ProtocolVersion::V22,
1254 firmware: "test".into(),
1255 };
1256 assert_eq!(
1257 registration.reported_address_for_peer(),
1258 Some("2001:db8::20".parse().unwrap())
1259 );
1260
1261 let mapped = DeviceRegistration {
1262 peer: "[::ffff:192.0.2.20]:2000".parse().unwrap(),
1263 ..registration
1264 };
1265 assert_eq!(
1266 mapped.reported_address_for_peer(),
1267 Some("192.0.2.20".parse().unwrap())
1268 );
1269 }
1270
1271 fn line_button(instance: u32, number: &str) -> ButtonDefinition {
1272 ButtonDefinition::Line(LineAppearance::new(
1273 instance,
1274 LineDefinition {
1275 number: number.into(),
1276 display_name: number.into(),
1277 },
1278 ))
1279 }
1280
1281 #[test]
1282 fn station_definition_accepts_non_line_buttons_between_lines() {
1283 let definition = DeviceDefinition {
1284 id: DeviceId::new("SEP001122334455").unwrap(),
1285 description: "Desk".into(),
1286 transport: StationTransportRequirement::Either,
1287 signaling_qos: None,
1288 buttons: vec![
1289 line_button(1, "1001"),
1290 ButtonDefinition::Unused,
1291 ButtonDefinition::SpeedDial(SpeedDialDefinition {
1292 instance: 1,
1293 number: "2001".into(),
1294 display_name: "Warehouse".into(),
1295 }),
1296 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1297 instance: 1,
1298 number: "2002".into(),
1299 display_name: "Dispatch".into(),
1300 }),
1301 ButtonDefinition::Feature(FeatureDefinition {
1302 instance: 2,
1303 label: "DND".into(),
1304 feature: crate::message::values::ButtonType::DoNotDisturb,
1305 }),
1306 ButtonDefinition::Service(ServiceDefinition {
1307 instance: 1,
1308 label: "Directory".into(),
1309 url: "http://pbx.test/directory".into(),
1310 }),
1311 ButtonDefinition::AddonModule(AddonModuleDefinition {
1312 slot: 1,
1313 device_type: crate::message::values::DeviceType::CiscoAddon7914,
1314 }),
1315 line_button(2, "1002"),
1316 ],
1317 soft_keys: SoftKeyProfile::default(),
1318 ui: StationUiPolicy::default(),
1319 };
1320
1321 definition.validate().unwrap();
1322 assert_eq!(definition.line_count(), 2);
1323 assert_eq!(definition.line(2).unwrap().number, "1002");
1324 }
1325
1326 #[test]
1327 fn station_definition_rejects_invalid_signaling_markings() {
1328 let mut definition = DeviceDefinition {
1329 id: DeviceId::new("SEP001122334455").unwrap(),
1330 description: "Desk".into(),
1331 transport: StationTransportRequirement::Either,
1332 signaling_qos: Some(SignalingQos::new(64, 0)),
1333 buttons: vec![line_button(1, "1001")],
1334 soft_keys: SoftKeyProfile::default(),
1335 ui: StationUiPolicy::default(),
1336 };
1337
1338 assert!(matches!(
1339 definition.validate(),
1340 Err(CodecError::InvalidDefinition(message)) if message.contains("DSCP 64")
1341 ));
1342
1343 definition.signaling_qos = Some(SignalingQos::new(26, 8));
1344 assert!(matches!(
1345 definition.validate(),
1346 Err(CodecError::InvalidDefinition(message)) if message.contains("COS 8")
1347 ));
1348 }
1349
1350 #[test]
1351 fn line_appearance_keeps_logical_and_device_specific_state_separate() {
1352 let logical = LineDefinition {
1353 number: "1001".into(),
1354 display_name: "Reception".into(),
1355 };
1356 let mut appearance = LineAppearance::new(2, logical.clone());
1357 appearance.label = Some("Private key".into());
1358 appearance.caller_id = CallerIdOverride {
1359 name: Some("Private desk".into()),
1360 number: None,
1361 };
1362 appearance.ring_mode = AppearanceRingMode::Silent;
1363 appearance.subscription_identity = Some("1001@internal".into());
1364 appearance.privacy = true;
1365
1366 assert_eq!(appearance.line, logical);
1367 assert_eq!(appearance.display_label(), "Private key");
1368 assert_eq!(appearance.number, "1001");
1369 assert_eq!(appearance.id, AppearanceId::new(2));
1370 }
1371
1372 #[test]
1373 fn station_definition_rejects_zero_and_duplicate_typed_instances() {
1374 let definition = DeviceDefinition {
1375 id: DeviceId::new("SEP001122334455").unwrap(),
1376 description: "Desk".into(),
1377 transport: StationTransportRequirement::Either,
1378 signaling_qos: None,
1379 buttons: vec![line_button(1, "1001"), line_button(1, "1002")],
1380 soft_keys: SoftKeyProfile::default(),
1381 ui: StationUiPolicy::default(),
1382 };
1383 assert!(matches!(
1384 definition.validate(),
1385 Err(CodecError::InvalidDefinition(message))
1386 if message.contains("repeats line button instance 1")
1387 ));
1388
1389 let definition = DeviceDefinition {
1390 id: DeviceId::new("SEP001122334455").unwrap(),
1391 description: "Desk".into(),
1392 transport: StationTransportRequirement::Either,
1393 signaling_qos: None,
1394 buttons: vec![
1395 line_button(1, "1001"),
1396 ButtonDefinition::Feature(FeatureDefinition {
1397 instance: 0,
1398 label: "DND".into(),
1399 feature: crate::message::values::ButtonType::DoNotDisturb,
1400 }),
1401 ],
1402 soft_keys: SoftKeyProfile::default(),
1403 ui: StationUiPolicy::default(),
1404 };
1405 assert!(matches!(
1406 definition.validate(),
1407 Err(CodecError::InvalidDefinition(message))
1408 if message.contains("feature button with instance zero")
1409 ));
1410
1411 let definition = DeviceDefinition {
1412 id: DeviceId::new("SEP001122334455").unwrap(),
1413 description: "Desk".into(),
1414 transport: StationTransportRequirement::Either,
1415 signaling_qos: None,
1416 buttons: vec![
1417 line_button(1, "1001"),
1418 ButtonDefinition::Feature(FeatureDefinition {
1419 instance: 1,
1420 label: "DND".into(),
1421 feature: crate::message::values::ButtonType::DoNotDisturb,
1422 }),
1423 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1424 instance: 1,
1425 number: "2001".into(),
1426 display_name: "Warehouse".into(),
1427 }),
1428 ],
1429 soft_keys: SoftKeyProfile::default(),
1430 ui: StationUiPolicy::default(),
1431 };
1432 assert!(matches!(
1433 definition.validate(),
1434 Err(CodecError::InvalidDefinition(message))
1435 if message.contains("repeats feature button instance 1")
1436 ));
1437
1438 let distinct_namespaces = DeviceDefinition {
1439 id: DeviceId::new("SEP001122334455").unwrap(),
1440 description: "Desk".into(),
1441 transport: StationTransportRequirement::Either,
1442 signaling_qos: None,
1443 buttons: vec![
1444 line_button(1, "1001"),
1445 ButtonDefinition::SpeedDial(SpeedDialDefinition {
1446 instance: 7,
1447 number: "2001".into(),
1448 display_name: "Warehouse".into(),
1449 }),
1450 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1451 instance: 7,
1452 number: "2002".into(),
1453 display_name: "Dispatch".into(),
1454 }),
1455 ],
1456 soft_keys: SoftKeyProfile::default(),
1457 ui: StationUiPolicy::default(),
1458 };
1459 distinct_namespaces.validate().unwrap();
1460 }
1461
1462 #[test]
1463 fn station_definition_enforces_one_byte_wire_instances_for_each_button_family() {
1464 let definition_with = |button| DeviceDefinition {
1465 id: DeviceId::new("SEP001122334455").unwrap(),
1466 description: "Desk".into(),
1467 transport: StationTransportRequirement::Either,
1468 signaling_qos: None,
1469 buttons: vec![line_button(1, "1001"), button],
1470 soft_keys: SoftKeyProfile::default(),
1471 ui: StationUiPolicy::default(),
1472 };
1473 let buttons = |instance| {
1474 [
1475 ButtonDefinition::SpeedDial(SpeedDialDefinition {
1476 instance,
1477 number: "2001".into(),
1478 display_name: "Speed".into(),
1479 }),
1480 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1481 instance,
1482 number: "2002".into(),
1483 display_name: "BLF".into(),
1484 }),
1485 ButtonDefinition::Feature(FeatureDefinition {
1486 instance,
1487 label: "DND".into(),
1488 feature: crate::message::values::ButtonType::DoNotDisturb,
1489 }),
1490 ButtonDefinition::Service(ServiceDefinition {
1491 instance,
1492 label: "Directory".into(),
1493 url: "https://pbx.example/directory".into(),
1494 }),
1495 ]
1496 };
1497
1498 for button in buttons(255) {
1499 definition_with(button).validate().unwrap();
1500 }
1501 for button in buttons(256) {
1502 assert!(matches!(
1503 definition_with(button).validate(),
1504 Err(CodecError::InvalidDefinition(message))
1505 if message.contains("maximum wire instance is 255")
1506 ));
1507 }
1508
1509 let line_255 = DeviceDefinition {
1510 buttons: vec![line_button(255, "1001")],
1511 ..definition_with(ButtonDefinition::Unused)
1512 };
1513 let mut line_255 = line_255;
1515 line_255.buttons.insert(
1516 0,
1517 ButtonDefinition::Feature(FeatureDefinition {
1518 instance: 1,
1519 label: "Mobility".into(),
1520 feature: crate::message::values::ButtonType::Mobility,
1521 }),
1522 );
1523 line_255.validate().unwrap();
1524 line_255.buttons[1] = line_button(256, "1001");
1525 assert!(matches!(
1526 line_255.validate(),
1527 Err(CodecError::InvalidDefinition(message))
1528 if message.contains("maximum wire instance is 255")
1529 ));
1530 }
1531
1532 #[test]
1533 fn blf_defaults_to_unknown() {
1534 assert_eq!(BlfState::default(), BlfState::Unknown);
1535 }
1536
1537 #[test]
1538 fn station_definition_enforces_bounded_logical_button_limit() {
1539 let definition = DeviceDefinition {
1540 id: DeviceId::new("SEP001122334455").unwrap(),
1541 description: "Desk".into(),
1542 transport: StationTransportRequirement::Either,
1543 signaling_qos: None,
1544 buttons: std::iter::once(line_button(1, "1001"))
1545 .chain(std::iter::repeat_n(ButtonDefinition::Unused, 256))
1546 .collect(),
1547 soft_keys: SoftKeyProfile::default(),
1548 ui: StationUiPolicy::default(),
1549 };
1550 assert!(matches!(
1551 definition.validate(),
1552 Err(CodecError::InvalidDefinition(message))
1553 if message.contains("logical layout limit is 256")
1554 ));
1555 }
1556
1557 #[test]
1558 fn service_urls_require_bounded_http_parameters() {
1559 let service_device = |url: &str| DeviceDefinition {
1560 id: DeviceId::new("SEP001122334455").unwrap(),
1561 description: "Desk".into(),
1562 transport: StationTransportRequirement::Either,
1563 signaling_qos: None,
1564 buttons: vec![
1565 line_button(1, "1001"),
1566 ButtonDefinition::Service(ServiceDefinition {
1567 instance: 1,
1568 label: "Directory".into(),
1569 url: url.into(),
1570 }),
1571 ],
1572 soft_keys: SoftKeyProfile::default(),
1573 ui: StationUiPolicy::default(),
1574 };
1575
1576 service_device("https://pbx.example/sccp/directory?q=Fran%C3%A7ois&page=2")
1577 .validate()
1578 .unwrap();
1579 service_device("https://user:secret@pbx.example/service")
1580 .validate()
1581 .unwrap();
1582 for invalid in [
1583 "file:///etc/passwd",
1584 "https://pbx.example/service#private",
1585 "https://pbx.example/service?=missing-name",
1586 "not a URL",
1587 ] {
1588 let error = service_device(invalid).validate().unwrap_err().to_string();
1589 assert!(!error.contains(invalid));
1590 }
1591 let excessive = format!(
1592 "https://pbx.example/service?{}",
1593 (0..33)
1594 .map(|index| format!("p{index}=v"))
1595 .collect::<Vec<_>>()
1596 .join("&")
1597 );
1598 assert!(service_device(&excessive).validate().is_err());
1599 }
1600
1601 #[test]
1602 fn soft_key_profiles_require_every_mode_and_unique_known_actions() {
1603 assert!(matches!(
1604 SoftKeyProfile::new([(KeyMode::OnHook, vec![SoftKey::NewCall])]),
1605 Err(CodecError::InvalidDefinition(message))
1606 if message.contains("every known key mode")
1607 ));
1608
1609 let duplicate = SoftKeyProfile::new(KeyMode::ALL_KNOWN.iter().copied().map(|mode| {
1610 (
1611 mode,
1612 if mode == KeyMode::Connected {
1613 vec![SoftKey::Hold, SoftKey::Hold]
1614 } else {
1615 Vec::new()
1616 },
1617 )
1618 }));
1619 assert!(matches!(
1620 duplicate,
1621 Err(CodecError::InvalidDefinition(message)) if message.contains("repeats action")
1622 ));
1623 }
1624}