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
723const MAX_STATION_HEADER_BYTES: usize = 39;
724
725#[derive(Clone, Debug, Eq, PartialEq)]
732pub struct DeviceDefinition {
733 pub id: DeviceId,
734 pub description: String,
739 pub transport: StationTransportRequirement,
740 pub signaling_qos: Option<SignalingQos>,
743 pub buttons: Vec<ButtonDefinition>,
749 pub soft_keys: SoftKeyProfile,
751 pub ui: StationUiPolicy,
753}
754
755#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
757pub enum StationTransportRequirement {
758 Clear,
759 Secure,
760 #[default]
761 Either,
762}
763
764#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
766pub enum StationTransport {
767 Clear,
768 Secure,
769}
770
771#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
778pub struct SignalingQos {
779 pub dscp: u8,
780 pub cos: u8,
781}
782
783impl SignalingQos {
784 pub const fn new(dscp: u8, cos: u8) -> Self {
785 Self { dscp, cos }
786 }
787
788 pub(crate) fn validate(self) -> Result<(), CodecError> {
789 if self.dscp > 63 {
790 return Err(CodecError::InvalidDefinition(format!(
791 "signaling DSCP {} is outside 0..=63",
792 self.dscp
793 )));
794 }
795 if self.cos > 7 {
796 return Err(CodecError::InvalidDefinition(format!(
797 "signaling COS {} is outside 0..=7",
798 self.cos
799 )));
800 }
801 Ok(())
802 }
803}
804
805#[derive(Clone, Copy, Debug, Eq, PartialEq)]
808pub struct StationUiPolicy {
809 pub placed_calls_redial_menu: bool,
812 pub hinted_ringing_notification: bool,
815 pub speed_dial_await_further_digits: bool,
818 pub mwi_lamp_mode: crate::message::values::LampMode,
820 pub mwi_on_call: bool,
822 pub legacy_code_page: LegacyCodePage,
825}
826
827#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
830pub enum LegacyCodePage {
831 #[default]
832 Iso8859_1,
833 Ascii,
834}
835
836impl Default for StationUiPolicy {
837 fn default() -> Self {
838 Self {
839 placed_calls_redial_menu: false,
840 hinted_ringing_notification: false,
841 speed_dial_await_further_digits: false,
842 mwi_lamp_mode: crate::message::values::LampMode::On,
843 mwi_on_call: false,
844 legacy_code_page: LegacyCodePage::Iso8859_1,
845 }
846 }
847}
848
849impl DeviceDefinition {
850 pub fn validate(&self) -> Result<(), CodecError> {
852 const MAX_BUTTONS: usize = 256;
856
857 if self.description.len() > MAX_STATION_HEADER_BYTES
858 || self.description.chars().any(char::is_control)
859 {
860 return Err(CodecError::InvalidDefinition(format!(
861 "device {} has an invalid station-header description",
862 self.id
863 )));
864 }
865
866 self.soft_keys.validate()?;
867 if let Some(signaling_qos) = self.signaling_qos {
868 signaling_qos.validate()?;
869 }
870
871 if self.buttons.len() > MAX_BUTTONS {
872 return Err(CodecError::InvalidDefinition(format!(
873 "device {} has {} buttons; the logical layout limit is {MAX_BUTTONS}",
874 self.id,
875 self.buttons.len()
876 )));
877 }
878
879 let mut expanded_buttons = 0_usize;
880 let mut addon_buttons_remaining = None;
881 for button in &self.buttons {
882 if let ButtonDefinition::AddonModule(addon) = button {
883 expanded_buttons += addon_buttons_remaining.take().unwrap_or_default();
884 addon_buttons_remaining = Some(addon.button_capacity().ok_or_else(|| {
885 CodecError::InvalidDefinition(format!(
886 "device {} has unsupported addon-module type {}",
887 self.id,
888 addon.device_type.wire_value()
889 ))
890 })?);
891 continue;
892 }
893 expanded_buttons += 1;
894 if let Some(remaining) = &mut addon_buttons_remaining {
895 if *remaining == 0 {
896 return Err(CodecError::InvalidDefinition(format!(
897 "device {} configures more buttons than its addon module provides",
898 self.id
899 )));
900 }
901 *remaining -= 1;
902 }
903 }
904 expanded_buttons += addon_buttons_remaining.unwrap_or_default();
905 if expanded_buttons > MAX_BUTTONS {
906 return Err(CodecError::InvalidDefinition(format!(
907 "device {} expands to {expanded_buttons} buttons; the logical layout limit is {MAX_BUTTONS}",
908 self.id
909 )));
910 }
911
912 let mut instances = HashSet::new();
913 let mut appearance_ids = HashSet::new();
914 for button in &self.buttons {
915 let Some((kind, instance)) = button.instance_key() else {
916 continue;
917 };
918 if instance == 0 {
919 return Err(CodecError::InvalidDefinition(format!(
920 "device {} has a {kind} button with instance zero",
921 self.id
922 )));
923 }
924 if kind != ButtonNamespace::AddonModule && instance > MAX_STATION_BUTTON_INSTANCE {
929 return Err(CodecError::InvalidDefinition(format!(
930 "device {} has a {kind} button with instance {instance}; maximum wire instance is {}",
931 self.id, MAX_STATION_BUTTON_INSTANCE
932 )));
933 }
934 if !instances.insert((kind, instance)) {
935 return Err(CodecError::InvalidDefinition(format!(
936 "device {} repeats {kind} button instance {instance}",
937 self.id
938 )));
939 }
940 match button {
941 ButtonDefinition::Line(appearance) => {
942 if appearance.id.get() == 0 {
943 return Err(CodecError::InvalidDefinition(format!(
944 "device {} has a line appearance with identifier zero",
945 self.id
946 )));
947 }
948 if !appearance_ids.insert(appearance.id) {
949 return Err(CodecError::InvalidDefinition(format!(
950 "device {} repeats line appearance identifier {}",
951 self.id, appearance.id
952 )));
953 }
954 }
955 ButtonDefinition::Recording(recording) => {
956 validate_recording_button_definition(&self.id, recording)?;
957 }
958 ButtonDefinition::Service(service) => {
959 validate_service_definition(&self.id, service)?;
960 }
961 _ => {}
962 }
963 }
964
965 let lines: Vec<_> = self.lines().collect();
966 if lines.is_empty() {
967 return Err(CodecError::InvalidDefinition(format!(
968 "device {} has no lines",
969 self.id
970 )));
971 }
972 let permits_sparse_lines = self.buttons.iter().any(|button| {
976 matches!(
977 button,
978 ButtonDefinition::Feature(feature)
979 if feature.feature == crate::message::values::ButtonType::Mobility
980 )
981 });
982 for (expected, line) in (1_u32..).zip(lines) {
983 if !permits_sparse_lines && line.instance != expected {
984 return Err(CodecError::InvalidDefinition(format!(
985 "device {} line instances must be contiguous from 1",
986 self.id
987 )));
988 }
989 if line.number.is_empty() || line.number.len() > 24 {
990 return Err(CodecError::InvalidDefinition(format!(
991 "device {} has an invalid line number",
992 self.id
993 )));
994 }
995 }
996 Ok(())
997 }
998
999 pub fn lines(&self) -> impl Iterator<Item = &LineAppearance> {
1000 self.buttons.iter().filter_map(|button| match button {
1001 ButtonDefinition::Line(line) => Some(line),
1002 _ => None,
1003 })
1004 }
1005
1006 pub fn line(&self, instance: u32) -> Option<&LineAppearance> {
1007 self.lines().find(|line| line.instance == instance)
1008 }
1009
1010 pub fn first_line(&self) -> Option<&LineAppearance> {
1011 self.lines().next()
1012 }
1013
1014 pub fn line_count(&self) -> usize {
1015 self.lines().count()
1016 }
1017
1018 pub(crate) fn feature_button(&self, instance: u32) -> Option<&FeatureDefinition> {
1019 self.buttons.iter().find_map(|button| match button {
1020 ButtonDefinition::Feature(feature) if feature.instance == instance => Some(feature),
1021 _ => None,
1022 })
1023 }
1024
1025 pub(crate) fn recording_button(&self, instance: u32) -> Option<&RecordingButtonDefinition> {
1026 self.buttons.iter().find_map(|button| match button {
1027 ButtonDefinition::Recording(recording) if recording.instance == instance => {
1028 Some(recording)
1029 }
1030 _ => None,
1031 })
1032 }
1033
1034 pub(crate) fn blf_button(&self, instance: u32) -> Option<&BlfSpeedDialDefinition> {
1035 self.buttons.iter().find_map(|button| match button {
1036 ButtonDefinition::BlfSpeedDial(blf) if blf.instance == instance => Some(blf),
1037 _ => None,
1038 })
1039 }
1040}
1041
1042fn validate_recording_button_definition(
1043 device: &DeviceId,
1044 recording: &RecordingButtonDefinition,
1045) -> Result<(), CodecError> {
1046 if recording.label.is_empty()
1047 || recording.label.len() > MAX_STATION_FEATURE_LABEL_BYTES
1048 || recording.label.chars().any(char::is_control)
1049 {
1050 return Err(CodecError::InvalidDefinition(format!(
1051 "device {device} has an invalid recording-button label"
1052 )));
1053 }
1054 Ok(())
1055}
1056
1057fn validate_service_definition(
1058 device: &DeviceId,
1059 service: &ServiceDefinition,
1060) -> Result<(), CodecError> {
1061 const MAX_SERVICE_URL_BYTES: usize = 255;
1062 const MAX_SERVICE_PARAMETERS: usize = 32;
1063 const MAX_SERVICE_PARAMETER_BYTES: usize = 128;
1064
1065 if service.label.is_empty()
1066 || service.label.len() > MAX_STATION_FEATURE_LABEL_BYTES
1067 || service.label.chars().any(char::is_control)
1068 {
1069 return Err(CodecError::InvalidDefinition(format!(
1070 "device {device} has an invalid service label"
1071 )));
1072 }
1073 if service.url.is_empty()
1074 || service.url.len() > MAX_SERVICE_URL_BYTES
1075 || service.url.chars().any(char::is_control)
1076 {
1077 return Err(CodecError::InvalidDefinition(format!(
1078 "device {device} has an invalid service URL"
1079 )));
1080 }
1081 let url = url::Url::parse(&service.url).map_err(|_| {
1082 CodecError::InvalidDefinition(format!("device {device} has a malformed service URL"))
1083 })?;
1084 if !matches!(url.scheme(), "http" | "https")
1085 || url.host_str().is_none()
1086 || url.fragment().is_some()
1087 {
1088 return Err(CodecError::InvalidDefinition(format!(
1089 "device {device} service URL must be HTTP(S) without a fragment"
1090 )));
1091 }
1092 let parameters = url.query_pairs().collect::<Vec<_>>();
1093 if parameters.len() > MAX_SERVICE_PARAMETERS
1094 || parameters.iter().any(|(name, value)| {
1095 name.is_empty()
1096 || name.len() > MAX_SERVICE_PARAMETER_BYTES
1097 || value.len() > MAX_SERVICE_PARAMETER_BYTES
1098 || name.chars().chain(value.chars()).any(char::is_control)
1099 })
1100 {
1101 return Err(CodecError::InvalidDefinition(format!(
1102 "device {device} service URL has invalid or excessive query parameters"
1103 )));
1104 }
1105 Ok(())
1106}
1107
1108impl ButtonDefinition {
1109 fn instance_key(&self) -> Option<(ButtonNamespace, u32)> {
1110 match self {
1111 Self::Line(definition) => Some((ButtonNamespace::Line, definition.instance)),
1112 Self::SpeedDial(definition) => Some((ButtonNamespace::SpeedDial, definition.instance)),
1113 Self::BlfSpeedDial(definition) => Some((ButtonNamespace::Feature, definition.instance)),
1114 Self::Feature(definition) => Some((ButtonNamespace::Feature, definition.instance)),
1115 Self::Recording(definition) => Some((ButtonNamespace::Feature, definition.instance)),
1116 Self::Service(definition) => Some((ButtonNamespace::Service, definition.instance)),
1117 Self::AddonModule(definition) => Some((ButtonNamespace::AddonModule, definition.slot)),
1118 Self::Unused => None,
1119 }
1120 }
1121}
1122
1123#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1124enum ButtonNamespace {
1125 Line,
1126 SpeedDial,
1127 Feature,
1128 Service,
1129 AddonModule,
1130}
1131
1132impl fmt::Display for ButtonNamespace {
1133 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1134 formatter.write_str(match self {
1135 Self::Line => "line",
1136 Self::SpeedDial => "speed dial",
1137 Self::Feature => "feature",
1138 Self::Service => "service URL",
1139 Self::AddonModule => "addon module",
1140 })
1141 }
1142}
1143
1144#[derive(Clone, Debug, Eq, PartialEq)]
1149pub struct DeviceRegistration {
1150 pub id: DeviceId,
1151 pub peer: SocketAddr,
1152 pub transport: StationTransport,
1153 pub reported_address: Option<Ipv4Addr>,
1154 pub reported_ipv6_address: Option<Ipv6Addr>,
1155 pub device_type: DeviceType,
1156 pub protocol: ProtocolVersion,
1157 pub firmware: String,
1158}
1159
1160impl DeviceRegistration {
1161 pub fn reported_address_for_peer(&self) -> Option<IpAddr> {
1164 match self.peer.ip() {
1165 IpAddr::V4(_) => self.reported_address.map(IpAddr::V4),
1166 IpAddr::V6(peer) => peer.to_ipv4_mapped().map_or_else(
1167 || self.reported_ipv6_address.map(IpAddr::V6),
1168 |_| self.reported_address.map(IpAddr::V4),
1169 ),
1170 }
1171 }
1172}
1173
1174#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1176pub enum CallDirection {
1177 Inbound,
1178 Outbound,
1179}
1180
1181impl From<CallDirection> for CallType {
1182 fn from(value: CallDirection) -> Self {
1183 match value {
1184 CallDirection::Inbound => Self::Inbound,
1185 CallDirection::Outbound => Self::Outbound,
1186 }
1187 }
1188}
1189
1190#[derive(Clone, Debug, Eq, PartialEq)]
1196pub struct CallInfo {
1197 pub direction: CallDirection,
1198 pub calling_name: String,
1199 pub calling_number: String,
1200 pub called_name: String,
1201 pub called_number: String,
1202 pub original_called_name: String,
1203 pub original_called_number: String,
1204 pub last_redirecting_name: String,
1205 pub last_redirecting_number: String,
1206 pub original_redirect_reason: u32,
1207 pub last_redirect_reason: u32,
1208 pub party_restrictions: u32,
1210}
1211
1212impl Default for CallInfo {
1213 fn default() -> Self {
1214 Self {
1215 direction: CallDirection::Outbound,
1216 calling_name: String::new(),
1217 calling_number: String::new(),
1218 called_name: String::new(),
1219 called_number: String::new(),
1220 original_called_name: String::new(),
1221 original_called_number: String::new(),
1222 last_redirecting_name: String::new(),
1223 last_redirecting_number: String::new(),
1224 original_redirect_reason: 0,
1225 last_redirect_reason: 0,
1226 party_restrictions: 0,
1227 }
1228 }
1229}
1230
1231pub const DEFAULT_AUDIO_PACKET_MS: u32 = 20;
1233pub const DEFAULT_AUDIO_MAX_FRAMES_PER_PACKET: u32 = 0;
1234
1235#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1238pub struct AudioProcessingPolicy {
1239 pub echo_cancellation: EchoCancellation,
1240 pub silence_suppression: SilenceSuppression,
1241}
1242
1243impl Default for AudioProcessingPolicy {
1244 fn default() -> Self {
1245 Self {
1246 echo_cancellation: EchoCancellation::On,
1247 silence_suppression: SilenceSuppression::Off,
1248 }
1249 }
1250}
1251
1252#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1257pub struct MediaEndpoint {
1258 pub address: IpAddr,
1259 pub rtp_port: u16,
1260 pub rtcp_port: u16,
1261 pub codec: Codec,
1262 pub packet_ms: u32,
1263 pub max_frames_per_packet: u32,
1264 pub telephone_event_payload: u8,
1266}
1267
1268#[cfg(test)]
1269mod tests {
1270 use super::*;
1271
1272 #[test]
1273 fn identifiers_are_explicit_and_lossless() {
1274 let reference = CallReference::new(42);
1275 assert_eq!(reference.get(), 42);
1276 assert_eq!(u32::from(reference), 42);
1277
1278 let appearance = AppearanceId::new(7);
1279 assert_eq!(appearance.get(), 7);
1280 assert_eq!(u32::from(appearance), 7);
1281
1282 let conference = ConferenceId::new(9);
1283 assert_eq!(conference.get(), 9);
1284
1285 let participant = ParticipantId::new(11);
1286 assert_eq!(participant.get(), 11);
1287 }
1288
1289 #[test]
1290 fn device_id_is_canonicalized() {
1291 let id: DeviceId = " sep001122334455 ".parse().unwrap();
1292 assert_eq!(id.as_str(), "SEP001122334455");
1293 assert_eq!(id.as_ref(), "SEP001122334455");
1294
1295 let mut devices = HashMap::new();
1296 devices.insert(id, "desk");
1297 assert_eq!(devices.get("SEP001122334455"), Some(&"desk"));
1298 }
1299
1300 #[test]
1301 fn registration_selects_the_report_matching_the_effective_peer_family() {
1302 let registration = DeviceRegistration {
1303 id: DeviceId::new("SEP001122334455").unwrap(),
1304 peer: "[2001:db8::20]:2000".parse().unwrap(),
1305 transport: StationTransport::Clear,
1306 reported_address: Some("192.0.2.20".parse().unwrap()),
1307 reported_ipv6_address: Some("2001:db8::20".parse().unwrap()),
1308 device_type: DeviceType::Cisco7962,
1309 protocol: ProtocolVersion::V22,
1310 firmware: "test".into(),
1311 };
1312 assert_eq!(
1313 registration.reported_address_for_peer(),
1314 Some("2001:db8::20".parse().unwrap())
1315 );
1316
1317 let mapped = DeviceRegistration {
1318 peer: "[::ffff:192.0.2.20]:2000".parse().unwrap(),
1319 ..registration
1320 };
1321 assert_eq!(
1322 mapped.reported_address_for_peer(),
1323 Some("192.0.2.20".parse().unwrap())
1324 );
1325 }
1326
1327 fn line_button(instance: u32, number: &str) -> ButtonDefinition {
1328 ButtonDefinition::Line(LineAppearance::new(
1329 instance,
1330 LineDefinition {
1331 number: number.into(),
1332 display_name: number.into(),
1333 },
1334 ))
1335 }
1336
1337 #[test]
1338 fn station_definition_accepts_non_line_buttons_between_lines() {
1339 let definition = DeviceDefinition {
1340 id: DeviceId::new("SEP001122334455").unwrap(),
1341 description: "Desk".into(),
1342 transport: StationTransportRequirement::Either,
1343 signaling_qos: None,
1344 buttons: vec![
1345 line_button(1, "1001"),
1346 ButtonDefinition::Unused,
1347 ButtonDefinition::SpeedDial(SpeedDialDefinition {
1348 instance: 1,
1349 number: "2001".into(),
1350 display_name: "Warehouse".into(),
1351 }),
1352 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1353 instance: 1,
1354 number: "2002".into(),
1355 display_name: "Dispatch".into(),
1356 }),
1357 ButtonDefinition::Feature(FeatureDefinition {
1358 instance: 2,
1359 label: "DND".into(),
1360 feature: crate::message::values::ButtonType::DoNotDisturb,
1361 }),
1362 ButtonDefinition::Service(ServiceDefinition {
1363 instance: 1,
1364 label: "Directory".into(),
1365 url: "http://pbx.test/directory".into(),
1366 }),
1367 ButtonDefinition::AddonModule(AddonModuleDefinition {
1368 slot: 1,
1369 device_type: crate::message::values::DeviceType::CiscoAddon7914,
1370 }),
1371 line_button(2, "1002"),
1372 ],
1373 soft_keys: SoftKeyProfile::default(),
1374 ui: StationUiPolicy::default(),
1375 };
1376
1377 definition.validate().unwrap();
1378 assert_eq!(definition.line_count(), 2);
1379 assert_eq!(definition.line(2).unwrap().number, "1002");
1380 }
1381
1382 #[test]
1383 fn station_definition_rejects_invalid_signaling_markings() {
1384 let mut definition = DeviceDefinition {
1385 id: DeviceId::new("SEP001122334455").unwrap(),
1386 description: "Desk".into(),
1387 transport: StationTransportRequirement::Either,
1388 signaling_qos: Some(SignalingQos::new(64, 0)),
1389 buttons: vec![line_button(1, "1001")],
1390 soft_keys: SoftKeyProfile::default(),
1391 ui: StationUiPolicy::default(),
1392 };
1393
1394 assert!(matches!(
1395 definition.validate(),
1396 Err(CodecError::InvalidDefinition(message)) if message.contains("DSCP 64")
1397 ));
1398
1399 definition.signaling_qos = Some(SignalingQos::new(26, 8));
1400 assert!(matches!(
1401 definition.validate(),
1402 Err(CodecError::InvalidDefinition(message)) if message.contains("COS 8")
1403 ));
1404 }
1405
1406 #[test]
1407 fn line_appearance_keeps_logical_and_device_specific_state_separate() {
1408 let logical = LineDefinition {
1409 number: "1001".into(),
1410 display_name: "Reception".into(),
1411 };
1412 let mut appearance = LineAppearance::new(2, logical.clone());
1413 appearance.label = Some("Private key".into());
1414 appearance.caller_id = CallerIdOverride {
1415 name: Some("Private desk".into()),
1416 number: None,
1417 };
1418 appearance.ring_mode = AppearanceRingMode::Silent;
1419 appearance.subscription_identity = Some("1001@internal".into());
1420 appearance.privacy = true;
1421
1422 assert_eq!(appearance.line, logical);
1423 assert_eq!(appearance.display_label(), "Private key");
1424 assert_eq!(appearance.number, "1001");
1425 assert_eq!(appearance.id, AppearanceId::new(2));
1426 }
1427
1428 #[test]
1429 fn station_definition_rejects_zero_and_duplicate_typed_instances() {
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![line_button(1, "1001"), line_button(1, "1002")],
1436 soft_keys: SoftKeyProfile::default(),
1437 ui: StationUiPolicy::default(),
1438 };
1439 assert!(matches!(
1440 definition.validate(),
1441 Err(CodecError::InvalidDefinition(message))
1442 if message.contains("repeats line button instance 1")
1443 ));
1444
1445 let definition = DeviceDefinition {
1446 id: DeviceId::new("SEP001122334455").unwrap(),
1447 description: "Desk".into(),
1448 transport: StationTransportRequirement::Either,
1449 signaling_qos: None,
1450 buttons: vec![
1451 line_button(1, "1001"),
1452 ButtonDefinition::Feature(FeatureDefinition {
1453 instance: 0,
1454 label: "DND".into(),
1455 feature: crate::message::values::ButtonType::DoNotDisturb,
1456 }),
1457 ],
1458 soft_keys: SoftKeyProfile::default(),
1459 ui: StationUiPolicy::default(),
1460 };
1461 assert!(matches!(
1462 definition.validate(),
1463 Err(CodecError::InvalidDefinition(message))
1464 if message.contains("feature button with instance zero")
1465 ));
1466
1467 let definition = DeviceDefinition {
1468 id: DeviceId::new("SEP001122334455").unwrap(),
1469 description: "Desk".into(),
1470 transport: StationTransportRequirement::Either,
1471 signaling_qos: None,
1472 buttons: vec![
1473 line_button(1, "1001"),
1474 ButtonDefinition::Feature(FeatureDefinition {
1475 instance: 1,
1476 label: "DND".into(),
1477 feature: crate::message::values::ButtonType::DoNotDisturb,
1478 }),
1479 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1480 instance: 1,
1481 number: "2001".into(),
1482 display_name: "Warehouse".into(),
1483 }),
1484 ],
1485 soft_keys: SoftKeyProfile::default(),
1486 ui: StationUiPolicy::default(),
1487 };
1488 assert!(matches!(
1489 definition.validate(),
1490 Err(CodecError::InvalidDefinition(message))
1491 if message.contains("repeats feature button instance 1")
1492 ));
1493
1494 let distinct_namespaces = DeviceDefinition {
1495 id: DeviceId::new("SEP001122334455").unwrap(),
1496 description: "Desk".into(),
1497 transport: StationTransportRequirement::Either,
1498 signaling_qos: None,
1499 buttons: vec![
1500 line_button(1, "1001"),
1501 ButtonDefinition::SpeedDial(SpeedDialDefinition {
1502 instance: 7,
1503 number: "2001".into(),
1504 display_name: "Warehouse".into(),
1505 }),
1506 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1507 instance: 7,
1508 number: "2002".into(),
1509 display_name: "Dispatch".into(),
1510 }),
1511 ],
1512 soft_keys: SoftKeyProfile::default(),
1513 ui: StationUiPolicy::default(),
1514 };
1515 distinct_namespaces.validate().unwrap();
1516 }
1517
1518 #[test]
1519 fn station_definition_enforces_one_byte_wire_instances_for_each_button_family() {
1520 let definition_with = |button| DeviceDefinition {
1521 id: DeviceId::new("SEP001122334455").unwrap(),
1522 description: "Desk".into(),
1523 transport: StationTransportRequirement::Either,
1524 signaling_qos: None,
1525 buttons: vec![line_button(1, "1001"), button],
1526 soft_keys: SoftKeyProfile::default(),
1527 ui: StationUiPolicy::default(),
1528 };
1529 let buttons = |instance| {
1530 [
1531 ButtonDefinition::SpeedDial(SpeedDialDefinition {
1532 instance,
1533 number: "2001".into(),
1534 display_name: "Speed".into(),
1535 }),
1536 ButtonDefinition::BlfSpeedDial(BlfSpeedDialDefinition {
1537 instance,
1538 number: "2002".into(),
1539 display_name: "BLF".into(),
1540 }),
1541 ButtonDefinition::Feature(FeatureDefinition {
1542 instance,
1543 label: "DND".into(),
1544 feature: crate::message::values::ButtonType::DoNotDisturb,
1545 }),
1546 ButtonDefinition::Recording(RecordingButtonDefinition {
1547 instance,
1548 label: "Record calls".into(),
1549 }),
1550 ButtonDefinition::Service(ServiceDefinition {
1551 instance,
1552 label: "Directory".into(),
1553 url: "https://pbx.example/directory".into(),
1554 }),
1555 ]
1556 };
1557
1558 for button in buttons(255) {
1559 definition_with(button).validate().unwrap();
1560 }
1561 for button in buttons(256) {
1562 assert!(matches!(
1563 definition_with(button).validate(),
1564 Err(CodecError::InvalidDefinition(message))
1565 if message.contains("maximum wire instance is 255")
1566 ));
1567 }
1568
1569 let line_255 = DeviceDefinition {
1570 buttons: vec![line_button(255, "1001")],
1571 ..definition_with(ButtonDefinition::Unused)
1572 };
1573 let mut line_255 = line_255;
1575 line_255.buttons.insert(
1576 0,
1577 ButtonDefinition::Feature(FeatureDefinition {
1578 instance: 1,
1579 label: "Mobility".into(),
1580 feature: crate::message::values::ButtonType::Mobility,
1581 }),
1582 );
1583 line_255.validate().unwrap();
1584 line_255.buttons[1] = line_button(256, "1001");
1585 assert!(matches!(
1586 line_255.validate(),
1587 Err(CodecError::InvalidDefinition(message))
1588 if message.contains("maximum wire instance is 255")
1589 ));
1590 }
1591
1592 #[test]
1593 fn station_definition_rejects_recording_labels_that_cannot_fit_legacy_status() {
1594 let definition_with_label = |label: String| DeviceDefinition {
1595 id: DeviceId::new("SEP001122334455").unwrap(),
1596 description: "Desk".into(),
1597 transport: StationTransportRequirement::Either,
1598 signaling_qos: None,
1599 buttons: vec![
1600 line_button(1, "1001"),
1601 ButtonDefinition::Recording(RecordingButtonDefinition { instance: 1, label }),
1602 ],
1603 soft_keys: SoftKeyProfile::default(),
1604 ui: StationUiPolicy::default(),
1605 };
1606
1607 definition_with_label("R".repeat(MAX_STATION_FEATURE_LABEL_BYTES))
1608 .validate()
1609 .unwrap();
1610 for label in [
1611 String::new(),
1612 "bad\nlabel".into(),
1613 "R".repeat(MAX_STATION_FEATURE_LABEL_BYTES + 1),
1614 ] {
1615 assert!(matches!(
1616 definition_with_label(label).validate(),
1617 Err(CodecError::InvalidDefinition(message))
1618 if message.contains("invalid recording-button label")
1619 ));
1620 }
1621 }
1622
1623 #[test]
1624 fn station_definition_enforces_station_header_text_contract() {
1625 let definition_with_description = |description: String| DeviceDefinition {
1626 id: DeviceId::new("SEP001122334455").unwrap(),
1627 description,
1628 transport: StationTransportRequirement::Either,
1629 signaling_qos: None,
1630 buttons: vec![line_button(1, "1001")],
1631 soft_keys: SoftKeyProfile::default(),
1632 ui: StationUiPolicy::default(),
1633 };
1634
1635 for description in [String::new(), "D".repeat(MAX_STATION_HEADER_BYTES)] {
1636 definition_with_description(description).validate().unwrap();
1637 }
1638 for description in [
1639 "bad\nheader".into(),
1640 "D".repeat(MAX_STATION_HEADER_BYTES + 1),
1641 ] {
1642 assert!(matches!(
1643 definition_with_description(description).validate(),
1644 Err(CodecError::InvalidDefinition(message))
1645 if message.contains("invalid station-header description")
1646 ));
1647 }
1648 }
1649
1650 #[test]
1651 fn blf_defaults_to_unknown() {
1652 assert_eq!(BlfState::default(), BlfState::Unknown);
1653 }
1654
1655 #[test]
1656 fn station_definition_enforces_bounded_logical_button_limit() {
1657 let definition = DeviceDefinition {
1658 id: DeviceId::new("SEP001122334455").unwrap(),
1659 description: "Desk".into(),
1660 transport: StationTransportRequirement::Either,
1661 signaling_qos: None,
1662 buttons: std::iter::once(line_button(1, "1001"))
1663 .chain(std::iter::repeat_n(ButtonDefinition::Unused, 256))
1664 .collect(),
1665 soft_keys: SoftKeyProfile::default(),
1666 ui: StationUiPolicy::default(),
1667 };
1668 assert!(matches!(
1669 definition.validate(),
1670 Err(CodecError::InvalidDefinition(message))
1671 if message.contains("logical layout limit is 256")
1672 ));
1673 }
1674
1675 #[test]
1676 fn service_urls_require_bounded_http_parameters() {
1677 let service_device = |url: &str| DeviceDefinition {
1678 id: DeviceId::new("SEP001122334455").unwrap(),
1679 description: "Desk".into(),
1680 transport: StationTransportRequirement::Either,
1681 signaling_qos: None,
1682 buttons: vec![
1683 line_button(1, "1001"),
1684 ButtonDefinition::Service(ServiceDefinition {
1685 instance: 1,
1686 label: "Directory".into(),
1687 url: url.into(),
1688 }),
1689 ],
1690 soft_keys: SoftKeyProfile::default(),
1691 ui: StationUiPolicy::default(),
1692 };
1693
1694 service_device("https://pbx.example/sccp/directory?q=Fran%C3%A7ois&page=2")
1695 .validate()
1696 .unwrap();
1697 service_device("https://user:secret@pbx.example/service")
1698 .validate()
1699 .unwrap();
1700 for invalid in [
1701 "file:///etc/passwd",
1702 "https://pbx.example/service#private",
1703 "https://pbx.example/service?=missing-name",
1704 "not a URL",
1705 ] {
1706 let error = service_device(invalid).validate().unwrap_err().to_string();
1707 assert!(!error.contains(invalid));
1708 }
1709 let excessive = format!(
1710 "https://pbx.example/service?{}",
1711 (0..33)
1712 .map(|index| format!("p{index}=v"))
1713 .collect::<Vec<_>>()
1714 .join("&")
1715 );
1716 assert!(service_device(&excessive).validate().is_err());
1717 }
1718
1719 #[test]
1720 fn soft_key_profiles_require_every_mode_and_unique_known_actions() {
1721 assert!(matches!(
1722 SoftKeyProfile::new([(KeyMode::OnHook, vec![SoftKey::NewCall])]),
1723 Err(CodecError::InvalidDefinition(message))
1724 if message.contains("every known key mode")
1725 ));
1726
1727 let duplicate = SoftKeyProfile::new(KeyMode::ALL_KNOWN.iter().copied().map(|mode| {
1728 (
1729 mode,
1730 if mode == KeyMode::Connected {
1731 vec![SoftKey::Hold, SoftKey::Hold]
1732 } else {
1733 Vec::new()
1734 },
1735 )
1736 }));
1737 assert!(matches!(
1738 duplicate,
1739 Err(CodecError::InvalidDefinition(message)) if message.contains("repeats action")
1740 ));
1741 }
1742}