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