Skip to content

fix: don't treat a literal CR at EOF as a Windows line ending - #701

Merged
ExplodingCabbage merged 11 commits into
kpdecker:masterfrom
maximilliangrand:fix/autoconvert-literal-cr-at-eof
Aug 18, 2026
Merged

fix: don't treat a literal CR at EOF as a Windows line ending#701
ExplodingCabbage merged 11 commits into
kpdecker:masterfrom
maximilliangrand:fix/autoconvert-literal-cr-at-eof

Conversation

@maximilliangrand

Copy link
Copy Markdown
Contributor

Bug

applyPatch corrupts output when a Unix source is patched with a patch whose final line's content ends in a literal carriage return and has no trailing newline. The \r is silently dropped.

import{structuredPatch,applyPatch}from'diff';constoldFile='line1\nline2\n';constnewFile='line1\nline2\nline3\r';constpatch=structuredPatch('f','f',oldFile,newFile,undefined,undefined,{context: 0});applyPatch(oldFile,patch);// => 'line1\nline2\nline3' (the '\r' is lost)

I found this by fuzzing random source/target pairs through structuredPatch -> applyPatch; 80 of 200k pairs failed to round-trip, all of this shape.

Cause

isWin and winToUnix treat any patch line ending in \r as a Windows CRLF line ending. But a line ending in \r that is immediately followed by a \ No newline at end of file marker is not a CRLF ending (Windows endings are \r\n) - it's a literal carriage return in the final line's content. A genuine Windows no-newline-at-EOF line never ends in \r.

So isWin misclassifies such a patch as Windows, and when applyPatch (with autoConvertLineEndings on) converts it to match the Unix source, winToUnix strips the \r.

unixToWin already guards this exact case (hunk.lines[i + 1]?.startsWith('\\')), so it won't add a \r to a no-newline final line. This change adds the symmetric guard to isWin and winToUnix.

Fix

Ignore a trailing \r that sits immediately before a \ No newline at end of file marker when deciding whether a patch is Windows / when stripping CRs.

Added tests: a winToUnix unit test, an isWin unit test, and an applyPatch round-trip regression test. Full suite passes with 100% coverage.

`isWin` and `winToUnix` treated any patch line ending in `\r` as a Windows
line ending. But a line whose content ends in `\r` and is immediately
followed by a `\ No newline at end of file` marker is not a CRLF ending
(those are `\r\n`) - it's a literal carriage return in the final line's
content. A genuine Windows no-newline-at-EOF line never ends in `\r`.
Because of this, `isWin` misclassified such patches as Windows, and when
`applyPatch` auto-converted them to match a Unix source, `winToUnix`
stripped the `\r`, silently corrupting the output. For example
`applyPatch('line1\nline2\n', structuredPatch('f','f','line1\nline2\n',
'line1\nline2\nline3\r', undefined, undefined, {context: 0}))` returned
`line1\nline2\nline3` instead of `line1\nline2\nline3\r`.
`unixToWin` already guards against this case (it won't add `\r` to a
no-newline final line); this makes `isWin` and `winToUnix` symmetric.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ExplodingCabbage

Copy link
Copy Markdown
Collaborator

when a Unix source is patched with a patch whose final line's content ends in a literal carriage return and has no trailing newline

It's more specific than that, no? Roughly, the patch also needs to not feature any other lines, in order for isWin to return true - i.e. it needs to be a single-line patch. Still a legit bug, though!

@ExplodingCabbage
ExplodingCabbage requested a balanced review from CopilotAugust 17, 2026 12:35

@ExplodingCabbageExplodingCabbage left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, thank you! Will just see if Copilot offers any useful input before I merge.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes applyPatch preserving literal carriage returns at EOF during line-ending conversion.

Changes:

  • Guards Windows detection/conversion around no-newline markers.
  • Adds unit and regression coverage.
  • Documents the fix in release notes.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
src/patch/line-endings.tsPreserves literal EOF carriage returns.
test/patch/line-endings.jsTests conversion and detection behavior.
test/patch/apply.jsAdds an applyPatch regression test.
release-notes.mdDocuments the bug fix.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// A trailing '\r' before a "\ No newline at end of file" marker is a literal carriage
// return in the final line's content, not a Windows line ending, so it isn't evidence that
// the patch uses Windows line endings.
(line, i) => line.endsWith('\r') && !hunk.lines[i + 1]?.startsWith('\\')
CopilotAIand others added 6 commits August 17, 2026 14:06
Co-authored-by: ExplodingCabbage <2358339+ExplodingCabbage@users.noreply.github.com>
…n CRLF source; update release notes
Co-authored-by: ExplodingCabbage <2358339+ExplodingCabbage@users.noreply.github.com>
…on' into fix/autoconvert-literal-cr-at-eof
Conflicts:
release-notes.md
@ExplodingCabbage
ExplodingCabbage merged commit 2d923ce into kpdecker:masterAug 18, 2026
Sign up for freeto 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.

4 participants

@maximilliangrand@ExplodingCabbage