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
492pub(crate) const MAX_STATION_FEATURE_LABEL_BYTES: usize = 39;
493
494#[derive(Clone, Debug, Eq, PartialEq)]
496pub struct RecordingButtonDefinition {
497 pub instance: u32,
498 pub label: String,
500}
501
502#[derive(Clone, Debug, Eq, PartialEq)]
504pub struct ServiceDefinition {
505 pub instance: u32,
507 pub label: String,
508 pub url: String,
513}
514
515#[derive(Clone, Debug, Eq, PartialEq)]
517pub struct AddonModuleDefinition {
518 pub slot: u32,
520 pub device_type: crate::message::values::DeviceType,
521}
522
523impl AddonModuleDefinition {
524 pub const fn button_capacity(&self) -> Option<usize> {
526 use crate::message::values::DeviceType;
527
528 match self.device_type {
529 DeviceType::CiscoAddon7914 => Some(14),
530 DeviceType::CiscoAddon7915_12 | DeviceType::CiscoAddon7916_12 => Some(12),
531 DeviceType::CiscoAddon7915_24 | DeviceType::CiscoAddon7916_24 => Some(24),
532 DeviceType::AddonSpa500s | DeviceType::AddonSpa500ds | DeviceType::AddonSpa932ds => {
533 Some(32)
534 }
535 _ => None,
536 }
537 }
538}
539
540#[derive(Clone, Debug, Eq, PartialEq)]
545pub enum ButtonDefinition {
546 Line(LineAppearance),
547 SpeedDial(SpeedDialDefinition),
548 BlfSpeedDial(BlfSpeedDialDefinition),
549 Feature(FeatureDefinition),
550 Recording(RecordingButtonDefinition),
551 Service(ServiceDefinition),
552 AddonModule(AddonModuleDefinition),
553 Unused,
554}
555
556#[derive(Clone, Debug, Eq, PartialEq)]
561pub struct SoftKeyProfile {
562 sets: HashMap<KeyMode, Vec<SoftKey>>,
563}
564
565impl SoftKeyProfile {
566 pub const MAX_KEYS_PER_MODE: usize = 16;
568
569 pub fn new(
574 sets: impl IntoIterator<Item = (KeyMode, Vec<SoftKey>)>,
575 ) -> Result<Self, CodecError> {
576 let profile = Self {
577 sets: sets.into_iter().collect(),
578 };
579 profile.validate()?;
580 Ok(profile)
581 }
582
583 pub fn empty() -> Self {
584 Self {
585 sets: KeyMode::ALL_KNOWN
586 .iter()
587 .copied()
588 .map(|mode| (mode, Vec::new()))
589 .collect(),
590 }
591 }
592
593 pub fn built_in() -> Self {
596 let mut profile = Self::empty();
597 profile.sets.extend([
598 (KeyMode::OnHook, vec![SoftKey::NewCall]),
599 (
600 KeyMode::Connected,
601 vec![SoftKey::Hold, SoftKey::EndCall, SoftKey::Transfer],
602 ),
603 (
604 KeyMode::OnHold,
605 vec![SoftKey::Resume, SoftKey::NewCall, SoftKey::EndCall],
606 ),
607 (KeyMode::RingIn, vec![SoftKey::Answer, SoftKey::EndCall]),
608 (KeyMode::OffHook, vec![SoftKey::EndCall]),
609 (
610 KeyMode::ConnectedTransfer,
611 vec![SoftKey::Hold, SoftKey::EndCall, SoftKey::Transfer],
612 ),
613 (
614 KeyMode::DigitsFollowing,
615 vec![SoftKey::Backspace, SoftKey::EndCall, SoftKey::Dial],
616 ),
617 (
618 KeyMode::ConnectedConference,
619 vec![SoftKey::Hold, SoftKey::EndCall],
620 ),
621 (KeyMode::RingOut, vec![SoftKey::EndCall]),
622 (
623 KeyMode::OffHookFeature,
624 vec![SoftKey::Resume, SoftKey::NewCall, SoftKey::EndCall],
625 ),
626 (
627 KeyMode::OnHookStealable,
628 vec![SoftKey::Intercept, SoftKey::NewCall],
629 ),
630 (
631 KeyMode::HoldConference,
632 vec![SoftKey::Resume, SoftKey::NewCall, SoftKey::EndCall],
633 ),
634 ]);
635 profile
636 }
637
638 pub fn actions(&self, mode: KeyMode) -> &[SoftKey] {
639 self.sets.get(&mode).map_or(&[], Vec::as_slice)
640 }
641
642 pub fn allows(&self, mode: KeyMode, action: SoftKey) -> bool {
643 action.is_known() && self.actions(mode).contains(&action)
644 }
645
646 pub fn valid_mask(&self, mode: KeyMode) -> u32 {
648 let count = self.actions(mode).len();
649 if count == 0 { 0 } else { (1_u32 << count) - 1 }
650 }
651
652 pub fn template_actions(&self) -> Vec<SoftKey> {
655 if self == &Self::built_in() {
656 return SoftKey::ALL_KNOWN.to_vec();
657 }
658 let configured: HashSet<_> = KeyMode::ALL_KNOWN
659 .iter()
660 .flat_map(|mode| self.actions(*mode).iter().copied())
661 .collect();
662 SoftKey::ALL_KNOWN
663 .iter()
664 .copied()
665 .filter(|action| configured.contains(action))
666 .collect()
667 }
668
669 pub fn validate(&self) -> Result<(), CodecError> {
671 if self.sets.len() != KeyMode::ALL_KNOWN.len()
672 || KeyMode::ALL_KNOWN
673 .iter()
674 .any(|mode| !self.sets.contains_key(mode))
675 {
676 return Err(CodecError::InvalidDefinition(
677 "soft-key profile must define every known key mode".into(),
678 ));
679 }
680 for (&mode, actions) in &self.sets {
681 if !mode.is_known() {
682 return Err(CodecError::InvalidDefinition(format!(
683 "soft-key profile contains unknown key mode {}",
684 mode.wire_value()
685 )));
686 }
687 if actions.len() > Self::MAX_KEYS_PER_MODE {
688 return Err(CodecError::InvalidDefinition(format!(
689 "soft-key mode {} contains {} actions; the protocol limit is {}",
690 mode.wire_value(),
691 actions.len(),
692 Self::MAX_KEYS_PER_MODE
693 )));
694 }
695 let mut seen = HashSet::new();
696 for &action in actions {
697 if !action.is_known() {
698 return Err(CodecError::InvalidDefinition(format!(
699 "soft-key mode {} contains unknown action {}",
700 mode.wire_value(),
701 action.wire_value()
702 )));
703 }
704 if !seen.insert(action) {
705 return Err(CodecError::InvalidDefinition(format!(
706 "soft-key mode {} repeats action {}",
707 mode.wire_value(),
708 action.wire_value()
709 )));
710 }
711 }
712 }
713 Ok(())
714 }
715}
716
717impl Default for SoftKeyProfile {
718 fn default() -> Self {
719 Self::built_in()
720 }
721}
722
723#[derive(Clone, Debug, Eq, PartialEq)]
730pub struct DeviceDefinition {
731 pub id: DeviceId,
732 pub description: String,
733 pub transport: StationTransportRequirement,
734 pub signaling_qos: Option<SignalingQos>,
737 pub buttons: Vec<ButtonDefinition>,
743 pub soft_keys: SoftKeyProfile,
745 pub ui: StationUiPolicy,
747}
748
749#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
751pub enum StationTransportRequirement {
752 Clear,
753 Secure,
754 #[default]
755 Either,
756}
757
758#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
760pub enum StationTransport {
761 Clear,
762 Secure,
763}
764
765#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
772pub struct SignalingQos {
773 pub dscp: u8,
774 pub cos: u8,
775}
776
777impl SignalingQos {
778 pub const fn new(dscp: u8, cos: u8) -> Self {
779 Self { dscp, cos }
780 }
781
782 pub(crate) fn validate(self) -> Result<(), CodecError> {
783 if self.dscp > 63 {
784 return Err(CodecError::InvalidDefinition(format!(
785 "signaling DSCP {} is outside 0..=63",
786 self.dscp
787 )));
788 }
789 if self.cos > 7 {
790 return Err(CodecError::InvalidDefinition(format!(
791 "signaling COS {} is outside 0..=7",
792 self.cos
793 )));
794 }
795 Ok(())
796 }
797}
798
799#[derive(Clone, Copy, Debug, Eq, PartialEq)]
802pub struct StationUiPolicy {
803 pub placed_calls_redial_menu: bool,
806 pub hinted_ringing_notification: bool,
809 pub speed_dial_await_further_digits: bool,
812 pub mwi_lamp_mode: crate::message::values::LampMode,
814 pub mwi_on_call: bool,
816 pub legacy_code_page: LegacyCodePage,
819}
820
821#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
824pub enum LegacyCodePage {
825 #[default]
826 Iso8859_1,
827 Ascii,
828}
829
830impl Default for StationUiPolicy {
831 fn default() -> Self {
832 Self {
833 placed_calls_redial_menu: false,
834 hinted_ringing_notification: false,
835 speed_dial_await_further_digits: false,
836 mwi_lamp_mode: crate::message::values::LampMode::On,
837 mwi_on_call: false,
838 legacy_code_page: LegacyCodePage::Iso8859_1,
839 }
840 }
841}
842
843impl DeviceDefinition {
844 pub fn validate(&self) -> Result<(), CodecError> {
846 const MAX_BUTTONS: usize = 256;
850
851 self.soft_keys.validate()?;
852 if let Some(signaling_qos) = self.signaling_qos {
853 signaling_qos.validate()?;
854 }
855
856 if self.buttons.len() > MAX_BUTTONS {
857 return Err(CodecError::InvalidDefinition(format!(
858 "device {} has {} buttons; the logical layout limit is {MAX_BUTTONS}",
859 self.id,
860 self.buttons.len()
861 )));
862 }
863
864 let mut expanded_buttons = 0_usize;
865 let mut addon_buttons_remaining = None;
866 for button in &self.buttons {
867 if let ButtonDefinition::AddonModule(addon) = button {
868 expanded_buttons += addon_buttons_remaining.take().unwrap_or_default();
869 addon_buttons_remaining = Some(addon.button_capacity().ok_or_else(|| {
870 CodecError::InvalidDefinition(format!(
871 "device {} has unsupported addon-module type {}",
872 self.id,
873 addon.device_type.wire_value()
874 ))
875 })?);
876 continue;
877 }
878 expanded_buttons += 1;
879 if let Some(remaining) = &mut addon_buttons_remaining {
880 if *remaining == 0 {
881 return Err(CodecError::InvalidDefinition(format!(
882 "device {} configures more buttons than its addon module provides",
883 self.id
884 )));
885 }
886 *remaining -= 1;
887 }
888 }
889 expanded_buttons += addon_buttons_remaining.unwrap_or_default();
890 if expanded_buttons > MAX_BUTTONS {
891 return Err(CodecError::InvalidDefinition(format!(
892 "device {} expands to {expanded_buttons} buttons; the logical layout limit is {MAX_BUTTONS}",
893 self.id
894 )));
895 }
896
897 let mut instances = HashSet::new();
898 let mut appearance_ids = HashSet::new();
899 for button in &self.buttons {
900 let Some((kind, instance)) = button.instance_key() else {
901 continue;
902 };
903 if instance == 0 {
904 return Err(CodecError::InvalidDefinition(format!(
905 "device {} has a {kind} button with instance zero",
906 self.id
907 )));
908 }
909 if kind != ButtonNamespace::AddonModule && instance > MAX_STATION_BUTTON_INSTANCE {
914 return Err(CodecError::InvalidDefinition(format!(
915 "device {} has a {kind} button with instance {instance}; maximum wire instance is {}",
916 self.id, MAX_STATION_BUTTON_INSTANCE
917 )));
918 }
919 if !instances.insert((kind, instance)) {
920 return Err(CodecError::InvalidDefinition(format!(
921 "device {} repeats {kind} button instance {instance}",
922 self.id
923 )));
924 }
925 match button {
926 ButtonDefinition::Line(appearance) => {
927 if appearance.id.get() == 0 {
928 return Err(CodecError::InvalidDefinition(format!(
929 "device {} has a line appearance with identifier zero",
930 self.id
931 )));
932 }
933 if !appearance_ids.insert(appearance.id) {
934 return Err(CodecError::InvalidDefinition(format!(
935 "device {} repeats line appearance identifier {}",
936 self.id, appearance.id
937 )));
938 }
939 }
940 ButtonDefinition::Recording(recording) => {
941 validate_recording_button_definition(&self.id, recording)?;
942 }
943 ButtonDefinition::Service(service) => {
944 validate_service_definition(&self.id, service)?;
945 }
946 _ => {}
947 }
948 }
949
950 let lines: Vec<_> = self.lines().collect();
951 if lines.is_empty() {
952 return Err(CodecError::InvalidDefinition(format!(
953 "device {} has no lines",
954 self.id
955 )));
956 }
957 let permits_sparse_lines = self.buttons.iter().any(|button| {
961 matches!(
962 button,
963 ButtonDefinition::Feature(feature)
964 if feature.feature == crate::message::values::ButtonType::Mobility
965 )
966 });
967 for (expected, line) in (1_u32..).zip(lines) {
968 if !permits_sparse_lines && line.instance != expected {
969 return Err(CodecError::InvalidDefinition(format!(
970 "device {} line instances must be contiguous from 1",
971 self.id
972 )));
973 }
974 if line.number.is_empty() || line.number.len() > 24 {
975 return Err(CodecError::InvalidDefinition(format!(
976 "device {} has an invalid line number",
977 self.id
978 )));
979 }
980 }
981 Ok(())
982 }
983
984 pub fn lines(&self) -> impl Iterator<Item = &LineAppearance> {
985 self.buttons.iter().filter_map(|button| match button {
986 ButtonDefinition::Line(line) => Some(line),
987 _ => None,
988 })
989 }
990
991 pub fn line(&self, instance: u32) -> Option<&LineAppearance> {
992 self.lines().find(|line| line.instance == instance)
993 }
994
995 pub fn first_line(&self) -> Option<&LineAppearance> {
996 self.lines().next()
997 }
998
999 pub fn line_count(&self) -> usize {
1000 self.lines().count()
1001 }
1002
1003 pub(crate) fn feature_button(&self, instance: u32) -> Option<&FeatureDefinition> {
1004 self.buttons.iter().find_map(|button| match button {
1005 ButtonDefinition::Feature(feature) if feature.instance == instance => Some(feature),
1006 _ => None,
1007 })
1008 }
1009
1010 pub(crate) fn recording_button(&self, instance: u32) -> Option<&RecordingButtonDefinition> {
1011 self.buttons.iter().find_map(|button| match button {
1012 ButtonDefinition::Recording(recording) if recording.instance == instance => {
1013 Some(recording)
1014 }
1015 _ => None,
1016 })
1017 }
1018
1019 pub(crate) fn blf_button(&self, instance: u32) -> Option<&BlfSpeedDialDefinition> {
1020 self.buttons.iter().find_map(|button| match button {
1021 ButtonDefinition::BlfSpeedDial(blf) if blf.instance == instance => Some(blf),
1022 _ => None,
1023 })
1024 }
1025}
1026
1027fn validate_recording_button_definition(
1028 device: &DeviceId,
1029 recording: &RecordingButtonDefinition,
1030) -> Result<(), CodecError> {
1031 if recording.label.is_empty()
1032 || recording.label.len() > MAX_STATION_FEATURE_LABEL_BYTES
1033 || recording.label.chars().any(char::is_control)
1034 {
1035 return Err(CodecError::InvalidDefinition(format!(
1036 "device {device} has an invalid recording-button label"
1037 )));
1038 }
1039 Ok(())
1040}
1041
1042fn validate_service_definition(
1043 device: &DeviceId,
1044 service: &ServiceDefinition,
1045) -> Result<(), CodecError> {
1046 const MAX_SERVICE_URL_BYTES: usize = 255;
1047 const MAX_SERVICE_PARAMETERS: usize = 32;
1048 const MAX_SERVICE_PARAMETER_BYTES: usize = 128;
1049
1050 if service.label.is_empty()
1051 || service.label.len() > MAX_STATION_FEATURE_LABEL_BYTES
1052 || service.label.chars().any(char::is_control)
1053 {
1054 return Err(CodecError::InvalidDefinition(format!(
1055 "device {device} has an invalid service label"
1056 )));
1057 }
1058 if service.url.is_empty()
1059 || service.url.len() > MAX_SERVICE_URL_BYTES
1060 || service.url.chars().any(char::is_control)
1061 {
1062 return Err(CodecError::InvalidDefinition(format!(
1063 "device {device} has an invalid service URL"
1064 )));
1065 }
1066 let url = url::Url::parse(&service.url).map_err(|_| {
1067 CodecError::InvalidDefinition(format!("device {device} has a malformed service URL"))
1068 })?;
1069 if !matches!(url.scheme(), "http" | "https")
1070 || url.host_str().is_none()
1071 || url.fragment().is_some()
1072 {
1073 return Err(CodecError::InvalidDefinition(format!(
1074 "device {device} service URL must be HTTP(S) without a fragment"
1075 )));
1076 }
1077 let parameters = url.query_pairs().collect::<Vec<_>>();
1078 if parameters.len() > MAX_SERVICE_PARAMETERS
1079 || parameters.iter().any(|(name, value)| {
1080 name.is_empty()
1081 || name.len() > MAX_SERVICE_PARAMETER_BYTES
1082 || value.len() > MAX_SERVICE_PARAMETER_BYTES
1083 || name.chars().chain(value.chars()).any(char::is_control)
1084 })
1085 {
1086 return Err(CodecError::InvalidDefinition(format!(
1087 "device {device} service URL has invalid or excessive query parameters"
1088 )));
1089 }
1090 Ok(())
1091}
1092
1093impl ButtonDefinition {
1094 fn instance_key(&self) -> Option<(ButtonNamespace, u32)> {
1095 match self {
1096 Self::Line(definition) => Some((ButtonNamespace::Line, definition.instance)),
1097 Self::SpeedDial(definition) => Some((ButtonNamespace::SpeedDial, definition.instance)),
1098 Self::BlfSpeedDial(definition) => Some((ButtonNamespace::Feature, definition.instance)),
1099 Self::Feature(definition) => Some((ButtonNamespace::Feature, definition.instance)),
1100 Self::Recording(definition) => Some((ButtonNamespace::Feature, definition.instance)),
1101 Self::Service(definition) => Some((ButtonNamespace::Service, definition.instance)),
1102 Self::AddonModule(definition) => Some((ButtonNamespace::AddonModule, definition.slot)),
1103 Self::Unused => None,
1104 }
1105 }
1106}
1107
1108#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1109enum ButtonNamespace {
1110 Line,
1111 SpeedDial,
1112 Feature,
1113 Service,
1114 AddonModule,
1115}
1116
1117impl fmt::Display for ButtonNamespace {
1118 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1119 formatter.write_str(match self {
1120 Self::Line => "line",
1121 Self::SpeedDial => "speed dial",
1122 Self::Feature => "feature",
1123 Self::Service => "service URL",
1124 Self::AddonModule => "addon module",
1125 })
1126 }
1127}
1128
1129#[derive(Clone, Debug, Eq, PartialEq)]
1134pub struct DeviceRegistration {
1135 pub id: DeviceId,
1136 pub peer: SocketAddr,
1137 pub transport: StationTransport,
1138 pub reported_address: Option<Ipv4Addr>,
1139 pub reported_ipv6_address: Option<Ipv6Addr>,
1140 pub device_type: DeviceType,
1141 pub protocol: ProtocolVersion,
1142 pub firmware: String,
1143}
1144
1145impl DeviceRegistration {
1146 pub fn reported_address_for_peer(&self) -> Option<IpAddr> {
1149 match self.peer.ip() {
1150 IpAddr::V4(_) => self.reported_address.map(IpAddr::V4),
1151 IpAddr::V6(peer) => peer.to_ipv4_mapped().map_or_else(
1152 || self.reported_ipv6_address.map(IpAddr::V6),
1153 |_| self.reported_address.map(IpAddr::V4),
1154 ),
1155 }
1156 }
1157}
1158
1159#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1161pub enum CallDirection {
1162 Inbound,
1163 Outbound,
1164}
1165
1166impl From<CallDirection> for CallType {
1167 fn from(value: CallDirection) -> Self {
1168 match value {
1169 CallDirection::Inbound => Self::Inbound,
1170 CallDirection::Outbound => Self::Outbound,
1171 }
1172 }
1173}
1174
1175#[derive(Clone, Debug, Eq, PartialEq)]
1181pub struct CallInfo {
1182 pub direction: CallDirection,
1183 pub calling_name: String,
1184 pub calling_number: String,
1185 pub called_name: String,
1186 pub called_number: String,
1187 pub original_called_name: String,
1188 pub original_called_number: String,
1189 pub last_redirecting_name: String,
1190 pub last_redirecting_number: String,
1191 pub original_redirect_reason: u32,
1192 pub last_redirect_reason: u32,
1193 pub party_restrictions: u32,
1195}
1196
1197impl Default for CallInfo {
1198 fn default() -> Self {
1199 Self {
1200 direction: CallDirection::Outbound,
1201 calling_name: String::new(),
1202 calling_number: String::new(),
1203 called_name: String::new(),
1204 called_number: String::new(),
1205 original_called_name: String::new(),
1206 original_called_number: String::new(),
1207 last_redirecting_name: String::new(),
1208 last_redirecting_number: String::new(),
1209 original_redirect_reason: 0,
1210 last_redirect_reason: 0,
1211 party_restrictions: 0,
1212 }
1213 }
1214}
1215
1216pub const DEFAULT_AUDIO_PACKET_MS: u32 = 20;
1218pub const DEFAULT_AUDIO_MAX_FRAMES_PER_PACKET: u32 = 0;
1219
1220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1223pub struct AudioProcessingPolicy {
1224 pub echo_cancellation: EchoCancellation,
1225 pub silence_suppression: SilenceSuppression,
1226}
1227
1228impl Default for AudioProcessingPolicy {
1229 fn default() -> Self {
1230 Self {
1231 echo_cancellation: EchoCancellation::On,
1232 silence_suppression: SilenceSuppression::Off,
1233 }
1234 }
1235}
1236
1237#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1242pub struct MediaEndpoint {
1243 pub address: IpAddr,
1244 pub rtp_port: u16,
1245 pub rtcp_port: u16,
1246 pub codec: Codec,
1247 pub packet_ms: u32,
1248 pub max_frames_per_packet: u32,
1249 pub telephone_event_payload: u8,
1251}
1252
1253#[cfg(test)]
1254mod tests {
1255 use super::*;
1256
1257 #[test]
1258 fn identifiers_are_explicit_and_lossless() {
1259 let reference = CallReference::new(42);
1260 assert_eq!(reference.get(), 42);
1261 assert_eq!(u32::from(reference), 42);
1262
1263 let appearance = AppearanceId::new(7);
1264 assert_eq!(appearance.get(), 7);
1265 assert_eq!(u32::from(appearance), 7);
1266
1267 let conference = ConferenceId::new(9);
1268 assert_eq!(conference.get(), 9);
1269
1270 let participant = ParticipantId::new(11);
1271 assert_eq!(participant.get(), 11);
1272 }
1273
1274 #[test]
1275 fn device_id_is_canonicalized() {
1276 let id: DeviceId = " sep001122334455 ".parse().unwrap();
1277 assert_eq!(id.as_str(), "SEP001122334455");
1278 assert_eq!(id.as_ref(), "SEP001122334455");
1279
1280 let mut devices = HashMap::new();
1281 devices.insert(id, "desk");
1282 assert_eq!(devices.get("SEP001122334455"), Some(&"desk"));
1283 }
1284
1285 #[test]
1286 fn registration_selects_the_report_matching_the_effective_peer_family() {
1287 let registration = DeviceRegistration {
1288 id: DeviceId::new("SEP001122334455").unwrap(),
1289 peer: "[2001:db8::20]:2000".parse().unwrap(),
1290 transport: StationTransport::Clear,
1291 reported_address: Some("192.0.2.20".parse().unwrap()),
1292 reported_ipv6_address: Some("2001:db8::20".parse().unwrap()),
1293 device_type: DeviceType::Cisco7962,
1294 protocol: ProtocolVersion::V22,
1295 firmware: "test".into(),
1296 };
1297 assert_eq!(
1298 registration.reported_address_for_peer(),
1299 Some("2001:db8::20".parse().unwrap())
1300 );
1301
1302 let mapped = DeviceRegistration {
1303 peer: "[::ffff:192.0.2.20]:2000".parse().unwrap(),
1304 ..registration
1305 };
1306 assert_eq!(
1307 mapped.reported_address_for_peer(),
1308 Some("192.0.2.20".parse().unwrap())
1309 );
1310 }
1311
1312 fn line_button(instance: u32, number: &str) -> ButtonDefinition {
1313 ButtonDefinition::Line(LineAppearance::new(
1314 instance,
1315 LineDefinition {
1316 number: number.into(),
1317 display_name: number.into(),
1318 },
1319 ))
1320 }
1321
1322 #[test]
1323 fn station_definition_accepts_non_line_buttons_between_lines() {
1324 let definition = DeviceDefinition {
1325 id: DeviceId::new("SEP001122334455").unwrap(),
1326 description: "Desk".into(),
1327 transport: StationTransportRequirement::Either,
1328 signaling_qos: None,
1329 buttons: vec![
1330 line_button(1, "1001"),
1331 ButtonDefinition::Unused,
1332 ButtonDefinition::SpeedDial(SpeedDialDefinition {
1333 instance: 1,
1334 number: "2001".into(),
1335 display_name: "Warehouse".into(),
1336 }),
1337 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1338 instance: 1,
1339 number: "2002".into(),
1340 display_name: "Dispatch".into(),
1341 }),
1342 ButtonDefinition::Feature(FeatureDefinition {
1343 instance: 2,
1344 label: "DND".into(),
1345 feature: crate::message::values::ButtonType::DoNotDisturb,
1346 }),
1347 ButtonDefinition::Service(ServiceDefinition {
1348 instance: 1,
1349 label: "Directory".into(),
1350 url: "http://pbx.test/directory".into(),
1351 }),
1352 ButtonDefinition::AddonModule(AddonModuleDefinition {
1353 slot: 1,
1354 device_type: crate::message::values::DeviceType::CiscoAddon7914,
1355 }),
1356 line_button(2, "1002"),
1357 ],
1358 soft_keys: SoftKeyProfile::default(),
1359 ui: StationUiPolicy::default(),
1360 };
1361
1362 definition.validate().unwrap();
1363 assert_eq!(definition.line_count(), 2);
1364 assert_eq!(definition.line(2).unwrap().number, "1002");
1365 }
1366
1367 #[test]
1368 fn station_definition_rejects_invalid_signaling_markings() {
1369 let mut definition = DeviceDefinition {
1370 id: DeviceId::new("SEP001122334455").unwrap(),
1371 description: "Desk".into(),
1372 transport: StationTransportRequirement::Either,
1373 signaling_qos: Some(SignalingQos::new(64, 0)),
1374 buttons: vec![line_button(1, "1001")],
1375 soft_keys: SoftKeyProfile::default(),
1376 ui: StationUiPolicy::default(),
1377 };
1378
1379 assert!(matches!(
1380 definition.validate(),
1381 Err(CodecError::InvalidDefinition(message)) if message.contains("DSCP 64")
1382 ));
1383
1384 definition.signaling_qos = Some(SignalingQos::new(26, 8));
1385 assert!(matches!(
1386 definition.validate(),
1387 Err(CodecError::InvalidDefinition(message)) if message.contains("COS 8")
1388 ));
1389 }
1390
1391 #[test]
1392 fn line_appearance_keeps_logical_and_device_specific_state_separate() {
1393 let logical = LineDefinition {
1394 number: "1001".into(),
1395 display_name: "Reception".into(),
1396 };
1397 let mut appearance = LineAppearance::new(2, logical.clone());
1398 appearance.label = Some("Private key".into());
1399 appearance.caller_id = CallerIdOverride {
1400 name: Some("Private desk".into()),
1401 number: None,
1402 };
1403 appearance.ring_mode = AppearanceRingMode::Silent;
1404 appearance.subscription_identity = Some("1001@internal".into());
1405 appearance.privacy = true;
1406
1407 assert_eq!(appearance.line, logical);
1408 assert_eq!(appearance.display_label(), "Private key");
1409 assert_eq!(appearance.number, "1001");
1410 assert_eq!(appearance.id, AppearanceId::new(2));
1411 }
1412
1413 #[test]
1414 fn station_definition_rejects_zero_and_duplicate_typed_instances() {
1415 let definition = DeviceDefinition {
1416 id: DeviceId::new("SEP001122334455").unwrap(),
1417 description: "Desk".into(),
1418 transport: StationTransportRequirement::Either,
1419 signaling_qos: None,
1420 buttons: vec![line_button(1, "1001"), line_button(1, "1002")],
1421 soft_keys: SoftKeyProfile::default(),
1422 ui: StationUiPolicy::default(),
1423 };
1424 assert!(matches!(
1425 definition.validate(),
1426 Err(CodecError::InvalidDefinition(message))
1427 if message.contains("repeats line button instance 1")
1428 ));
1429
1430 let definition = DeviceDefinition {
1431 id: DeviceId::new("SEP001122334455").unwrap(),
1432 description: "Desk".into(),
1433 transport: StationTransportRequirement::Either,
1434 signaling_qos: None,
1435 buttons: vec![
1436 line_button(1, "1001"),
1437 ButtonDefinition::Feature(FeatureDefinition {
1438 instance: 0,
1439 label: "DND".into(),
1440 feature: crate::message::values::ButtonType::DoNotDisturb,
1441 }),
1442 ],
1443 soft_keys: SoftKeyProfile::default(),
1444 ui: StationUiPolicy::default(),
1445 };
1446 assert!(matches!(
1447 definition.validate(),
1448 Err(CodecError::InvalidDefinition(message))
1449 if message.contains("feature button with instance zero")
1450 ));
1451
1452 let definition = DeviceDefinition {
1453 id: DeviceId::new("SEP001122334455").unwrap(),
1454 description: "Desk".into(),
1455 transport: StationTransportRequirement::Either,
1456 signaling_qos: None,
1457 buttons: vec![
1458 line_button(1, "1001"),
1459 ButtonDefinition::Feature(FeatureDefinition {
1460 instance: 1,
1461 label: "DND".into(),
1462 feature: crate::message::values::ButtonType::DoNotDisturb,
1463 }),
1464 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1465 instance: 1,
1466 number: "2001".into(),
1467 display_name: "Warehouse".into(),
1468 }),
1469 ],
1470 soft_keys: SoftKeyProfile::default(),
1471 ui: StationUiPolicy::default(),
1472 };
1473 assert!(matches!(
1474 definition.validate(),
1475 Err(CodecError::InvalidDefinition(message))
1476 if message.contains("repeats feature button instance 1")
1477 ));
1478
1479 let distinct_namespaces = DeviceDefinition {
1480 id: DeviceId::new("SEP001122334455").unwrap(),
1481 description: "Desk".into(),
1482 transport: StationTransportRequirement::Either,
1483 signaling_qos: None,
1484 buttons: vec![
1485 line_button(1, "1001"),
1486 ButtonDefinition::SpeedDial(SpeedDialDefinition {
1487 instance: 7,
1488 number: "2001".into(),
1489 display_name: "Warehouse".into(),
1490 }),
1491 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1492 instance: 7,
1493 number: "2002".into(),
1494 display_name: "Dispatch".into(),
1495 }),
1496 ],
1497 soft_keys: SoftKeyProfile::default(),
1498 ui: StationUiPolicy::default(),
1499 };
1500 distinct_namespaces.validate().unwrap();
1501 }
1502
1503 #[test]
1504 fn station_definition_enforces_one_byte_wire_instances_for_each_button_family() {
1505 let definition_with = |button| DeviceDefinition {
1506 id: DeviceId::new("SEP001122334455").unwrap(),
1507 description: "Desk".into(),
1508 transport: StationTransportRequirement::Either,
1509 signaling_qos: None,
1510 buttons: vec![line_button(1, "1001"), button],
1511 soft_keys: SoftKeyProfile::default(),
1512 ui: StationUiPolicy::default(),
1513 };
1514 let buttons = |instance| {
1515 [
1516 ButtonDefinition::SpeedDial(SpeedDialDefinition {
1517 instance,
1518 number: "2001".into(),
1519 display_name: "Speed".into(),
1520 }),
1521 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1522 instance,
1523 number: "2002".into(),
1524 display_name: "BLF".into(),
1525 }),
1526 ButtonDefinition::Feature(FeatureDefinition {
1527 instance,
1528 label: "DND".into(),
1529 feature: crate::message::values::ButtonType::DoNotDisturb,
1530 }),
1531 ButtonDefinition::Recording(RecordingButtonDefinition {
1532 instance,
1533 label: "Record calls".into(),
1534 }),
1535 ButtonDefinition::Service(ServiceDefinition {
1536 instance,
1537 label: "Directory".into(),
1538 url: "https://pbx.example/directory".into(),
1539 }),
1540 ]
1541 };
1542
1543 for button in buttons(255) {
1544 definition_with(button).validate().unwrap();
1545 }
1546 for button in buttons(256) {
1547 assert!(matches!(
1548 definition_with(button).validate(),
1549 Err(CodecError::InvalidDefinition(message))
1550 if message.contains("maximum wire instance is 255")
1551 ));
1552 }
1553
1554 let line_255 = DeviceDefinition {
1555 buttons: vec![line_button(255, "1001")],
1556 ..definition_with(ButtonDefinition::Unused)
1557 };
1558 let mut line_255 = line_255;
1560 line_255.buttons.insert(
1561 0,
1562 ButtonDefinition::Feature(FeatureDefinition {
1563 instance: 1,
1564 label: "Mobility".into(),
1565 feature: crate::message::values::ButtonType::Mobility,
1566 }),
1567 );
1568 line_255.validate().unwrap();
1569 line_255.buttons[1] = line_button(256, "1001");
1570 assert!(matches!(
1571 line_255.validate(),
1572 Err(CodecError::InvalidDefinition(message))
1573 if message.contains("maximum wire instance is 255")
1574 ));
1575 }
1576
1577 #[test]
1578 fn station_definition_rejects_recording_labels_that_cannot_fit_legacy_status() {
1579 let definition_with_label = |label: String| DeviceDefinition {
1580 id: DeviceId::new("SEP001122334455").unwrap(),
1581 description: "Desk".into(),
1582 transport: StationTransportRequirement::Either,
1583 signaling_qos: None,
1584 buttons: vec![
1585 line_button(1, "1001"),
1586 ButtonDefinition::Recording(RecordingButtonDefinition { instance: 1, label }),
1587 ],
1588 soft_keys: SoftKeyProfile::default(),
1589 ui: StationUiPolicy::default(),
1590 };
1591
1592 definition_with_label("R".repeat(MAX_STATION_FEATURE_LABEL_BYTES))
1593 .validate()
1594 .unwrap();
1595 for label in [
1596 String::new(),
1597 "bad\nlabel".into(),
1598 "R".repeat(MAX_STATION_FEATURE_LABEL_BYTES + 1),
1599 ] {
1600 assert!(matches!(
1601 definition_with_label(label).validate(),
1602 Err(CodecError::InvalidDefinition(message))
1603 if message.contains("invalid recording-button label")
1604 ));
1605 }
1606 }
1607
1608 #[test]
1609 fn blf_defaults_to_unknown() {
1610 assert_eq!(BlfState::default(), BlfState::Unknown);
1611 }
1612
1613 #[test]
1614 fn station_definition_enforces_bounded_logical_button_limit() {
1615 let definition = DeviceDefinition {
1616 id: DeviceId::new("SEP001122334455").unwrap(),
1617 description: "Desk".into(),
1618 transport: StationTransportRequirement::Either,
1619 signaling_qos: None,
1620 buttons: std::iter::once(line_button(1, "1001"))
1621 .chain(std::iter::repeat_n(ButtonDefinition::Unused, 256))
1622 .collect(),
1623 soft_keys: SoftKeyProfile::default(),
1624 ui: StationUiPolicy::default(),
1625 };
1626 assert!(matches!(
1627 definition.validate(),
1628 Err(CodecError::InvalidDefinition(message))
1629 if message.contains("logical layout limit is 256")
1630 ));
1631 }
1632
1633 #[test]
1634 fn service_urls_require_bounded_http_parameters() {
1635 let service_device = |url: &str| DeviceDefinition {
1636 id: DeviceId::new("SEP001122334455").unwrap(),
1637 description: "Desk".into(),
1638 transport: StationTransportRequirement::Either,
1639 signaling_qos: None,
1640 buttons: vec![
1641 line_button(1, "1001"),
1642 ButtonDefinition::Service(ServiceDefinition {
1643 instance: 1,
1644 label: "Directory".into(),
1645 url: url.into(),
1646 }),
1647 ],
1648 soft_keys: SoftKeyProfile::default(),
1649 ui: StationUiPolicy::default(),
1650 };
1651
1652 service_device("https://pbx.example/sccp/directory?q=Fran%C3%A7ois&page=2")
1653 .validate()
1654 .unwrap();
1655 service_device("https://user:secret@pbx.example/service")
1656 .validate()
1657 .unwrap();
1658 for invalid in [
1659 "file:///etc/passwd",
1660 "https://pbx.example/service#private",
1661 "https://pbx.example/service?=missing-name",
1662 "not a URL",
1663 ] {
1664 let error = service_device(invalid).validate().unwrap_err().to_string();
1665 assert!(!error.contains(invalid));
1666 }
1667 let excessive = format!(
1668 "https://pbx.example/service?{}",
1669 (0..33)
1670 .map(|index| format!("p{index}=v"))
1671 .collect::<Vec<_>>()
1672 .join("&")
1673 );
1674 assert!(service_device(&excessive).validate().is_err());
1675 }
1676
1677 #[test]
1678 fn soft_key_profiles_require_every_mode_and_unique_known_actions() {
1679 assert!(matches!(
1680 SoftKeyProfile::new([(KeyMode::OnHook, vec![SoftKey::NewCall])]),
1681 Err(CodecError::InvalidDefinition(message))
1682 if message.contains("every known key mode")
1683 ));
1684
1685 let duplicate = SoftKeyProfile::new(KeyMode::ALL_KNOWN.iter().copied().map(|mode| {
1686 (
1687 mode,
1688 if mode == KeyMode::Connected {
1689 vec![SoftKey::Hold, SoftKey::Hold]
1690 } else {
1691 Vec::new()
1692 },
1693 )
1694 }));
1695 assert!(matches!(
1696 duplicate,
1697 Err(CodecError::InvalidDefinition(message)) if message.contains("repeats action")
1698 ));
1699 }
1700}