Repository: tafia/quick-xml
Pull request #421 (2022-07-15 18:54:25)

Enforce unescape functions to operate over UTF-8 and fix serde bugs
They are always operate over UTF-8 input, but that was not reflected in types.

This PR also fixes a bug in the serde deserializer: the order of unescape and decode operations was swapped. Although we are still going to abandon decoding on demand, it is still need to mention this fix in the changelog.

Also, because of new encoding policy, deserializing of bytes content using serde now disallowed. This is anyway works incorrectly, because XML is not able to store binary data, they are always should be encoded in some way. Now this is enforced by deserializer and allows us to decode all input at once ans work with UTF-8 representation internally.


Patch:
diff --git a/Changelog.md b/Changelog.md
index 0e88eeb..0dda87b 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -42,6 +42,11 @@
 - [#363]: Do not generate empty `Event::Text` events
 - [#412]: Fix using incorrect encoding if `read_to_end` family of methods or `read_text`
   method not found a corresponding end tag and reader has non-UTF-8 encoding
+- [#421]: Fix incorrect order of unescape and decode operations for serde deserializer:
+  decoding should be first, unescape is the second
+- [#421]: Fixed unknown bug in serde deserialization of externally tagged enums
+  when an enum variant represented as a `Text` event (i.e. `<xml>tag</xml>`)
+  and a document encoding is not an UTF-8
 
 ### Misc Changes
 
@@ -109,6 +114,7 @@
   |`read_event_unbuffered`  |`read_event`
   |`read_to_end_unbuffered` |`read_to_end`
 - [#412]: Change `read_to_end*` and `read_text_into` to accept `QName` instead of `AsRef<[u8]>`
+
 - [#415]: Changed custom entity unescaping API to accept closures rather than a mapping of entity to
   replacement text. This avoids needing to allocate a map and provides the user with more flexibility.
 - [#415]: Renamed many functions following the pattern `unescape_and_decode*` to `decode_and_unescape*`
@@ -118,9 +124,16 @@
   `BytesText::unescape()`, `BytesText::unescaped_with()` renamed to `BytesText::unescape_with()`,
   `Attribute::escaped_value()` renamed to `Attribute::escape_value()`, and `Attribute::escaped_value_with()`
   renamed to `Attribute::escape_value_with()` for consistency across the API.
+
 - [#416]: `BytesStart::to_borrowed` renamed to `BytesStart::borrow`, the same method
   added to all events
 
+- [#421]: `decode_and_unescape*` methods now does one less allocation if unescaping is not required
+- [#421]: Removed ability to deserialize byte arrays from serde deserializer.
+  XML is not able to store binary data directly, you should always use some encoding
+  scheme, for example, HEX or Base64
+- [#421]: All unescaping functions now accepts and returns strings instead of byte slices
+
 ### New Tests
 
 - [#9]: Added tests for incorrect nested tags in input
@@ -148,6 +161,7 @@
 [#415]: https://github.com/tafia/quick-xml/pull/415
 [#416]: https://github.com/tafia/quick-xml/pull/416
 [#418]: https://github.com/tafia/quick-xml/pull/418
+[#421]: https://github.com/tafia/quick-xml/pull/421
 
 ## 0.23.0 -- 2022-05-08
 
diff --git a/README.md b/README.md
index 2e437c0..4cbcac0 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,7 @@ loop {
                 _ => (),
             }
         },
-        Ok(Event::Text(e)) => txt.push(e.unescape_and_decode(&reader).unwrap()),
+        Ok(Event::Text(e)) => txt.push(e.unescape_and_decode(&reader).unwrap().into_owned()),
         Ok(Event::Eof) => break, // exits the loop when reaching end of file
         Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
         _ => (), // There are several other `Event`s we do not consider here
diff --git a/benches/macrobenches.rs b/benches/macrobenches.rs
index f9d75c9..3358f3a 100644
--- a/benches/macrobenches.rs
+++ b/benches/macrobenches.rs
@@ -24,11 +24,11 @@ fn parse_document(doc: &[u8]) -> XmlResult<()> {
         match r.read_event()? {
             Event::Start(e) | Event::Empty(e) => {
                 for attr in e.attributes() {
-                    criterion::black_box(attr?.unescape_value()?);
+                    criterion::black_box(attr?.decode_and_unescape_value(&r)?);
                 }
             }
             Event::Text(e) => {
-                criterion::black_box(e.unescape()?);
+                criterion::black_box(e.decode_and_unescape(&r)?);
             }
             Event::CData(e) => {
                 criterion::black_box(e.into_inner());
diff --git a/benches/microbenches.rs b/benches/microbenches.rs
index 0524f3e..893f4aa 100644
--- a/benches/microbenches.rs
+++ b/benches/microbenches.rs
@@ -8,8 +8,8 @@ use quick_xml::Reader;
 static SAMPLE: &[u8] = include_bytes!("../tests/documents/sample_rss.xml");
 static PLAYERS: &[u8] = include_bytes!("../tests/documents/players.xml");
 
-static LOREM_IPSUM_TEXT: &[u8] =
-b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
+static LOREM_IPSUM_TEXT: &str =
+"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
 ut labore et dolore magna aliqua. Hac habitasse platea dictumst vestibulum rhoncus est pellentesque.
 Risus ultricies tristique nulla aliquet enim tortor at. Fermentum odio eu feugiat pretium nibh ipsum.
 Volutpat sed cras ornare arcu dui. Scelerisque fermentum dui faucibus in ornare quam. Arcu cursus
@@ -176,7 +176,7 @@ fn one_event(c: &mut Criterion) {
                 .check_comments(false)
                 .trim_text(true);
             match r.read_event_into(&mut buf) {
-                Ok(Event::Comment(ref e)) => nbtxt += e.unescape().unwrap().len(),
+                Ok(Event::Comment(e)) => nbtxt += e.decode_and_unescape(&r).unwrap().len(),
                 something_else => panic!("Did not expect {:?}", something_else),
             };
 
@@ -293,7 +293,7 @@ fn escaping(c: &mut Criterion) {
 
     group.bench_function("no_chars_to_escape_long", |b| {
         b.iter(|| {
-            criterion::black_box(escape(LOREM_IPSUM_TEXT));
+            criterion::black_box(escape(LOREM_IPSUM_TEXT.as_bytes()));
         })
     });
 
@@ -345,31 +345,31 @@ fn unescaping(c: &mut Criterion) {
 
     group.bench_function("no_chars_to_unescape_short", |b| {
         b.iter(|| {
-            criterion::black_box(unescape(b"just a bit of text")).unwrap();
+            criterion::black_box(unescape("just a bit of text")).unwrap();
         })
     });
 
     group.bench_function("char_reference", |b| {
         b.iter(|| {
-            let text = b"prefix &#34;some stuff&#34;,&#x22;more stuff&#x22;";
+            let text = "prefix &#34;some stuff&#34;,&#x22;more stuff&#x22;";
             criterion::black_box(unescape(text)).unwrap();
-            let text = b"&#38;&#60;";
+            let text = "&#38;&#60;";
             criterion::black_box(unescape(text)).unwrap();
         })
     });
 
     group.bench_function("entity_reference", |b| {
         b.iter(|| {
-            let text = b"age &gt; 72 &amp;&amp; age &lt; 21";
+            let text = "age &gt; 72 &amp;&amp; age &lt; 21";
             criterion::black_box(unescape(text)).unwrap();
-            let text = b"&quot;what&apos;s that?&quot;";
+            let text = "&quot;what&apos;s that?&quot;";
             criterion::black_box(unescape(text)).unwrap();
         })
     });
 
     group.bench_function("mixed", |b| {
         let text =
-b"Lorem ipsum dolor sit amet, &amp;consectetur adipiscing elit, sed do eiusmod tempor incididunt
+"Lorem ipsum dolor sit amet, &amp;consectetur adipiscing elit, sed do eiusmod tempor incididunt
 ut labore et dolore magna aliqua. Hac habitasse platea dictumst vestibulum rhoncus est pellentesque.
 Risus ultricies &quot;tristique nulla aliquet enim tortor&quot; at. Fermentum odio eu feugiat pretium
 nibh ipsum. Volutpat sed cras ornare arcu dui. Scelerisque fermentum dui faucibus in ornare quam. Arcu
diff --git a/examples/custom_entities.rs b/examples/custom_entities.rs
index f563ea2..4d59d49 100644
--- a/examples/custom_entities.rs
+++ b/examples/custom_entities.rs
@@ -28,7 +28,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
     reader.trim_text(true);
 
     let mut buf = Vec::new();
-    let mut custom_entities: HashMap<Vec<u8>, String> = HashMap::new();
+    let mut custom_entities: HashMap<String, String> = HashMap::new();
     let entity_re = Regex::new(r#"<!ENTITY\s+([^ \t\r\n]+)\s+"([^"]*)"\s*>"#)?;
 
     loop {
@@ -36,7 +36,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
             Ok(Event::DocType(ref e)) => {
                 for cap in entity_re.captures_iter(&e) {
                     custom_entities.insert(
-                        cap[1].to_vec(),
+                        reader.decoder().decode(&cap[1])?.into_owned(),
                         reader.decoder().decode(&cap[2])?.into_owned(),
                     );
                 }
@@ -51,6 +51,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
                                     custom_entities.get(ent).map(|s| s.as_str())
                                 })
                                 .unwrap()
+                                .into_owned()
                         })
                         .collect::<Vec<_>>();
                     println!("attributes values: {:?}", attributes);
diff --git a/src/de/escape.rs b/src/de/escape.rs
index e47109e..badc329 100644
--- a/src/de/escape.rs
+++ b/src/de/escape.rs
@@ -1,7 +1,9 @@
 //! Serde `Deserializer` module
 
 use crate::de::deserialize_bool;
-use crate::{errors::serialize::DeError, errors::Error, escape::unescape, reader::Decoder};
+use crate::errors::serialize::DeError;
+use crate::escape::unescape;
+use crate::reader::Decoder;
 use serde::de::{DeserializeSeed, EnumAccess, VariantAccess, Visitor};
 use serde::{self, forward_to_deserialize_any, serde_if_integer128};
 use std::borrow::Cow;
@@ -30,13 +32,6 @@ impl<'a> EscapedDeserializer<'a> {
             escaped,
         }
     }
-    fn unescaped(&self) -> Result<Cow<[u8]>, DeError> {
-        if self.escaped {
-            unescape(&self.escaped_value).map_err(|e| DeError::InvalidXml(Error::EscapeError(e)))
-        } else {
-            Ok(Cow::Borrowed(&self.escaped_value))
-        }
-    }
 }
 
 macro_rules! deserialize_num {
@@ -66,20 +61,31 @@ impl<'de, 'a> serde::Deserializer<'de> for EscapedDeserializer<'a> {
     where
         V: Visitor<'de>,
     {
-        let unescaped = self.unescaped()?;
-        let value = self.decoder.decode(&unescaped)?;
-
-        visitor.visit_str(&value)
+        let decoded = self.decoder.decode(&self.escaped_value)?;
+        if self.escaped {
+            match unescape(&decoded)? {
+                Cow::Borrowed(s) => visitor.visit_str(s),
+                Cow::Owned(s) => visitor.visit_string(s),
+            }
+        } else {
+            match decoded {
+                Cow::Borrowed(s) => visitor.visit_str(s),
+                Cow::Owned(s) => visitor.visit_string(s),
+            }
+        }
     }
 
-    fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    /// Returns [`DeError::Unsupported`]
+    fn deserialize_bytes<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
     where
         V: Visitor<'de>,
     {
-        let v = self.unescaped()?;
-        visitor.visit_bytes(&v)
+        Err(DeError::Unsupported(
+            "binary data content is not supported by XML format",
+        ))
     }
 
+    /// Forwards deserialization to the [`Self::deserialize_bytes`]
     fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Self::Error>
     where
         V: Visitor<'de>,
diff --git a/src/de/map.rs b/src/de/map.rs
index beecaae..c6261bf 100644
--- a/src/de/map.rs
+++ b/src/de/map.rs
@@ -4,11 +4,10 @@ use crate::{
     de::escape::EscapedDeserializer,
     de::seq::{not_in, TagFilter},
     de::simple_type::SimpleTypeDeserializer,
-    de::{deserialize_bool, DeEvent, Deserializer, XmlRead, INNER_VALUE, UNFLATTEN_PREFIX},
+    de::{str2bool, DeEvent, Deserializer, XmlRead, INNER_VALUE, UNFLATTEN_PREFIX},
     errors::serialize::DeError,
     events::attributes::IterState,
-    events::{BytesCData, BytesStart},
-    reader::Decoder,
+    events::BytesStart,
 };
 use serde::de::{self, DeserializeSeed, IntoDeserializer, SeqAccess, Visitor};
 use serde::serde_if_integer128;
@@ -479,15 +478,9 @@ where
 {
     /// Returns a text event, used inside [`deserialize_primitives!()`]
     #[inline]
-    fn next_text(&mut self, unescape: bool) -> Result<BytesCData<'de>, DeError> {
+    fn next_text(&mut self, unescape: bool) -> Result<Cow<'de, str>, DeError> {
         self.map.de.next_text_impl(unescape, self.allow_start)
     }
-
-    /// Returns a decoder, used inside [`deserialize_primitives!()`]
-    #[inline]
-    fn decoder(&self) -> Decoder {
-        self.map.de.reader.decoder()
-    }
 }
 
 impl<'de, 'a, 'm, R> de::Deserializer<'de> for MapValueDeserializer<'de, 'a, 'm, R>
@@ -649,15 +642,9 @@ where
 {
     /// Returns a text event, used inside [`deserialize_primitives!()`]
     #[inline]
-    fn next_text(&mut self, unescape: bool) -> Result<BytesCData<'de>, DeError> {
+    fn next_text(&mut self, unescape: bool) -> Result<Cow<'de, str>, DeError> {
         self.map.de.next_text_impl(unescape, true)
     }
-
-    /// Returns a decoder, used inside [`deserialize_primitives!()`]
-    #[inline]
-    fn decoder(&self) -> Decoder {
-        self.map.de.reader.decoder()
-    }
 }
 
 impl<'de, 'a, 'm, R> de::Deserializer<'de> for SeqValueDeserializer<'de, 'a, 'm, R>
diff --git a/src/de/mod.rs b/src/de/mod.rs
index ae94b26..dc6ab4c 100644
--- a/src/de/mod.rs
+++ b/src/de/mod.rs
@@ -117,8 +117,7 @@ macro_rules! deserialize_type {
         {
             // No need to unescape because valid integer representations cannot be escaped
             let text = self.next_text(false)?;
-            let string = text.decode(self.decoder())?;
-            visitor.$visit(string.parse()?)
+            visitor.$visit(text.parse()?)
         }
     };
 }
@@ -152,7 +151,7 @@ macro_rules! deserialize_primitives {
             // No need to unescape because valid boolean representations cannot be escaped
             let text = self.next_text(false)?;
 
-            deserialize_bool(text.as_ref(), self.decoder(), visitor)
+            str2bool(&text, visitor)
         }
 
         /// Representation of owned strings the same as [non-owned](#method.deserialize_str).
@@ -176,30 +175,26 @@ macro_rules! deserialize_primitives {
             V: Visitor<'de>,
         {
             let text = self.next_text(true)?;
-            let string = text.decode(self.decoder())?;
-            match string {
+            match text {
                 Cow::Borrowed(string) => visitor.visit_borrowed_str(string),
                 Cow::Owned(string) => visitor.visit_string(string),
             }
         }
 
-        fn deserialize_bytes<V>($($mut)? self, visitor: V) -> Result<V::Value, DeError>
+        /// Returns [`DeError::Unsupported`]
+        fn deserialize_bytes<V>(self, _visitor: V) -> Result<V::Value, DeError>
         where
             V: Visitor<'de>,
         {
-            // No need to unescape because bytes gives access to the raw XML input
-            let text = self.next_text(false)?;
-            visitor.visit_bytes(&text)
+            Err(DeError::Unsupported("binary data content is not supported by XML format"))
         }
 
-        fn deserialize_byte_buf<V>($($mut)? self, visitor: V) -> Result<V::Value, DeError>
+        /// Forwards deserialization to the [`deserialize_bytes`](#method.deserialize_bytes).
+        fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, DeError>
         where
             V: Visitor<'de>,
         {
-            // No need to unescape because bytes gives access to the raw XML input
-            let text = self.next_text(false)?;
-            let value = text.into_inner().into_owned();
-            visitor.visit_byte_buf(value)
+            self.deserialize_bytes(visitor)
         }
 
         /// Identifiers represented as [strings](#method.deserialize_str).
@@ -576,7 +571,7 @@ where
     }
 
     #[inline]
-    fn next_text(&mut self, unescape: bool) -> Result<BytesCData<'de>, DeError> {
+    fn next_text(&mut self, unescape: bool) -> Result<Cow<'de, str>, DeError> {
         self.next_text_impl(unescape, true)
     }
 
@@ -616,25 +611,24 @@ where
         &mut self,
         unescape: bool,
         allow_start: bool,
-    ) -> Result<BytesCData<'de>, DeError> {
+    ) -> Result<Cow<'de, str>, DeError> {
+        let decoder = self.reader.decoder();
         match self.next()? {
-            DeEvent::Text(e) if unescape => e.unescape_as_cdata().map_err(Into::into),
-            DeEvent::Text(e) => Ok(BytesCData::new(e.into_inner())),
-            DeEvent::CData(e) => Ok(e),
+            DeEvent::Text(e) => Ok(e.decode(decoder, unescape)?),
+            DeEvent::CData(e) => Ok(e.decode(decoder)?),
             DeEvent::Start(e) if allow_start => {
                 // allow one nested level
                 let inner = self.next()?;
                 let t = match inner {
-                    DeEvent::Text(t) if unescape => t.unescape_as_cdata()?,
-                    DeEvent::Text(t) => BytesCData::new(t.into_inner()),
-                    DeEvent::CData(t) => t,
+                    DeEvent::Text(t) => t.decode(decoder, unescape)?,
+                    DeEvent::CData(t) => t.decode(decoder)?,
                     DeEvent::Start(s) => {
                         return Err(DeError::UnexpectedStart(s.name().as_ref().to_owned()))
                     }
                     // We can get End event in case of `<tag></tag>` or `<tag/>` input
                     // Return empty text in that case
                     DeEvent::End(end) if end.name() == e.name() => {
-                        return Ok(BytesCData::new(&[] as &[u8]));
+                        return Ok("".into());
                     }
                     DeEvent::End(end) => {
                         return Err(DeError::UnexpectedEnd(end.name().as_ref().to_owned()))
@@ -650,12 +644,6 @@ where
         }
     }
 
-    /// Returns a decoder, used inside `deserialize_primitives!()`
-    #[inline]
-    fn decoder(&self) -> Decoder {
-        self.reader.decoder()
-    }
-
     /// 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")]
diff --git a/src/de/simple_type.rs b/src/de/simple_type.rs
index b9a865b..dc0b157 100644
--- a/src/de/simple_type.rs
+++ b/src/de/simple_type.rs
@@ -218,9 +218,9 @@ impl<'de, 'a> Deserializer<'de> for AtomicDeserializer<'de, 'a> {
         V: Visitor<'de>,
     {
         if self.escaped {
-            match unescape(self.content.as_str().as_bytes())? {
+            match unescape(self.content.as_str())? {
                 Cow::Borrowed(_) => self.content.deserialize_item(visitor),
-                Cow::Owned(buf) => visitor.visit_string(String::from_utf8(buf)?),
+                Cow::Owned(s) => visitor.visit_string(s),
             }
         } else {
             self.content.deserialize_item(visitor)
@@ -625,9 +625,9 @@ impl<'de, 'a> Deserializer<'de> for SimpleTypeDeserializer<'de, 'a> {
     {
         let content = self.decode()?;
         if self.escaped {
-            match unescape(content.as_str().as_bytes())? {
+            match unescape(content.as_str())? {
                 Cow::Borrowed(_) => content.deserialize_all(visitor),
-                Cow::Owned(buf) => visitor.visit_string(String::from_utf8(buf)?),
+                Cow::Owned(s) => visitor.visit_string(s),
             }
         } else {
             content.deserialize_all(visitor)
@@ -642,15 +642,14 @@ impl<'de, 'a> Deserializer<'de> for SimpleTypeDeserializer<'de, 'a> {
         self.deserialize_str(visitor)
     }
 
-    fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    /// Returns [`DeError::Unsupported`]
+    fn deserialize_bytes<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
     where
         V: Visitor<'de>,
     {
-        match self.content {
-            CowRef::Input(content) => visitor.visit_borrowed_bytes(content),
-            CowRef::Slice(content) => visitor.visit_bytes(content),
-            CowRef::Owned(content) => visitor.visit_byte_buf(content),
-        }
+        Err(DeError::Unsupported(
+            "binary data content is not supported by XML format",
+        ))
     }
 
     /// Forwards deserialization to the [`Self::deserialize_bytes`]
@@ -1176,10 +1175,12 @@ mod tests {
         simple!(utf8, char_escaped: char = "&lt;" => '<');
 
         simple!(utf8, string: String = "&lt;escaped&#x20;string" => "<escaped string");
-        simple!(utf8, byte_buf: ByteBuf = "&lt;escaped&#x20;string" => ByteBuf(b"&lt;escaped&#x20;string".to_vec()));
+        err!(utf8, byte_buf: ByteBuf = "&lt;escaped&#x20;string"
+             => Unsupported("binary data content is not supported by XML format"));
 
         simple!(utf8, borrowed_str: &str = "non-escaped string" => "non-escaped string");
-        simple!(utf8, borrowed_bytes: Bytes = "&lt;escaped&#x20;string" => Bytes(b"&lt;escaped&#x20;string"));
+        err!(utf8, borrowed_bytes: Bytes = "&lt;escaped&#x20;string"
+             => Unsupported("binary data content is not supported by XML format"));
 
         simple!(utf8, option_none: Option<&str> = "" => None);
         simple!(utf8, option_some: Option<&str> = "non-escaped string" => Some("non-escaped string"));
@@ -1258,7 +1259,8 @@ mod tests {
         utf16!(char_escaped: char = "&lt;" => '<');
 
         utf16!(string: String = "&lt;escaped&#x20;string" => "<escaped string");
-        utf16!(byte_buf: ByteBuf = "&lt;escaped&#x20;string" => ByteBuf(to_utf16("&lt;escaped&#x20;string")));
+        unsupported!(borrowed_bytes: Bytes = "&lt;escaped&#x20;string"
+                     => "binary data content is not supported by XML format");
 
         utf16!(option_none: Option<()> = "" => None);
         utf16!(option_some: Option<()> = "any data" => Some(()));
@@ -1278,12 +1280,12 @@ mod tests {
                      => "structures are not supported for XSD `simpleType`s");
 
         utf16!(enum_unit: Enum = "Unit" => Enum::Unit);
-        err!(utf16, enum_newtype: Enum = to_utf16("Newtype")
-             => Unsupported("enum newtype variants are not supported for XSD `simpleType`s"));
-        err!(utf16, enum_tuple: Enum = to_utf16("Tuple")
-             => Unsupported("enum tuple variants are not supported for XSD `simpleType`s"));
-        err!(utf16, enum_struct: Enum = to_utf16("Struct")
-             => Unsupported("enum struct variants are not supported for XSD `simpleType`s"));
+        unsupported!(enum_newtype: Enum = "Newtype"
+                     => "enum newtype variants are not supported for XSD `simpleType`s");
+        unsupported!(enum_tuple: Enum = "Tuple"
+                     => "enum tuple variants are not supported for XSD `simpleType`s");
+        unsupported!(enum_struct: Enum = "Struct"
+                     => "enum struct variants are not supported for XSD `simpleType`s");
         err!(utf16, enum_other: Enum = to_utf16("any data")
              => Custom("unknown variant `any data`, expected one of `Unit`, `Newtype`, `Tuple`, `Struct`"));
 
diff --git a/src/escapei.rs b/src/escapei.rs
index 6af41e7..b2d7e5f 100644
--- a/src/escapei.rs
+++ b/src/escapei.rs
@@ -7,18 +7,15 @@ use std::ops::Range;
 #[cfg(test)]
 use pretty_assertions::assert_eq;
 
-/// Error for XML escape/unescqpe.
+/// Error for XML escape / unescape.
 #[derive(Debug)]
 pub enum EscapeError {
     /// Entity with Null character
-    EntityWithNull(::std::ops::Range<usize>),
+    EntityWithNull(Range<usize>),
     /// Unrecognized escape symbol
-    UnrecognizedSymbol(
-        ::std::ops::Range<usize>,
-        ::std::result::Result<String, ::std::string::FromUtf8Error>,
-    ),
+    UnrecognizedSymbol(Range<usize>, String),
     /// Cannot find `;` after `&`
-    UnterminatedEntity(::std::ops::Range<usize>),
+    UnterminatedEntity(Range<usize>),
     /// Cannot convert Hexa to utf8
     TooLongHexadecimal,
     /// Character is not a valid hexadecimal value
@@ -62,22 +59,41 @@ impl std::fmt::Display for EscapeError {
 
 impl std::error::Error for EscapeError {}
 
-/// Escapes a `&[u8]` and replaces all xml special characters (<, >, &, ', ") with their
-/// corresponding xml escaped value.
+/// Escapes a `&[u8]` and replaces all xml special characters (`<`, `>`, `&`, `'`, `"`)
+/// with their corresponding xml escaped value.
+///
+/// This function performs following replacements:
+///
+/// | Character | Replacement
+/// |-----------|------------
+/// | `<`       | `&lt;`
+/// | `>`       | `&gt;`
+/// | `&`       | `&amp;`
+/// | `'`       | `&apos;`
+/// | `"`       | `&quot;`
 pub fn escape(raw: &[u8]) -> Cow<[u8]> {
     _escape(raw, |ch| matches!(ch, b'<' | b'>' | b'&' | b'\'' | b'\"'))
 }
 
-/// Should only be used for escaping text content. In xml text content, it is allowed
-/// (though not recommended) to leave the quote special characters " and ' unescaped.
-/// This function escapes a `&[u8]` and replaces xml special characters (<, >, &) with
-/// their corresponding xml escaped value, but does not escape quote characters.
+/// Escapes a `&[u8]` and replaces xml special characters (`<`, `>`, `&`)
+/// with their corresponding xml escaped value.
+///
+/// Should only be used for escaping text content. In XML text content, it is allowed
+/// (though not recommended) to leave the quote special characters `"` and `'` unescaped.
+///
+/// This function performs following replacements:
+///
+/// | Character | Replacement
+/// |-----------|------------
+/// | `<`       | `&lt;`
+/// | `>`       | `&gt;`
+/// | `&`       | `&amp;`
 pub fn partial_escape(raw: &[u8]) -> Cow<[u8]> {
     _escape(raw, |ch| matches!(ch, b'<' | b'>' | b'&'))
 }
 
-/// Escapes a `&[u8]` and replaces a subset of xml special characters (<, >, &, ', ") with their
-/// corresponding xml escaped value.
+/// Escapes a `&[u8]` and replaces a subset of xml special characters (`<`, `>`,
+/// `&`, `'`, `"`) with their corresponding xml escaped value.
 fn _escape<F: Fn(u8) -> bool>(raw: &[u8], escape_chars: F) -> Cow<[u8]> {
     let mut escaped = None;
     let mut bytes = raw.iter();
@@ -110,53 +126,50 @@ fn _escape<F: Fn(u8) -> bool>(raw: &[u8], escape_chars: F) -> Cow<[u8]> {
     }
 }
 
-/// Unescape a `&[u8]` and replaces all xml escaped characters ('&...;') into their corresponding
-/// value
-pub fn unescape(raw: &[u8]) -> Result<Cow<[u8]>, EscapeError> {
+/// Unescape an `&str` and replaces all xml escaped characters (`&...;`) into
+/// their corresponding value
+pub fn unescape(raw: &str) -> Result<Cow<str>, EscapeError> {
     unescape_with(raw, |_| None)
 }
 
-/// Unescape a `&[u8]` and replaces all xml escaped characters ('&...;') into their corresponding
-/// value, using a resolver function for custom entities.
-///
-/// # Pre-condition
-///
-/// The implementation of `resolve_entity` is expected to operate over UTF-8 inputs.
+/// Unescape an `&str` and replaces all xml escaped characters (`&...;`) into
+/// their corresponding value, using a resolver function for custom entities.
 pub fn unescape_with<'input, 'entity, F>(
-    raw: &'input [u8],
+    raw: &'input str,
     resolve_entity: F,
-) -> Result<Cow<'input, [u8]>, EscapeError>
+) -> Result<Cow<'input, str>, EscapeError>
 where
     // the lifetime of the output comes from a capture or is `'static`
-    F: Fn(&[u8]) -> Option<&'entity str>,
+    F: Fn(&str) -> Option<&'entity str>,
 {
+    let bytes = raw.as_bytes();
     let mut unescaped = None;
     let mut last_end = 0;
-    let mut iter = memchr::memchr2_iter(b'&', b';', raw);
-    while let Some(start) = iter.by_ref().find(|p| raw[*p] == b'&') {
+    let mut iter = memchr::memchr2_iter(b'&', b';', bytes);
+    while let Some(start) = iter.by_ref().find(|p| bytes[*p] == b'&') {
         match iter.next() {
-            Some(end) if raw[end] == b';' => {
+            Some(end) if bytes[end] == b';' => {
                 // append valid data
                 if unescaped.is_none() {
-                    unescaped = Some(Vec::with_capacity(raw.len()));
+                    unescaped = Some(String::with_capacity(raw.len()));
                 }
                 let unescaped = unescaped.as_mut().expect("initialized");
-                unescaped.extend_from_slice(&raw[last_end..start]);
+                unescaped.push_str(&raw[last_end..start]);
 
                 // search for character correctness
                 let pat = &raw[start + 1..end];
-                if pat.starts_with(b"#") {
+                if pat.starts_with("#") {
                     let entity = &pat[1..]; // starts after the #
                     let codepoint = parse_number(entity, start..end)?;
-                    push_utf8(unescaped, codepoint);
+                    unescaped.push_str(codepoint.encode_utf8(&mut [0u8; 4]));
                 } else if let Some(value) = named_entity(pat) {
-                    unescaped.extend_from_slice(value.as_bytes());
+                    unescaped.push_str(value);
                 } else if let Some(value) = resolve_entity(pat) {
-                    unescaped.extend_from_slice(value.as_bytes());
+                    unescaped.push_str(value);
                 } else {
                     return Err(EscapeError::UnrecognizedSymbol(
                         start + 1..end,
-                        String::from_utf8(pat.to_vec()),
+                        pat.to_string(),
                     ));
                 }
 
@@ -168,7 +181,7 @@ where
 
     if let Some(mut unescaped) = unescaped {
         if let Some(raw) = raw.get(last_end..) {
-            unescaped.extend_from_slice(raw);
+            unescaped.push_str(raw);
         }
         Ok(Cow::Owned(unescaped))
     } else {
@@ -177,8 +190,9 @@ where
 }
 
 #[cfg(not(feature = "escape-html"))]
-const fn named_entity(name: &[u8]) -> Option<&str> {
-    let s = match name {
+const fn named_entity(name: &str) -> Option<&str> {
+    // match over strings are not allowed in const functions
+    let s = match name.as_bytes() {
         b"lt" => "<",
         b"gt" => ">",
         b"amp" => "&",
@@ -189,9 +203,10 @@ const fn named_entity(name: &[u8]) -> Option<&str> {
     Some(s)
 }
 #[cfg(feature = "escape-html")]
-const fn named_entity(name: &[u8]) -> Option<&str> {
+const fn named_entity(name: &str) -> Option<&str> {
     // imported from https://dev.w3.org/html5/html-author/charref
-    let s = match name {
+    // match over strings are not allowed in const functions
+    let s = match name.as_bytes() {
         b"Tab" => "\u{09}",
         b"NewLine" => "\u{0A}",
         b"excl" => "\u{21}",
@@ -1651,13 +1666,8 @@ const fn named_entity(name: &[u8]) -> Option<&str> {
     Some(s)
 }
 
-fn push_utf8(out: &mut Vec<u8>, code: char) {
-    let mut buf = [0u8; 4];
-    out.extend_from_slice(code.encode_utf8(&mut buf).as_bytes());
-}
-
-fn parse_number(bytes: &[u8], range: Range<usize>) -> Result<char, EscapeError> {
-    let code = if bytes.starts_with(b"x") {
+fn parse_number(bytes: &str, range: Range<usize>) -> Result<char, EscapeError> {
+    let code = if bytes.starts_with("x") {
         parse_hexadecimal(&bytes[1..])
     } else {
         parse_decimal(&bytes)
@@ -1671,13 +1681,13 @@ fn parse_number(bytes: &[u8], range: Range<usize>) -> Result<char, EscapeError>
     }
 }
 
-fn parse_hexadecimal(bytes: &[u8]) -> Result<u32, EscapeError> {
+fn parse_hexadecimal(bytes: &str) -> Result<u32, EscapeError> {
     // maximum code is 0x10FFFF => 6 characters
     if bytes.len() > 6 {
         return Err(EscapeError::TooLongHexadecimal);
     }
     let mut code = 0;
-    for &b in bytes {
+    for b in bytes.bytes() {
         code <<= 4;
         code += match b {
             b'0'..=b'9' => b - b'0',
@@ -1689,13 +1699,13 @@ fn parse_hexadecimal(bytes: &[u8]) -> Result<u32, EscapeError> {
     Ok(code)
 }
 
-fn parse_decimal(bytes: &[u8]) -> Result<u32, EscapeError> {
+fn parse_decimal(bytes: &str) -> Result<u32, EscapeError> {
     // maximum code is 0x10FFFF = 1114111 => 7 characters
     if bytes.len() > 7 {
         return Err(EscapeError::TooLongDecimal);
     }
     let mut code = 0;
-    for &b in bytes {
+    for b in bytes.bytes() {
         code *= 10;
         code += match b {
             b'0'..=b'9' => b - b'0',
@@ -1707,29 +1717,29 @@ fn parse_decimal(bytes: &[u8]) -> Result<u32, EscapeError> {
 
 #[test]
 fn test_unescape() {
-    assert_eq!(&*unescape(b"test").unwrap(), b"test");
-    assert_eq!(&*unescape(b"&lt;test&gt;").unwrap(), b"<test>");
-    assert_eq!(&*unescape(b"&#x30;").unwrap(), b"0");
-    assert_eq!(&*unescape(b"&#48;").unwrap(), b"0");
-    assert!(unescape(b"&foo;").is_err());
+    assert_eq!(&*unescape("test").unwrap(), "test");
+    assert_eq!(&*unescape("&lt;test&gt;").unwrap(), "<test>");
+    assert_eq!(&*unescape("&#x30;").unwrap(), "0");
+    assert_eq!(&*unescape("&#48;").unwrap(), "0");
+    assert!(unescape("&foo;").is_err());
 }
 
 #[test]
 fn test_unescape_with() {
-    let custom_entities = |ent: &[u8]| match ent {
-        b"foo" => Some("BAR"),
+    let custom_entities = |ent: &str| match ent {
+        "foo" => Some("BAR"),
         _ => None,
     };
 
-    assert_eq!(&*unescape_with(b"test", custom_entities).unwrap(), b"test");
+    assert_eq!(&*unescape_with("test", custom_entities).unwrap(), "test");
     assert_eq!(
-        &*unescape_with(b"&lt;test&gt;", custom_entities).unwrap(),
-        b"<test>"
+        &*unescape_with("&lt;test&gt;", custom_entities).unwrap(),
+        "<test>"
     );
-    assert_eq!(&*unescape_with(b"&#x30;", custom_entities).unwrap(), b"0");
-    assert_eq!(&*unescape_with(b"&#48;", custom_entities).unwrap(), b"0");
-    assert_eq!(&*unescape_with(b"&foo;", custom_entities).unwrap(), b"BAR");
-    assert!(unescape_with(b"&fop;", custom_entities).is_err());
+    assert_eq!(&*unescape_with("&#x30;", custom_entities).unwrap(), "0");
+    assert_eq!(&*unescape_with("&#48;", custom_entities).unwrap(), "0");
+    assert_eq!(&*unescape_with("&foo;", custom_entities).unwrap(), "BAR");
+    assert!(unescape_with("&fop;", custom_entities).is_err());
 }
 
 #[test]
diff --git a/src/events/attributes.rs b/src/events/attributes.rs
index 1271586..d1f1e16 100644
--- a/src/events/attributes.rs
+++ b/src/events/attributes.rs
@@ -30,7 +30,7 @@ pub struct Attribute<'a> {
 }
 
 impl<'a> Attribute<'a> {
-    /// Returns the unescaped value.
+    /// Decodes using UTF-8 then unescapes the value.
     ///
     /// This is normally the value you are interested in. Escape sequences such as `&gt;` are
     /// replaced with their unescaped equivalents such as `>`.
@@ -38,11 +38,14 @@ impl<'a> Attribute<'a> {
     /// This will allocate if the value contains any escape sequences.
     ///
     /// See also [`unescape_value_with()`](Self::unescape_value_with)
-    pub fn unescape_value(&self) -> XmlResult<Cow<[u8]>> {
+    ///
+    /// This method is available only if `encoding` feature is **not** enabled.
+    #[cfg(any(doc, not(feature = "encoding")))]
+    pub fn unescape_value(&self) -> XmlResult<Cow<str>> {
         self.unescape_value_with(|_| None)
     }
 
-    /// Returns the unescaped value, using custom entities.
+    /// Decodes using UTF-8 then unescapes the value, using custom entities.
     ///
     /// This is normally the value you are interested in. Escape sequences such as `&gt;` are
     /// replaced with their unescaped equivalents such as `>`.
@@ -53,52 +56,43 @@ impl<'a> Attribute<'a> {
     ///
     /// See also [`unescape_value()`](Self::unescape_value)
     ///
-    /// # Pre-condition
-    ///
-    /// The implementation of `resolve_entity` is expected to operate over UTF-8 inputs.
-    pub fn unescape_value_with<'s, 'entity>(
-        &'s self,
-        resolve_entity: impl Fn(&[u8]) -> Option<&'entity str>,
-    ) -> XmlResult<Cow<'s, [u8]>> {
-        Ok(unescape_with(&*self.value, resolve_entity)?)
+    /// This method is available only if `encoding` feature is **not** enabled.
+    #[cfg(any(doc, not(feature = "encoding")))]
+    pub fn unescape_value_with<'entity>(
+        &self,
+        resolve_entity: impl Fn(&str) -> Option<&'entity str>,
+    ) -> XmlResult<Cow<str>> {
+        // from_utf8 should never fail because content is always UTF-8 encoded
+        Ok(unescape_with(
+            std::str::from_utf8(&self.value)?,
+            resolve_entity,
+        )?)
     }
 
-    /// Decodes then unescapes the value
+    /// Decodes then unescapes the value.
     ///
-    /// This allocates a `String` in all cases. For performance reasons it might be a better idea to
-    /// instead use one of:
-    ///
-    /// * [`Reader::decoder().decode()`], as it only allocates when the decoding can't be performed otherwise.
-    /// * [`unescape_value()`], as it doesn't allocate when no escape sequences are used.
-    ///
-    /// [`unescape_value()`]: Self::unescape_value
-    /// [`Reader::decoder().decode()`]: crate::reader::Decoder::decode
-    pub fn decode_and_unescape_value<B>(&self, reader: &Reader<B>) -> XmlResult<String> {
+    /// This will allocate if the value contains any escape sequences or in
+    /// non-UTF-8 encoding.
+    pub fn decode_and_unescape_value<B>(&self, reader: &Reader<B>) -> XmlResult<Cow<str>> {
         self.decode_and_unescape_value_with(reader, |_| None)
     }
 
-    /// Decodes then unescapes the value with custom entities
-    ///
-    /// This allocates a `String` in all cases. For performance reasons it might be a better idea to
-    /// instead use one of:
+    /// Decodes then unescapes the value with custom entities.
     ///
-    /// * [`Reader::decoder().decode()`], as it only allocates when the decoding can't be performed otherwise.
-    /// * [`unescape_value_with()`], as it doesn't allocate when no escape sequences are used.
-    ///
-    /// [`unescape_value_with()`]: Self::unescape_value_with
-    /// [`Reader::decoder().decode()`]: crate::reader::Decoder::decode
-    ///
-    /// # Pre-condition
-    ///
-    /// The implementation of `resolve_entity` is expected to operate over UTF-8 inputs.
+    /// This will allocate if the value contains any escape sequences or in
+    /// non-UTF-8 encoding.
     pub fn decode_and_unescape_value_with<'entity, B>(
         &self,
         reader: &Reader<B>,
-        resolve_entity: impl Fn(&[u8]) -> Option<&'entity str>,
-    ) -> XmlResult<String> {
+        resolve_entity: impl Fn(&str) -> Option<&'entity str>,
+    ) -> XmlResult<Cow<str>> {
         let decoded = reader.decoder().decode(&*self.value)?;
-        let unescaped = unescape_with(decoded.as_bytes(), resolve_entity)?;
-        Ok(String::from_utf8(unescaped.into_owned())?)
+
+        match unescape_with(&decoded, resolve_entity)? {
+            // Because result is borrowed, no replacements was done and we can use original string
+            Cow::Borrowed(_) => Ok(decoded),
+            Cow::Owned(s) => Ok(s.into()),
+        }
     }
 }
 
diff --git a/src/events/mod.rs b/src/events/mod.rs
index 7454095..b6b6b88 100644
--- a/src/events/mod.rs
+++ b/src/events/mod.rs
@@ -48,9 +48,6 @@ use crate::reader::{Decoder, Reader};
 use crate::utils::write_cow_string;
 use attributes::{Attribute, Attributes};
 
-#[cfg(feature = "serialize")]
-use crate::escape::EscapeError;
-
 /// Text that appeared before an XML declaration, a start element or a comment.
 ///
 /// In well-formed XML it could contain a Byte-Order-Mark (BOM). If this event
@@ -732,65 +729,49 @@ impl<'a> BytesText<'a> {
         }
     }
 
-    /// Returns unescaped version of the text content, that can be written
-    /// as CDATA in XML
-    #[cfg(feature = "serialize")]
-    pub(crate) fn unescape_as_cdata(self) -> std::result::Result<BytesCData<'a>, EscapeError> {
-        //TODO: need to think about better API instead of dozens similar functions
-        // Maybe use builder pattern. After that expose function as public API
-        //FIXME: need to take into account entities defined in the document
-        Ok(BytesCData::new(
-            match unescape_with(&self.content, |_| None)? {
-                Cow::Borrowed(_) => self.content,
-                Cow::Owned(unescaped) => Cow::Owned(unescaped),
-            },
-        ))
-    }
-
-    /// gets escaped content
+    /// Decodes using UTF-8 then unescapes the content of the event.
     ///
     /// Searches for '&' into content and try to escape the coded character if possible
     /// returns Malformed error with index within element if '&' is not followed by ';'
     ///
     /// See also [`unescape_with()`](Self::unescape_with)
-    pub fn unescape(&self) -> Result<Cow<[u8]>> {
+    ///
+    /// This method is available only if `encoding` feature is **not** enabled.
+    #[cfg(any(doc, not(feature = "encoding")))]
+    pub fn unescape(&self) -> Result<Cow<str>> {
         self.unescape_with(|_| None)
     }
 
-    /// gets escaped content with custom entities
+    /// Decodes using UTF-8 then unescapes the content of the event with custom entities.
     ///
     /// Searches for '&' into content and try to escape the coded character if possible
     /// returns Malformed error with index within element if '&' is not followed by ';'
     /// A fallback resolver for additional custom entities can be provided via `resolve_entity`.
     ///
-    /// # Pre-condition
-    ///
-    /// The implementation of `resolve_entity` is expected to operate over UTF-8 inputs.
-    ///
     /// See also [`unescape()`](Self::unescape)
-    pub fn unescape_with<'s, 'entity>(
-        &'s self,
-        resolve_entity: impl Fn(&[u8]) -> Option<&'entity str>,
-    ) -> Result<Cow<'s, [u8]>> {
-        Ok(unescape_with(self, resolve_entity)?)
+    ///
+    /// This method is available only if `encoding` feature is **not** enabled.
+    #[cfg(any(doc, not(feature = "encoding")))]
+    pub fn unescape_with<'entity>(
+        &self,
+        resolve_entity: impl Fn(&str) -> Option<&'entity str>,
+    ) -> Result<Cow<str>> {
+        // from_utf8 should never fail because content is always UTF-8 encoded
+        Ok(unescape_with(from_utf8(&self.content)?, resolve_entity)?)
     }
 
-    /// helper method to unescape then decode self using the reader encoding
+    /// Decodes then unescapes the content of the event.
     ///
-    /// for performance reasons (could avoid allocating a `String`),
-    /// it might be wiser to manually use
-    /// 1. Reader::decode(...)
-    /// 2. BytesText::unescaped()
-    pub fn decode_and_unescape<B>(&self, reader: &Reader<B>) -> Result<String> {
+    /// This will allocate if the value contains any escape sequences or in
+    /// non-UTF-8 encoding.
+    pub fn decode_and_unescape<B>(&self, reader: &Reader<B>) -> Result<Cow<str>> {
         self.decode_and_unescape_with(reader, |_| None)
     }
 
-    /// helper method to unescape then decode self using the reader encoding with custom entities
+    /// Decodes then unescapes the content of the event with custom entities.
     ///
-    /// for performance reasons (could avoid allocating a `String`),
-    /// it might be wiser to manually use
-    /// 1. Reader::decode(...)
-    /// 2. BytesText::unescaped()
+    /// This will allocate if the value contains any escape sequences or in
+    /// non-UTF-8 encoding.
     ///
     /// # Pre-condition
     ///
@@ -798,18 +779,45 @@ impl<'a> BytesText<'a> {
     pub fn decode_and_unescape_with<'entity, B>(
         &self,
         reader: &Reader<B>,
-        resolve_entity: impl Fn(&[u8]) -> Option<&'entity str>,
-    ) -> Result<String> {
+        resolve_entity: impl Fn(&str) -> Option<&'entity str>,
+    ) -> Result<Cow<str>> {
         let decoded = reader.decoder().decode(&*self)?;
 
-        let unescaped = unescape_with(decoded.as_bytes(), resolve_entity)?;
-        Ok(String::from_utf8(unescaped.into_owned())?)
+        match unescape_with(&decoded, resolve_entity)? {
+            // Because result is borrowed, no replacements was done and we can use original string
+            Cow::Borrowed(_) => Ok(decoded),
+            Cow::Owned(s) => Ok(s.into()),
+        }
     }
 
     /// Gets escaped content.
     pub fn escape(&self) -> &[u8] {
         self.content.as_ref()
     }
+
+    /// Gets content of this text buffer in the specified encoding and optionally
+    /// unescapes it. Unlike [`Self::decode_and_unescape`] & Co., the lifetime
+    /// of the returned `Cow` is bound to the original buffer / input
+    #[cfg(feature = "serialize")]
+    pub(crate) fn decode(&self, decoder: Decoder, unescape: bool) -> Result<Cow<'a, str>> {
+        //TODO: too many copies, can be optimized
+        let text = match &self.content {
+            Cow::Borrowed(bytes) => decoder.decode(bytes)?,
+            // Convert to owned, because otherwise Cow will be bound with wrong lifetime
+            Cow::Owned(bytes) => decoder.decode(bytes)?.into_owned().into(),
+        };
+        let text = if unescape {
+            //FIXME: need to take into account entities defined in the document
+            match unescape_with(&text, |_| None)? {
+                // Because result is borrowed, no replacements was done and we can use original string
+                Cow::Borrowed(_) => text,
+                Cow::Owned(s) => s.into(),
+            }
+        } else {
+            text
+        };
+        Ok(text)
+    }
 }
 
 impl<'a> Debug for BytesText<'a> {
@@ -922,14 +930,11 @@ impl<'a> BytesCData<'a> {
 
     /// Gets content of this text buffer in the specified encoding
     #[cfg(feature = "serialize")]
-    pub(crate) fn decode(&self, decoder: crate::reader::Decoder) -> Result<Cow<'a, str>> {
+    pub(crate) fn decode(&self, decoder: Decoder) -> Result<Cow<'a, str>> {
         Ok(match &self.content {
             Cow::Borrowed(bytes) => decoder.decode(bytes)?,
-            Cow::Owned(bytes) => {
-                let decoded = decoder.decode(bytes)?.to_string();
-
-                decoded.into()
-            }
+            // Convert to owned, because otherwise Cow will be bound with wrong lifetime
+            Cow::Owned(bytes) => decoder.decode(bytes)?.into_owned().into(),
         })
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index 57c6f9c..70a6c31 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -59,7 +59,7 @@
 //!             }
 //!         },
 //!         // unescape and decode the text event using the reader encoding
-//!         Ok(Event::Text(e)) => txt.push(e.decode_and_unescape(&reader).unwrap()),
+//!         Ok(Event::Text(e)) => txt.push(e.decode_and_unescape(&reader).unwrap().into_owned()),
 //!         Ok(Event::Eof) => break, // exits the loop when reaching end of file
 //!         Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
 //!         _ => (), // There are several other `Event`s we do not consider here
diff --git a/src/reader.rs b/src/reader.rs
index b2ac946..19d6a35 100644
--- a/src/reader.rs
+++ b/src/reader.rs
@@ -135,7 +135,7 @@ impl EncodingRef {
 ///                 _ => (),
 ///             }
 ///         },
-///         Ok(Event::Text(e)) => txt.push(e.decode_and_unescape(&reader).unwrap()),
+///         Ok(Event::Text(e)) => txt.push(e.decode_and_unescape(&reader).unwrap().into_owned()),
 ///         Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
 ///         Ok(Event::Eof) => break,
 ///         _ => (),
@@ -496,7 +496,7 @@ impl<R: BufRead> Reader<R> {
     /// loop {
     ///     match reader.read_event_into(&mut buf) {
     ///         Ok(Event::Start(ref e)) => count += 1,
-    ///         Ok(Event::Text(e)) => txt.push(e.decode_and_unescape(&reader).expect("Error!")),
+    ///         Ok(Event::Text(e)) => txt.push(e.decode_and_unescape(&reader).unwrap().into_owned()),
     ///         Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
     ///         Ok(Event::Eof) => break,
     ///         _ => (),
@@ -549,7 +549,7 @@ impl<R: BufRead> Reader<R> {
     ///             panic!("Undeclared namespace prefix {:?}", String::from_utf8(p))
     ///         }
     ///         Ok((_, Event::Text(e))) => {
-    ///             txt.push(e.decode_and_unescape(&reader).expect("Error!"))
+    ///             txt.push(e.decode_and_unescape(&reader).unwrap().into_owned())
     ///         },
     ///         Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
     ///         Ok((_, Event::Eof)) => break,
@@ -750,13 +750,13 @@ impl<R: BufRead> Reader<R> {
         let s = match self.read_event_into(buf) {
             Err(e) => return Err(e),
 
-            Ok(Event::Text(e)) => e.decode_and_unescape(self),
+            Ok(Event::Text(e)) => e.decode_and_unescape(self)?.into_owned(),
             Ok(Event::End(e)) if e.name() == end => return Ok("".to_string()),
             Ok(Event::Eof) => return Err(Error::UnexpectedEof("Text".to_string())),
             _ => return Err(Error::TextNotFound),
         };
         self.read_to_end_into(end, buf)?;
-        s
+        Ok(s)
     }
 }
 


Test patch:
diff --git a/tests/serde-de.rs b/tests/serde-de.rs
index 07e4e27..78ec5b7 100644
--- a/tests/serde-de.rs
+++ b/tests/serde-de.rs
@@ -1,5 +1,5 @@
 use quick_xml::de::Deserializer;
-use quick_xml::utils::ByteBuf;
+use quick_xml::utils::{ByteBuf, Bytes};
 use quick_xml::DeError;
 
 use pretty_assertions::assert_eq;
@@ -152,13 +152,6 @@ fn collection_of_enums() {
     );
 }
 
-#[test]
-fn deserialize_bytes() {
-    let item: ByteBuf = from_str(r#"<item>bytes</item>"#).unwrap();
-
-    assert_eq!(item, ByteBuf(b"bytes".to_vec()));
-}
-
 /// Test for https://github.com/tafia/quick-xml/issues/231
 #[test]
 fn implicit_value() {
@@ -233,11 +226,12 @@ mod trivial {
         ($name:ident: $type:ty = $value:expr) => {
             #[test]
             fn $name() {
-                let item = from_str::<$type>($value).unwrap_err();
-
-                match item {
-                    DeError::UnexpectedEof => (),
-                    _ => panic!("Expected `UnexpectedEof`, found {:?}", item),
+                match from_str::<$type>($value) {
+                    Err(DeError::UnexpectedEof) => (),
+                    x => panic!(
+                        r#"Expected `Err(DeError::UnexpectedEof)`, but got `{:?}`"#,
+                        x
+                    ),
                 }
             }
         };
@@ -267,15 +261,43 @@ mod trivial {
             eof!(char_: char = $value);
 
             eof!(string: String = $value);
-            eof!(byte_buf: ByteBuf = $value);
 
+            /// XML does not able to store binary data
             #[test]
-            fn unit() {
-                let item = from_str::<()>($value).unwrap_err();
+            fn byte_buf() {
+                match from_str::<ByteBuf>($value) {
+                    Err(DeError::Unsupported(msg)) => {
+                        assert_eq!(msg, "binary data content is not supported by XML format")
+                    }
+                    x => panic!(
+                        r#"Expected `Err(DeError::Unsupported("binary data content is not supported by XML format"))`, but got `{:?}`"#,
+                        x
+                    ),
+                }
+            }
 
-                match item {
-                    DeError::UnexpectedEof => (),
-                    _ => panic!("Expected `UnexpectedEof`, found {:?}", item),
+            /// XML does not able to store binary data
+            #[test]
+            fn bytes() {
+                match from_str::<Bytes>($value) {
+                    Err(DeError::Unsupported(msg)) => {
+                        assert_eq!(msg, "binary data content is not supported by XML format")
+                    }
+                    x => panic!(
+                        r#"Expected `Err(DeError::Unsupported("binary data content is not supported by XML format"))`, but got `{:?}`"#,
+                        x
+                    ),
+                }
+            }
+
+            #[test]
+            fn unit() {
+                match from_str::<()>($value) {
+                    Err(DeError::UnexpectedEof) => (),
+                    x => panic!(
+                        r#"Expected `Err(DeError::UnexpectedEof)`, but got `{:?}`"#,
+                        x
+                    ),
                 }
             }
         };
@@ -284,12 +306,16 @@ mod trivial {
     /// Empty document should considered invalid no matter what type we try to deserialize
     mod empty_doc {
         use super::*;
+        use pretty_assertions::assert_eq;
+
         eof!("");
     }
 
     /// Document that contains only comment should be handled as if it is empty
     mod only_comment {
         use super::*;
+        use pretty_assertions::assert_eq;
+
         eof!("<!--comment-->");
     }
 
@@ -360,11 +386,34 @@ mod trivial {
             in_struct!(char_: char = "<root>r</root>", 'r');
 
             in_struct!(string:   String  = "<root>escaped&#x20;string</root>", "escaped string".into());
-            // Byte buffers gives access to the raw data from the input, so never treated as escaped
-            // TODO: It is a bit unusual and it would be better completely forbid deserialization
-            // into bytes, because XML cannot store any bytes natively. User should use some sort
-            // of encoding to a string, for example, hex or base64
-            in_struct!(byte_buf: ByteBuf = "<root>escaped&#x20;byte_buf</root>", ByteBuf(r"escaped&#x20;byte_buf".into()));
+
+            /// XML does not able to store binary data
+            #[test]
+            fn byte_buf() {
+                match from_str::<Trivial<ByteBuf>>("<root>escaped&#x20;byte_buf</root>") {
+                    Err(DeError::Unsupported(msg)) => {
+                        assert_eq!(msg, "binary data content is not supported by XML format")
+                    }
+                    x => panic!(
+                        r#"Expected `Err(DeError::Unsupported("binary data content is not supported by XML format"))`, but got `{:?}`"#,
+                        x
+                    ),
+                }
+            }
+
+            /// XML does not able to store binary data
+            #[test]
+            fn bytes() {
+                match from_str::<Trivial<Bytes>>("<root>escaped&#x20;byte_buf</root>") {
+                    Err(DeError::Unsupported(msg)) => {
+                        assert_eq!(msg, "binary data content is not supported by XML format")
+                    }
+                    x => panic!(
+                        r#"Expected `Err(DeError::Unsupported("binary data content is not supported by XML format"))`, but got `{:?}`"#,
+                        x
+                    ),
+                }
+            }
         }
 
         /// Tests deserialization from CDATA content in a tag.
@@ -400,7 +449,34 @@ mod trivial {
 
             // Escape sequences does not processed inside CDATA section
             in_struct!(string:   String  = "<root><![CDATA[escaped&#x20;string]]></root>", "escaped&#x20;string".into());
-            in_struct!(byte_buf: ByteBuf = "<root><![CDATA[escaped&#x20;byte_buf]]></root>", ByteBuf(r"escaped&#x20;byte_buf".into()));
+
+            /// XML does not able to store binary data
+            #[test]
+            fn byte_buf() {
+                match from_str::<Trivial<ByteBuf>>("<root><![CDATA[escaped&#x20;byte_buf]]></root>") {
+                    Err(DeError::Unsupported(msg)) => {
+                        assert_eq!(msg, "binary data content is not supported by XML format")
+                    }
+                    x => panic!(
+                        r#"Expected `Err(DeError::Unsupported("binary data content is not supported by XML format"))`, but got `{:?}`"#,
+                        x
+                    ),
+                }
+            }
+
+            /// XML does not able to store binary data
+            #[test]
+            fn bytes() {
+                match from_str::<Trivial<Bytes>>("<root><![CDATA[escaped&#x20;byte_buf]]></root>") {
+                    Err(DeError::Unsupported(msg)) => {
+                        assert_eq!(msg, "binary data content is not supported by XML format")
+                    }
+                    x => panic!(
+                        r#"Expected `Err(DeError::Unsupported("binary data content is not supported by XML format"))`, but got `{:?}`"#,
+                        x
+                    ),
+                }
+            }
         }
     }
 }
diff --git a/tests/test.rs b/tests/test.rs
index 162a8bd..f296d10 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -155,15 +155,17 @@ fn fuzz_101() {
     let mut buf = vec![];
     loop {
         match reader.read_event_into(&mut buf) {
-            Ok(Start(ref e)) | Ok(Empty(ref e)) => {
+            Ok(Start(e)) | Ok(Empty(e)) => {
                 for a in e.attributes() {
-                    if a.ok().map_or(true, |a| a.unescape_value().is_err()) {
+                    if a.ok()
+                        .map_or(true, |a| a.decode_and_unescape_value(&reader).is_err())
+                    {
                         break;
                     }
                 }
             }
-            Ok(Text(ref e)) => {
-                if e.unescape().is_err() {
+            Ok(Text(e)) => {
+                if e.decode_and_unescape(&reader).is_err() {
                     break;
                 }
             }
diff --git a/tests/unit_tests.rs b/tests/unit_tests.rs
index 6c6a061..001fce2 100644
--- a/tests/unit_tests.rs
+++ b/tests/unit_tests.rs
@@ -523,13 +523,8 @@ fn test_escaped_content() {
                 "content unexpected: expecting '&lt;test&gt;', got '{:?}'",
                 from_utf8(&*e)
             );
-            match e.unescape() {
-                Ok(ref c) => assert_eq!(
-                    &**c,
-                    b"<test>",
-                    "unescaped content unexpected: expecting '&lt;test&lt;', got '{:?}'",
-                    from_utf8(c)
-                ),
+            match e.decode_and_unescape(&r) {
+                Ok(c) => assert_eq!(c, "<test>"),
                 Err(e) => panic!(
                     "cannot escape content at position {}: {:?}",
                     r.buffer_position(),
diff --git a/tests/xmlrs_reader_tests.rs b/tests/xmlrs_reader_tests.rs
index 8eaa66c..28401b7 100644
--- a/tests/xmlrs_reader_tests.rs
+++ b/tests/xmlrs_reader_tests.rs
@@ -243,7 +243,7 @@ fn issue_93_large_characters_in_entity_references() {
         r#"<hello>&𤶼;</hello>"#,
         r#"
             |StartElement(hello)
-            |1:10 FailedUnescape([38, 240, 164, 182, 188, 59]; Error while escaping character at range 1..5: Unrecognized escape symbol: Ok("𤶼"))
+            |1:10 FailedUnescape([38, 240, 164, 182, 188, 59]; Error while escaping character at range 1..5: Unrecognized escape symbol: "𤶼")
             |EndElement(hello)
             |EndDocument
         "#,
@@ -417,12 +417,12 @@ fn make_attrs(e: &BytesStart, decoder: Decoder) -> ::std::result::Result<String,
                 if a.key.as_namespace_binding().is_none() {
                     let key = decoder.decode(a.key.as_ref()).unwrap();
                     let value = decoder.decode(a.value.as_ref()).unwrap();
-                    let unescaped_value = unescape(value.as_bytes()).unwrap();
+                    let unescaped_value = unescape(&value).unwrap();
                     atts.push(format!(
                         "{}=\"{}\"",
                         key,
                         // unescape does not change validity of an UTF-8 string
-                        from_utf8(&*unescaped_value).unwrap()
+                        &unescaped_value
                     ));
                 }
             }
@@ -457,8 +457,8 @@ fn xmlrs_display(opt_event: Result<(ResolveResult, Event)>, decoder: Decoder) ->
         }
         Ok((_, Event::Comment(e))) => format!("Comment({})", decoder.decode(&e).unwrap()),
         Ok((_, Event::CData(e))) => format!("CData({})", decoder.decode(&e).unwrap()),
-        Ok((_, Event::Text(e))) => match unescape(decoder.decode(&e).unwrap().as_bytes()) {
-            Ok(c) => format!("Characters({})", from_utf8(c.as_ref()).unwrap()),
+        Ok((_, Event::Text(e))) => match unescape(&decoder.decode(&e).unwrap()) {
+            Ok(c) => format!("Characters({})", &c),
             Err(err) => format!("FailedUnescape({:?}; {})", e.escape(), err),
         },
         Ok((_, Event::Decl(e))) => {