1use crate::error::{Error, Position, Result};
7use crate::escape::unescape;
8use memchr::memchr;
9use std::borrow::Cow;
10
11static IS_WHITESPACE: [bool; 256] = {
13 let mut lut = [false; 256];
14 lut[b' ' as usize] = true;
15 lut[b'\t' as usize] = true;
16 lut[b'\n' as usize] = true;
17 lut[b'\r' as usize] = true;
18 lut
19};
20
21static IS_NAME_START: [bool; 256] = {
23 let mut lut = [false; 256];
24 let mut i = b'A';
25 while i <= b'Z' {
26 lut[i as usize] = true;
27 i += 1;
28 }
29 let mut i = b'a';
30 while i <= b'z' {
31 lut[i as usize] = true;
32 i += 1;
33 }
34 lut[b'_' as usize] = true;
35 lut[b':' as usize] = true;
36 let mut i: usize = 0x80;
38 while i < 256 {
39 lut[i] = true;
40 i += 1;
41 }
42 lut
43};
44
45static IS_NAME_CHAR: [bool; 256] = {
47 let mut lut = IS_NAME_START;
48 let mut i = b'0';
49 while i <= b'9' {
50 lut[i as usize] = true;
51 i += 1;
52 }
53 lut[b'-' as usize] = true;
54 lut[b'.' as usize] = true;
55 lut
56};
57
58#[derive(Debug, Clone, PartialEq)]
60pub enum XmlEvent<'a> {
61 XmlDecl {
63 version: Cow<'a, str>,
65 encoding: Option<Cow<'a, str>>,
67 standalone: Option<bool>,
69 },
70 StartElement {
72 name: Cow<'a, str>,
74 attributes: Vec<Attribute<'a>>,
76 },
77 EndElement {
79 name: Cow<'a, str>,
81 },
82 EmptyElement {
84 name: Cow<'a, str>,
86 attributes: Vec<Attribute<'a>>,
88 },
89 Text(Cow<'a, str>),
91 CData(Cow<'a, str>),
93 Comment(Cow<'a, str>),
95 ProcessingInstruction {
97 target: Cow<'a, str>,
99 data: Option<Cow<'a, str>>,
101 },
102 Eof,
104}
105
106#[derive(Debug, Clone, PartialEq)]
108pub struct Attribute<'a> {
109 pub name: Cow<'a, str>,
111 pub value: Cow<'a, str>,
113}
114
115pub struct XmlReader<'a> {
117 input: &'a [u8],
118 pos: usize,
119 element_stack: Vec<&'a str>,
121}
122
123impl<'a> XmlReader<'a> {
124 #[inline]
126 #[allow(clippy::should_implement_trait)]
127 pub fn from_str(s: &'a str) -> Self {
128 Self::from_bytes(s.as_bytes())
129 }
130
131 #[inline]
133 pub fn from_bytes(input: &'a [u8]) -> Self {
134 Self {
135 input,
136 pos: 0,
137 element_stack: Vec::with_capacity(8), }
139 }
140
141 #[inline]
146 pub fn position(&self) -> Position {
147 self.position_at(self.pos)
148 }
149
150 #[inline]
152 pub fn depth(&self) -> usize {
153 self.element_stack.len()
154 }
155
156 #[inline]
162 pub fn next_event(&mut self) -> Result<XmlEvent<'a>> {
163 self.skip_whitespace_fast();
164
165 if self.pos >= self.input.len() {
166 if let Some(tag) = self.element_stack.pop() {
167 return Err(Error::unclosed_tag(tag).with_position(self.position()));
168 }
169 return Ok(XmlEvent::Eof);
170 }
171
172 if self.input[self.pos] == b'<' {
173 self.read_tag()
174 } else {
175 self.read_text()
176 }
177 }
178
179 #[inline(always)]
181 fn skip_whitespace_fast(&mut self) {
182 while self.pos < self.input.len() && IS_WHITESPACE[self.input[self.pos] as usize] {
183 self.pos += 1;
184 }
185 }
186
187 #[inline]
189 fn read_text(&mut self) -> Result<XmlEvent<'a>> {
190 let start = self.pos;
191
192 match memchr(b'<', &self.input[self.pos..]) {
194 Some(offset) => self.pos += offset,
195 None => self.pos = self.input.len(),
196 }
197
198 let text = std::str::from_utf8(&self.input[start..self.pos])
199 .map_err(|_| Error::new(crate::error::ErrorKind::InvalidUtf8).with_position(self.position()))?;
200
201 let trimmed = text.trim();
203 if trimmed.is_empty() {
204 return self.next_event();
205 }
206
207 match unescape(trimmed) {
209 Ok(unescaped) => Ok(XmlEvent::Text(unescaped)),
210 Err(e) => {
211 let leading_ws = text.len() - text.trim_start().len();
214 let err_pos = self.position_at(start + leading_ws + e.position);
215 Err(Error::invalid_escape(e.entity).with_position(err_pos))
216 }
217 }
218 }
219
220 fn position_at(&self, offset: usize) -> Position {
227 let mut line = 1;
228 let mut column = 1;
229 for &b in &self.input[..offset] {
230 if b == b'\n' {
231 line += 1;
232 column = 1;
233 } else if b & 0xC0 != 0x80 {
234 column += 1;
235 }
236 }
237 Position { line, column, offset }
238 }
239
240 #[inline]
242 fn read_tag(&mut self) -> Result<XmlEvent<'a>> {
243 debug_assert_eq!(self.input[self.pos], b'<');
244 self.pos += 1;
245
246 if self.pos >= self.input.len() {
247 return Err(Error::unexpected_eof().with_position(self.position()));
248 }
249
250 match self.input[self.pos] {
251 b'/' => self.read_end_element(),
252 b'?' => self.read_processing_instruction(),
253 b'!' => self.read_special(),
254 _ => self.read_start_element(),
255 }
256 }
257
258 #[inline]
260 fn read_start_element(&mut self) -> Result<XmlEvent<'a>> {
261 let name = self.read_name()?;
262 let attributes = self.read_attributes()?;
263
264 self.skip_whitespace_fast();
265
266 if self.pos >= self.input.len() {
267 return Err(Error::unexpected_eof().with_position(self.position()));
268 }
269
270 if self.input[self.pos] == b'/' {
271 self.pos += 1;
273 self.expect_char(b'>')?;
274 Ok(XmlEvent::EmptyElement {
275 name: Cow::Borrowed(name),
276 attributes,
277 })
278 } else if self.input[self.pos] == b'>' {
279 self.pos += 1;
281 self.element_stack.push(name);
282 Ok(XmlEvent::StartElement {
283 name: Cow::Borrowed(name),
284 attributes,
285 })
286 } else {
287 Err(Error::syntax("expected '>' or '/>'").with_position(self.position()))
288 }
289 }
290
291 #[inline]
293 fn read_end_element(&mut self) -> Result<XmlEvent<'a>> {
294 debug_assert_eq!(self.input[self.pos], b'/');
295 self.pos += 1;
296
297 let name = self.read_name()?;
298 self.skip_whitespace_fast();
299 self.expect_char(b'>')?;
300
301 match self.element_stack.pop() {
303 Some(expected) if expected == name => Ok(XmlEvent::EndElement {
304 name: Cow::Borrowed(name),
305 }),
306 Some(expected) => Err(Error::mismatched_tag(expected, name).with_position(self.position())),
307 None => Err(Error::syntax(format!("unexpected closing tag: {}", name))
308 .with_position(self.position())),
309 }
310 }
311
312 fn read_processing_instruction(&mut self) -> Result<XmlEvent<'a>> {
314 debug_assert_eq!(self.input[self.pos], b'?');
315 self.pos += 1;
316
317 let target = self.read_name()?;
318
319 if target.eq_ignore_ascii_case("xml") {
321 return self.read_xml_decl();
322 }
323
324 self.skip_whitespace_fast();
325
326 let data_start = self.pos;
328
329 while self.pos + 1 < self.input.len() {
330 if let Some(offset) = memchr(b'?', &self.input[self.pos..]) {
331 let check_pos = self.pos + offset;
332 if check_pos + 1 < self.input.len() && self.input[check_pos + 1] == b'>' {
333 self.pos = check_pos;
334
335 let data = std::str::from_utf8(&self.input[data_start..self.pos])
336 .map_err(|_| Error::new(crate::error::ErrorKind::InvalidUtf8).with_position(self.position()))?;
337 self.pos += 2;
338 return Ok(XmlEvent::ProcessingInstruction {
339 target: Cow::Borrowed(target),
340 data: if data.trim().is_empty() {
341 None
342 } else {
343 Some(Cow::Borrowed(data.trim()))
344 },
345 });
346 }
347 self.pos = check_pos + 1;
349 } else {
350 break;
351 }
352 }
353
354 Err(Error::syntax("unterminated processing instruction").with_position(self.position()))
355 }
356
357 fn read_xml_decl(&mut self) -> Result<XmlEvent<'a>> {
359 let attributes = self.read_attributes()?;
360 self.skip_whitespace_fast();
361
362 if self.pos + 1 >= self.input.len()
363 || self.input[self.pos] != b'?'
364 || self.input[self.pos + 1] != b'>'
365 {
366 return Err(Error::syntax("expected '?>'").with_position(self.position()));
367 }
368 self.pos += 2;
369
370 let mut version = None;
371 let mut encoding = None;
372 let mut standalone = None;
373
374 for attr in attributes {
375 match attr.name.as_ref() {
376 "version" => version = Some(attr.value),
377 "encoding" => encoding = Some(attr.value),
378 "standalone" => {
379 standalone = Some(attr.value.as_ref() == "yes");
380 }
381 _ => {}
382 }
383 }
384
385 Ok(XmlEvent::XmlDecl {
386 version: version.unwrap_or(Cow::Borrowed("1.0")),
387 encoding,
388 standalone,
389 })
390 }
391
392 fn read_special(&mut self) -> Result<XmlEvent<'a>> {
394 debug_assert_eq!(self.input[self.pos], b'!');
395 self.pos += 1;
396
397 if self.pos >= self.input.len() {
398 return Err(Error::unexpected_eof().with_position(self.position()));
399 }
400
401 if self.pos + 1 < self.input.len()
403 && self.input[self.pos] == b'-'
404 && self.input[self.pos + 1] == b'-'
405 {
406 return self.read_comment();
407 }
408
409 if self.pos + 6 < self.input.len() && &self.input[self.pos..self.pos + 7] == b"[CDATA[" {
411 return self.read_cdata();
412 }
413
414 if self.pos + 6 < self.input.len() && self.input[self.pos..].starts_with(b"DOCTYPE") {
416 return self.skip_doctype();
417 }
418
419 Err(Error::syntax("unknown construct after '<!'").with_position(self.position()))
420 }
421
422 fn read_comment(&mut self) -> Result<XmlEvent<'a>> {
424 self.pos += 2; let start = self.pos;
426
427 while self.pos + 2 < self.input.len() {
429 if let Some(offset) = memchr(b'-', &self.input[self.pos..]) {
430 let check_pos = self.pos + offset;
431 if check_pos + 2 < self.input.len()
432 && self.input[check_pos + 1] == b'-'
433 && self.input[check_pos + 2] == b'>'
434 {
435 let comment = std::str::from_utf8(&self.input[start..check_pos])
436 .map_err(|_| Error::new(crate::error::ErrorKind::InvalidUtf8).with_position(self.position()))?;
437 self.pos = check_pos + 3;
438 return Ok(XmlEvent::Comment(Cow::Borrowed(comment.trim())));
439 }
440 self.pos = check_pos + 1;
441 } else {
442 break;
443 }
444 }
445
446 Err(Error::syntax("unterminated comment").with_position(self.position()))
447 }
448
449 fn read_cdata(&mut self) -> Result<XmlEvent<'a>> {
451 self.pos += 7; let start = self.pos;
453
454 while self.pos + 2 < self.input.len() {
456 if let Some(offset) = memchr(b']', &self.input[self.pos..]) {
457 let check_pos = self.pos + offset;
458 if check_pos + 2 < self.input.len()
459 && self.input[check_pos + 1] == b']'
460 && self.input[check_pos + 2] == b'>'
461 {
462 let data = std::str::from_utf8(&self.input[start..check_pos])
463 .map_err(|_| Error::new(crate::error::ErrorKind::InvalidUtf8).with_position(self.position()))?;
464 self.pos = check_pos + 3;
465 return Ok(XmlEvent::CData(Cow::Borrowed(data)));
466 }
467 self.pos = check_pos + 1;
468 } else {
469 break;
470 }
471 }
472
473 Err(Error::syntax("unterminated CDATA section").with_position(self.position()))
474 }
475
476 fn skip_doctype(&mut self) -> Result<XmlEvent<'a>> {
481 let mut depth = 1;
482
483 while self.pos < self.input.len() && depth > 0 {
484 match self.input[self.pos] {
485 b'<' => depth += 1,
486 b'>' => depth -= 1,
487 quote @ (b'"' | b'\'') => {
488 let literal_start = self.pos + 1;
491 match memchr(quote, &self.input[literal_start..]) {
492 Some(offset) => {
493 self.pos = literal_start + offset;
494 }
496 None => {
497 self.pos = self.input.len();
498 break;
499 }
500 }
501 }
502 _ => {}
503 }
504
505 self.pos += 1;
506 }
507
508 self.next_event()
510 }
511
512 #[inline]
514 fn read_name(&mut self) -> Result<&'a str> {
515 let start = self.pos;
516
517 if self.pos >= self.input.len() {
519 return Err(Error::unexpected_eof().with_position(self.position()));
520 }
521
522 let first = self.input[self.pos];
523 if !IS_NAME_START[first as usize] {
524 return Err(Error::invalid_name(format!("invalid name start character: {:?}", first as char))
525 .with_position(self.position()));
526 }
527 self.pos += 1;
528
529 while self.pos < self.input.len() && IS_NAME_CHAR[self.input[self.pos] as usize] {
531 self.pos += 1;
532 }
533
534 std::str::from_utf8(&self.input[start..self.pos])
535 .map_err(|_| Error::new(crate::error::ErrorKind::InvalidUtf8).with_position(self.position()))
536 }
537
538 #[inline]
540 fn read_attributes(&mut self) -> Result<Vec<Attribute<'a>>> {
541 let mut attributes = Vec::new();
543
544 loop {
545 self.skip_whitespace_fast();
546
547 if self.pos >= self.input.len() {
548 break;
549 }
550
551 let c = self.input[self.pos];
553 if c == b'>' || c == b'/' || c == b'?' {
554 break;
555 }
556
557 let name = self.read_name()?;
559 self.skip_whitespace_fast();
560
561 self.expect_char(b'=')?;
563 self.skip_whitespace_fast();
564
565 let value = self.read_attribute_value()?;
567
568 attributes.push(Attribute {
569 name: Cow::Borrowed(name),
570 value,
571 });
572 }
573
574 Ok(attributes)
575 }
576
577 #[inline]
579 fn read_attribute_value(&mut self) -> Result<Cow<'a, str>> {
580 if self.pos >= self.input.len() {
581 return Err(Error::unexpected_eof().with_position(self.position()));
582 }
583
584 let quote = self.input[self.pos];
585 if quote != b'"' && quote != b'\'' {
586 return Err(Error::syntax("expected quote").with_position(self.position()));
587 }
588 self.pos += 1;
589
590 let start = self.pos;
591
592 match memchr(quote, &self.input[self.pos..]) {
594 Some(offset) => {
595 let value = std::str::from_utf8(&self.input[start..self.pos + offset])
596 .map_err(|_| Error::new(crate::error::ErrorKind::InvalidUtf8).with_position(self.position()))?;
597 self.pos += offset + 1; match unescape(value) {
601 Ok(unescaped) => Ok(unescaped),
602 Err(e) => {
603 let err_pos = self.position_at(start + e.position);
606 Err(Error::invalid_escape(e.entity).with_position(err_pos))
607 }
608 }
609 }
610 None => Err(Error::syntax("unterminated attribute value").with_position(self.position())),
611 }
612 }
613
614 #[inline(always)]
616 fn expect_char(&mut self, expected: u8) -> Result<()> {
617 if self.pos >= self.input.len() {
618 return Err(Error::unexpected_eof().with_position(self.position()));
619 }
620
621 if self.input[self.pos] != expected {
622 return Err(Error::syntax(format!(
623 "expected '{}', found '{}'",
624 expected as char,
625 self.input[self.pos] as char
626 ))
627 .with_position(self.position()));
628 }
629
630 self.pos += 1;
631 Ok(())
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638
639 #[test]
640 fn test_simple_element() {
641 let mut reader = XmlReader::from_str("<root></root>");
642
643 match reader.next_event().unwrap() {
644 XmlEvent::StartElement { name, attributes } => {
645 assert_eq!(name, "root");
646 assert!(attributes.is_empty());
647 }
648 _ => panic!("expected StartElement"),
649 }
650
651 match reader.next_event().unwrap() {
652 XmlEvent::EndElement { name } => {
653 assert_eq!(name, "root");
654 }
655 _ => panic!("expected EndElement"),
656 }
657
658 assert!(matches!(reader.next_event().unwrap(), XmlEvent::Eof));
659 }
660
661 #[test]
662 fn test_empty_element() {
663 let mut reader = XmlReader::from_str("<root/>");
664
665 match reader.next_event().unwrap() {
666 XmlEvent::EmptyElement { name, attributes } => {
667 assert_eq!(name, "root");
668 assert!(attributes.is_empty());
669 }
670 _ => panic!("expected EmptyElement"),
671 }
672
673 assert!(matches!(reader.next_event().unwrap(), XmlEvent::Eof));
674 }
675
676 #[test]
677 fn test_attributes() {
678 let mut reader = XmlReader::from_str(r#"<root id="1" name="test"/>"#);
679
680 match reader.next_event().unwrap() {
681 XmlEvent::EmptyElement { name, attributes } => {
682 assert_eq!(name, "root");
683 assert_eq!(attributes.len(), 2);
684 assert_eq!(attributes[0].name, "id");
685 assert_eq!(attributes[0].value, "1");
686 assert_eq!(attributes[1].name, "name");
687 assert_eq!(attributes[1].value, "test");
688 }
689 _ => panic!("expected EmptyElement"),
690 }
691 }
692
693 #[test]
694 fn test_text_content() {
695 let mut reader = XmlReader::from_str("<root>Hello, World!</root>");
696
697 reader.next_event().unwrap(); match reader.next_event().unwrap() {
700 XmlEvent::Text(text) => {
701 assert_eq!(text, "Hello, World!");
702 }
703 _ => panic!("expected Text"),
704 }
705 }
706
707 #[test]
708 fn test_escaped_text() {
709 let mut reader = XmlReader::from_str("<root><Hello></root>");
710
711 reader.next_event().unwrap(); match reader.next_event().unwrap() {
714 XmlEvent::Text(text) => {
715 assert_eq!(text, "<Hello>");
716 }
717 _ => panic!("expected Text"),
718 }
719 }
720
721 #[test]
722 fn test_xml_declaration() {
723 let mut reader = XmlReader::from_str(r#"<?xml version="1.0" encoding="UTF-8"?><root/>"#);
724
725 match reader.next_event().unwrap() {
726 XmlEvent::XmlDecl { version, encoding, standalone } => {
727 assert_eq!(version, "1.0");
728 assert_eq!(encoding.as_deref(), Some("UTF-8"));
729 assert_eq!(standalone, None);
730 }
731 _ => panic!("expected XmlDecl"),
732 }
733 }
734
735 #[test]
736 fn test_comment() {
737 let mut reader = XmlReader::from_str("<!-- This is a comment --><root/>");
738
739 match reader.next_event().unwrap() {
740 XmlEvent::Comment(comment) => {
741 assert_eq!(comment, "This is a comment");
742 }
743 _ => panic!("expected Comment"),
744 }
745 }
746
747 #[test]
748 fn test_cdata() {
749 let mut reader = XmlReader::from_str("<root><![CDATA[<special>content</special>]]></root>");
750
751 reader.next_event().unwrap(); match reader.next_event().unwrap() {
754 XmlEvent::CData(data) => {
755 assert_eq!(data, "<special>content</special>");
756 }
757 _ => panic!("expected CData"),
758 }
759 }
760
761 #[test]
762 fn test_nested_elements() {
763 let xml = r#"<root><child1><grandchild/></child1><child2/></root>"#;
764 let mut reader = XmlReader::from_str(xml);
765
766 let events: Vec<_> = std::iter::from_fn(|| {
767 match reader.next_event() {
768 Ok(XmlEvent::Eof) => None,
769 Ok(event) => Some(event),
770 Err(_) => None,
771 }
772 }).collect();
773
774 assert_eq!(events.len(), 6);
775 }
776
777 #[test]
778 fn test_mismatched_tags() {
779 let mut reader = XmlReader::from_str("<root></wrong>");
780 reader.next_event().unwrap(); assert!(reader.next_event().is_err());
782 }
783
784 #[test]
785 fn test_unclosed_tag() {
786 let mut reader = XmlReader::from_str("<root>");
787 reader.next_event().unwrap(); assert!(reader.next_event().is_err());
789 }
790
791 #[test]
792 fn test_processing_instruction() {
793 let mut reader = XmlReader::from_str("<?target data?><root/>");
794
795 match reader.next_event().unwrap() {
796 XmlEvent::ProcessingInstruction { target, data } => {
797 assert_eq!(target, "target");
798 assert_eq!(data.as_deref(), Some("data"));
799 }
800 _ => panic!("expected ProcessingInstruction"),
801 }
802 }
803
804 #[test]
805 fn test_attribute_with_single_quotes() {
806 let mut reader = XmlReader::from_str("<root attr='value'/>");
807
808 match reader.next_event().unwrap() {
809 XmlEvent::EmptyElement { attributes, .. } => {
810 assert_eq!(attributes[0].value, "value");
811 }
812 _ => panic!("expected EmptyElement"),
813 }
814 }
815
816 #[test]
817 fn test_position_tracking() {
818 let xml = "<root>\n <child/>\n</root>";
819 let mut reader = XmlReader::from_str(xml);
820
821 reader.next_event().unwrap(); reader.next_event().unwrap(); let pos = reader.position();
825 assert!(pos.line >= 2);
826 }
827
828 #[test]
829 fn test_depth_tracking() {
830 let mut reader = XmlReader::from_str("<a><b><c></c></b></a>");
831
832 assert_eq!(reader.depth(), 0);
833 reader.next_event().unwrap(); assert_eq!(reader.depth(), 1);
835 reader.next_event().unwrap(); assert_eq!(reader.depth(), 2);
837 reader.next_event().unwrap(); assert_eq!(reader.depth(), 3);
839 reader.next_event().unwrap(); assert_eq!(reader.depth(), 2);
841 }
842
843 #[test]
844 fn test_invalid_escape_has_position() {
845 let mut reader = XmlReader::from_str("<a>&bogus;</a>");
846 reader.next_event().unwrap(); let err = reader.next_event().unwrap_err();
849 let pos = err.position().expect("invalid escape error should carry a position");
850 assert_eq!(pos.line, 1);
851 assert_eq!(pos.column, 4);
852 assert_eq!(pos.offset, 3);
853 }
854
855 #[test]
856 fn test_invalid_escape_in_attribute_has_position() {
857 let mut reader = XmlReader::from_str(r#"<a b="&bogus;"/>"#);
858
859 let err = reader.next_event().unwrap_err();
860 let pos = err.position().expect("invalid escape error should carry a position");
861 assert_eq!(pos.line, 1);
862 assert_eq!(pos.column, 7);
863 assert_eq!(pos.offset, 6);
864 }
865
866 #[test]
867 fn test_multiline_attribute_value_position() {
868 let mut reader = XmlReader::from_str("<root attr=\"a\nb\">\n <child/>\n</root>");
869
870 reader.next_event().unwrap(); assert_eq!(reader.position().line, 2);
872 reader.next_event().unwrap(); assert_eq!(reader.position().line, 3);
874 }
875
876 #[test]
877 fn test_multibyte_column_tracking() {
878 let mut reader = XmlReader::from_str("<a>héllo</a>");
879
880 reader.next_event().unwrap(); reader.next_event().unwrap(); assert_eq!(reader.position().column, 9);
884 assert_eq!(reader.position().offset, 9);
885 }
886
887 #[test]
888 fn test_doctype_with_quoted_gt() {
889 let mut reader = XmlReader::from_str(r#"<!DOCTYPE root SYSTEM "weird>name"><root/>"#);
890
891 match reader.next_event().unwrap() {
892 XmlEvent::EmptyElement { name, .. } => assert_eq!(name, "root"),
893 other => panic!("expected EmptyElement, got {:?}", other),
894 }
895 assert!(matches!(reader.next_event().unwrap(), XmlEvent::Eof));
896 }
897
898 #[test]
899 fn test_doctype_internal_subset() {
900 let mut reader =
901 XmlReader::from_str("<!DOCTYPE root [<!ELEMENT root (#PCDATA)>]><root/>");
902
903 match reader.next_event().unwrap() {
904 XmlEvent::EmptyElement { name, .. } => assert_eq!(name, "root"),
905 other => panic!("expected EmptyElement, got {:?}", other),
906 }
907 assert!(matches!(reader.next_event().unwrap(), XmlEvent::Eof));
908 }
909
910 #[test]
911 fn test_doctype_unterminated_quote() {
912 let mut reader = XmlReader::from_str(r#"<!DOCTYPE root SYSTEM "oops"#);
915 assert!(matches!(reader.next_event().unwrap(), XmlEvent::Eof));
916 }
917}