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 pub hint: String,
449}
450
451#[derive(Clone, Copy, Debug, Eq, PartialEq)]
453pub enum BlfState {
454 Idle,
455 Ringing,
456 Busy,
457 Held,
458 Unavailable,
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 mwi_lamp_mode: crate::message::values::LampMode,
798 pub mwi_on_call: bool,
800 pub legacy_code_page: LegacyCodePage,
803}
804
805#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
808pub enum LegacyCodePage {
809 #[default]
810 Iso8859_1,
811 Ascii,
812}
813
814impl Default for StationUiPolicy {
815 fn default() -> Self {
816 Self {
817 placed_calls_redial_menu: false,
818 hinted_ringing_notification: false,
819 mwi_lamp_mode: crate::message::values::LampMode::On,
820 mwi_on_call: false,
821 legacy_code_page: LegacyCodePage::Iso8859_1,
822 }
823 }
824}
825
826impl DeviceDefinition {
827 pub fn validate(&self) -> Result<(), CodecError> {
829 const MAX_BUTTONS: usize = 42;
830
831 self.soft_keys.validate()?;
832 if let Some(signaling_qos) = self.signaling_qos {
833 signaling_qos.validate()?;
834 }
835
836 if self.buttons.len() > MAX_BUTTONS {
837 return Err(CodecError::InvalidDefinition(format!(
838 "device {} has {} buttons; the protocol limit is {MAX_BUTTONS}",
839 self.id,
840 self.buttons.len()
841 )));
842 }
843
844 let mut expanded_buttons = 0_usize;
845 let mut addon_buttons_remaining = None;
846 for button in &self.buttons {
847 if let ButtonDefinition::AddonModule(addon) = button {
848 expanded_buttons += addon_buttons_remaining.take().unwrap_or_default();
849 addon_buttons_remaining = Some(addon.button_capacity().ok_or_else(|| {
850 CodecError::InvalidDefinition(format!(
851 "device {} has unsupported addon-module type {}",
852 self.id,
853 addon.device_type.wire_value()
854 ))
855 })?);
856 continue;
857 }
858 expanded_buttons += 1;
859 if let Some(remaining) = &mut addon_buttons_remaining {
860 if *remaining == 0 {
861 return Err(CodecError::InvalidDefinition(format!(
862 "device {} configures more buttons than its addon module provides",
863 self.id
864 )));
865 }
866 *remaining -= 1;
867 }
868 }
869 expanded_buttons += addon_buttons_remaining.unwrap_or_default();
870 if expanded_buttons > MAX_BUTTONS {
871 return Err(CodecError::InvalidDefinition(format!(
872 "device {} expands to {expanded_buttons} buttons; the protocol limit is {MAX_BUTTONS}",
873 self.id
874 )));
875 }
876
877 let mut instances = HashSet::new();
878 let mut appearance_ids = HashSet::new();
879 for button in &self.buttons {
880 let Some((kind, instance)) = button.instance_key() else {
881 continue;
882 };
883 if instance == 0 {
884 return Err(CodecError::InvalidDefinition(format!(
885 "device {} has a {kind} button with instance zero",
886 self.id
887 )));
888 }
889 if !instances.insert((kind, instance)) {
890 return Err(CodecError::InvalidDefinition(format!(
891 "device {} repeats {kind} button instance {instance}",
892 self.id
893 )));
894 }
895 if let ButtonDefinition::Line(appearance) = button {
896 if appearance.id.get() == 0 {
897 return Err(CodecError::InvalidDefinition(format!(
898 "device {} has a line appearance with identifier zero",
899 self.id
900 )));
901 }
902 if !appearance_ids.insert(appearance.id) {
903 return Err(CodecError::InvalidDefinition(format!(
904 "device {} repeats line appearance identifier {}",
905 self.id, appearance.id
906 )));
907 }
908 }
909 if let ButtonDefinition::Service(service) = button {
910 validate_service_definition(&self.id, service)?;
911 }
912 }
913
914 let lines: Vec<_> = self.lines().collect();
915 if lines.is_empty() {
916 return Err(CodecError::InvalidDefinition(format!(
917 "device {} has no lines",
918 self.id
919 )));
920 }
921 let permits_sparse_lines = self.buttons.iter().any(|button| {
925 matches!(
926 button,
927 ButtonDefinition::Feature(feature)
928 if feature.feature == crate::message::values::ButtonType::Mobility
929 )
930 });
931 for (expected, line) in (1_u32..).zip(lines) {
932 if !permits_sparse_lines && line.instance != expected {
933 return Err(CodecError::InvalidDefinition(format!(
934 "device {} line instances must be contiguous from 1",
935 self.id
936 )));
937 }
938 if line.number.is_empty() || line.number.len() > 24 {
939 return Err(CodecError::InvalidDefinition(format!(
940 "device {} has an invalid line number",
941 self.id
942 )));
943 }
944 }
945 Ok(())
946 }
947
948 pub fn lines(&self) -> impl Iterator<Item = &LineAppearance> {
949 self.buttons.iter().filter_map(|button| match button {
950 ButtonDefinition::Line(line) => Some(line),
951 _ => None,
952 })
953 }
954
955 pub fn line(&self, instance: u32) -> Option<&LineAppearance> {
956 self.lines().find(|line| line.instance == instance)
957 }
958
959 pub fn first_line(&self) -> Option<&LineAppearance> {
960 self.lines().next()
961 }
962
963 pub fn line_count(&self) -> usize {
964 self.lines().count()
965 }
966}
967
968fn validate_service_definition(
969 device: &DeviceId,
970 service: &ServiceDefinition,
971) -> Result<(), CodecError> {
972 const MAX_SERVICE_URL_BYTES: usize = 255;
973 const MAX_SERVICE_LABEL_BYTES: usize = 39;
974 const MAX_SERVICE_PARAMETERS: usize = 32;
975 const MAX_SERVICE_PARAMETER_BYTES: usize = 128;
976
977 if service.label.is_empty()
978 || service.label.len() > MAX_SERVICE_LABEL_BYTES
979 || service.label.chars().any(char::is_control)
980 {
981 return Err(CodecError::InvalidDefinition(format!(
982 "device {device} has an invalid service label"
983 )));
984 }
985 if service.url.is_empty()
986 || service.url.len() > MAX_SERVICE_URL_BYTES
987 || service.url.chars().any(char::is_control)
988 {
989 return Err(CodecError::InvalidDefinition(format!(
990 "device {device} has an invalid service URL"
991 )));
992 }
993 let url = url::Url::parse(&service.url).map_err(|_| {
994 CodecError::InvalidDefinition(format!("device {device} has a malformed service URL"))
995 })?;
996 if !matches!(url.scheme(), "http" | "https")
997 || url.host_str().is_none()
998 || url.fragment().is_some()
999 {
1000 return Err(CodecError::InvalidDefinition(format!(
1001 "device {device} service URL must be HTTP(S) without a fragment"
1002 )));
1003 }
1004 let parameters = url.query_pairs().collect::<Vec<_>>();
1005 if parameters.len() > MAX_SERVICE_PARAMETERS
1006 || parameters.iter().any(|(name, value)| {
1007 name.is_empty()
1008 || name.len() > MAX_SERVICE_PARAMETER_BYTES
1009 || value.len() > MAX_SERVICE_PARAMETER_BYTES
1010 || name.chars().chain(value.chars()).any(char::is_control)
1011 })
1012 {
1013 return Err(CodecError::InvalidDefinition(format!(
1014 "device {device} service URL has invalid or excessive query parameters"
1015 )));
1016 }
1017 Ok(())
1018}
1019
1020impl ButtonDefinition {
1021 fn instance_key(&self) -> Option<(&'static str, u32)> {
1022 match self {
1023 Self::Line(definition) => Some(("line", definition.instance)),
1024 Self::SpeedDial(definition) => Some(("speed dial", definition.instance)),
1025 Self::BlfSpeedDial(definition) => Some(("BLF speed dial", definition.instance)),
1026 Self::Feature(definition) => Some(("feature", definition.instance)),
1027 Self::Service(definition) => Some(("service URL", definition.instance)),
1028 Self::AddonModule(definition) => Some(("addon module", definition.slot)),
1029 Self::Unused => None,
1030 }
1031 }
1032}
1033
1034#[derive(Clone, Debug, Eq, PartialEq)]
1039pub struct DeviceRegistration {
1040 pub id: DeviceId,
1041 pub peer: SocketAddr,
1042 pub transport: StationTransport,
1043 pub reported_address: Option<Ipv4Addr>,
1044 pub reported_ipv6_address: Option<Ipv6Addr>,
1045 pub device_type: DeviceType,
1046 pub protocol: ProtocolVersion,
1047 pub firmware: String,
1048}
1049
1050impl DeviceRegistration {
1051 pub fn reported_address_for_peer(&self) -> Option<IpAddr> {
1054 match self.peer.ip() {
1055 IpAddr::V4(_) => self.reported_address.map(IpAddr::V4),
1056 IpAddr::V6(peer) => peer.to_ipv4_mapped().map_or_else(
1057 || self.reported_ipv6_address.map(IpAddr::V6),
1058 |_| self.reported_address.map(IpAddr::V4),
1059 ),
1060 }
1061 }
1062}
1063
1064#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1066pub enum CallDirection {
1067 Inbound,
1068 Outbound,
1069}
1070
1071impl From<CallDirection> for CallType {
1072 fn from(value: CallDirection) -> Self {
1073 match value {
1074 CallDirection::Inbound => Self::Inbound,
1075 CallDirection::Outbound => Self::Outbound,
1076 }
1077 }
1078}
1079
1080#[derive(Clone, Debug, Eq, PartialEq)]
1086pub struct CallInfo {
1087 pub direction: CallDirection,
1088 pub calling_name: String,
1089 pub calling_number: String,
1090 pub called_name: String,
1091 pub called_number: String,
1092 pub original_called_name: String,
1093 pub original_called_number: String,
1094 pub last_redirecting_name: String,
1095 pub last_redirecting_number: String,
1096 pub original_redirect_reason: u32,
1097 pub last_redirect_reason: u32,
1098 pub party_restrictions: u32,
1100}
1101
1102impl Default for CallInfo {
1103 fn default() -> Self {
1104 Self {
1105 direction: CallDirection::Outbound,
1106 calling_name: String::new(),
1107 calling_number: String::new(),
1108 called_name: String::new(),
1109 called_number: String::new(),
1110 original_called_name: String::new(),
1111 original_called_number: String::new(),
1112 last_redirecting_name: String::new(),
1113 last_redirecting_number: String::new(),
1114 original_redirect_reason: 0,
1115 last_redirect_reason: 0,
1116 party_restrictions: 0,
1117 }
1118 }
1119}
1120
1121pub const DEFAULT_AUDIO_PACKET_MS: u32 = 20;
1123pub const DEFAULT_AUDIO_MAX_FRAMES_PER_PACKET: u32 = 1;
1125
1126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1129pub struct AudioProcessingPolicy {
1130 pub echo_cancellation: EchoCancellation,
1131 pub silence_suppression: SilenceSuppression,
1132}
1133
1134impl Default for AudioProcessingPolicy {
1135 fn default() -> Self {
1136 Self {
1137 echo_cancellation: EchoCancellation::On,
1138 silence_suppression: SilenceSuppression::Off,
1139 }
1140 }
1141}
1142
1143#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1148pub struct MediaEndpoint {
1149 pub address: IpAddr,
1150 pub rtp_port: u16,
1151 pub rtcp_port: u16,
1152 pub codec: Codec,
1153 pub packet_ms: u32,
1154 pub max_frames_per_packet: u32,
1155 pub telephone_event_payload: u8,
1157}
1158
1159#[cfg(test)]
1160mod tests {
1161 use super::*;
1162
1163 #[test]
1164 fn identifiers_are_explicit_and_lossless() {
1165 let reference = CallReference::new(42);
1166 assert_eq!(reference.get(), 42);
1167 assert_eq!(u32::from(reference), 42);
1168
1169 let appearance = AppearanceId::new(7);
1170 assert_eq!(appearance.get(), 7);
1171 assert_eq!(u32::from(appearance), 7);
1172
1173 let conference = ConferenceId::new(9);
1174 assert_eq!(conference.get(), 9);
1175
1176 let participant = ParticipantId::new(11);
1177 assert_eq!(participant.get(), 11);
1178 }
1179
1180 #[test]
1181 fn device_id_is_canonicalized() {
1182 let id: DeviceId = " sep001122334455 ".parse().unwrap();
1183 assert_eq!(id.as_str(), "SEP001122334455");
1184 assert_eq!(id.as_ref(), "SEP001122334455");
1185
1186 let mut devices = HashMap::new();
1187 devices.insert(id, "desk");
1188 assert_eq!(devices.get("SEP001122334455"), Some(&"desk"));
1189 }
1190
1191 #[test]
1192 fn registration_selects_the_report_matching_the_effective_peer_family() {
1193 let registration = DeviceRegistration {
1194 id: DeviceId::new("SEP001122334455").unwrap(),
1195 peer: "[2001:db8::20]:2000".parse().unwrap(),
1196 transport: StationTransport::Clear,
1197 reported_address: Some("192.0.2.20".parse().unwrap()),
1198 reported_ipv6_address: Some("2001:db8::20".parse().unwrap()),
1199 device_type: DeviceType::Cisco7962,
1200 protocol: ProtocolVersion::V22,
1201 firmware: "test".into(),
1202 };
1203 assert_eq!(
1204 registration.reported_address_for_peer(),
1205 Some("2001:db8::20".parse().unwrap())
1206 );
1207
1208 let mapped = DeviceRegistration {
1209 peer: "[::ffff:192.0.2.20]:2000".parse().unwrap(),
1210 ..registration
1211 };
1212 assert_eq!(
1213 mapped.reported_address_for_peer(),
1214 Some("192.0.2.20".parse().unwrap())
1215 );
1216 }
1217
1218 fn line_button(instance: u32, number: &str) -> ButtonDefinition {
1219 ButtonDefinition::Line(LineAppearance::new(
1220 instance,
1221 LineDefinition {
1222 number: number.into(),
1223 display_name: number.into(),
1224 },
1225 ))
1226 }
1227
1228 #[test]
1229 fn station_definition_accepts_non_line_buttons_between_lines() {
1230 let definition = DeviceDefinition {
1231 id: DeviceId::new("SEP001122334455").unwrap(),
1232 description: "Desk".into(),
1233 transport: StationTransportRequirement::Either,
1234 signaling_qos: None,
1235 buttons: vec![
1236 line_button(1, "1001"),
1237 ButtonDefinition::Unused,
1238 ButtonDefinition::SpeedDial(SpeedDialDefinition {
1239 instance: 1,
1240 number: "2001".into(),
1241 display_name: "Warehouse".into(),
1242 }),
1243 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1244 instance: 1,
1245 number: "2002".into(),
1246 display_name: "Dispatch".into(),
1247 hint: "2002@internal".into(),
1248 }),
1249 ButtonDefinition::Feature(FeatureDefinition {
1250 instance: 1,
1251 label: "DND".into(),
1252 feature: crate::message::values::ButtonType::DoNotDisturb,
1253 }),
1254 ButtonDefinition::Service(ServiceDefinition {
1255 instance: 1,
1256 label: "Directory".into(),
1257 url: "http://pbx.test/directory".into(),
1258 }),
1259 ButtonDefinition::AddonModule(AddonModuleDefinition {
1260 slot: 1,
1261 device_type: crate::message::values::DeviceType::CiscoAddon7914,
1262 }),
1263 line_button(2, "1002"),
1264 ],
1265 soft_keys: SoftKeyProfile::default(),
1266 ui: StationUiPolicy::default(),
1267 };
1268
1269 definition.validate().unwrap();
1270 assert_eq!(definition.line_count(), 2);
1271 assert_eq!(definition.line(2).unwrap().number, "1002");
1272 }
1273
1274 #[test]
1275 fn station_definition_rejects_invalid_signaling_markings() {
1276 let mut definition = DeviceDefinition {
1277 id: DeviceId::new("SEP001122334455").unwrap(),
1278 description: "Desk".into(),
1279 transport: StationTransportRequirement::Either,
1280 signaling_qos: Some(SignalingQos::new(64, 0)),
1281 buttons: vec![line_button(1, "1001")],
1282 soft_keys: SoftKeyProfile::default(),
1283 ui: StationUiPolicy::default(),
1284 };
1285
1286 assert!(matches!(
1287 definition.validate(),
1288 Err(CodecError::InvalidDefinition(message)) if message.contains("DSCP 64")
1289 ));
1290
1291 definition.signaling_qos = Some(SignalingQos::new(26, 8));
1292 assert!(matches!(
1293 definition.validate(),
1294 Err(CodecError::InvalidDefinition(message)) if message.contains("COS 8")
1295 ));
1296 }
1297
1298 #[test]
1299 fn line_appearance_keeps_logical_and_device_specific_state_separate() {
1300 let logical = LineDefinition {
1301 number: "1001".into(),
1302 display_name: "Reception".into(),
1303 };
1304 let mut appearance = LineAppearance::new(2, logical.clone());
1305 appearance.label = Some("Private key".into());
1306 appearance.caller_id = CallerIdOverride {
1307 name: Some("Private desk".into()),
1308 number: None,
1309 };
1310 appearance.ring_mode = AppearanceRingMode::Silent;
1311 appearance.subscription_identity = Some("1001@internal".into());
1312 appearance.privacy = true;
1313
1314 assert_eq!(appearance.line, logical);
1315 assert_eq!(appearance.display_label(), "Private key");
1316 assert_eq!(appearance.number, "1001");
1317 assert_eq!(appearance.id, AppearanceId::new(2));
1318 }
1319
1320 #[test]
1321 fn station_definition_rejects_zero_and_duplicate_typed_instances() {
1322 let definition = DeviceDefinition {
1323 id: DeviceId::new("SEP001122334455").unwrap(),
1324 description: "Desk".into(),
1325 transport: StationTransportRequirement::Either,
1326 signaling_qos: None,
1327 buttons: vec![line_button(1, "1001"), line_button(1, "1002")],
1328 soft_keys: SoftKeyProfile::default(),
1329 ui: StationUiPolicy::default(),
1330 };
1331 assert!(matches!(
1332 definition.validate(),
1333 Err(CodecError::InvalidDefinition(message))
1334 if message.contains("repeats line button instance 1")
1335 ));
1336
1337 let definition = DeviceDefinition {
1338 id: DeviceId::new("SEP001122334455").unwrap(),
1339 description: "Desk".into(),
1340 transport: StationTransportRequirement::Either,
1341 signaling_qos: None,
1342 buttons: vec![
1343 line_button(1, "1001"),
1344 ButtonDefinition::Feature(FeatureDefinition {
1345 instance: 0,
1346 label: "DND".into(),
1347 feature: crate::message::values::ButtonType::DoNotDisturb,
1348 }),
1349 ],
1350 soft_keys: SoftKeyProfile::default(),
1351 ui: StationUiPolicy::default(),
1352 };
1353 assert!(matches!(
1354 definition.validate(),
1355 Err(CodecError::InvalidDefinition(message))
1356 if message.contains("feature button with instance zero")
1357 ));
1358 }
1359
1360 #[test]
1361 fn station_definition_enforces_protocol_button_limit() {
1362 let definition = DeviceDefinition {
1363 id: DeviceId::new("SEP001122334455").unwrap(),
1364 description: "Desk".into(),
1365 transport: StationTransportRequirement::Either,
1366 signaling_qos: None,
1367 buttons: std::iter::once(line_button(1, "1001"))
1368 .chain(std::iter::repeat_n(ButtonDefinition::Unused, 42))
1369 .collect(),
1370 soft_keys: SoftKeyProfile::default(),
1371 ui: StationUiPolicy::default(),
1372 };
1373 assert!(matches!(
1374 definition.validate(),
1375 Err(CodecError::InvalidDefinition(message))
1376 if message.contains("protocol limit is 42")
1377 ));
1378 }
1379
1380 #[test]
1381 fn service_urls_require_bounded_http_parameters() {
1382 let service_device = |url: &str| DeviceDefinition {
1383 id: DeviceId::new("SEP001122334455").unwrap(),
1384 description: "Desk".into(),
1385 transport: StationTransportRequirement::Either,
1386 signaling_qos: None,
1387 buttons: vec![
1388 line_button(1, "1001"),
1389 ButtonDefinition::Service(ServiceDefinition {
1390 instance: 1,
1391 label: "Directory".into(),
1392 url: url.into(),
1393 }),
1394 ],
1395 soft_keys: SoftKeyProfile::default(),
1396 ui: StationUiPolicy::default(),
1397 };
1398
1399 service_device("https://pbx.example/sccp/directory?q=Fran%C3%A7ois&page=2")
1400 .validate()
1401 .unwrap();
1402 service_device("https://user:secret@pbx.example/service")
1403 .validate()
1404 .unwrap();
1405 for invalid in [
1406 "file:///etc/passwd",
1407 "https://pbx.example/service#private",
1408 "https://pbx.example/service?=missing-name",
1409 "not a URL",
1410 ] {
1411 let error = service_device(invalid).validate().unwrap_err().to_string();
1412 assert!(!error.contains(invalid));
1413 }
1414 let excessive = format!(
1415 "https://pbx.example/service?{}",
1416 (0..33)
1417 .map(|index| format!("p{index}=v"))
1418 .collect::<Vec<_>>()
1419 .join("&")
1420 );
1421 assert!(service_device(&excessive).validate().is_err());
1422 }
1423
1424 #[test]
1425 fn soft_key_profiles_require_every_mode_and_unique_known_actions() {
1426 assert!(matches!(
1427 SoftKeyProfile::new([(KeyMode::OnHook, vec![SoftKey::NewCall])]),
1428 Err(CodecError::InvalidDefinition(message))
1429 if message.contains("every known key mode")
1430 ));
1431
1432 let duplicate = SoftKeyProfile::new(KeyMode::ALL_KNOWN.iter().copied().map(|mode| {
1433 (
1434 mode,
1435 if mode == KeyMode::Connected {
1436 vec![SoftKey::Hold, SoftKey::Hold]
1437 } else {
1438 Vec::new()
1439 },
1440 )
1441 }));
1442 assert!(matches!(
1443 duplicate,
1444 Err(CodecError::InvalidDefinition(message)) if message.contains("repeats action")
1445 ));
1446 }
1447}