Repository: tafia/quick-xml (rust)

Issue:
Debug assertion triggered on empty doctype
Thank you for quick-xml and sorry for comming back with an other bug.

When parsing the file `<!DoctyPE>` using rusc "dev" profile the assertion `"DocType must have a name"` is triggered. In the "release" profile the assertion is not triggered and the parser returns an empty `Event::Doctype()`

Test failing in "dev" profile and passing in "release" profile:
```rust
#[test]
fn reproduce() {
    let data = b"<!DoctyPE>";
    let mut reader = Reader::from_reader(data.as_slice());
    let mut buf = Vec::new();
    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Eof) => break,
            Ok(Event::DocType(e)) => assert!(e.is_empty()), // assertion only executed in "release" mode
            _ => unreachable!()
        }
    }
}
```

Pull request description:
Return an error on empty doctype
The parser was crashing with debug assertions enabled but passing with debug assertions disabled

Closes #608

Patch:
diff --git a/Changelog.md b/Changelog.md
index f1bf280..1cb0acc 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -21,6 +21,8 @@
 - [#603]: Fix a regression from [#581] that an XML comment or a processing
   instruction between a <!DOCTYPE> and the root element in the file brokes
   deserialization of structs by returning `DeError::ExpectedStart`
+- [#608]: Return a new error `Error::EmptyDocType` on empty doctype instead
+  of crashing because of a debug assertion.
 
 ### Misc Changes
 
@@ -28,6 +30,7 @@
 [#601]: https://github.com/tafia/quick-xml/pull/601
 [#603]: https://github.com/tafia/quick-xml/pull/603
 [#606]: https://github.com/tafia/quick-xml/pull/606
+[#608]: https://github.com/tafia/quick-xml/issues/608
 
 
 ## 0.28.2 -- 2023-04-12
diff --git a/src/errors.rs b/src/errors.rs
index 265855e..14cd7a5 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -37,6 +37,10 @@ pub enum Error {
     /// `Event::BytesDecl` must start with *version* attribute. Contains the attribute
     /// that was found or `None` if an xml declaration doesn't contain attributes.
     XmlDeclWithoutVersion(Option<String>),
+    /// Empty `Event::DocType`. `<!doctype foo>` is correct but `<!doctype > is not.
+    ///
+    /// See <https://www.w3.org/TR/xml11/#NT-doctypedecl>
+    EmptyDocType,
     /// Attribute parsing error
     InvalidAttr(AttrError),
     /// Escape error
@@ -109,6 +113,7 @@ impl fmt::Display for Error {
                 "XmlDecl must start with 'version' attribute, found {:?}",
                 e
             ),
+            Error::EmptyDocType => write!(f, "DOCTYPE declaration must not be empty"),
             Error::InvalidAttr(e) => write!(f, "error while parsing attribute: {}", e),
             Error::EscapeError(e) => write!(f, "{}", e),
             Error::UnknownPrefix(prefix) => {
diff --git a/src/reader/parser.rs b/src/reader/parser.rs
index 1cb6086..b69df6f 100644
--- a/src/reader/parser.rs
+++ b/src/reader/parser.rs
@@ -117,7 +117,9 @@ impl Parser {
                     .iter()
                     .position(|b| !is_whitespace(*b))
                     .unwrap_or(len - 8);
-                debug_assert!(start < len - 8, "DocType must have a name");
+                if start + 8 >= len {
+                    return Err(Error::EmptyDocType);
+                }
                 Ok(Event::DocType(BytesText::wrap(
                     &buf[8 + start..],
                     self.decoder(),


Test patch:
diff --git a/tests/fuzzing.rs b/tests/fuzzing.rs
index 69f3b31..9189d66 100644
--- a/tests/fuzzing.rs
+++ b/tests/fuzzing.rs
@@ -2,15 +2,14 @@
 
 use quick_xml::events::Event;
 use quick_xml::reader::Reader;
-use std::io::Cursor;
+use quick_xml::Error;
 
 #[test]
 fn fuzz_53() {
     let data: &[u8] = b"\xe9\x00\x00\x00\x00\x00\x00\x00\x00\
 \x00\x00\x00\x00\n(\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\
 \x00<>\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00<<\x00\x00\x00";
-    let cursor = Cursor::new(data);
-    let mut reader = Reader::from_reader(cursor);
+    let mut reader = Reader::from_reader(data);
     let mut buf = vec![];
     loop {
         match reader.read_event_into(&mut buf) {
@@ -25,8 +24,7 @@ fn fuzz_101() {
     let data: &[u8] = b"\x00\x00<\x00\x00\x0a>&#44444444401?#\x0a413518\
                        #\x0a\x0a\x0a;<:<)(<:\x0a\x0a\x0a\x0a;<:\x0a\x0a\
                        <:\x0a\x0a\x0a\x0a\x0a<\x00*\x00\x00\x00\x00";
-    let cursor = Cursor::new(data);
-    let mut reader = Reader::from_reader(cursor);
+    let mut reader = Reader::from_reader(data);
     let mut buf = vec![];
     loop {
         match reader.read_event_into(&mut buf) {
@@ -50,3 +48,15 @@ fn fuzz_101() {
         buf.clear();
     }
 }
+
+#[test]
+fn fuzz_empty_doctype() {
+    let data: &[u8] = b"<!doctype  \n    >";
+    let mut reader = Reader::from_reader(data);
+    let mut buf = Vec::new();
+    assert!(matches!(
+        reader.read_event_into(&mut buf).unwrap_err(),
+        Error::EmptyDocType
+    ));
+    assert_eq!(reader.read_event_into(&mut buf).unwrap(), Event::Eof);
+}