netconf/message/rpc/
error.rs

1use std::{
2    convert::Infallible,
3    fmt::{self, Debug},
4    str::{from_utf8, FromStr},
5    sync::Arc,
6};
7
8use quick_xml::{
9    events::{BytesStart, Event},
10    name::ResolveResult,
11    NsReader,
12};
13
14use crate::{message::ReadError, session::SessionId};
15
16use super::{xmlns, ReadXml};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Errors {
20    inner: Vec<Error>,
21}
22
23impl Errors {
24    pub(super) const fn new() -> Self {
25        Self { inner: Vec::new() }
26    }
27
28    #[must_use]
29    pub fn is_empty(&self) -> bool {
30        self.inner.is_empty()
31    }
32
33    #[must_use]
34    pub fn len(&self) -> usize {
35        self.inner.len()
36    }
37
38    pub(super) fn push(&mut self, err: Error) {
39        self.inner.push(err);
40    }
41
42    pub fn iter(&self) -> impl Iterator<Item = &Error> {
43        self.inner.iter()
44    }
45}
46
47impl fmt::Display for Errors {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        self.iter().try_for_each(|err| writeln!(f, "{err}"))
50    }
51}
52
53impl std::error::Error for Errors {}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct Error {
57    error_type: Type,
58    error_tag: Tag,
59    severity: Severity,
60    app_tag: Option<AppTag>,
61    path: Option<Path>,
62    message: Option<Message>,
63    info: Info,
64}
65
66impl ReadXml for Error {
67    #[tracing::instrument(skip_all, fields(tag = ?start.local_name()), level = "debug")]
68    fn read_xml(reader: &mut NsReader<&[u8]>, start: &BytesStart<'_>) -> Result<Self, ReadError> {
69        let end = start.to_end();
70        let mut error_type = None;
71        let mut error_tag = None;
72        let mut severity = None;
73        let mut app_tag = None;
74        let mut path = None;
75        let mut message = None;
76        let mut info = None;
77        loop {
78            match reader.read_resolved_event()? {
79                (ResolveResult::Bound(ns), Event::Start(tag))
80                    if ns == xmlns::BASE
81                        && tag.local_name().as_ref() == b"error-type"
82                        && error_type.is_none() =>
83                {
84                    tracing::debug!(?tag);
85                    error_type = Some(reader.read_text(tag.to_end().name())?.trim().parse()?);
86                }
87                (ResolveResult::Bound(ns), Event::Start(tag))
88                    if ns == xmlns::BASE
89                        && tag.local_name().as_ref() == b"error-tag"
90                        && error_tag.is_none() =>
91                {
92                    tracing::debug!(?tag);
93                    error_tag = Some(reader.read_text(tag.to_end().name())?.trim().parse()?);
94                }
95                (ResolveResult::Bound(ns), Event::Start(tag))
96                    if ns == xmlns::BASE
97                        && tag.local_name().as_ref() == b"error-severity"
98                        && severity.is_none() =>
99                {
100                    tracing::debug!(?tag);
101                    severity = Some(reader.read_text(tag.to_end().name())?.trim().parse()?);
102                }
103                (ResolveResult::Bound(ns), Event::Start(tag))
104                    if ns == xmlns::BASE
105                        && tag.local_name().as_ref() == b"error-app-tag"
106                        && app_tag.is_none() =>
107                {
108                    tracing::debug!(?tag);
109                    app_tag = Some(
110                        reader
111                            .read_text(tag.to_end().name())?
112                            .trim()
113                            .parse()
114                            .unwrap_or_else(|_| unreachable!()),
115                    );
116                }
117                (ResolveResult::Bound(ns), Event::Start(tag))
118                    if ns == xmlns::BASE
119                        && tag.local_name().as_ref() == b"error-path"
120                        && path.is_none() =>
121                {
122                    tracing::debug!(?tag);
123                    path = Some(
124                        reader
125                            .read_text(tag.to_end().name())?
126                            .trim()
127                            .parse()
128                            .unwrap_or_else(|_| unreachable!()),
129                    );
130                }
131                (ResolveResult::Bound(ns), Event::Start(tag))
132                    if ns == xmlns::BASE
133                        && tag.local_name().as_ref() == b"error-message"
134                        && message.is_none() =>
135                {
136                    tracing::debug!(?tag);
137                    message = Some(
138                        reader
139                            .read_text(tag.to_end().name())?
140                            .trim()
141                            .parse()
142                            .unwrap_or_else(|_| unreachable!()),
143                    );
144                }
145                (ResolveResult::Bound(ns), Event::Start(tag))
146                    if ns == xmlns::BASE
147                        && tag.local_name().as_ref() == b"error-info"
148                        && info.is_none() =>
149                {
150                    tracing::debug!(?tag);
151                    info = Some(Info::read_xml(reader, &tag)?);
152                }
153                (_, Event::Comment(_)) => continue,
154                (_, Event::End(tag)) if tag == end => break,
155                (ns, event) => {
156                    tracing::error!(?event, ?ns, "unexpected xml event");
157                    return Err(ReadError::UnexpectedXmlEvent(event.into_owned()));
158                }
159            }
160        }
161        Ok(Self {
162            error_type: error_type
163                .ok_or_else(|| ReadError::missing_element("rpc-error", "error-type"))?,
164            error_tag: error_tag
165                .ok_or_else(|| ReadError::missing_element("rpc-error", "error-tag"))?,
166            severity: severity
167                .ok_or_else(|| ReadError::missing_element("rpc-error", "error-severity"))?,
168            app_tag,
169            path,
170            message,
171            info: info.unwrap_or_else(Info::new),
172        })
173    }
174}
175
176impl fmt::Display for Error {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(
179            f,
180            "{} {}: {}",
181            self.error_type, self.severity, self.error_tag
182        )
183    }
184}
185
186impl std::error::Error for Error {}
187
188#[derive(Debug, Copy, Clone, PartialEq, Eq)]
189pub enum Type {
190    Transport,
191    Rpc,
192    Protocol,
193    Application,
194}
195
196impl FromStr for Type {
197    type Err = ReadError;
198
199    fn from_str(s: &str) -> Result<Self, Self::Err> {
200        match s {
201            "transport" => Ok(Self::Transport),
202            "rpc" => Ok(Self::Rpc),
203            "protocol" => Ok(Self::Protocol),
204            "application" => Ok(Self::Application),
205            _ => Err(Self::Err::UnknownErrorType(s.to_string())),
206        }
207    }
208}
209
210impl fmt::Display for Type {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        let ty = match self {
213            Self::Transport => "transport",
214            Self::Rpc => "rpc",
215            Self::Protocol => "protocol",
216            Self::Application => "application",
217        };
218        f.write_str(ty)
219    }
220}
221
222#[derive(Debug, Copy, Clone, PartialEq, Eq)]
223pub enum Tag {
224    InUse,
225    InvalidValue,
226    TooBig,
227    MissingAttribute,
228    BadAttribute,
229    UnknownAttribute,
230    MissingElement,
231    BadElement,
232    UnknownElement,
233    UnknownNamespace,
234    AccessDenied,
235    LockDenied,
236    ResourceDenied,
237    RollbackFailed,
238    DataExists,
239    DataMissing,
240    OperationNotSupported,
241    OperationFailed,
242    MalformedMessage,
243
244    // Deprecated:
245    PartialOperation,
246}
247
248impl FromStr for Tag {
249    type Err = ReadError;
250
251    fn from_str(s: &str) -> Result<Self, Self::Err> {
252        match s {
253            "in-use" => Ok(Self::InUse),
254            "invalid-value" => Ok(Self::InvalidValue),
255            "too-big" => Ok(Self::TooBig),
256            "missing-attribute" => Ok(Self::MissingAttribute),
257            "bad-attribute" => Ok(Self::BadAttribute),
258            "unknown-attribute" => Ok(Self::UnknownAttribute),
259            "missing-element" => Ok(Self::MissingElement),
260            "bad-element" => Ok(Self::BadElement),
261            "unknown-element" => Ok(Self::UnknownElement),
262            "unknown-namespace" => Ok(Self::UnknownNamespace),
263            "access-denied" => Ok(Self::AccessDenied),
264            "lock-denied" => Ok(Self::LockDenied),
265            "resource-denied" => Ok(Self::ResourceDenied),
266            "rollback-failed" => Ok(Self::RollbackFailed),
267            "data-exists" => Ok(Self::DataExists),
268            "data-missing" => Ok(Self::DataMissing),
269            "operation-not-supported" => Ok(Self::OperationNotSupported),
270            "operation-failed" => Ok(Self::OperationFailed),
271            "malformed-message" => Ok(Self::MalformedMessage),
272            "partial-operation" => Ok(Self::PartialOperation),
273            _ => Err(Self::Err::UnknownErrorTag(s.to_string())),
274        }
275    }
276}
277
278impl fmt::Display for Tag {
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        let msg = match self {
281            Self::InUse => "The request requires a resource that already is in use",
282            Self::InvalidValue => "The request specifies an unacceptable value for one or more parameters",
283            Self::TooBig => "The request or response (that would be generated) is too large for the implementation to handle",
284            Self::MissingAttribute => "An expected attribute is missing",
285            Self::BadAttribute => "An attribute value is not correct; e.g., wrong type, out of range, pattern mismatch",
286            Self::UnknownAttribute => "An unexpected attribute is present",
287            Self::MissingElement => "An expected element is missing",
288            Self::BadElement => "An element value is not correct; e.g., wrong type, out of range, pattern mismatch",
289            Self::UnknownElement => "An unexpected element is present",
290            Self::UnknownNamespace => "An unexpected namespace is present",
291            Self::AccessDenied => "Access to the requested protocol operation or data model is denied because authorization failed",
292            Self::LockDenied => "Access to the requested lock is denied because the lock is currently held by another entity",
293            Self::ResourceDenied => "Request could not be completed because of insufficient resources",
294            Self::RollbackFailed => "Request to roll back some configuration change (via rollback-on-error or <discard-changes> operations) was not completed for some reason",
295            Self::DataExists => "Request could not be completed because the relevant data model content already exists",
296            Self::DataMissing => "Request could not be completed because the relevant data model content does not exist",
297            Self::OperationNotSupported => "Request could not be completed because the requested operation is not supported by this implementation",
298            Self::OperationFailed => "Request could not be completed because the requested operation failed for some reason not covered by any other error condition",
299            Self::MalformedMessage => "A message could not be handled because it failed to be parsed correctly",
300            Self::PartialOperation => "Some part of the requested operation failed or was not attempted for some reason",
301        };
302        f.write_str(msg)
303    }
304}
305
306#[derive(Debug, Copy, Clone, PartialEq, Eq)]
307pub enum Severity {
308    Warning,
309    Error,
310}
311
312impl FromStr for Severity {
313    type Err = ReadError;
314
315    fn from_str(s: &str) -> Result<Self, Self::Err> {
316        match s {
317            "error" => Ok(Self::Error),
318            "warning" => Ok(Self::Warning),
319            _ => Err(Self::Err::UnknownErrorSeverity(s.to_string())),
320        }
321    }
322}
323
324impl fmt::Display for Severity {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        let severity = match self {
327            Self::Error => "error",
328            Self::Warning => "warning",
329        };
330        f.write_str(severity)
331    }
332}
333
334#[derive(Debug, Clone, PartialEq, Eq)]
335pub struct AppTag {
336    inner: Arc<str>,
337}
338
339impl FromStr for AppTag {
340    type Err = Infallible;
341
342    fn from_str(s: &str) -> Result<Self, Self::Err> {
343        Ok(Self { inner: s.into() })
344    }
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
348pub struct Path {
349    inner: Arc<str>,
350}
351
352impl FromStr for Path {
353    type Err = Infallible;
354
355    fn from_str(s: &str) -> Result<Self, Self::Err> {
356        Ok(Self { inner: s.into() })
357    }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct Message {
362    // TODO
363    lang: (),
364    inner: Arc<str>,
365}
366
367impl FromStr for Message {
368    type Err = Infallible;
369
370    fn from_str(s: &str) -> Result<Self, Self::Err> {
371        Ok(Self {
372            lang: (),
373            inner: s.into(),
374        })
375    }
376}
377
378#[derive(Debug, Clone, PartialEq, Eq)]
379pub struct Info {
380    inner: Vec<InfoElement>,
381}
382
383impl Info {
384    const fn new() -> Self {
385        Self { inner: Vec::new() }
386    }
387}
388
389impl ReadXml for Info {
390    #[tracing::instrument(skip_all, fields(tag = ?start.local_name()), level = "debug")]
391    fn read_xml(reader: &mut NsReader<&[u8]>, start: &BytesStart<'_>) -> Result<Self, ReadError> {
392        let end = start.to_end();
393        let mut inner = Vec::new();
394        tracing::debug!("expecting error-info element");
395        loop {
396            match reader.read_resolved_event()? {
397                (ResolveResult::Bound(ns), Event::Start(tag)) if ns == xmlns::BASE => {
398                    match tag.local_name().as_ref() {
399                        b"bad-attribute" => inner.push(InfoElement::BadAttribute(
400                            reader.read_text(tag.to_end().name())?.as_ref().into(),
401                        )),
402                        b"bad-element" => inner.push(InfoElement::BadElement(
403                            reader.read_text(tag.to_end().name())?.as_ref().into(),
404                        )),
405                        b"bad-namespace" => inner.push(InfoElement::BadNamespace(
406                            reader.read_text(tag.to_end().name())?.as_ref().into(),
407                        )),
408                        b"session-id" => inner.push(InfoElement::SessionId(
409                            reader
410                                .read_text(tag.to_end().name())?
411                                .as_ref()
412                                .parse()
413                                .map_err(ReadError::SessionIdParse)
414                                .map(|session_id| SessionId::new(session_id).ok())?,
415                        )),
416                        b"ok-element" => inner.push(InfoElement::OkElement(
417                            reader.read_text(tag.to_end().name())?.as_ref().into(),
418                        )),
419                        b"err-element" => inner.push(InfoElement::ErrElement(
420                            reader.read_text(tag.to_end().name())?.as_ref().into(),
421                        )),
422                        b"noop-element" => inner.push(InfoElement::NoopElement(
423                            reader.read_text(tag.to_end().name())?.as_ref().into(),
424                        )),
425                        name => {
426                            return Err(ReadError::UnknownErrorInfo(from_utf8(name)?.to_string()))
427                        }
428                    }
429                }
430                (_, Event::Comment(_)) => continue,
431                (_, Event::End(tag)) if tag == end => break,
432                (ns, event) => {
433                    tracing::error!(?event, ?ns, "unexpected xml event");
434                    return Err(ReadError::UnexpectedXmlEvent(event.into_owned()));
435                }
436            }
437        }
438        Ok(Self { inner })
439    }
440}
441
442#[derive(Debug, Clone, PartialEq, Eq)]
443pub enum InfoElement {
444    BadAttribute(Arc<str>),
445    BadElement(Arc<str>),
446    BadNamespace(Arc<str>),
447    SessionId(Option<SessionId>),
448
449    // Deprecated error-info elements:
450    OkElement(Arc<str>),
451    ErrElement(Arc<str>),
452    NoopElement(Arc<str>),
453}
454
455#[cfg(test)]
456mod tests {
457    use std::io::Write;
458
459    use quick_xml::Writer;
460
461    use super::*;
462    use crate::{
463        capabilities::Requirements,
464        message::{
465            rpc::{operation, EmptyReply, MessageId, Operation, PartialReply, Reply},
466            ServerMsg, WriteError, WriteXml,
467        },
468    };
469
470    #[derive(Debug, PartialEq)]
471    struct Dummy;
472
473    impl Operation for Dummy {
474        const NAME: &'static str = "dummy";
475        const REQUIRED_CAPABILITIES: Requirements = Requirements::None;
476        type Builder<'a> = Builder;
477        type Reply = EmptyReply;
478    }
479
480    impl WriteXml for Dummy {
481        fn write_xml<W: Write>(&self, _: &mut Writer<W>) -> Result<(), WriteError> {
482            Ok(())
483        }
484    }
485
486    #[derive(Debug)]
487    struct Builder;
488
489    impl operation::Builder<'_, Dummy> for Builder {
490        fn new(_: &crate::session::Context) -> Self {
491            Self
492        }
493
494        fn finish(self) -> Result<Dummy, crate::Error> {
495            Ok(Dummy)
496        }
497    }
498
499    #[test]
500    fn deserialize_error_reply_rfc6241_s1_example1() {
501        // message-id has been added, as we do not support it's omission
502        let data = r#"
503            <rpc-reply message-id="101"
504              xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
505              <rpc-error>
506                <error-type>rpc</error-type>
507                <error-tag>missing-attribute</error-tag>
508                <error-severity>error</error-severity>
509                <error-info>
510                  <bad-attribute>message-id</bad-attribute>
511                  <bad-element>rpc</bad-element>
512                </error-info>
513              </rpc-error>
514            </rpc-reply>
515        "#;
516        let expect: Reply<Dummy> = Reply {
517            message_id: MessageId(101),
518            inner: EmptyReply::Errs(Errors {
519                inner: vec![Error {
520                    error_type: Type::Rpc,
521                    error_tag: Tag::MissingAttribute,
522                    severity: Severity::Error,
523                    app_tag: None,
524                    path: None,
525                    message: None,
526                    info: Info {
527                        inner: vec![
528                            InfoElement::BadAttribute("message-id".into()),
529                            InfoElement::BadElement("rpc".into()),
530                        ],
531                    },
532                }],
533            }),
534        };
535        assert_eq!(
536            expect,
537            PartialReply::from_xml(data)
538                .and_then(Reply::try_from)
539                .unwrap()
540        );
541    }
542    #[test]
543    fn deserialize_error_reply_rfc6241_s1_example2() {
544        let data = r#"
545            <rpc-reply message-id="101"
546              xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"
547              xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
548              <rpc-error>
549                <error-type>application</error-type>
550                <error-tag>invalid-value</error-tag>
551                <error-severity>error</error-severity>
552                <error-path xmlns:t="http://example.com/schema/1.2/config">
553                  /t:top/t:interface[t:name="Ethernet0/0"]/t:mtu
554                </error-path>
555                <error-message xml:lang="en">
556                  MTU value 25000 is not within range 256..9192
557                </error-message>
558              </rpc-error>
559              <rpc-error>
560                <error-type>application</error-type>
561                <error-tag>invalid-value</error-tag>
562                <error-severity>error</error-severity>
563                <error-path xmlns:t="http://example.com/schema/1.2/config">
564                  /t:top/t:interface[t:name="Ethernet1/0"]/t:address/t:name
565                </error-path>
566                <error-message xml:lang="en">
567                  Invalid IP address for interface Ethernet1/0
568                </error-message>
569              </rpc-error>
570            </rpc-reply>
571        "#;
572        let expect: Reply<Dummy> = Reply {
573            message_id: MessageId(101),
574            inner: EmptyReply::Errs(Errors {
575                inner: vec![
576                    Error {
577                        error_type: Type::Application,
578                        error_tag: Tag::InvalidValue,
579                        severity: Severity::Error,
580                        app_tag: None,
581                        path: Some(Path {
582                            inner: r#"/t:top/t:interface[t:name="Ethernet0/0"]/t:mtu"#.into(),
583                        }),
584                        message: Some(Message {
585                            lang: (),
586                            inner: "MTU value 25000 is not within range 256..9192".into(),
587                        }),
588                        info: Info::new(),
589                    },
590                    Error {
591                        error_type: Type::Application,
592                        error_tag: Tag::InvalidValue,
593                        severity: Severity::Error,
594                        app_tag: None,
595                        path: Some(Path {
596                            inner: r#"/t:top/t:interface[t:name="Ethernet1/0"]/t:address/t:name"#
597                                .into(),
598                        }),
599                        message: Some(Message {
600                            lang: (),
601                            inner: "Invalid IP address for interface Ethernet1/0".into(),
602                        }),
603                        info: Info::new(),
604                    },
605                ],
606            }),
607        };
608        assert_eq!(
609            expect,
610            PartialReply::from_xml(data)
611                .and_then(Reply::try_from)
612                .unwrap()
613        );
614    }
615}