Skip to main content

sdp_types/
clock_signalling.rs

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