Skip to content

Return OutputFull when replaying non-BOM bytes into a short buffer - #149

Merged
hsivonen merged 1 commit into
hsivonen:mainfrom
yuxi-liu-wired:fix/bom-replay-output-full
Oct 30, 2026
Merged

hsivonen merged 1 commit into
hsivonen:mainfrom
yuxi-liu-wired:fix/bom-replay-output-full

Conversation

@yuxi-liu-wired

Copy link
Copy Markdown
Contributor

A decoder made with new_decoder() panics when the first bytes of a potential BOM arrive at the end of one buffer, turn out not to be a BOM, and the next call's output buffer is too short for them. A four-byte buffer is enough to trigger it, and four bytes is the documented minimum for UTF-8:

let mut decoder = IBM866.new_decoder();
let mut dst = [0u8; 16];
decoder.decode_to_utf8(b"\xEF\xBB", &mut dst, false); // InputEmpty, both bytes held
let mut dst = [0u8; 4];
decoder.decode_to_utf8(b"ab", &mut dst, false);
// panicked at src/macros.rs: Output buffer must have been too small.

decode_to_utf_after_one_potential_bom_byte and decode_to_utf_after_two_potential_bom_bytes decode the held bytes into dst and treat OutputFull as unreachable. That's wrong whenever EF and BB decode to more than four UTF-8 bytes together. Going through every encoding, held prefix and next buffer, I found 704 panicking cases, all with EF BB held and a 4- or 5-byte dst:

  • x-user-defined (U+F7EF U+F7BB),
  • ISO-2022-JP (two U+FFFD),
  • IBM866 (я╗),
  • windows-874,
  • the other single-byte encodings that map EF/BB outside Latin-1.

The same happens through decode_to_str and with the _without_replacement variants.

With this change, both functions return OutputFull and set the life cycle so that the next call replays what wasn't decoded:

  • one held byte goes back to its Seen*First state;
  • EF BB with nothing decoded goes back to SeenUtf8Second;
  • EF decoded but BB not goes to ConvertingWithPendingBB, which the Malformed path already uses for that case.

out_read/first_read stay 0, because none of the held bytes came from the current src.

To check the resumed state, I ran every encoding, held prefix (EF, EF BB, FE, FF), seven next buffers and every output size from the minimum to 8, through decode_to_utf8, decode_to_utf8_without_replacement and decode_to_utf16, retrying after OutputFull. All 19,040 runs give exactly the spec's decode result and final encoding.

The new test test_short_buffer_after_non_bom_bytes covers IBM866, x-user-defined and ISO-2022-JP with every split of the input and buffers of 4–7 bytes, and compares the joined output. It panics without the change. cargo test passes on stable, 1.88, --no-default-features, std, simd-accel (nightly), and the fast-legacy-encode / less-slow-* features. cargo fmt --check passes too.

I found this by fuzzing the streaming decoders against a transcription of the Encoding Standard, with BOM sniffing, random chunking and small output buffers. The test is dedicated to the public domain like the rest of the test module.

When a sniffing decoder has seen the first byte or two of a potential
BOM at the end of one buffer and the next buffer shows that they aren't a
BOM, the held bytes are decoded through the real decoder into the next
call's `dst`. If that `dst` is too short for their output, the decoder
panicked with "Output buffer must have been too small." That happens
with a four-byte UTF-8 buffer (the documented minimum) and any encoding
where EF and BB decode to more than four bytes together: x-user-defined,
ISO-2022-JP, IBM866, windows-874, and the other single-byte encodings
that map them outside Latin-1.

Instead, return OutputFull and set the life cycle so that the next call
replays the bytes that weren't decoded: one held byte goes back to its
Seen*First state, EF BB goes back to SeenUtf8Second, and only BB left
goes to ConvertingWithPendingBB.
@hsivonen
hsivonen merged commit e410a42 into hsivonen:main Oct 30, 2026
4 checks passed
@hsivonen

Copy link
Copy Markdown
Owner

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants