Skip to main content

hickory_proto/rr/rdata/
txt.rs

1// Copyright 2015-2023 Benjamin Fry <benjaminfry@me.com>
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! text records for storing arbitrary data
9use alloc::{
10    boxed::Box,
11    string::{String, ToString},
12    vec::Vec,
13};
14use core::fmt;
15
16#[cfg(feature = "serde")]
17use serde::{Deserialize, Serialize};
18
19use crate::{
20    error::ProtoResult,
21    rr::{RData, RecordData, RecordDataDecodable, RecordType},
22    serialize::{binary::*, txt::ParseError},
23};
24
25/// [RFC 1035, DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION, November 1987](https://tools.ietf.org/html/rfc1035)
26///
27/// ```text
28/// 3.3.14. TXT RDATA format
29///
30///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
31///     /                   TXT-DATA                    /
32///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
33///
34///
35/// TXT RRs are used to hold descriptive text.  The semantics of the text
36/// depends on the domain where it is found.
37/// ```
38#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
39#[derive(Debug, PartialEq, Eq, Hash, Clone)]
40#[non_exhaustive]
41pub struct TXT {
42    /// ```text
43    /// TXT-DATA        One or more <character-string>s.
44    /// ```
45    pub txt_data: Box<[Box<[u8]>]>,
46}
47
48impl TXT {
49    /// Creates a new TXT record data.
50    ///
51    /// # Arguments
52    ///
53    /// * `txt_data` - the set of strings which make up the txt_data.
54    ///
55    /// # Return value
56    ///
57    /// The new TXT record data.
58    pub fn new(txt_data: Vec<String>) -> Self {
59        Self {
60            txt_data: txt_data
61                .into_iter()
62                .map(|s| s.into_bytes().into_boxed_slice())
63                .collect::<Vec<_>>()
64                .into_boxed_slice(),
65        }
66    }
67
68    /// Creates a new TXT record data from bytes.
69    /// Allows creating binary record data.
70    ///
71    /// # Arguments
72    ///
73    /// * `txt_data` - the set of bytes which make up the txt_data.
74    ///
75    /// # Return value
76    ///
77    /// The new TXT record data.
78    pub fn from_bytes(txt_data: Vec<&[u8]>) -> Self {
79        Self {
80            txt_data: txt_data
81                .into_iter()
82                .map(|s| s.to_vec().into_boxed_slice())
83                .collect::<Vec<_>>()
84                .into_boxed_slice(),
85        }
86    }
87
88    /// Parse the RData from a set of Tokens
89    #[allow(clippy::unnecessary_wraps)]
90    pub(crate) fn from_tokens<'i, I: Iterator<Item = &'i str>>(
91        tokens: I,
92    ) -> Result<Self, ParseError> {
93        let txt_data = tokens.map(ToString::to_string).collect::<Vec<_>>();
94        Ok(Self::new(txt_data))
95    }
96}
97
98impl BinEncodable for TXT {
99    fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> {
100        for s in &self.txt_data {
101            encoder.emit_character_data(s)?;
102        }
103
104        Ok(())
105    }
106}
107
108impl RecordDataDecodable<'_> for TXT {
109    fn read_data(
110        decoder: &mut BinDecoder<'_>,
111        rdata_length: Restrict<u16>,
112    ) -> Result<Self, DecodeError> {
113        let data_len = decoder.len();
114        let mut strings = Vec::with_capacity(1);
115
116        // no unsafe usage of rdata length after this point
117        let rdata_length =
118            rdata_length.map(|u| u as usize).unverified(/*used as a higher bound, safely*/);
119        while data_len - decoder.len() < rdata_length {
120            let string = decoder.read_character_data()?.unverified(/*any data should be validate in TXT usage*/);
121            strings.push(string.to_vec().into_boxed_slice());
122        }
123        Ok(Self {
124            txt_data: strings.into_boxed_slice(),
125        })
126    }
127}
128
129impl RecordData for TXT {
130    fn try_borrow(data: &RData) -> Option<&Self> {
131        match data {
132            RData::TXT(data) => Some(data),
133            _ => None,
134        }
135    }
136
137    fn record_type(&self) -> RecordType {
138        RecordType::TXT
139    }
140
141    fn into_rdata(self) -> RData {
142        RData::TXT(self)
143    }
144}
145
146impl fmt::Display for TXT {
147    /// Format a [TXT] with lossy conversion of invalid utf8.
148    ///
149    /// ## Case of invalid utf8
150    ///
151    /// Invalid utf8 will be converted to:
152    /// `U+FFFD REPLACEMENT CHARACTER`, which looks like this: �
153    ///
154    /// Same behaviour as `alloc::string::String::from_utf8_lossy`.
155    /// ```rust
156    /// # use hickory_proto::rr::rdata::TXT;
157    /// let first_bytes = b"Invalid utf8 <\xF0\x90\x80>.";
158    /// let second_bytes = b" Valid utf8 <\xF0\x9F\xA4\xA3>";
159    /// let rdata: Vec<&[u8]> = vec![first_bytes, second_bytes];
160    /// let txt = TXT::from_bytes(rdata);
161    ///
162    /// let tested = format!("{}", txt);
163    /// assert_eq!(
164    ///     tested.as_bytes(),
165    ///     b"Invalid utf8 <\xEF\xBF\xBD>. Valid utf8 <\xF0\x9F\xA4\xA3>",
166    ///     "Utf8 lossy conversion error! Mismatch between input and expected"
167    /// );
168    /// ```
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
170        for txt in self.txt_data.iter() {
171            f.write_str(&String::from_utf8_lossy(txt))?;
172        }
173
174        Ok(())
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    #![allow(clippy::dbg_macro, clippy::print_stdout)]
181
182    use alloc::string::ToString;
183    #[cfg(feature = "std")]
184    use std::println;
185
186    use super::*;
187
188    #[test]
189    fn test() {
190        let rdata = TXT::new(vec!["Test me some".to_string(), "more please".to_string()]);
191
192        let mut bytes = Vec::new();
193        let mut encoder: BinEncoder<'_> = BinEncoder::new(&mut bytes);
194        assert!(rdata.emit(&mut encoder).is_ok());
195        let bytes = encoder.into_bytes();
196
197        #[cfg(feature = "std")]
198        println!("bytes: {bytes:?}");
199
200        let mut decoder: BinDecoder<'_> = BinDecoder::new(bytes);
201        let restrict = Restrict::new(bytes.len() as u16);
202        let read_rdata = TXT::read_data(&mut decoder, restrict).expect("Decoding error");
203        assert_eq!(rdata, read_rdata);
204    }
205
206    #[test]
207    fn publish_binary_txt_record() {
208        let bin_data = vec![0, 1, 2, 3, 4, 5, 6, 7, 8];
209        let rdata = TXT::from_bytes(vec![b"Test me some", &bin_data]);
210
211        let mut bytes = Vec::new();
212        let mut encoder: BinEncoder<'_> = BinEncoder::new(&mut bytes);
213        assert!(rdata.emit(&mut encoder).is_ok());
214        let bytes = encoder.into_bytes();
215
216        #[cfg(feature = "std")]
217        println!("bytes: {bytes:?}");
218
219        let mut decoder: BinDecoder<'_> = BinDecoder::new(bytes);
220        let restrict = Restrict::new(bytes.len() as u16);
221        let read_rdata = TXT::read_data(&mut decoder, restrict).expect("Decoding error");
222        assert_eq!(rdata, read_rdata);
223    }
224}