From 57b9a8b2df1ca81be8b38d0eb15d655186ba1b6c Mon Sep 17 00:00:00 2001 From: Sasha Mitchell Date: Mon, 12 Oct 2026 00:50:36 +0700 Subject: [PATCH] fix: swap GIT binary patch sections when reversing git apply uses the first encoded section as the forward result, so leaving the sections in the original order makes the reversed patch fail. --- diff/reverse.go | 74 ++++++++++++++++++++++++++++++++++++++++ diff/reverse_git_test.go | 33 +++++++++++++----- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/diff/reverse.go b/diff/reverse.go index 0c4d1b6..173a9e2 100644 --- a/diff/reverse.go +++ b/diff/reverse.go @@ -59,9 +59,83 @@ func reverseExtendedHeaders(headers []string, origName, newName string) ([]strin } swapHeaderValues(reversed, parsed, gitModeHeaderPair) swapHeaderValues(reversed, parsed, gitRenameHeaderPair) + reverseBinaryPatchSections(reversed) return reversed, nil } +// reverseBinaryPatchSections swaps the two encoded sections of a GIT binary +// patch. git apply uses the first section as the forward result and the +// second as the reverse result. +func reverseBinaryPatchSections(headers []string) { + start := -1 + for i, header := range headers { + if strings.TrimRight(header, "\r") == gitExtendedHeaderBinaryPatch { + start = i + break + } + } + if start < 0 || start+1 >= len(headers) { + return + } + first, rest, ok := takeBinarySection(headers[start+1:]) + if !ok { + return + } + second, tail, ok := takeBinarySection(rest) + if !ok { + return + } + swapped := make([]string, 0, len(headers)) + swapped = append(swapped, headers[:start+1]...) + swapped = append(swapped, second...) + swapped = append(swapped, first...) + swapped = append(swapped, tail...) + copy(headers, swapped) +} + +func takeBinarySection(lines []string) (section, rest []string, ok bool) { + if len(lines) == 0 || !isBinarySectionHeader(lines[0]) { + return nil, lines, false + } + end := 1 + for end < len(lines) { + if isBinarySectionHeader(lines[end]) { + break + } + end++ + if isBlankHeader(lines[end-1]) { + break + } + } + return lines[:end], lines[end:], true +} + +func isBinarySectionHeader(line string) bool { + line = strings.TrimRight(line, "\r") + var n string + switch { + case strings.HasPrefix(line, "literal "): + n = strings.TrimPrefix(line, "literal ") + case strings.HasPrefix(line, "delta "): + n = strings.TrimPrefix(line, "delta ") + default: + return false + } + if n == "" { + return false + } + for i := 0; i < len(n); i++ { + if n[i] < '0' || n[i] > '9' { + return false + } + } + return true +} + +func isBlankHeader(line string) bool { + return strings.TrimRight(line, "\r") == "" +} + // reverseDiffGitHeader swaps the two path arguments while preserving their // original quoting. The parsed filenames are used only to reject malformed or // ambiguous input; names recovered from other headers can disambiguate Git's diff --git a/diff/reverse_git_test.go b/diff/reverse_git_test.go index b4ba2ff..5365db2 100644 --- a/diff/reverse_git_test.go +++ b/diff/reverse_git_test.go @@ -114,8 +114,8 @@ func TestReverseFileDiffGitSemantics(t *testing.T) { } } -// Git binary patches contain both directions. Reversing the index hashes makes -// git apply select the inverse payload without reordering the encoded sections. +// git apply uses the first encoded section as the forward result and the +// second as the reverse result. Reversing a patch swaps those sections. func TestReverseFileDiffGitBinarySemantics(t *testing.T) { tests := []struct { name string @@ -134,10 +134,10 @@ func TestReverseFileDiffGitBinarySemantics(t *testing.T) { reverse: "diff --git b/literal.bin a/literal.bin\n" + "index e0d84f457d2b5c802ecd286e7e576365d94af3b9..acdc746399905e5ea0d9d4163d2b3c4d04b05123 100644\n" + "GIT binary patch\n" + - "literal 8\n" + - "PcmZQzWMXDvWn%{b09*ha\n\n" + "literal 64\n" + - "LcmZ>CqznK65PlFr\n\n", + "LcmZ>CqznK65PlFr\n\n" + + "literal 8\n" + + "PcmZQzWMXDvWn%{b09*ha\n\n", }, { name: "delta", @@ -151,10 +151,27 @@ func TestReverseFileDiffGitBinarySemantics(t *testing.T) { reverse: "diff --git b/delta.bin a/delta.bin\n" + "index b762194ffce13918858bfd54f1c4ded144c7e3b4..df437f42c808d41dec5d543d60ce94c8cb8a044a 100644\n" + "GIT binary patch\n" + - "delta 20\n" + - "bcmZorXi(U2ft@2cBQY;MHAQjb4Gj(ePVNVE\n\n" + "delta 10\n" + - "PcmZorXi!+h!2v`75gY=g\n\n", + "PcmZorXi!+h!2v`75gY=g\n\n" + + "delta 20\n" + + "bcmZorXi(U2ft@2cBQY;MHAQjb4Gj(ePVNVE\n\n", + }, + { + name: "reported lengths", + forward: "diff --git a/bin.dat b/bin.dat\n" + + "index bdc955b7b2e610ad5a72302b139a2e6cb325519a..eaf36c1daccfdf325514461cd1a2ffbc139b5464 100644\n" + + "GIT binary patch\n" + + "literal 4\n" + + "LcmZQzWMT#Y01f~L\n\n" + + "literal 2\n" + + "JcmZQz1ONa700IC2\n\n", + reverse: "diff --git b/bin.dat a/bin.dat\n" + + "index eaf36c1daccfdf325514461cd1a2ffbc139b5464..bdc955b7b2e610ad5a72302b139a2e6cb325519a 100644\n" + + "GIT binary patch\n" + + "literal 2\n" + + "JcmZQz1ONa700IC2\n\n" + + "literal 4\n" + + "LcmZQzWMT#Y01f~L\n\n", }, }