domain/base/opt/
exterr.rs

1//! EDNS option for extended DNS errors.
2//!
3//! The option in this module – [`ExtendedError<Octs>`] – allows a server to
4//! provide more detailed information why a query has failed.
5//!
6//! The option is defined in [RFC 8914](https://tools.ietf.org/html/rfc8914).
7
8use super::super::iana::exterr::{
9    ExtendedErrorCode, EDE_PRIVATE_RANGE_BEGIN,
10};
11use super::super::iana::OptionCode;
12use super::super::message_builder::OptBuilder;
13use super::super::wire::ParseError;
14use super::super::wire::{Compose, Composer};
15use super::{
16    BuildDataError, ComposeOptData, LongOptData, Opt, OptData, ParseOptData,
17};
18use core::convert::Infallible;
19use core::{fmt, hash, str};
20use octseq::builder::OctetsBuilder;
21use octseq::octets::{Octets, OctetsFrom};
22use octseq::parse::Parser;
23use octseq::str::Str;
24use octseq::{EmptyBuilder, FromBuilder};
25
26//------------ ExtendedError -------------------------------------------------
27
28/// Option data for an extended DNS error.
29///
30/// The Extended DNS Error option allows a server to include more detailed
31/// information in a response to a failed query why it did. It contains a
32/// standardized [`ExtendedErrorCode`] for machines and an optional UTF-8
33/// error text for humans.
34#[derive(Clone)]
35#[cfg_attr(
36    feature = "serde",
37    derive(serde::Serialize),
38    serde(bound(serialize = "Octs: AsRef<[u8]>"))
39)]
40pub struct ExtendedError<Octs> {
41    /// The extended error code.
42    code: ExtendedErrorCode,
43
44    /// Optional human-readable error information.
45    ///
46    /// See `text` for the interpretation of the result.
47    #[cfg_attr(feature = "serde", serde(serialize_with = "lossy_text"))]
48    text: Option<Result<Str<Octs>, LossyOctets<Octs>>>,
49}
50
51#[cfg(feature = "serde")]
52fn lossy_text<S, Octs: AsRef<[u8]>>(
53    text: &Option<Result<Str<Octs>, LossyOctets<Octs>>>,
54    serializer: S,
55) -> Result<S::Ok, S::Error>
56where
57    S: serde::Serializer,
58{
59    match text {
60        Some(Ok(text)) => serializer.serialize_str(text),
61        Some(Err(text)) => serializer.collect_str(text),
62        None => serializer.serialize_none(),
63    }
64}
65
66impl ExtendedError<()> {
67    /// The option code for this option.
68    pub(super) const CODE: OptionCode = OptionCode::EXTENDED_ERROR;
69}
70
71impl<Octs> ExtendedError<Octs> {
72    /// Creates a new value from a code and optional text.
73    ///
74    /// Returns an error if `text` is present but is too long to fit into
75    /// an option.
76    pub fn new(
77        code: ExtendedErrorCode,
78        text: Option<Str<Octs>>,
79    ) -> Result<Self, LongOptData>
80    where
81        Octs: AsRef<[u8]>,
82    {
83        if let Some(ref text) = text {
84            LongOptData::check_len(
85                text.len() + usize::from(ExtendedErrorCode::COMPOSE_LEN),
86            )?
87        }
88        Ok(unsafe { Self::new_unchecked(code, text.map(Ok)) })
89    }
90
91    pub fn new_with_str(
92        code: ExtendedErrorCode,
93        text: &str,
94    ) -> Result<Self, LongOptData>
95    where
96        Octs: AsRef<[u8]> + FromBuilder,
97        <Octs as FromBuilder>::Builder: EmptyBuilder,
98        <<Octs as FromBuilder>::Builder as OctetsBuilder>::AppendError:
99            Into<Infallible>,
100    {
101        Self::new(code, Some(Str::copy_from_str(text)))
102    }
103
104    /// Creates a new value without checking for the option length.
105    ///
106    /// # Safety
107    ///
108    /// The caller must ensure that the length of the wire format of the
109    /// value does not exceed 65,535 octets.
110    pub unsafe fn new_unchecked(
111        code: ExtendedErrorCode,
112        text: Option<Result<Str<Octs>, Octs>>,
113    ) -> Self {
114        Self {
115            code,
116            text: text.map(|res| res.map_err(LossyOctets)),
117        }
118    }
119
120    /// Returns the error code.
121    pub fn code(&self) -> ExtendedErrorCode {
122        self.code
123    }
124
125    /// Returns the text.
126    ///
127    /// If there is no text, returns `None`. If there is text and it is
128    /// correctly encoded UTF-8, returns `Some(Ok(_))`. If there is text but
129    /// it is not UTF-8, returns `Some(Err(_))`.
130    pub fn text(&self) -> Option<Result<&Str<Octs>, &Octs>> {
131        self.text
132            .as_ref()
133            .map(|res| res.as_ref().map_err(|err| err.as_ref()))
134    }
135
136    /// Returns the text as an octets slice.
137    pub fn text_slice(&self) -> Option<&[u8]>
138    where
139        Octs: AsRef<[u8]>,
140    {
141        match self.text {
142            Some(Ok(ref text)) => Some(text.as_slice()),
143            Some(Err(ref text)) => Some(text.as_slice()),
144            None => None,
145        }
146    }
147
148    /// Sets the text field.
149    pub fn set_text(&mut self, text: Str<Octs>) {
150        self.text = Some(Ok(text));
151    }
152
153    /// Returns true if the code is in the private range.
154    pub fn is_private(&self) -> bool {
155        self.code().to_int() >= EDE_PRIVATE_RANGE_BEGIN
156    }
157
158    pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
159        parser: &mut Parser<'a, Src>,
160    ) -> Result<Self, ParseError>
161    where
162        Octs: AsRef<[u8]>,
163    {
164        let code = ExtendedErrorCode::parse(parser)?;
165        let text = match parser.remaining() {
166            0 => None,
167            n => Some(
168                Str::from_utf8(parser.parse_octets(n)?)
169                    .map_err(|err| err.into_octets()),
170            ),
171        };
172        Ok(unsafe { Self::new_unchecked(code, text) })
173    }
174}
175
176//--- From and TryFrom
177
178impl<Octs> From<ExtendedErrorCode> for ExtendedError<Octs> {
179    fn from(code: ExtendedErrorCode) -> Self {
180        Self { code, text: None }
181    }
182}
183
184impl<Octs> From<u16> for ExtendedError<Octs> {
185    fn from(code: u16) -> Self {
186        Self {
187            code: ExtendedErrorCode::from_int(code),
188            text: None,
189        }
190    }
191}
192
193impl<Octs> TryFrom<(ExtendedErrorCode, Str<Octs>)> for ExtendedError<Octs>
194where
195    Octs: AsRef<[u8]>,
196{
197    type Error = LongOptData;
198
199    fn try_from(
200        (code, text): (ExtendedErrorCode, Str<Octs>),
201    ) -> Result<Self, Self::Error> {
202        Self::new(code, Some(text))
203    }
204}
205
206//--- OctetsFrom
207
208impl<Octs, SrcOcts> OctetsFrom<ExtendedError<SrcOcts>> for ExtendedError<Octs>
209where
210    Octs: OctetsFrom<SrcOcts>,
211{
212    type Error = Octs::Error;
213
214    fn try_octets_from(
215        source: ExtendedError<SrcOcts>,
216    ) -> Result<Self, Self::Error> {
217        let text = match source.text {
218            Some(Ok(text)) => Some(Ok(Str::try_octets_from(text)?)),
219            Some(Err(octs)) => {
220                Some(Err(LossyOctets(Octs::try_octets_from(octs.0)?)))
221            }
222            None => None,
223        };
224        Ok(Self {
225            code: source.code,
226            text,
227        })
228    }
229}
230//--- OptData, ParseOptData, and ComposeOptData
231
232impl<Octs> OptData for ExtendedError<Octs> {
233    fn code(&self) -> OptionCode {
234        OptionCode::EXTENDED_ERROR
235    }
236}
237
238impl<'a, Octs> ParseOptData<'a, Octs> for ExtendedError<Octs::Range<'a>>
239where
240    Octs: Octets + ?Sized,
241{
242    fn parse_option(
243        code: OptionCode,
244        parser: &mut Parser<'a, Octs>,
245    ) -> Result<Option<Self>, ParseError> {
246        if code == OptionCode::EXTENDED_ERROR {
247            Self::parse(parser).map(Some)
248        } else {
249            Ok(None)
250        }
251    }
252}
253
254impl<Octs: AsRef<[u8]>> ComposeOptData for ExtendedError<Octs> {
255    fn compose_len(&self) -> u16 {
256        if let Some(text) = self.text_slice() {
257            text.len()
258                .checked_add(ExtendedErrorCode::COMPOSE_LEN.into())
259                .expect("long option data")
260                .try_into()
261                .expect("long option data")
262        } else {
263            ExtendedErrorCode::COMPOSE_LEN
264        }
265    }
266
267    fn compose_option<Target: OctetsBuilder + ?Sized>(
268        &self,
269        target: &mut Target,
270    ) -> Result<(), Target::AppendError> {
271        self.code.to_int().compose(target)?;
272        if let Some(text) = self.text_slice() {
273            target.append_slice(text)?;
274        }
275        Ok(())
276    }
277}
278
279//--- Display and Debug
280
281impl<Octs: AsRef<[u8]>> fmt::Display for ExtendedError<Octs> {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        self.code.fmt(f)?;
284        match self.text {
285            Some(Ok(ref text)) => write!(f, " {}", text)?,
286            Some(Err(ref text)) => write!(f, " {}", text)?,
287            None => {}
288        }
289        Ok(())
290    }
291}
292
293impl<Octs: AsRef<[u8]>> fmt::Debug for ExtendedError<Octs> {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        f.debug_struct("ExtendedError")
296            .field("code", &self.code)
297            .field(
298                "text",
299                &self
300                    .text
301                    .as_ref()
302                    .map(|text| text.as_ref().map_err(|err| err.0.as_ref())),
303            )
304            .finish()
305    }
306}
307
308//--- PartialEq and Eq
309
310impl<Octs, Other> PartialEq<ExtendedError<Other>> for ExtendedError<Octs>
311where
312    Octs: AsRef<[u8]>,
313    Other: AsRef<[u8]>,
314{
315    fn eq(&self, other: &ExtendedError<Other>) -> bool {
316        self.code.eq(&other.code) && self.text_slice().eq(&other.text_slice())
317    }
318}
319
320impl<Octs: AsRef<[u8]>> Eq for ExtendedError<Octs> {}
321
322//--- Hash
323
324impl<Octs: AsRef<[u8]>> hash::Hash for ExtendedError<Octs> {
325    fn hash<H: hash::Hasher>(&self, state: &mut H) {
326        self.code.hash(state);
327        self.text_slice().hash(state);
328    }
329}
330
331//--- Extended Opt and OptBuilder
332
333impl<Octs: Octets> Opt<Octs> {
334    /// Returns the first extended DNS error option if present.
335    ///
336    /// The extended DNS error option carries additional error information in
337    /// a failed answer.
338    pub fn extended_error(&self) -> Option<ExtendedError<Octs::Range<'_>>> {
339        self.first()
340    }
341}
342
343impl<Target: Composer> OptBuilder<'_, Target> {
344    /// Appends an extended DNS error option.
345    ///
346    /// The extended DNS error option carries additional error information in
347    /// a failed answer. The `code` argument is a standardized error code
348    /// while the optional `text` carries human-readable information.
349    ///
350    /// The method fails if `text` is too long to be part of an option or if
351    /// target runs out of space.
352    pub fn extended_error<Octs: AsRef<[u8]>>(
353        &mut self,
354        code: ExtendedErrorCode,
355        text: Option<&Str<Octs>>,
356    ) -> Result<(), BuildDataError> {
357        self.push(&ExtendedError::new(
358            code,
359            text.map(|text| unsafe {
360                Str::from_utf8_unchecked(text.as_slice())
361            }),
362        )?)?;
363        Ok(())
364    }
365}
366
367//------------ LossyOctets ---------------------------------------------------
368
369/// An octets wrapper that displays its content as a lossy UTF-8 sequence.
370#[derive(Clone)]
371struct LossyOctets<Octs>(Octs);
372
373impl<Octs: AsRef<[u8]>> LossyOctets<Octs> {
374    fn as_slice(&self) -> &[u8] {
375        self.0.as_ref()
376    }
377}
378
379impl<Octs> From<Octs> for LossyOctets<Octs> {
380    fn from(src: Octs) -> Self {
381        Self(src)
382    }
383}
384
385impl<Octs> AsRef<Octs> for LossyOctets<Octs> {
386    fn as_ref(&self) -> &Octs {
387        &self.0
388    }
389}
390
391impl<Octs: AsRef<[u8]>> fmt::Display for LossyOctets<Octs> {
392    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393        let mut source = self.0.as_ref();
394        loop {
395            match str::from_utf8(source) {
396                Ok(s) => {
397                    f.write_str(s)?;
398                    break;
399                }
400                Err(err) => {
401                    let (good, mut tail) = source.split_at(err.valid_up_to());
402                    f.write_str(
403                        // Safety: valid UTF8 for this part was confirmed
404                        // above.
405                        unsafe { str::from_utf8_unchecked(good) },
406                    )?;
407                    f.write_str("\u{fffd}")?;
408                    match err.error_len() {
409                        None => break,
410                        Some(len) => {
411                            tail = &tail[len..];
412                        }
413                    }
414                    source = tail;
415                }
416            }
417        }
418        Ok(())
419    }
420}
421
422//============ Tests =========================================================
423
424#[cfg(all(test, feature = "std", feature = "bytes"))]
425mod tests {
426    use super::super::test::test_option_compose_parse;
427    use super::*;
428
429    #[test]
430    #[allow(clippy::redundant_closure)] // lifetimes ...
431    fn nsid_compose_parse() {
432        let ede = ExtendedError::new(
433            ExtendedErrorCode::STALE_ANSWER,
434            Some(Str::from_string("some text".into())),
435        )
436        .unwrap();
437        test_option_compose_parse(&ede, |parser| {
438            ExtendedError::parse(parser)
439        });
440    }
441
442    #[test]
443    fn private() {
444        let ede: ExtendedError<&[u8]> =
445            ExtendedErrorCode::DNSSEC_BOGUS.into();
446        assert!(!ede.is_private());
447
448        let ede: ExtendedError<&[u8]> = EDE_PRIVATE_RANGE_BEGIN.into();
449        assert!(ede.is_private());
450    }
451
452    #[test]
453    fn display_lossy_octets() {
454        use std::string::ToString;
455
456        assert_eq!(LossyOctets(b"foo").to_string(), "foo");
457        assert_eq!(LossyOctets(b"foo\xe7").to_string(), "foo\u{fffd}");
458        assert_eq!(LossyOctets(b"foo\xe7foo").to_string(), "foo\u{fffd}foo");
459    }
460}