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