diff --git a/src/generated.rs b/src/generated.rs index 652f8a124..39bc1d64c 100644 --- a/src/generated.rs +++ b/src/generated.rs @@ -2419,17 +2419,28 @@ impl SkipWhitespace { #[cfg(feature = "std")] impl Read for SkipWhitespace { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let n = self.inner.read(buf)?; + loop { + let n = self.inner.read(buf)?; + if n == 0 { + return Ok(0); + } - let mut written = 0; - for read in 0..n { - if !buf[read].is_ascii_whitespace() { - buf[written] = buf[read]; - written += 1; + let mut written = 0; + for read in 0..n { + if !buf[read].is_ascii_whitespace() { + buf[written] = buf[read]; + written += 1; + } } - } - Ok(written) + // Only report EOF (Ok(0)) when the inner reader is genuinely at end + // (n == 0 above). If an entire read was whitespace, keep reading + // instead of returning Ok(0), which the caller treats as EOF and + // would use to silently truncate the decoded stream. + if written > 0 { + return Ok(written); + } + } } } diff --git a/tests/tx_base64_skip_whitespace.rs b/tests/tx_base64_skip_whitespace.rs index 366dc8580..ec31193c2 100644 --- a/tests/tx_base64_skip_whitespace.rs +++ b/tests/tx_base64_skip_whitespace.rs @@ -6,6 +6,36 @@ use std::io::Cursor; use stellar_xdr::Error; use stellar_xdr::{Limited, Limits, ReadXdr, WriteXdr}; +#[test] +fn test_skip_whitespace_long_run() -> Result<(), Error> { + // A long contiguous whitespace run (2048 bytes here, large enough to fill + // at least one of the base64 decoder's internal read buffers) must not be + // treated as end-of-input. Before the fix, SkipWhitespace::read returned + // Ok(0) when a whole delegate read was whitespace, which the decoder + // interprets as EOF -> silent truncation. + let v_bytes = [1u32.to_xdr(Limits::none())?, 2u32.to_xdr(Limits::none())?].concat(); + let core = base64::engine::general_purpose::STANDARD.encode(&v_bytes); + assert_eq!(core, "AAAAAQAAAAI="); + let ws = " ".repeat(2048); + + // Leading long whitespace run. + let leading = format!("{ws}{core}"); + assert_eq!( + u64::from_xdr_base64(&leading, Limits::none()), + Ok((1u64 << 32) | 2u64) + ); + + // Interior long whitespace run (split the base64 mid-string). + let (a, b) = core.split_at(4); + let interior = format!("{a}{ws}{b}"); + assert_eq!( + u64::from_xdr_base64(&interior, Limits::none()), + Ok((1u64 << 32) | 2u64) + ); + + Ok(()) +} + #[test] fn test_skip_whitespace() -> Result<(), Error> { let v_bytes = [1u32.to_xdr(Limits::none())?, 2u32.to_xdr(Limits::none())?].concat(); diff --git a/xdr-generator-rust/generator/header.rs b/xdr-generator-rust/generator/header.rs index 7418b9f6a..bf9f3683c 100644 --- a/xdr-generator-rust/generator/header.rs +++ b/xdr-generator-rust/generator/header.rs @@ -2342,17 +2342,28 @@ impl SkipWhitespace { #[cfg(feature = "std")] impl Read for SkipWhitespace { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let n = self.inner.read(buf)?; + loop { + let n = self.inner.read(buf)?; + if n == 0 { + return Ok(0); + } - let mut written = 0; - for read in 0..n { - if !buf[read].is_ascii_whitespace() { - buf[written] = buf[read]; - written += 1; + let mut written = 0; + for read in 0..n { + if !buf[read].is_ascii_whitespace() { + buf[written] = buf[read]; + written += 1; + } } - } - Ok(written) + // Only report EOF (Ok(0)) when the inner reader is genuinely at end + // (n == 0 above). If an entire read was whitespace, keep reading + // instead of returning Ok(0), which the caller treats as EOF and + // would use to silently truncate the decoded stream. + if written > 0 { + return Ok(written); + } + } } }