pdk_xml_validator_lib/
lib.rs1use classy::hl::{BodyStream, BodyStreamAsyncReader};
24use quick_xml::events::BytesStart;
25use quick_xml::{events::Event, reader::NsReader};
26use std::error::Error;
27use thiserror::Error;
28use tokio::io::{AsyncRead, BufReader};
29use tokio_util::compat::FuturesAsyncReadCompatExt;
30
31#[non_exhaustive]
33#[derive(Debug, Error)]
34pub enum XmlValidationError {
35 #[error("Node depth count has been exceeded. Maximum allowed is: {0}")]
37 MaxDepthExceeded(usize),
38 #[error("Attribute count has been exceeded. Maximum allowed is: {0}")]
40 MaxAttributeCountExceeded(usize),
41 #[error("Text length has been exceeded. Maximum allowed is: {0}")]
43 MaxTextLengthExceeded(usize),
44 #[error("Comment length has been exceeded. Maximum allowed is: {0}")]
46 MaxCommentLengthExceeded(usize),
47 #[error("Attribute value length has been exceeded. Maximum allowed is: {0}")]
49 MaxAttributeLengthExceeded(usize),
50 #[error("Child count has been exceeded. Maximum allowed is: {0}")]
52 MaxChildCountExceeded(usize),
53 #[error("Can not read attributes: {0}")]
55 AttributeError(Box<dyn Error>),
56 #[error("Malformed document: {0}")]
58 MalformedDocument(Box<dyn Error>),
59 #[error("XML Validation Error: {0}")]
61 InvalidStructure(String),
62}
63
64#[derive(Debug, Default)]
65pub struct XmlValidatorBuilder {
66 max_depth: Option<usize>,
67 max_attribute_count: Option<usize>,
68 max_child_count: Option<usize>,
69 max_text_length: Option<usize>,
70 max_attribute_length: Option<usize>,
71 max_comment_length: Option<usize>,
72}
73
74impl XmlValidatorBuilder {
75 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn with_max_depth(mut self, depth: usize) -> Self {
82 self.max_depth = Some(depth);
83 self
84 }
85
86 pub fn with_max_attribute_count(mut self, count: usize) -> Self {
88 self.max_attribute_count = Some(count);
89 self
90 }
91
92 pub fn with_max_child_count(mut self, count: usize) -> Self {
94 self.max_child_count = Some(count);
95 self
96 }
97
98 pub fn with_max_text_length(mut self, length: usize) -> Self {
100 self.max_text_length = Some(length);
101 self
102 }
103
104 pub fn with_max_attribute_length(mut self, length: usize) -> Self {
106 self.max_attribute_length = Some(length);
107 self
108 }
109
110 pub fn with_max_comment_length(mut self, length: usize) -> Self {
112 self.max_comment_length = Some(length);
113 self
114 }
115
116 pub fn build(self) -> XmlValidator {
118 XmlValidator {
119 max_depth: self.max_depth,
120 max_attribute_count: self.max_attribute_count,
121 max_child_count: self.max_child_count,
122 max_text_length: self.max_text_length,
123 max_attribute_length: self.max_attribute_length,
124 max_comment_length: self.max_comment_length,
125 }
126 }
127}
128
129pub struct XmlValidator {
131 max_depth: Option<usize>,
133 max_attribute_count: Option<usize>,
135 max_child_count: Option<usize>,
137 max_text_length: Option<usize>,
139 max_attribute_length: Option<usize>,
141 max_comment_length: Option<usize>,
143}
144
145struct ElementInfo {
146 child_count: usize,
147}
148
149impl XmlValidator {
150 pub async fn validate_stream(&self, stream: BodyStream<'_>) -> Result<(), XmlValidationError> {
152 self.validate(BodyStreamAsyncReader::new(stream).compat())
153 .await
154 }
155
156 pub async fn validate(&self, reader: impl AsyncRead + Unpin) -> Result<(), XmlValidationError> {
158 let reader = BufReader::new(reader);
159 let mut buf = Vec::with_capacity(1024);
160 let mut xml_reader = NsReader::from_reader(reader);
161 let mut depth = 0usize;
162 let mut element_stack: Vec<ElementInfo> = Vec::new();
163 let mut text_run_len = 0usize;
170
171 loop {
172 let event = xml_reader.read_event_into_async(&mut buf).await;
173 match event {
174 Ok(ref event @ Event::Start(ref e)) | Ok(ref event @ Event::Empty(ref e)) => {
175 text_run_len = 0;
177 depth += 1;
178
179 if let Some(max_depth) = self.max_depth {
180 if depth > max_depth {
181 return Err(XmlValidationError::MaxDepthExceeded(max_depth));
182 }
183 }
184
185 if let Event::Empty(_) = event {
186 depth -= 1;
187 }
188
189 if let Some(max_attr_count) = self.max_attribute_count {
190 if check_attribute_count(e, max_attr_count)
191 .map_err(|err| XmlValidationError::AttributeError(Box::new(err)))?
192 {
193 return Err(XmlValidationError::MaxAttributeCountExceeded(
194 max_attr_count,
195 ));
196 }
197 }
198
199 if let Some(max_attr_len) = self.max_attribute_length {
200 if check_attribute_length(e, max_attr_len)
201 .map_err(|err| XmlValidationError::AttributeError(Box::new(err)))?
202 {
203 return Err(XmlValidationError::MaxAttributeLengthExceeded(
204 max_attr_len,
205 ));
206 }
207 }
208
209 if !element_stack.is_empty() {
210 if let Some(parent) = element_stack.last_mut() {
211 parent.child_count += 1;
212 if let Some(max_child_count) = self.max_child_count {
213 if parent.child_count > max_child_count {
214 return Err(XmlValidationError::MaxChildCountExceeded(
215 max_child_count,
216 ));
217 }
218 }
219 }
220 }
221
222 if let Event::Start(_) = event {
223 element_stack.push(ElementInfo { child_count: 0 });
224 }
225 }
226 Ok(Event::End(_)) => {
227 text_run_len = 0;
228 element_stack.pop();
229 depth -= 1;
230 }
231 Ok(Event::Eof) => {
232 if !element_stack.is_empty() {
233 return Err(XmlValidationError::InvalidStructure(
234 "XML document structures must start and end within the same entity."
235 .to_string(),
236 ));
237 }
238 return Ok(());
239 }
240 Ok(Event::Text(t)) => {
241 let is_only_whitespace = t.as_ref().iter().all(|&b| b.is_ascii_whitespace());
242 if is_only_whitespace {
243 continue;
244 }
245
246 if element_stack.is_empty() {
247 return Err(XmlValidationError::InvalidStructure(
248 "Text found outside of an element".to_string(),
249 ));
250 }
251
252 if let Some(max_text_len) = self.max_text_length {
253 text_run_len = text_run_len.saturating_add(unescape_event(&t)?.len());
254 if text_run_len > max_text_len {
255 return Err(XmlValidationError::MaxTextLengthExceeded(max_text_len));
256 }
257 }
258 }
259 Ok(Event::GeneralRef(r)) => {
260 if let Some(max_text_len) = self.max_text_length {
265 text_run_len = text_run_len.saturating_add(resolve_general_ref(&r)?.len());
266 if text_run_len > max_text_len {
267 return Err(XmlValidationError::MaxTextLengthExceeded(max_text_len));
268 }
269 }
270 }
271 Ok(Event::Comment(t)) => {
272 if let Some(max_comment_len) = self.max_comment_length {
273 let comment = unescape_event(&t)?;
274 if comment.len() > max_comment_len {
275 return Err(XmlValidationError::MaxCommentLengthExceeded(
276 max_comment_len,
277 ));
278 }
279 }
280 }
281 Err(e) => {
282 return Err(XmlValidationError::MalformedDocument(Box::new(e)));
283 }
284 Ok(_) => {}
285 }
286 buf.clear();
287 }
288 }
289}
290
291fn unescape_event(t: &quick_xml::events::BytesText<'_>) -> Result<String, XmlValidationError> {
294 let decoded = t
295 .decode()
296 .map_err(|err| XmlValidationError::MalformedDocument(Box::new(err)))?;
297 let unescaped = quick_xml::escape::unescape(&decoded)
298 .map_err(|err| XmlValidationError::MalformedDocument(Box::new(err)))?;
299 Ok(unescaped.into_owned())
300}
301
302fn resolve_general_ref(r: &quick_xml::events::BytesRef<'_>) -> Result<String, XmlValidationError> {
308 let name = r
309 .decode()
310 .map_err(|err| XmlValidationError::MalformedDocument(Box::new(err)))?;
311 let reconstructed = format!("&{name};");
312 let unescaped = quick_xml::escape::unescape(&reconstructed)
313 .map_err(|err| XmlValidationError::MalformedDocument(Box::new(err)))?;
314 Ok(unescaped.into_owned())
315}
316
317fn check_attribute_count(
318 e: &BytesStart<'_>,
319 max_attr_count: usize,
320) -> Result<bool, quick_xml::Error> {
321 let mut count = 0usize;
322 for attr in e.attributes() {
323 let attr_name = String::from_utf8_lossy(attr?.key.0);
324 if !attr_name.starts_with("xmlns") {
325 count += 1;
326 }
327 if count > max_attr_count {
328 return Ok(true);
329 }
330 }
331 Ok(false)
332}
333
334fn check_attribute_length(e: &BytesStart<'_>, max_len: usize) -> Result<bool, quick_xml::Error> {
335 for attr in e.attributes() {
336 let attr = attr?;
337 let decoded = std::str::from_utf8(attr.value.as_ref())
339 .map_err(quick_xml::encoding::EncodingError::Utf8)?;
340 let unescaped = quick_xml::escape::unescape(decoded)?;
342 if unescaped.len() > max_len {
343 return Ok(true);
344 }
345 }
346 Ok(false)
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use quick_xml::events::Event;
353 use quick_xml::reader::Reader;
354
355 fn build_validator() -> XmlValidator {
356 XmlValidatorBuilder::new()
357 .with_max_depth(3)
358 .with_max_attribute_count(2)
359 .with_max_child_count(2)
360 .with_max_text_length(10)
361 .with_max_attribute_length(5)
362 .with_max_comment_length(10)
363 .build()
364 }
365
366 #[tokio::test]
367 async fn validate_accepts_valid_xml_stream() {
368 let validator = build_validator();
369
370 let xml = r#"<root><child attr="ok">text</child><child2/></root>"#;
371 let result = validator.validate(xml.as_bytes()).await;
372 assert!(result.is_ok());
373 }
374
375 #[tokio::test]
376 async fn validate_accepts_vec_buffer_as_slice() {
377 let validator = XmlValidatorBuilder::new().build();
378 let bytes = b"<root><n/></root>".to_vec();
379 let result = validator.validate(bytes.as_slice()).await;
380 assert!(result.is_ok());
381 }
382
383 #[tokio::test]
384 async fn validate_vec_single_byte_slice_is_rejected() {
385 let validator = XmlValidatorBuilder::new().build();
386 let bytes = vec![0u8];
387 let result = validator.validate(bytes.as_slice()).await;
388 assert!(result.is_err());
389 }
390
391 #[tokio::test]
392 async fn validate_rejects_depth_over_limit() {
393 let validator = XmlValidatorBuilder::new().with_max_depth(1).build();
394
395 let xml = r#"<root><child><grandchild/></child></root>"#;
396 let result = validator.validate(xml.as_bytes()).await;
397 assert!(matches!(
398 result,
399 Err(XmlValidationError::MaxDepthExceeded(1))
400 ));
401 }
402
403 #[tokio::test]
404 async fn validate_rejects_attribute_count_over_limit() {
405 let validator = build_validator();
406 let xml = r#"<root a="1" b="2" c="3"/>"#;
407 let result = validator.validate(xml.as_bytes()).await;
408 assert!(matches!(
409 result,
410 Err(XmlValidationError::MaxAttributeCountExceeded(2))
411 ));
412 }
413
414 #[tokio::test]
415 async fn validate_rejects_text_outside_element() {
416 let validator = build_validator();
417 let xml = r#"text<root/>"#;
418 let result = validator.validate(xml.as_bytes()).await;
419 assert!(matches!(
420 result,
421 Err(XmlValidationError::InvalidStructure(_))
422 ));
423 }
424
425 #[tokio::test]
426 async fn validate_rejects_attribute_length_over_limit() {
427 let validator = build_validator();
428 let xml = r#"<root attr="123456"/>"#;
429 let result = validator.validate(xml.as_bytes()).await;
430 assert!(matches!(
431 result,
432 Err(XmlValidationError::MaxAttributeLengthExceeded(5))
433 ));
434 }
435
436 #[tokio::test]
437 async fn validate_rejects_child_count_over_limit() {
438 let validator = XmlValidatorBuilder::new().with_max_child_count(1).build();
439 let xml = r#"<root><c1/><c2/></root>"#;
440 let result = validator.validate(xml.as_bytes()).await;
441 assert!(matches!(
442 result,
443 Err(XmlValidationError::MaxChildCountExceeded(1))
444 ));
445 }
446
447 #[tokio::test]
448 async fn validate_rejects_text_length_over_limit() {
449 let validator = XmlValidatorBuilder::new().with_max_text_length(3).build();
450 let xml = r#"<root>abcd</root>"#;
451 let result = validator.validate(xml.as_bytes()).await;
452 assert!(matches!(
453 result,
454 Err(XmlValidationError::MaxTextLengthExceeded(3))
455 ));
456 }
457
458 #[tokio::test]
459 async fn validate_rejects_entity_text_length_over_limit() {
460 let validator = XmlValidatorBuilder::new().with_max_text_length(3).build();
466 let xml = r#"<root><<<<<<</root>"#;
467 let result = validator.validate(xml.as_bytes()).await;
468 assert!(matches!(
469 result,
470 Err(XmlValidationError::MaxTextLengthExceeded(3))
471 ));
472 }
473
474 #[tokio::test]
475 async fn validate_accepts_entity_text_within_limit() {
476 let validator = XmlValidatorBuilder::new().with_max_text_length(3).build();
479 let xml = r#"<root><<<</root>"#;
480 let result = validator.validate(xml.as_bytes()).await;
481 assert!(result.is_ok(), "unexpected: {:?}", result);
482 }
483
484 #[tokio::test]
485 async fn validate_resets_text_run_between_elements() {
486 let validator = XmlValidatorBuilder::new().with_max_text_length(3).build();
489 let xml = r#"<root><a>ab</a><b>cd</b></root>"#;
490 let result = validator.validate(xml.as_bytes()).await;
491 assert!(result.is_ok(), "unexpected: {:?}", result);
492 }
493
494 #[tokio::test]
495 async fn validate_rejects_comment_length_over_limit() {
496 let validator = XmlValidatorBuilder::new()
497 .with_max_comment_length(3)
498 .build();
499 let xml = r#"<root><!--abcd--></root>"#;
500 let result = validator.validate(xml.as_bytes()).await;
501 assert!(matches!(
502 result,
503 Err(XmlValidationError::MaxCommentLengthExceeded(3))
504 ));
505 }
506
507 #[tokio::test]
508 async fn validate_excludes_xmlns_from_attribute_count() {
509 let validator = XmlValidatorBuilder::new()
510 .with_max_attribute_count(1)
511 .build();
512 let xml = r#"<root xmlns="urn:test" xmlns:x="urn:test2" attr="1"/>"#;
513 let result = validator.validate(xml.as_bytes()).await;
514 assert!(result.is_ok());
515 }
516
517 #[tokio::test]
518 async fn validate_accepts_whitespace_outside_root() {
519 let validator = XmlValidatorBuilder::new().build();
520 let xml = " \n\t<root/>";
521 let result = validator.validate(xml.as_bytes()).await;
522 assert!(result.is_ok());
523 }
524
525 #[tokio::test]
526 async fn validate_rejects_malformed_document() {
527 let validator = XmlValidatorBuilder::new().build();
528 let xml = r#"<root><child></root>"#;
529 let result = validator.validate(xml.as_bytes()).await;
530 assert!(matches!(
531 result,
532 Err(XmlValidationError::MalformedDocument(_))
533 ));
534 }
535
536 #[tokio::test]
537 async fn validate_rejects_unclosed_structure_at_eof() {
538 let validator = XmlValidatorBuilder::new().build();
539 let xml = r#"<root>"#;
540 let result = validator.validate(xml.as_bytes()).await;
541 assert!(matches!(
542 result,
543 Err(XmlValidationError::InvalidStructure(_))
544 ));
545 }
546
547 #[test]
548 fn check_attribute_helpers_cover_non_exceeded_paths() {
549 let mut count_reader = Reader::from_str(r#"<root xmlns="urn:test" attr="123"/>"#);
550 let mut count_buf = Vec::new();
551 let count_event = count_reader
552 .read_event_into(&mut count_buf)
553 .expect("must parse start");
554 let count_start = match count_event {
555 Event::Start(e) | Event::Empty(e) => e,
556 _ => panic!("expected start or empty event"),
557 };
558
559 let mut length_reader = Reader::from_str(r#"<root attr="123"/>"#);
560 let mut length_buf = Vec::new();
561 let length_event = length_reader
562 .read_event_into(&mut length_buf)
563 .expect("must parse start");
564 let length_start = match length_event {
565 Event::Start(e) | Event::Empty(e) => e,
566 _ => panic!("expected start or empty event"),
567 };
568
569 let count_exceeded =
570 check_attribute_count(&count_start, 1).expect("count check must succeed");
571 let len_exceeded =
572 check_attribute_length(&length_start, 3).expect("length check must succeed");
573 assert!(!count_exceeded);
574 assert!(!len_exceeded);
575 }
576}