1use crate::xml_input as xml_sec_xml_input;
4use std::borrow::Cow;
5
6pub use crate::xml_input_shared::Error as XmlEncodingError;
10
11#[cfg(feature = "xmldsig")]
12#[derive(Debug)]
13pub(crate) enum DecodeError {
14 Encoding(XmlEncodingError),
15 Label(XmlEncodingError<structured_asn1::memory::OwnedText>),
16 Execution(structured_asn1::memory::MemoryError),
17}
18
19#[cfg(feature = "xmldsig")]
20impl From<XmlEncodingError> for DecodeError {
21 fn from(error: XmlEncodingError) -> Self {
22 Self::Encoding(error)
23 }
24}
25
26#[cfg(feature = "xmldsig")]
27impl From<structured_asn1::memory::MemoryError> for DecodeError {
28 fn from(error: structured_asn1::memory::MemoryError) -> Self {
29 Self::Execution(error)
30 }
31}
32
33#[cfg(feature = "xmldsig")]
34pub(crate) struct AccountedDecoder<'w, W: structured_asn1::memory::Work>(pub &'w W);
35
36#[cfg(feature = "xmldsig")]
37impl<W: structured_asn1::memory::Work> crate::xml_input_shared::DecodeStorage
38 for AccountedDecoder<'_, W>
39{
40 type Text = structured_asn1::memory::OwnedText<W>;
41 type Buffer = structured_asn1::memory::OwnedVec<u8, W>;
42 type Error = DecodeError;
43
44 fn label_error(&self, kind: crate::xml_input_shared::LabelError, label: &str) -> DecodeError {
45 use crate::xml_input_shared::LabelError;
46 let text = match structured_asn1::memory::OwnedText::try_from_str(label, self.0) {
47 Ok(text) => text.into_owned(),
48 Err(error) => return DecodeError::Execution(error),
49 };
50 DecodeError::Label(match kind {
51 LabelError::Unsupported => XmlEncodingError::UnsupportedEncoding(text),
52 LabelError::Forbidden => XmlEncodingError::ForbiddenEncoding(text),
53 LabelError::Conflicting => XmlEncodingError::ConflictingEncoding(text),
54 })
55 }
56
57 fn charge(&self, bytes: usize) -> Result<(), DecodeError> {
58 self.0.charge_work(
59 u64::try_from(bytes).map_err(|_| structured_asn1::memory::MemoryError::WorkExceeded)?,
60 )?;
61 Ok(())
62 }
63 fn buffer(&self) -> Self::Buffer {
64 self.0.vector()
65 }
66 fn len(&self, buffer: &Self::Buffer) -> usize {
67 buffer.len()
68 }
69 fn append(&self, buffer: &mut Self::Buffer, text: &str) -> Result<(), DecodeError> {
70 buffer.try_extend_from_slice(text.as_bytes())?;
71 Ok(())
72 }
73 fn finish(&self, buffer: Self::Buffer) -> Result<Self::Text, DecodeError> {
74 use structured_asn1::memory::{OwnedText, TextErrorKind};
75 OwnedText::from_utf8(buffer).map_err(|error| match error.kind() {
76 TextErrorKind::Resource(error) => DecodeError::Execution(error),
77 TextErrorKind::Utf8(_) => unreachable!("decoder appends only valid UTF-8 fragments"),
78 })
79 }
80 fn replace(
81 &self,
82 text: Self::Text,
83 range: std::ops::Range<usize>,
84 replacement: &str,
85 ) -> Result<Self::Text, DecodeError> {
86 let tail = text.len() - range.end;
89 let old_length = text.len();
90 let new_length = old_length - range.len() + replacement.len();
91 self.charge(tail)?;
92 self.charge(replacement.len())?;
93 self.charge(old_length.saturating_sub(new_length))?;
94 let mut bytes = text.into_bytes();
95 while bytes.len() < new_length {
96 bytes.try_push(0)?;
97 }
98 bytes.copy_within(range.end..old_length, range.start + replacement.len());
99 bytes[range.start..range.start + replacement.len()].copy_from_slice(replacement.as_bytes());
100 while bytes.len() > new_length {
101 bytes.pop();
102 }
103 self.finish(bytes)
104 }
105}
106
107#[cfg(all(test, feature = "xmldsig"))]
108fn decode_xml_octets_in<'a, W: structured_asn1::memory::Work>(
109 bytes: &'a [u8],
110 maximum: usize,
111 work: &W,
112) -> Result<crate::xml_input_shared::Decoded<'a, structured_asn1::memory::OwnedText<W>>, DecodeError>
113{
114 crate::xml_input_shared::decode_xml_in(bytes, maximum, |_, _| Ok(()), &AccountedDecoder(work))
115}
116
117pub fn decode_xml_octets(
124 bytes: &[u8],
125 maximum_decoded_bytes: usize,
126) -> Result<Cow<'_, str>, XmlEncodingError> {
127 xml_sec_xml_input::decode_xml_bounded(bytes, None, maximum_decoded_bytes)
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 #[cfg(feature = "xmldsig")]
136 fn scoped_transcoding_retains_actual_capacity_until_text_is_dropped() {
137 use structured_asn1::memory::{Memory, MemoryAccount};
140 let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(4096, 100_000)));
141 let input = b"<?xml version='1.0' encoding='ISO-8859-1'?><r>caf\xe9</r>";
142 let decoded = decode_xml_octets_in(input, 4096, &memory).expect("Latin-1 output fits");
143 assert_eq!(
144 decoded.as_ref(),
145 "<?xml version='1.0' encoding='UTF-8'?><r>café</r>"
146 );
147 assert!(memory.used() >= decoded.as_ref().len());
148 drop(decoded);
149 assert_eq!(memory.used(), 0);
150 }
151
152 #[test]
153 #[cfg(feature = "xmldsig")]
154 fn scoped_transcoding_refuses_capacity_but_borrows_valid_utf8() {
155 use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
158 let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 100_000)));
159 let utf8 = b"<r>text</r>";
160 let decoded = decode_xml_octets_in(utf8, 4096, &memory).expect("UTF-8 stays borrowed");
161 assert_eq!(decoded.as_ref().as_ptr(), utf8.as_ptr());
162 let input = b"<?xml version='1.0' encoding='ISO-8859-1'?><r>\xe9</r>";
163 assert!(matches!(
164 decode_xml_octets_in(input, 4096, &memory),
165 Err(DecodeError::Execution(MemoryError::LimitExceeded))
166 ));
167 assert_eq!(memory.used(), 0);
168 }
169
170 #[test]
171 #[cfg(feature = "xmldsig")]
172 fn scoped_encoding_diagnostics_do_not_escape_the_capacity_account() {
173 use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
176 let label = "Unregistered-".repeat(1024);
177 let input = format!("<?xml version='1.0' encoding='{label}'?><r/>");
178 let denied = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1 << 20)));
179 assert!(matches!(
180 decode_xml_octets_in(input.as_bytes(), 1 << 20, &denied),
181 Err(DecodeError::Execution(MemoryError::LimitExceeded))
182 ));
183 assert_eq!(denied.used(), 0);
184 let admitted = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 20)));
185 let error = match decode_xml_octets_in(input.as_bytes(), 1 << 20, &admitted) {
186 Err(error) => error,
187 Ok(_) => panic!("unsupported label must not decode"),
188 };
189 assert!(
190 matches!(&error, DecodeError::Label(XmlEncodingError::UnsupportedEncoding(text)) if text.as_str() == label)
191 );
192 assert!(admitted.used() >= label.len());
193 drop(error);
194 assert_eq!(admitted.used(), 0);
195 }
196
197 #[test]
198 #[cfg(feature = "xmldsig")]
199 fn scoped_transcoding_preserves_local_query_refusal_before_decode() {
200 use structured_asn1::{
203 Encoding, Limits, Parser,
204 memory::{Memory, MemoryAccount, MemoryError},
205 };
206 let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(4096, 100_000)));
207 let parser = Parser::new(
208 Encoding::Der,
209 Limits {
210 max_work: 0,
211 ..Default::default()
212 },
213 memory.clone(),
214 );
215 let reader = parser.read(&[], 0).expect("empty query carrier");
216 for input in [
217 b"<r/>".as_slice(),
218 b"<?xml version='1.0' encoding='not-supported'?><r/>",
219 ] {
220 assert!(matches!(
221 decode_xml_octets_in(input, 4096, &reader.execution()),
222 Err(DecodeError::Execution(MemoryError::WorkExceeded))
223 ));
224 }
225 assert_eq!(memory.used(), 0);
226 assert_eq!(memory.work_used(), 0);
227 }
228
229 #[test]
230 #[cfg(feature = "xmldsig")]
231 fn scoped_transcoding_admits_exact_work_and_refuses_one_less() {
232 use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
235 let input = b"<?xml version='1.0' encoding='ISO-8859-1'?><r>\xe9</r>";
236 let baseline = Memory::new(std::sync::Arc::new(MemoryAccount::new(4096, 100_000)));
237 let output = decode_xml_octets_in(input, 4096, &baseline).expect("baseline fits");
238 let used = baseline.work_used();
239 let exact = Memory::new(std::sync::Arc::new(MemoryAccount::new(4096, used)));
240 assert_eq!(
241 decode_xml_octets_in(input, 4096, &exact)
242 .expect("exact work fits")
243 .as_ref(),
244 output.as_ref()
245 );
246 assert_eq!(exact.used(), 0);
247 let short = Memory::new(std::sync::Arc::new(MemoryAccount::new(4096, used - 1)));
248 assert!(matches!(
249 decode_xml_octets_in(input, 4096, &short),
250 Err(DecodeError::Execution(MemoryError::WorkExceeded))
251 ));
252 assert_eq!(short.used(), 0);
253 }
254
255 #[test]
256 #[cfg(feature = "xmldsig")]
257 fn scoped_declaration_rewrite_reuses_the_normalization_allocation() {
258 use crate::xml_input_shared::DecodeStorage;
261 use structured_asn1::memory::{Memory, MemoryAccount, OwnedText};
262 let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(4096, 100_000)));
263 let storage = AccountedDecoder(&memory);
264 for label in ["ISO-8859-1", "x"] {
265 let mut bytes = memory.vector();
266 bytes.try_reserve_exact(128).expect("test capacity fits");
267 bytes
268 .try_extend_from_slice(b"before:")
269 .expect("prefix fits");
270 bytes
271 .try_extend_from_slice(label.as_bytes())
272 .expect("label fits");
273 bytes.try_extend_from_slice(b":after").expect("suffix fits");
274 let pointer = bytes.as_ptr();
275 let capacity = bytes.capacity();
276 let text = OwnedText::from_utf8(bytes).expect("ASCII test text");
277 let rewritten = storage
278 .replace(text, 7..7 + label.len(), "UTF-8")
279 .expect("rewrite fits");
280 assert_eq!(rewritten.as_str(), "before:UTF-8:after");
281 assert_eq!(rewritten.as_str().as_ptr(), pointer);
282 assert_eq!(memory.used(), capacity);
283 drop(rewritten);
284 assert_eq!(memory.used(), 0);
285 }
286 }
287
288 #[test]
289 #[cfg(feature = "xmldsig")]
290 fn scoped_transcoding_preserves_state_across_bounded_input_chunks() {
291 use structured_asn1::memory::{Memory, MemoryAccount};
294 let prefix = "<?xml version='1.0' encoding='UTF-16'?><r>";
295 let xml = format!("{prefix}{}😀</r>", "x".repeat(2047 - prefix.len()));
296 for little_endian in [false, true] {
297 let bytes = utf16_bytes(&xml, little_endian, true);
298 let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 20)));
299 let decoded =
300 decode_xml_octets_in(&bytes, 1 << 20, &memory).expect("split pair is valid");
301 assert_eq!(decoded.as_ref(), xml.replace("UTF-16", "UTF-8"));
302 assert_eq!(
303 decode_xml_octets(&bytes, 1 << 20).expect("native split pair is valid"),
304 decoded.as_ref()
305 );
306 }
307 }
308
309 #[test]
310 #[cfg(feature = "xmldsig")]
311 fn scoped_transcoding_keeps_stateful_escape_sequences_across_chunks() {
312 use structured_asn1::memory::{Memory, MemoryAccount};
315 let xml = format!(
316 "<?xml version='1.0' encoding='ISO-2022-JP'?><r>{}</r>",
317 "aあbい".repeat(4096)
318 );
319 let (bytes, _, malformed) = encoding_rs::ISO_2022_JP.encode(&xml);
320 assert!(!malformed);
321 let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
322 let decoded = decode_xml_octets_in(&bytes, 1 << 20, &memory).expect("valid stateful XML");
323 assert_eq!(decoded.as_ref(), xml.replace("ISO-2022-JP", "UTF-8"));
324 assert_eq!(
325 decode_xml_octets(&bytes, 1 << 20).expect("same native decoder"),
326 decoded.as_ref()
327 );
328 drop(decoded);
329 assert_eq!(memory.used(), 0);
330 }
331
332 #[test]
333 #[cfg(feature = "xmldsig")]
334 fn scoped_transcoding_releases_partial_output_on_a_malformed_tail() {
335 use structured_asn1::memory::{Memory, MemoryAccount};
338 let xml = format!(
339 "<?xml version='1.0' encoding='UTF-16'?><r>{}",
340 "x".repeat(8192)
341 );
342 let mut bytes = utf16_bytes(&xml, true, true);
343 bytes.extend_from_slice(&0xd800_u16.to_le_bytes());
344 let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
345 assert!(matches!(
346 decode_xml_octets_in(&bytes, 1 << 20, &memory),
347 Err(DecodeError::Encoding(XmlEncodingError::InvalidBytes(
348 "UTF-16LE"
349 )))
350 ));
351 assert_eq!(memory.used(), 0);
352 }
353
354 fn utf16_bytes(xml: &str, little_endian: bool, bom: bool) -> Vec<u8> {
355 let mut bytes = if bom {
356 if little_endian {
357 vec![0xff, 0xfe]
358 } else {
359 vec![0xfe, 0xff]
360 }
361 } else {
362 Vec::new()
363 };
364 bytes.extend(xml.encode_utf16().flat_map(|value| {
365 if little_endian {
366 value.to_le_bytes()
367 } else {
368 value.to_be_bytes()
369 }
370 }));
371 bytes
372 }
373
374 #[test]
375 fn transcoding_normalizes_non_utf8_declarations() {
376 for bytes in [
377 utf16_bytes(
378 "<?xml version=\"1.0\" encoding=\"UTF-16\"?><root/>",
379 true,
380 true,
381 ),
382 b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><root>caf\xe9</root>".to_vec(),
383 ] {
384 let decoded =
385 decode_xml_octets(&bytes, 1_024).expect("declared XML encoding must decode");
386 assert!(decoded.contains("encoding=\"UTF-8\"") || decoded.contains("encoding='UTF-8'"));
387 }
388 }
389
390 #[test]
391 fn declared_single_byte_xml_is_transcoded_strictly() {
392 let latin1 = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><root>caf\xe9</root>";
394 let decoded = decode_xml_octets(latin1, 1_024).expect("declared Latin-1 XML must decode");
395 assert!(decoded.contains("<root>café</root>"));
396 assert!(decoded.contains("encoding=\"UTF-8\""));
397 }
398
399 #[test]
400 fn byte_signatures_and_declarations_must_agree() {
401 let conflicting = utf16_bytes(
402 "<?xml version=\"1.0\" encoding=\"UTF-16BE\"?><root/>",
403 true,
404 true,
405 );
406 assert!(matches!(
407 decode_xml_octets(&conflicting, 1_024),
408 Err(XmlEncodingError::ConflictingEncoding(_))
409 ));
410
411 let matching_signature = utf16_bytes(
412 "<?xml version=\"1.0\" encoding=\"UTF-16BE\"?><root/>",
413 false,
414 true,
415 );
416 assert!(decode_xml_octets(&matching_signature, 1_024).is_ok());
417
418 let bomless_generic = utf16_bytes(
419 "<?xml version=\"1.0\" encoding=\"UTF-16\"?><root/>",
420 true,
421 false,
422 );
423 assert!(matches!(
424 decode_xml_octets(&bomless_generic, 1_024),
425 Err(XmlEncodingError::MissingUtf16ByteOrder)
426 ));
427 }
428
429 #[test]
430 fn utf8_is_borrowed_and_malformed_bytes_are_rejected() {
431 let xml = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>";
432 assert!(matches!(
433 decode_xml_octets(xml, xml.len()),
434 Ok(Cow::Borrowed(_))
435 ));
436 assert!(decode_xml_octets(b"<root>\xff</root>", 1_024).is_err());
437 }
438
439 #[test]
440 fn declaration_detection_ignores_other_processing_instructions() {
441 for xml in [
442 r#"<?xml-stylesheet encoding="ISO-8859-1"?><root/>"#,
443 r#"<root data-encoding="ISO-8859-1"/>"#,
444 ] {
445 decode_xml_octets(xml.as_bytes(), 1_024)
446 .expect("ordinary content does not select encoding");
447 }
448 }
449
450 #[test]
451 fn transcoding_stops_at_the_decoded_document_limit() {
452 let bytes = utf16_bytes("<?xml version=\"1.0\"?><root>expanded</root>", true, true);
453 assert!(matches!(
454 decode_xml_octets(&bytes, 16),
455 Err(XmlEncodingError::DecodedLimit { maximum: 16, .. })
456 ));
457 }
458}