Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@

### Misc Changes

- [#1009]: Split of `BytesComment` from `BytesText`.

[#1005]: https://github.com/tafia/quick-xml/pull/1005
[#1007]: https://github.com/tafia/quick-xml/pull/1007
[#1009]: https://github.com/tafia/quick-xml/pull/1009


## 0.42.0 -- 2026-08-22
Expand Down
12 changes: 8 additions & 4 deletions fuzz/fuzz_targets/fuzz_chunked_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ fuzz_target!(|data: &[u8]| {
// is not legal, and any capacity `>= data.len()` degenerates to the
// Cursor case already covered by `fuzz_target_1`, so a small range is
// both sufficient and the most efficient use of fuzz budget.
let Some((&cap_byte, xml)) = data.split_first() else { return };
let Some((&cap_byte, xml)) = data.split_first() else {
return;
};
let capacity = (cap_byte as usize).max(1);

let mut reader =
Reader::from_reader(BufReader::with_capacity(capacity, Cursor::new(xml)));
let mut reader = Reader::from_reader(BufReader::with_capacity(capacity, Cursor::new(xml)));
let mut buf = Vec::new();
loop {
// Touch the event payload enough to exercise the borrowed-data
Expand All @@ -50,7 +51,10 @@ fuzz_target!(|data: &[u8]| {
}
}
}
Ok(Event::Text(ref e)) | Ok(Event::Comment(ref e)) | Ok(Event::DocType(ref e)) => {
Ok(Event::Text(ref e)) | Ok(Event::DocType(ref e)) => {
let _ = black_box(e);
}
Ok(Event::Comment(ref e)) => {
let _ = black_box(e);
}
Ok(Event::CData(e)) => {
Expand Down
7 changes: 5 additions & 2 deletions fuzz/fuzz_targets/fuzz_target_1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use libfuzzer_sys::fuzz_target;
use std::hint::black_box;

use quick_xml::{events::Event, reader::Reader, writer::Writer, XmlVersion};
use quick_xml::{XmlVersion, events::Event, reader::Reader, writer::Writer};
use std::io::Cursor;

macro_rules! debug_format {
Expand Down Expand Up @@ -42,7 +42,10 @@ where
}
}
}
Ok(Event::Text(ref e)) | Ok(Event::Comment(ref e)) | Ok(Event::DocType(ref e)) => {
Ok(Event::Text(ref e)) | Ok(Event::DocType(ref e)) => {
debug_format!(e);
}
Ok(Event::Comment(ref e)) => {
debug_format!(e);
}
Ok(Event::CData(e)) => {
Expand Down
206 changes: 194 additions & 12 deletions src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ use attributes::{AttrError, Attribute, Attributes};
/// assert_eq!(reader.read_event().unwrap(), Event::Empty(event.borrow()));
/// assert_eq!(reader.read_event().unwrap(), Event::Start(event.borrow()));
/// // deref coercion of &BytesStart to &str
/// assert_eq!(&event as &str, content);
/// // AsRef<str> for &T
/// assert_eq!(event.as_ref(), content);
/// ```
///
Expand Down Expand Up @@ -385,6 +387,8 @@ impl<'i> arbitrary::Arbitrary<'i> for BytesStart<'i> {
/// assert_eq!(reader.read_event().unwrap(), Event::End(event.borrow()));
/// assert_eq!(event.name().as_ref(), content);
/// // deref coercion of &BytesEnd to &str
/// assert_eq!(&event as &str, content);
/// // AsRef<str> for &T
/// assert_eq!(event.as_ref(), content);
/// ```
///
Expand Down Expand Up @@ -496,11 +500,9 @@ impl<'i> arbitrary::Arbitrary<'i> for BytesEnd<'i> {
/// Data from various events (most notably, `Event::Text`).
///
/// This event implements `Deref<Target = str>`. The `deref()` implementation
/// returns the content of this event. In case of comment this is everything
/// between `<!--` and `-->` and the text of comment may not contain `-->` inside
/// (if [`Config::check_comments`] is set to `true`).
/// In case of DTD this is everything between `<!DOCTYPE` + spaces and closing `>`
/// (i.e. in case of DTD the first character is never space):
/// returns the content of this event. In case of DTD this is everything between
/// `<!DOCTYPE` + spaces and closing `>` (i.e. in case of DTD the first character
/// is never space):
///
/// ```
/// # use quick_xml::events::{BytesText, Event};
Expand All @@ -509,17 +511,17 @@ impl<'i> arbitrary::Arbitrary<'i> for BytesEnd<'i> {
/// // Remember, that \ at the end of string literal strips
/// // all space characters to the first non-space character
/// let mut reader = Reader::from_str("\
/// <!DOCTYPE comment or text >\
/// comment or text \
/// <!--comment or text -->"
/// <!DOCTYPE text >\
/// text "
/// );
/// let content = "comment or text ";
/// let content = "text ";
/// let event = BytesText::new(content);
///
/// assert_eq!(reader.read_event().unwrap(), Event::DocType(event.borrow()));
/// assert_eq!(reader.read_event().unwrap(), Event::Text(event.borrow()));
/// assert_eq!(reader.read_event().unwrap(), Event::Comment(event.borrow()));
/// // deref coercion of &BytesText to &str
/// assert_eq!(&event as &str, content);
/// // AsRef<str> for &T
/// assert_eq!(event.as_ref(), content);
/// ```
///
Expand All @@ -531,7 +533,6 @@ impl<'i> arbitrary::Arbitrary<'i> for BytesEnd<'i> {
/// If such event need to outlive the single parsing loop iteration, take ownership of the data
/// using [`.into_owned()`].
///
/// [`Config::check_comments`]: crate::reader::Config::check_comments
/// [`.into_owned()`]: Self::into_owned
#[derive(Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct BytesText<'i> {
Expand Down Expand Up @@ -743,6 +744,8 @@ impl<'i> arbitrary::Arbitrary<'i> for BytesText<'i> {
///
/// assert_eq!(reader.read_event().unwrap(), Event::CData(event.borrow()));
/// // deref coercion of &BytesCData to &str
/// assert_eq!(&event as &str, content);
/// // AsRef<str> for &T
/// assert_eq!(event.as_ref(), content);
/// ```
///
Expand Down Expand Up @@ -1013,6 +1016,179 @@ impl FusedIterator for CDataIterator<'_> {}

////////////////////////////////////////////////////////////////////////////////////////////////////

/// A data between `<!--` and `-->` of an XML comment.
///
/// This event implements `Deref<Target = str>`. The `deref()` implementation
/// returns the content of this event. This is everything between `<!--` and `-->`
/// and the text of comment may not contain `--` inside (when [`Config::check_comments`] is set to `true`).
///
/// ```
/// # use quick_xml::events::{BytesComment, Event};
/// # use quick_xml::reader::Reader;
/// # use pretty_assertions::assert_eq;
/// let mut reader = Reader::from_str("<!--comment -- -->");
/// let content = "comment -- ";
/// let event = BytesComment::new(content).unwrap();
///
/// assert_eq!(reader.read_event().unwrap(), Event::Comment(event.borrow()));
/// // deref coercion of &BytesComment to &str
/// assert_eq!(&event as &str, content);
/// // AsRef<str> for &T
/// assert_eq!(event.as_ref(), content);
/// ```
///
/// # Lifetime
///
/// `'i` (stands of "input") is a lifetime of the original buffer from which event was parsed.
/// In particular, when reader was created from a string, this is lifetime of the string.
/// If event come from a buffered reader, this is lifetime of the user-provided buffer.
/// If such event need to outlive the single parsing loop iteration, take ownership of the data
/// using [`.into_owned()`].
///
/// [`Config::check_comments`]: crate::reader::Config::check_comments
/// [`.into_owned()`]: Self::into_owned
#[derive(Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct BytesComment<'i> {
content: BytesText<'i>,
}

impl<'i> BytesComment<'i> {
/// Creates a new `BytesComment` from a string as it appeared in the XML source.
#[inline]
pub(crate) const fn wrap(content: &'i str) -> Self {
Self {
content: BytesText::wrap(content),
}
}

/// Creates a new comment from a string. Returns error if `content` contains the `-->` sequence,
/// with the position of `-->`.
///
/// ```
/// # use quick_xml::events::BytesComment;
/// #
/// assert!(BytesComment::new("--").is_ok());
/// assert!(matches!(BytesComment::new("illegal -->"), Err(8)));
/// ```
#[inline]
pub fn new(content: &'i str) -> Result<Self, usize> {
match content.find("-->") {
Some(p) => Err(p),
None => Ok(Self::wrap(content)),
}
}

/// Ensures that all data is owned to extend the object's lifetime if necessary.
#[inline]
pub fn into_owned(self) -> BytesComment<'static> {
BytesComment {
content: self.content.into_owned(),
}
}

/// Extracts the inner `Cow` from the `BytesComment` event container.
#[inline]
pub fn into_inner(self) -> Cow<'i, str> {
self.content.into_inner()
}

/// Converts the event into a borrowed event.
#[inline]
pub fn borrow(&self) -> BytesComment<'_> {
BytesComment {
content: self.content.borrow(),
}
}

/// Returns the content of the XML 1.0 or HTML event with EOL normalization applied.
///
/// This will allocate if EOL normalization is required.
///
/// Note, that this method should be used only if event represents XML 1.0 or HTML content,
/// because rules for normalizing EOLs for [XML 1.0] / [HTML] and [XML 1.1] differs.
///
/// This method also can be used to get HTML content, because rules the same.
///
/// [XML 1.0]: https://www.w3.org/TR/xml/#sec-line-ends
/// [XML 1.1]: https://www.w3.org/TR/xml11/#sec-line-ends
/// [HTML]: https://html.spec.whatwg.org/#normalize-newlines
#[inline]
pub fn xml10_content(&self) -> Cow<'i, str> {
self.content.xml10_content()
}

/// Returns the content of the XML 1.1 event with EOL normalization applied.
///
/// This will allocate if EOL normalization is required.
///
/// Note, that this method should be used only if event represents XML 1.1 content,
/// because rules for normalizing EOLs for [XML 1.0], [XML 1.1] and [HTML] differs.
///
/// To get HTML content use [`xml10_content()`](Self::xml10_content).
///
/// [XML 1.0]: https://www.w3.org/TR/xml/#sec-line-ends
/// [XML 1.1]: https://www.w3.org/TR/xml11/#sec-line-ends
/// [HTML]: https://html.spec.whatwg.org/#normalize-newlines
#[inline]
pub fn xml11_content(&self) -> Cow<'i, str> {
self.content.xml11_content()
}

/// Returns the content of the XML event with EOL normalization applied
/// according to the specified version.
///
/// This will allocate if EOL normalization is required.
#[inline]
pub fn xml_content(&self, version: XmlVersion) -> Cow<'i, str> {
self.content.xml_content(version)
}

/// Alias for [`xml10_content()`](Self::xml10_content).
#[inline]
pub fn html_content(&self) -> Cow<'i, str> {
self.content.html_content()
}
}

