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
13static 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#[derive(Clone)]
46pub struct DataPayload {
47 bytes: Bytes,
48 start: usize,
49 end: usize,
50}
51
52impl DataPayload {
53 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 pub(crate) fn empty() -> Self {
67 Self {
68 bytes: Bytes::new(),
69 start: 0,
70 end: 0,
71 }
72 }
73
74 pub fn as_str(&self) -> &str {
79 unsafe { std::str::from_utf8_unchecked(&self.bytes[self.start..self.end]) }
83 }
84
85 pub fn into_string(self) -> String {
90 self.as_str().to_string()
91 }
92
93 pub fn as_bytes(&self) -> &[u8] {
95 &self.bytes[self.start..self.end]
96 }
97
98 pub fn slice(&self) -> Bytes {
100 self.bytes.slice(self.start..self.end)
101 }
102
103 pub fn raw_bytes(&self) -> &Bytes {
105 &self.bytes
106 }
107
108 pub fn len(&self) -> usize {
110 self.end - self.start
111 }
112
113 pub fn is_empty(&self) -> bool {
115 self.start == self.end
116 }
117
118 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
176pub 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 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 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#[derive(Debug)]
248pub enum ServerMessage {
249 RpcReply(RpcReply),
250 }
252
253pub 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
321pub 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
354fn 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
373fn 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 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 let first_error = parse_single_rpc_error(reader)?;
415 let mut errors = vec![first_error];
416
417 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#[derive(Clone, Copy)]
462enum ErrorField {
463 Type,
464 Tag,
465 Severity,
466 Message,
467}
468
469fn 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 & 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 assert!(data.contains("<!-- comment preserved -->"));
649 assert!(data.contains("x & y"));
650 assert!(data.contains("<config"));
651 }
652 _ => panic!("expected Data reply"),
653 }
654 }
655
656 #[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 #[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 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 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 assert!(raw_str.contains("<rpc-reply"));
820 assert!(raw_str.contains("</rpc-reply>"));
821 assert!(raw_str.contains("message-id=\"50\""));
822 assert!(raw_str.contains("<hostname>router1</hostname>"));
824 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 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 assert_eq!(&slice[..], b"content");
860 assert!(raw.len() > slice.len());
862 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}