Skip to main content

rusty_xml_valid/
c14n.rs

1//! Canonical XML 1.0 / exclusive C14N. Byte-identity vs `xmllint --c14n`.
2
3use std::rc::Rc;
4use rusty_xml_tree::{NodeId, NodeKind, XmlDoc};
5use std::cmp::Ordering;
6
7/// libxml2 `xmlC14NMode`.
8pub const XML_C14N_1_0: i32 = 0;
9pub const XML_C14N_EXCLUSIVE_1_0: i32 = 1;
10pub const XML_C14N_1_1: i32 = 2;
11
12const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
13
14/// `xmlC14NDocDumpMemory`.
15#[doc(alias = "xmlC14NDocDumpMemory")]
16pub fn xml_c14n_doc_dump_memory(
17    doc: &XmlDoc,
18    exclusive: bool,
19    with_comments: bool,
20) -> Result<Vec<u8>, String> {
21    let mut out = String::new();
22    emit_node(doc, NodeId::DOCUMENT, exclusive, with_comments, &[], &[], &mut out)?;
23    Ok(out.into_bytes())
24}
25
26/// Inclusive C14N 1.0 without comments.
27pub fn xml_c14n_1_0(doc: &XmlDoc) -> Result<Vec<u8>, String> {
28    xml_c14n_doc_dump_memory(doc, false, false)
29}
30
31/// Exclusive C14N 1.0 without comments.
32pub fn xml_exc_c14n_1_0(doc: &XmlDoc) -> Result<Vec<u8>, String> {
33    xml_c14n_doc_dump_memory(doc, true, false)
34}
35
36/// One unit of canonicalization work, replacing recursion into children.
37///
38/// This used to recurse, carrying the inherited namespace rendering down with
39/// it, so depth cost stack: a 2000-deep document ABORTED THE PROCESS while
40/// being canonicalized, inside a parser limit of 5000. It was bounded at 400
41/// as a stopgap; the bound is gone now because the traversal is.
42///
43/// The inherited set is shared by `Rc` rather than cloned per child, which is
44/// what made the recursive form expensive in the first place.
45enum Step {
46    /// Emit this element's start tag, then its children and end tag.
47    Open(NodeId, Rc<Vec<(String, String)>>),
48    /// A leaf: text, CDATA, a PI or a comment.
49    Inline(NodeId),
50    /// Emit the end tag of an element whose children are done.
51    Close(String),
52}
53
54fn emit_node(
55    doc: &XmlDoc,
56    id: NodeId,
57    exclusive: bool,
58    with_comments: bool,
59    vis_prefixes: &[&str],
60    rendered: &[(String, String)],
61    out: &mut String,
62) -> Result<(), String> {
63    match doc.kind(id) {
64        NodeKind::Document | NodeKind::HtmlDocument => {
65            let mut kids = Vec::new();
66            let mut c = doc.first_child(id);
67            while let Some(x) = c {
68                kids.push(x);
69                c = doc.next_sibling(x);
70            }
71            let mut after_element = false;
72            let mut before_element = true;
73            let mut seen_pi_or_comment = false;
74            for kid in &kids {
75                match doc.kind(*kid) {
76                    NodeKind::Element => {
77                        before_element = false;
78                        emit_node(doc, *kid, exclusive, with_comments, vis_prefixes, rendered, out)?;
79                        after_element = true;
80                    }
81                    NodeKind::Pi => {
82                        if after_element || seen_pi_or_comment {
83                            out.push('\n');
84                        }
85                        emit_pi(doc, *kid, out);
86                        if before_element {
87                            out.push('\n');
88                        }
89                        seen_pi_or_comment = true;
90                    }
91                    NodeKind::Comment if with_comments => {
92                        if after_element || seen_pi_or_comment {
93                            out.push('\n');
94                        }
95                        emit_comment(doc, *kid, out);
96                        if before_element {
97                            out.push('\n');
98                        }
99                        seen_pi_or_comment = true;
100                    }
101                    _ => {}
102                }
103            }
104        }
105        NodeKind::Element => {
106            emit_element(doc, id, exclusive, with_comments, vis_prefixes, rendered, out)?
107        }
108        NodeKind::Text => out.push_str(&escape_text(doc.content(id))),
109        NodeKind::CData => out.push_str(&escape_text(doc.content(id))),
110        NodeKind::Pi => emit_pi(doc, id, out),
111        NodeKind::Comment if with_comments => emit_comment(doc, id, out),
112        _ => {}
113    }
114    Ok(())
115}
116
117fn emit_pi(doc: &XmlDoc, id: NodeId, out: &mut String) {
118    out.push_str("<?");
119    out.push_str(doc.name(id));
120    let data = doc.content(id);
121    if !data.is_empty() {
122        out.push(' ');
123        out.push_str(data);
124    }
125    out.push_str("?>");
126}
127
128fn emit_comment(doc: &XmlDoc, id: NodeId, out: &mut String) {
129    out.push_str("<!--");
130    out.push_str(doc.content(id));
131    out.push_str("-->");
132}
133
134fn emit_element(
135    doc: &XmlDoc,
136    id: NodeId,
137    exclusive: bool,
138    with_comments: bool,
139    vis_prefixes: &[&str],
140    rendered: &[(String, String)],
141    out: &mut String,
142) -> Result<(), String> {
143    let mut stack: Vec<Step> = vec![Step::Open(id, Rc::new(rendered.to_vec()))];
144    while let Some(step) = stack.pop() {
145        let (id, rendered) = match step {
146            Step::Close(qn) => {
147                out.push_str("</");
148                out.push_str(&qn);
149                out.push('>');
150                continue;
151            }
152            // Text, CDATA, PIs and comments have no children, so they never
153            // needed a frame; emit them where they stand.
154            Step::Inline(id) => {
155                match doc.kind(id) {
156                    NodeKind::Text | NodeKind::CData => {
157                        out.push_str(&escape_text(doc.content(id)))
158                    }
159                    NodeKind::Pi => emit_pi(doc, id, out),
160                    NodeKind::Comment if with_comments => emit_comment(doc, id, out),
161                    _ => {}
162                }
163                continue;
164            }
165            Step::Open(id, rendered) => (id, rendered),
166        };
167
168        let qn = qname(doc.prefix(id), doc.name(id));
169        out.push('<');
170        out.push_str(&qn);
171
172        let mut ns_attrs = namespaces_to_emit(doc, id, exclusive, vis_prefixes, &rendered);
173        ns_attrs.sort_by(|a, b| a.0.cmp(&b.0));
174        for (pre, href) in &ns_attrs {
175            out.push(' ');
176            if pre.is_empty() {
177                out.push_str("xmlns");
178            } else {
179                out.push_str("xmlns:");
180                out.push_str(pre);
181            }
182            out.push_str("=\"");
183            out.push_str(&escape_attr(href));
184            out.push('"');
185        }
186
187        // Every child of this element shares one inherited set, so it is
188        // built once and shared by pointer rather than cloned per child.
189        let child_rendered = if ns_attrs.is_empty() {
190            Rc::clone(&rendered)
191        } else {
192            let mut v: Vec<(String, String)> = rendered.as_ref().clone();
193            for (pre, href) in &ns_attrs {
194                v.retain(|(p, _)| p != pre);
195                v.push((pre.clone(), href.clone()));
196            }
197            Rc::new(v)
198        };
199
200        let mut attrs: Vec<(String, String, String)> = Vec::new();
201        let mut a = doc.first_attr(id);
202        while let Some(x) = a {
203            let ns = doc.ns_uri(x).unwrap_or("").to_string();
204            attrs.push((ns, doc.qname(x), doc.content(x).to_string()));
205            a = doc.next_sibling(x);
206        }
207        attrs.sort_by(|a, b| cmp_attr(&a.0, &a.1, &b.0, &b.1));
208        for (_ns, name, val) in attrs {
209            out.push(' ');
210            out.push_str(&name);
211            out.push_str("=\"");
212            out.push_str(&escape_attr(&val));
213            out.push('"');
214        }
215        out.push('>');
216
217        // Pushed in reverse so they pop in document order.
218        stack.push(Step::Close(qn));
219        let mut kids: Vec<NodeId> = Vec::new();
220        let mut c = doc.first_child(id);
221        while let Some(x) = c {
222            kids.push(x);
223            c = doc.next_sibling(x);
224        }
225        for x in kids.into_iter().rev() {
226            if doc.kind(x) == NodeKind::Element {
227                stack.push(Step::Open(x, Rc::clone(&child_rendered)));
228            } else {
229                stack.push(Step::Inline(x));
230            }
231        }
232    }
233    Ok(())
234}
235
236fn qname(prefix: Option<&str>, local: &str) -> String {
237    match prefix {
238        Some(p) if !p.is_empty() => format!("{p}:{local}"),
239        _ => local.to_string(),
240    }
241}
242
243fn in_scope_ns(doc: &XmlDoc, id: NodeId) -> Vec<(String, String)> {
244    let mut map: Vec<(String, String)> = Vec::new();
245    let mut cur = Some(id);
246    while let Some(n) = cur {
247        for (pre, href) in doc.ns_defs(n).iter().rev() {
248            let key = pre.clone().unwrap_or_default();
249            if !map.iter().any(|(k, _)| k == &key) {
250                map.push((key, href.clone()));
251            }
252        }
253        cur = doc.parent(n);
254        if cur == Some(NodeId::DOCUMENT) {
255            break;
256        }
257    }
258    map
259}
260
261fn namespaces_to_emit(
262    doc: &XmlDoc,
263    id: NodeId,
264    exclusive: bool,
265    vis_prefixes: &[&str],
266    rendered: &[(String, String)],
267) -> Vec<(String, String)> {
268    let mut map = in_scope_ns(doc, id);
269    map.retain(|(pre, href)| {
270        if pre == "xml" && href == XML_NS {
271            return false;
272        }
273        if exclusive {
274            if !visibly_used(doc, id, pre, vis_prefixes) {
275                return false;
276            }
277            if pre.is_empty() && href.is_empty() {
278                return rendered.iter().any(|(p, h)| p.is_empty() && !h.is_empty());
279            }
280            return !rendered.iter().any(|(p, h)| p == pre && h == href);
281        }
282        if rendered.iter().any(|(p, h)| p == pre && h == href) {
283            return false;
284        }
285        if pre.is_empty() && href.is_empty() {
286            return rendered.iter().any(|(p, h)| p.is_empty() && !h.is_empty());
287        }
288        true
289    });
290    map
291}
292
293fn visibly_used(doc: &XmlDoc, id: NodeId, pre: &str, extra: &[&str]) -> bool {
294    if extra.contains(&pre) {
295        return true;
296    }
297    if pre.is_empty() {
298        return doc.prefix(id).is_none();
299    }
300    if doc.prefix(id) == Some(pre) {
301        return true;
302    }
303    let mut a = doc.first_attr(id);
304    while let Some(x) = a {
305        if doc.prefix(x) == Some(pre) {
306            return true;
307        }
308        a = doc.next_sibling(x);
309    }
310    false
311}
312
313fn cmp_attr(ans: &str, an: &str, bns: &str, bn: &str) -> Ordering {
314    let au = ans;
315    let bu = bns;
316    match (au.is_empty(), bu.is_empty()) {
317        (true, false) => Ordering::Less,
318        (false, true) => Ordering::Greater,
319        _ => au.cmp(bu).then_with(|| {
320            let al = an.rsplit(':').next().unwrap_or(an);
321            let bl = bn.rsplit(':').next().unwrap_or(bn);
322            al.cmp(bl)
323        }),
324    }
325}
326
327fn escape_text(s: &str) -> String {
328    let mut out = String::new();
329    for c in s.chars() {
330        match c {
331            '&' => out.push_str("&amp;"),
332            '<' => out.push_str("&lt;"),
333            '>' => out.push_str("&gt;"),
334            '\r' => out.push_str("&#xD;"),
335            c => out.push(c),
336        }
337    }
338    out
339}
340
341fn escape_attr(s: &str) -> String {
342    let mut out = String::new();
343    for c in s.chars() {
344        match c {
345            '&' => out.push_str("&amp;"),
346            '<' => out.push_str("&lt;"),
347            '"' => out.push_str("&quot;"),
348            '\t' => out.push_str("&#x9;"),
349            '\n' => out.push_str("&#xA;"),
350            '\r' => out.push_str("&#xD;"),
351            c => out.push(c),
352        }
353    }
354    out
355}