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
23#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
28pub struct DateTemplate(String);
29
30impl DateTemplate {
31 pub fn new(value: impl Into<String>) -> Result<Self, CodecError> {
37 let value = value.into();
38 let date = value.strip_suffix('A').unwrap_or(&value);
39 let mut fields = date.split(|character: char| !character.is_ascii_alphabetic());
40 let parsed = [fields.next(), fields.next(), fields.next()];
41 let separators = date
42 .bytes()
43 .filter(|byte| !byte.is_ascii_alphabetic())
44 .collect::<Vec<_>>();
45 let valid_fields = matches!(parsed, [Some(_), Some(_), Some(_)])
46 && fields.next().is_none()
47 && parsed
48 .into_iter()
49 .flatten()
50 .all(|field| matches!(field, "D" | "M" | "Y" | "YY"))
51 && parsed
52 .into_iter()
53 .flatten()
54 .filter(|field| *field == "D")
55 .count()
56 == 1
57 && parsed
58 .into_iter()
59 .flatten()
60 .filter(|field| *field == "M")
61 .count()
62 == 1
63 && parsed
64 .into_iter()
65 .flatten()
66 .filter(|field| matches!(*field, "Y" | "YY"))
67 .count()
68 == 1;
69 if value.len() > 7
70 || separators.len() != 2
71 || !separators
72 .iter()
73 .all(|byte| matches!(byte, b'/' | b'.' | b'-' | b' '))
74 || !valid_fields
75 {
76 return Err(CodecError::InvalidDefinition(
77 "date template must contain D, M, and Y/YY once, two supported separators, and an optional trailing A for 12-hour time"
78 .into(),
79 ));
80 }
81 Ok(Self(value))
82 }
83
84 pub fn as_str(&self) -> &str {
85 &self.0
86 }
87
88 pub fn uses_twelve_hour_clock(&self) -> bool {
89 self.0.ends_with('A')
90 }
91}
92
93impl AsRef<str> for DateTemplate {
94 fn as_ref(&self) -> &str {
95 self.as_str()
96 }
97}
98
99impl fmt::Display for DateTemplate {
100 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101 formatter.write_str(self.as_str())
102 }
103}
104
105impl FromStr for DateTemplate {
106 type Err = CodecError;
107
108 fn from_str(value: &str) -> Result<Self, Self::Err> {
109 Self::new(value)
110 }
111}
112
113impl TryFrom<String> for DateTemplate {
114 type Error = CodecError;
115
116 fn try_from(value: String) -> Result<Self, Self::Error> {
117 Self::new(value)
118 }
119}
120
121impl Default for DateTemplate {
122 fn default() -> Self {
123 Self("D/M/Y".into())
124 }
125}
126
127impl fmt::Debug for DateTemplate {
128 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129 formatter
130 .debug_tuple("DateTemplate")
131 .field(&self.0)
132 .finish()
133 }
134}
135
136#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
138pub struct DeviceId(String);
139
140impl DeviceId {
141 pub fn new(value: impl Into<String>) -> Result<Self, CodecError> {
147 let value = value.into().trim().to_ascii_uppercase();
148 if value.is_empty() || value.len() > 15 || !value.bytes().all(|b| b.is_ascii_alphanumeric())
149 {
150 return Err(CodecError::InvalidDeviceId(value));
151 }
152 Ok(Self(value))
153 }
154
155 pub fn as_str(&self) -> &str {
156 &self.0
157 }
158}
159
160impl fmt::Display for DeviceId {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 f.write_str(&self.0)
163 }
164}
165
166impl AsRef<str> for DeviceId {
167 fn as_ref(&self) -> &str {
168 self.as_str()
169 }
170}
171
172impl Borrow<str> for DeviceId {
173 fn borrow(&self) -> &str {
174 self.as_str()
175 }
176}
177
178#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
183pub struct SessionGeneration(NonZeroU64);
184
185impl SessionGeneration {
186 pub const fn new(value: u64) -> Option<Self> {
189 match NonZeroU64::new(value) {
190 Some(value) => Some(Self(value)),
191 None => None,
192 }
193 }
194
195 pub const fn get(self) -> u64 {
196 self.0.get()
197 }
198}
199
200impl From<SessionGeneration> for u64 {
201 fn from(value: SessionGeneration) -> Self {
202 value.get()
203 }
204}
205
206impl fmt::Display for SessionGeneration {
207 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
208 self.0.fmt(formatter)
209 }
210}
211
212impl FromStr for DeviceId {
213 type Err = CodecError;
214
215 fn from_str(s: &str) -> Result<Self, Self::Err> {
216 Self::new(s)
217 }
218}
219
220impl TryFrom<String> for DeviceId {
221 type Error = CodecError;
222
223 fn try_from(value: String) -> Result<Self, Self::Error> {
224 Self::new(value)
225 }
226}
227
228macro_rules! id_newtype {
229 ($(#[$meta:meta])* $name:ident) => {
230 $(#[$meta])*
231 #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
232 pub struct $name(pub u32);
233
234 impl $name {
235 pub const fn new(value: u32) -> Self {
236 Self(value)
237 }
238
239 pub const fn get(self) -> u32 {
240 self.0
241 }
242 }
243
244 impl From<u32> for $name {
245 fn from(value: u32) -> Self {
246 Self(value)
247 }
248 }
249
250 impl From<$name> for u32 {
251 fn from(value: $name) -> Self {
252 value.0
253 }
254 }
255
256 impl fmt::Display for $name {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 self.0.fmt(f)
259 }
260 }
261 };
262}
263
264id_newtype!(LineInstance);
266id_newtype!(CallReference);
268id_newtype!(PassthroughPartyId);
270id_newtype!(AppearanceId);
272id_newtype!(ConferenceId);
274id_newtype!(ParticipantId);
276id_newtype!(ApplicationId);
278id_newtype!(TransactionId);
280
281#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
283pub struct MediaTrafficClass(u8);
284
285impl MediaTrafficClass {
286 pub const fn from_wire(value: u8) -> Self {
287 Self(value)
288 }
289
290 pub const fn from_dscp(dscp: u8) -> Option<Self> {
291 if dscp <= 63 {
292 Some(Self(dscp << 2))
293 } else {
294 None
295 }
296 }
297
298 pub const fn get(self) -> u8 {
299 self.0
300 }
301}
302
303impl From<MediaTrafficClass> for u8 {
304 fn from(value: MediaTrafficClass) -> Self {
305 value.get()
306 }
307}
308
309impl From<MediaTrafficClass> for u32 {
310 fn from(value: MediaTrafficClass) -> Self {
311 u32::from(value.get())
312 }
313}
314
315#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
321pub struct CallId(pub u64);
322
323impl CallId {
324 pub const fn new(value: u64) -> Self {
325 Self(value)
326 }
327
328 pub const fn get(self) -> u64 {
329 self.0
330 }
331}
332
333impl From<u64> for CallId {
334 fn from(value: u64) -> Self {
335 Self::new(value)
336 }
337}
338
339impl From<CallId> for u64 {
340 fn from(value: CallId) -> Self {
341 value.get()
342 }
343}
344
345impl fmt::Display for CallId {
346 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
347 self.0.fmt(formatter)
348 }
349}
350
351#[derive(Clone, Debug, Eq, PartialEq)]
356pub struct LineDefinition {
357 pub number: String,
360 pub display_name: String,
361}
362
363#[derive(Clone, Debug, Default, Eq, PartialEq)]
368pub struct CallerIdOverride {
369 pub name: Option<String>,
370 pub number: Option<String>,
371}
372
373#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
375pub enum AppearanceRingMode {
376 #[default]
377 Normal,
378 Silent,
379 Disabled,
380}
381
382#[derive(Clone, Debug, Eq, PartialEq)]
384pub struct LineAppearance {
385 pub id: AppearanceId,
387 pub instance: u32,
389 pub line: LineDefinition,
390 pub label: Option<String>,
392 pub caller_id: CallerIdOverride,
393 pub ring_mode: AppearanceRingMode,
394 pub initial_tone: Tone,
396 pub subscription_identity: Option<String>,
398 pub privacy: bool,
399}
400
401impl LineAppearance {
402 pub fn new(instance: u32, line: LineDefinition) -> Self {
404 Self {
405 id: AppearanceId::new(instance),
406 instance,
407 line,
408 label: None,
409 caller_id: CallerIdOverride::default(),
410 ring_mode: AppearanceRingMode::Normal,
411 initial_tone: Tone::InsideDial,
412 subscription_identity: None,
413 privacy: false,
414 }
415 }
416
417 pub fn display_label(&self) -> &str {
419 self.label.as_deref().unwrap_or(&self.line.display_name)
420 }
421}
422
423impl Deref for LineAppearance {
424 type Target = LineDefinition;
425
426 fn deref(&self) -> &Self::Target {
427 &self.line
428 }
429}
430
431#[derive(Clone, Debug, Eq, PartialEq)]
433pub struct SpeedDialDefinition {
434 pub instance: u32,
436 pub number: String,
437 pub display_name: String,
438}
439
440#[derive(Clone, Debug, Eq, PartialEq)]
442pub struct BlfSpeedDialDefinition {
443 pub instance: u32,
445 pub number: String,
446 pub display_name: String,
447}
448
449#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
451pub enum BlfState {
452 Idle,
453 Ringing,
454 Busy,
455 Held,
456 DoNotDisturb,
457 Unavailable,
458 #[default]
459 Unknown,
460}
461
462#[derive(Clone, Debug, Default, Eq, PartialEq)]
464pub struct BlfCallerInfo {
465 pub name: String,
466 pub number: String,
467}
468
469impl BlfCallerInfo {
470 pub fn display(&self) -> String {
472 match (self.name.trim(), self.number.trim()) {
473 ("", "") => String::new(),
474 ("", number) => number.to_owned(),
475 (name, "") => name.to_owned(),
476 (name, number) => format!("{name} ({number})"),
477 }
478 }
479}
480
481#[derive(Clone, Debug, Eq, PartialEq)]
483pub struct FeatureDefinition {
484 pub instance: u32,
486 pub label: String,
487 pub feature: crate::message::values::ButtonType,
488}
489
490#[derive(Clone, Debug, Eq, PartialEq)]
492pub struct ServiceDefinition {
493 pub instance: u32,
495 pub label: String,
496 pub url: String,
501}
502
503#[derive(Clone, Debug, Eq, PartialEq)]
505pub struct AddonModuleDefinition {
506 pub slot: u32,
508 pub device_type: crate::message::values::DeviceType,
509}
510
511impl AddonModuleDefinition {
512 pub const fn button_capacity(&self) -> Option<usize> {
514 use crate::message::values::DeviceType;
515
516 match self.device_type {
517 DeviceType::CiscoAddon7914 => Some(14),
518 DeviceType::CiscoAddon7915_12 | DeviceType::CiscoAddon7916_12 => Some(12),
519 DeviceType::CiscoAddon7915_24 | DeviceType::CiscoAddon7916_24 => Some(24),
520 DeviceType::AddonSpa500s | DeviceType::AddonSpa500ds | DeviceType::AddonSpa932ds => {
521 Some(32)
522 }
523 _ => None,
524 }
525 }
526}
527
528#[derive(Clone, Debug, Eq, PartialEq)]
533pub enum ButtonDefinition {
534 Line(LineAppearance),
535 SpeedDial(SpeedDialDefinition),
536 BlfSpeedDial(BlfSpeedDialDefinition),
537 Feature(FeatureDefinition),
538 Service(ServiceDefinition),
539 AddonModule(AddonModuleDefinition),
540 Unused,
541}
542
543#[derive(Clone, Debug, Eq, PartialEq)]
548pub struct SoftKeyProfile {
549 sets: HashMap<KeyMode, Vec<SoftKey>>,
550}
551
552impl SoftKeyProfile {
553 pub const MAX_KEYS_PER_MODE: usize = 16;
555
556 pub fn new(
561 sets: impl IntoIterator<Item = (KeyMode, Vec<SoftKey>)>,
562 ) -> Result<Self, CodecError> {
563 let profile = Self {
564 sets: sets.into_iter().collect(),
565 };
566 profile.validate()?;
567 Ok(profile)
568 }
569
570 pub fn empty() -> Self {
571 Self {
572 sets: KeyMode::ALL_KNOWN
573 .iter()
574 .copied()
575 .map(|mode| (mode, Vec::new()))
576 .collect(),
577 }
578 }
579
580 pub fn built_in() -> Self {
583 let mut profile = Self::empty();
584 profile.sets.extend([
585 (KeyMode::OnHook, vec![SoftKey::NewCall]),
586 (
587 KeyMode::Connected,
588 vec![SoftKey::Hold, SoftKey::EndCall, SoftKey::Transfer],
589 ),
590 (
591 KeyMode::OnHold,
592 vec![SoftKey::Resume, SoftKey::NewCall, SoftKey::EndCall],
593 ),
594 (KeyMode::RingIn, vec![SoftKey::Answer, SoftKey::EndCall]),
595 (KeyMode::OffHook, vec![SoftKey::EndCall]),
596 (
597 KeyMode::ConnectedTransfer,
598 vec![SoftKey::Hold, SoftKey::EndCall, SoftKey::Transfer],
599 ),
600 (
601 KeyMode::DigitsFollowing,
602 vec![SoftKey::Backspace, SoftKey::EndCall, SoftKey::Dial],
603 ),
604 (
605 KeyMode::ConnectedConference,
606 vec![SoftKey::Hold, SoftKey::EndCall],
607 ),
608 (KeyMode::RingOut, vec![SoftKey::EndCall]),
609 (
610 KeyMode::OffHookFeature,
611 vec![SoftKey::Resume, SoftKey::NewCall, SoftKey::EndCall],
612 ),
613 (
614 KeyMode::OnHookStealable,
615 vec![SoftKey::Intercept, SoftKey::NewCall],
616 ),
617 (
618 KeyMode::HoldConference,
619 vec![SoftKey::Resume, SoftKey::NewCall, SoftKey::EndCall],
620 ),
621 ]);
622 profile
623 }
624
625 pub fn actions(&self, mode: KeyMode) -> &[SoftKey] {
626 self.sets.get(&mode).map_or(&[], Vec::as_slice)
627 }
628
629 pub fn allows(&self, mode: KeyMode, action: SoftKey) -> bool {
630 action.is_known() && self.actions(mode).contains(&action)
631 }
632
633 pub fn valid_mask(&self, mode: KeyMode) -> u32 {
635 let count = self.actions(mode).len();
636 if count == 0 { 0 } else { (1_u32 << count) - 1 }
637 }
638
639 pub fn template_actions(&self) -> Vec<SoftKey> {
642 if self == &Self::built_in() {
643 return SoftKey::ALL_KNOWN.to_vec();
644 }
645 let configured: HashSet<_> = KeyMode::ALL_KNOWN
646 .iter()
647 .flat_map(|mode| self.actions(*mode).iter().copied())
648 .collect();
649 SoftKey::ALL_KNOWN
650 .iter()
651 .copied()
652 .filter(|action| configured.contains(action))
653 .collect()
654 }
655
656 pub fn validate(&self) -> Result<(), CodecError> {
658 if self.sets.len() != KeyMode::ALL_KNOWN.len()
659 || KeyMode::ALL_KNOWN
660 .iter()
661 .any(|mode| !self.sets.contains_key(mode))
662 {
663 return Err(CodecError::InvalidDefinition(
664 "soft-key profile must define every known key mode".into(),
665 ));
666 }
667 for (&mode, actions) in &self.sets {
668 if !mode.is_known() {
669 return Err(CodecError::InvalidDefinition(format!(
670 "soft-key profile contains unknown key mode {}",
671 mode.wire_value()
672 )));
673 }
674 if actions.len() > Self::MAX_KEYS_PER_MODE {
675 return Err(CodecError::InvalidDefinition(format!(
676 "soft-key mode {} contains {} actions; the protocol limit is {}",
677 mode.wire_value(),
678 actions.len(),
679 Self::MAX_KEYS_PER_MODE
680 )));
681 }
682 let mut seen = HashSet::new();
683 for &action in actions {
684 if !action.is_known() {
685 return Err(CodecError::InvalidDefinition(format!(
686 "soft-key mode {} contains unknown action {}",
687 mode.wire_value(),
688 action.wire_value()
689 )));
690 }
691 if !seen.insert(action) {
692 return Err(CodecError::InvalidDefinition(format!(
693 "soft-key mode {} repeats action {}",
694 mode.wire_value(),
695 action.wire_value()
696 )));
697 }
698 }
699 }
700 Ok(())
701 }
702}
703
704impl Default for SoftKeyProfile {
705 fn default() -> Self {
706 Self::built_in()
707 }
708}
709
710#[derive(Clone, Debug, Eq, PartialEq)]
717pub struct DeviceDefinition {
718 pub id: DeviceId,
719 pub description: String,
720 pub transport: StationTransportRequirement,
721 pub signaling_qos: Option<SignalingQos>,
724 pub buttons: Vec<ButtonDefinition>,
730 pub soft_keys: SoftKeyProfile,
732 pub ui: StationUiPolicy,
734}
735
736#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
738pub enum StationTransportRequirement {
739 Clear,
740 Secure,
741 #[default]
742 Either,
743}
744
745#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
747pub enum StationTransport {
748 Clear,
749 Secure,
750}
751
752#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
759pub struct SignalingQos {
760 pub dscp: u8,
761 pub cos: u8,
762}
763
764impl SignalingQos {
765 pub const fn new(dscp: u8, cos: u8) -> Self {
766 Self { dscp, cos }
767 }
768
769 pub(crate) fn validate(self) -> Result<(), CodecError> {
770 if self.dscp > 63 {
771 return Err(CodecError::InvalidDefinition(format!(
772 "signaling DSCP {} is outside 0..=63",
773 self.dscp
774 )));
775 }
776 if self.cos > 7 {
777 return Err(CodecError::InvalidDefinition(format!(
778 "signaling COS {} is outside 0..=7",
779 self.cos
780 )));
781 }
782 Ok(())
783 }
784}
785
786#[derive(Clone, Copy, Debug, Eq, PartialEq)]
789pub struct StationUiPolicy {
790 pub placed_calls_redial_menu: bool,
793 pub hinted_ringing_notification: bool,
796 pub speed_dial_await_further_digits: bool,
799 pub mwi_lamp_mode: crate::message::values::LampMode,
801 pub mwi_on_call: bool,
803 pub legacy_code_page: LegacyCodePage,
806}
807
808#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
811pub enum LegacyCodePage {
812 #[default]
813 Iso8859_1,
814 Ascii,
815}
816
817impl Default for StationUiPolicy {
818 fn default() -> Self {
819 Self {
820 placed_calls_redial_menu: false,
821 hinted_ringing_notification: false,
822 speed_dial_await_further_digits: false,
823 mwi_lamp_mode: crate::message::values::LampMode::On,
824 mwi_on_call: false,
825 legacy_code_page: LegacyCodePage::Iso8859_1,
826 }
827 }
828}
829
830impl DeviceDefinition {
831 pub fn validate(&self) -> Result<(), CodecError> {
833 const MAX_BUTTONS: usize = 256;
837
838 self.soft_keys.validate()?;
839 if let Some(signaling_qos) = self.signaling_qos {
840 signaling_qos.validate()?;
841 }
842
843 if self.buttons.len() > MAX_BUTTONS {
844 return Err(CodecError::InvalidDefinition(format!(
845 "device {} has {} buttons; the logical layout limit is {MAX_BUTTONS}",
846 self.id,
847 self.buttons.len()
848 )));
849 }
850
851 let mut expanded_buttons = 0_usize;
852 let mut addon_buttons_remaining = None;
853 for button in &self.buttons {
854 if let ButtonDefinition::AddonModule(addon) = button {
855 expanded_buttons += addon_buttons_remaining.take().unwrap_or_default();
856 addon_buttons_remaining = Some(addon.button_capacity().ok_or_else(|| {
857 CodecError::InvalidDefinition(format!(
858 "device {} has unsupported addon-module type {}",
859 self.id,
860 addon.device_type.wire_value()
861 ))
862 })?);
863 continue;
864 }
865 expanded_buttons += 1;
866 if let Some(remaining) = &mut addon_buttons_remaining {
867 if *remaining == 0 {
868 return Err(CodecError::InvalidDefinition(format!(
869 "device {} configures more buttons than its addon module provides",
870 self.id
871 )));
872 }
873 *remaining -= 1;
874 }
875 }
876 expanded_buttons += addon_buttons_remaining.unwrap_or_default();
877 if expanded_buttons > MAX_BUTTONS {
878 return Err(CodecError::InvalidDefinition(format!(
879 "device {} expands to {expanded_buttons} buttons; the logical layout limit is {MAX_BUTTONS}",
880 self.id
881 )));
882 }
883
884 let mut instances = HashSet::new();
885 let mut appearance_ids = HashSet::new();
886 for button in &self.buttons {
887 let Some((kind, instance)) = button.instance_key() else {
888 continue;
889 };
890 if instance == 0 {
891 return Err(CodecError::InvalidDefinition(format!(
892 "device {} has a {kind} button with instance zero",
893 self.id
894 )));
895 }
896 if kind != ButtonNamespace::AddonModule && instance > u32::from(u8::MAX) {
901 return Err(CodecError::InvalidDefinition(format!(
902 "device {} has a {kind} button with instance {instance}; maximum wire instance is {}",
903 self.id,
904 u8::MAX
905 )));
906 }
907 if !instances.insert((kind, instance)) {
908 return Err(CodecError::InvalidDefinition(format!(
909 "device {} repeats {kind} button instance {instance}",
910 self.id
911 )));
912 }
913 if let ButtonDefinition::Line(appearance) = button {
914 if appearance.id.get() == 0 {
915 return Err(CodecError::InvalidDefinition(format!(
916 "device {} has a line appearance with identifier zero",
917 self.id
918 )));
919 }
920 if !appearance_ids.insert(appearance.id) {
921 return Err(CodecError::InvalidDefinition(format!(
922 "device {} repeats line appearance identifier {}",
923 self.id, appearance.id
924 )));
925 }
926 }
927 if let ButtonDefinition::Service(service) = button {
928 validate_service_definition(&self.id, service)?;
929 }
930 }
931
932 let lines: Vec<_> = self.lines().collect();
933 if lines.is_empty() {
934 return Err(CodecError::InvalidDefinition(format!(
935 "device {} has no lines",
936 self.id
937 )));
938 }
939 let permits_sparse_lines = self.buttons.iter().any(|button| {
943 matches!(
944 button,
945 ButtonDefinition::Feature(feature)
946 if feature.feature == crate::message::values::ButtonType::Mobility
947 )
948 });
949 for (expected, line) in (1_u32..).zip(lines) {
950 if !permits_sparse_lines && line.instance != expected {
951 return Err(CodecError::InvalidDefinition(format!(
952 "device {} line instances must be contiguous from 1",
953 self.id
954 )));
955 }
956 if line.number.is_empty() || line.number.len() > 24 {
957 return Err(CodecError::InvalidDefinition(format!(
958 "device {} has an invalid line number",
959 self.id
960 )));
961 }
962 }
963 Ok(())
964 }
965
966 pub fn lines(&self) -> impl Iterator<Item = &LineAppearance> {
967 self.buttons.iter().filter_map(|button| match button {
968 ButtonDefinition::Line(line) => Some(line),
969 _ => None,
970 })
971 }
972
973 pub fn line(&self, instance: u32) -> Option<&LineAppearance> {
974 self.lines().find(|line| line.instance == instance)
975 }
976
977 pub fn first_line(&self) -> Option<&LineAppearance> {
978 self.lines().next()
979 }
980
981 pub fn line_count(&self) -> usize {
982 self.lines().count()
983 }
984
985 pub(crate) fn feature_button(&self, instance: u32) -> Option<&FeatureDefinition> {
986 self.buttons.iter().find_map(|button| match button {
987 ButtonDefinition::Feature(feature) if feature.instance == instance => Some(feature),
988 _ => None,
989 })
990 }
991
992 pub(crate) fn blf_button(&self, instance: u32) -> Option<&BlfSpeedDialDefinition> {
993 self.buttons.iter().find_map(|button| match button {
994 ButtonDefinition::BlfSpeedDial(blf) if blf.instance == instance => Some(blf),
995 _ => None,
996 })
997 }
998}
999
1000fn validate_service_definition(
1001 device: &DeviceId,
1002 service: &ServiceDefinition,
1003) -> Result<(), CodecError> {
1004 const MAX_SERVICE_URL_BYTES: usize = 255;
1005 const MAX_SERVICE_LABEL_BYTES: usize = 39;
1006 const MAX_SERVICE_PARAMETERS: usize = 32;
1007 const MAX_SERVICE_PARAMETER_BYTES: usize = 128;
1008
1009 if service.label.is_empty()
1010 || service.label.len() > MAX_SERVICE_LABEL_BYTES
1011 || service.label.chars().any(char::is_control)
1012 {
1013 return Err(CodecError::InvalidDefinition(format!(
1014 "device {device} has an invalid service label"
1015 )));
1016 }
1017 if service.url.is_empty()
1018 || service.url.len() > MAX_SERVICE_URL_BYTES
1019 || service.url.chars().any(char::is_control)
1020 {
1021 return Err(CodecError::InvalidDefinition(format!(
1022 "device {device} has an invalid service URL"
1023 )));
1024 }
1025 let url = url::Url::parse(&service.url).map_err(|_| {
1026 CodecError::InvalidDefinition(format!("device {device} has a malformed service URL"))
1027 })?;
1028 if !matches!(url.scheme(), "http" | "https")
1029 || url.host_str().is_none()
1030 || url.fragment().is_some()
1031 {
1032 return Err(CodecError::InvalidDefinition(format!(
1033 "device {device} service URL must be HTTP(S) without a fragment"
1034 )));
1035 }
1036 let parameters = url.query_pairs().collect::<Vec<_>>();
1037 if parameters.len() > MAX_SERVICE_PARAMETERS
1038 || parameters.iter().any(|(name, value)| {
1039 name.is_empty()
1040 || name.len() > MAX_SERVICE_PARAMETER_BYTES
1041 || value.len() > MAX_SERVICE_PARAMETER_BYTES
1042 || name.chars().chain(value.chars()).any(char::is_control)
1043 })
1044 {
1045 return Err(CodecError::InvalidDefinition(format!(
1046 "device {device} service URL has invalid or excessive query parameters"
1047 )));
1048 }
1049 Ok(())
1050}
1051
1052impl ButtonDefinition {
1053 fn instance_key(&self) -> Option<(ButtonNamespace, u32)> {
1054 match self {
1055 Self::Line(definition) => Some((ButtonNamespace::Line, definition.instance)),
1056 Self::SpeedDial(definition) => Some((ButtonNamespace::SpeedDial, definition.instance)),
1057 Self::BlfSpeedDial(definition) => Some((ButtonNamespace::Feature, definition.instance)),
1058 Self::Feature(definition) => Some((ButtonNamespace::Feature, definition.instance)),
1059 Self::Service(definition) => Some((ButtonNamespace::Service, definition.instance)),
1060 Self::AddonModule(definition) => Some((ButtonNamespace::AddonModule, definition.slot)),
1061 Self::Unused => None,
1062 }
1063 }
1064}
1065
1066#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1067enum ButtonNamespace {
1068 Line,
1069 SpeedDial,
1070 Feature,
1071 Service,
1072 AddonModule,
1073}
1074
1075impl fmt::Display for ButtonNamespace {
1076 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1077 formatter.write_str(match self {
1078 Self::Line => "line",
1079 Self::SpeedDial => "speed dial",
1080 Self::Feature => "feature",
1081 Self::Service => "service URL",
1082 Self::AddonModule => "addon module",
1083 })
1084 }
1085}
1086
1087#[derive(Clone, Debug, Eq, PartialEq)]
1092pub struct DeviceRegistration {
1093 pub id: DeviceId,
1094 pub peer: SocketAddr,
1095 pub transport: StationTransport,
1096 pub reported_address: Option<Ipv4Addr>,
1097 pub reported_ipv6_address: Option<Ipv6Addr>,
1098 pub device_type: DeviceType,
1099 pub protocol: ProtocolVersion,
1100 pub firmware: String,
1101}
1102
1103impl DeviceRegistration {
1104 pub fn reported_address_for_peer(&self) -> Option<IpAddr> {
1107 match self.peer.ip() {
1108 IpAddr::V4(_) => self.reported_address.map(IpAddr::V4),
1109 IpAddr::V6(peer) => peer.to_ipv4_mapped().map_or_else(
1110 || self.reported_ipv6_address.map(IpAddr::V6),
1111 |_| self.reported_address.map(IpAddr::V4),
1112 ),
1113 }
1114 }
1115}
1116
1117#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1119pub enum CallDirection {
1120 Inbound,
1121 Outbound,
1122}
1123
1124impl From<CallDirection> for CallType {
1125 fn from(value: CallDirection) -> Self {
1126 match value {
1127 CallDirection::Inbound => Self::Inbound,
1128 CallDirection::Outbound => Self::Outbound,
1129 }
1130 }
1131}
1132
1133#[derive(Clone, Debug, Eq, PartialEq)]
1139pub struct CallInfo {
1140 pub direction: CallDirection,
1141 pub calling_name: String,
1142 pub calling_number: String,
1143 pub called_name: String,
1144 pub called_number: String,
1145 pub original_called_name: String,
1146 pub original_called_number: String,
1147 pub last_redirecting_name: String,
1148 pub last_redirecting_number: String,
1149 pub original_redirect_reason: u32,
1150 pub last_redirect_reason: u32,
1151 pub party_restrictions: u32,
1153}
1154
1155impl Default for CallInfo {
1156 fn default() -> Self {
1157 Self {
1158 direction: CallDirection::Outbound,
1159 calling_name: String::new(),
1160 calling_number: String::new(),
1161 called_name: String::new(),
1162 called_number: String::new(),
1163 original_called_name: String::new(),
1164 original_called_number: String::new(),
1165 last_redirecting_name: String::new(),
1166 last_redirecting_number: String::new(),
1167 original_redirect_reason: 0,
1168 last_redirect_reason: 0,
1169 party_restrictions: 0,
1170 }
1171 }
1172}
1173
1174pub const DEFAULT_AUDIO_PACKET_MS: u32 = 20;
1176pub const DEFAULT_AUDIO_MAX_FRAMES_PER_PACKET: u32 = 1;
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}