Repository navigation
Conversation
GitHeader::parse extracts header values (file modes, rename/copy
paths, the diff --git line) via strip_line_ending, which stripped
only the trailing \n. In a patch file saved with CRLF line endings
(e.g. on Windows) every extracted value therefore kept its trailing
\r, and parsing a mode header failed outright:
error parsing patches at byte 0: invalid file mode: 100644\r
Strip a trailing \r as well, mirroring how GNU patch strips trailing
CRs from CRLF patches (the behavior the TODO on strip_line_ending
already pointed at). This only affects git extended header lines;
hunk content is handled by the patch parser itself.
Downstream report: pnpm/pnpm#16641, where a CRLF patch file that
creates a new file fails with ERR_PNPM_INVALID_PATCH on pnpm 12
(pnpm parses patches with diffy's PatchSet::parse).
Tests: crlf_new_file_with_content and crlf_mode_only_change fail on
the unpatched parser with InvalidFileMode("100644\r") and pass with
this change; the full suite (131 lib tests) passes.
1 task
4 tasks done
zkochan
pushed a commit
to tunglambk/pnpm
that referenced
this pull request
Oct 8, 2026
`diffy`'s `GitHeader::parse` strips only the trailing `\n` from a git extended header, so on a patch file saved with CRLF line endings every value it extracts keeps its `\r`: a file mode fails the exact match, and a rename, copy, or `diff --git` fallback path comes out with an invalid character. `PatchSet::parse` is what `apply_patch_to_dir` uses, so a CRLF patch that creates, deletes, or changes the mode of a file failed with `ERR_PNPM_INVALID_PATCH` (pnpm#16641). Strip the `\r` from those header lines before parsing. Hunk lines are left alone: a `\r` there is content, and an LF patch for a CRLF file depends on it being preserved. bmwill/diffy#89 fixes the same defect upstream and is still open with no release past 0.5.2, so the tolerance lives in the patching crate's pre-parse layer for now, next to the existing no-newline-marker tolerance. pnpm 11 is not affected: `@pnpm/patch-package` trims every header value it extracts with `.trim()`.
zkochan
added a commit
to pnpm/pnpm
that referenced
this pull request
Oct 8, 2026
`diffy`'s `GitHeader::parse` strips only the trailing `\n` from a git extended header, so on a patch file saved with CRLF line endings every value it extracts keeps its `\r`: a file mode fails the exact match, and a rename, copy, or `diff --git` fallback path comes out with an invalid character. `PatchSet::parse` is what `apply_patch_to_dir` uses, so a CRLF patch that creates, deletes, or changes the mode of a file failed with `ERR_PNPM_INVALID_PATCH` (#16641). Strip the `\r` from those header lines before parsing. Hunk lines are left alone: a `\r` there is content, and an LF patch for a CRLF file depends on it being preserved. bmwill/diffy#89 fixes the same defect upstream and is still open with no release past 0.5.2, so the tolerance lives in the patching crate's pre-parse layer for now, next to the existing no-newline-marker tolerance. pnpm 11 is not affected: `@pnpm/patch-package` trims every header value it extracts with `.trim()`. close #16641 --------- Co-authored-by: Zoltan Kochan <z@kochan.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
bmwill
pushed a commit
that referenced
this pull request
Oct 8, 2026
Previously, only the `---`/`+++` headers accepted CRLF line endings (#87). The git extended headers kept the `\r` in their values, so in a CRLF patch mode lines failed with `InvalidFileMode("100644\r")`, renames and copies failed with `InvalidCharInUnquotedFilename`, and mode-only changes failed with `InvalidDiffGitPath`. A `git format-patch` email with CRLF line endings also kept its commit message, because the separator was matched as `\n---\n`, so a message line starting with `diff --git` began a bogus patch. The same bug was reported in #89. With this commit, `Text::strip_line_ending` strips either `\n` or `\r\n`, and every parser that reads patch syntax uses it: the git extended headers, the `---`/`+++` filenames, the binary patch parser (which had two hand-written copies), and the email separator. A `\r` that no `\n` follows is not a line ending, as in #87. Hunk lines are unchanged: they keep their `\r` as content, which matches `git apply`, and the docs for `Line` and `PatchSet` now say so. GNU patch instead strips the `\r` from every line of a CRLF patch. Also make the gitdiff test helper check that the CRLF form of each input parses the same as the LF form, and add tests for format-patch emails, hunk content, the bytes API, and a lone `\r`.
Owner
|
Thanks for the report and bug fix! I took the opportunity to look at CRLF handling in a few other places (and fix some other bugs that I found) and ended up fixing the bug in a slightly different way in #90. Thanks again for helping make diffy better, I really appreciate it! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
PatchSet::parsewithParseOptions::gitdiff()fails on patch files that use CRLF line endings whenever a git extended header carries a value.GitHeader::parseextracts header values throughstrip_line_ending, which stripped only the trailing\n, so in a CRLF patch every extracted value keeps its\r— and a file mode then fails the exact match inFileMode::from_str:(the
\ris invisible in most terminals, which makes the message read as if the perfectly valid100644were rejected).Reproduced against diffy 0.5.2 with the new-file patch from the downstream report — LF parses fine, the identical bytes with CRLF fail:
The fix
strip_line_endingnow also strips a trailing\r. This is the behavior the function's TODO already pointed at (GNU patch strips trailing CRs from CRLF patches), and it is scoped to git extended header lines only — hunk content lines are handled by the patch parser itself, so a patch whose content legitimately ends lines with\ris unaffected beyond the header metadata. Rename/copy paths and thediff --gitfallback line get the same treatment since they flow through the same helper.This complements #87, which fixed CRLF handling for the
---/+++headers in the classicPatchparser; thepatch_setgit-header path had the same class of bug in its mode/rename/copy extraction.Downstream report
pnpm 12 parses patch files with
PatchSet::parse(pnpm/crates/patching), and pnpm/pnpm#16641 reportsERR_PNPM_INVALID_PATCH … invalid file mode: 100644for a CRLF patch file that creates a new file — a regression from pnpm 11, whose JS parser accepted it. Fixing it here restores that for everyPatchSetconsumer.Verification
crlf_new_file_with_contentandcrlf_mode_only_changeinpatch_set::tests: both fail unpatched (InvalidFileMode("100644\r")), pass patched, and the parsed operations/modes match the LF equivalents exactly.cargo test --all-features— 131 lib tests pass (129 before + the 2 new), plus integration and doc tests;cargo fmt --checkclean.No bounty is attached to this — offered freely. Tips welcome: PayPal kyleblake0659@gmail.com · BTC 3GnR7TWBXAB3pPztBWpNF4LMNEX5yX8vZK · ETH 0xA1d3CEB7bD707847c3c6aB59d30D385FE8DD85Fc · SOL DLg1ua1ufewQ81J7dQomwWhXzhZreq4jEwTaQJR31Exb