diff --git a/crates/netconf-proto/src/protocol.rs b/crates/netconf-proto/src/protocol.rs index 0895017d..641a19d6 100644 --- a/crates/netconf-proto/src/protocol.rs +++ b/crates/netconf-proto/src/protocol.rs @@ -1,3 +1,4 @@ +// Copyright (C) 2026-present The NetCalyx Authors. // Copyright (C) 2025-present The NetGauze Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -2598,6 +2599,33 @@ mod tests { Ok(()) } + /// A ``/`` reply whose filter matched nothing comes back + /// as a self-closing ``. This must parse as empty data, not fail + /// with `ParsingError::Eof` from scanning past `` for a + /// non-existent `` end tag. + #[test] + fn test_rpc_reply_empty_data() -> Result<(), ParsingError> { + let expected = RpcReply { + message_id: Some("215".into()), + reply: RpcReplyContent::ErrorsAndData { + errors: vec![], + responses: RpcResponse::WellKnown(WellKnownRpcResponse::Data("".into())), + }, + }; + + // self-closing form + test_xml_value( + r#""#, + expected.clone(), + )?; + // explicit open/close form + test_xml_value( + r#""#, + expected, + )?; + Ok(()) + } + #[test] fn test_rpc_reply_content() { let data = RpcResponse::Raw("SomeData".into()); diff --git a/crates/netconf-proto/src/xml_utils.rs b/crates/netconf-proto/src/xml_utils.rs index 8646944b..b6a066b1 100644 --- a/crates/netconf-proto/src/xml_utils.rs +++ b/crates/netconf-proto/src/xml_utils.rs @@ -1,10 +1,11 @@ +// Copyright (C) 2026-present The NetCalyx Authors. // Copyright (C) 2025-present The NetGauze Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -575,6 +576,12 @@ impl<'a, R: io::BufRead> XmlParser<'a, R> { /// - The namespace resolution only occurs once (on the first start tag) to /// minimize overhead pub fn copy_buffer_till(&mut self, tag: &'_ [u8]) -> Result, ParsingError> { + // A self-closing container (e.g. ``) has no children and thus no + // matching end tag to copy up to; scanning would run to EOF. Mirror the + // guard used by `collect_xml_sequence` and return empty content. + if !self.parent_has_child() { + return Ok("".into()); + } let cursor = io::Cursor::new(vec![]); let mut writer = quick_xml::writer::Writer::new(cursor); let mut wrote_ns = false; @@ -637,6 +644,13 @@ impl<'a, R: io::BufRead> XmlParser<'a, R> { &mut self, tag: &'_ [u8], ) -> Result { + // Empty/self-closing container: no children, no matching end tag. + if !self.parent_has_child() { + return Ok(CopiedSubtree { + xml: "".into(), + namespaces: IndexMap::new(), + }); + } let mut writer = quick_xml::writer::Writer::new(io::Cursor::new(Vec::new())); let mut namespaces: IndexMap = IndexMap::new(); @@ -920,6 +934,41 @@ mod tests { assert_eq!(parser_empty.peek(), &Event::Eof); } + /// A self-closing container must not make `copy_buffer_till` scan to EOF. + #[test] + fn test_copy_buffer_till_empty_element() { + // self-closing + let mut parser = create_parser(r#""#); + parser.open(None, "data").expect("open "); + assert_eq!(parser.copy_buffer_till(b"data"), Ok("".into())); + parser.close().expect("close "); + + // explicit empty + let mut parser = create_parser(r#""#); + parser.open(None, "data").expect("open "); + assert_eq!(parser.copy_buffer_till(b"data"), Ok("".into())); + parser.close().expect("close "); + + // non-empty still copies content + let mut parser = create_parser(r#"1"#); + parser.open(None, "data").expect("open "); + let copied = parser.copy_buffer_till(b"data").expect("copy content"); + assert!(copied.contains("1"), "got `{copied}`"); + } + + /// Same guard for the namespace-aware variant. + #[test] + fn test_copy_buffer_till_with_namespaces_empty_element() { + let mut parser = create_parser(r#""#); + parser.open(None, "filter").expect("open "); + let subtree = parser + .copy_buffer_till_with_namespaces(b"filter") + .expect("copy empty"); + assert_eq!(&*subtree.xml, ""); + assert!(subtree.namespaces.is_empty()); + parser.close().expect("close "); + } + #[test] fn test_skip_top_elements() { let xml = r#"text123"#;