Skip to main content

rusty_xml_sax/
lib.rs

1//! SAX2 events and an xmllint `--sax` debug dump.
2//!
3//! Structured equality is the gate. The debug printer exists so a line-for-line
4//! diff against pinned `xmllint --sax` is possible; C's `%.4s` attribute quirk
5//! (reads past the value into the input) is reproduced when `value_input_off` is set.
6
7#![forbid(unsafe_code)]
8
9/// One SAX2 callback as recorded for the event-exact gate.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum SaxEvent {
12    SetDocumentLocator,
13    StartDocument,
14    EndDocument,
15    StartElementNs {
16        local: String,
17        prefix: Option<String>,
18        uri: Option<String>,
19        namespaces: Vec<(Option<String>, String)>,
20        attributes: Vec<SaxAttr>,
21        nb_defaulted: i32,
22    },
23    EndElementNs {
24        local: String,
25        prefix: Option<String>,
26        uri: Option<String>,
27    },
28    Characters {
29        data: String,
30    },
31    CData {
32        data: String,
33    },
34    Comment(String),
35    ProcessingInstruction {
36        target: String,
37        data: Option<String>,
38    },
39    Warning(String),
40    Error(String),
41}
42
43/// Attribute as delivered to `startElementNs`.
44#[derive(Clone, Debug)]
45pub struct SaxAttr {
46    pub local: String,
47    pub prefix: Option<String>,
48    pub uri: Option<String>,
49    pub value: String,
50    /// Byte offset in the original input of the first value character (after the quote).
51    /// Used only to reproduce xmllint's `%.4s` debug print.
52    pub value_input_off: Option<usize>,
53}
54
55impl PartialEq for SaxAttr {
56    fn eq(&self, other: &Self) -> bool {
57        self.local == other.local
58            && self.prefix == other.prefix
59            && self.uri == other.uri
60            && self.value == other.value
61    }
62}
63
64impl Eq for SaxAttr {}
65
66/// SAX2 handler. Default methods are no-ops so a recorder can override a subset.
67pub trait SaxHandler {
68    fn set_document_locator(&mut self) {}
69    fn start_document(&mut self) {}
70    fn end_document(&mut self) {}
71    fn start_element_ns(
72        &mut self,
73        local: &str,
74        prefix: Option<&str>,
75        uri: Option<&str>,
76        namespaces: &[(Option<String>, String)],
77        attributes: &[SaxAttr],
78        nb_defaulted: i32,
79    ) {
80        let _ = (local, prefix, uri, namespaces, attributes, nb_defaulted);
81    }
82    fn end_element_ns(&mut self, local: &str, prefix: Option<&str>, uri: Option<&str>) {
83        let _ = (local, prefix, uri);
84    }
85    fn characters(&mut self, data: &str) {
86        let _ = data;
87    }
88    fn cdata_block(&mut self, data: &str) {
89        let _ = data;
90    }
91    fn comment(&mut self, data: &str) {
92        let _ = data;
93    }
94    fn processing_instruction(&mut self, target: &str, data: Option<&str>) {
95        let _ = (target, data);
96    }
97    fn warning(&mut self, msg: &str) {
98        let _ = msg;
99    }
100    fn error(&mut self, msg: &str) {
101        let _ = msg;
102    }
103}
104
105/// A handler that discards every callback, using the trait's default bodies.
106///
107/// Building a tree does not need the SAX event stream, but the tree entry
108/// points used [`SaxRecorder`], which deep-copies the local name, prefix, URI,
109/// namespace list and *every attribute* of every element into a log that is
110/// then dropped. On a 627 KB document that was over half of all allocations.
111#[derive(Clone, Copy, Debug, Default)]
112pub struct NullSax;
113
114impl SaxHandler for NullSax {}
115
116/// Records every callback for the event-exact gate.
117#[derive(Clone, Debug, Default)]
118pub struct SaxRecorder {
119    pub events: Vec<SaxEvent>,
120}
121
122impl SaxHandler for SaxRecorder {
123    fn set_document_locator(&mut self) {
124        self.events.push(SaxEvent::SetDocumentLocator);
125    }
126    fn start_document(&mut self) {
127        self.events.push(SaxEvent::StartDocument);
128    }
129    fn end_document(&mut self) {
130        self.events.push(SaxEvent::EndDocument);
131    }
132    fn start_element_ns(
133        &mut self,
134        local: &str,
135        prefix: Option<&str>,
136        uri: Option<&str>,
137        namespaces: &[(Option<String>, String)],
138        attributes: &[SaxAttr],
139        nb_defaulted: i32,
140    ) {
141        self.events.push(SaxEvent::StartElementNs {
142            local: local.to_string(),
143            prefix: prefix.map(str::to_string),
144            uri: uri.map(str::to_string),
145            namespaces: namespaces.to_vec(),
146            attributes: attributes.to_vec(),
147            nb_defaulted,
148        });
149    }
150    fn end_element_ns(&mut self, local: &str, prefix: Option<&str>, uri: Option<&str>) {
151        self.events.push(SaxEvent::EndElementNs {
152            local: local.to_string(),
153            prefix: prefix.map(str::to_string),
154            uri: uri.map(str::to_string),
155        });
156    }
157    fn characters(&mut self, data: &str) {
158        self.events.push(SaxEvent::Characters {
159            data: data.to_string(),
160        });
161    }
162    fn cdata_block(&mut self, data: &str) {
163        self.events.push(SaxEvent::CData {
164            data: data.to_string(),
165        });
166    }
167    fn comment(&mut self, data: &str) {
168        self.events.push(SaxEvent::Comment(data.to_string()));
169    }
170    fn processing_instruction(&mut self, target: &str, data: Option<&str>) {
171        self.events.push(SaxEvent::ProcessingInstruction {
172            target: target.to_string(),
173            data: data.map(str::to_string),
174        });
175    }
176    fn warning(&mut self, msg: &str) {
177        self.events.push(SaxEvent::Warning(msg.to_string()));
178    }
179    fn error(&mut self, msg: &str) {
180        self.events.push(SaxEvent::Error(msg.to_string()));
181    }
182}
183
184impl SaxRecorder {
185    pub fn new() -> Self {
186        Self::default()
187    }
188
189    /// Line-for-line dump matching `oracle/src/xmllint.c` debug SAX handlers.
190    pub fn to_xmllint_debug(&self, input: &[u8]) -> String {
191        let mut out = String::new();
192        for ev in &self.events {
193            out.push_str(&event_to_xmllint_debug(ev, input));
194        }
195        out
196    }
197}
198
199fn trunc_bytes(s: &str, n: usize) -> String {
200    let b = s.as_bytes();
201    let k = b.len().min(n);
202    String::from_utf8_lossy(&b[..k]).into_owned()
203}
204
205fn opt_name(p: &Option<String>) -> String {
206    match p {
207        None => "NULL".into(),
208        Some(s) => s.clone(),
209    }
210}
211
212fn opt_uri(u: &Option<String>) -> String {
213    match u {
214        None => "NULL".into(),
215        Some(s) => format!("'{s}'"),
216    }
217}
218
219/// Format one event the way pinned xmllint `--sax` prints it.
220pub fn event_to_xmllint_debug(ev: &SaxEvent, input: &[u8]) -> String {
221    match ev {
222        SaxEvent::SetDocumentLocator => "SAX.setDocumentLocator()\n".into(),
223        SaxEvent::StartDocument => "SAX.startDocument()\n".into(),
224        SaxEvent::EndDocument => "SAX.endDocument()\n".into(),
225        SaxEvent::StartElementNs {
226            local,
227            prefix,
228            uri,
229            namespaces,
230            attributes,
231            nb_defaulted,
232        } => {
233            let mut s = format!(
234                "SAX.startElementNs({local}, {}, {}, {}",
235                opt_name(prefix),
236                opt_uri(uri),
237                namespaces.len()
238            );
239            for (pre, href) in namespaces {
240                s.push_str(", xmlns");
241                if let Some(p) = pre {
242                    s.push(':');
243                    s.push_str(p);
244                }
245                s.push_str(&format!("='{href}'"));
246            }
247            s.push_str(&format!(", {}, {nb_defaulted}", attributes.len()));
248            for a in attributes {
249                if let Some(p) = &a.prefix {
250                    s.push_str(&format!(", {p}:{}='", a.local));
251                } else {
252                    s.push_str(&format!(", {}='", a.local));
253                }
254                let four = if let Some(off) = a.value_input_off {
255                    let end = (off + 4).min(input.len());
256                    if off < input.len() {
257                        String::from_utf8_lossy(&input[off..end]).into_owned()
258                    } else {
259                        trunc_bytes(&a.value, 4)
260                    }
261                } else {
262                    trunc_bytes(&a.value, 4)
263                };
264                s.push_str(&format!("{four}...', {}", a.value.len()));
265            }
266            s.push_str(")\n");
267            s
268        }
269        SaxEvent::EndElementNs {
270            local,
271            prefix,
272            uri,
273        } => {
274            if uri.is_none() {
275                format!("SAX.endElementNs({local}, {}, NULL)\n", opt_name(prefix))
276            } else {
277                format!(
278                    "SAX.endElementNs({local}, {}, {})\n",
279                    opt_name(prefix),
280                    opt_uri(uri)
281                )
282            }
283        }
284        SaxEvent::Characters { data } => {
285            format!("SAX.characters({}, {})\n", trunc_bytes(data, 30), data.len())
286        }
287        SaxEvent::CData { data } => {
288            format!("SAX.pcdata({}, {})\n", trunc_bytes(data, 20), data.len())
289        }
290        SaxEvent::Comment(c) => format!("SAX.comment({c})\n"),
291        SaxEvent::ProcessingInstruction { target, data } => match data {
292            Some(d) => format!("SAX.processingInstruction({target}, {d})\n"),
293            None => format!("SAX.processingInstruction({target}, NULL)\n"),
294        },
295        SaxEvent::Warning(m) => format!("SAX.warning: {m}"),
296        SaxEvent::Error(m) => format!("SAX.error: {m}"),
297    }
298}
299
300/// `xmlSAX2InitDefaultSAXHandler` is a no-op beyond constructing a recorder.
301#[doc(alias = "xmlSAX2InitDefaultSAXHandler")]
302pub fn xml_sax2_init_default_sax_handler() -> SaxRecorder {
303    SaxRecorder::new()
304}
305
306/// `xmlSAXVersion` — we speak SAX2.
307#[doc(alias = "xmlSAXVersion")]
308pub fn xml_sax_version() -> i32 {
309    2
310}