Sitelet https://github.com/java-diff-utils/java-diff-utils/pull/232
Skip to content

Reject a chunk that ends exactly at the end of the target - #232

Open
youdie006 wants to merge 1 commit into
java-diff-utils:masterfrom
youdie006:fix/verify-chunk-end-boundary
Open

Reject a chunk that ends exactly at the end of the target#232
youdie006 wants to merge 1 commit into
java-diff-utils:masterfrom
youdie006:fix/verify-chunk-end-boundary

Conversation

@youdie006

Copy link
Copy Markdown

The problem

Chunk.verifyChunk lets a chunk that ends exactly at the end of the target through its bounds check, so List.get throws instead of the documented PatchFailedException.

With a chunk covering target indices 7..10:

target length expected actual
9 (last 2 past) POSITION_OUT_OF_TARGET POSITION_OUT_OF_TARGET
10 (last 1 past) POSITION_OUT_OF_TARGET IndexOutOfBoundsException: Index 10 out of bounds for length 10
11 OK OK

Reached through the public API as well — Patch.applyTo / DiffUtils.patch on a target one line shorter than the patch expects raises IndexOutOfBoundsException rather than PatchFailedException, so a caller's catch (PatchFailedException) does not hold.

Cause

java-diff-utils/src/main/java/com/github/difflib/patch/Chunk.java:116

if (position + fuzz > target.size() || last - fuzz > target.size()) {

last is an index (last = position + size() - 1, line 114) but is compared against a count. The loop at 119-120 reads up to target.get(last - fuzz), which requires last - fuzz < target.size(), so the second disjunct has to reject >=, not just >.

Why this is the code and not the doc

Patch.java:50-56 documents @throws PatchFailedException if the patch cannot be applied, repeated at Patch.java:68, DiffUtils.java:203, DiffUtils.java:215, and Chunk.java:95/:108. Patch.java:229 also defaults conflictOutput to CONFLICT_PRODUCES_EXCEPTION, so surfacing a failure as PatchFailedException is the intended path. The two-short case already does exactly that; only the one-short boundary leaks.

Scope of the change

Only the second disjunct. The first stays > deliberately: an InsertDelta appending at end of file has an empty source chunk with position == target.size(), and >= there would reject a valid append. An empty chunk has last == position - 1, so the second disjunct is already correct for that case.

Tests

No test referenced POSITION_OUT_OF_TARGET, verifyChunk's bounds, or "could not apply patch" — ChunkTest covered OK and CONTENT_DOES_NOT_MATCH_TARGET only, and the apply-failure tests in PatchWithMyerDiffTest / PatchWithAllDiffAlgorithmsTest all use catch (PatchFailedException e) { fail(...) } around successful applies. Added verifyChunkAtTheEndOfTheTarget covering the boundary and one past it, plus the length that must still succeed.

Verification

mvn -B package on JDK 17: BUILD SUCCESS, 148 tests + 2 in the jgit module, 0 failures, 0 errors — spotless and checkstyle both clean.

Reverting only Chunk.java and keeping the test fails with IndexOutOfBoundsException: Index 10 out of bounds for length 10, so the new test exercises this bug rather than verifyChunk in general.

(Note for anyone reproducing: mvn under JDK 21 fails before running tests — spotless 2.30.0 / palantir-java-format hits NoSuchMethodError: JCTree$JCImport.getQualifiedIdentifier(). JDK 17 is fine.)


Disclosure: this patch was prepared with AI assistance. The reproduction, the red/green check and the build above were executed against this branch; happy to adjust anything on request.

verifyChunk compares last, an index, against target.size(), a count:

    if (position + fuzz > target.size() || last - fuzz > target.size())

The loop below reads target.get(last - fuzz), so it needs
last - fuzz < target.size(). Rejecting only > lets the exactly-one-past
case through, and List.get throws:

    chunk covering target indices 7..10, target of length 10
    -> IndexOutOfBoundsException: Index 10 out of bounds for length 10

applyTo and DiffUtils.patch document PatchFailedException for a patch
that cannot be applied, and the neighbouring case (target two short)
already returns POSITION_OUT_OF_TARGET, so a target one short should
too.

The first disjunct stays >: an InsertDelta appending at end of file has
an empty source chunk with position == target.size(), and >= there would
reject a valid append. An empty chunk has last == position - 1, so the
second disjunct is already correct for it.

No test referenced POSITION_OUT_OF_TARGET; ChunkTest covered OK and
CONTENT_DOES_NOT_MATCH_TARGET only.
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.

1 participant