Skip to main content

rusty_xml_valid/
c14n.rs

1//! Canonical XML 1.0 / exclusive C14N. Byte-identity vs `xmllint --c14n`.
2
3use rusty_xml_tree::{NodeId, NodeKind, XmlDoc};
4use std::cmp::Ordering;
5
6/// libxml2 `xmlC14NMode`.
7pub const XML_C14N_1_0: i32 = 0;
8pub const XML_C14N_EXCLUSIVE_1_0: i32 = 1;
9pub const XML_C14N_1_1: i32 = 2;
10
11const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
12
13/// `xmlC14NDocDumpMemory`.
14#[doc(alias = "xmlC14NDocDumpMemory")]
15pub fn xml_c14n_doc_dump_memory(
16    doc: &XmlDoc,
17    exclusive: bool,
18    with_comments: bool,
19) -> Result<Vec<u8>, String> {
20    let mut out = String::new();
21    emit_node(doc, NodeId::DOCUMENT, exclusive, with_comments, &[], &[], &mut out);
22    Ok(out.into_bytes())
23}
24
25/// Inclusive C14N 1.0 without comments.
26pub fn xml_c14n_1_0(doc: &XmlDoc) -> Result<Vec<u8>, String> {
27    xml_c14n_doc_dump_memory(doc, false, false)
28}
29
30/// Exclusive C14N 1.0 without comments.
31pub fn xml_exc_c14n_1_0(doc: &XmlDoc) -> Result<Vec<u8>, String> {
32    xml_c14n_doc_dump_memory(doc, true, false)
33}
34
35fn emit_node(
36    doc: &XmlDoc,
37    id: NodeId,
38    exclusive: bool,
39    with_comments: bool,
40    vis_prefixes: &[&str],
41    rendered: &[(String, String)],
42    out: &mut String,
43) {
44    match doc.kind(id) {
45        NodeKind::Document | NodeKind::HtmlDocument => {
46            let mut kids = Vec::new();
47            let mut c = doc.first_child(id);
48            while let Some(x) = c {
49                kids.push(x);
50                c = doc.next_sibling(x);
51            }
52            let mut after_element = false;
53            let mut before_element = true;
54            let mut seen_pi_or_comment = false;
55            for kid in &kids {
56                match doc.kind(*kid) {
57                    NodeKind::Element => {
58                        before_element = false;
59                        emit_node(doc, *kid, exclusive, with_comments, vis_prefixes, rendered, out);
60                        after_element = true;
61                    }
62                    NodeKind::Pi => {
63                        if after_element || seen_pi_or_comment {
64                            out.push('\n');
65                        }
66                        emit_pi(doc, *kid, out);
67                        if before_element {
68                            out.push('\n');
69                        }
70                        seen_pi_or_comment = true;
71                    }
72                    NodeKind::Comment if with_comments => {
73                        if after_element || seen_pi_or_comment {
74                            out.push('\n');
75                        }
76                        emit_comment(doc, *kid, out);
77                        if before_element {
78                            out.push('\n');
79                        }
80                        seen_pi_or_comment = true;
81                    }
82                    _ => {}
83                }
84            }
85        }
86        NodeKind::Element => emit_element(doc, id, exclusive, with_comments, vis_prefixes, rendered, out),
87        NodeKind::Text => out.push_str(&escape_text(doc.content(id))),
88        NodeKind::CData => out.push_str(&escape_text(doc.content(id))),
89        NodeKind::Pi => emit_pi(doc, id, out),
90        NodeKind::Comment if with_comments => emit_comment(doc, id, out),
91        _ => {}
92    }
93}
94
95fn emit_pi(doc: &XmlDoc, id: NodeId, out: &mut String) {
96    out.push_str("<?");
97    out.push_str(doc.name(id));
98    let data = doc.content(id);
99    if !data.is_empty() {
100        out.push(' ');
101        out.push_str(data);
102    }
103    out.push_str("?>");
104}
105
106fn emit_comment(doc: &XmlDoc, id: NodeId, out: &mut String) {
107    out.push_str("<!--");
108    out.push_str(doc.content(id));
109    out.push_str("-->");
110}
111
112fn emit_element(
113    doc: &XmlDoc,
114    id: NodeId,
115    exclusive: bool,
116    with_comments: bool,
117    vis_prefixes: &[&str],
118    rendered: &[(String, String)],
119    out: &mut String,
120) {
121    let qn = qname(doc.prefix(id), doc.name(id));
122    out.push('<');
123    out.push_str(&qn);
124
125    let mut ns_attrs = namespaces_to_emit(doc, id, exclusive, vis_prefixes, rendered);
126    ns_attrs.sort_by(|a, b| a.0.cmp(&b.0));
127    for (pre, href) in &ns_attrs {
128        out.push(' ');
129        if pre.is_empty() {
130            out.push_str("xmlns");
131        } else {
132            out.push_str("xmlns:");
133            out.push_str(pre);
134        }
135        out.push_str("=\"");
136        out.push_str(&escape_attr(href));
137        out.push('"');
138    }
139
140    let mut child_rendered: Vec<(String, String)> = rendered.to_vec();
141    for (pre, href) in &ns_attrs {
142        child_rendered.retain(|(p, _)| p != pre);
143        child_rendered.push((pre.clone(), href.clone()));
144    }
145
146    let mut attrs: Vec<(String, String, String)> = Vec::new();
147    let mut a = doc.first_attr(id);
148    while let Some(x) = a {
149        let ns = doc.ns_uri(x).unwrap_or("").to_string();
150        let local = doc.name(x).to_string();
151        let qn = doc.qname(x);
152        attrs.push((ns, qn, doc.content(x).to_string()));
153        let _ = local;
154        a = doc.next_sibling(x);
155    }
156    attrs.sort_by(|a, b| cmp_attr(&a.0, &a.1, &b.0, &b.1));
157    for (_ns, name, val) in attrs {
158        out.push(' ');
159        out.push_str(&name);
160        out.push_str("=\"");
161        out.push_str(&escape_attr(&val));
162        out.push('"');
163    }
164
165    out.push('>');
166    let mut c = doc.first_child(id);
167    while let Some(x) = c {
168        emit_node(doc, x, exclusive, with_comments, vis_prefixes, &child_rendered, out);
169        c = doc.next_sibling(x);
170    }
171    out.push_str("</");
172    out.push_str(&qn);
173    out.push('>');
174}
175
176fn qname(prefix: Option<&str>, local: &str) -> String {
177    match prefix {
178        Some(p) if !p.is_empty() => format!("{p}:{local}"),
179        _ => local.to_string(),
180    }
181}
182
183fn in_scope_ns(doc: &XmlDoc, id: NodeId) -> Vec<(String, String)> {
184    let mut map: Vec<(String, String)> = Vec::new();
185    let mut cur = Some(id);
186    while let Some(n) = cur {
187        for (pre, href) in doc.ns_defs(n).iter().rev() {
188            let key = pre.clone().unwrap_or_default();
189            if !map.iter().any(|(k, _)| k == &key) {
190                map.push((key, href.clone()));
191            }
192        }
193        cur = doc.parent(n);
194        if cur == Some(NodeId::DOCUMENT) {
195            break;
196        }
197    }
198    map
199}
200
201fn namespaces_to_emit(
202    doc: &XmlDoc,
203    id: NodeId,
204    exclusive: bool,
205    vis_prefixes: &[&str],
206    rendered: &[(String, String)],
207) -> Vec<(String, String)> {
208    let mut map = in_scope_ns(doc, id);
209    map.retain(|(pre, href)| {
210        if pre == "xml" && href == XML_NS {
211            return false;
212        }
213        if exclusive {
214            if !visibly_used(doc, id, pre, vis_prefixes) {
215                return false;
216            }
217            if pre.is_empty() && href.is_empty() {
218                return rendered.iter().any(|(p, h)| p.is_empty() && !h.is_empty());
219            }
220            return !rendered.iter().any(|(p, h)| p == pre && h == href);
221        }
222        if rendered.iter().any(|(p, h)| p == pre && h == href) {
223            return false;
224        }
225        if pre.is_empty() && href.is_empty() {
226            return rendered.iter().any(|(p, h)| p.is_empty() && !h.is_empty());
227        }
228        true
229    });
230    map
231}
232
233fn visibly_used(doc: &XmlDoc, id: NodeId, pre: &str, extra: &[&str]) -> bool {
234    if extra.contains(&pre) {
235        return true;
236    }
237    if pre.is_empty() {
238        return doc.prefix(id).is_none();
239    }
240    if doc.prefix(id) == Some(pre) {
241        return true;
242    }
243    let mut a = doc.first_attr(id);
244    while let Some(x) = a {
245        if doc.prefix(x) == Some(pre) {
246            return true;
247        }
248        a = doc.next_sibling(x);
249    }
250    false
251}
252
253fn cmp_attr(ans: &str, an: &str, bns: &str, bn: &str) -> Ordering {
254    let au = ans;
255    let bu = bns;
256    match (au.is_empty(), bu.is_empty()) {
257        (true, false) => Ordering::Less,
258        (false, true) => Ordering::Greater,
259        _ => au.cmp(bu).then_with(|| {
260            let al = an.rsplit(':').next().unwrap_or(an);
261            let bl = bn.rsplit(':').next().unwrap_or(bn);
262            al.cmp(bl)
263        }),
264    }
265}
266
267fn escape_text(s: &str) -> String {
268    let mut out = String::new();
269    for c in s.chars() {
270        match c {
271            '&' => out.push_str("&amp;"),
272            '<' => out.push_str("&lt;"),
273            '>' => out.push_str("&gt;"),
274            '\r' => out.push_str("&#xD;"),
275            c => out.push(c),
276        }
277    }
278    out
279}
280
281fn escape_attr(s: &str) -> String {
282    let mut out = String::new();
283    for c in s.chars() {
284        match c {
285            '&' => out.push_str("&amp;"),
286            '<' => out.push_str("&lt;"),
287            '"' => out.push_str("&quot;"),
288            '\t' => out.push_str("&#x9;"),
289            '\n' => out.push_str("&#xA;"),
290            '\r' => out.push_str("&#xD;"),
291            c => out.push(c),
292        }
293    }
294    out
295}