openthread/ot/
dnssd.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::ot::WrongSize;
6use crate::prelude_internal::*;
7
8/// Functional equivalent of [`otsys::otDnsTxtEntry`](crate::otsys::otDnsTxtEntry).
9#[derive(Default, Clone)]
10pub struct DnsTxtEntry<'a>(pub otDnsTxtEntry, PhantomData<&'a str>);
11
12impl_ot_castable!(lifetime DnsTxtEntry<'_>, otDnsTxtEntry, Default::default());
13
14impl<'a> DnsTxtEntry<'a> {
15    /// Tries to create a new `DnsTxtEntry` instance. Fails if value is too large.
16    pub fn try_new(
17        key: Option<&'a CStr>,
18        value: Option<&'a [u8]>,
19    ) -> Result<DnsTxtEntry<'a>, WrongSize> {
20        let mut ret = DnsTxtEntry::default();
21
22        ret.0.mKey = key.map(CStr::as_ptr).unwrap_or(std::ptr::null());
23
24        if let Some(value) = value {
25            ret.0.mValueLength = u16::try_from(value.len()).map_err(|_| WrongSize)?;
26            ret.0.mValue = value.as_ptr();
27        }
28
29        Ok(ret)
30    }
31
32    /// Accessor for the `mKey` field from [`otsys::otDnsTxtEntry`](crate::otsys::otDnsTxtEntry).
33    pub fn key_field(&self) -> Option<&'a CStr> {
34        if self.0.mKey.is_null() {
35            None
36        } else {
37            let cstr = unsafe { CStr::from_ptr(self.0.mKey) };
38            Some(cstr)
39        }
40    }
41
42    /// Accessor for the `mValue` field from [`otsys::otDnsTxtEntry`](crate::otsys::otDnsTxtEntry).
43    pub fn value_field(&self) -> Option<&'a [u8]> {
44        if self.0.mValue.is_null() {
45            None
46        } else {
47            let value =
48                unsafe { std::slice::from_raw_parts(self.0.mValue, self.0.mValueLength as usize) };
49            Some(value)
50        }
51    }
52
53    /// The key for this TXT entry.
54    ///
55    /// Will extract the key from the value field if `key_field()` returns `None`.
56    pub fn key(&self) -> Option<&'a str> {
57        if let Some(cstr) = self.key_field() {
58            match cstr.to_str() {
59                Ok(x) => Some(x),
60                Err(x) => {
61                    // We don't panic here because that would create a DoS
62                    // vulnerability. Instead we log the error and return None.
63                    warn!("Bad DNS TXT key {:?}: {:?}", cstr, x);
64                    None
65                }
66            }
67        } else if let Some(value) = self.value_field() {
68            let key_bytes = value.splitn(2, |x| *x == b'=').next().unwrap();
69            match std::str::from_utf8(key_bytes) {
70                Ok(x) => Some(x),
71                Err(x) => {
72                    // We don't panic here because that would create a DoS
73                    // vulnerability. Instead we log the error and return None.
74                    warn!("Bad DNS TXT key {:?}: {:?}", key_bytes, x);
75                    None
76                }
77            }
78        } else {
79            None
80        }
81    }
82
83    /// The value for this TXT entry.
84    ///
85    /// Will only return the value part if the key is included in `value_field()`.
86    pub fn value(&self) -> Option<&'a [u8]> {
87        #[allow(clippy::manual_filter)]
88        if let Some(value) = self.value_field() {
89            if self.0.mKey.is_null() {
90                let mut iter = value.splitn(2, |x| *x == b'=');
91                let a = iter.next();
92                let b = iter.next();
93                match (a, b) {
94                    (Some(_), Some(value)) => Some(value),
95                    _ => None,
96                }
97            } else {
98                Some(value)
99            }
100        } else {
101            None
102        }
103    }
104
105    /// Renders out this key-value pair to a `Vec<u8>`.
106    pub fn to_vec(&self) -> Vec<u8> {
107        let mut pair = vec![];
108        match (self.key(), self.value()) {
109            (Some(key), Some(value)) => {
110                pair.extend_from_slice(key.as_bytes());
111                pair.push(b'=');
112                pair.extend_from_slice(value);
113            }
114            (Some(key), None) => {
115                pair.extend_from_slice(key.as_bytes());
116            }
117            _ => {}
118        }
119        pair.truncate(u8::MAX as usize);
120        pair
121    }
122}
123
124impl std::fmt::Debug for DnsTxtEntry<'_> {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        f.debug_struct("DnsTxtEntry")
127            .field("key_field", &self.key_field())
128            .field("value_field", &self.value_field().map(ascii_dump))
129            .finish()
130    }
131}
132
133impl std::fmt::Display for DnsTxtEntry<'_> {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        write!(f, "{}", ascii_dump(&self.to_vec()))
136    }
137}
138
139/// Functional equivalent of [`otsys::otDnsTxtEntryIterator`](crate::otsys::otDnsTxtEntryIterator).
140#[derive(Default, Debug, Clone)]
141pub struct DnsTxtEntryIterator<'a>(pub otDnsTxtEntryIterator, PhantomData<&'a str>);
142
143impl_ot_castable!(lifetime DnsTxtEntryIterator<'_>, otDnsTxtEntryIterator, Default::default());
144
145impl<'a> DnsTxtEntryIterator<'a> {
146    /// Tries to create a new `DnsTxtEntry` instance. Functional equivalent
147    /// of [`otsys::otDnsInitTxtEntryIterator`](crate::otsys::otDnsInitTxtEntryIterator).
148    ///
149    /// Fails if `txt_data` is too large.
150    pub fn try_new(txt_data: &'a [u8]) -> Result<DnsTxtEntryIterator<'a>, WrongSize> {
151        if let Ok(len) = u16::try_from(txt_data.len()) {
152            let mut ret = DnsTxtEntryIterator::default();
153            // SAFETY: All values being passed into this function have been validated.
154            unsafe { otDnsInitTxtEntryIterator(ret.as_ot_mut_ptr(), txt_data.as_ptr(), len) }
155            Ok(ret)
156        } else {
157            Err(WrongSize)
158        }
159    }
160}
161
162impl<'a> Iterator for DnsTxtEntryIterator<'a> {
163    type Item = Result<DnsTxtEntry<'a>>;
164
165    fn next(&mut self) -> Option<Self::Item> {
166        let mut ret = DnsTxtEntry::default();
167
168        match Error::from(unsafe {
169            otDnsGetNextTxtEntry(self.as_ot_mut_ptr(), ret.as_ot_mut_ptr())
170        }) {
171            Error::None => Some(Ok(ret)),
172            Error::NotFound => None,
173            err => Some(Err(err)),
174        }
175    }
176}
177
178/// Converts a iterator of strings into a single string separated with
179/// [`DNSSD_TXT_SEPARATOR_STR`].
180pub fn dns_txt_flatten<I: IntoIterator<Item = (String, Option<Vec<u8>>)>>(txt: I) -> Vec<u8> {
181    let mut ret = vec![];
182    for (key, value) in txt {
183        let mut pair = vec![];
184
185        if let Some(value) = value {
186            pair.extend_from_slice(key.as_bytes());
187            pair.push(b'=');
188            pair.extend_from_slice(&value);
189        } else {
190            pair.extend_from_slice(key.as_bytes());
191        }
192        pair.truncate(u8::MAX as usize);
193        ret.push(u8::try_from(pair.len()).unwrap());
194        ret.extend(pair);
195    }
196    ret
197}
198
199/// Represents the type of a DNS query
200///
201/// Functional equivalent of [`otsys::otDnsQueryType`](crate::otsys::otDnsQueryType).
202#[derive(Debug, Copy, Clone, Eq, Ord, PartialOrd, PartialEq, num_derive::FromPrimitive)]
203#[allow(missing_docs)]
204pub enum DnssdQueryType {
205    /// Functional equivalent of [`otsys::otDnsQueryType_OT_DNSSD_QUERY_TYPE_NONE`](crate::otsys::otDnsQueryType_OT_DNSSD_QUERY_TYPE_NONE).
206    None = OT_DNSSD_QUERY_TYPE_NONE as isize,
207
208    /// Functional equivalent of [`otsys::otDnsQueryType_OT_DNSSD_QUERY_TYPE_BROWSE`](crate::otsys::otDnsQueryType_OT_DNSSD_QUERY_TYPE_BROWSE).
209    Browse = OT_DNSSD_QUERY_TYPE_BROWSE as isize,
210
211    /// Functional equivalent of [`otsys::otDnsQueryType_OT_DNSSD_QUERY_TYPE_RESOLVE`](crate::otsys::otDnsQueryType_OT_DNSSD_QUERY_TYPE_RESOLVE).
212    ResolveService = OT_DNSSD_QUERY_TYPE_RESOLVE as isize,
213
214    /// Functional equivalent of [`otsys::otDnsQueryType_OT_DNSSD_QUERY_TYPE_RESOLVE_HOST`](crate::otsys::otDnsQueryType_OT_DNSSD_QUERY_TYPE_RESOLVE_HOST).
215    ResolveHost = OT_DNSSD_QUERY_TYPE_RESOLVE_HOST as isize,
216}
217
218/// Functional equivalent of `otDnssdQuery`
219#[repr(transparent)]
220pub struct DnssdQuery(otDnssdQuery, PhantomData<*mut otDnssdQuery>);
221
222impl_ot_castable!(opaque DnssdQuery, otDnssdQuery);
223
224impl std::fmt::Debug for DnssdQuery {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        let (query_type, name) = self.query_type_and_name();
227        f.debug_struct("otDnsQuery").field("type", &query_type).field("name", &name).finish()
228    }
229}
230
231impl DnssdQuery {
232    /// Functional equivalent of `otDnssdGetQueryTypeAndName`.
233    pub fn query_type_and_name(&self) -> (DnssdQueryType, CString) {
234        let mut bytes: [::std::os::raw::c_char; OT_DNS_MAX_NAME_SIZE as usize] =
235            [0; OT_DNS_MAX_NAME_SIZE as usize];
236        let query_type = unsafe {
237            otDnssdGetQueryTypeAndName(
238                self.as_ot_ptr(),
239                bytes.as_mut_ptr() as *mut [::std::os::raw::c_char; OT_DNS_MAX_NAME_SIZE as usize],
240            )
241        };
242        let name = unsafe { CStr::from_ptr(bytes.as_ptr()) };
243
244        (DnssdQueryType::from_u32(query_type).unwrap(), name.to_owned())
245    }
246}
247
248/// Methods from the [OpenThread DNS-SD Server Module][1].
249///
250/// [1]: https://openthread.io/reference/group/api-dnssd-server
251pub trait Dnssd {
252    /// Functional equivalent to `otDnssdGetNextQuery`.
253    ///
254    /// Arguments:
255    ///
256    /// * `prev`: Reference to the previous `DnssdQuery`, or `None` to get the first DnssdQuery.
257    fn dnssd_get_next_query(&self, prev: Option<&DnssdQuery>) -> Option<&DnssdQuery>;
258
259    /// Functional equivalent to `otDnssdQueryHandleDiscoveredHost`.
260    fn dnssd_query_handle_discovered_host(
261        &self,
262        hostname: &CStr,
263        addresses: &[Ip6Address],
264        ttl: u32,
265    );
266
267    /// Functional equivalent to `otDnssdQueryHandleDiscoveredServiceInstance`.
268    ///
269    /// Arguments:
270    ///
271    /// * `service_full_name`: Full service name (e.g.`_ipps._tcp.default.service.arpa.`)
272    ///   Must end with `.`.
273    /// * `addresses`: Reference to array of addresses for service.
274    /// * `full_name`: Full instance name (e.g.`OpenThread._ipps._tcp.default.service.arpa.`).
275    ///   Must end with `.`.
276    /// * `host_name`: Host name (e.g. `ot-host.default.service.arpa.`). Must end with `.`.
277    /// * `port`: Service port.
278    /// * `priority`: Service priority.
279    /// * `ttl`: Service TTL (in seconds).
280    /// * `txt_data`: Array of bytes representing the TXT record.
281    /// * `weight`: Service weight.
282    #[allow(clippy::too_many_arguments)]
283    fn dnssd_query_handle_discovered_service_instance(
284        &self,
285        service_full_name: &CStr,
286        addresses: &[Ip6Address],
287        full_name: &CStr,
288        host_name: &CStr,
289        port: u16,
290        priority: u16,
291        ttl: u32,
292        txt_data: &[u8],
293        weight: u16,
294    );
295
296    /// Functional equivalent of
297    /// [`otsys::otDnssdQuerySetCallbacks`](crate::otsys::otDnssdQuerySetCallbacks).
298    ///
299    /// The callback closure takes two arguments:
300    ///
301    /// * bool: True if subscribing, false if unsubscribing.
302    /// * &CStr: Full name.
303    fn dnssd_query_set_callbacks<'a, F>(&'a self, f: Option<F>)
304    where
305        F: FnMut(bool, &CStr) + 'a;
306
307    /// Functional equivalent of
308    /// [`otsys::otDnssdGetCounters`](crate::otsys::otDnssdGetCounters).
309    fn dnssd_get_counters(&self) -> &DnssdCounters;
310
311    /// Functional equivalent of
312    /// [`otsys::otDnssdUpstreamQueryIsEnabled`](crate::otsys::otDnssdUpstreamQueryIsEnabled)
313    fn dnssd_upstream_query_is_enabled(&self) -> bool;
314
315    /// Functional equivalent of
316    /// [`otsys::otDnssdUpstreamQuerySetEnabled`](crate::otsys::otDnssdUpstreamQuerySetEnabled)
317    fn dnssd_upstream_query_set_enabled(&self, enabled: bool);
318}
319
320impl<T: Dnssd + Boxable> Dnssd for ot::Box<T> {
321    fn dnssd_get_next_query(&self, prev: Option<&DnssdQuery>) -> Option<&DnssdQuery> {
322        self.as_ref().dnssd_get_next_query(prev)
323    }
324
325    fn dnssd_query_handle_discovered_host(
326        &self,
327        hostname: &CStr,
328        addresses: &[Ip6Address],
329        ttl: u32,
330    ) {
331        self.as_ref().dnssd_query_handle_discovered_host(hostname, addresses, ttl);
332    }
333
334    fn dnssd_query_handle_discovered_service_instance(
335        &self,
336        service_full_name: &CStr,
337        addresses: &[Ip6Address],
338        full_name: &CStr,
339        host_name: &CStr,
340        port: u16,
341        priority: u16,
342        ttl: u32,
343        txt_data: &[u8],
344        weight: u16,
345    ) {
346        self.as_ref().dnssd_query_handle_discovered_service_instance(
347            service_full_name,
348            addresses,
349            full_name,
350            host_name,
351            port,
352            priority,
353            ttl,
354            txt_data,
355            weight,
356        );
357    }
358
359    fn dnssd_query_set_callbacks<'a, F>(&'a self, f: Option<F>)
360    where
361        F: FnMut(bool, &CStr) + 'a,
362    {
363        self.as_ref().dnssd_query_set_callbacks(f)
364    }
365
366    fn dnssd_get_counters(&self) -> &DnssdCounters {
367        self.as_ref().dnssd_get_counters()
368    }
369
370    fn dnssd_upstream_query_is_enabled(&self) -> bool {
371        self.as_ref().dnssd_upstream_query_is_enabled()
372    }
373
374    fn dnssd_upstream_query_set_enabled(&self, enabled: bool) {
375        self.as_ref().dnssd_upstream_query_set_enabled(enabled)
376    }
377}
378
379impl Dnssd for Instance {
380    fn dnssd_get_next_query(&self, prev: Option<&DnssdQuery>) -> Option<&DnssdQuery> {
381        use std::ptr::null_mut;
382        unsafe {
383            DnssdQuery::ref_from_ot_ptr(otDnssdGetNextQuery(
384                self.as_ot_ptr(),
385                prev.map(DnssdQuery::as_ot_ptr).unwrap_or(null_mut()),
386            ) as *mut otDnssdQuery)
387        }
388    }
389
390    fn dnssd_query_handle_discovered_host(
391        &self,
392        hostname: &CStr,
393        addresses: &[Ip6Address],
394        ttl: u32,
395    ) {
396        unsafe {
397            otDnssdQueryHandleDiscoveredHost(
398                self.as_ot_ptr(),
399                hostname.as_ptr(),
400                &mut otDnssdHostInfo {
401                    mAddressNum: addresses.len().try_into().unwrap(),
402                    mAddresses: addresses.as_ptr() as *const otIp6Address,
403                    mTtl: ttl,
404                } as *mut otDnssdHostInfo,
405            )
406        }
407    }
408
409    fn dnssd_query_handle_discovered_service_instance(
410        &self,
411        service_full_name: &CStr,
412        addresses: &[Ip6Address],
413        full_name: &CStr,
414        host_name: &CStr,
415        port: u16,
416        priority: u16,
417        ttl: u32,
418        txt_data: &[u8],
419        weight: u16,
420    ) {
421        unsafe {
422            otDnssdQueryHandleDiscoveredServiceInstance(
423                self.as_ot_ptr(),
424                service_full_name.as_ptr(),
425                &mut otDnssdServiceInstanceInfo {
426                    mFullName: full_name.as_ptr(),
427                    mHostName: host_name.as_ptr(),
428                    mAddressNum: addresses.len().try_into().unwrap(),
429                    mAddresses: addresses.as_ptr() as *const otIp6Address,
430                    mPort: port,
431                    mPriority: priority,
432                    mWeight: weight,
433                    mTxtLength: txt_data.len().try_into().unwrap(),
434                    mTxtData: txt_data.as_ptr(),
435                    mTtl: ttl,
436                } as *mut otDnssdServiceInstanceInfo,
437            )
438        }
439    }
440
441    fn dnssd_query_set_callbacks<'a, F>(&'a self, f: Option<F>)
442    where
443        F: FnMut(bool, &CStr) + 'a,
444    {
445        unsafe extern "C" fn _ot_dnssd_query_subscribe_callback<'a, F: FnMut(bool, &CStr) + 'a>(
446            context: *mut ::std::os::raw::c_void,
447            full_name_ptr: *const c_char,
448        ) {
449            trace!("_ot_dnssd_query_subscribe_callback");
450
451            let full_name_cstr = CStr::from_ptr(full_name_ptr);
452
453            // Reconstitute a reference to our closure.
454            let sender = &mut *(context as *mut F);
455
456            sender(true, full_name_cstr)
457        }
458
459        unsafe extern "C" fn _ot_dnssd_query_unsubscribe_callback<
460            'a,
461            F: FnMut(bool, &CStr) + 'a,
462        >(
463            context: *mut ::std::os::raw::c_void,
464            full_name_ptr: *const c_char,
465        ) {
466            trace!("_ot_dnssd_query_unsubscribe_callback");
467
468            let full_name_cstr = CStr::from_ptr(full_name_ptr);
469
470            // Reconstitute a reference to our closure.
471            let sender = &mut *(context as *mut F);
472
473            sender(false, full_name_cstr)
474        }
475
476        let (fn_ptr, fn_box, cb_sub, cb_unsub): (
477            _,
478            _,
479            otDnssdQuerySubscribeCallback,
480            otDnssdQueryUnsubscribeCallback,
481        ) = if let Some(f) = f {
482            let mut x = Box::new(f);
483
484            (
485                x.as_mut() as *mut F as *mut ::std::os::raw::c_void,
486                Some(x as Box<dyn FnMut(bool, &CStr) + 'a>),
487                Some(_ot_dnssd_query_subscribe_callback::<F>),
488                Some(_ot_dnssd_query_unsubscribe_callback::<F>),
489            )
490        } else {
491            (std::ptr::null_mut() as *mut ::std::os::raw::c_void, None, None, None)
492        };
493
494        unsafe {
495            otDnssdQuerySetCallbacks(self.as_ot_ptr(), cb_sub, cb_unsub, fn_ptr);
496
497            // Make sure our object eventually gets cleaned up.
498            // Here we must also transmute our closure to have a 'static lifetime.
499            // We need to do this because the borrow checker cannot infer the
500            // proper lifetime for the singleton instance backing, but
501            // this is guaranteed by the API.
502            self.borrow_backing().dnssd_query_sub_unsub_fn.set(std::mem::transmute::<
503                Option<Box<dyn FnMut(bool, &CStr) + 'a>>,
504                Option<Box<dyn FnMut(bool, &CStr) + 'static>>,
505            >(fn_box));
506        }
507    }
508
509    fn dnssd_get_counters(&self) -> &DnssdCounters {
510        unsafe { DnssdCounters::ref_from_ot_ptr(otDnssdGetCounters(self.as_ot_ptr())).unwrap() }
511    }
512
513    fn dnssd_upstream_query_is_enabled(&self) -> bool {
514        unsafe { otDnssdUpstreamQueryIsEnabled(self.as_ot_ptr()) }
515    }
516
517    fn dnssd_upstream_query_set_enabled(&self, enabled: bool) {
518        unsafe { otDnssdUpstreamQuerySetEnabled(self.as_ot_ptr(), enabled) }
519    }
520}
521
522/// Iterator type for DNS-SD Queries
523#[derive(Debug, Clone)]
524pub struct DnssdQueryIterator<'a, T: Dnssd> {
525    prev: Option<&'a DnssdQuery>,
526    ot_instance: &'a T,
527}
528
529impl<'a, T: Dnssd> Iterator for DnssdQueryIterator<'a, T> {
530    type Item = &'a DnssdQuery;
531
532    fn next(&mut self) -> Option<Self::Item> {
533        self.prev = self.ot_instance.dnssd_get_next_query(self.prev);
534        self.prev
535    }
536}
537
538/// Extension trait for the trait [`Dnssd`].
539pub trait DnssdExt: Dnssd {
540    /// Iterator for easily iterating over all of the DNS-SD queries.
541    fn dnssd_queries(&self) -> DnssdQueryIterator<'_, Self>
542    where
543        Self: Sized,
544    {
545        DnssdQueryIterator { prev: None, ot_instance: self }
546    }
547}
548
549impl<T: Dnssd> DnssdExt for T {}
550
551#[cfg(test)]
552mod test {
553    use super::*;
554
555    #[test]
556    fn test_dnstxtentry_new() {
557        let cstring = CString::new("CRA").unwrap();
558        assert_eq!(
559            DnsTxtEntry::try_new(Some(&cstring), Some(b"300")).unwrap().to_string(),
560            "CRA=300".to_string()
561        );
562        assert_eq!(
563            DnsTxtEntry::try_new(None, Some(b"CRA=300")).unwrap().to_string(),
564            "CRA=300".to_string()
565        );
566        assert_eq!(
567            DnsTxtEntry::try_new(Some(&cstring), None).unwrap().to_string(),
568            "CRA".to_string()
569        );
570    }
571
572    #[test]
573    fn test_split_txt() {
574        assert_eq!(
575            DnsTxtEntryIterator::try_new(b"")
576                .unwrap()
577                .map(|x| x.unwrap().to_string())
578                .collect::<Vec<_>>(),
579            Vec::<String>::new()
580        );
581        assert_eq!(
582            DnsTxtEntryIterator::try_new(b"\x13xa=a7bfc4981f4e4d22\x13xp=029c6f4dbae059cb")
583                .unwrap()
584                .map(|x| x.unwrap().to_string())
585                .collect::<Vec<_>>(),
586            vec!["xa=a7bfc4981f4e4d22".to_string(), "xp=029c6f4dbae059cb".to_string()]
587        );
588        assert_eq!(
589            DnsTxtEntryIterator::try_new(b"\x13xa=a7bfc4981f4e4d22\x11xp=029c6f4dbae059")
590                .unwrap()
591                .map(|x| x.unwrap().to_string())
592                .collect::<Vec<_>>(),
593            vec!["xa=a7bfc4981f4e4d22".to_string(), "xp=029c6f4dbae059".to_string()]
594        );
595        assert_eq!(
596            DnsTxtEntryIterator::try_new(b"\x13xa=a7bfc4981f4e4d22\x04flag\x11xp=029c6f4dbae059")
597                .unwrap()
598                .map(|x| x.unwrap().to_string())
599                .collect::<Vec<_>>(),
600            vec![
601                "xa=a7bfc4981f4e4d22".to_string(),
602                "flag".to_string(),
603                "xp=029c6f4dbae059".to_string()
604            ]
605        );
606    }
607
608    #[test]
609    fn test_flatten_txt() {
610        assert_eq!(ot::dns_txt_flatten(None), vec![]);
611        assert_eq!(ot::dns_txt_flatten(vec![]), vec![]);
612        assert_eq!(
613            ot::dns_txt_flatten(vec![("xa".to_string(), Some(b"a7bfc4981f4e4d22".to_vec()))]),
614            b"\x13xa=a7bfc4981f4e4d22".to_vec()
615        );
616        assert_eq!(
617            ot::dns_txt_flatten(vec![
618                ("xa".to_string(), Some(b"a7bfc4981f4e4d22".to_vec())),
619                ("xp".to_string(), Some(b"029c6f4dbae059cb".to_vec()))
620            ]),
621            b"\x13xa=a7bfc4981f4e4d22\x13xp=029c6f4dbae059cb".to_vec()
622        );
623        assert_eq!(
624            ot::dns_txt_flatten(vec![
625                ("xa".to_string(), Some(b"a7bfc4981f4e4d22".to_vec())),
626                ("flag".to_string(), None),
627                ("xp".to_string(), Some(b"029c6f4dbae059cb".to_vec()))
628            ]),
629            b"\x13xa=a7bfc4981f4e4d22\x04flag\x13xp=029c6f4dbae059cb".to_vec()
630        );
631    }
632}