rice_c/
candidate.rs

1// Copyright (C) 2025 Matthew Waters <matthew@centricular.com>
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! ICE Candidates
10
11use std::ffi::{CStr, CString};
12
13/// ICE candidate.
14pub trait CandidateApi: sealed::CandidateAsC {
15    /// The component
16    fn component_id(&self) -> usize {
17        unsafe { (*self.as_c()).component_id }
18    }
19    /// The type of the Candidate
20    fn candidate_type(&self) -> CandidateType {
21        unsafe { (*self.as_c()).candidate_type.into() }
22    }
23    /// The network transport
24    fn transport(&self) -> TransportType {
25        unsafe { (*self.as_c()).transport_type.into() }
26    }
27
28    /// The (unique) foundation
29    fn foundation(&self) -> String {
30        unsafe {
31            CStr::from_ptr((*self.as_c()).foundation)
32                .to_str()
33                .unwrap()
34                .to_owned()
35        }
36    }
37    /// The priority
38    fn priority(&self) -> u32 {
39        unsafe { (*self.as_c()).priority }
40    }
41    /// The address to send to
42    fn address(&self) -> crate::Address {
43        unsafe { crate::Address::from_c_none((*self.as_c()).address) }
44    }
45    /// The address to send from
46    fn base_address(&self) -> crate::Address {
47        unsafe { crate::Address::from_c_none((*self.as_c()).base_address) }
48    }
49    /// Any related address that generated this candidate, e.g. STUN/TURN server
50    fn related_address(&self) -> Option<crate::Address> {
51        unsafe {
52            let related = (*self.as_c()).related_address;
53            if related.is_null() {
54                None
55            } else {
56                Some(crate::Address::from_c_none(related))
57            }
58        }
59    }
60    /// The type of TCP candidate
61    fn tcp_type(&self) -> TcpType {
62        unsafe { (*self.as_c()).tcp_type.into() }
63    }
64    // TODO: extensions
65}
66
67/// Errors produced when parsing a candidate
68#[derive(Debug, Copy, Clone, PartialEq, Eq)]
69#[repr(i32)]
70pub enum ParseCandidateError {
71    /// Not a candidate message.
72    NotCandidate = -1,
73    /// Invalid foundation value.
74    BadFoundation = -2,
75    /// Invalid component id.
76    BadComponentId = -3,
77    /// Invalid transport type.
78    BadTransportType = -4,
79    /// Invalid priority value.
80    BadPriority = -5,
81    /// Invalid network address.
82    BadAddress = -6,
83    /// Invalid candidate type.
84    BadCandidateType = -7,
85    /// Invalid extension format.
86    BadExtension = -8,
87    /// Data is not well formed.
88    Malformed = -9,
89}
90
91impl ParseCandidateError {
92    pub(crate) fn from_c(
93        value: crate::ffi::RiceParseCandidateError,
94    ) -> Result<(), ParseCandidateError> {
95        match value {
96            crate::ffi::RICE_PARSE_CANDIDATE_ERROR_SUCCESS => Ok(()),
97            crate::ffi::RICE_PARSE_CANDIDATE_ERROR_NOT_CANDIDATE => {
98                Err(ParseCandidateError::NotCandidate)
99            }
100            crate::ffi::RICE_PARSE_CANDIDATE_ERROR_BAD_FOUNDATION => {
101                Err(ParseCandidateError::BadFoundation)
102            }
103            crate::ffi::RICE_PARSE_CANDIDATE_ERROR_BAD_COMPONENT_ID => {
104                Err(ParseCandidateError::BadComponentId)
105            }
106            crate::ffi::RICE_PARSE_CANDIDATE_ERROR_BAD_TRANSPORT_TYPE => {
107                Err(ParseCandidateError::BadTransportType)
108            }
109            crate::ffi::RICE_PARSE_CANDIDATE_ERROR_BAD_PRIORITY => {
110                Err(ParseCandidateError::BadPriority)
111            }
112            crate::ffi::RICE_PARSE_CANDIDATE_ERROR_BAD_ADDRESS => {
113                Err(ParseCandidateError::BadAddress)
114            }
115            crate::ffi::RICE_PARSE_CANDIDATE_ERROR_BAD_CANDIDATE_TYPE => {
116                Err(ParseCandidateError::BadCandidateType)
117            }
118            crate::ffi::RICE_PARSE_CANDIDATE_ERROR_BAD_EXTENSION => {
119                Err(ParseCandidateError::BadExtension)
120            }
121            crate::ffi::RICE_PARSE_CANDIDATE_ERROR_MALFORMED => Err(ParseCandidateError::Malformed),
122            val => panic!("Unknown RiceParseCandidateError value {val:x?}"),
123        }
124    }
125}
126
127mod sealed {
128    pub trait CandidateAsC {
129        fn as_c(&self) -> *const crate::ffi::RiceCandidate;
130    }
131}
132
133/// An ICE candidate.
134#[derive(Debug)]
135pub struct Candidate {
136    ffi: crate::ffi::RiceCandidate,
137}
138
139unsafe impl Send for Candidate {}
140unsafe impl Sync for Candidate {}
141
142impl PartialEq<Candidate> for Candidate {
143    fn eq(&self, other: &Candidate) -> bool {
144        unsafe { crate::ffi::rice_candidate_eq(&self.ffi, &other.ffi) }
145    }
146}
147
148impl PartialEq<CandidateOwned> for Candidate {
149    fn eq(&self, other: &CandidateOwned) -> bool {
150        unsafe { crate::ffi::rice_candidate_eq(&self.ffi, other.ffi) }
151    }
152}
153
154impl Eq for Candidate {}
155
156impl Clone for Candidate {
157    fn clone(&self) -> Self {
158        unsafe {
159            let mut ret = Self {
160                ffi: crate::ffi::RiceCandidate::zeroed(),
161            };
162            crate::ffi::rice_candidate_copy_into(&self.ffi, &mut ret.ffi);
163            ret
164        }
165    }
166}
167
168impl CandidateApi for Candidate {}
169impl sealed::CandidateAsC for Candidate {
170    fn as_c(&self) -> *const crate::ffi::RiceCandidate {
171        &self.ffi
172    }
173}
174
175impl Drop for Candidate {
176    fn drop(&mut self) {
177        unsafe { crate::ffi::rice_candidate_clear(&mut self.ffi) }
178    }
179}
180
181impl Candidate {
182    /// Builds the candidate
183    ///
184    /// # Examples
185    ///
186    /// ```
187    /// # use rice_c::candidate::*;
188    /// # use rice_c::Address;
189    /// let addr: Address = "127.0.0.1:2345".parse().unwrap();
190    /// let candidate = Candidate::builder(
191    ///     1,
192    ///     CandidateType::Host,
193    ///     TransportType::Udp,
194    ///     "foundation",
195    ///     addr,
196    /// )
197    /// .priority(1234)
198    /// .build();
199    /// assert_eq!(candidate.to_sdp_string(), "a=candidate:foundation 1 UDP 1234 127.0.0.1 2345 typ host")
200    /// ```
201    pub fn builder(
202        component_id: usize,
203        ctype: CandidateType,
204        ttype: TransportType,
205        foundation: &str,
206        address: crate::Address,
207    ) -> CandidateBuilder {
208        unsafe {
209            let foundation = CString::new(foundation).unwrap();
210            let mut ret = CandidateBuilder {
211                ffi: crate::ffi::RiceCandidate::zeroed(),
212            };
213            let address = address.into_c_full();
214            let res = crate::ffi::rice_candidate_init(
215                &mut ret.ffi,
216                component_id,
217                ctype.into(),
218                ttype.into(),
219                foundation.as_ptr(),
220                address,
221            );
222            if res != crate::ffi::RICE_ERROR_SUCCESS {
223                let _address = crate::Address::from_c_full(address);
224                panic!("Failed to crate ICE candidate!");
225            }
226            ret
227        }
228    }
229
230    pub(crate) fn as_c(&self) -> *const crate::ffi::RiceCandidate {
231        &self.ffi
232    }
233
234    pub(crate) unsafe fn from_c_none(candidate: *const crate::ffi::RiceCandidate) -> Self {
235        unsafe {
236            let mut ret = Self {
237                ffi: crate::ffi::RiceCandidate::zeroed(),
238            };
239            crate::ffi::rice_candidate_copy_into(candidate, &mut ret.ffi);
240            ret
241        }
242    }
243
244    pub(crate) fn from_c_full(candidate: crate::ffi::RiceCandidate) -> Self {
245        Self { ffi: candidate }
246    }
247
248    /// Copy this candidate into a heap allocated [`CandidateOwned`].
249    pub fn to_owned(&self) -> CandidateOwned {
250        unsafe {
251            CandidateOwned {
252                ffi: crate::ffi::rice_candidate_copy(&self.ffi),
253            }
254        }
255    }
256
257    /// Serialize this candidate to a string for use in SDP
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// # use rice_c::candidate::*;
263    /// # use rice_c::Address;
264    /// let addr: Address = "127.0.0.1:2345".parse().unwrap();
265    /// let candidate = Candidate::builder(
266    ///     1,
267    ///     CandidateType::Host,
268    ///     TransportType::Udp,
269    ///     "foundation",
270    ///     addr,
271    /// )
272    /// .priority(1234)
273    /// .build();
274    /// assert_eq!(candidate.to_sdp_string(), "a=candidate:foundation 1 UDP 1234 127.0.0.1 2345 typ host")
275    /// ```
276    pub fn to_sdp_string(&self) -> String {
277        unsafe {
278            let res = crate::ffi::rice_candidate_to_sdp_string(&self.ffi);
279            let s = CStr::from_ptr(res);
280            let ret = s.to_str().unwrap().to_owned();
281            crate::ffi::rice_string_free(res);
282            ret
283        }
284    }
285
286    /// Parse an SDP candidate string into a candidate.
287    pub fn from_sdp_string(s: &str) -> Result<Candidate, ParseCandidateError> {
288        let cand_str = std::ffi::CString::new(s).unwrap();
289        unsafe {
290            let mut ret = Candidate {
291                ffi: crate::ffi::RiceCandidate::zeroed(),
292            };
293            let res =
294                crate::ffi::rice_candidate_init_from_sdp_string(&mut ret.ffi, cand_str.as_ptr());
295            ParseCandidateError::from_c(res)?;
296            Ok(ret)
297        }
298    }
299}
300
301/// An ICE candidate.
302///
303/// Backed inside a heap allocation.
304#[derive(Eq)]
305pub struct CandidateOwned {
306    ffi: *mut crate::ffi::RiceCandidate,
307}
308
309unsafe impl Send for CandidateOwned {}
310unsafe impl Sync for CandidateOwned {}
311
312impl core::fmt::Debug for CandidateOwned {
313    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314        unsafe {
315            let mut dbg = f.debug_struct("Candidate");
316            let dbg2 = &mut dbg;
317            dbg2.field("ffi", &self.ffi);
318            if !self.ffi.is_null() {
319                dbg2.field("value", &*self.ffi);
320            }
321            dbg.finish()
322        }
323    }
324}
325
326impl PartialEq<CandidateOwned> for CandidateOwned {
327    fn eq(&self, other: &CandidateOwned) -> bool {
328        unsafe { crate::ffi::rice_candidate_eq(self.ffi, other.ffi) }
329    }
330}
331
332impl PartialEq<Candidate> for CandidateOwned {
333    fn eq(&self, other: &Candidate) -> bool {
334        unsafe { crate::ffi::rice_candidate_eq(self.ffi, &other.ffi) }
335    }
336}
337
338impl Clone for CandidateOwned {
339    fn clone(&self) -> Self {
340        unsafe {
341            Self {
342                ffi: crate::ffi::rice_candidate_copy(self.ffi),
343            }
344        }
345    }
346}
347
348impl Drop for CandidateOwned {
349    fn drop(&mut self) {
350        unsafe { crate::ffi::rice_candidate_free(self.ffi) }
351    }
352}
353
354impl CandidateApi for CandidateOwned {}
355impl sealed::CandidateAsC for CandidateOwned {
356    fn as_c(&self) -> *const crate::ffi::RiceCandidate {
357        self.ffi
358    }
359}
360
361impl CandidateOwned {
362    pub(crate) fn as_c(&self) -> *const crate::ffi::RiceCandidate {
363        self.ffi
364    }
365
366    /// Serialize this candidate to a string for use in SDP
367    ///
368    /// # Examples
369    ///
370    /// ```
371    /// # use rice_c::candidate::*;
372    /// # use rice_c::Address;
373    /// let addr: Address = "127.0.0.1:2345".parse().unwrap();
374    /// let candidate = Candidate::builder(
375    ///     1,
376    ///     CandidateType::Host,
377    ///     TransportType::Udp,
378    ///     "foundation",
379    ///     addr,
380    /// )
381    /// .priority(1234)
382    /// .build();
383    /// assert_eq!(candidate.to_sdp_string(), "a=candidate:foundation 1 UDP 1234 127.0.0.1 2345 typ host")
384    /// ```
385    pub fn to_sdp_string(&self) -> String {
386        unsafe {
387            let res = crate::ffi::rice_candidate_to_sdp_string(self.ffi);
388            let s = CStr::from_ptr(res);
389            let ret = s.to_str().unwrap().to_owned();
390            crate::ffi::rice_string_free(res);
391            ret
392        }
393    }
394}
395
396/// A builder for a [`Candidate`]
397#[derive(Debug)]
398pub struct CandidateBuilder {
399    ffi: crate::ffi::RiceCandidate,
400}
401
402impl CandidateBuilder {
403    /// Consume this builder a construct a new [`Candidate`].
404    pub fn build(self) -> Candidate {
405        Candidate { ffi: self.ffi }
406    }
407
408    /// Specify the priority of the to be built candidate
409    pub fn priority(mut self, priority: u32) -> Self {
410        unsafe {
411            crate::ffi::rice_candidate_set_priority(&mut self.ffi, priority);
412            self
413        }
414    }
415
416    /// Specify the base address of the to be built candidate
417    pub fn base_address(mut self, base: crate::Address) -> Self {
418        unsafe {
419            crate::ffi::rice_candidate_set_base_address(&mut self.ffi, base.into_c_full());
420            self
421        }
422    }
423
424    /// Specify the related address of the to be built candidate
425    pub fn related_address(mut self, related: crate::Address) -> Self {
426        unsafe {
427            crate::ffi::rice_candidate_set_related_address(&mut self.ffi, related.into_c_full());
428            self
429        }
430    }
431
432    /// Specify the type of TCP connection of the to be built candidate
433    ///
434    /// - This will panic at build() time if the transport type is not [`TransportType::Tcp`].
435    /// - This will panic at build() time if this function is not called but the
436    ///   transport type is [`TransportType::Tcp`]
437    pub fn tcp_type(mut self, typ: TcpType) -> Self {
438        unsafe {
439            if self.ffi.transport_type != TransportType::Tcp.into() && typ != TcpType::None {
440                panic!("Attempt made to set the TcpType of a non-TCP candidate");
441            }
442            crate::ffi::rice_candidate_set_tcp_type(&mut self.ffi, typ.into());
443            self
444        }
445    }
446
447    // TODO: extensions
448}
449
450/// The type of the candidate
451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
452#[repr(u32)]
453pub enum CandidateType {
454    /// The candidate is a local network interface
455    Host = crate::ffi::RICE_CANDIDATE_TYPE_HOST,
456    /// The candidate was discovered from incoming data
457    PeerReflexive = crate::ffi::RICE_CANDIDATE_TYPE_PEER_REFLEXIVE,
458    /// The candidate was discovered by asking an external server (STUN/TURN)
459    ServerReflexive = crate::ffi::RICE_CANDIDATE_TYPE_SERVER_REFLEXIVE,
460    /// The candidate will relay all data through an external server (TURN).
461    Relayed = crate::ffi::RICE_CANDIDATE_TYPE_RELAYED,
462}
463
464impl From<crate::ffi::RiceCandidateType> for CandidateType {
465    fn from(value: crate::ffi::RiceCandidateType) -> Self {
466        match value {
467            crate::ffi::RICE_CANDIDATE_TYPE_HOST => Self::Host,
468            crate::ffi::RICE_CANDIDATE_TYPE_PEER_REFLEXIVE => Self::PeerReflexive,
469            crate::ffi::RICE_CANDIDATE_TYPE_SERVER_REFLEXIVE => Self::ServerReflexive,
470            crate::ffi::RICE_CANDIDATE_TYPE_RELAYED => Self::Relayed,
471            val => panic!("Unknown candidate type {val:x?}"),
472        }
473    }
474}
475
476impl From<CandidateType> for crate::ffi::RiceCandidateType {
477    fn from(value: CandidateType) -> Self {
478        match value {
479            CandidateType::Host => crate::ffi::RICE_CANDIDATE_TYPE_HOST,
480            CandidateType::PeerReflexive => crate::ffi::RICE_CANDIDATE_TYPE_PEER_REFLEXIVE,
481            CandidateType::ServerReflexive => crate::ffi::RICE_CANDIDATE_TYPE_SERVER_REFLEXIVE,
482            CandidateType::Relayed => crate::ffi::RICE_CANDIDATE_TYPE_RELAYED,
483        }
484    }
485}
486
487/// The type of TCP candidate
488#[derive(Debug, Copy, Clone, PartialEq, Eq)]
489#[repr(u32)]
490pub enum TcpType {
491    /// Not a TCP candidate.
492    None = crate::ffi::RICE_TCP_TYPE_NONE,
493    /// The candidate address will connect to a remote address.
494    Active = crate::ffi::RICE_TCP_TYPE_ACTIVE,
495    /// The candidate will listen for incominng TCP connections.
496    Passive = crate::ffi::RICE_TCP_TYPE_PASSIVE,
497    /// Simultaneous open.  The candidate will both listen for incoming connections, and connect to
498    /// remote addresses.
499    So = crate::ffi::RICE_TCP_TYPE_SO,
500}
501
502impl From<crate::ffi::RiceTcpType> for TcpType {
503    fn from(value: crate::ffi::RiceTcpType) -> Self {
504        match value {
505            crate::ffi::RICE_TCP_TYPE_NONE => Self::None,
506            crate::ffi::RICE_TCP_TYPE_ACTIVE => Self::Active,
507            crate::ffi::RICE_TCP_TYPE_PASSIVE => Self::Passive,
508            crate::ffi::RICE_TCP_TYPE_SO => Self::So,
509            val => panic!("Unknown RiceTcpType valyue {val:x?}"),
510        }
511    }
512}
513
514impl From<TcpType> for crate::ffi::RiceTcpType {
515    fn from(value: TcpType) -> Self {
516        match value {
517            TcpType::None => crate::ffi::RICE_TCP_TYPE_NONE,
518            TcpType::Active => crate::ffi::RICE_TCP_TYPE_ACTIVE,
519            TcpType::Passive => crate::ffi::RICE_TCP_TYPE_PASSIVE,
520            TcpType::So => crate::ffi::RICE_TCP_TYPE_SO,
521        }
522    }
523}
524
525/// The transport type.
526#[derive(Debug, Copy, Clone, PartialEq, Eq)]
527pub enum TransportType {
528    /// UDP transport.
529    Udp,
530    /// TCP transport.
531    Tcp,
532}
533
534impl core::fmt::Display for TransportType {
535    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536        write!(f, "{self:?}")
537    }
538}
539
540impl From<crate::ffi::RiceTransportType> for TransportType {
541    fn from(value: crate::ffi::RiceTransportType) -> Self {
542        match value {
543            crate::ffi::RICE_TRANSPORT_TYPE_UDP => Self::Udp,
544            crate::ffi::RICE_TRANSPORT_TYPE_TCP => Self::Tcp,
545            _ => panic!("Unknown RiceTransportType value"),
546        }
547    }
548}
549
550impl From<TransportType> for crate::ffi::RiceTransportType {
551    fn from(value: TransportType) -> Self {
552        match value {
553            TransportType::Udp => crate::ffi::RICE_TRANSPORT_TYPE_UDP,
554            TransportType::Tcp => crate::ffi::RICE_TRANSPORT_TYPE_TCP,
555        }
556    }
557}
558
559impl core::str::FromStr for TransportType {
560    type Err = ParseTransportTypeError;
561
562    fn from_str(s: &str) -> Result<Self, Self::Err> {
563        if s.eq_ignore_ascii_case("udp") {
564            Ok(Self::Udp)
565        } else if s.eq_ignore_ascii_case("tcp") {
566            Ok(Self::Tcp)
567        } else {
568            Err(ParseTransportTypeError::UnknownTransport)
569        }
570    }
571}
572
573/// Errors when parsing a [`TransportType`]
574#[derive(Debug, thiserror::Error)]
575pub enum ParseTransportTypeError {
576    /// An unknown transport value was provided
577    #[error("Unknown transport value was provided")]
578    UnknownTransport,
579}
580
581/// Paired local and remote candidate
582#[derive(Debug, Clone, PartialEq, Eq)]
583pub struct CandidatePair {
584    /// The local [`Candidate`]
585    pub local: CandidateOwned,
586    /// The remote [`Candidate`]
587    pub remote: CandidateOwned,
588}
589
590impl CandidatePair {
591    /// Create a new [`CandidatePair`]
592    pub fn new(local: CandidateOwned, remote: CandidateOwned) -> Self {
593        Self { local, remote }
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600
601    fn base_address() -> crate::Address {
602        "127.0.0.1:1000".parse().unwrap()
603    }
604
605    fn address() -> crate::Address {
606        "127.0.0.2:2000".parse().unwrap()
607    }
608
609    fn related_address() -> crate::Address {
610        "127.0.0.3:3000".parse().unwrap()
611    }
612
613    #[test]
614    fn candidate_build() {
615        let _log = crate::tests::test_init_log();
616
617        let base = base_address();
618        let addr = address();
619        let related = related_address();
620        let cand = Candidate::builder(
621            1,
622            CandidateType::PeerReflexive,
623            TransportType::Tcp,
624            "foundation",
625            addr.clone(),
626        )
627        .base_address(base.clone())
628        .related_address(related.clone())
629        .tcp_type(TcpType::Active)
630        .build();
631        assert_eq!(cand.component_id(), 1);
632        assert_eq!(cand.candidate_type(), CandidateType::PeerReflexive);
633        assert_eq!(cand.transport(), TransportType::Tcp);
634        assert_eq!(cand.foundation(), "foundation");
635        assert_eq!(cand.address(), addr);
636        assert_eq!(cand.base_address(), base);
637        assert_eq!(cand.related_address(), Some(related));
638        assert_eq!(cand.tcp_type(), TcpType::Active);
639
640        let cand_clone = cand.clone();
641        assert_eq!(cand, cand_clone);
642    }
643}