impl<'i> Debug for BytesComment<'i> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "BytesComment {{ content: ")?;
write_cow_string(f, &self.content.content)?;
write!(f, " }}")
}
}

impl<'i> Deref for BytesComment<'i> {
type Target = str;

fn deref(&self) -> &str {
&self.content
}
}

impl AsRef<str> for BytesComment<'_> {
fn as_ref(&self) -> &str {
self
}
}

#[cfg(feature = "arbitrary")]
impl<'i> arbitrary::Arbitrary<'i> for BytesComment<'i> {
fn arbitrary(u: &mut arbitrary::Unstructured<'i>) -> arbitrary::Result<Self> {
let s = <&str>::arbitrary(u)?;
if !s.chars().all(char::is_alphanumeric) {
return Err(arbitrary::Error::IncorrectFormat);
}
Self::new(s).map_err(|_| arbitrary::Error::IncorrectFormat)
}

fn size_hint(depth: usize) -> (usize, Option<usize>) {
<&str as arbitrary::Arbitrary>::size_hint(depth)
}
}

////////////////////////////////////////////////////////////////////////////////////////////////////

/// [Processing instructions][PI] (PIs) allow documents to contain instructions for applications.
///
/// This event implements `Deref<Target = str>`. The `deref()` implementation
Expand All @@ -1030,6 +1206,8 @@ impl FusedIterator for CDataIterator<'_> {}
///
/// assert_eq!(reader.read_event().unwrap(), Event::PI(event.borrow()));
/// // deref coercion of &BytesPI to &str
/// assert_eq!(&event as &str, content);
/// // AsRef<str> for &T
/// assert_eq!(event.as_ref(), content);
/// ```
///
Expand Down Expand Up @@ -1216,6 +1394,8 @@ impl<'i> arbitrary::Arbitrary<'i> for BytesPI<'i> {
///
/// assert_eq!(reader.read_event().unwrap(), Event::Decl(event.borrow()));
/// // deref coercion of &BytesDecl to &str
/// assert_eq!(&event as &str, content);
/// // AsRef<str> for &T
/// assert_eq!(event.as_ref(), content);
/// ```
///
Expand Down Expand Up @@ -1574,6 +1754,8 @@ impl<'i> arbitrary::Arbitrary<'i> for BytesDecl<'i> {
///
/// assert_eq!(reader.read_event().unwrap(), Event::GeneralRef(event.borrow()));
/// // deref coercion of &BytesRef to &str
/// assert_eq!(&event as &str, content);
/// // AsRef<str> for &T
/// assert_eq!(event.as_ref(), content);
/// ```
///
Expand Down Expand Up @@ -1791,7 +1973,7 @@ pub enum Event<'i> {
/// Unescaped character data stored in `<![CDATA[...]]>`.
CData(BytesCData<'i>),
/// Comment `<!-- ... -->`.
Comment(BytesText<'i>),
Comment(BytesComment<'i>),
/// XML declaration `<?xml ...?>`.
Decl(BytesDecl<'i>),
/// Processing instruction `<?...?>`.
Expand Down
4 changes: 2 additions & 2 deletions src/reader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2086,7 +2086,7 @@ mod test {

/// Ensures, that no empty `Text` events are generated
mod $read_event {
use crate::events::{BytesCData, BytesDecl, BytesEnd, BytesPI, BytesStart, BytesText, Event};
use crate::events::{BytesCData, BytesComment, BytesDecl, BytesEnd, BytesPI, BytesStart, BytesText, Event};
use crate::reader::Reader;
use pretty_assertions::assert_eq;

Expand Down Expand Up @@ -2212,7 +2212,7 @@ mod test {

assert_eq!(
reader.$read_event($buf) $(.$await)? .unwrap(),
Event::Comment(BytesText::from_escaped(""))
Event::Comment(BytesComment::new("").unwrap())
);
}

Expand Down
6 changes: 4 additions & 2 deletions src/reader/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use std::fmt::Debug;
use encoding_rs::UTF_8;

use crate::errors::{Error, IllFormedError, Result};
use crate::events::{BytesCData, BytesDecl, BytesEnd, BytesPI, BytesStart, BytesText, Event};
use crate::events::{
BytesCData, BytesComment, BytesDecl, BytesEnd, BytesPI, BytesStart, BytesText, Event,
};
use crate::parser::{Parser, PiParser};
#[cfg(feature = "encoding")]
use crate::reader::EncodingRef;
Expand Down Expand Up @@ -136,7 +138,7 @@ impl ReaderState {
haystack = &haystack[p + 1..];
}
}
Ok(Event::Comment(BytesText::wrap(
Ok(Event::Comment(BytesComment::wrap(
// Cut of `<!--` and `-->` from start and end
&buf[4..len - 3],
)))
Expand Down
2 changes: 1 addition & 1 deletion src/writer/async_tokio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ mod tests {

test!(
comment,
Event::Comment(BytesText::new("this is a comment")),
Event::Comment(BytesComment::new("this is a comment").unwrap()),
r#"<!--this is a comment-->"#
);

Expand Down
Loading
Loading