Skip to main content

oxml_core/
raw_xml.rs

1//! Raw XML capture utilities for preserving unknown elements during round-trip.
2
3use quick_xml::events::{BytesStart, Event};
4use quick_xml::{Reader, Writer};
5
6use crate::error::Result;
7
8/// Capture a full XML subtree (from after the start tag through the matching end tag)
9/// and return it as raw bytes. The returned bytes include the start tag, all children,
10/// and the end tag.
11pub fn capture_element(reader: &mut Reader<&[u8]>, start: &BytesStart) -> Result<Vec<u8>> {
12    let mut writer = Writer::new(Vec::new());
13
14    // Write the start tag
15    writer.write_event(Event::Start(start.to_owned()))?;
16
17    // Track nesting depth for the tag name
18    let tag_name = start.name().as_ref().to_vec();
19    let mut depth = 1u32;
20    let mut buf = Vec::new();
21
22    loop {
23        match reader.read_event_into(&mut buf) {
24            Ok(Event::Start(ref e)) => {
25                if e.name().as_ref() == tag_name {
26                    depth += 1;
27                }
28                writer.write_event(Event::Start(e.to_owned()))?;
29            }
30            Ok(Event::End(ref e)) => {
31                if e.name().as_ref() == tag_name {
32                    depth -= 1;
33                }
34                writer.write_event(Event::End(e.to_owned()))?;
35                if depth == 0 {
36                    break;
37                }
38            }
39            Ok(Event::Empty(ref e)) => {
40                writer.write_event(Event::Empty(e.to_owned()))?;
41            }
42            Ok(Event::Text(ref e)) => {
43                writer.write_event(Event::Text(e.to_owned().into_owned()))?;
44            }
45            Ok(Event::CData(ref e)) => {
46                writer.write_event(Event::CData(e.to_owned().into_owned()))?;
47            }
48            Ok(Event::Comment(ref e)) => {
49                writer.write_event(Event::Comment(e.to_owned().into_owned()))?;
50            }
51            Ok(Event::PI(ref e)) => {
52                writer.write_event(Event::PI(e.to_owned().into_owned()))?;
53            }
54            Ok(Event::Decl(ref e)) => {
55                writer.write_event(Event::Decl(e.to_owned().into_owned()))?;
56            }
57            Ok(Event::DocType(ref e)) => {
58                writer.write_event(Event::DocType(e.to_owned().into_owned()))?;
59            }
60            // Entity references are their own event since quick-xml 0.41;
61            // re-emit them verbatim so captured markup round-trips unchanged.
62            Ok(Event::GeneralRef(ref e)) => {
63                writer.write_event(Event::GeneralRef(e.to_owned().into_owned()))?;
64            }
65            Ok(Event::Eof) => break,
66            Err(e) => return Err(e.into()),
67        }
68        buf.clear();
69    }
70
71    Ok(writer.into_inner())
72}
73
74/// Capture an empty (self-closing) element as raw bytes.
75pub fn capture_empty_element(e: &BytesStart) -> Result<Vec<u8>> {
76    let mut writer = Writer::new(Vec::new());
77    writer.write_event(Event::Empty(e.to_owned()))?;
78    Ok(writer.into_inner())
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn capture_simple_element() {
87        let xml = r#"<root><child>text</child><sibling/></root>"#;
88        let mut reader = Reader::from_str(xml);
89        reader.config_mut().trim_text(true);
90        let mut buf = Vec::new();
91
92        // Read past <root>
93        loop {
94            match reader.read_event_into(&mut buf) {
95                Ok(Event::Start(ref e)) if e.name().as_ref() == b"child" => {
96                    let captured = capture_element(&mut reader, e).unwrap();
97                    let s = String::from_utf8(captured).unwrap();
98                    assert!(s.contains("<child>"));
99                    assert!(s.contains("text"));
100                    assert!(s.contains("</child>"));
101                    return;
102                }
103                _ => {}
104            }
105            buf.clear();
106        }
107    }
108
109    #[test]
110    fn capture_empty() {
111        let xml = r#"<item attr="val"/>"#;
112        let mut reader = Reader::from_str(xml);
113        reader.config_mut().trim_text(true);
114        let mut buf = Vec::new();
115
116        loop {
117            if let Ok(Event::Empty(ref e)) = reader.read_event_into(&mut buf) {
118                let captured = capture_empty_element(e).unwrap();
119                let s = String::from_utf8(captured).unwrap();
120                assert!(s.contains("item"));
121                assert!(s.contains("attr"));
122                return;
123            }
124            buf.clear();
125        }
126    }
127
128    #[test]
129    fn capture_nested_element() {
130        let xml = r#"<outer><inner><deep>data</deep></inner></outer>"#;
131        let mut reader = Reader::from_str(xml);
132        reader.config_mut().trim_text(true);
133        let mut buf = Vec::new();
134
135        loop {
136            match reader.read_event_into(&mut buf) {
137                Ok(Event::Start(ref e)) if e.name().as_ref() == b"outer" => {
138                    let captured = capture_element(&mut reader, e).unwrap();
139                    let s = String::from_utf8(captured).unwrap();
140                    assert!(s.contains("<outer>"));
141                    assert!(s.contains("<inner>"));
142                    assert!(s.contains("<deep>"));
143                    assert!(s.contains("data"));
144                    assert!(s.contains("</deep>"));
145                    assert!(s.contains("</inner>"));
146                    assert!(s.contains("</outer>"));
147                    return;
148                }
149                _ => {}
150            }
151            buf.clear();
152        }
153    }
154
155    #[test]
156    fn unmodelled_subtree_is_preserved_byte_for_byte() {
157        let xml = br#"<root><x:item x:id="7"><x:child>one &amp; two</x:child><!--note--></x:item></root>"#;
158        let mut reader = Reader::from_reader(xml.as_slice());
159        let mut buf = Vec::new();
160
161        loop {
162            match reader.read_event_into(&mut buf) {
163                Ok(Event::Start(ref e)) if e.name().as_ref() == b"x:item" => {
164                    assert_eq!(
165                        capture_element(&mut reader, e).unwrap(),
166                        br#"<x:item x:id="7"><x:child>one &amp; two</x:child><!--note--></x:item>"#
167                    );
168                    return;
169                }
170                Ok(Event::Eof) => panic!("no unmodelled subtree"),
171                _ => {}
172            }
173            buf.clear();
174        }
175    }
176}