Skip to content

Reset TwoByteMatcher partial match on mismatching byte#37053

Open
junhyeong9812 wants to merge 1 commit into
spring-projects:mainfrom
junhyeong9812:fix/databufferutils-twobyte-matcher-reset
Open

Reset TwoByteMatcher partial match on mismatching byte#37053
junhyeong9812 wants to merge 1 commit into
spring-projects:mainfrom
junhyeong9812:fix/databufferutils-twobyte-matcher-reset

Conversation

@junhyeong9812

Copy link
Copy Markdown
Contributor

Overview

DataBufferUtils.TwoByteMatcher does not reset its partial-match counter when a byte fails to match after the first delimiter byte has matched. As a result the matcher reports a delimiter match across non-contiguous bytes, which causes StringDecoder (and any consumer of DataBufferUtils.matcher(byte[]...)) to silently drop a character whenever a lone \r appears inside a line. This PR adds the missing reset so the two-byte matcher only matches a contiguous delimiter.

Problem

AbstractNestedMatcher.match(byte) deliberately leaves the fallback (what to do on a mismatch mid-match) to its subclasses: KnuthMorrisPrattMatcher overrides match(byte) to backtrack via its suffix-prefix table, and SingleByteMatcher is stateless. TwoByteMatcher, however, inherited the base method without providing any fallback, so after the first delimiter byte matched (matches == 1) the counter stayed at 1 across any number of non-matching bytes, and a later occurrence of the second delimiter byte falsely completed the match.

For the default delimiters used by StringDecoder.allMimeTypes() (\r\n and \n), decoding "a\rXY\nb":

first line
expected a\rXY
actual a\rX

CompositeMatcher prefers the longest delimiter that matches at a position, so the false \r\n match (length 2) is chosen over the real \n (length 1), and the byte before \n is consumed as part of the delimiter and lost.

Fix

TwoByteMatcher now overrides match(byte) to reset the counter to 0 when the incoming byte is not the expected next delimiter byte, then delegates to super.match(b) — mirroring the structure of KnuthMorrisPrattMatcher (its while loop collapses to a single reset because a two-byte delimiter can only be in a matches == 1 partial state). A genuine contiguous delimiter is unaffected: when the byte is the expected one the reset is skipped and the match completes.

Added two tests: a unit test asserting DataBufferUtils.matcher("\r\n") returns -1 for non-contiguous input, and a StringDecoder test asserting the character before a lone \n is preserved.

DataBufferUtils.TwoByteMatcher inherited AbstractNestedMatcher.match(byte)
without providing the mismatch fallback that its siblings implement
(KnuthMorrisPrattMatcher backtracks via its suffix-prefix table, and
SingleByteMatcher is stateless). As a result, once the first delimiter
byte had matched, the match counter stayed at 1 across any number of
intervening non-matching bytes, so a later occurrence of the second
delimiter byte falsely completed the match.

For a two-byte delimiter such as \r\n this made the matcher report a
match across non-contiguous bytes. CompositeMatcher prefers the longest
delimiter that matches at a position, so the false \r\n match was chosen
over a real single \n, causing StringDecoder to strip two bytes and drop
the character preceding a lone \n whenever a line contained a stray \r.

TwoByteMatcher now overrides match(byte) to reset the counter to 0 when
the incoming byte is not the expected next delimiter byte before
delegating to super.match(), mirroring KnuthMorrisPrattMatcher. A
genuine contiguous delimiter is unaffected.

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged or decided on

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants