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