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 len(&self) -> usize {
105 self.end - self.start
106 }
107
108 pub fn is_empty(&self) -> bool {
110 self.start == self.end
111 }
112
113 pub fn reader(&self) -> Reader<&[u8]> {
118 Reader::from_reader(self.as_bytes())
119 }
120}
121
122impl fmt::Debug for DataPayload {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 let s = self.as_str();
125 if s.len() > 200 {
126 write!(f, "DataPayload({} bytes: {:?}...)", s.len(), &s[..200])
127 } else {
128 write!(f, "DataPayload({:?})", s)
129 }
130 }
131}
132
133impl fmt::Display for DataPayload {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 f.write_str(self.as_str())
136 }
137}
138
139impl Deref for DataPayload {
140 type Target = str;
141
142 fn deref(&self) -> &str {
143 self.as_str()
144 }
145}
146
147impl AsRef<str> for DataPayload {
148 fn as_ref(&self) -> &str {
149 self.as_str()
150 }
151}
152
153impl AsRef<[u8]> for DataPayload {
154 fn as_ref(&self) -> &[u8] {
155 self.as_bytes()
156 }
157}
158
159impl PartialEq<str> for DataPayload {
160 fn eq(&self, other: &str) -> bool {
161 self.as_str() == other
162 }
163}
164
165impl PartialEq<&str> for DataPayload {
166 fn eq(&self, other: &&str) -> bool {
167 self.as_str() == *other
168 }
169}
170
171pub fn build_rpc(inner_xml: &str) -> (u32, String) {
173 let id = next_message_id();
174 let id_str = id.to_string();
175 let mut writer = Writer::new(Cursor::new(Vec::new()));
176
177 writer
178 .write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
179 .unwrap();
180 writer.get_mut().write_all(b"\n").unwrap();
181
182 let mut rpc = BytesStart::new("rpc");
183 rpc.push_attribute(("message-id", id_str.as_str()));
184 rpc.push_attribute(("xmlns", NETCONF_NS));
185 writer.write_event(Event::Start(rpc)).unwrap();
186
187 writer.get_mut().write_all(b"\n ").unwrap();
189 writer.get_mut().write_all(inner_xml.as_bytes()).unwrap();
190 writer.get_mut().write_all(b"\n").unwrap();
191
192 writer
193 .write_event(Event::End(BytesEnd::new("rpc")))
194 .unwrap();
195
196 let bytes = writer.into_inner().into_inner();
197 (id, String::from_utf8(bytes).unwrap())
198}
199
200#[derive(Debug)]
201pub struct RpcReply {
202 pub message_id: u32,
203 pub body: RpcReplyBody,
204}
205
206impl RpcReply {
207 pub fn into_data(self) -> crate::Result<DataPayload> {
212 match self.body {
213 RpcReplyBody::Data(payload) => Ok(payload),
214 RpcReplyBody::Ok => Ok(DataPayload::empty()),
215 RpcReplyBody::Error(errors) => Err(crate::Error::Rpc {
216 message_id: self.message_id,
217 error: errors
218 .first()
219 .map(|e| e.error_message.clone())
220 .unwrap_or_default(),
221 }),
222 }
223 }
224}
225
226#[derive(Debug)]
227pub enum RpcReplyBody {
228 Ok,
229 Data(DataPayload),
230 Error(Vec<RpcError>),
231}
232
233#[derive(Debug, Default)]
234pub struct RpcError {
235 pub error_type: String,
236 pub error_tag: String,
237 pub error_severity: String,
238 pub error_message: String,
239}
240
241#[derive(Debug)]
243pub enum ServerMessage {
244 RpcReply(RpcReply),
245 }
247
248pub fn classify_message(bytes: Bytes) -> crate::Result<ServerMessage> {
256 let xml = std::str::from_utf8(&bytes)
257 .map_err(|_| crate::Error::UnexpectedResponse("received non-UTF-8 message".into()))?;
258
259 let mut reader = Reader::from_str(xml);
260 reader.config_mut().trim_text(true);
261
262 loop {
263 match reader.read_event() {
264 Ok(Event::Start(e)) => {
265 let local = e.local_name();
266 match local.as_ref() {
267 b"rpc-reply" => {
268 let reply = parse_rpc_reply_body(xml, &bytes, &mut reader, &e)?;
269 return Ok(ServerMessage::RpcReply(reply));
270 }
271 b"notification" => {
272 unimplemented!("notification not implemented yet")
273 }
274 other => {
275 return Err(crate::Error::UnexpectedResponse(format!(
276 "unknown root element: <{}>",
277 String::from_utf8_lossy(other)
278 )));
279 }
280 }
281 }
282 Ok(Event::Empty(e)) => {
283 let local = e.local_name();
284 match local.as_ref() {
285 b"rpc-reply" => {
286 let message_id = extract_message_id(&e)?;
287 return Ok(ServerMessage::RpcReply(RpcReply {
288 message_id,
289 body: RpcReplyBody::Ok,
290 }));
291 }
292 other => {
293 return Err(crate::Error::UnexpectedResponse(format!(
294 "unknown root element: <{}>",
295 String::from_utf8_lossy(other)
296 )));
297 }
298 }
299 }
300 Ok(Event::Decl(_)) | Ok(Event::Comment(_)) | Ok(Event::PI(_)) => continue,
301 Ok(Event::Eof) => {
302 return Err(crate::Error::UnexpectedResponse(
303 "empty message: no root element".into(),
304 ));
305 }
306 Err(e) => {
307 return Err(crate::Error::UnexpectedResponse(format!(
308 "XML parse error: {e}"
309 )));
310 }
311 _ => continue,
312 }
313 }
314}
315
316pub fn parse_rpc_reply(xml: &str) -> crate::Result<RpcReply> {
318 let bytes = Bytes::from(xml.to_string());
319 let mut reader = Reader::from_str(xml);
320 reader.config_mut().trim_text(true);
321
322 loop {
323 match reader.read_event() {
324 Ok(Event::Start(e)) if e.local_name().as_ref() == b"rpc-reply" => {
325 return parse_rpc_reply_body(xml, &bytes, &mut reader, &e);
326 }
327 Ok(Event::Empty(e)) if e.local_name().as_ref() == b"rpc-reply" => {
328 let message_id = extract_message_id(&e)?;
329 return Ok(RpcReply {
330 message_id,
331 body: RpcReplyBody::Ok,
332 });
333 }
334 Ok(Event::Eof) => {
335 return Err(crate::Error::UnexpectedResponse(
336 "no rpc-reply element found".into(),
337 ));
338 }
339 Err(e) => {
340 return Err(crate::Error::UnexpectedResponse(format!(
341 "XML parse error: {e}"
342 )));
343 }
344 _ => continue,
345 }
346 }
347}
348
349fn extract_message_id(e: &BytesStart<'_>) -> crate::Result<u32> {
351 let attr = e
352 .try_get_attribute("message-id")
353 .map_err(|err| {
354 crate::Error::UnexpectedResponse(format!("invalid rpc-reply attributes: {err}"))
355 })?
356 .ok_or_else(|| {
357 crate::Error::UnexpectedResponse("missing message-id in rpc-reply".into())
358 })?;
359
360 let val = attr.unescape_value().map_err(|err| {
361 crate::Error::UnexpectedResponse(format!("invalid message-id attr: {err}"))
362 })?;
363
364 val.parse::<u32>()
365 .map_err(|e| crate::Error::UnexpectedResponse(format!("invalid message-id '{val}': {e}")))
366}
367
368fn parse_rpc_reply_body(
376 xml: &str,
377 raw: &Bytes,
378 reader: &mut Reader<&[u8]>,
379 root: &BytesStart<'_>,
380) -> crate::Result<RpcReply> {
381 let message_id = extract_message_id(root)?;
382 let mut body: Option<RpcReplyBody> = None;
383
384 loop {
385 match reader.read_event() {
386 Ok(Event::Start(e)) => {
387 let local = e.local_name();
388 match local.as_ref() {
389 b"data" => {
390 let span = reader.read_to_end(e.name()).map_err(|e| {
393 crate::Error::UnexpectedResponse(format!(
394 "XML parse error in <data>: {e}"
395 ))
396 })?;
397 let inner = xml[span.start as usize..span.end as usize].trim();
398 let trimmed_start = inner.as_ptr() as usize - xml.as_ptr() as usize;
399 let trimmed_end = trimmed_start + inner.len();
400 body = Some(RpcReplyBody::Data(DataPayload::new(
401 raw.clone(),
402 trimmed_start,
403 trimmed_end,
404 )));
405 }
406 b"rpc-error" => {
407 let first_error = parse_single_rpc_error(reader)?;
410 let mut errors = vec![first_error];
411
412 loop {
414 match reader.read_event() {
415 Ok(Event::Start(e2))
416 if e2.local_name().as_ref() == b"rpc-error" =>
417 {
418 errors.push(parse_single_rpc_error(reader)?);
419 }
420 Ok(Event::End(_)) | Ok(Event::Eof) => break,
421 Err(e) => {
422 return Err(crate::Error::UnexpectedResponse(format!(
423 "XML parse error: {e}"
424 )));
425 }
426 _ => {}
427 }
428 }
429
430 body = Some(RpcReplyBody::Error(errors));
431 break;
432 }
433 _ => {}
434 }
435 }
436 Ok(Event::Empty(e)) => match e.local_name().as_ref() {
437 b"ok" => body = Some(RpcReplyBody::Ok),
438 b"data" => body = Some(RpcReplyBody::Data(DataPayload::empty())),
439 _ => {}
440 },
441 Ok(Event::Eof) => break,
442 Err(e) => {
443 return Err(crate::Error::UnexpectedResponse(format!(
444 "XML parse error: {e}"
445 )));
446 }
447 _ => {}
448 }
449 }
450
451 let body = body.unwrap_or(RpcReplyBody::Ok);
452 Ok(RpcReply { message_id, body })
453}
454
455#[derive(Clone, Copy)]
457enum ErrorField {
458 Type,
459 Tag,
460 Severity,
461 Message,
462}
463
464fn parse_single_rpc_error(reader: &mut Reader<&[u8]>) -> crate::Result<RpcError> {
469 let mut error = RpcError::default();
470 let mut current_field: Option<ErrorField> = None;
471
472 loop {
473 match reader.read_event() {
474 Ok(Event::Start(e)) => {
475 current_field = match e.local_name().as_ref() {
476 b"error-type" => Some(ErrorField::Type),
477 b"error-tag" => Some(ErrorField::Tag),
478 b"error-severity" => Some(ErrorField::Severity),
479 b"error-message" => Some(ErrorField::Message),
480 _ => None,
481 };
482 }
483 Ok(Event::Text(e)) => {
484 if let Some(field) = current_field {
485 let text = e.xml_content().unwrap_or_default().to_string();
486 match field {
487 ErrorField::Type => error.error_type = text,
488 ErrorField::Tag => error.error_tag = text,
489 ErrorField::Severity => error.error_severity = text,
490 ErrorField::Message => error.error_message = text,
491 }
492 }
493 }
494 Ok(Event::End(e)) => {
495 if e.local_name().as_ref() == b"rpc-error" {
496 break;
497 }
498 current_field = None;
499 }
500 Ok(Event::Eof) => break,
501 Err(e) => {
502 return Err(crate::Error::UnexpectedResponse(format!(
503 "XML parse error: {e}"
504 )));
505 }
506 _ => {}
507 }
508 }
509
510 Ok(error)
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516
517 #[test]
518 fn test_build_rpc() {
519 let (id, xml) = build_rpc("<get/>");
520 assert!(id > 0);
521 assert!(xml.contains(&format!("message-id=\"{id}\"")));
522 assert!(xml.contains("<get/>"));
523 assert!(xml.contains("<rpc"));
524 assert!(xml.contains("</rpc>"));
525 }
526
527 #[test]
528 fn test_parse_ok_reply() {
529 let xml = r#"<rpc-reply message-id="1" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
530 <ok/>
531</rpc-reply>"#;
532 let reply = parse_rpc_reply(xml).unwrap();
533 assert_eq!(reply.message_id, 1);
534 assert!(matches!(reply.body, RpcReplyBody::Ok));
535 }
536
537 #[test]
538 fn test_parse_data_reply() {
539 let xml = r#"<rpc-reply message-id="2" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
540 <data>
541 <interfaces xmlns="urn:example:interfaces">
542 <interface>
543 <name>eth0</name>
544 </interface>
545 </interfaces>
546 </data>
547</rpc-reply>"#;
548 let reply = parse_rpc_reply(xml).unwrap();
549 assert_eq!(reply.message_id, 2);
550 match &reply.body {
551 RpcReplyBody::Data(data) => {
552 assert!(data.contains("<interfaces"));
553 assert!(data.contains("eth0"));
554 }
555 _ => panic!("expected Data reply"),
556 }
557 }
558
559 #[test]
560 fn test_parse_error_reply() {
561 let xml = r#"<rpc-reply message-id="3" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
562 <rpc-error>
563 <error-type>application</error-type>
564 <error-tag>invalid-value</error-tag>
565 <error-severity>error</error-severity>
566 <error-message>Invalid input</error-message>
567 </rpc-error>
568</rpc-reply>"#;
569 let reply = parse_rpc_reply(xml).unwrap();
570 assert_eq!(reply.message_id, 3);
571 match &reply.body {
572 RpcReplyBody::Error(errors) => {
573 assert_eq!(errors.len(), 1);
574 assert_eq!(errors[0].error_type, "application");
575 assert_eq!(errors[0].error_tag, "invalid-value");
576 assert_eq!(errors[0].error_severity, "error");
577 assert_eq!(errors[0].error_message, "Invalid input");
578 }
579 _ => panic!("expected Error reply"),
580 }
581 }
582
583 #[test]
584 fn test_parse_multiple_errors() {
585 let xml = r#"<rpc-reply message-id="10" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
586 <rpc-error>
587 <error-type>application</error-type>
588 <error-tag>invalid-value</error-tag>
589 <error-severity>error</error-severity>
590 <error-message>First error</error-message>
591 </rpc-error>
592 <rpc-error>
593 <error-type>protocol</error-type>
594 <error-tag>bad-element</error-tag>
595 <error-severity>error</error-severity>
596 <error-message>Second error</error-message>
597 </rpc-error>
598</rpc-reply>"#;
599 let reply = parse_rpc_reply(xml).unwrap();
600 assert_eq!(reply.message_id, 10);
601 match &reply.body {
602 RpcReplyBody::Error(errors) => {
603 assert_eq!(errors.len(), 2);
604 assert_eq!(errors[0].error_message, "First error");
605 assert_eq!(errors[1].error_message, "Second error");
606 assert_eq!(errors[1].error_type, "protocol");
607 }
608 _ => panic!("expected Error reply"),
609 }
610 }
611
612 #[test]
613 fn test_parse_empty_data_reply() {
614 let xml = r#"<rpc-reply message-id="4" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
615 <data/>
616</rpc-reply>"#;
617 let reply = parse_rpc_reply(xml).unwrap();
618 assert_eq!(reply.message_id, 4);
619 assert!(matches!(reply.body, RpcReplyBody::Data(ref s) if s.is_empty()));
620 }
621
622 #[test]
623 fn test_message_ids_increment() {
624 let (id1, _) = build_rpc("<get/>");
625 let (id2, _) = build_rpc("<get/>");
626 assert_eq!(id2, id1 + 1);
627 }
628
629 #[test]
630 fn test_data_preserves_inner_xml_exactly() {
631 let xml = r#"<rpc-reply message-id="5" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
632 <data>
633 <!-- comment preserved -->
634 <config xmlns="urn:example">
635 <value attr="x & y">text</value>
636 </config>
637 </data>
638</rpc-reply>"#;
639 let reply = parse_rpc_reply(xml).unwrap();
640 match &reply.body {
641 RpcReplyBody::Data(data) => {
642 assert!(data.contains("<!-- comment preserved -->"));
644 assert!(data.contains("x & y"));
645 assert!(data.contains("<config"));
646 }
647 _ => panic!("expected Data reply"),
648 }
649 }
650
651 #[test]
654 fn classify_rpc_reply() {
655 let xml = r#"<rpc-reply message-id="1" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><ok/></rpc-reply>"#;
656 let msg = classify_message(Bytes::from(xml)).unwrap();
657 assert!(matches!(msg, ServerMessage::RpcReply(_)));
658 }
659
660 #[test]
661 fn classify_with_xml_declaration() {
662 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>"#;
663 let msg = classify_message(Bytes::from(xml)).unwrap();
664 assert!(matches!(msg, ServerMessage::RpcReply(_)));
665 }
666
667 #[test]
668 fn classify_unknown_root() {
669 let xml = r#"<unknown-element/>"#;
670 let result = classify_message(Bytes::from(xml));
671 assert!(result.is_err());
672 }
673
674 #[test]
675 fn classify_empty_message() {
676 let result = classify_message(Bytes::from(""));
677 assert!(result.is_err());
678 }
679
680 #[test]
683 fn data_payload_as_str() {
684 let xml = r#"<rpc-reply message-id="20" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
685 <data>
686 <config><hostname>router1</hostname></config>
687 </data>
688</rpc-reply>"#;
689 let reply = parse_rpc_reply(xml).unwrap();
690 match &reply.body {
691 RpcReplyBody::Data(payload) => {
692 let s = payload.as_str();
693 assert!(s.contains("<config>"));
694 assert!(s.contains("router1"));
695 assert!(payload.contains("router1"));
697 }
698 _ => panic!("expected Data reply"),
699 }
700 }
701
702 #[test]
703 fn data_payload_into_string() {
704 let xml = r#"<rpc-reply message-id="21" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
705 <data><value>hello</value></data>
706</rpc-reply>"#;
707 let reply = parse_rpc_reply(xml).unwrap();
708 match reply.body {
709 RpcReplyBody::Data(payload) => {
710 let s = payload.into_string();
711 assert!(s.contains("<value>hello</value>"));
712 }
713 _ => panic!("expected Data reply"),
714 }
715 }
716
717 #[test]
718 fn data_payload_reader() {
719 let xml = r#"<rpc-reply message-id="22" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
720 <data><item>one</item><item>two</item></data>
721</rpc-reply>"#;
722 let reply = parse_rpc_reply(xml).unwrap();
723 match &reply.body {
724 RpcReplyBody::Data(payload) => {
725 let mut reader = payload.reader();
726 let mut buf = Vec::new();
727 let mut items = Vec::new();
728 loop {
729 match reader.read_event_into(&mut buf) {
730 Ok(Event::Start(e)) if e.local_name().as_ref() == b"item" => {}
731 Ok(Event::Text(e)) => {
732 items.push(e.xml_content().unwrap().to_string());
733 }
734 Ok(Event::Eof) => break,
735 _ => {}
736 }
737 buf.clear();
738 }
739 assert_eq!(items, vec!["one", "two"]);
740 }
741 _ => panic!("expected Data reply"),
742 }
743 }
744
745 #[test]
746 fn data_payload_empty() {
747 let payload = DataPayload::empty();
748 assert!(payload.is_empty());
749 assert_eq!(payload.len(), 0);
750 assert_eq!(payload.as_str(), "");
751 assert_eq!(payload.into_string(), "");
752 }
753
754 #[test]
755 fn data_payload_len_and_is_empty() {
756 let xml = r#"<rpc-reply message-id="23" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
757 <data>abc</data>
758</rpc-reply>"#;
759 let reply = parse_rpc_reply(xml).unwrap();
760 match &reply.body {
761 RpcReplyBody::Data(payload) => {
762 assert_eq!(payload.len(), 3);
763 assert!(!payload.is_empty());
764 }
765 _ => panic!("expected Data reply"),
766 }
767 }
768
769 #[test]
770 fn data_payload_large_preserves_content() {
771 let inner = "<item>x</item>".repeat(1000);
773 let xml = format!(
774 r#"<rpc-reply message-id="24" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
775 <data>{inner}</data>
776</rpc-reply>"#
777 );
778 let reply = parse_rpc_reply(&xml).unwrap();
779 match &reply.body {
780 RpcReplyBody::Data(payload) => {
781 assert_eq!(payload.as_str(), inner);
782 assert_eq!(payload.len(), inner.len());
783 }
784 _ => panic!("expected Data reply"),
785 }
786 }
787
788 #[test]
789 fn data_payload_partial_eq() {
790 let xml = r#"<rpc-reply message-id="25" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
791 <data>hello</data>
792</rpc-reply>"#;
793 let reply = parse_rpc_reply(xml).unwrap();
794 match &reply.body {
795 RpcReplyBody::Data(payload) => {
796 assert!(*payload == *"hello");
797 assert!(*payload != *"world");
798 }
799 _ => panic!("expected Data reply"),
800 }
801 }
802
803 #[test]
804 fn rpc_reply_into_data() {
805 let xml = r#"<rpc-reply message-id="26" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
806 <data><config/></data>
807</rpc-reply>"#;
808 let reply = parse_rpc_reply(xml).unwrap();
809 let payload = reply.into_data().unwrap();
810 assert!(payload.contains("<config/>"));
811 }
812
813 #[test]
814 fn rpc_reply_into_data_ok() {
815 let xml = r#"<rpc-reply message-id="27" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
816 <ok/>
817</rpc-reply>"#;
818 let reply = parse_rpc_reply(xml).unwrap();
819 let payload = reply.into_data().unwrap();
820 assert!(payload.is_empty());
821 }
822
823 #[test]
824 fn rpc_reply_into_data_error() {
825 let xml = r#"<rpc-reply message-id="28" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
826 <rpc-error>
827 <error-type>application</error-type>
828 <error-tag>invalid-value</error-tag>
829 <error-severity>error</error-severity>
830 <error-message>bad</error-message>
831 </rpc-error>
832</rpc-reply>"#;
833 let reply = parse_rpc_reply(xml).unwrap();
834 assert!(reply.into_data().is_err());
835 }
836}