Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions src/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2419,17 +2419,28 @@ impl<R: Read> SkipWhitespace<R> {
#[cfg(feature = "std")]
impl<R: Read> Read for SkipWhitespace<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
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);
}
}
}
}

Expand Down
29 changes: 29 additions & 0 deletions tests/tx_base64_skip_whitespace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@ 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 whitespace run at least as long as the base64 decoder's internal read
// buffer (1024 bytes) must not be treated as end-of-input. Before the fix,
// SkipWhitespace::read returned Ok(0) when a whole delegate read was

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — reworded in 492d03f so the comment no longer pins the base64 crate's private 1024-byte constant; it now just says the run (2048 bytes) is large enough to fill at least one internal read buffer.

// 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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The base64::Engine trait is already explicitly imported at the top of this file (use base64::Engine;, line 3) — that's what resolves the .encode(...) call here, and the pre-existing test_skip_whitespace relies on the same import for its own encode. So method resolution isn't relying on anything non-obvious. Adding use base64::Engine as _; would just duplicate an import that's already present, so leaving as-is.

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();
Expand Down
27 changes: 19 additions & 8 deletions xdr-generator-rust/generator/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2342,17 +2342,28 @@ impl<R: Read> SkipWhitespace<R> {
#[cfg(feature = "std")]
impl<R: Read> Read for SkipWhitespace<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
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);
}
}
}
}

Expand Down