Skip to main content

netconf_rust/
message.rs

1use std::fmt;
2use std::io::{Cursor, Write};
3use std::ops::Deref;
4use std::sync::atomic::{AtomicU32, Ordering};
5
6use bytes::Bytes;
7use quick_xml::Reader;
8use quick_xml::Writer;
9use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
10
11const NETCONF_NS: &str = "urn:ietf:params:xml:ns:netconf:base:1.0";
12
13// Global message-id counter shared across all sessions.
14//
15// A NETCONF session is a single SSH connection to a device. Each session
16// has its own stream of RPCs and replies.
17//
18// Each RPC needs a unique message-id so the client can match replies to
19// requests. This is needed for pipelining, where multiple RPCs are
20// sent without waiting for replies — when the replies arrive, the
21// message-id is how we know which reply belongs to which request.
22//
23// IDs only need to be unique within a
24// session, and globally unique is a superset of that.
25//
26// Relaxed ordering: we only need each fetch_add to return a different
27// number. No other memory operations depend on this value, so we don't
28// need the stronger (and more expensive) ordering guarantees.
29static MESSAGE_ID_COUNTER: AtomicU32 = AtomicU32::new(1);
30
31pub fn next_message_id() -> u32 {
32    MESSAGE_ID_COUNTER.fetch_add(1, Ordering::Relaxed)
33}
34
35/// Zero-copy wrapper around a `Bytes` buffer with span offsets pointing to the
36/// `<data>` content within an RPC reply.
37///
38/// `DataPayload` avoids copying the XML content into a `String`. The underlying
39/// `Bytes` is reference-counted (O(1) clone), and `as_str()` returns a view
40/// without allocation. Users who need a `String` can call `into_string()`.
41///
42/// For streaming XML processing, `reader()` returns a `quick_xml::Reader` that
43/// borrows directly from the payload's bytes — events reference the same memory
44/// with no intermediate copies.
45#[derive(Clone)]
46pub struct DataPayload {
47    bytes: Bytes,
48    start: usize,
49    end: usize,
50}
51
52impl DataPayload {
53    /// Create a new `DataPayload` referencing a slice of `bytes`.
54    ///
55    /// # Safety contract
56    /// The caller must ensure `bytes[start..end]` is valid UTF-8.
57    /// This is guaranteed when called from the parser, which validates
58    /// UTF-8 before parsing.
59    pub(crate) fn new(bytes: Bytes, start: usize, end: usize) -> Self {
60        debug_assert!(start <= end);
61        debug_assert!(end <= bytes.len());
62        Self { bytes, start, end }
63    }
64
65    /// Create an empty `DataPayload` (for `<data/>` responses).
66    pub(crate) fn empty() -> Self {
67        Self {
68            bytes: Bytes::new(),
69            start: 0,
70            end: 0,
71        }
72    }
73
74    /// View the data content as `&str` without copying.
75    ///
76    /// This is O(1) — no allocation or UTF-8 validation. The bytes were
77    /// validated as UTF-8 by the reader task before parsing.
78    pub fn as_str(&self) -> &str {
79        // SAFETY: The reader task validates UTF-8 (via std::str::from_utf8)
80        // before the parser constructs a DataPayload. The bytes are immutable
81        // (Bytes is ref-counted), so the UTF-8 invariant is preserved.
82        unsafe { std::str::from_utf8_unchecked(&self.bytes[self.start..self.end]) }
83    }
84
85    /// Convert to an owned `String`.
86    ///
87    /// This copies the data content. Use `as_str()` to avoid the copy
88    /// when you only need a `&str` view.
89    pub fn into_string(self) -> String {
90        self.as_str().to_string()
91    }
92
93    /// View the data content as `&[u8]`.
94    pub fn as_bytes(&self) -> &[u8] {
95        &self.bytes[self.start..self.end]
96    }
97
98    /// Get the underlying `Bytes` slice covering just the data content.
99    pub fn slice(&self) -> Bytes {
100        self.bytes.slice(self.start..self.end)
101    }
102
103    /// Get the full raw RPC reply buffer including the `<rpc-reply>` envelope.
104    pub fn raw_bytes(&self) -> &Bytes {
105        &self.bytes
106    }
107
108    /// Length of the data content in bytes.
109    pub fn len(&self) -> usize {
110        self.end - self.start
111    }
112
113    /// Returns `true` if the data content is empty.
114    pub fn is_empty(&self) -> bool {
115        self.start == self.end
116    }
117
118    /// Create a `quick_xml::Reader` over the data content.
119    ///
120    /// Events borrow directly from the payload's bytes — no intermediate copies.
121    /// This enables StAX-style streaming processing of large responses.
122    pub fn reader(&self) -> Reader<&[u8]> {
123        Reader::from_reader(self.as_bytes())
124    }
125}
126
127impl fmt::Debug for DataPayload {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        let s = self.as_str();
130        if s.len() > 200 {
131            write!(f, "DataPayload({} bytes: {:?}...)", s.len(), &s[..200])
132        } else {
133            write!(f, "DataPayload({:?})", s)
134        }
135    }
136}
137
138impl fmt::Display for DataPayload {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        f.write_str(self.as_str())
141    }
142}
143
144impl Deref for DataPayload {
145    type Target = str;
146
147    fn deref(&self) -> &str {
148        self.as_str()
149    }
150}
151
152impl AsRef<str> for DataPayload {
153    fn as_ref(&self) -> &str {
154        self.as_str()
155    }
156}
157
158impl AsRef<[u8]> for DataPayload {
159    fn as_ref(&self) -> &[u8] {
160        self.as_bytes()
161    }
162}
163
164impl PartialEq<str> for DataPayload {
165    fn eq(&self, other: &str) -> bool {
166        self.as_str() == other
167    }
168}
169
170impl PartialEq<&str> for DataPayload {
171    fn eq(&self, other: &&str) -> bool {
172        self.as_str() == *other
173    }
174}
175
176/// Build an RPC envelope around inner XML content using quick-xml's Writer.
177pub fn build_rpc(inner_xml: &str) -> (u32, String) {
178    let id = next_message_id();
179    let id_str = id.to_string();
180    let mut writer = Writer::new(Cursor::new(Vec::new()));
181
182    writer
183        .write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
184        .unwrap();
185    writer.get_mut().write_all(b"\n").unwrap();
186
187    let mut rpc = BytesStart::new("rpc");
188    rpc.push_attribute(("message-id", id_str.as_str()));
189    rpc.push_attribute(("xmlns", NETCONF_NS));
190    writer.write_event(Event::Start(rpc)).unwrap();
191
192    // Inner XML is written verbatim — it may be a simple <get/> or complex filter XML
193    writer.get_mut().write_all(b"\n  ").unwrap();
194    writer.get_mut().write_all(inner_xml.as_bytes()).unwrap();
195    writer.get_mut().write_all(b"\n").unwrap();
196
197    writer
198        .write_event(Event::End(BytesEnd::new("rpc")))
199        .unwrap();
200
201    let bytes = writer.into_inner().into_inner();
202    (id, String::from_utf8(bytes).unwrap())
203}
204
205#[derive(Debug)]
206pub struct RpcReply {
207    pub message_id: u32,
208    pub body: RpcReplyBody,
209}
210
211impl RpcReply {
212    /// Extract the data payload from this reply, or propagate an error.
213    ///
214    /// Returns `Ok(DataPayload)` for `Data` replies, `Ok(DataPayload::empty())`
215    /// for `Ok` replies, and `Err` for error replies.
216    pub fn into_data(self) -> crate::Result<DataPayload> {
217        match self.body {
218            RpcReplyBody::Data(payload) => Ok(payload),
219            RpcReplyBody::Ok => Ok(DataPayload::empty()),
220            RpcReplyBody::Error(errors) => Err(crate::Error::Rpc {
221                message_id: self.message_id,
222                error: errors
223                    .first()
224                    .map(|e| e.error_message.clone())
225                    .unwrap_or_default(),
226            }),
227        }
228    }
229}
230
231#[derive(Debug)]
232pub enum RpcReplyBody {
233    Ok,
234    Data(DataPayload),
235    Error(Vec<RpcError>),
236}
237
238#[derive(Debug, Default)]
239pub struct RpcError {
240    pub error_type: String,
241    pub error_tag: String,
242    pub error_severity: String,
243    pub error_message: String,
244}
245
246/// A message received from the NETCONF server.
247#[derive(Debug)]
248pub enum ServerMessage {
249    RpcReply(RpcReply),
250    //Notification(Notification),
251}
252
253/// Classify and parse an incoming server message in a single pass.
254///
255/// Creates one `Reader`, identifies the root element, then continues
256/// parsing with the same reader — no double-parsing.
257///
258/// Takes ownership of `Bytes` from the codec so that `DataPayload` can
259/// reference the buffer without copying.
260pub fn classify_message(bytes: Bytes) -> crate::Result<ServerMessage> {
261    let xml = std::str::from_utf8(&bytes)
262        .map_err(|_| crate::Error::UnexpectedResponse("received non-UTF-8 message".into()))?;
263
264    let mut reader = Reader::from_str(xml);
265    reader.config_mut().trim_text(true);
266
267    loop {
268        match reader.read_event() {
269            Ok(Event::Start(e)) => {
270                let local = e.local_name();
271                match local.as_ref() {
272                    b"rpc-reply" => {
273                        let reply = parse_rpc_reply_body(xml, &bytes, &mut reader, &e)?;
274                        return Ok(ServerMessage::RpcReply(reply));
275                    }
276                    b"notification" => {
277                        unimplemented!("notification not implemented yet")
278                    }
279                    other => {
280                        return Err(crate::Error::UnexpectedResponse(format!(
281                            "unknown root element: <{}>",
282                            String::from_utf8_lossy(other)
283                        )));
284                    }
285                }
286            }
287            Ok(Event::Empty(e)) => {
288                let local = e.local_name();
289                match local.as_ref() {
290                    b"rpc-reply" => {
291                        let message_id = extract_message_id(&e)?;
292                        return Ok(ServerMessage::RpcReply(RpcReply {
293                            message_id,
294                            body: RpcReplyBody::Ok,
295                        }));
296                    }
297                    other => {
298                        return Err(crate::Error::UnexpectedResponse(format!(
299                            "unknown root element: <{}>",
300                            String::from_utf8_lossy(other)
301                        )));
302                    }
303                }
304            }
305            Ok(Event::Decl(_)) | Ok(Event::Comment(_)) | Ok(Event::PI(_)) => continue,
306            Ok(Event::Eof) => {
307                return Err(crate::Error::UnexpectedResponse(
308                    "empty message: no root element".into(),
309                ));
310            }
311            Err(e) => {
312                return Err(crate::Error::UnexpectedResponse(format!(
313                    "XML parse error: {e}"
314                )));
315            }
316            _ => continue,
317        }
318    }
319}
320
321/// Standalone entry point for parsing an `<rpc-reply>` (used by tests).
322pub fn parse_rpc_reply(xml: &str) -> crate::Result<RpcReply> {
323    let bytes = Bytes::from(xml.to_string());
324    let mut reader = Reader::from_str(xml);
325    reader.config_mut().trim_text(true);
326
327    loop {
328        match reader.read_event() {
329            Ok(Event::Start(e)) if e.local_name().as_ref() == b"rpc-reply" => {
330                return parse_rpc_reply_body(xml, &bytes, &mut reader, &e);
331            }
332            Ok(Event::Empty(e)) if e.local_name().as_ref() == b"rpc-reply" => {
333                let message_id = extract_message_id(&e)?;
334                return Ok(RpcReply {
335                    message_id,
336                    body: RpcReplyBody::Ok,
337                });
338            }
339            Ok(Event::Eof) => {
340                return Err(crate::Error::UnexpectedResponse(
341                    "no rpc-reply element found".into(),
342                ));
343            }
344            Err(e) => {
345                return Err(crate::Error::UnexpectedResponse(format!(
346                    "XML parse error: {e}"
347                )));
348            }
349            _ => continue,
350        }
351    }
352}
353
354/// Extract `message-id` from a `<rpc-reply>` element using `try_get_attribute`.
355fn extract_message_id(e: &BytesStart<'_>) -> crate::Result<u32> {
356    let attr = e
357        .try_get_attribute("message-id")
358        .map_err(|err| {
359            crate::Error::UnexpectedResponse(format!("invalid rpc-reply attributes: {err}"))
360        })?
361        .ok_or_else(|| {
362            crate::Error::UnexpectedResponse("missing message-id in rpc-reply".into())
363        })?;
364
365    let val = attr.unescape_value().map_err(|err| {
366        crate::Error::UnexpectedResponse(format!("invalid message-id attr: {err}"))
367    })?;
368
369    val.parse::<u32>()
370        .map_err(|e| crate::Error::UnexpectedResponse(format!("invalid message-id '{val}': {e}")))
371}
372
373/// Parse the body of an `<rpc-reply>` after the root Start event has been consumed.
374///
375/// Uses span-based slicing for `<data>` content (zero-copy from the original XML)
376/// and inline parsing for `<rpc-error>` (no re-parse of the document).
377///
378/// The `raw` parameter is the ref-counted `Bytes` buffer from the codec, used
379/// to construct `DataPayload` without copying.
380fn parse_rpc_reply_body(
381    xml: &str,
382    raw: &Bytes,
383    reader: &mut Reader<&[u8]>,
384    root: &BytesStart<'_>,
385) -> crate::Result<RpcReply> {
386    let message_id = extract_message_id(root)?;
387    let mut body: Option<RpcReplyBody> = None;
388
389    loop {
390        match reader.read_event() {
391            Ok(Event::Start(e)) => {
392                let local = e.local_name();
393                match local.as_ref() {
394                    b"data" => {
395                        // Span-based: read_to_end returns byte offsets into the original XML.
396                        // Slice the original string directly — zero-copy, exact preservation.
397                        let span = reader.read_to_end(e.name()).map_err(|e| {
398                            crate::Error::UnexpectedResponse(format!(
399                                "XML parse error in <data>: {e}"
400                            ))
401                        })?;
402                        let inner = xml[span.start as usize..span.end as usize].trim();
403                        let trimmed_start = inner.as_ptr() as usize - xml.as_ptr() as usize;
404                        let trimmed_end = trimmed_start + inner.len();
405                        body = Some(RpcReplyBody::Data(DataPayload::new(
406                            raw.clone(),
407                            trimmed_start,
408                            trimmed_end,
409                        )));
410                    }
411                    b"rpc-error" => {
412                        // Inline error parsing: continue with the current reader
413                        // instead of re-parsing the entire document.
414                        let first_error = parse_single_rpc_error(reader)?;
415                        let mut errors = vec![first_error];
416
417                        // Check for additional <rpc-error> elements
418                        loop {
419                            match reader.read_event() {
420                                Ok(Event::Start(e2))
421                                    if e2.local_name().as_ref() == b"rpc-error" =>
422                                {
423                                    errors.push(parse_single_rpc_error(reader)?);
424                                }
425                                Ok(Event::End(_)) | Ok(Event::Eof) => break,
426                                Err(e) => {
427                                    return Err(crate::Error::UnexpectedResponse(format!(
428                                        "XML parse error: {e}"
429                                    )));
430                                }
431                                _ => {}
432                            }
433                        }
434
435                        body = Some(RpcReplyBody::Error(errors));
436                        break;
437                    }
438                    _ => {}
439                }
440            }
441            Ok(Event::Empty(e)) => match e.local_name().as_ref() {
442                b"ok" => body = Some(RpcReplyBody::Ok),
443                b"data" => body = Some(RpcReplyBody::Data(DataPayload::empty())),
444                _ => {}
445            },
446            Ok(Event::Eof) => break,
447            Err(e) => {
448                return Err(crate::Error::UnexpectedResponse(format!(
449                    "XML parse error: {e}"
450                )));
451            }
452            _ => {}
453        }
454    }
455
456    let body = body.unwrap_or(RpcReplyBody::Ok);
457    Ok(RpcReply { message_id, body })
458}
459
460/// Fields within an `<rpc-error>` element.
461#[derive(Clone, Copy)]
462enum ErrorField {
463    Type,
464    Tag,
465    Severity,
466    Message,
467}
468
469/// Parse a single `<rpc-error>` block from the current reader position.
470///
471/// Called after the `<rpc-error>` Start event has been consumed.
472/// Reads until the matching `</rpc-error>` End event.
473fn parse_single_rpc_error(reader: &mut Reader<&[u8]>) -> crate::Result<RpcError> {
474    let mut error = RpcError::default();
475    let mut current_field: Option<ErrorField> = None;
476
477    loop {
478        match reader.read_event() {
479            Ok(Event::Start(e)) => {
480                current_field = match e.local_name().as_ref() {
481                    b"error-type" => Some(ErrorField::Type),
482                    b"error-tag" => Some(ErrorField::Tag),
483                    b"error-severity" => Some(ErrorField::Severity),
484                    b"error-message" => Some(ErrorField::Message),
485                    _ => None,
486                };
487            }
488            Ok(Event::Text(e)) => {
489                if let Some(field) = current_field {
490                    let text = e.xml_content().unwrap_or_default().to_string();
491                    match field {
492                        ErrorField::Type => error.error_type = text,
493                        ErrorField::Tag => error.error_tag = text,
494                        ErrorField::Severity => error.error_severity = text,
495                        ErrorField::Message => error.error_message = text,
496                    }
497                }
498            }
499            Ok(Event::End(e)) => {
500                if e.local_name().as_ref() == b"rpc-error" {
501                    break;
502                }
503                current_field = None;
504            }
505            Ok(Event::Eof) => break,
506            Err(e) => {
507                return Err(crate::Error::UnexpectedResponse(format!(
508                    "XML parse error: {e}"
509                )));
510            }
511            _ => {}
512        }
513    }
514
515    Ok(error)
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521
522    #[test]
523    fn test_build_rpc() {
524        let (id, xml) = build_rpc("<get/>");
525        assert!(id > 0);
526        assert!(xml.contains(&format!("message-id=\"{id}\"")));
527        assert!(xml.contains("<get/>"));
528        assert!(xml.contains("<rpc"));
529        assert!(xml.contains("</rpc>"));
530    }
531
532    #[test]
533    fn test_parse_ok_reply() {
534        let xml = r#"<rpc-reply message-id="1" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
535  <ok/>
536</rpc-reply>"#;
537        let reply = parse_rpc_reply(xml).unwrap();
538        assert_eq!(reply.message_id, 1);
539        assert!(matches!(reply.body, RpcReplyBody::Ok));
540    }
541
542    #[test]
543    fn test_parse_data_reply() {
544        let xml = r#"<rpc-reply message-id="2" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
545  <data>
546    <interfaces xmlns="urn:example:interfaces">
547      <interface>
548        <name>eth0</name>
549      </interface>
550    </interfaces>
551  </data>
552</rpc-reply>"#;
553        let reply = parse_rpc_reply(xml).unwrap();
554        assert_eq!(reply.message_id, 2);
555        match &reply.body {
556            RpcReplyBody::Data(data) => {
557                assert!(data.contains("<interfaces"));
558                assert!(data.contains("eth0"));
559            }
560            _ => panic!("expected Data reply"),
561        }
562    }
563
564    #[test]
565    fn test_parse_error_reply() {
566        let xml = r#"<rpc-reply message-id="3" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
567  <rpc-error>
568    <error-type>application</error-type>
569    <error-tag>invalid-value</error-tag>
570    <error-severity>error</error-severity>
571    <error-message>Invalid input</error-message>
572  </rpc-error>
573</rpc-reply>"#;
574        let reply = parse_rpc_reply(xml).unwrap();
575        assert_eq!(reply.message_id, 3);
576        match &reply.body {
577            RpcReplyBody::Error(errors) => {
578                assert_eq!(errors.len(), 1);
579                assert_eq!(errors[0].error_type, "application");
580                assert_eq!(errors[0].error_tag, "invalid-value");
581                assert_eq!(errors[0].error_severity, "error");
582                assert_eq!(errors[0].error_message, "Invalid input");
583            }
584            _ => panic!("expected Error reply"),
585        }
586    }
587
588    #[test]
589    fn test_parse_multiple_errors() {
590        let xml = r#"<rpc-reply message-id="10" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
591  <rpc-error>
592    <error-type>application</error-type>
593    <error-tag>invalid-value</error-tag>
594    <error-severity>error</error-severity>
595    <error-message>First error</error-message>
596  </rpc-error>
597  <rpc-error>
598    <error-type>protocol</error-type>
599    <error-tag>bad-element</error-tag>
600    <error-severity>error</error-severity>
601    <error-message>Second error</error-message>
602  </rpc-error>
603</rpc-reply>"#;
604        let reply = parse_rpc_reply(xml).unwrap();
605        assert_eq!(reply.message_id, 10);
606        match &reply.body {
607            RpcReplyBody::Error(errors) => {
608                assert_eq!(errors.len(), 2);
609                assert_eq!(errors[0].error_message, "First error");
610                assert_eq!(errors[1].error_message, "Second error");
611                assert_eq!(errors[1].error_type, "protocol");
612            }
613            _ => panic!("expected Error reply"),
614        }
615    }
616
617    #[test]
618    fn test_parse_empty_data_reply() {
619        let xml = r#"<rpc-reply message-id="4" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
620  <data/>
621</rpc-reply>"#;
622        let reply = parse_rpc_reply(xml).unwrap();
623        assert_eq!(reply.message_id, 4);
624        assert!(matches!(reply.body, RpcReplyBody::Data(ref s) if s.is_empty()));
625    }
626
627    #[test]
628    fn test_message_ids_increment() {
629        let (id1, _) = build_rpc("<get/>");
630        let (id2, _) = build_rpc("<get/>");
631        assert_eq!(id2, id1 + 1);
632    }
633
634    #[test]
635    fn test_data_preserves_inner_xml_exactly() {
636        let xml = r#"<rpc-reply message-id="5" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
637  <data>
638    <!-- comment preserved -->
639    <config xmlns="urn:example">
640      <value attr="x &amp; y">text</value>
641    </config>
642  </data>
643</rpc-reply>"#;
644        let reply = parse_rpc_reply(xml).unwrap();
645        match &reply.body {
646            RpcReplyBody::Data(data) => {
647                // Span-based extraction preserves comments, entities, and attributes exactly
648                assert!(data.contains("<!-- comment preserved -->"));
649                assert!(data.contains("x &amp; y"));
650                assert!(data.contains("<config"));
651            }
652            _ => panic!("expected Data reply"),
653        }
654    }
655
656    // ── classify_message tests ──────────────────────────────────────────
657
658    #[test]
659    fn classify_rpc_reply() {
660        let xml = r#"<rpc-reply message-id="1" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><ok/></rpc-reply>"#;
661        let msg = classify_message(Bytes::from(xml)).unwrap();
662        assert!(matches!(msg, ServerMessage::RpcReply(_)));
663    }
664
665    #[test]
666    fn classify_with_xml_declaration() {
667        let xml = r#"<?xml version="1.0" encoding="UTF-8"?><rpc-reply message-id="5" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><ok/></rpc-reply>"#;
668        let msg = classify_message(Bytes::from(xml)).unwrap();
669        assert!(matches!(msg, ServerMessage::RpcReply(_)));
670    }
671
672    #[test]
673    fn classify_unknown_root() {
674        let xml = r#"<unknown-element/>"#;
675        let result = classify_message(Bytes::from(xml));
676        assert!(result.is_err());
677    }
678
679    #[test]
680    fn classify_empty_message() {
681        let result = classify_message(Bytes::from(""));
682        assert!(result.is_err());
683    }
684
685    // ── DataPayload tests ──────────────────────────────────────────────
686
687    #[test]
688    fn data_payload_as_str() {
689        let xml = r#"<rpc-reply message-id="20" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
690  <data>
691    <config><hostname>router1</hostname></config>
692  </data>
693</rpc-reply>"#;
694        let reply = parse_rpc_reply(xml).unwrap();
695        match &reply.body {
696            RpcReplyBody::Data(payload) => {
697                let s = payload.as_str();
698                assert!(s.contains("<config>"));
699                assert!(s.contains("router1"));
700                // Deref<Target=str> works
701                assert!(payload.contains("router1"));
702            }
703            _ => panic!("expected Data reply"),
704        }
705    }
706
707    #[test]
708    fn data_payload_into_string() {
709        let xml = r#"<rpc-reply message-id="21" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
710  <data><value>hello</value></data>
711</rpc-reply>"#;
712        let reply = parse_rpc_reply(xml).unwrap();
713        match reply.body {
714            RpcReplyBody::Data(payload) => {
715                let s = payload.into_string();
716                assert!(s.contains("<value>hello</value>"));
717            }
718            _ => panic!("expected Data reply"),
719        }
720    }
721
722    #[test]
723    fn data_payload_reader() {
724        let xml = r#"<rpc-reply message-id="22" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
725  <data><item>one</item><item>two</item></data>
726</rpc-reply>"#;
727        let reply = parse_rpc_reply(xml).unwrap();
728        match &reply.body {
729            RpcReplyBody::Data(payload) => {
730                let mut reader = payload.reader();
731                let mut buf = Vec::new();
732                let mut items = Vec::new();
733                loop {
734                    match reader.read_event_into(&mut buf) {
735                        Ok(Event::Start(e)) if e.local_name().as_ref() == b"item" => {}
736                        Ok(Event::Text(e)) => {
737                            items.push(e.xml_content().unwrap().to_string());
738                        }
739                        Ok(Event::Eof) => break,
740                        _ => {}
741                    }
742                    buf.clear();
743                }
744                assert_eq!(items, vec!["one", "two"]);
745            }
746            _ => panic!("expected Data reply"),
747        }
748    }
749
750    #[test]
751    fn data_payload_empty() {
752        let payload = DataPayload::empty();
753        assert!(payload.is_empty());
754        assert_eq!(payload.len(), 0);
755        assert_eq!(payload.as_str(), "");
756        assert_eq!(payload.into_string(), "");
757    }
758
759    #[test]
760    fn data_payload_len_and_is_empty() {
761        let xml = r#"<rpc-reply message-id="23" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
762  <data>abc</data>
763</rpc-reply>"#;
764        let reply = parse_rpc_reply(xml).unwrap();
765        match &reply.body {
766            RpcReplyBody::Data(payload) => {
767                assert_eq!(payload.len(), 3);
768                assert!(!payload.is_empty());
769            }
770            _ => panic!("expected Data reply"),
771        }
772    }
773
774    #[test]
775    fn data_payload_large_preserves_content() {
776        // Generate a large data payload to verify no truncation
777        let inner = "<item>x</item>".repeat(1000);
778        let xml = format!(
779            r#"<rpc-reply message-id="24" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
780  <data>{inner}</data>
781</rpc-reply>"#
782        );
783        let reply = parse_rpc_reply(&xml).unwrap();
784        match &reply.body {
785            RpcReplyBody::Data(payload) => {
786                assert_eq!(payload.as_str(), inner);
787                assert_eq!(payload.len(), inner.len());
788            }
789            _ => panic!("expected Data reply"),
790        }
791    }
792
793    #[test]
794    fn data_payload_partial_eq() {
795        let xml = r#"<rpc-reply message-id="25" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
796  <data>hello</data>
797</rpc-reply>"#;
798        let reply = parse_rpc_reply(xml).unwrap();
799        match &reply.body {
800            RpcReplyBody::Data(payload) => {
801                assert!(*payload == *"hello");
802                assert!(*payload != *"world");
803            }
804            _ => panic!("expected Data reply"),
805        }
806    }
807
808    #[test]
809    fn data_payload_raw_bytes_contains_envelope() {
810        let xml = r#"<rpc-reply message-id="50" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
811  <data><config><hostname>router1</hostname></config></data>
812</rpc-reply>"#;
813        let reply = parse_rpc_reply(xml).unwrap();
814        match &reply.body {
815            RpcReplyBody::Data(payload) => {
816                let raw = payload.raw_bytes();
817                let raw_str = std::str::from_utf8(raw).unwrap();
818                // Raw should contain the full envelope
819                assert!(raw_str.contains("<rpc-reply"));
820                assert!(raw_str.contains("</rpc-reply>"));
821                assert!(raw_str.contains("message-id=\"50\""));
822                // And the data content
823                assert!(raw_str.contains("<hostname>router1</hostname>"));
824                // Raw should be larger than just the data content
825                assert!(raw.len() > payload.len());
826            }
827            _ => panic!("expected Data reply"),
828        }
829    }
830
831    #[test]
832    fn data_payload_raw_bytes_empty_payload() {
833        let xml = r#"<rpc-reply message-id="51" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
834  <data/>
835</rpc-reply>"#;
836        let reply = parse_rpc_reply(xml).unwrap();
837        match &reply.body {
838            RpcReplyBody::Data(payload) => {
839                assert!(payload.is_empty());
840                let raw = payload.raw_bytes();
841                // Raw bytes should still be empty for DataPayload::empty()
842                assert!(raw.is_empty());
843            }
844            _ => panic!("expected Data reply"),
845        }
846    }
847
848    #[test]
849    fn data_payload_raw_bytes_vs_slice() {
850        let xml = r#"<rpc-reply message-id="52" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
851  <data>content</data>
852</rpc-reply>"#;
853        let reply = parse_rpc_reply(xml).unwrap();
854        match &reply.body {
855            RpcReplyBody::Data(payload) => {
856                let raw = payload.raw_bytes();
857                let slice = payload.slice();
858                // slice is just the data content
859                assert_eq!(&slice[..], b"content");
860                // raw is the full reply
861                assert!(raw.len() > slice.len());
862                // slice bytes should be a subset of raw bytes
863                let raw_str = std::str::from_utf8(raw).unwrap();
864                assert!(raw_str.contains(std::str::from_utf8(&slice).unwrap()));
865            }
866            _ => panic!("expected Data reply"),
867        }
868    }
869
870    #[test]
871    fn rpc_reply_into_data() {
872        let xml = r#"<rpc-reply message-id="26" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
873  <data><config/></data>
874</rpc-reply>"#;
875        let reply = parse_rpc_reply(xml).unwrap();
876        let payload = reply.into_data().unwrap();
877        assert!(payload.contains("<config/>"));
878    }
879
880    #[test]
881    fn rpc_reply_into_data_ok() {
882        let xml = r#"<rpc-reply message-id="27" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
883  <ok/>
884</rpc-reply>"#;
885        let reply = parse_rpc_reply(xml).unwrap();
886        let payload = reply.into_data().unwrap();
887        assert!(payload.is_empty());
888    }
889
890    #[test]
891    fn rpc_reply_into_data_error() {
892        let xml = r#"<rpc-reply message-id="28" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
893  <rpc-error>
894    <error-type>application</error-type>
895    <error-tag>invalid-value</error-tag>
896    <error-severity>error</error-severity>
897    <error-message>bad</error-message>
898  </rpc-error>
899</rpc-reply>"#;
900        let reply = parse_rpc_reply(xml).unwrap();
901        assert!(reply.into_data().is_err());
902    }
903}