Skip to main content

sdp_types/
clock_signalling.rs

1use std::fmt;
2use std::net::IpAddr;
3use std::str::FromStr;
4
5use crate::attributes::{AttributeError, ErrorContext, TypedAttribute};
6
7/// Reference clock.
8///
9/// This maps to the `ts-refclk` attribute.
10#[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    /// Constructs an NTP [`ReferenceClock`] from the specified hostname.
24    pub fn from_ntp_hostname(hostname: impl ToString) -> Self {
25        NtpServerAddr::from_hostname(hostname).into()
26    }
27
28    /// Constructs an NTP [`ReferenceClock`] from the specified hostname and port.
29    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    /// Constructs a traceable NTP [`ReferenceClock`].
34    pub fn new_ntp_traceable() -> Self {
35        NtpServerAddr::new_traceable().into()
36    }
37
38    /// Constructs a PTP [`ReferenceClock`] from the specified [`PtpVersion`] and GMID.
39    pub fn from_ptp_gmid(version: PtpVersion, gmid: impl Into<Eui64>) -> Self {
40        Ptp::from_gmid(version, gmid).into()
41    }
42
43    /// Constructs a PTP [`ReferenceClock`] from the specified GMID and [`PtpDomain`].
44    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    /// Constructs a PTP [`ReferenceClock`] from the specified GMID and domain name.
49    ///
50    /// This will assign version IEEE 1588-2002.
51    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    /// Tries to construct a PTP [`ReferenceClock`] from the specified GMID and domain  number.
56    ///
57    /// This will assign version IEEE 1588-2008.
58    ///
59    /// Returns an `Error` if `number` is not in range (0-127) (inclusive)
60    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    /// Constructs a traceable PTP [`ReferenceClock`].
68    pub fn new_ptp_traceable(version: PtpVersion) -> Self {
69        Ptp::new_traceable(version).into()
70    }
71
72    /// Constructs a standard private source [`ReferenceClock`].
73    pub fn new_private_source_standard() -> Self {
74        PrivateSource::Standard.into()
75    }
76
77    /// Constructs a traceable private source [`ReferenceClock`].
78    pub fn new_private_source_traceable() -> Self {
79        PrivateSource::Traceable.into()
80    }
81
82    /// Constructs an extended clock source [`ReferenceClock`] from the specified name.
83    pub fn from_clock_source_ext_name(name: impl ToString) -> Self {
84        ClockSourceExt::new(name).into()
85    }
86
87    /// Constructs an extended clock source [`ReferenceClock`] from the specified name and value.
88    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/// NTP clock source with server address.
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct Ntp {
192    pub server: NtpServerAddr,
193}
194
195impl Ntp {
196    /// Constructs an [`Ntp`] clock source from the specified hostname.
197    pub fn from_hostname(hostname: impl ToString) -> Ntp {
198        NtpServerAddr::from_hostname(hostname).into()
199    }
200
201    /// Constructs an [`Ntp`] clock source from the specified hostname and port.
202    pub fn from_hostname_with_port(hostname: impl ToString, port: u16) -> Ntp {
203        NtpServerAddr::from_hostname_with_port(hostname, port).into()
204    }
205
206    /// Constructs a traceable [`Ntp`] clock source.
207    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/// NTP server address: hostport or "/traceable/".
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub enum NtpServerAddr {
236    HostPort { hostname: String, port: Option<u16> },
237    Traceable,
238}
239
240impl NtpServerAddr {
241    /// Constructs an [`NtpServerAddr`] from the specified hostname.
242    pub fn from_hostname(hostname: impl ToString) -> NtpServerAddr {
243        NtpServerAddr::HostPort {
244            hostname: hostname.to_string(),
245            port: None,
246        }
247    }
248
249    /// Constructs an [`NtpServerAddr`] from the specified hostname and port.
250    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    /// Constructs a traceable [`NtpServerAddr`].
258    pub fn new_traceable() -> NtpServerAddr {
259        NtpServerAddr::Traceable
260    }
261
262    /// Constructs a [`crate::builders::NtpServerAddr`] from the specified hostname.
263    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(), // will be set bubbling up
276            });
277        }
278
279        if s == "/traceable/" {
280            Ok(Self::Traceable)
281        } else if s.parse::<IpAddr>().is_ok() {
282            // A bare IP address, in particular an IPv6 address which must not
283            // be mistaken for a host:port split.
284            Ok(Self::HostPort {
285                hostname: s.to_string(),
286                port: None,
287            })
288        } else if let Some(rest) = s.strip_prefix('[') {
289            // Bracketed IPv6 address, with or without port.
290            let invalid = || AttributeError::InvalidParamValue {
291                param: "NTP server address".to_string(),
292                val: s.to_string(),
293                attr: String::new(), // will be set bubbling up
294            };
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(), // will be set bubbling up
314                });
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(), // will be set bubbling up
323                })?;
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                // Bracket IPv6 addresses so the address and port stay
346                // distinguishable.
347                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/// PTP clock source with version and server details.
363#[derive(Debug, Clone, PartialEq, Eq)]
364pub struct Ptp {
365    pub version: PtpVersion,
366    pub server: PtpServer,
367}
368
369impl Ptp {
370    /// Constructs a [`Ptp`] with the specified [`PtpVersion`] and [`PtpServer`].
371    pub fn new(version: PtpVersion, server: PtpServer) -> Self {
372        Ptp { version, server }
373    }
374
375    /// Constructs a [`Ptp`] from the specified [`PtpVersion`] and GMID.
376    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    /// Constructs a [`Ptp`] from the specified GMID and [`PtpDomain`].
384    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    /// Constructs a [`Ptp`] from the specified GMID and domain name.
397    ///
398    /// This will assign version IEEE 1588-2002.
399    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    /// Tries to construct a [`Ptp`] from the specified GMID and domain  number.
404    ///
405    /// This will assign version IEEE 1588-2008.
406    ///
407    /// Returns an `Error` if `number` is not in range (0-127) (inclusive)
408    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    /// Constructs a traceable [`Ptp`].
419    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(), // will be set bubbling up
442            })?;
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(), // will be set bubbling up
449            });
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/// PTP protocol version.
465#[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(), // will be set bubbling up
487                    });
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/// PTP server: GMID and domain or traceable.
507#[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    /// Constructs a [`PtpServer`] from the specified GMID.
518    pub fn from_gmid(gmid: impl Into<Eui64>) -> Self {
519        PtpServer::GmidDomain {
520            gmid: gmid.into(),
521            domain: None,
522        }
523    }
524
525    /// Constructs a [`PtpServer`] from the specified GMID and [`PtpDomain`].
526    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    /// Constructs a [`PtpServer`] from the specified GMID and domain name.
534    ///
535    /// This will assign version IEEE 1588-2002.
536    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    /// Tries to construct a [`PtpServer`] from the specified GMID and domain number.
544    ///
545    /// Returns an `Error` if `number` is not in range (0-127) (inclusive)
546    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(), // will be set bubbling up
578        })?;
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/// EUI-64 identifier.
602#[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(), // will be set bubbling up
628            });
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(), // will be set bubbling up
637                });
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(), // will be set bubbling up
644                })?;
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/// PTP domain.
667///
668/// This deviates from the RFC 7273 grammar, which defines both variants
669/// with `domain-name=` and `domain-nmbr=` prefixes and a domain name of
670/// 1-16 characters. None of the examples in the RFC use the prefixes, so
671/// parsing follows the examples and accepts a bare domain name or a bare
672/// domain number (0-127).
673///
674/// A domain name must be exactly 16 characters: a shorter one would be
675/// indistinguishable from a domain number when parsing the bare value.
676#[derive(Debug, Clone, PartialEq, Eq)]
677pub enum PtpDomain {
678    /// IEEE 1588-2002
679    DomainName {
680        name: String,
681    },
682    // IEEE 1588-2008 (Range: 0-127)
683    DomainNumber(u8),
684}
685
686impl PtpDomain {
687    /// Constructs a [`PtpDomain`] from the specified name.
688    pub fn from_name(name: impl ToString) -> Self {
689        PtpDomain::DomainName {
690            name: name.to_string(),
691        }
692    }
693
694    /// Tries to construct a [`PtpDomain`] from the specified number.
695    ///
696    /// Returns an `Error` if `number` is not in range (0-127) (inclusive)
697    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(), // will be set bubbling up
703            });
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        // Bare values instead of the `domain-name=`/`domain-nmbr=`
715        // prefixes of the RFC grammar, see the [`PtpDomain`] docs.
716        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(), // will be set bubbling up
726                });
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(), // will be set bubbling up
735            })
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/// Private source with optional traceable flag.
750#[derive(Debug, Clone, Copy, PartialEq, Eq)]
751pub enum PrivateSource {
752    Standard,
753    Traceable,
754}
755
756/// Extended clock source.
757#[derive(Debug, Clone, PartialEq, Eq)]
758pub struct ClockSourceExt {
759    pub name: String,
760    pub value: Option<String>,
761}
762
763impl ClockSourceExt {
764    /// Constructs a [`ClockSourceExt`] with the specified name.
765    pub fn new(name: impl ToString) -> Self {
766        ClockSourceExt {
767            name: name.to_string(),
768            value: None,
769        }
770    }
771
772    /// Constructs a [`ClockSourceExt`] with the specified name and value.
773    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    /// Constructs a [`crate::builders::ClockSourceExt`].
785    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(), // will be set bubbling up
802            });
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/// Media clock source type enumeration.
823///
824/// This maps to the `mediaclk` attribute.
825#[derive(Debug, Clone, PartialEq, Eq, Default)]
826pub struct MediaClockSource {
827    pub id: Option<MediaClockId>,
828    pub clock: MediaClock,
829}
830
831impl MediaClockSource {
832    /// Constructs a [`MediaClockSource`] from the specified [`MediaClock`].
833    pub fn new(clock: impl Into<MediaClock>) -> Self {
834        MediaClockSource {
835            id: None,
836            clock: clock.into(),
837        }
838    }
839
840    /// Constructs a [`MediaClockSource`] from the specified [`MediaClock`] and [`MediaClockId`].
841    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    /// Constructs a direct [`MediaClockSource`].
849    pub fn new_direct() -> Self {
850        MediaClock::new_direct().into()
851    }
852
853    /// Constructs a direct [`MediaClockSource`] from the specified offset.
854    pub fn from_direct_offset(offset: u32) -> Self {
855        MediaClock::from_direct_offset(offset).into()
856    }
857
858    /// Constructs a direct [`MediaClockSource`] from the specified offset and [`MediaClockId`].
859    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    /// Constructs a direct [`MediaClockSource`] from the specified rate.
867    pub fn from_direct_rate(rate: impl Into<Rate>) -> Self {
868        MediaClock::from_direct_rate(rate).into()
869    }
870
871    /// Constructs a direct [`MediaClockSource`] from the specified rate and [`MediaClockId`].
872    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    /// Constructs a direct [`MediaClockSource`] from the specified offset and rate.
880    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    /// Constructs a direct [`MediaClockSource`] from the specified offset, rate and [`MediaClockId`].
885    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    /// Constructs a [`MediaClockSource`] from the specified IEEE1722 stream id.
897    pub fn from_ieee1722_stream_id(ieee1722_stream_id: Eui64) -> Self {
898        MediaClock::from_ieee1722_stream_id(ieee1722_stream_id).into()
899    }
900
901    /// Constructs a [`MediaClockSource`] from the specified IEEE1722 stream id and [`MediaClockId`].
902    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    /// Constructs an extended [`MediaClockSource`] from the specified name.
910    pub fn from_extended_name(name: impl ToString) -> Self {
911        MediaClock::from_extended_name(name).into()
912    }
913
914    /// Constructs an extended [`MediaClockSource`] from the specified name and [`MediaClockId`].
915    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    /// Constructs an extended [`MediaClockSource`] from the specified name and value.
923    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    /// Constructs an extended [`MediaClockSource`] from the specified name and value and [`MediaClockId`].
928    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    /// Constructs a [`crate::builders::MediaClockSource`].
944    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/// Media clock.
998#[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    /// Constructs a direct [`MediaClock`].
1009    pub fn new_direct() -> Self {
1010        MediaClock::Direct(Direct::new())
1011    }
1012
1013    /// Constructs a direct [`MediaClock`] from the specified offset.
1014    pub fn from_direct_offset(offset: u32) -> Self {
1015        MediaClock::Direct(Direct::with_offset(offset))
1016    }
1017
1018    /// Constructs a direct [`MediaClock`] from the specified rate.
1019    pub fn from_direct_rate(rate: impl Into<Rate>) -> Self {
1020        MediaClock::Direct(Direct::with_rate(rate))
1021    }
1022
1023    /// Constructs a direct [`MediaClock`] from the specified offset and rate.
1024    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    /// Constructs a [`MediaClock`] from the specified IEEE1722 stream id.
1029    pub fn from_ieee1722_stream_id(ieee1722_stream_id: Eui64) -> Self {
1030        MediaClock::Ieee1722StreamId(ieee1722_stream_id)
1031    }
1032
1033    /// Constructs an extended [`MediaClock`] from the specified name.
1034    pub fn from_extended_name(name: impl ToString) -> Self {
1035        MediaClock::Ext(MediaClockExt::new(name))
1036    }
1037
1038    /// Constructs an extended [`MediaClock`] from the specified name and value.
1039    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/// Media clock ID with optional source prefix.
1113#[derive(Debug, Clone, PartialEq, Eq)]
1114pub struct MediaClockId {
1115    pub src: bool,
1116    pub tag: String,
1117}
1118
1119impl MediaClockId {
1120    /// Constructs a [`MediaClockId`] with the specified tag and without the 'src' prefix.
1121    pub fn new(tag: impl ToString) -> Self {
1122        MediaClockId {
1123            src: false,
1124            tag: tag.to_string(),
1125        }
1126    }
1127
1128    /// Constructs a [`MediaClockId`] with the specified tag and with the 'src' prefix.
1129    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(), // will be set bubbling up
1155            });
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/// Fractional rate.
1176#[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    /// Whether this Rate is the 1/1 fraction.
1191    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/// Direct media clock with optional rate.
1206#[derive(Debug, Clone, PartialEq, Eq, Default)]
1207pub struct Direct {
1208    pub offset: Option<u32>,
1209    pub rate: Option<Rate>,
1210}
1211
1212impl Direct {
1213    /// Constructs a [`Direct`] media clock with no offset nor rate.
1214    pub fn new() -> Self {
1215        Direct {
1216            offset: None,
1217            rate: None,
1218        }
1219    }
1220
1221    /// Constructs a [`Direct`] media clock with the specified offset.
1222    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    /// Constructs a [`Direct`] media clock with the specified rate.
1234    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    /// Constructs a [`Direct`] media clock with the specified offset & rate.
1246    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    /// Constructs a [`crate::builders::Direct`].
1254    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(), // will be set bubbling up
1280                    })?;
1281
1282            (Some(offset), s)
1283        } else if let Some(s) = s.strip_prefix(' ') {
1284            (None, s)
1285        } else {
1286            // Anything not separated from the "direct" token by a space is
1287            // invalid, e.g. "directfoo".
1288            return Err(AttributeError::InvalidParamValue {
1289                param: "direct clock parameters".to_string(),
1290                val: s.to_string(),
1291                attr: String::new(), // will be set bubbling up
1292            });
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(), // will be set bubbling up
1304                    })?;
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(), // will be set bubbling up
1313                    })?;
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(), // will be set bubbling up
1321                    })?;
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(), // will be set bubbling up
1327                });
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(), // will be set bubbling up
1339            });
1340        };
1341
1342        Ok(Self { offset, rate })
1343    }
1344}
1345
1346/// Extended media clock.
1347#[derive(Debug, Clone, PartialEq, Eq)]
1348pub struct MediaClockExt {
1349    pub name: String,
1350    pub value: Option<String>,
1351}
1352
1353impl MediaClockExt {
1354    /// Constructs a [`MediaClockExt`] with the specified name.
1355    pub fn new(name: impl ToString) -> Self {
1356        MediaClockExt {
1357            name: name.to_string(),
1358            value: None,
1359        }
1360    }
1361
1362    /// Constructs a [`MediaClockExt`] with the specified name and value.
1363    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    /// Constructs a [`crate::builders::MediaClockExt`].
1375    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(), // will be set bubbling up
1393            });
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        // Bracketed IPv6 without a port parses; Display omits the brackets
1621        // (RFC 5954 only requires them together with a port).
1622        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                // Garbage appended directly to the "direct" token (no
1771                // separating space) must be rejected instead of silently
1772                // producing an empty Direct clock.
1773                "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}