netconf/message/rpc/
mod.rs

1use std::{fmt::Debug, io::Write, str::from_utf8};
2
3use quick_xml::{
4    events::{attributes::Attribute, BytesStart, Event},
5    name::{Namespace, ResolveResult},
6    NsReader, Writer,
7};
8
9use super::{xmlns, ClientMsg, ReadError, ReadXml, ServerMsg, WriteError, WriteXml, MARKER};
10
11pub mod error;
12pub use self::error::{Error, Errors};
13
14pub mod operation;
15pub(crate) use self::operation::Operation;
16
17#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
18pub struct MessageId(usize);
19
20impl MessageId {
21    pub(crate) fn increment(&mut self) -> Self {
22        self.0 += 1;
23        *self
24    }
25}
26
27impl TryFrom<Attribute<'_>> for MessageId {
28    type Error = ReadError;
29
30    fn try_from(value: Attribute<'_>) -> Result<Self, Self::Error> {
31        Ok(Self(
32            value
33                .unescape_value()?
34                .as_ref()
35                .parse()
36                .map_err(ReadError::MessageIdParse)?,
37        ))
38    }
39}
40
41#[derive(Debug, Clone)]
42pub(crate) struct Request<O: Operation> {
43    message_id: MessageId,
44    operation: O,
45}
46
47impl<O: Operation> Request<O> {
48    pub(crate) const fn new(message_id: MessageId, operation: O) -> Self {
49        Self {
50            message_id,
51            operation,
52        }
53    }
54}
55
56impl<O: Operation> WriteXml for Request<O> {
57    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
58        writer
59            .create_element("rpc")
60            .with_attribute(("message-id", self.message_id.0.to_string().as_ref()))
61            .write_inner_content(|writer| self.operation.write_xml(writer))
62            .map(|_| ())
63    }
64}
65
66impl<O: Operation> ClientMsg for Request<O> {}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub(crate) struct PartialReply {
70    message_id: MessageId,
71    buf: Box<str>,
72}
73
74impl PartialReply {
75    pub(crate) const fn message_id(&self) -> MessageId {
76        self.message_id
77    }
78}
79
80impl ServerMsg for PartialReply {
81    const TAG_NS: Namespace<'static> = xmlns::BASE;
82    const TAG_NAME: &'static str = "rpc-reply";
83
84    // TODO:
85    // This is a hack - we need to find a way to save the buffer without resorting to this trick
86    // of calling `read_xml` with a dummy start tag
87    #[tracing::instrument(skip(input))]
88    fn from_xml<S>(input: S) -> Result<Self, ReadError>
89    where
90        S: AsRef<str> + Debug,
91    {
92        let mut reader = NsReader::from_str(input.as_ref());
93        _ = reader.trim_text(true);
94        Self::read_xml(&mut reader, &BytesStart::new("dummy"))
95    }
96}
97
98impl ReadXml for PartialReply {
99    #[tracing::instrument(skip_all, level = "debug")]
100    fn read_xml(reader: &mut NsReader<&[u8]>, _: &BytesStart<'_>) -> Result<Self, ReadError> {
101        let buf = from_utf8(reader.get_ref())?.into();
102        tracing::debug!("expecting <{}>", Self::TAG_NAME);
103        let mut message_id = None;
104        loop {
105            match reader.read_resolved_event()? {
106                (ResolveResult::Bound(ns), Event::Start(tag))
107                    if ns == Self::TAG_NS
108                        && tag.local_name().as_ref() == Self::TAG_NAME.as_bytes() =>
109                {
110                    let end = tag.to_end();
111                    tracing::debug!("trying to parse message-id");
112                    message_id = tag
113                        .try_get_attribute("message-id")?
114                        .map(MessageId::try_from)
115                        .transpose()?;
116                    _ = reader.read_to_end(end.name());
117                }
118                (_, Event::Comment(_)) => continue,
119                (_, Event::Eof) => break,
120                (_, Event::Text(txt)) if &*txt == MARKER => break,
121                (ns, event) => {
122                    tracing::error!(?event, ?ns, "unexpected xml event");
123                    return Err(ReadError::UnexpectedXmlEvent(event.into_owned()));
124                }
125            }
126        }
127        Ok(Self {
128            message_id: message_id.ok_or_else(|| ReadError::NoMessageId)?,
129            buf,
130        })
131    }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub(crate) struct Reply<O: Operation> {
136    message_id: MessageId,
137    inner: O::Reply,
138}
139
140impl<O: Operation> Reply<O> {
141    pub(crate) fn into_result(self) -> Result<<O::Reply as IntoResult>::Ok, crate::Error> {
142        self.inner.into_result()
143    }
144}
145
146impl<O: Operation> ReadXml for Reply<O> {
147    #[tracing::instrument(skip_all, fields(tag = ?start.local_name()), level = "debug")]
148    fn read_xml(reader: &mut NsReader<&[u8]>, start: &BytesStart<'_>) -> Result<Self, ReadError> {
149        tracing::debug!("trying to parse message-id");
150        let message_id = start
151            .try_get_attribute("message-id")?
152            .ok_or_else(|| ReadError::NoMessageId)
153            .and_then(MessageId::try_from)?;
154        let inner = O::Reply::read_xml(reader, start)?;
155        Ok(Self { message_id, inner })
156    }
157}
158
159impl<O: Operation> ServerMsg for Reply<O> {
160    const TAG_NS: Namespace<'static> = xmlns::BASE;
161    const TAG_NAME: &'static str = "rpc-reply";
162}
163
164impl<O: Operation> TryFrom<PartialReply> for Reply<O> {
165    type Error = ReadError;
166
167    #[tracing::instrument(skip(value), level = "debug")]
168    fn try_from(value: PartialReply) -> Result<Self, Self::Error> {
169        let this = Self::from_xml(&value.buf)?;
170        if this.message_id != value.message_id {
171            return Err(Self::Error::message_id_mismatch(
172                value.message_id,
173                this.message_id,
174            ));
175        };
176        Ok(this)
177    }
178}
179
180pub trait IntoResult {
181    type Ok;
182    fn into_result(self) -> Result<Self::Ok, crate::Error>;
183}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub enum EmptyReply {
187    Ok,
188    Errs(Errors),
189}
190
191impl ReadXml for EmptyReply {
192    #[tracing::instrument(skip_all, fields(tag = ?start.local_name()), level = "debug")]
193    fn read_xml(reader: &mut NsReader<&[u8]>, start: &BytesStart<'_>) -> Result<Self, ReadError> {
194        let end = start.to_end();
195        let mut errors = Errors::new();
196        let mut this = None;
197        tracing::debug!("expecting <ok> or <rpc-error>");
198        loop {
199            match reader.read_resolved_event()? {
200                (ResolveResult::Bound(ns), Event::Empty(tag))
201                    if ns == xmlns::BASE
202                        && tag.local_name().as_ref() == b"ok"
203                        && this.is_none()
204                        && errors.is_empty() =>
205                {
206                    tracing::debug!(?tag);
207                    this = Some(Self::Ok);
208                }
209                (ResolveResult::Bound(ns), Event::Start(tag))
210                    if ns == xmlns::BASE
211                        && tag.local_name().as_ref() == b"rpc-error"
212                        && this.is_none() =>
213                {
214                    tracing::debug!(?tag);
215                    errors.push(Error::read_xml(reader, &tag)?);
216                }
217                (_, Event::Comment(_)) => continue,
218                (_, Event::End(tag)) if tag == end => break,
219                (ns, event) => {
220                    tracing::error!(?event, ?ns, "unexpected xml event");
221                    return Err(ReadError::UnexpectedXmlEvent(event.into_owned()));
222                }
223            }
224        }
225        this.or_else(|| (!errors.is_empty()).then(|| Self::Errs(errors)))
226            .ok_or_else(|| ReadError::missing_element("rpc-reply", "ok/rpc-error"))
227    }
228}
229
230impl IntoResult for EmptyReply {
231    type Ok = ();
232    fn into_result(self) -> Result<<Self as IntoResult>::Ok, crate::Error> {
233        match self {
234            Self::Ok => Ok(()),
235            Self::Errs(errs) => Err(errs.into()),
236        }
237    }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub enum DataReply<D> {
242    Data(D),
243    Errs(Errors),
244}
245
246impl<D: ReadXml> ReadXml for DataReply<D> {
247    #[tracing::instrument(skip_all, fields(tag = ?start.local_name()), level = "debug")]
248    fn read_xml(reader: &mut NsReader<&[u8]>, start: &BytesStart<'_>) -> Result<Self, ReadError> {
249        let end = start.to_end();
250        let mut errors = Errors::new();
251        let mut this = None;
252        tracing::debug!("expecting <data> or <rpc-error>");
253        loop {
254            match reader.read_resolved_event()? {
255                (ResolveResult::Bound(ns), Event::Start(tag))
256                    if ns == xmlns::BASE
257                        && tag.local_name().as_ref() == b"data"
258                        && this.is_none()
259                        && errors.is_empty() =>
260                {
261                    tracing::debug!(?tag);
262                    this = Some(Self::Data(D::read_xml(reader, &tag)?));
263                }
264                (ResolveResult::Bound(ns), Event::Start(tag))
265                    if ns == xmlns::BASE
266                        && tag.local_name().as_ref() == b"rpc-error"
267                        && this.is_none() =>
268                {
269                    tracing::debug!(?tag);
270                    errors.push(Error::read_xml(reader, &tag)?);
271                }
272                (_, Event::Comment(_)) => continue,
273                (_, Event::End(tag)) if tag == end => break,
274                (ns, event) => {
275                    tracing::error!(?event, ?ns, "unexpected xml event");
276                    return Err(ReadError::UnexpectedXmlEvent(event.into_owned()));
277                }
278            }
279        }
280        this.or_else(|| (!errors.is_empty()).then(|| Self::Errs(errors)))
281            .ok_or_else(|| ReadError::missing_element("rpc-reply", "data/rpc-error"))
282    }
283}
284
285impl<D> IntoResult for DataReply<D> {
286    type Ok = D;
287    fn into_result(self) -> Result<Self::Ok, crate::Error> {
288        match self {
289            Self::Data(data) => Ok(data),
290            Self::Errs(errs) => Err(errs.into()),
291        }
292    }
293}
294#[cfg(test)]
295mod tests {
296    use quick_xml::events::BytesText;
297
298    use super::*;
299    use crate::capabilities::Requirements;
300
301    #[derive(Debug, Clone, PartialEq, Eq)]
302    struct Foo {
303        foo: &'static str,
304    }
305
306    impl WriteXml for Foo {
307        fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
308            _ = writer
309                .create_element("foo")
310                .write_text_content(BytesText::new(self.foo))?;
311            Ok(())
312        }
313    }
314
315    impl Operation for Foo {
316        const NAME: &'static str = "foo";
317        const REQUIRED_CAPABILITIES: Requirements = Requirements::None;
318        type Builder<'a> = FooBuilder;
319        type Reply = EmptyReply;
320    }
321
322    #[derive(Debug, Default)]
323    struct FooBuilder {
324        foo: Option<&'static str>,
325    }
326
327    impl operation::Builder<'_, Foo> for FooBuilder {
328        fn new(_: &crate::session::Context) -> Self {
329            Self { foo: None }
330        }
331
332        fn finish(self) -> Result<Foo, crate::Error> {
333            let foo = self
334                .foo
335                .ok_or_else(|| crate::Error::missing_operation_parameter("foo", "foo"))?;
336            Ok(Foo { foo })
337        }
338    }
339
340    #[test]
341    fn serialize_foo_request() {
342        let req: Request<Foo> = Request {
343            message_id: MessageId(101),
344            operation: Foo { foo: "bar" },
345        };
346        let expect = r#"<rpc message-id="101"><foo>bar</foo></rpc>]]>]]>"#;
347        assert_eq!(req.to_xml().unwrap(), expect);
348    }
349
350    #[test]
351    fn deserialize_foo_reply() {
352        let data = r#"
353            <rpc-reply
354                message-id="101"
355                xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
356                <ok/>
357            </rpc-reply>
358        "#;
359        let expect: Reply<Foo> = Reply {
360            message_id: MessageId(101),
361            inner: EmptyReply::Ok,
362        };
363        assert_eq!(
364            expect,
365            PartialReply::from_xml(data)
366                .and_then(Reply::try_from)
367                .unwrap()
368        );
369    }
370
371    #[test]
372    fn deserialize_foo_reply_with_xmlns() {
373        let data = r#"
374            <nc:rpc-reply
375                message-id="101"
376                xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0"
377                xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos">
378                <nc:ok/>
379            </nc:rpc-reply>
380            ]]>]]>
381        "#;
382        let expect: Reply<Foo> = Reply {
383            message_id: MessageId(101),
384            inner: EmptyReply::Ok,
385        };
386        assert_eq!(
387            expect,
388            PartialReply::from_xml(data)
389                .and_then(Reply::try_from)
390                .unwrap()
391        );
392    }
393
394    #[derive(Debug, Clone, PartialEq, Eq)]
395    struct Bar;
396
397    impl WriteXml for Bar {
398        fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
399            _ = writer.create_element("bar").write_empty()?;
400            Ok(())
401        }
402    }
403
404    impl Operation for Bar {
405        const NAME: &'static str = "bar";
406        const REQUIRED_CAPABILITIES: Requirements = Requirements::None;
407        type Builder<'a> = BarBuilder;
408        type Reply = DataReply<BarReply>;
409    }
410
411    #[derive(Debug, Clone, PartialEq, Eq)]
412    struct BarReply(usize);
413
414    impl ReadXml for BarReply {
415        #[tracing::instrument(skip_all, fields(tag = ?start.local_name()), level = "debug")]
416        fn read_xml(
417            reader: &mut NsReader<&[u8]>,
418            start: &BytesStart<'_>,
419        ) -> Result<Self, ReadError> {
420            let end = start.to_end();
421            let mut result = None;
422            loop {
423                match reader.read_resolved_event()? {
424                    (ResolveResult::Bound(ns), Event::Start(tag))
425                        if ns == Namespace(b"bar")
426                            && tag.local_name().as_ref() == b"result"
427                            && result.is_none() =>
428                    {
429                        result = Some(
430                            reader
431                                .read_text(tag.to_end().name())?
432                                .parse::<usize>()
433                                .map_err(|err| ReadError::Other(err.into()))?,
434                        );
435                    }
436                    (_, Event::Comment(_)) => continue,
437                    (_, Event::End(tag)) if tag == end => break,
438                    (ns, event) => {
439                        tracing::error!(?event, ?ns, "unexpected xml event");
440                        return Err(ReadError::UnexpectedXmlEvent(event.into_owned()));
441                    }
442                }
443            }
444            Ok(Self(result.ok_or_else(|| {
445                ReadError::missing_element("rpc-reply", "result")
446            })?))
447        }
448    }
449
450    #[derive(Debug, Default)]
451    struct BarBuilder;
452
453    impl operation::Builder<'_, Bar> for BarBuilder {
454        fn new(_: &crate::session::Context) -> Self {
455            Self
456        }
457
458        fn finish(self) -> Result<Bar, crate::Error> {
459            Ok(Bar)
460        }
461    }
462
463    #[test]
464    fn serialize_bar_request() {
465        let req = Request {
466            message_id: MessageId(101),
467            operation: Bar,
468        };
469        let expect = r#"<rpc message-id="101"><bar/></rpc>]]>]]>"#;
470        assert_eq!(req.to_xml().unwrap(), expect);
471    }
472
473    #[test]
474    fn deserialize_bar_reply() {
475        let data = r#"
476            <rpc-reply
477                message-id="101"
478                xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
479                <data>
480                    <result xmlns="bar">99</result>
481                </data>
482            </rpc-reply>
483        "#;
484        let expect: Reply<Bar> = Reply {
485            message_id: MessageId(101),
486            inner: DataReply::Data(BarReply(99)),
487        };
488        assert_eq!(
489            expect,
490            PartialReply::from_xml(data)
491                .and_then(Reply::try_from)
492                .unwrap()
493        );
494    }
495
496    #[test]
497    fn deserialize_bar_reply_with_xmlns() {
498        let data = r#"
499            <nc:rpc-reply
500                message-id="101"
501                xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0"
502                xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos">
503                <nc:data>
504                    <bar:result xmlns:bar="bar">99</bar:result>
505                </nc:data>
506            </nc:rpc-reply>
507            ]]>]]>
508        "#;
509        let expect: Reply<Bar> = Reply {
510            message_id: MessageId(101),
511            inner: DataReply::Data(BarReply(99)),
512        };
513        assert_eq!(
514            expect,
515            PartialReply::from_xml(data)
516                .and_then(Reply::try_from)
517                .unwrap()
518        );
519    }
520
521    #[test]
522    fn deserialize_ok_partial_reply() {
523        let data = r#"
524            <rpc-reply
525                message-id="101"
526                xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
527                <ok/>
528            </rpc-reply>
529        "#;
530        let expect = PartialReply {
531            message_id: MessageId(101),
532            buf: data.into(),
533        };
534        assert_eq!(expect, PartialReply::from_xml(data).unwrap());
535    }
536
537    #[test]
538    fn deserialize_data_partial_reply() {
539        let data = r#"
540            <rpc-reply
541                message-id="101"
542                xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
543                <data><foo/></data>
544            </rpc-reply>
545        "#;
546        let expect = PartialReply {
547            message_id: MessageId(101),
548            buf: data.into(),
549        };
550        assert_eq!(expect, PartialReply::from_xml(data).unwrap());
551    }
552}