diff --git a/Changelog.md b/Changelog.md index 205e5ecb..26cac81c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -68,6 +68,9 @@ The MSRV has been raised to 1.86. as ` `, ` `, and ` ` respectively, preventing silent data loss from XML attribute-value normalization on round-trip. Likewise `Attribute::from` performs the same transformation. +- [#953]: The serde `Deserializer` now correctly handles namespaces. Previously + the namespace bindings might be applied or removed before the event actually + was consumed which lead to a couple of bugs. - [#989]: `Attributes::new` and `Attributes::html` now return empty iterators when their starting position is past the end of the input instead of panicking. - [#977]: `NamespaceResolver::push` (and hence every `NsReader` `Start`/`Empty` @@ -99,9 +102,17 @@ The MSRV has been raised to 1.86. `decoded_and_normalized_value_with()`, `decode_and_unescape_value()`, and `decode_and_unescape_value_with()`. Use `normalized_value()` and `normalized_value_with()` instead. +- [#1002]: Added `NamespaceResolver::with` that allows temporary applying namespace + bindings from the start tag for the scope of a provided closure F, without making any + persistent change to the resolver. It is useful to check a peeked event which is + not yet consumed in custom implementations of peekable reader. +- [#1002]: Added `Deserializer::resolver` and `Deserializer::resolver_mut` methods + to get a namespace resolver used by this deserializer, because it no longer uses + an `NsReader` internally. [#670]: https://github.com/tafia/quick-xml/issues/670 [#859]: https://github.com/tafia/quick-xml/issues/859 +[#953]: https://github.com/tafia/quick-xml/issues/953 [#963]: https://github.com/tafia/quick-xml/pull/963 [#977]: https://github.com/tafia/quick-xml/issues/977 [#978]: https://github.com/tafia/quick-xml/issues/978 @@ -110,6 +121,7 @@ The MSRV has been raised to 1.86. [#989]: https://github.com/tafia/quick-xml/issues/989 [#990]: https://github.com/tafia/quick-xml/issues/990 [#1000]: https://github.com/tafia/quick-xml/pull/1000 +[#1002]: https://github.com/tafia/quick-xml/pull/1002 ## 0.41.0 -- 2026-06-29 diff --git a/src/de/map.rs b/src/de/map.rs index 324c6130..63a14226 100644 --- a/src/de/map.rs +++ b/src/de/map.rs @@ -221,21 +221,6 @@ where }) } - /// Determines if subtree started with the specified event should be skipped. - /// - /// Used to map elements with `xsi:nil` attribute set to true to `None` in optional contexts. - /// - /// We need to handle two attributes: - /// - on parent element: `` - /// - on this element: `` - /// - /// We check parent element too because `xsi:nil` affects only nested elements of the - /// tag where it is defined. We can map structure with fields mapped to attributes to - /// the `` element and set to `None` all its optional elements. - fn should_skip_subtree(&self, start: &BytesStart) -> bool { - self.de.reader.reader.has_nil_attr(&self.start) || self.de.reader.reader.has_nil_attr(start) - } - /// Skips whitespaces when they are not preserved #[inline] fn skip_whitespaces(&mut self) -> Result<(), DeError> { @@ -577,15 +562,16 @@ where where V: Visitor<'de>, { - // We cannot use result of `peek()` directly because of borrow checker - let _ = self.map.de.peek()?; - match self.map.de.last_peeked() { - DeEvent::Text(t) if t.is_empty() => visitor.visit_none(), - DeEvent::Start(start) if self.map.should_skip_subtree(start) => { - self.map.de.skip_next_tree()?; - visitor.visit_none() - } - _ => visitor.visit_some(self), + // `self.map.start` already was taken from the reader, so its namespace bindings were already processed. + let has_nil = self + .map + .start + .attributes() + .has_nil(&self.map.de.ns_resolver); + if self.map.de.deserialize_opt(Some(has_nil))? { + visitor.visit_some(self) + } else { + visitor.visit_none() } } diff --git a/src/de/mod.rs b/src/de/mod.rs index 973e70d0..51cc4885 100644 --- a/src/de/mod.rs +++ b/src/de/mod.rs @@ -2114,8 +2114,8 @@ use crate::{ errors::Error, escape::{parse_number, EscapeError}, events::{BytesCData, BytesEnd, BytesRef, BytesStart, BytesText, Event}, - name::QName, - reader::NsReader, + name::{NamespaceResolver, QName}, + reader::{NsReader, Reader}, }; use serde::de::{ self, Deserialize, DeserializeOwned, DeserializeSeed, IntoDeserializer, SeqAccess, Visitor, @@ -2536,6 +2536,8 @@ where { /// An XML reader that streams events into this deserializer reader: XmlReader<'de, R, E>, + /// A buffer to manage namespaces + ns_resolver: NamespaceResolver, /// When deserializing sequences sometimes we have to skip unwanted events. /// That events should be stored and then replayed. This is a replay buffer, @@ -2585,9 +2587,10 @@ where /// /// - [`Deserializer::from_str`] /// - [`Deserializer::from_reader`] - fn new(reader: R, entity_resolver: E) -> Self { + fn new(reader: R, ns_resolver: NamespaceResolver, entity_resolver: E) -> Self { Self { reader: XmlReader::new(reader, entity_resolver), + ns_resolver, #[cfg(feature = "overlapped-lists")] read: VecDeque::new(), @@ -2626,7 +2629,7 @@ where /// # use pretty_assertions::assert_eq; /// use serde::Deserialize; /// use quick_xml::de::Deserializer; - /// use quick_xml::NsReader; + /// use quick_xml::Reader; /// /// #[derive(Deserialize)] /// struct SomeStruct { @@ -2643,7 +2646,7 @@ where /// let err = SomeStruct::deserialize(&mut de); /// assert!(err.is_err()); /// - /// let reader: &NsReader<_> = de.get_ref().get_ref(); + /// let reader: &Reader<_> = de.get_ref().get_ref(); /// /// assert_eq!(reader.error_position(), 28); /// assert_eq!(reader.buffer_position(), 41); @@ -2652,6 +2655,22 @@ where &self.reader.reader } + /// Returns a storage of namespace bindings associated with this deserializer. + #[inline] + pub const fn resolver(&self) -> &NamespaceResolver { + &self.ns_resolver + } + + /// Returns a mutable reference to the storage of namespace bindings + /// associated with this deserializer. + /// + /// Useful for configuring the resolver, e.g. to change the + /// [namespace-binding limit](NamespaceResolver::set_max_namespace_bindings). + #[inline] + pub fn resolver_mut(&mut self) -> &mut NamespaceResolver { + &mut self.ns_resolver + } + /// Set the maximum number of events that could be skipped during deserialization /// of sequences. /// @@ -2770,6 +2789,7 @@ where return Ok(event); } // SAFETY: `self.read` was filled in the code above. + // NOTE: with msrv=1.95 we may use push_front_mut // NOTE: Can be replaced with `unsafe { std::hint::unreachable_unchecked() }` // if unsafe code will be allowed unreachable!() @@ -2782,35 +2802,38 @@ where } } - #[inline] - fn last_peeked(&self) -> &DeEvent<'de> { - #[cfg(feature = "overlapped-lists")] - { - self.read - .front() - .expect("`Deserializer::peek()` should be called") - } - #[cfg(not(feature = "overlapped-lists"))] - { - self.peek - .as_ref() - .expect("`Deserializer::peek()` should be called") - } + #[cfg(feature = "overlapped-lists")] + fn take_peeked(&mut self) -> Option> { + self.read.pop_front() } - fn next(&mut self) -> Result, DeError> { + #[cfg(not(feature = "overlapped-lists"))] + fn take_peeked(&mut self) -> Option> { + self.peek.take() + } + + fn next_impl(&mut self) -> Result, DeError> { // Replay skipped or peeked events - #[cfg(feature = "overlapped-lists")] - if let Some(event) = self.read.pop_front() { - return Ok(event); - } - #[cfg(not(feature = "overlapped-lists"))] - if let Some(e) = self.peek.take() { + if let Some(e) = self.take_peeked() { return Ok(e); } self.reader.next() } + fn next(&mut self) -> Result, DeError> { + match self.next_impl() { + Ok(DeEvent::Start(e)) => { + self.ns_resolver.push(&e)?; + Ok(DeEvent::Start(e)) + } + Ok(DeEvent::End(e)) => { + self.ns_resolver.pop(); + Ok(DeEvent::End(e)) + } + e => e, + } + } + fn skip_whitespaces(&mut self) -> Result<(), DeError> { loop { match self.peek()? { @@ -2993,11 +3016,10 @@ where /// Drops all events until event with [name](BytesEnd::name()) `name` won't be /// dropped. This method should be called after [`Self::next()`] - #[cfg(feature = "overlapped-lists")] fn read_to_end(&mut self, name: QName) -> Result<(), DeError> { let mut depth = 0; loop { - match self.read.pop_front() { + match self.take_peeked() { Some(DeEvent::Start(e)) if e.name() == name => { depth += 1; } @@ -3035,25 +3057,95 @@ where } } } + // read_to_end will consume closing tag. Because nobody can access to its + // content anymore, we directly pop namespace of the opening tag + self.ns_resolver.pop(); Ok(()) } - #[cfg(not(feature = "overlapped-lists"))] - fn read_to_end(&mut self, name: QName) -> Result<(), DeError> { - // First one might be in self.peek - match self.next()? { - DeEvent::Start(e) => self.reader.read_to_end(e.name())?, - DeEvent::End(e) if e.name() == name => return Ok(()), - _ => (), - } - self.reader.read_to_end(name) - } - fn skip_next_tree(&mut self) -> Result<(), DeError> { - let DeEvent::Start(start) = self.next()? else { - unreachable!("Only call this if the next event is a start event") + /// Determines if `Option` should be deserialized as `Some` or `None`. + /// + /// It handles `xsi:nil` attribute in two places: + /// - on parent element: `` + /// - on checked element: `` + /// + /// According to the [specification], `xsi:nil` controls only ability to (not) have nested + /// elements, but it does not applied to attributes: + /// + /// > 2.7.2 xsi:nil + /// > ------------- + /// > + /// > _XML Schema Definition Language: Structures_ introduces a mechanism for signaling that + /// > an element must be accepted as ·valid· when it has no content despite a content type + /// > which does not require or even necessarily allow empty content. An element can be + /// > ·valid· without content if it has the attribute `xsi:nil` with the value `true`. + /// > An element so labeled must be empty, but can carry attributes if permitted by the + /// > corresponding complex type. + /// + /// Due to that we must deserialize all attributes from the ``. + /// To get an access to them we define Rust struct as follow: + /// + /// ```ignore + /// struct MapTag { + /// #[serde(rename = "@attr")] + /// attr: String, + /// // element + /// opt: Option, + /// } + /// ``` + /// + /// `` should be deserialized as + /// `MapTag { attr: "value", foo: None }`. + /// + /// `` is invalid XML (see the quote from + /// the specification above), but will be deserialized the same. + /// + /// When we at top-level, `parent` is `None` and we handle only the `` case. + /// + /// Returns `true` if `visit_some()` should be called and `false` if `visit_none()`. + /// + /// [specification]: https://www.w3.org/TR/xmlschema11-1/#Instance_Document_Constructions + fn deserialize_opt(&mut self, parent_is_nil: Option) -> Result { + // We cannot use result of `peek()` directly because of borrow checker, so it's inlined here + #[cfg(feature = "overlapped-lists")] + let event = { + if self.read.is_empty() { + self.read.push_front(self.reader.next()?); + } + // SAFETY: `self.read` was filled in the code above. + // NOTE: with msrv=1.95 we may use push_front_mut + self.read + .front() + .expect("`self.read` was filled in the code above") + }; + + #[cfg(not(feature = "overlapped-lists"))] + let event = match &mut self.peek { + Some(event) => event, + empty_peek @ None => empty_peek.insert(self.reader.next()?), }; - let name = start.name(); - self.read_to_end(name) + + Ok(match event { + DeEvent::Text(t) if t.is_empty() => false, + // If we inside the tree, call visit_some for Eof to get an error from the visitor + // (getting Eof means that XML tag is not closed). On top-level Eof is true None + DeEvent::Eof => parent_is_nil.is_some(), + // if the `xsi:nil` attribute is set to true we got a none value + DeEvent::Start(start) + // Because we only peek event here, its namespace bindings not yet processed. + // Temporary push them inside `with` to check the presence of `xsi:nil` + if parent_is_nil.unwrap_or(false) || self.ns_resolver.with(start, |resolver| { + start.attributes().has_nil(resolver) + })? => + { + let DeEvent::Start(start) = self.next()? else { + unreachable!("Just checked that the next event is a start event") + }; + self.read_to_end(start.name())?; + false + } + _ => true, + }) } /// Method for testing Deserializer implementation. Checks that all events was consumed during @@ -3137,15 +3229,25 @@ where /// Note, that config option [`Config::expand_empty_elements`] will be set to `true`. /// /// [`Config::expand_empty_elements`]: crate::reader::Config::expand_empty_elements - pub fn borrowing_with_resolver(mut reader: NsReader<&'de [u8]>, entity_resolver: E) -> Self { + pub fn borrowing_with_resolver(reader: NsReader<&'de [u8]>, entity_resolver: E) -> Self { + let NsReader { + mut reader, + mut ns_resolver, + pending_pop, + } = reader; let config = reader.config_mut(); config.expand_empty_elements = true; + if pending_pop { + ns_resolver.pop(); + } + Self::new( SliceReader { reader, version: XmlVersion::Implicit1_0, }, + ns_resolver, entity_resolver, ) } @@ -3219,7 +3321,7 @@ where /// will borrow instead of copy. If you have `&[u8]` which is known to represent /// UTF-8, you can decode it first before using [`from_str`]. pub fn with_resolver(reader: R, entity_resolver: E) -> Self { - let mut reader = NsReader::from_reader(reader); + let mut reader = Reader::from_reader(reader); let config = reader.config_mut(); config.expand_empty_elements = true; @@ -3229,6 +3331,7 @@ where buf: Vec::new(), version: XmlVersion::Implicit1_0, }, + NamespaceResolver::default(), entity_resolver, ) } @@ -3239,16 +3342,26 @@ where /// Note, that config option [`Config::expand_empty_elements`] will be set to `true`. /// /// [`Config::expand_empty_elements`]: crate::reader::Config::expand_empty_elements - pub fn buffering_with_resolver(mut reader: NsReader, entity_resolver: E) -> Self { + pub fn buffering_with_resolver(reader: NsReader, entity_resolver: E) -> Self { + let NsReader { + mut reader, + mut ns_resolver, + pending_pop, + } = reader; let config = reader.config_mut(); config.expand_empty_elements = true; + if pending_pop { + ns_resolver.pop(); + } + Self::new( IoReader { reader, buf: Vec::new(), version: XmlVersion::Implicit1_0, }, + ns_resolver, entity_resolver, ) } @@ -3370,17 +3483,10 @@ where where V: Visitor<'de>, { - // We cannot use result of `peek()` directly because of borrow checker - let _ = self.peek()?; - match self.last_peeked() { - DeEvent::Text(t) if t.is_empty() => visitor.visit_none(), - DeEvent::Eof => visitor.visit_none(), - // if the `xsi:nil` attribute is set to true we got a none value - DeEvent::Start(start) if self.reader.reader.has_nil_attr(start) => { - self.skip_next_tree()?; - visitor.visit_none() - } - _ => visitor.visit_some(self), + if self.deserialize_opt(None)? { + visitor.visit_some(self) + } else { + visitor.visit_none() } } @@ -3480,12 +3586,6 @@ pub trait XmlRead<'i> { /// Return an XML version of the source. fn xml_version(&self) -> XmlVersion; - - /// Checks if the `start` tag has a [`xsi:nil`] attribute. This method ignores - /// any errors in attributes. - /// - /// [`xsi:nil`]: https://www.w3.org/TR/xmlschema-1/#xsi_nil - fn has_nil_attr(&self, start: &BytesStart) -> bool; } /// XML input source that reads from a std::io input stream. @@ -3493,7 +3593,7 @@ pub trait XmlRead<'i> { /// You cannot create it, it is created automatically when you call /// [`Deserializer::from_reader`] pub struct IoReader { - reader: NsReader, + reader: Reader, buf: Vec, version: XmlVersion, } @@ -3506,7 +3606,7 @@ impl IoReader { /// use serde::Deserialize; /// use std::io::Cursor; /// use quick_xml::de::Deserializer; - /// use quick_xml::NsReader; + /// use quick_xml::Reader; /// /// #[derive(Deserialize)] /// struct SomeStruct { @@ -3523,12 +3623,12 @@ impl IoReader { /// let err = SomeStruct::deserialize(&mut de); /// assert!(err.is_err()); /// - /// let reader: &NsReader> = de.get_ref().get_ref(); + /// let reader: &Reader> = de.get_ref().get_ref(); /// /// assert_eq!(reader.error_position(), 28); /// assert_eq!(reader.buffer_position(), 41); /// ``` - pub const fn get_ref(&self) -> &NsReader { + pub const fn get_ref(&self) -> &Reader { &self.reader } } @@ -3559,10 +3659,6 @@ impl<'i, R: BufRead> XmlRead<'i> for IoReader { fn xml_version(&self) -> XmlVersion { self.version } - - fn has_nil_attr(&self, start: &BytesStart) -> bool { - start.attributes().has_nil(self.reader.resolver()) - } } /// XML input source that reads from a slice of bytes and can borrow from it. @@ -3570,7 +3666,7 @@ impl<'i, R: BufRead> XmlRead<'i> for IoReader { /// You cannot create it, it is created automatically when you call /// [`Deserializer::from_str`]. pub struct SliceReader<'de> { - reader: NsReader<&'de [u8]>, + reader: Reader<&'de [u8]>, version: XmlVersion, } @@ -3581,7 +3677,7 @@ impl<'de> SliceReader<'de> { /// # use pretty_assertions::assert_eq; /// use serde::Deserialize; /// use quick_xml::de::Deserializer; - /// use quick_xml::NsReader; + /// use quick_xml::Reader; /// /// #[derive(Deserialize)] /// struct SomeStruct { @@ -3598,12 +3694,12 @@ impl<'de> SliceReader<'de> { /// let err = SomeStruct::deserialize(&mut de); /// assert!(err.is_err()); /// - /// let reader: &NsReader<&[u8]> = de.get_ref().get_ref(); + /// let reader: &Reader<&[u8]> = de.get_ref().get_ref(); /// /// assert_eq!(reader.error_position(), 28); /// assert_eq!(reader.buffer_position(), 41); /// ``` - pub const fn get_ref(&self) -> &NsReader<&'de [u8]> { + pub const fn get_ref(&self) -> &Reader<&'de [u8]> { &self.reader } } @@ -3632,10 +3728,6 @@ impl<'de> XmlRead<'de> for SliceReader<'de> { fn xml_version(&self) -> XmlVersion { self.version } - - fn has_nil_attr(&self, start: &BytesStart) -> bool { - start.attributes().has_nil(self.reader.resolver()) - } } #[cfg(test)] @@ -4218,12 +4310,12 @@ mod tests { "#; let mut reader1 = IoReader { - reader: NsReader::from_reader(s.as_bytes()), + reader: Reader::from_reader(s.as_bytes()), buf: Vec::new(), version: XmlVersion::Implicit1_0, }; let mut reader2 = SliceReader { - reader: NsReader::from_str(s), + reader: Reader::from_str(s), version: XmlVersion::Implicit1_0, }; @@ -4249,7 +4341,7 @@ mod tests { "#; let mut reader = SliceReader { - reader: NsReader::from_str(s), + reader: Reader::from_str(s), version: XmlVersion::Implicit1_0, }; diff --git a/src/errors.rs b/src/errors.rs index abcaf885..0982f448 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -418,6 +418,13 @@ pub mod serialize { } } + impl From for DeError { + #[inline] + fn from(e: NamespaceError) -> Self { + Self::InvalidXml(e.into()) + } + } + /// Serialization error #[derive(Clone, Debug)] pub enum SeError { diff --git a/src/name.rs b/src/name.rs index e38c106a..e3dd1053 100644 --- a/src/name.rs +++ b/src/name.rs @@ -777,6 +777,18 @@ impl NamespaceResolver { self.set_level(self.nesting_level.saturating_sub(1)); } + /// Runs action as if all namespaces from the specified `start` element were added + /// to the resolver, but without actually changing the resolver state. + pub fn with(&mut self, start: &BytesStart, mut action: F) -> Result + where + F: FnMut(&Self) -> R, + { + self.push(start)?; + let result = action(self); + self.pop(); + Ok(result) + } + /// Sets new number of [`push`] calls that were not followed by [`pop`] calls. /// /// When set to value lesser than current [`level`], behaves as if [`pop`] diff --git a/src/reader/ns_reader.rs b/src/reader/ns_reader.rs index 0d164d89..e7b1c6c7 100644 --- a/src/reader/ns_reader.rs +++ b/src/reader/ns_reader.rs @@ -20,13 +20,13 @@ use crate::reader::{Config, Reader, Span, XmlSource}; #[derive(Debug, Clone)] pub struct NsReader { /// An XML reader - pub(super) reader: Reader, + pub(crate) reader: Reader, /// A buffer to manage namespaces - pub(super) ns_resolver: NamespaceResolver, + pub(crate) ns_resolver: NamespaceResolver, /// We cannot pop data from the namespace stack until returned `Empty` or `End` /// event will be processed by the user, so we only mark that we should that /// in the next [`Self::read_event_impl()`] call. - pending_pop: bool, + pub(crate) pending_pop: bool, } /// Builder methods diff --git a/tests/serde-de-xsi.rs b/tests/serde-de-xsi.rs index 31350c94..a2ff454c 100644 --- a/tests/serde-de-xsi.rs +++ b/tests/serde-de-xsi.rs @@ -239,6 +239,43 @@ mod top_level_option { } } } + + /// Namespace binding from nested element must not leak into the parent element + mod nested_nil { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn unit() { + assert_eq!( + from_str::>( + "\ + \ + \ + \ + " + ) + .unwrap(), + Some(()) + ); + } + + #[test] + fn string() { + assert!(matches!( + from_str::>( + "\ + \ + \ + text\ + \ + " + ) + .unwrap_err(), + DeError::MixedContent(x) if x == "any" + )); + } + } } mod as_field { @@ -701,4 +738,174 @@ mod as_field { } } } + + /// Namespace binding from nested element must not leak into the parent element + mod nested_nil { + use super::*; + use pretty_assertions::assert_eq; + + mod field { + use super::*; + use pretty_assertions::assert_eq; + + #[derive(Debug, Deserialize, PartialEq)] + struct Xml { + value: Option<()>, + } + + #[test] + fn nil_on_parent() { + assert_eq!( + from_str::( + "\ + \ + \ + \ + " + ) + .unwrap(), + Xml { value: Some(()) } + ); + } + + #[test] + fn nil_on_self() { + assert_eq!( + from_str::( + "\ + \ + \ + \ + \ + \ + " + ) + .unwrap(), + Xml { value: Some(()) } + ); + } + } + + #[test] + fn text() { + #[derive(Debug, Deserialize, PartialEq)] + struct Xml { + #[serde(rename = "$text")] + value: Option<&'static str>, + } + assert_eq!( + from_str::( + "\ + \ + \ + text\ + \ + " + ) + .unwrap(), + Xml { + value: Some("text"), + } + ); + } + + mod value { + use super::*; + use pretty_assertions::assert_eq; + + #[derive(Debug, Deserialize, PartialEq)] + struct Xml { + #[serde(rename = "$value")] + value: Option<()>, + } + + #[test] + fn nil_on_parent() { + assert_eq!( + from_str::( + "\ + \ + \ + \ + " + ) + .unwrap(), + Xml { value: Some(()) } + ); + } + + #[test] + fn nil_on_self() { + assert_eq!( + from_str::( + "\ + \ + \ + \ + \ + \ + " + ) + .unwrap(), + Xml { value: Some(()) } + ); + } + } + + /// Namespace binding from the previous element or its child must not leak into the next element + /// + /// Reaches `Deserializer::read_to_end`, because it is used to skip subtree that was mapped + /// to unit struct. + mod unit { + use super::*; + use pretty_assertions::assert_eq; + + #[derive(Debug, Deserialize, PartialEq)] + struct Xml { + unit: (), + some: Option<()>, + } + + #[test] + fn nil_on_parent() { + assert_eq!( + from_str::( + "\ + \ + \ + \ + \ + " + ) + .unwrap(), + Xml { + unit: (), + some: Some(()), + } + ); + } + + #[test] + fn nil_on_self() { + assert_eq!( + from_str::( + "\ + \ + \ + \ + \ + \ + \ + \ + " + ) + .unwrap(), + Xml { + unit: (), + some: Some(()), + } + ); + } + } + } } diff --git a/tests/serde-issues.rs b/tests/serde-issues.rs index 6e8b2d7b..6320346f 100644 --- a/tests/serde-issues.rs +++ b/tests/serde-issues.rs @@ -931,6 +931,59 @@ fn issue928() { ); } +/// Regression test for https://github.com/tafia/quick-xml/issues/953. +mod issue953 { + use super::*; + use pretty_assertions::assert_eq; + + #[derive(Debug, Deserialize, PartialEq)] + struct Root { + vec: Vec<()>, + opt: Option, + } + + #[derive(Debug, Deserialize, PartialEq)] + struct Value { + field: (), + } + + #[test] + fn open_close() { + let input = r#" + + + + + "#; + let value: Root = from_str(&input).unwrap(); + assert_eq!( + value, + Root { + vec: vec![()], + opt: None, + } + ); + } + + #[test] + fn self_closed() { + let input = r#" + + + + + "#; + let value: Root = from_str(&input).unwrap(); + assert_eq!( + value, + Root { + vec: vec![()], + opt: None, + } + ); + } +} + /// Regression tests for https://github.com/tafia/quick-xml/issues/978. /// /// Deeply nested XML should produce `DeError::TooDeeplyNested` instead of diff --git a/tests/serde_helpers/mod.rs b/tests/serde_helpers/mod.rs index 637e732b..4132239a 100644 --- a/tests/serde_helpers/mod.rs +++ b/tests/serde_helpers/mod.rs @@ -1,5 +1,6 @@ //! Utility functions for serde integration tests +use pretty_assertions::assert_eq; use quick_xml::de::Deserializer; use quick_xml::DeError; use serde::Deserialize; @@ -14,11 +15,26 @@ where // Log XML that we try to deserialize to see it in the failed tests output dbg!(source); let mut de = Deserializer::from_str(source); + assert_eq!( + de.resolver().level(), + 0, + "no user namespace bindings expected just after creation: {:#?}", + de.resolver() + ); + let result = T::deserialize(&mut de); // If type was deserialized, the whole XML document should be consumed if result.is_ok() { de.check_eof_reached(); + + let resolver = de.resolver(); + assert_eq!( + resolver.level(), + 0, + "all namespace bindings must be popped, level should be zero: {:#?}", + resolver + ); } result