confium_pki/xmldsig/
c14n.rs1pub fn canonicalize(xml: &str) -> Result<String, CanonicalizationError> {
31 let mut s = xml.to_string();
32
33 if let Some(start) = s.find("<?xml") {
35 if let Some(end) = s[start..].find("?>") {
36 s.replace_range(start..(start + end + 2), "");
37 }
38 }
39
40 s = remove_processing_instructions(&s)?;
43
44 s = normalize_line_endings(&s);
46
47 s = normalize_entities(&s)?;
49
50 s = s.trim().to_string();
52
53 Ok(s)
54}
55
56#[allow(dead_code)]
62pub fn canonicalize_exclusive(xml: &str) -> Result<String, CanonicalizationError> {
63 canonicalize(xml)
64}
65
66#[derive(Debug, thiserror::Error)]
68pub enum CanonicalizationError {
69 #[error("malformed XML: {0}")]
71 Malformed(String),
72}
73
74fn remove_processing_instructions(s: &str) -> Result<String, CanonicalizationError> {
75 let mut out = String::with_capacity(s.len());
76 let mut i = 0;
77 let bytes = s.as_bytes();
78 while i < bytes.len() {
79 if i + 1 < bytes.len() && bytes[i] == b'<' && bytes[i + 1] == b'?' {
80 if let Some(end) = s[i..].find("?>") {
82 i += end + 2;
83 continue;
84 } else {
85 return Err(CanonicalizationError::Malformed(
86 "unterminated processing instruction".into(),
87 ));
88 }
89 }
90 if i + 3 < bytes.len() && &bytes[i..i + 4] == b"<!--" {
91 if let Some(end) = s[i..].find("-->") {
93 i += end + 3;
94 continue;
95 } else {
96 return Err(CanonicalizationError::Malformed(
97 "unterminated comment".into(),
98 ));
99 }
100 }
101 out.push(bytes[i] as char);
102 i += 1;
103 }
104 Ok(out)
105}
106
107fn normalize_line_endings(s: &str) -> String {
108 let mut out = String::with_capacity(s.len());
110 let bytes = s.as_bytes();
111 let mut i = 0;
112 while i < bytes.len() {
113 if bytes[i] == b'\r' {
114 out.push('\n');
115 if i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
116 i += 2;
117 } else {
118 i += 1;
119 }
120 } else {
121 out.push(bytes[i] as char);
122 i += 1;
123 }
124 }
125 out
126}
127
128fn normalize_entities(s: &str) -> Result<String, CanonicalizationError> {
129 let mut out = String::with_capacity(s.len());
130 let mut in_text = true;
131 let mut i = 0;
132 let bytes = s.as_bytes();
133
134 while i < bytes.len() {
135 let b = bytes[i];
136
137 if b == b'<' {
139 in_text = false;
140 if i + 8 < bytes.len() && &bytes[i..i + 9] == b"<![CDATA[" {
142 if let Some(end) = s[i..].find("]]>") {
143 out.push_str(&s[i..i + end + 3]);
145 i += end + 3;
146 continue;
147 } else {
148 return Err(CanonicalizationError::Malformed(
149 "unterminated CDATA section".into(),
150 ));
151 }
152 }
153 out.push('<');
154 i += 1;
155 continue;
156 }
157 if b == b'>' && !in_text {
158 in_text = true;
159 out.push('>');
160 i += 1;
161 continue;
162 }
163
164 if in_text {
165 if b == b'&' {
167 if let Some(semi) = s[i..].find(';') {
168 let entity = &s[i + 1..i + semi];
169 let decoded = match entity {
170 "amp" => '&',
171 "lt" => '<',
172 "gt" => '>',
173 "quot" => '"',
174 "apos" => '\'',
175 _ => {
176 if let Some(rest) = entity.strip_prefix("#x") {
178 let n = u32::from_str_radix(rest, 16).map_err(|_| {
179 CanonicalizationError::Malformed(format!(
180 "bad numeric entity: &{entity};"
181 ))
182 })?;
183 char::from_u32(n).ok_or_else(|| {
184 CanonicalizationError::Malformed(format!(
185 "invalid codepoint: &{entity};"
186 ))
187 })?
188 } else if let Some(rest) = entity.strip_prefix('#') {
189 let n: u32 = rest.parse().map_err(|_| {
190 CanonicalizationError::Malformed(format!(
191 "bad numeric entity: &{entity};"
192 ))
193 })?;
194 char::from_u32(n).ok_or_else(|| {
195 CanonicalizationError::Malformed(format!(
196 "invalid codepoint: &{entity};"
197 ))
198 })?
199 } else {
200 out.push('&');
202 out.push_str(entity);
203 out.push(';');
204 i += semi + 1;
205 continue;
206 }
207 }
208 };
209 out.push(decoded);
210 i += semi + 1;
211 continue;
212 } else {
213 return Err(CanonicalizationError::Malformed(
214 "unterminated entity reference".into(),
215 ));
216 }
217 }
218 match b as char {
220 '<' => out.push_str("<"),
221 '>' => out.push_str(">"),
222 '&' => out.push_str("&"),
223 _ => out.push(b as char),
224 }
225 i += 1;
226 } else {
227 out.push(b as char);
229 i += 1;
230 }
231 }
232
233 Ok(out)
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn strips_xml_declaration() {
242 let xml = "<?xml version=\"1.0\"?>\n<root/>";
243 let c = canonicalize(xml).unwrap();
244 assert!(!c.contains("<?xml"));
245 assert!(c.contains("<root/>"));
246 }
247
248 #[test]
249 fn normalizes_crlf() {
250 let xml = "<root>\r\nhello\r\n</root>";
251 let c = canonicalize(xml).unwrap();
252 assert_eq!(c, "<root>\nhello\n</root>");
253 }
254
255 #[test]
256 fn normalizes_cr_alone() {
257 let xml = "<root>\rhello\r</root>";
258 let c = canonicalize(xml).unwrap();
259 assert_eq!(c, "<root>\nhello\n</root>");
260 }
261
262 #[test]
263 fn expands_known_entities() {
264 let xml = "<root>1 & 2 < 3 > 0</root>";
265 let c = canonicalize(xml).unwrap();
266 assert_eq!(c, "<root>1 & 2 < 3 > 0</root>");
267 }
268
269 #[test]
270 fn expands_numeric_entities() {
271 let xml = "<root>AB</root>";
272 let c = canonicalize(xml).unwrap();
273 assert_eq!(c, "<root>AB</root>");
274 }
275
276 #[test]
277 fn re_encodes_specials_in_text() {
278 let xml = "<root>text with > char</root>";
280 let c = canonicalize(xml).unwrap();
281 assert_eq!(c, "<root>text with > char</root>");
282 }
283
284 #[test]
285 fn strips_comments() {
286 let xml = "<root><!-- comment -->data</root>";
287 let c = canonicalize(xml).unwrap();
288 assert_eq!(c, "<root>data</root>");
289 }
290
291 #[test]
292 fn strips_processing_instructions() {
293 let xml = "<root><?pi data?>content</root>";
294 let c = canonicalize(xml).unwrap();
295 assert_eq!(c, "<root>content</root>");
296 }
297
298 #[test]
299 fn preserves_cdata_verbatim() {
300 let xml = "<root><![CDATA[<not a tag>]]></root>";
301 let c = canonicalize(xml).unwrap();
302 assert_eq!(c, "<root><![CDATA[<not a tag>]]></root>");
303 }
304
305 #[test]
306 fn unterminated_entity_fails() {
307 let xml = "<root>& no semi</root>";
308 let result = canonicalize(xml);
309 assert!(result.is_err());
310 }
311
312 #[test]
313 fn round_trip_through_exclusive() {
314 let xml = "<root attr=\"value\"><child>text</child></root>";
315 let c1 = canonicalize_exclusive(xml).unwrap();
316 let c2 = canonicalize_exclusive(&c1).unwrap();
317 assert_eq!(c1, c2);
318 }
319}