Repository: tafia/quick-xml
Pull request #772 (2024-06-26 21:28:23)

Add `allow_unmatched_ends` reader config option
Fixes https://github.com/tafia/quick-xml/issues/770.


Patch:
diff --git a/Changelog.md b/Changelog.md
index 2882371..ab8548b 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -15,10 +15,14 @@
 
 ### New Features
 
+- [#772]: Add `reader::Config::allow_unmatched_ends` to permit dangling end tags
+
 ### Bug Fixes
 
 ### Misc Changes
 
+[#772]: https://github.com/tafia/quick-xml/pull/772
+
 
 ## 0.34.0 -- 2024-06-25
 
diff --git a/src/reader/mod.rs b/src/reader/mod.rs
index 843b703..68853cb 100644
--- a/src/reader/mod.rs
+++ b/src/reader/mod.rs
@@ -76,6 +76,25 @@ pub struct Config {
     /// [`expand_empty_elements`]: Self::expand_empty_elements
     pub check_end_names: bool,
 
+    /// Whether unmatched closing tag names should be allowed. Unless enabled,
+    /// in case of a dangling end tag, the [`Error::IllFormed(UnmatchedEndTag)`]
+    /// is returned from read methods.
+    ///
+    /// When set to `true`, it won't check if a closing tag has a corresponding
+    /// opening tag at all. For example, `<a></a></b>` will be permitted.
+    ///
+    /// Note that the emitted [`End`] event will not be modified if this is enabled,
+    /// ie. it will contain the data of the unmatched end tag.
+    ///
+    /// Note, that setting this to `true` will lead to additional allocates that
+    /// needed to store tag name for an [`End`] event.
+    ///
+    /// Default: `false`
+    ///
+    /// [`Error::IllFormed(UnmatchedEndTag)`]: crate::errors::IllFormedError::UnmatchedEndTag
+    /// [`End`]: crate::events::Event::End
+    pub allow_unmatched_ends: bool,
+
     /// Whether empty elements should be split into an `Open` and a `Close` event.
     ///
     /// When set to `true`, all [`Empty`] events produced by a self-closing tag
@@ -192,6 +211,7 @@ impl Default for Config {
         Self {
             check_comments: false,
             check_end_names: true,
+            allow_unmatched_ends: false,
             expand_empty_elements: false,
             trim_markup_names_in_closing_tags: true,
             trim_text_start: false,
diff --git a/src/reader/state.rs b/src/reader/state.rs
index 9b1117e..2f3e851 100644
--- a/src/reader/state.rs
+++ b/src/reader/state.rs
@@ -212,12 +212,14 @@ impl ReaderState {
                 self.opened_buffer.truncate(start);
             }
             None => {
-                // Report error at start of the end tag at `<` character
-                // -2 for `<` and `>`
-                self.last_error_offset = self.offset - buf.len() as u64 - 2;
-                return Err(Error::IllFormed(IllFormedError::UnmatchedEndTag(
-                    decoder.decode(name).unwrap_or_default().into_owned(),
-                )));
+                if !self.config.allow_unmatched_ends {
+                    // Report error at start of the end tag at `<` character
+                    // -2 for `<` and `>`
+                    self.last_error_offset = self.offset - buf.len() as u64 - 2;
+                    return Err(Error::IllFormed(IllFormedError::UnmatchedEndTag(
+                        decoder.decode(name).unwrap_or_default().into_owned(),
+                    )));
+                }
             }
         }
 


Test patch:
diff --git a/tests/reader-config.rs b/tests/reader-config.rs
index fd968ce..bd2465e 100644
--- a/tests/reader-config.rs
+++ b/tests/reader-config.rs
@@ -344,6 +344,27 @@ mod check_end_names {
             );
             assert_eq!(reader.read_event().unwrap(), Event::Eof);
         }
+
+        #[test]
+        fn unmatched_end_tags() {
+            let mut reader = Reader::from_str("<tag></tag></unmatched>");
+            reader.config_mut().allow_unmatched_ends = true;
+
+            assert_eq!(
+                reader.read_event().unwrap(),
+                Event::Start(BytesStart::new("tag"))
+            );
+            assert_eq!(
+                reader.read_event().unwrap(),
+                Event::End(BytesEnd::new("tag"))
+            );
+            // #770: We want to allow this
+            assert_eq!(
+                reader.read_event().unwrap(),
+                Event::End(BytesEnd::new("unmatched"))
+            );
+            assert_eq!(reader.read_event().unwrap(), Event::Eof);
+        }
     }
 }