From 00fd76100d6683d98398f97277f02c13323d354f Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sun, 5 Apr 2026 13:58:36 +0200 Subject: [PATCH 1/2] file: handle EOF explicitly in File::peek fgetc() returns an int so it can represent every byte value plus EOF. Casting that result directly to char loses the EOF sentinel and can produce an ordinary byte value instead, depending on the platform's char signedness. --- src/file.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/file.cpp b/src/file.cpp index 1127118..41616c5 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -164,7 +164,7 @@ char File::peek() int c = fgetc(m_file); ungetc(c, m_file); - return static_cast(c); + return c == EOF ? '\0' : static_cast(c); } bool File::get_line(std::string& line, NewLine* newline) From 5e8808bd7db80cd260f5809f56bdebb313e64dfe Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sun, 5 Apr 2026 13:21:06 +0200 Subject: [PATCH 2/2] parser: avoid deleting files for unified diffs that only empty them A unified diff that removes all lines from a file uses a new file range of 0,0, but that does not mean the file should be deleted. We were inferring Operation::Delete from that hunk header alone, so applying such a patch removed the target file entirely instead of leaving it as an empty file. Match traditional patch behavior by only inferring add/delete from /dev/null headers when file paths are present, and keep the old hunk-range fallback for headerless cases. --- src/parser.cpp | 10 ++++++++-- tests/test_basic.cpp | 27 +++++++++++++++++++++++++++ tests/test_parser.cpp | 21 +++++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/parser.cpp b/src/parser.cpp index cc6291b..d606178 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -631,10 +631,16 @@ bool Parser::parse_patch_header(Patch& patch, PatchHeaderInfo& header_info, int } if (patch.operation == Operation::Change) { - if (hunk.new_file_range.start_line == 0) + if (patch.new_file_path == "/dev/null") { patch.operation = Operation::Delete; - else if (hunk.old_file_range.start_line == 0) + } else if (patch.old_file_path == "/dev/null") { patch.operation = Operation::Add; + } else if (patch.old_file_path.empty() && patch.new_file_path.empty()) { + if (hunk.new_file_range.start_line == 0) + patch.operation = Operation::Delete; + else if (hunk.old_file_range.start_line == 0) + patch.operation = Operation::Add; + } } return should_parse_body; diff --git a/tests/test_basic.cpp b/tests/test_basic.cpp index 4002c38..4191aa9 100644 --- a/tests/test_basic.cpp +++ b/tests/test_basic.cpp @@ -769,6 +769,33 @@ PATCH_TEST(remove_file_successfully_posix_and_remove_flag) remove_file(patch_path, true, { "--posix", "--remove-empty-files" }); } +PATCH_TEST(unified_patch_that_empties_file_keeps_empty_file) +{ + { + Patch::File file("diff.patch", std::ios_base::out); + file << R"(--- old.txt 2026-04-05 13:10:37 ++++ new.txt 2026-04-05 13:10:37 +@@ -1,2 +0,0 @@ +- +-n[@CuBYRF+w.Ul.jw]M)e=XYaAc +)"; + } + + { + Patch::File file("target.txt", std::ios_base::out); + file << "\n" + "n[@CuBYRF+w.Ul.jw]M)e=XYaAc\n"; + } + + Process process(patch_path, { patch_path, "-i", "diff.patch", "target.txt", nullptr }); + + EXPECT_EQ(process.stdout_data(), "patching file target.txt\n"); + EXPECT_EQ(process.stderr_data(), ""); + EXPECT_EQ(process.return_code(), 0); + EXPECT_TRUE(Patch::filesystem::exists("target.txt")); + EXPECT_FILE_EQ("target.txt", ""); +} + PATCH_TEST(git_patch_remove_file) { { diff --git a/tests/test_parser.cpp b/tests/test_parser.cpp index 839f7cf..383b21d 100644 --- a/tests/test_parser.cpp +++ b/tests/test_parser.cpp @@ -805,3 +805,24 @@ TEST(parser_malformed_range_line_fails) "+1\n"); EXPECT_THROW(Patch::parse_patch(patch_file), std::runtime_error); } + +TEST(parser_unified_patch_that_empties_file_is_not_delete) +{ + Patch::File patch_file = Patch::File::create_temporary_with_content(R"( +--- old.txt 2026-04-05 13:10:37 ++++ new.txt 2026-04-05 13:10:37 +@@ -1,2 +0,0 @@ +- +-n[@CuBYRF+w.Ul.jw]M)e=XYaAc +)"); + + auto patch = Patch::parse_patch(patch_file); + EXPECT_EQ(patch.operation, Patch::Operation::Change); + EXPECT_EQ(patch.old_file_path, "old.txt"); + EXPECT_EQ(patch.new_file_path, "new.txt"); + EXPECT_EQ(patch.hunks.size(), 1); + EXPECT_EQ(patch.hunks[0].old_file_range.start_line, 1); + EXPECT_EQ(patch.hunks[0].old_file_range.number_of_lines, 2); + EXPECT_EQ(patch.hunks[0].new_file_range.start_line, 0); + EXPECT_EQ(patch.hunks[0].new_file_range.number_of_lines, 0); +}