1use std::fmt;
2use std::net::IpAddr;
3use std::str::FromStr;
4
5use crate::attributes::{AttributeError, ErrorContext, TypedAttribute};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum ReferenceClock {
12 Ntp(Ntp),
13 Ptp(Ptp),
14 Gps,
15 Gal,
16 Glonass,
17 Local,
18 Private(PrivateSource),
19 Ext(ClockSourceExt),
20}
21
22impl ReferenceClock {
23 pub fn from_ntp_hostname(hostname: impl ToString) -> Self {
25 NtpServerAddr::from_hostname(hostname).into()
26 }
27
28 pub fn from_ntp_hostname_with_port(hostname: impl ToString, port: u16) -> Self {
30 NtpServerAddr::from_hostname_with_port(hostname, port).into()
31 }
32
33 pub fn new_ntp_traceable() -> Self {
35 NtpServerAddr::new_traceable().into()
36 }
37
38 pub fn from_ptp_gmid(version: PtpVersion, gmid: impl Into<Eui64>) -> Self {
40 Ptp::from_gmid(version, gmid).into()
41 }
42
43 pub fn from_ptp_gmid_with_domain(gmid: impl Into<Eui64>, domain: PtpDomain) -> Self {
45 Ptp::from_gmid_with_domain(gmid, domain).into()
46 }
47
48 pub fn from_ptp_gmid_with_domain_name(gmid: impl Into<Eui64>, name: impl ToString) -> Self {
52 Ptp::from_gmid_with_domain_name(gmid, name).into()
53 }
54
55 pub fn try_from_ptp_gmid_with_domain_number(
61 gmid: impl Into<Eui64>,
62 number: u8,
63 ) -> Result<Self, AttributeError> {
64 Ok(Ptp::try_from_gmid_with_domain_number(gmid, number)?.into())
65 }
66
67 pub fn new_ptp_traceable(version: PtpVersion) -> Self {
69 Ptp::new_traceable(version).into()
70 }
71
72 pub fn new_private_source_standard() -> Self {
74 PrivateSource::Standard.into()
75 }
76
77 pub fn new_private_source_traceable() -> Self {
79 PrivateSource::Traceable.into()
80 }
81
82 pub fn from_clock_source_ext_name(name: impl ToString) -> Self {
84 ClockSourceExt::new(name).into()
85 }
86
87 pub fn from_clock_source_ext_name_with_value(
89 name: impl ToString,
90 value: impl ToString,
91 ) -> Self {
92 ClockSourceExt::with_value(name, value).into()
93 }
94}
95
96impl FromStr for ReferenceClock {
97 type Err = AttributeError;
98
99 fn from_str(s: &str) -> Result<Self, AttributeError> {
100 if let Some(ntp) = s.strip_prefix("ntp=") {
101 Ok(Self::Ntp(
102 Ntp::from_str(ntp).with_attr(<Self as TypedAttribute>::NAME)?,
103 ))
104 } else if let Some(ptp) = s.strip_prefix("ptp=") {
105 Ok(Self::Ptp(
106 Ptp::from_str(ptp).with_attr(<Self as TypedAttribute>::NAME)?,
107 ))
108 } else if s == "gps" {
109 Ok(Self::Gps)
110 } else if s == "gal" {
111 Ok(Self::Gal)
112 } else if s == "glonass" {
113 Ok(Self::Glonass)
114 } else if s == "local" {
115 Ok(Self::Local)
116 } else if s == "private" {
117 Ok(Self::Private(PrivateSource::Standard))
118 } else if s == "private:traceable" {
119 Ok(Self::Private(PrivateSource::Traceable))
120 } else {
121 Ok(Self::Ext(
122 ClockSourceExt::from_str(s).with_attr(<Self as TypedAttribute>::NAME)?,
123 ))
124 }
125 }
126}
127
128impl fmt::Display for ReferenceClock {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 Self::Ntp(ntp) => write!(f, "ntp={ntp}"),
132 Self::Ptp(ptp) => write!(f, "ptp={ptp}"),
133 Self::Gps => write!(f, "gps"),
134 Self::Gal => write!(f, "gal"),
135 Self::Glonass => write!(f, "glonass"),
136 Self::Local => write!(f, "local"),
137 Self::Private(private) => {
138 if *private == PrivateSource::Traceable {
139 write!(f, "private:traceable")
140 } else {
141 write!(f, "private")
142 }
143 }
144 Self::Ext(ext) => write!(f, "{ext}"),
145 }
146 }
147}
148
149impl TypedAttribute for ReferenceClock {
150 const NAME: &'static str = "ts-refclk";
151}
152
153impl From<Ntp> for ReferenceClock {
154 fn from(ntp: Ntp) -> Self {
155 ReferenceClock::Ntp(ntp)
156 }
157}
158
159impl From<NtpServerAddr> for ReferenceClock {
160 fn from(server: NtpServerAddr) -> Self {
161 ReferenceClock::Ntp(Ntp { server })
162 }
163}
164
165impl From<Ptp> for ReferenceClock {
166 fn from(ptp: Ptp) -> Self {
167 ReferenceClock::Ptp(ptp)
168 }
169}
170
171impl From<(PtpVersion, PtpServer)> for ReferenceClock {
172 fn from(ptp: (PtpVersion, PtpServer)) -> Self {
173 ReferenceClock::Ptp(Ptp::new(ptp.0, ptp.1))
174 }
175}
176
177impl From<PrivateSource> for ReferenceClock {
178 fn from(private_src: PrivateSource) -> Self {
179 ReferenceClock::Private(private_src)
180 }
181}
182
183impl From<ClockSourceExt> for ReferenceClock {
184 fn from(clock_src_ext: ClockSourceExt) -> Self {
185 ReferenceClock::Ext(clock_src_ext)
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct Ntp {
192 pub server: NtpServerAddr,
193}
194
195impl Ntp {
196 pub fn from_hostname(hostname: impl ToString) -> Ntp {
198 NtpServerAddr::from_hostname(hostname).into()
199 }
200
201 pub fn from_hostname_with_port(hostname: impl ToString, port: u16) -> Ntp {
203 NtpServerAddr::from_hostname_with_port(hostname, port).into()
204 }
205
206 pub fn new_traceable() -> Ntp {
208 NtpServerAddr::new_traceable().into()
209 }
210}
211
212impl FromStr for Ntp {
213 type Err = AttributeError;
214
215 fn from_str(s: &str) -> Result<Self, AttributeError> {
216 let server = NtpServerAddr::from_str(s)?;
217 Ok(Self { server })
218 }
219}
220
221impl fmt::Display for Ntp {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 write!(f, "{}", self.server)
224 }
225}
226
227impl From<NtpServerAddr> for Ntp {
228 fn from(server: NtpServerAddr) -> Self {
229 Ntp { server }
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
235pub enum NtpServerAddr {
236 HostPort { hostname: String, port: Option<u16> },
237 Traceable,
238}
239
240impl NtpServerAddr {
241 pub fn from_hostname(hostname: impl ToString) -> NtpServerAddr {
243 NtpServerAddr::HostPort {
244 hostname: hostname.to_string(),
245 port: None,
246 }
247 }
248
249 pub fn from_hostname_with_port(hostname: impl ToString, port: u16) -> NtpServerAddr {
251 NtpServerAddr::HostPort {
252 hostname: hostname.to_string(),
253 port: Some(port),
254 }
255 }
256
257 pub fn new_traceable() -> NtpServerAddr {
259 NtpServerAddr::Traceable
260 }
261
262 pub fn builder(hostname: impl ToString) -> crate::builders::NtpServerAddr {
264 crate::builders::NtpServerAddr::new(hostname)
265 }
266}
267
268impl FromStr for NtpServerAddr {
269 type Err = AttributeError;
270
271 fn from_str(s: &str) -> Result<Self, AttributeError> {
272 if s.is_empty() {
273 return Err(AttributeError::ParamNotFound {
274 param: "NTP server address".to_string(),
275 attr: String::new(), });
277 }
278
279 if s == "/traceable/" {
280 Ok(Self::Traceable)
281 } else if s.parse::<IpAddr>().is_ok() {
282 Ok(Self::HostPort {
285 hostname: s.to_string(),
286 port: None,
287 })
288 } else if let Some(rest) = s.strip_prefix('[') {
289 let invalid = || AttributeError::InvalidParamValue {
291 param: "NTP server address".to_string(),
292 val: s.to_string(),
293 attr: String::new(), };
295 let (address, port) = match rest.split_once("]:") {
296 Some((address, port_str)) => {
297 let port = port_str.parse::<u16>().map_err(|_| invalid())?;
298 (address, Some(port))
299 }
300 None => (rest.strip_suffix(']').ok_or_else(invalid)?, None),
301 };
302 if address.parse::<IpAddr>().is_err() {
303 return Err(invalid());
304 }
305 Ok(Self::HostPort {
306 hostname: address.to_string(),
307 port,
308 })
309 } else if let Some((hostname, port_str)) = s.rsplit_once(':') {
310 if hostname.is_empty() {
311 return Err(AttributeError::ParamNotFound {
312 param: "hostname in NTP server address".to_string(),
313 attr: String::new(), });
315 }
316
317 let port = port_str
318 .parse::<u16>()
319 .map_err(|_| AttributeError::InvalidParamValue {
320 param: "port in NTP server address".to_string(),
321 val: port_str.to_string(),
322 attr: String::new(), })?;
324
325 Ok(Self::HostPort {
326 hostname: hostname.to_string(),
327 port: Some(port),
328 })
329 } else {
330 Ok(Self::HostPort {
331 hostname: s.to_string(),
332 port: None,
333 })
334 }
335 }
336}
337
338impl fmt::Display for NtpServerAddr {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 match self {
341 Self::HostPort {
342 hostname,
343 port: Some(port),
344 } => {
345 if hostname.contains(':') {
348 write!(f, "[{hostname}]:{port}")
349 } else {
350 write!(f, "{hostname}:{port}")
351 }
352 }
353 Self::HostPort {
354 hostname,
355 port: None,
356 } => write!(f, "{hostname}"),
357 Self::Traceable => write!(f, "/traceable/"),
358 }
359 }
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
364pub struct Ptp {
365 pub version: PtpVersion,
366 pub server: PtpServer,
367}
368
369impl Ptp {
370 pub fn new(version: PtpVersion, server: PtpServer) -> Self {
372 Ptp { version, server }
373 }
374
375 pub fn from_gmid(version: PtpVersion, gmid: impl Into<Eui64>) -> Self {
377 Ptp {
378 version,
379 server: PtpServer::from_gmid(gmid),
380 }
381 }
382
383 pub fn from_gmid_with_domain(gmid: impl Into<Eui64>, domain: PtpDomain) -> Self {
385 let version = match domain {
386 PtpDomain::DomainName { .. } => PtpVersion::Ieee1588_2002,
387 PtpDomain::DomainNumber(_) => PtpVersion::Ieee1588_2008,
388 };
389
390 Ptp {
391 version,
392 server: PtpServer::from_gmid_with_domain(gmid, domain),
393 }
394 }
395
396 pub fn from_gmid_with_domain_name(gmid: impl Into<Eui64>, name: impl ToString) -> Self {
400 Self::from_gmid_with_domain(gmid, PtpDomain::from_name(name))
401 }
402
403 pub fn try_from_gmid_with_domain_number(
409 gmid: impl Into<Eui64>,
410 number: u8,
411 ) -> Result<Self, AttributeError> {
412 Ok(Self::from_gmid_with_domain(
413 gmid,
414 PtpDomain::try_from_number(number)?,
415 ))
416 }
417
418 pub fn new_traceable(version: PtpVersion) -> Self {
420 Ptp {
421 version,
422 server: PtpServer::new_traceable(),
423 }
424 }
425}
426
427impl From<(PtpVersion, PtpServer)> for Ptp {
428 fn from(ptp: (PtpVersion, PtpServer)) -> Self {
429 Ptp::new(ptp.0, ptp.1)
430 }
431}
432
433impl FromStr for Ptp {
434 type Err = AttributeError;
435
436 fn from_str(s: &str) -> Result<Self, AttributeError> {
437 let (version_str, s) = s
438 .split_once(':')
439 .ok_or_else(|| AttributeError::ParamNotFound {
440 param: "PTP version".to_string(),
441 attr: String::new(), })?;
443 let version = PtpVersion::from_str(version_str)?;
444
445 if s.is_empty() {
446 return Err(AttributeError::ParamNotFound {
447 param: "PTP server".to_string(),
448 attr: String::new(), });
450 }
451
452 let server = PtpServer::from_str(s)?;
453
454 Ok(Self { version, server })
455 }
456}
457
458impl fmt::Display for Ptp {
459 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460 write!(f, "{}:{}", self.version, self.server)
461 }
462}
463
464#[derive(Debug, Clone, PartialEq, Eq)]
466pub enum PtpVersion {
467 Ieee1588_2002,
468 Ieee1588_2008,
469 #[allow(non_camel_case_types)]
470 Ieee8021As_2011,
471 Ext(String),
472}
473
474impl FromStr for PtpVersion {
475 type Err = AttributeError;
476
477 fn from_str(s: &str) -> Result<Self, AttributeError> {
478 match s {
479 "IEEE1588-2002" => Ok(Self::Ieee1588_2002),
480 "IEEE1588-2008" => Ok(Self::Ieee1588_2008),
481 "IEEE802.1AS-2011" => Ok(Self::Ieee8021As_2011),
482 _ => {
483 if s.is_empty() {
484 return Err(AttributeError::ParamNotFound {
485 param: "PTP version".to_string(),
486 attr: String::new(), });
488 }
489 Ok(Self::Ext(s.to_string()))
490 }
491 }
492 }
493}
494
495impl fmt::Display for PtpVersion {
496 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
497 match self {
498 Self::Ieee1588_2002 => write!(f, "IEEE1588-2002"),
499 Self::Ieee1588_2008 => write!(f, "IEEE1588-2008"),
500 Self::Ieee8021As_2011 => write!(f, "IEEE802.1AS-2011"),
501 Self::Ext(v) => write!(f, "{v}"),
502 }
503 }
504}
505
506#[derive(Debug, Clone, PartialEq, Eq)]
508pub enum PtpServer {
509 GmidDomain {
510 gmid: Eui64,
511 domain: Option<PtpDomain>,
512 },
513 Traceable,
514}
515
516impl PtpServer {
517 pub fn from_gmid(gmid: impl Into<Eui64>) -> Self {
519 PtpServer::GmidDomain {
520 gmid: gmid.into(),
521 domain: None,
522 }
523 }
524
525 pub fn from_gmid_with_domain(gmid: impl Into<Eui64>, domain: PtpDomain) -> Self {
527 PtpServer::GmidDomain {
528 gmid: gmid.into(),
529 domain: Some(domain),
530 }
531 }
532
533 pub fn from_gmid_with_domain_name(gmid: impl Into<Eui64>, name: impl ToString) -> Self {
537 PtpServer::GmidDomain {
538 gmid: gmid.into(),
539 domain: Some(PtpDomain::from_name(name)),
540 }
541 }
542
543 pub fn try_from_ptp_gmid_with_domain_number(
547 gmid: impl Into<Eui64>,
548 number: u8,
549 ) -> Result<Self, AttributeError> {
550 Ok(PtpServer::GmidDomain {
551 gmid: gmid.into(),
552 domain: Some(PtpDomain::try_from_number(number)?),
553 })
554 }
555
556 pub fn new_traceable() -> Self {
557 PtpServer::Traceable
558 }
559}
560
561impl FromStr for PtpServer {
562 type Err = AttributeError;
563
564 fn from_str(s: &str) -> Result<Self, AttributeError> {
565 if s == "traceable" {
566 return Ok(Self::Traceable);
567 }
568
569 let (gmid_str, domain_str) = s
570 .split_once(':')
571 .map(|(gmid, domain)| (gmid, Some(domain)))
572 .unwrap_or((s, None));
573
574 let gmid = Eui64::from_str(gmid_str).map_err(|_| AttributeError::InvalidParamValue {
575 param: "PTP GMID".to_string(),
576 val: gmid_str.to_string(),
577 attr: String::new(), })?;
579
580 let domain = domain_str.map(PtpDomain::from_str).transpose()?;
581
582 Ok(Self::GmidDomain { gmid, domain })
583 }
584}
585
586impl fmt::Display for PtpServer {
587 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588 match self {
589 Self::Traceable => write!(f, "traceable"),
590 Self::GmidDomain { gmid, domain } => {
591 if let Some(dom) = domain {
592 write!(f, "{gmid}:{dom}")
593 } else {
594 write!(f, "{gmid}")
595 }
596 }
597 }
598 }
599}
600
601#[derive(Debug, Copy, Clone, PartialEq, Eq)]
603pub struct Eui64 {
604 pub bytes: [u8; 8],
605}
606
607impl Eui64 {
608 pub fn new(id: u64) -> Self {
609 Self {
610 bytes: id.to_be_bytes(),
611 }
612 }
613
614 pub fn as_u64(&self) -> u64 {
615 u64::from_be_bytes(self.bytes)
616 }
617}
618
619impl FromStr for Eui64 {
620 type Err = AttributeError;
621
622 fn from_str(s: &str) -> Result<Self, AttributeError> {
623 if s.len() != 7 * 3 + 2 {
624 return Err(AttributeError::InvalidParamValue {
625 param: "EUI64".to_string(),
626 val: s.to_string(),
627 attr: String::new(), });
629 }
630 let mut bytes = [0u8; 8];
631 for (i, digit) in s.split('-').enumerate() {
632 if digit.len() != 2 {
633 return Err(AttributeError::InvalidParamValue {
634 param: "EUI64 segment".to_string(),
635 val: digit.to_string(),
636 attr: String::new(), });
638 }
639 bytes[i] =
640 u8::from_str_radix(digit, 16).map_err(|_| AttributeError::InvalidParamValue {
641 param: "EUI64 hex digit".to_string(),
642 val: digit.to_string(),
643 attr: String::new(), })?;
645 }
646
647 Ok(Self { bytes })
648 }
649}
650
651impl From<u64> for Eui64 {
652 fn from(id: u64) -> Self {
653 Eui64::new(id)
654 }
655}
656
657impl fmt::Display for Eui64 {
658 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659 for byte in &self.bytes[..7] {
660 write!(f, "{:02X}-", byte)?;
661 }
662 write!(f, "{:02X}", self.bytes[7])
663 }
664}
665
666#[derive(Debug, Clone, PartialEq, Eq)]
677pub enum PtpDomain {
678 DomainName {
680 name: String,
681 },
682 DomainNumber(u8),
684}
685
686impl PtpDomain {
687 pub fn from_name(name: impl ToString) -> Self {
689 PtpDomain::DomainName {
690 name: name.to_string(),
691 }
692 }
693
694 pub fn try_from_number(number: u8) -> Result<Self, AttributeError> {
698 if number > 127 {
699 return Err(AttributeError::InvalidParamValue {
700 param: "PTP domain".to_string(),
701 val: number.to_string(),
702 attr: String::new(), });
704 }
705
706 Ok(PtpDomain::DomainNumber(number))
707 }
708}
709
710impl FromStr for PtpDomain {
711 type Err = AttributeError;
712
713 fn from_str(s: &str) -> Result<Self, AttributeError> {
714 if s.len() == 16 {
717 Ok(Self::DomainName {
718 name: s.to_string(),
719 })
720 } else if let Ok(domain_num) = s.parse::<u8>() {
721 if domain_num > 127 {
722 return Err(AttributeError::InvalidParamValue {
723 param: "PTP domain".to_string(),
724 val: domain_num.to_string(),
725 attr: String::new(), });
727 }
728
729 Ok(Self::DomainNumber(domain_num))
730 } else {
731 Err(AttributeError::InvalidParamValue {
732 param: "PTP domain".to_string(),
733 val: s.to_string(),
734 attr: String::new(), })
736 }
737 }
738}
739
740impl fmt::Display for PtpDomain {
741 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742 match self {
743 Self::DomainName { name } => write!(f, "{name}"),
744 Self::DomainNumber(n) => write!(f, "{n}"),
745 }
746 }
747}
748
749#[derive(Debug, Clone, Copy, PartialEq, Eq)]
751pub enum PrivateSource {
752 Standard,
753 Traceable,
754}
755
756#[derive(Debug, Clone, PartialEq, Eq)]
758pub struct ClockSourceExt {
759 pub name: String,
760 pub value: Option<String>,
761}
762
763impl ClockSourceExt {
764 pub fn new(name: impl ToString) -> Self {
766 ClockSourceExt {
767 name: name.to_string(),
768 value: None,
769 }
770 }
771
772 pub fn with_value(name: impl ToString, value: impl ToString) -> Self {
774 ClockSourceExt {
775 name: name.to_string(),
776 value: Some(value.to_string()),
777 }
778 }
779
780 pub fn set_value(&mut self, value: impl ToString) {
781 self.value = Some(value.to_string());
782 }
783
784 pub fn builder(name: impl ToString) -> crate::builders::ClockSourceExt {
786 crate::builders::ClockSourceExt::new(name)
787 }
788}
789
790impl FromStr for ClockSourceExt {
791 type Err = AttributeError;
792
793 fn from_str(s: &str) -> Result<Self, AttributeError> {
794 let (name, value) = s
795 .split_once('=')
796 .map(|(name, value)| (name, Some(value)))
797 .unwrap_or((s, None));
798 if name.is_empty() {
799 return Err(AttributeError::ParamNotFound {
800 param: "Clock source name".to_string(),
801 attr: String::new(), });
803 }
804
805 Ok(Self {
806 name: name.to_string(),
807 value: value.map(|s| s.to_string()),
808 })
809 }
810}
811
812impl fmt::Display for ClockSourceExt {
813 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
814 if let Some(ref value) = self.value {
815 write!(f, "{}={value}", self.name)
816 } else {
817 write!(f, "{}", self.name)
818 }
819 }
820}
821
822#[derive(Debug, Clone, PartialEq, Eq, Default)]
826pub struct MediaClockSource {
827 pub id: Option<MediaClockId>,
828 pub clock: MediaClock,
829}
830
831impl MediaClockSource {
832 pub fn new(clock: impl Into<MediaClock>) -> Self {
834 MediaClockSource {
835 id: None,
836 clock: clock.into(),
837 }
838 }
839
840 pub fn with_clock_id(clock: impl Into<MediaClock>, id: MediaClockId) -> Self {
842 MediaClockSource {
843 id: Some(id),
844 clock: clock.into(),
845 }
846 }
847
848 pub fn new_direct() -> Self {
850 MediaClock::new_direct().into()
851 }
852
853 pub fn from_direct_offset(offset: u32) -> Self {
855 MediaClock::from_direct_offset(offset).into()
856 }
857
858 pub fn from_direct_offset_with_id(offset: u32, id: MediaClockId) -> Self {
860 MediaClockSource {
861 id: Some(id),
862 clock: MediaClock::from_direct_offset(offset),
863 }
864 }
865
866 pub fn from_direct_rate(rate: impl Into<Rate>) -> Self {
868 MediaClock::from_direct_rate(rate).into()
869 }
870
871 pub fn from_direct_rate_with_id(rate: impl Into<Rate>, id: MediaClockId) -> Self {
873 MediaClockSource {
874 id: Some(id),
875 clock: MediaClock::from_direct_rate(rate),
876 }
877 }
878
879 pub fn from_direct_offset_and_rate(offset: u32, rate: impl Into<Rate>) -> Self {
881 MediaClock::from_direct_offset_and_rate(offset, rate).into()
882 }
883
884 pub fn from_direct_offset_and_rate_with_id(
886 offset: u32,
887 rate: impl Into<Rate>,
888 id: MediaClockId,
889 ) -> Self {
890 MediaClockSource {
891 id: Some(id),
892 clock: MediaClock::from_direct_offset_and_rate(offset, rate),
893 }
894 }
895
896 pub fn from_ieee1722_stream_id(ieee1722_stream_id: Eui64) -> Self {
898 MediaClock::from_ieee1722_stream_id(ieee1722_stream_id).into()
899 }
900
901 pub fn from_ieee1722_stream_id_with_id(ieee1722_stream_id: Eui64, id: MediaClockId) -> Self {
903 MediaClockSource {
904 id: Some(id),
905 clock: MediaClock::from_ieee1722_stream_id(ieee1722_stream_id),
906 }
907 }
908
909 pub fn from_extended_name(name: impl ToString) -> Self {
911 MediaClock::from_extended_name(name).into()
912 }
913
914 pub fn from_extended_name_with_id(name: impl ToString, id: MediaClockId) -> Self {
916 MediaClockSource {
917 id: Some(id),
918 clock: MediaClock::from_extended_name(name),
919 }
920 }
921
922 pub fn from_extended_name_with_value(name: impl ToString, value: impl ToString) -> Self {
924 MediaClock::from_extended_name_with_value(name, value).into()
925 }
926
927 pub fn from_extended_name_with_value_with_id(
929 name: impl ToString,
930 value: impl ToString,
931 id: MediaClockId,
932 ) -> Self {
933 MediaClockSource {
934 id: Some(id),
935 clock: MediaClock::from_extended_name_with_value(name, value),
936 }
937 }
938
939 pub fn set_id(&mut self, id: MediaClockId) {
940 self.id = Some(id);
941 }
942
943 pub fn builder(clock: impl Into<MediaClock>) -> crate::builders::MediaClockSource {
945 crate::builders::MediaClockSource::new(clock)
946 }
947}
948
949impl<T: Into<MediaClock>> From<T> for MediaClockSource {
950 fn from(media_clock: T) -> Self {
951 MediaClockSource {
952 id: None,
953 clock: media_clock.into(),
954 }
955 }
956}
957
958impl FromStr for MediaClockSource {
959 type Err = AttributeError;
960
961 fn from_str(mut s: &str) -> Result<Self, AttributeError> {
962 let mut id = None;
963 if let Some(id_str) = s.strip_prefix("id=") {
964 let (id_str, rest) =
965 id_str
966 .split_once(' ')
967 .ok_or_else(|| AttributeError::ParamNotFound {
968 param: "media clock source".to_string(),
969 attr: <Self as TypedAttribute>::NAME.to_string(),
970 })?;
971 id = Some(MediaClockId::from_str(id_str).with_attr(<Self as TypedAttribute>::NAME)?);
972
973 s = rest;
974 }
975
976 let clock = MediaClock::from_str(s).with_attr(<Self as TypedAttribute>::NAME)?;
977
978 Ok(Self { id, clock })
979 }
980}
981
982impl fmt::Display for MediaClockSource {
983 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
984 if let Some(ref id) = self.id {
985 write!(f, "id={id} {}", self.clock)?;
986 } else {
987 write!(f, "{}", self.clock)?;
988 }
989 Ok(())
990 }
991}
992
993impl TypedAttribute for MediaClockSource {
994 const NAME: &'static str = "mediaclk";
995}
996
997#[derive(Debug, Clone, Default, PartialEq, Eq)]
999pub enum MediaClock {
1000 #[default]
1001 Sender,
1002 Direct(Direct),
1003 Ieee1722StreamId(Eui64),
1004 Ext(MediaClockExt),
1005}
1006
1007impl MediaClock {
1008 pub fn new_direct() -> Self {
1010 MediaClock::Direct(Direct::new())
1011 }
1012
1013 pub fn from_direct_offset(offset: u32) -> Self {
1015 MediaClock::Direct(Direct::with_offset(offset))
1016 }
1017
1018 pub fn from_direct_rate(rate: impl Into<Rate>) -> Self {
1020 MediaClock::Direct(Direct::with_rate(rate))
1021 }
1022
1023 pub fn from_direct_offset_and_rate(offset: u32, rate: impl Into<Rate>) -> Self {
1025 MediaClock::Direct(Direct::with_offset_and_rate(offset, rate))
1026 }
1027
1028 pub fn from_ieee1722_stream_id(ieee1722_stream_id: Eui64) -> Self {
1030 MediaClock::Ieee1722StreamId(ieee1722_stream_id)
1031 }
1032
1033 pub fn from_extended_name(name: impl ToString) -> Self {
1035 MediaClock::Ext(MediaClockExt::new(name))
1036 }
1037
1038 pub fn from_extended_name_with_value(name: impl ToString, value: impl ToString) -> Self {
1040 MediaClock::Ext(MediaClockExt::with_value(name, value))
1041 }
1042}
1043
1044impl From<Direct> for MediaClock {
1045 fn from(direct: Direct) -> Self {
1046 MediaClock::Direct(direct)
1047 }
1048}
1049
1050impl From<Eui64> for MediaClock {
1051 fn from(ieee1722_stream_id: Eui64) -> Self {
1052 MediaClock::Ieee1722StreamId(ieee1722_stream_id)
1053 }
1054}
1055
1056impl From<MediaClockExt> for MediaClock {
1057 fn from(media_clock_ext: MediaClockExt) -> Self {
1058 MediaClock::Ext(media_clock_ext)
1059 }
1060}
1061
1062impl FromStr for MediaClock {
1063 type Err = AttributeError;
1064
1065 fn from_str(s: &str) -> Result<Self, AttributeError> {
1066 if s == "sender" {
1067 Ok(Self::Sender)
1068 } else if let Some(s) = s.strip_prefix("direct") {
1069 Ok(Self::Direct(
1070 Direct::from_str(s).with_param_context("direct")?,
1071 ))
1072 } else if let Some(s) = s.strip_prefix("IEEE1722=") {
1073 let gmid = Eui64::from_str(s).with_param_context("IEEE1722 stream ID")?;
1074 Ok(Self::Ieee1722StreamId(gmid))
1075 } else {
1076 Ok(Self::Ext(MediaClockExt::from_str(s)?))
1077 }
1078 }
1079}
1080
1081impl fmt::Display for MediaClock {
1082 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1083 match self {
1084 Self::Sender => write!(f, "sender"),
1085 Self::Direct(Direct { offset, rate }) => {
1086 if let Some(offset) = offset {
1087 write!(f, "direct={offset}")?;
1088 } else {
1089 write!(f, "direct")?;
1090 }
1091 if let Some(Rate {
1092 numerator,
1093 denominator,
1094 }) = rate
1095 {
1096 write!(f, " rate={numerator}/{denominator}")?;
1097 }
1098 Ok(())
1099 }
1100 Self::Ieee1722StreamId(gmid) => write!(f, "IEEE1722={gmid}"),
1101 Self::Ext(ext) => {
1102 if let Some(ref value) = ext.value {
1103 write!(f, "{}={value}", ext.name)
1104 } else {
1105 write!(f, "{}", ext.name)
1106 }
1107 }
1108 }
1109 }
1110}
1111
1112#[derive(Debug, Clone, PartialEq, Eq)]
1114pub struct MediaClockId {
1115 pub src: bool,
1116 pub tag: String,
1117}
1118
1119impl MediaClockId {
1120 pub fn new(tag: impl ToString) -> Self {
1122 MediaClockId {
1123 src: false,
1124 tag: tag.to_string(),
1125 }
1126 }
1127
1128 pub fn new_with_src_prefix(tag: impl ToString) -> Self {
1130 MediaClockId {
1131 src: true,
1132 tag: tag.to_string(),
1133 }
1134 }
1135
1136 pub fn set_src_prefix(&mut self, src: bool) {
1137 self.src = src;
1138 }
1139}
1140
1141impl FromStr for MediaClockId {
1142 type Err = AttributeError;
1143
1144 fn from_str(s: &str) -> Result<Self, AttributeError> {
1145 let (src, tag_str) = if let Some(s) = s.strip_prefix("src:") {
1146 (true, s)
1147 } else {
1148 (false, s)
1149 };
1150
1151 if tag_str.is_empty() {
1152 return Err(AttributeError::ParamNotFound {
1153 param: "media clock tag".to_string(),
1154 attr: String::new(), });
1156 }
1157
1158 Ok(Self {
1159 src,
1160 tag: tag_str.to_string(),
1161 })
1162 }
1163}
1164
1165impl fmt::Display for MediaClockId {
1166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1167 if self.src {
1168 write!(f, "src:{}", self.tag)
1169 } else {
1170 write!(f, "{}", self.tag)
1171 }
1172 }
1173}
1174
1175#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
1177pub struct Rate {
1178 pub numerator: u32,
1179 pub denominator: u32,
1180}
1181
1182impl Rate {
1183 pub fn new(numerator: u32, denominator: u32) -> Self {
1184 Rate {
1185 numerator,
1186 denominator,
1187 }
1188 }
1189
1190 pub fn is_one(&self) -> bool {
1192 self.numerator == 1 && self.denominator == 1
1193 }
1194}
1195
1196impl From<(u32, u32)> for Rate {
1197 fn from(rate: (u32, u32)) -> Self {
1198 Rate {
1199 numerator: rate.0,
1200 denominator: rate.1,
1201 }
1202 }
1203}
1204
1205#[derive(Debug, Clone, PartialEq, Eq, Default)]
1207pub struct Direct {
1208 pub offset: Option<u32>,
1209 pub rate: Option<Rate>,
1210}
1211
1212impl Direct {
1213 pub fn new() -> Self {
1215 Direct {
1216 offset: None,
1217 rate: None,
1218 }
1219 }
1220
1221 pub fn with_offset(offset: u32) -> Self {
1223 Direct {
1224 offset: Some(offset),
1225 rate: None,
1226 }
1227 }
1228
1229 pub fn set_offset(&mut self, offset: u32) {
1230 self.offset = Some(offset);
1231 }
1232
1233 pub fn with_rate(rate: impl Into<Rate>) -> Self {
1235 Direct {
1236 offset: None,
1237 rate: Some(rate.into()),
1238 }
1239 }
1240
1241 pub fn set_rate(&mut self, rate: impl Into<Rate>) {
1242 self.rate = Some(rate.into());
1243 }
1244
1245 pub fn with_offset_and_rate(offset: u32, rate: impl Into<Rate>) -> Self {
1247 Direct {
1248 offset: Some(offset),
1249 rate: Some(rate.into()),
1250 }
1251 }
1252
1253 pub fn builder() -> crate::builders::Direct {
1255 crate::builders::Direct::new()
1256 }
1257}
1258
1259impl FromStr for Direct {
1260 type Err = AttributeError;
1261
1262 fn from_str(s: &str) -> Result<Self, AttributeError> {
1263 if s.is_empty() {
1264 return Ok(Self {
1265 offset: None,
1266 rate: None,
1267 });
1268 }
1269
1270 let (offset, s) = if let Some(s) = s.strip_prefix('=') {
1271 let (offset_str, s) = s.split_once(' ').unwrap_or((s, ""));
1272
1273 let offset =
1274 offset_str
1275 .parse::<u32>()
1276 .map_err(|_| AttributeError::InvalidParamValue {
1277 param: "offset".to_string(),
1278 val: offset_str.to_string(),
1279 attr: String::new(), })?;
1281
1282 (Some(offset), s)
1283 } else if let Some(s) = s.strip_prefix(' ') {
1284 (None, s)
1285 } else {
1286 return Err(AttributeError::InvalidParamValue {
1289 param: "direct clock parameters".to_string(),
1290 val: s.to_string(),
1291 attr: String::new(), });
1293 };
1294
1295 let rate = if s.is_empty() {
1296 None
1297 } else if let Some(s) = s.strip_prefix("rate=") {
1298 let (num_str, den_str) =
1299 s.split_once('/')
1300 .ok_or_else(|| AttributeError::InvalidParamValue {
1301 param: "rate format".to_string(),
1302 val: s.to_string(),
1303 attr: String::new(), })?;
1305
1306 let numerator =
1307 num_str
1308 .parse::<u32>()
1309 .map_err(|_| AttributeError::InvalidParamValue {
1310 param: "rate numerator".to_string(),
1311 val: num_str.to_string(),
1312 attr: String::new(), })?;
1314 let denominator =
1315 den_str
1316 .parse::<u32>()
1317 .map_err(|_| AttributeError::InvalidParamValue {
1318 param: "rate denominator".to_string(),
1319 val: den_str.to_string(),
1320 attr: String::new(), })?;
1322 if denominator == 0 {
1323 return Err(AttributeError::InvalidParamValue {
1324 param: "rate denominator".to_string(),
1325 val: den_str.to_string(),
1326 attr: String::new(), });
1328 }
1329
1330 Some(Rate {
1331 numerator,
1332 denominator,
1333 })
1334 } else {
1335 return Err(AttributeError::InvalidParamValue {
1336 param: "direct clock parameters".to_string(),
1337 val: s.to_string(),
1338 attr: String::new(), });
1340 };
1341
1342 Ok(Self { offset, rate })
1343 }
1344}
1345
1346#[derive(Debug, Clone, PartialEq, Eq)]
1348pub struct MediaClockExt {
1349 pub name: String,
1350 pub value: Option<String>,
1351}
1352
1353impl MediaClockExt {
1354 pub fn new(name: impl ToString) -> Self {
1356 MediaClockExt {
1357 name: name.to_string(),
1358 value: None,
1359 }
1360 }
1361
1362 pub fn with_value(name: impl ToString, value: impl ToString) -> Self {
1364 MediaClockExt {
1365 name: name.to_string(),
1366 value: Some(value.to_string()),
1367 }
1368 }
1369
1370 pub fn set_value(&mut self, value: impl ToString) {
1371 self.value = Some(value.to_string());
1372 }
1373
1374 pub fn builder(name: impl ToString) -> crate::builders::MediaClockExt {
1376 crate::builders::MediaClockExt::new(name)
1377 }
1378}
1379
1380impl FromStr for MediaClockExt {
1381 type Err = AttributeError;
1382
1383 fn from_str(s: &str) -> Result<Self, AttributeError> {
1384 let (name, value) = s
1385 .split_once('=')
1386 .map(|(name, value)| (name, Some(value)))
1387 .unwrap_or((s, None));
1388
1389 if name.is_empty() {
1390 return Err(AttributeError::ParamNotFound {
1391 param: "clock source name".to_string(),
1392 attr: String::new(), });
1394 }
1395
1396 Ok(Self {
1397 name: name.to_string(),
1398 value: value.map(|s| s.to_string()),
1399 })
1400 }
1401}
1402
1403impl fmt::Display for MediaClockExt {
1404 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1405 if let Some(ref value) = self.value {
1406 write!(f, "{}={value}", self.name)
1407 } else {
1408 write!(f, "{}", self.name)
1409 }
1410 }
1411}
1412
1413#[cfg(test)]
1414mod tests {
1415 use super::*;
1416
1417 #[test]
1418 fn test_reference_clock() {
1419 let test_cases = [
1420 (
1421 "ntp=/traceable/",
1422 ReferenceClock::Ntp(Ntp {
1423 server: NtpServerAddr::Traceable,
1424 }),
1425 ),
1426 ("local", ReferenceClock::Local),
1427 (
1428 "ntp=203.0.113.10",
1429 ReferenceClock::Ntp(Ntp {
1430 server: NtpServerAddr::HostPort {
1431 hostname: "203.0.113.10".to_string(),
1432 port: None,
1433 },
1434 }),
1435 ),
1436 (
1437 "ntp=ntp.example.com:123",
1438 ReferenceClock::Ntp(Ntp {
1439 server: NtpServerAddr::HostPort {
1440 hostname: "ntp.example.com".to_string(),
1441 port: Some(123),
1442 },
1443 }),
1444 ),
1445 (
1446 "ntp=[::1]:123",
1447 ReferenceClock::Ntp(Ntp {
1448 server: NtpServerAddr::HostPort {
1449 hostname: "::1".to_string(),
1450 port: Some(123),
1451 },
1452 }),
1453 ),
1454 (
1455 "ntp=::1",
1456 ReferenceClock::Ntp(Ntp {
1457 server: NtpServerAddr::HostPort {
1458 hostname: "::1".to_string(),
1459 port: None,
1460 },
1461 }),
1462 ),
1463 (
1464 "ntp=2001:db8::1",
1465 ReferenceClock::Ntp(Ntp {
1466 server: NtpServerAddr::HostPort {
1467 hostname: "2001:db8::1".to_string(),
1468 port: None,
1469 },
1470 }),
1471 ),
1472 (
1473 "ptp=IEEE802.1AS-2011:39-A7-94-FF-FE-07-CB-D0",
1474 ReferenceClock::Ptp(Ptp {
1475 version: PtpVersion::Ieee8021As_2011,
1476 server: PtpServer::GmidDomain {
1477 gmid: Eui64 {
1478 bytes: [0x39, 0xA7, 0x94, 0xFF, 0xFE, 0x07, 0xCB, 0xD0],
1479 },
1480 domain: None,
1481 },
1482 }),
1483 ),
1484 (
1485 "ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:0",
1486 ReferenceClock::Ptp(Ptp {
1487 version: PtpVersion::Ieee1588_2008,
1488 server: PtpServer::GmidDomain {
1489 gmid: Eui64 {
1490 bytes: [0x39, 0xA7, 0x94, 0xFF, 0xFE, 0x07, 0xCB, 0xD0],
1491 },
1492 domain: Some(PtpDomain::DomainNumber(0)),
1493 },
1494 }),
1495 ),
1496 (
1497 "ptp=IEEE1588-2002:39-A7-94-FF-FE-07-CB-D0:testdomain123456",
1498 ReferenceClock::Ptp(Ptp {
1499 version: PtpVersion::Ieee1588_2002,
1500 server: PtpServer::GmidDomain {
1501 gmid: Eui64 {
1502 bytes: [0x39, 0xA7, 0x94, 0xFF, 0xFE, 0x07, 0xCB, 0xD0],
1503 },
1504 domain: Some(PtpDomain::DomainName {
1505 name: "testdomain123456".to_string(),
1506 }),
1507 },
1508 }),
1509 ),
1510 (
1511 "ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:127",
1512 ReferenceClock::Ptp(Ptp {
1513 version: PtpVersion::Ieee1588_2008,
1514 server: PtpServer::GmidDomain {
1515 gmid: Eui64 {
1516 bytes: [0x39, 0xA7, 0x94, 0xFF, 0xFE, 0x07, 0xCB, 0xD0],
1517 },
1518 domain: Some(PtpDomain::DomainNumber(127)),
1519 },
1520 }),
1521 ),
1522 ("private", ReferenceClock::Private(PrivateSource::Standard)),
1523 (
1524 "private:traceable",
1525 ReferenceClock::Private(PrivateSource::Traceable),
1526 ),
1527 ("gps", ReferenceClock::Gps),
1528 ("gal", ReferenceClock::Gal),
1529 ("glonass", ReferenceClock::Glonass),
1530 ];
1531
1532 for (input, expected) in test_cases {
1533 println!("Testing: {input}");
1534 let parsed = match input.parse::<ReferenceClock>() {
1535 Ok(parsed) => parsed,
1536 Err(err) => {
1537 unreachable!("Failed to parse '{}': {}", input, err);
1538 }
1539 };
1540
1541 assert_eq!(parsed, expected, "Parse mismatch for: {input}");
1542
1543 let roundtrip = expected.to_string();
1544 assert_eq!(roundtrip, input, "Round-trip failed for: {input}");
1545 }
1546 }
1547
1548 #[test]
1549 fn test_reference_clock_invalid() {
1550 let invalid_cases = vec![
1551 (
1552 "",
1553 AttributeError::ParamNotFound {
1554 param: "Clock source name".to_string(),
1555 attr: "ts-refclk".to_string(),
1556 },
1557 ),
1558 (
1559 "ntp=",
1560 AttributeError::ParamNotFound {
1561 param: "NTP server address".to_string(),
1562 attr: "ts-refclk".to_string(),
1563 },
1564 ),
1565 (
1566 "ntp=:123",
1567 AttributeError::ParamNotFound {
1568 param: "hostname in NTP server address".to_string(),
1569 attr: "ts-refclk".to_string(),
1570 },
1571 ),
1572 (
1573 "ptp=:39-A7-94-FF-FE-07-CB-D0",
1574 AttributeError::ParamNotFound {
1575 param: "PTP version".to_string(),
1576 attr: "ts-refclk".to_string(),
1577 },
1578 ),
1579 (
1580 "ptp=IEEE1588-2008:",
1581 AttributeError::ParamNotFound {
1582 param: "PTP server".to_string(),
1583 attr: "ts-refclk".to_string(),
1584 },
1585 ),
1586 (
1587 "ptp=IEEE1588-2008:invalid-eui64",
1588 AttributeError::InvalidParamValue {
1589 param: "PTP GMID".to_string(),
1590 val: "invalid-eui64".to_string(),
1591 attr: "ts-refclk".to_string(),
1592 },
1593 ),
1594 (
1595 "ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:tooshortname",
1596 AttributeError::InvalidParamValue {
1597 param: "PTP domain".to_string(),
1598 val: "tooshortname".to_string(),
1599 attr: "ts-refclk".to_string(),
1600 },
1601 ),
1602 (
1603 "ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:128",
1604 AttributeError::InvalidParamValue {
1605 param: "PTP domain".to_string(),
1606 val: "128".to_string(),
1607 attr: "ts-refclk".to_string(),
1608 },
1609 ),
1610 ];
1611
1612 for (input, expected_err) in invalid_cases {
1613 println!("Testing invalid: {input}");
1614 assert_eq!(input.parse::<ReferenceClock>(), Err(expected_err));
1615 }
1616 }
1617
1618 #[test]
1619 fn ntp_server_addr_ipv6() {
1620 let clock = "ntp=[2001:db8::1]".parse::<ReferenceClock>().unwrap();
1623 assert_eq!(
1624 clock,
1625 ReferenceClock::Ntp(Ntp {
1626 server: NtpServerAddr::HostPort {
1627 hostname: "2001:db8::1".to_string(),
1628 port: None,
1629 }
1630 })
1631 );
1632 assert_eq!(clock.to_string(), "ntp=2001:db8::1");
1633
1634 for invalid in ["ntp=[foo]", "ntp=[::1", "ntp=[:]", "ntp=[::1]:notaport"] {
1635 assert!(
1636 invalid.parse::<ReferenceClock>().is_err(),
1637 "{} should be invalid",
1638 invalid
1639 );
1640 }
1641 }
1642
1643 #[test]
1644 fn test_media_clock_source() {
1645 let test_cases = vec![
1646 (
1647 "direct",
1648 MediaClockSource {
1649 id: None,
1650 clock: MediaClock::Direct(Direct {
1651 offset: None,
1652 rate: None,
1653 }),
1654 },
1655 ),
1656 (
1657 "direct=963214424",
1658 MediaClockSource {
1659 id: None,
1660 clock: MediaClock::Direct(Direct {
1661 offset: Some(963214424),
1662 rate: None,
1663 }),
1664 },
1665 ),
1666 (
1667 "direct=963214424 rate=1000/1001",
1668 MediaClockSource {
1669 id: None,
1670 clock: MediaClock::Direct(Direct {
1671 offset: Some(963214424),
1672 rate: Some(Rate::new(1000, 1001)),
1673 }),
1674 },
1675 ),
1676 (
1677 "direct rate=1000/1001",
1678 MediaClockSource {
1679 id: None,
1680 clock: MediaClock::Direct(Direct {
1681 offset: None,
1682 rate: Some(Rate::new(1000, 1001)),
1683 }),
1684 },
1685 ),
1686 (
1687 "sender",
1688 MediaClockSource {
1689 id: None,
1690 clock: MediaClock::Sender,
1691 },
1692 ),
1693 (
1694 "IEEE1722=38-D6-6D-8E-D2-78-13-2F",
1695 MediaClockSource {
1696 id: None,
1697 clock: MediaClock::Ieee1722StreamId(Eui64 {
1698 bytes: [0x38, 0xD6, 0x6D, 0x8E, 0xD2, 0x78, 0x13, 0x2F],
1699 }),
1700 },
1701 ),
1702 (
1703 "id=MDA6NjA6MmI6MjA6MTI6MWY= sender",
1704 MediaClockSource {
1705 id: Some(MediaClockId {
1706 src: false,
1707 tag: "MDA6NjA6MmI6MjA6MTI6MWY=".to_string(),
1708 }),
1709 clock: MediaClock::Sender,
1710 },
1711 ),
1712 (
1713 "id=src:MDA6NjA6MmI6MjA6MTI6MWY= direct",
1714 MediaClockSource {
1715 id: Some(MediaClockId {
1716 src: true,
1717 tag: "MDA6NjA6MmI6MjA6MTI6MWY=".to_string(),
1718 }),
1719 clock: MediaClock::Direct(Direct {
1720 offset: None,
1721 rate: None,
1722 }),
1723 },
1724 ),
1725 ];
1726
1727 for (input, expected) in test_cases {
1728 println!("Testing: {input}");
1729 let parsed = match input.parse::<MediaClockSource>() {
1730 Ok(parsed) => parsed,
1731 Err(err) => {
1732 unreachable!("Failed to parse '{}': {}", input, err);
1733 }
1734 };
1735
1736 assert_eq!(parsed, expected, "Parse mismatch for: {input}");
1737
1738 let roundtrip = expected.to_string();
1739 assert_eq!(roundtrip, input, "Round-trip failed for: {input}");
1740 }
1741 }
1742
1743 #[test]
1744 fn test_media_clock_source_invalid() {
1745 let invalid_cases = vec![
1746 (
1747 "",
1748 AttributeError::ParamNotFound {
1749 param: "clock source name".to_string(),
1750 attr: "mediaclk".to_string(),
1751 },
1752 ),
1753 (
1754 "direct=notanumber",
1755 AttributeError::InvalidParamValue {
1756 param: "direct (offset)".to_string(),
1757 val: "notanumber".to_string(),
1758 attr: "mediaclk".to_string(),
1759 },
1760 ),
1761 (
1762 "direct=count rate=100/abc",
1763 AttributeError::InvalidParamValue {
1764 param: "direct (offset)".to_string(),
1765 val: "count".to_string(),
1766 attr: "mediaclk".to_string(),
1767 },
1768 ),
1769 (
1770 "directfoo",
1774 AttributeError::InvalidParamValue {
1775 param: "direct (direct clock parameters)".to_string(),
1776 val: "foo".to_string(),
1777 attr: "mediaclk".to_string(),
1778 },
1779 ),
1780 (
1781 "direct rate=1000/0",
1782 AttributeError::InvalidParamValue {
1783 param: "direct (rate denominator)".to_string(),
1784 val: "0".to_string(),
1785 attr: "mediaclk".to_string(),
1786 },
1787 ),
1788 (
1789 "IEEE1722=invalid-eui64",
1790 AttributeError::InvalidParamValue {
1791 param: "IEEE1722 stream ID (EUI64)".to_string(),
1792 val: "invalid-eui64".to_string(),
1793 attr: "mediaclk".to_string(),
1794 },
1795 ),
1796 (
1797 "id= sender",
1798 AttributeError::ParamNotFound {
1799 param: "media clock tag".to_string(),
1800 attr: "mediaclk".to_string(),
1801 },
1802 ),
1803 (
1804 "id=tag",
1805 AttributeError::ParamNotFound {
1806 param: "media clock source".to_string(),
1807 attr: "mediaclk".to_string(),
1808 },
1809 ),
1810 ];
1811
1812 for (input, expected_err) in invalid_cases {
1813 println!("Testing invalid: {input}");
1814 assert_eq!(input.parse::<MediaClockSource>(), Err(expected_err));
1815 }
1816 }
1817
1818 #[test]
1819 fn parse_clock_signalling_attr() {
1820 use crate::{Ssrc, SsrcAttribute};
1821
1822 let sdp = "v=0\n\
1823 m=image 54111 TCP t38\n\
1824 c=IN IP4 192.0.2.2\n\
1825 a=ts-refclk:local\n\
1826 a=mediaclk:direct=654321\n\
1827 a=ssrc:1234 ts-refclk:ntp=pool.ntp.org\n\
1828 a=ssrc:1234 mediaclk:direct=987654\n\
1829 ";
1830
1831 let medias = crate::Session::parse(sdp.as_bytes()).unwrap().medias;
1832 assert_eq!(
1833 medias[0].get_first_attribute_typed::<ReferenceClock>(),
1834 Some(Ok(ReferenceClock::Local))
1835 );
1836 assert_eq!(
1837 medias[0].get_first_attribute_typed::<MediaClockSource>(),
1838 Some(Ok(MediaClockSource {
1839 id: None,
1840 clock: MediaClock::Direct(Direct {
1841 offset: Some(654321),
1842 rate: None
1843 })
1844 })),
1845 );
1846
1847 let ssrc_attrs = medias[0].attributes_typed::<Ssrc>();
1848 for ssrc_attr in ssrc_attrs {
1849 let ssrc_attr = ssrc_attr.unwrap();
1850 match ssrc_attr.attribute {
1851 SsrcAttribute::ReferenceClock => {
1852 let ssrc_refclk = ssrc_attr.get_typed::<ReferenceClock>().unwrap();
1853 assert_eq!(
1854 ssrc_refclk,
1855 ReferenceClock::Ntp(Ntp {
1856 server: NtpServerAddr::HostPort {
1857 hostname: "pool.ntp.org".to_string(),
1858 port: None,
1859 }
1860 })
1861 );
1862 }
1863 SsrcAttribute::MediaClockSource => {
1864 let ssrc_mediaclk = ssrc_attr.get_typed::<MediaClockSource>().unwrap();
1865 assert_eq!(
1866 ssrc_mediaclk,
1867 MediaClockSource {
1868 id: None,
1869 clock: MediaClock::Direct(Direct {
1870 offset: Some(987654),
1871 rate: None
1872 })
1873 }
1874 );
1875 }
1876 other => unreachable!("{:?}", other),
1877 }
1878 }
1879 }
1880
1881 #[test]
1882 fn build_clock_signalling_attr() {
1883 use crate::{Media, MediaType, TransportProto};
1884
1885 const NTP_SERVER_ADDR: &str = "pool.ntp.org";
1886
1887 let media = Media::builder(MediaType::Audio, 5000, TransportProto::RtpAvp, 96)
1888 .attribute(ReferenceClock::Local)
1889 .attribute(ReferenceClock::from_ntp_hostname(NTP_SERVER_ADDR))
1890 .build();
1891
1892 let mut refclk_iter = media.attributes_typed::<ReferenceClock>();
1893 assert_eq!(refclk_iter.next(), Some(Ok(ReferenceClock::Local)));
1894 assert_eq!(
1895 refclk_iter.next(),
1896 Some(Ok(ReferenceClock::Ntp(Ntp {
1897 server: NtpServerAddr::HostPort {
1898 hostname: NTP_SERVER_ADDR.to_string(),
1899 port: None
1900 }
1901 })))
1902 );
1903 }
1904}