Repository: tafia/quick-xml (rust)

Issue:
`Reader<R: BufRead>` unnecessarily panics on 32-bit targets when the document size exceeds 4 GiB
[`quick_xml::reader::Reader<R: BufRead>`](https://docs.rs/quick-xml/0.31.0/quick_xml/reader/struct.Reader.html#impl-Reader%3CR%3E-1) allows reading arbitrarily large files without allocating much memory. I am using this in combination with [`flate2::read::GzDecoder`](https://docs.rs/flate2/1.0.30/flate2/read/struct.GzDecoder.html) in order to decompress and simultaneously parse very large XML files (several gigabytes per document) without having to store the decompressed file (either in memory or on disk).

Unfortunately, because offsets within the document are computed as `usize` internally, this causes `quick-xml` to panic when the document size exceeds 4 GiB on 32-bit targets.

Notably, [`buffer_position()` returns `usize`](https://docs.rs/quick-xml/0.31.0/quick_xml/reader/struct.Reader.html#method.buffer_position), which is not necessarily sufficient for representing the correct position if the input document size exceeds 4 GiB.

Rust's built-in `std::io` types typically use `u64` for file sizes (and `i64` for relative offsets within files). Hence, I believe it would be appropriate to use `u64` for internal computations (and to return it from [`buffer_position()`](https://docs.rs/quick-xml/0.31.0/quick_xml/reader/struct.Reader.html#method.buffer_position) etc.) as well.

Pull request description:
Fix overflow when read 4GB+ fiiles on 32-bit targets
Added new check to CI and check that tests (for sync and async methods) are failed before the fix (I checked) and passed after.

Fixes #751

Commit which is failed before the fix: https://github.com/Mingun/quick-xml/commit/f5608f0929a83df15b97da664e44ebb608c12749
Job result for it: https://github.com/Mingun/quick-xml/actions/runs/9650155180/job/26615200796

Patch:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index ba7e7ab..a9020bd 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -103,3 +103,29 @@ jobs:
         verbose: true
       continue-on-error: true
 
+  # Check that tests that are sensitive to target are passed
+  x86:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+    - name: Install 32-bit target
+      run: rustup target add i686-unknown-linux-gnu
+    - name: Install 32-bit libs (for criterion)
+      # Criterion wants to compile something.
+      # Cargo builds criterion even when it is not required for those tests.
+      # Without those libs compilation failed with:
+      # error: linking with `cc` failed: exit status: 1
+      #   |
+      #   = note: LC_ALL="C" PATH="..." ...
+      #   = note: /usr/bin/ld: cannot find Scrt1.o: No such file or directory
+      #           /usr/bin/ld: cannot find crti.o: No such file or directory
+      #           /usr/bin/ld: skipping incompatible /usr/lib/gcc/x86_64-linux-gnu/11/libgcc.a when searching for -lgcc
+      #           /usr/bin/ld: cannot find -lgcc: No such file or directory
+      #           collect2: error: ld returned 1 exit status
+      # Fixed as suggested in this answer:
+      # https://stackoverflow.com/a/16016792/7518605
+      run: sudo apt install gcc-multilib
+    - name: Run some tests on 32-bit target
+      run: cargo test --target i686-unknown-linux-gnu --test issues
+    - name: Run some tests on 32-bit target (async-tokio)
+      run: cargo test --target i686-unknown-linux-gnu --features async-tokio --test async-tokio
diff --git a/Changelog.md b/Changelog.md
index 255f2b3..22096c2 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -17,12 +17,17 @@
 
 ### Bug Fixes
 
+- [#751]: Fix internal overflow when read 4GB+ files on 32-bit targets using `Reader<impl BufRead>` readers.
+
 ### Misc Changes
 
 - [#760]: `Attribute::decode_and_unescape_value` and `Attribute::decode_and_unescape_value_with` now
   accepts `Decoder` instead of `Reader`. Use `Reader::decoder()` to get it.
 - [#760]: `Writer::write_event` now consumes event. Use `Event::borrow()` if you want to keep ownership.
+- [#751]: Type of `Reader::error_position()` and `Reader::buffer_position()` changed from `usize` to `u64`.
+- [#751]: Type alias `Span` changed from `Range<usize>` to `Range<u64>`.
 
+[#751]: https://github.com/tafia/quick-xml/issues/751
 [#760]: https://github.com/tafia/quick-xml/pull/760
 
 
diff --git a/src/reader/buffered_reader.rs b/src/reader/buffered_reader.rs
index e515826..548fea9 100644
--- a/src/reader/buffered_reader.rs
+++ b/src/reader/buffered_reader.rs
@@ -53,7 +53,7 @@ macro_rules! impl_buffered_source {
         $($async)? fn read_text $(<$lf>)? (
             &mut self,
             buf: &'b mut Vec<u8>,
-            position: &mut usize,
+            position: &mut u64,
         ) -> ReadTextResult<'b, &'b mut Vec<u8>> {
             let mut read = 0;
             let start = buf.len();
@@ -79,7 +79,7 @@ macro_rules! impl_buffered_source {
 
                         let used = i + 1;
                         self $(.$reader)? .consume(used);
-                        read += used;
+                        read += used as u64;
 
                         *position += read;
                         return ReadTextResult::UpToMarkup(&buf[start..]);
@@ -89,7 +89,7 @@ macro_rules! impl_buffered_source {
 
                         let used = available.len();
                         self $(.$reader)? .consume(used);
-                        read += used;
+                        read += used as u64;
                     }
                 }
             }
@@ -103,7 +103,7 @@ macro_rules! impl_buffered_source {
             &mut self,
             byte: u8,
             buf: &'b mut Vec<u8>,
-            position: &mut usize,
+            position: &mut u64,
         ) -> io::Result<(&'b [u8], bool)> {
             // search byte must be within the ascii range
             debug_assert!(byte.is_ascii());
@@ -127,7 +127,7 @@ macro_rules! impl_buffered_source {
 
                         let used = i + 1;
                         self $(.$reader)? .consume(used);
-                        read += used;
+                        read += used as u64;
 
                         *position += read;
                         return Ok((&buf[start..], true));
@@ -137,7 +137,7 @@ macro_rules! impl_buffered_source {
 
                         let used = available.len();
                         self $(.$reader)? .consume(used);
-                        read += used;
+                        read += used as u64;
                     }
                 }
             }
@@ -151,7 +151,7 @@ macro_rules! impl_buffered_source {
             &mut self,
             mut parser: P,
             buf: &'b mut Vec<u8>,
-            position: &mut usize,
+            position: &mut u64,
         ) -> Result<&'b [u8]> {
             let mut read = 0;
             let start = buf.len();
@@ -171,7 +171,7 @@ macro_rules! impl_buffered_source {
 
                     // +1 for `>` which we do not include
                     self $(.$reader)? .consume(i + 1);
-                    read += i + 1;
+                    read += i as u64 + 1;
 
                     *position += read;
                     return Ok(&buf[start..]);
@@ -182,7 +182,7 @@ macro_rules! impl_buffered_source {
 
                 let used = available.len();
                 self $(.$reader)? .consume(used);
-                read += used;
+                read += used as u64;
             }
 
             *position += read;
@@ -193,7 +193,7 @@ macro_rules! impl_buffered_source {
         $($async)? fn read_bang_element $(<$lf>)? (
             &mut self,
             buf: &'b mut Vec<u8>,
-            position: &mut usize,
+            position: &mut u64,
         ) -> Result<(BangType, &'b [u8])> {
             // Peeked one bang ('!') before being called, so it's guaranteed to
             // start with it.
@@ -216,7 +216,7 @@ macro_rules! impl_buffered_source {
                             buf.extend_from_slice(consumed);
 
                             self $(.$reader)? .consume(used);
-                            read += used;
+                            read += used as u64;
 
                             *position += read;
                             return Ok((bang_type, &buf[start..]));
@@ -225,7 +225,7 @@ macro_rules! impl_buffered_source {
 
                             let used = available.len();
                             self $(.$reader)? .consume(used);
-                            read += used;
+                            read += used as u64;
                         }
                     }
                     Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
@@ -241,14 +241,14 @@ macro_rules! impl_buffered_source {
         }
 
         #[inline]
-        $($async)? fn skip_whitespace(&mut self, position: &mut usize) -> io::Result<()> {
+        $($async)? fn skip_whitespace(&mut self, position: &mut u64) -> io::Result<()> {
             loop {
                 break match self $(.$reader)? .fill_buf() $(.$await)? {
                     Ok(n) => {
                         let count = n.iter().position(|b| !is_whitespace(*b)).unwrap_or(n.len());
                         if count > 0 {
                             self $(.$reader)? .consume(count);
-                            *position += count;
+                            *position += count as u64;
                             continue;
                         } else {
                             Ok(())
diff --git a/src/reader/mod.rs b/src/reader/mod.rs
index bfef4d9..843b703 100644
--- a/src/reader/mod.rs
+++ b/src/reader/mod.rs
@@ -416,7 +416,7 @@ pub use ns_reader::NsReader;
 pub use pi::PiParser;
 
 /// Range of input in bytes, that corresponds to some piece of XML
-pub type Span = Range<usize>;
+pub type Span = Range<u64>;
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -619,7 +619,8 @@ impl<R> Reader<R> {
     /// let mut buf = Vec::new();
     ///
     /// fn into_line_and_column(reader: Reader<Cursor<&[u8]>>) -> (usize, usize) {
-    ///     let end_pos = reader.buffer_position();
+    ///     // We known that size cannot exceed usize::MAX because we created parser from single &[u8]
+    ///     let end_pos = reader.buffer_position() as usize;
     ///     let mut cursor = reader.into_inner();
     ///     let s = String::from_utf8(cursor.into_inner()[0..end_pos].to_owned())
     ///         .expect("can't make a string");
@@ -667,7 +668,7 @@ impl<R> Reader<R> {
     }
 
     /// Gets the current byte position in the input data.
-    pub const fn buffer_position(&self) -> usize {
+    pub const fn buffer_position(&self) -> u64 {
         // when internal state is InsideMarkup, we have actually read until '<',
         // which we don't want to show
         if let ParseState::InsideMarkup = self.state.state {
@@ -688,7 +689,7 @@ impl<R> Reader<R> {
     /// markup element (i. e. to the `<` character).
     ///
     /// This position is always `<= buffer_position()`.
-    pub const fn error_position(&self) -> usize {
+    pub const fn error_position(&self) -> u64 {
         self.state.last_error_offset
     }
 
@@ -813,7 +814,7 @@ trait XmlSource<'r, B> {
     /// - `position`: Will be increased by amount of bytes consumed
     ///
     /// [events]: crate::events::Event
-    fn read_text(&mut self, buf: B, position: &mut usize) -> ReadTextResult<'r, B>;
+    fn read_text(&mut self, buf: B, position: &mut u64) -> ReadTextResult<'r, B>;
 
     /// Read input until `byte` is found or end of input is reached.
     ///
@@ -845,7 +846,7 @@ trait XmlSource<'r, B> {
         &mut self,
         byte: u8,
         buf: B,
-        position: &mut usize,
+        position: &mut u64,
     ) -> io::Result<(&'r [u8], bool)>;
 
     /// Read input until processing instruction is finished.
@@ -867,7 +868,7 @@ trait XmlSource<'r, B> {
     /// reader which provides bytes fed into the parser.
     ///
     /// [events]: crate::events::Event
-    fn read_with<P>(&mut self, parser: P, buf: B, position: &mut usize) -> Result<&'r [u8]>
+    fn read_with<P>(&mut self, parser: P, buf: B, position: &mut u64) -> Result<&'r [u8]>
     where
         P: Parser;
 
@@ -886,14 +887,14 @@ trait XmlSource<'r, B> {
     /// - `position`: Will be increased by amount of bytes consumed
     ///
     /// [events]: crate::events::Event
-    fn read_bang_element(&mut self, buf: B, position: &mut usize) -> Result<(BangType, &'r [u8])>;
+    fn read_bang_element(&mut self, buf: B, position: &mut u64) -> Result<(BangType, &'r [u8])>;
 
     /// Consume and discard all the whitespace until the next non-whitespace
     /// character or EOF.
     ///
     /// # Parameters
     /// - `position`: Will be increased by amount of bytes consumed
-    fn skip_whitespace(&mut self, position: &mut usize) -> io::Result<()>;
+    fn skip_whitespace(&mut self, position: &mut u64) -> io::Result<()>;
 
     /// Return one character without consuming it, so that future `read_*` calls
     /// will still include it. On EOF, return `None`.
diff --git a/src/reader/slice_reader.rs b/src/reader/slice_reader.rs
index 89a9dd9..88b9e2f 100644
--- a/src/reader/slice_reader.rs
+++ b/src/reader/slice_reader.rs
@@ -229,7 +229,10 @@ impl<'a> Reader<&'a [u8]> {
         let buffer = self.reader;
         let span = self.read_to_end(end)?;
 
-        self.decoder().decode(&buffer[0..span.len()])
+        let len = span.end - span.start;
+        // SAFETY: `span` can only contain indexes up to usize::MAX because it
+        // was created from offsets from a single &[u8] slice
+        self.decoder().decode(&buffer[0..len as usize])
     }
 }
 
@@ -258,7 +261,7 @@ impl<'a> XmlSource<'a, ()> for &'a [u8] {
     }
 
     #[inline]
-    fn read_text(&mut self, _buf: (), position: &mut usize) -> ReadTextResult<'a, ()> {
+    fn read_text(&mut self, _buf: (), position: &mut u64) -> ReadTextResult<'a, ()> {
         match memchr::memchr(b'<', self) {
             Some(0) => {
                 *position += 1;
@@ -266,13 +269,13 @@ impl<'a> XmlSource<'a, ()> for &'a [u8] {
                 ReadTextResult::Markup(())
             }
             Some(i) => {
-                *position += i + 1;
+                *position += i as u64 + 1;
                 let bytes = &self[..i];
                 *self = &self[i + 1..];
                 ReadTextResult::UpToMarkup(bytes)
             }
             None => {
-                *position += self.len();
+                *position += self.len() as u64;
                 let bytes = &self[..];
                 *self = &[];
                 ReadTextResult::UpToEof(bytes)
@@ -285,18 +288,18 @@ impl<'a> XmlSource<'a, ()> for &'a [u8] {
         &mut self,
         byte: u8,
         _buf: (),
-        position: &mut usize,
+        position: &mut u64,
     ) -> io::Result<(&'a [u8], bool)> {
         // search byte must be within the ascii range
         debug_assert!(byte.is_ascii());
 
         if let Some(i) = memchr::memchr(byte, self) {
-            *position += i + 1;
+            *position += i as u64 + 1;
             let bytes = &self[..i];
             *self = &self[i + 1..];
             Ok((bytes, true))
         } else {
-            *position += self.len();
+            *position += self.len() as u64;
             let bytes = &self[..];
             *self = &[];
             Ok((bytes, false))
@@ -304,28 +307,24 @@ impl<'a> XmlSource<'a, ()> for &'a [u8] {
     }
 
     #[inline]
-    fn read_with<P>(&mut self, mut parser: P, _buf: (), position: &mut usize) -> Result<&'a [u8]>
+    fn read_with<P>(&mut self, mut parser: P, _buf: (), position: &mut u64) -> Result<&'a [u8]>
     where
         P: Parser,
     {
         if let Some(i) = parser.feed(self) {
             // +1 for `>` which we do not include
-            *position += i + 1;
+            *position += i as u64 + 1;
             let bytes = &self[..i];
             *self = &self[i + 1..];
             return Ok(bytes);
         }
 
-        *position += self.len();
+        *position += self.len() as u64;
         Err(Error::Syntax(P::eof_error()))
     }
 
     #[inline]
-    fn read_bang_element(
-        &mut self,
-        _buf: (),
-        position: &mut usize,
-    ) -> Result<(BangType, &'a [u8])> {
+    fn read_bang_element(&mut self, _buf: (), position: &mut u64) -> Result<(BangType, &'a [u8])> {
         // Peeked one bang ('!') before being called, so it's guaranteed to
         // start with it.
         debug_assert_eq!(self[0], b'!');
@@ -333,22 +332,22 @@ impl<'a> XmlSource<'a, ()> for &'a [u8] {
         let bang_type = BangType::new(self[1..].first().copied())?;
 
         if let Some((bytes, i)) = bang_type.parse(&[], self) {
-            *position += i;
+            *position += i as u64;
             *self = &self[i..];
             return Ok((bang_type, bytes));
         }
 
-        *position += self.len();
+        *position += self.len() as u64;
         Err(bang_type.to_err())
     }
 
     #[inline]
-    fn skip_whitespace(&mut self, position: &mut usize) -> io::Result<()> {
+    fn skip_whitespace(&mut self, position: &mut u64) -> io::Result<()> {
         let whitespaces = self
             .iter()
             .position(|b| !is_whitespace(*b))
             .unwrap_or(self.len());
-        *position += whitespaces;
+        *position += whitespaces as u64;
         *self = &self[whitespaces..];
         Ok(())
     }
diff --git a/src/reader/state.rs b/src/reader/state.rs
index 128a21a..9b1117e 100644
--- a/src/reader/state.rs
+++ b/src/reader/state.rs
@@ -15,12 +15,12 @@ use crate::utils::{is_whitespace, name_len};
 #[derive(Clone, Debug)]
 pub(super) struct ReaderState {
     /// Number of bytes read from the source of data since the reader was created
-    pub offset: usize,
+    pub offset: u64,
     /// A snapshot of an `offset` of the last error returned. It can be less than
     /// `offset`, because some errors conveniently report at earlier position,
     /// and changing `offset` is not possible, because `Error::IllFormed` errors
     /// are recoverable.
-    pub last_error_offset: usize,
+    pub last_error_offset: u64,
     /// Defines how to process next byte
     pub state: ParseState,
     /// User-defined settings that affect parsing
@@ -115,7 +115,7 @@ impl ReaderState {
                             //   ^ :   :           - self.offset - len
                             //     ^   :           - self.offset - len + 2
                             //         ^           - self.offset - len + 2 + p
-                            self.last_error_offset = self.offset - len + 2 + p;
+                            self.last_error_offset = self.offset - len as u64 + 2 + p as u64;
                             return Err(Error::IllFormed(IllFormedError::DoubleHyphenInComment));
                         }
                         // Continue search after single `-` (+1 to skip it)
@@ -156,7 +156,7 @@ impl ReaderState {
                 // <!....>
                 //  ^^^^^ - `buf` does not contain `<` and `>`, but `self.offset` is after `>`.
                 // ^------- We report error at that position, so we need to subtract 2 and buf len
-                self.last_error_offset = self.offset - len - 2;
+                self.last_error_offset = self.offset - len as u64 - 2;
                 Err(bang_type.to_err())
             }
         }
@@ -201,7 +201,7 @@ impl ReaderState {
 
                         // Report error at start of the end tag at `<` character
                         // -2 for `<` and `>`
-                        self.last_error_offset = self.offset - buf.len() - 2;
+                        self.last_error_offset = self.offset - buf.len() as u64 - 2;
                         return Err(Error::IllFormed(IllFormedError::MismatchedEndTag {
                             expected,
                             found: decoder.decode(name).unwrap_or_default().into_owned(),
@@ -214,7 +214,7 @@ impl ReaderState {
             None => {
                 // Report error at start of the end tag at `<` character
                 // -2 for `<` and `>`
-                self.last_error_offset = self.offset - buf.len() - 2;
+                self.last_error_offset = self.offset - buf.len() as u64 - 2;
                 return Err(Error::IllFormed(IllFormedError::UnmatchedEndTag(
                     decoder.decode(name).unwrap_or_default().into_owned(),
                 )));
@@ -259,7 +259,7 @@ impl ReaderState {
             // <?....EOF
             //  ^^^^^ - `buf` does not contains `<`, but we want to report error at `<`,
             //          so we move offset to it (-2 for `<` and `>`)
-            self.last_error_offset = self.offset - len - 2;
+            self.last_error_offset = self.offset - len as u64 - 2;
             Err(Error::Syntax(SyntaxError::UnclosedPIOrXmlDecl))
         }
     }
diff --git a/src/utils.rs b/src/utils.rs
index 09b6298..95b3b65 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,7 +1,14 @@
 use std::borrow::{Borrow, Cow};
 use std::fmt::{self, Debug, Formatter};
+use std::io;
 use std::ops::Deref;
 
+#[cfg(feature = "async-tokio")]
+use std::{
+    pin::Pin,
+    task::{Context, Poll},
+};
+
 #[cfg(feature = "serialize")]
 use serde::de::{Deserialize, Deserializer, Error, Visitor};
 #[cfg(feature = "serialize")]
@@ -197,6 +204,75 @@ impl<'de> Serialize for Bytes<'de> {
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 
+/// A simple producer of infinite stream of bytes, useful in tests.
+///
+/// Will repeat `chunk` field indefinitely.
+pub struct Fountain<'a> {
+    /// That piece of data repeated infinitely...
+    pub chunk: &'a [u8],
+    /// Part of `chunk` that was consumed by BufRead impl
+    pub consumed: usize,
+    /// The overall count of read bytes
+    pub overall_read: u64,
+}
+
+impl<'a> io::Read for Fountain<'a> {
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        let available = &self.chunk[self.consumed..];
+        let len = buf.len().min(available.len());
+        let (portion, _) = available.split_at(len);
+
+        buf.copy_from_slice(portion);
+        Ok(len)
+    }
+}
+
+impl<'a> io::BufRead for Fountain<'a> {
+    #[inline]
+    fn fill_buf(&mut self) -> io::Result<&[u8]> {
+        Ok(&self.chunk[self.consumed..])
+    }
+
+    fn consume(&mut self, amt: usize) {
+        self.consumed += amt;
+        if self.consumed == self.chunk.len() {
+            self.consumed = 0;
+        }
+        self.overall_read += amt as u64;
+    }
+}
+
+#[cfg(feature = "async-tokio")]
+impl<'a> tokio::io::AsyncRead for Fountain<'a> {
+    fn poll_read(
+        self: Pin<&mut Self>,
+        _cx: &mut Context<'_>,
+        buf: &mut tokio::io::ReadBuf<'_>,
+    ) -> Poll<io::Result<()>> {
+        let available = &self.chunk[self.consumed..];
+        let len = buf.remaining().min(available.len());
+        let (portion, _) = available.split_at(len);
+
+        buf.put_slice(portion);
+        Poll::Ready(Ok(()))
+    }
+}
+
+#[cfg(feature = "async-tokio")]
+impl<'a> tokio::io::AsyncBufRead for Fountain<'a> {
+    #[inline]
+    fn poll_fill_buf(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<&[u8]>> {
+        Poll::Ready(io::BufRead::fill_buf(self.get_mut()))
+    }
+
+    #[inline]
+    fn consume(self: Pin<&mut Self>, amt: usize) {
+        io::BufRead::consume(self.get_mut(), amt);
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+
 /// A function to check whether the byte is a whitespace (blank, new line, carriage return or tab).
 #[inline]
 pub const fn is_whitespace(b: u8) -> bool {


Test patch:
diff --git a/tests/async-tokio.rs b/tests/async-tokio.rs
index 9323fd9..3d70c50 100644
--- a/tests/async-tokio.rs
+++ b/tests/async-tokio.rs
@@ -1,5 +1,8 @@
+use std::iter;
+
 use pretty_assertions::assert_eq;
 use quick_xml::events::Event::*;
+use quick_xml::name::QName;
 use quick_xml::reader::Reader;
 
 // Import `small_buffers_tests!`
@@ -36,3 +39,65 @@ async fn test_sample() {
     }
     assert_eq!((count, reads), (1247, 5245));
 }
+
+/// Regression test for https://github.com/tafia/quick-xml/issues/751
+///
+/// Actually, that error was not found in async reader, but we would to test it as well.
+#[tokio::test]
+async fn issue751() {
+    let mut text = Vec::new();
+    let mut chunk = Vec::new();
+    chunk.extend_from_slice(b"<content>");
+    for data in iter::repeat(b"some text inside").take(1000) {
+        chunk.extend_from_slice(data);
+        text.extend_from_slice(data);
+    }
+    chunk.extend_from_slice(b"</content>");
+
+    let mut reader = Reader::from_reader(quick_xml::utils::Fountain {
+        chunk: &chunk,
+        consumed: 0,
+        overall_read: 0,
+    });
+    let mut buf = Vec::new();
+    let mut starts = 0u64;
+    let mut ends = 0u64;
+    let mut texts = 0u64;
+    loop {
+        buf.clear();
+        match reader.read_event_into_async(&mut buf).await {
+            Err(e) => panic!("Error at position {}: {:?}", reader.error_position(), e),
+            Ok(Eof) => break,
+
+            Ok(Start(e)) => {
+                starts += 1;
+                assert_eq!(
+                    e.name(),
+                    QName(b"content"),
+                    "starts: {starts}, ends: {ends}, texts: {texts}"
+                );
+            }
+            Ok(End(e)) => {
+                ends += 1;
+                assert_eq!(
+                    e.name(),
+                    QName(b"content"),
+                    "starts: {starts}, ends: {ends}, texts: {texts}"
+                );
+            }
+            Ok(Text(e)) => {
+                texts += 1;
+                assert_eq!(
+                    e.as_ref(),
+                    text,
+                    "starts: {starts}, ends: {ends}, texts: {texts}"
+                );
+            }
+            _ => (),
+        }
+        // If we successfully read more than `u32::MAX`, the test is passed
+        if reader.get_ref().overall_read >= u32::MAX as u64 {
+            break;
+        }
+    }
+}
diff --git a/tests/issues.rs b/tests/issues.rs
index 198a47d..163a17b 100644
--- a/tests/issues.rs
+++ b/tests/issues.rs
@@ -2,6 +2,7 @@
 //!
 //! Name each module / test as `issue<GH number>` and keep sorted by issue number
 
+use std::iter;
 use std::sync::mpsc;
 
 use quick_xml::errors::{Error, IllFormedError, SyntaxError};
@@ -276,3 +277,63 @@ fn issue706() {
         }
     }
 }
+
+/// Regression test for https://github.com/tafia/quick-xml/issues/751
+#[test]
+fn issue751() {
+    let mut text = Vec::new();
+    let mut chunk = Vec::new();
+    chunk.extend_from_slice(b"<content>");
+    for data in iter::repeat(b"some text inside").take(1000) {
+        chunk.extend_from_slice(data);
+        text.extend_from_slice(data);
+    }
+    chunk.extend_from_slice(b"</content>");
+
+    let mut reader = Reader::from_reader(quick_xml::utils::Fountain {
+        chunk: &chunk,
+        consumed: 0,
+        overall_read: 0,
+    });
+    let mut buf = Vec::new();
+    let mut starts = 0u64;
+    let mut ends = 0u64;
+    let mut texts = 0u64;
+    loop {
+        buf.clear();
+        match reader.read_event_into(&mut buf) {
+            Err(e) => panic!("Error at position {}: {:?}", reader.error_position(), e),
+            Ok(Event::Eof) => break,
+
+            Ok(Event::Start(e)) => {
+                starts += 1;
+                assert_eq!(
+                    e.name(),
+                    QName(b"content"),
+                    "starts: {starts}, ends: {ends}, texts: {texts}"
+                );
+            }
+            Ok(Event::End(e)) => {
+                ends += 1;
+                assert_eq!(
+                    e.name(),
+                    QName(b"content"),
+                    "starts: {starts}, ends: {ends}, texts: {texts}"
+                );
+            }
+            Ok(Event::Text(e)) => {
+                texts += 1;
+                assert_eq!(
+                    e.as_ref(),
+                    text,
+                    "starts: {starts}, ends: {ends}, texts: {texts}"
+                );
+            }
+            _ => (),
+        }
+        // If we successfully read more than `u32::MAX`, the test is passed
+        if reader.get_ref().overall_read >= u32::MAX as u64 {
+            break;
+        }
+    }
+}
diff --git a/tests/reader-errors.rs b/tests/reader-errors.rs
index 24a61c3..e5d4bfc 100644
--- a/tests/reader-errors.rs
+++ b/tests/reader-errors.rs
@@ -219,7 +219,7 @@ mod syntax {
             }
         };
         ($test:ident($xml:literal) => $cause:expr) => {
-            err!($test($xml) => $xml.len(), $cause);
+            err!($test($xml) => $xml.len() as u64, $cause);
         };
     }
 
@@ -435,7 +435,7 @@ mod ill_formed {
                         match reader.read_event() {
                             Err(Error::IllFormed(cause)) => assert_eq!(
                                 (cause, reader.error_position(), reader.buffer_position()),
-                                ($cause, $pos, $xml.len()),
+                                ($cause, $pos, $xml.len() as u64),
                             ),
                             x => panic!("Expected `Err(IllFormed(_))`, but got {:?}", x),
                         }
@@ -447,7 +447,7 @@ mod ill_formed {
                         );
                         assert_eq!(
                             reader.buffer_position(),
-                            xml.len(),
+                            xml.len() as u64,
                             ".buffer_position() is incorrect in the end"
                         );
                     }
@@ -461,7 +461,7 @@ mod ill_formed {
                         match reader.read_event_into(&mut buf) {
                             Err(Error::IllFormed(cause)) => assert_eq!(
                                 (cause, reader.error_position(), reader.buffer_position()),
-                                ($cause, $pos, $xml.len()),
+                                ($cause, $pos, $xml.len() as u64),
                             ),
                             x => panic!("Expected `Err(IllFormed(_))`, but got {:?}", x),
                         }
@@ -473,7 +473,7 @@ mod ill_formed {
                         );
                         assert_eq!(
                             reader.buffer_position(),
-                            xml.len(),
+                            xml.len() as u64,
                             ".buffer_position() is incorrect in the end"
                         );
                     }
@@ -488,7 +488,7 @@ mod ill_formed {
                         match reader.read_event_into_async(&mut buf).await {
                             Err(Error::IllFormed(cause)) => assert_eq!(
                                 (cause, reader.error_position(), reader.buffer_position()),
-                                ($cause, $pos, $xml.len()),
+                                ($cause, $pos, $xml.len() as u64),
                             ),
                             x => panic!("Expected `Err(IllFormed(_))`, but got {:?}", x),
                         }
@@ -500,7 +500,7 @@ mod ill_formed {
                         );
                         assert_eq!(
                             reader.buffer_position(),
-                            xml.len(),
+                            xml.len() as u64,
                             ".buffer_position() is incorrect in the end"
                         );
                     }
@@ -518,7 +518,7 @@ mod ill_formed {
                         match reader.read_resolved_event() {
                             Err(Error::IllFormed(cause)) => assert_eq!(
                                 (cause, reader.error_position(), reader.buffer_position()),
-                                ($cause, $pos, $xml.len()),
+                                ($cause, $pos, $xml.len() as u64),
                             ),
                             x => panic!("Expected `Err(IllFormed(_))`, but got {:?}", x),
                         }
@@ -533,7 +533,7 @@ mod ill_formed {
                         );
                         assert_eq!(
                             reader.buffer_position(),
-                            xml.len(),
+                            xml.len() as u64,
                             ".buffer_position() is incorrect in the end"
                         );
                     }
@@ -547,7 +547,7 @@ mod ill_formed {
                         match reader.read_resolved_event_into(&mut buf) {
                             Err(Error::IllFormed(cause)) => assert_eq!(
                                 (cause, reader.error_position(), reader.buffer_position()),
-                                ($cause, $pos, $xml.len()),
+                                ($cause, $pos, $xml.len() as u64),
                             ),
                             x => panic!("Expected `Err(IllFormed(_))`, but got {:?}", x),
                         }
@@ -562,7 +562,7 @@ mod ill_formed {
                         );
                         assert_eq!(
                             reader.buffer_position(),
-                            xml.len(),
+                            xml.len() as u64,
                             ".buffer_position() is incorrect in the end"
                         );
                     }
@@ -577,7 +577,7 @@ mod ill_formed {
                         match reader.read_resolved_event_into_async(&mut buf).await {
                             Err(Error::IllFormed(cause)) => assert_eq!(
                                 (cause, reader.error_position(), reader.buffer_position()),
-                                ($cause, $pos, $xml.len()),
+                                ($cause, $pos, $xml.len() as u64),
                             ),
                             x => panic!("Expected `Err(IllFormed(_))`, but got {:?}", x),
                         }
@@ -593,7 +593,7 @@ mod ill_formed {
                         );
                         assert_eq!(
                             reader.buffer_position(),
-                            xml.len(),
+                            xml.len() as u64,
                             ".buffer_position() is incorrect in the end"
                         );
                     }
@@ -621,7 +621,7 @@ mod ill_formed {
                         match reader.read_event() {
                             Err(Error::IllFormed(cause)) => assert_eq!(
                                 (cause, reader.error_position(), reader.buffer_position()),
-                                ($cause, $pos, $xml.len()),
+                                ($cause, $pos, $xml.len() as u64),
                             ),
                             x => panic!("Expected `Err(IllFormed(_))`, but got {:?}", x),
                         }
@@ -633,7 +633,7 @@ mod ill_formed {
                         );
                         assert_eq!(
                             reader.buffer_position(),
-                            xml.len(),
+                            xml.len() as u64,
                             ".buffer_position() is incorrect in the end"
                         );
                     }
@@ -650,7 +650,7 @@ mod ill_formed {
                         match reader.read_event_into(&mut buf) {
                             Err(Error::IllFormed(cause)) => assert_eq!(
                                 (cause, reader.error_position(), reader.buffer_position()),
-                                ($cause, $pos, $xml.len()),
+                                ($cause, $pos, $xml.len() as u64),
                             ),
                             x => panic!("Expected `Err(IllFormed(_))`, but got {:?}", x),
                         }
@@ -662,7 +662,7 @@ mod ill_formed {
                         );
                         assert_eq!(
                             reader.buffer_position(),
-                            xml.len(),
+                            xml.len() as u64,
                             ".buffer_position() is incorrect in the end"
                         );
                     }
@@ -681,7 +681,7 @@ mod ill_formed {
                         match reader.read_event_into_async(&mut buf).await {
                             Err(Error::IllFormed(cause)) => assert_eq!(
                                 (cause, reader.error_position(), reader.buffer_position()),
-                                ($cause, $pos, $xml.len()),
+                                ($cause, $pos, $xml.len() as u64),
                             ),
                             x => panic!("Expected `Err(IllFormed(_))`, but got {:?}", x),
                         }
@@ -693,7 +693,7 @@ mod ill_formed {
                         );
                         assert_eq!(
                             reader.buffer_position(),
-                            xml.len(),
+                            xml.len() as u64,
                             ".buffer_position() is incorrect in the end"
                         );
                     }
@@ -714,7 +714,7 @@ mod ill_formed {
                         match reader.read_resolved_event() {
                             Err(Error::IllFormed(cause)) => assert_eq!(
                                 (cause, reader.error_position(), reader.buffer_position()),
-                                ($cause, $pos, $xml.len()),
+                                ($cause, $pos, $xml.len() as u64),
                             ),
                             x => panic!("Expected `Err(IllFormed(_))`, but got {:?}", x),
                         }
@@ -729,7 +729,7 @@ mod ill_formed {
                         );
                         assert_eq!(
                             reader.buffer_position(),
-                            xml.len(),
+                            xml.len() as u64,
                             ".buffer_position() is incorrect in the end"
                         );
                     }
@@ -746,7 +746,7 @@ mod ill_formed {
                         match reader.read_resolved_event_into(&mut buf) {
                             Err(Error::IllFormed(cause)) => assert_eq!(
                                 (cause, reader.error_position(), reader.buffer_position()),
-                                ($cause, $pos, $xml.len()),
+                                ($cause, $pos, $xml.len() as u64),
                             ),
                             x => panic!("Expected `Err(IllFormed(_))`, but got {:?}", x),
                         }
@@ -761,7 +761,7 @@ mod ill_formed {
                         );
                         assert_eq!(
                             reader.buffer_position(),
-                            xml.len(),
+                            xml.len() as u64,
                             ".buffer_position() is incorrect in the end"
                         );
                     }
@@ -780,7 +780,7 @@ mod ill_formed {
                         match reader.read_resolved_event_into_async(&mut buf).await {
                             Err(Error::IllFormed(cause)) => assert_eq!(
                                 (cause, reader.error_position(), reader.buffer_position()),
-                                ($cause, $pos, $xml.len()),
+                                ($cause, $pos, $xml.len() as u64),
                             ),
                             x => panic!("Expected `Err(IllFormed(_))`, but got {:?}", x),
                         }
@@ -796,7 +796,7 @@ mod ill_formed {
                         );
                         assert_eq!(
                             reader.buffer_position(),
-                            xml.len(),
+                            xml.len() as u64,
                             ".buffer_position() is incorrect in the end"
                         );
                     }