From eb6ff372722339cf5623deb819ac8f0b7c39aa64 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 25 Jul 2026 21:38:53 +0200 Subject: [PATCH 1/2] locator: keep a hunk with no context inside the file A hunk with no surrounding context is placed wherever its range claims it belongs, which may be beyond the end of the file, or before the start of it once an offset from an earlier hunk is taken into account. Applying such a patch read the input lines out of range, and the resulting std::out_of_range escaped as the diagnostic "**** vector". Clamp the guess into the file, so that a location is always somewhere the applier can write. --- src/locator.cpp | 3 ++- tests/test_applier.cpp | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/locator.cpp b/src/locator.cpp index 95c1884..752ab22 100644 --- a/src/locator.cpp +++ b/src/locator.cpp @@ -105,7 +105,8 @@ Location locate_hunk(const std::vector& content, const Hunk& hunk, bool ig if (hunk.old_file_range.number_of_lines == 0) { if (hunk.old_file_range.start_line == 0 && !content.empty()) return {}; - return { offset_guess, 0, 0 }; + const auto end = static_cast(content.size()); + return { std::min(std::max(offset_guess, 0), end), 0, 0 }; } LineNumber patch_prefix_content = 0; diff --git a/tests/test_applier.cpp b/tests/test_applier.cpp index 997c14e..cc5b0df 100644 --- a/tests/test_applier.cpp +++ b/tests/test_applier.cpp @@ -90,6 +90,35 @@ int another() EXPECT_FILE_EQ("a.rej", patch); } +static void write_three_line_file() +{ + Patch::File file("a", std::ios_base::out); + file << "a\nb\nc\n"; + file.close(); +} + +COMPAT_TEST(applier_insertion_past_end_of_file) +{ + { + Patch::File file("diff.patch", std::ios_base::out); + file << R"(--- a ++++ a +@@ -100,0 +100,1 @@ ++inserted +)"; + file.close(); + } + + write_three_line_file(); + + Process process(patch_path, { patch_path, "--force", "-i", "diff.patch", nullptr }); + + EXPECT_EQ(process.stdout_data(), "patching file a\n"); + EXPECT_EQ(process.stderr_data(), ""); + EXPECT_EQ(process.return_code(), 0); + EXPECT_FILE_EQ("a", "a\nb\nc\ninserted\n"); +} + COMPAT_TEST(applier_end_of_file_hunk_requires_fuzz_when_no_longer_at_end) { const std::string patch = R"(--- a From 92af188ffd7540da81d7678c2074d72cc8d3ccaa Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 25 Jul 2026 21:39:09 +0200 Subject: [PATCH 2/2] applier: do not read past the end of the file being patched Fuzz can match a hunk whose trailing context runs past the end of the file, since that context is dropped rather than compared. Those lines were still written out from the input, reading it out of range, and the resulting std::out_of_range escaped as the diagnostic "**** vector". Skip context and removals which have no line left to consume. --- src/applier.cpp | 12 +++++++++- tests/test_applier.cpp | 53 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/applier.cpp b/src/applier.cpp index 3262d4f..decc1dd 100644 --- a/src/applier.cpp +++ b/src/applier.cpp @@ -88,6 +88,9 @@ static LineNumber write_define_hunk(LineWriter& output, const Hunk& hunk, const for (const auto& patch_line : hunk.lines) { if (patch_line.operation == ' ') { + if (line_number >= lines.size()) + continue; + const auto& line = lines.at(line_number); ++line_number; if (define_state != DefineState::Outside) { @@ -105,6 +108,9 @@ static LineNumber write_define_hunk(LineWriter& output, const Hunk& hunk, const } output << patch_line.line; } else if (patch_line.operation == '-') { + if (line_number >= lines.size()) + continue; + const auto& line = lines.at(line_number); ++line_number; @@ -136,12 +142,16 @@ static LineNumber write_hunk(LineWriter& output, const Hunk& hunk, const Locatio for (const auto& patch_line : hunk.lines) { if (patch_line.operation == ' ') { + if (line_number >= lines.size()) + continue; + output << lines.at(line_number); ++line_number; } else if (patch_line.operation == '+') { output << patch_line.line; } else if (patch_line.operation == '-') { - ++line_number; + if (line_number < lines.size()) + ++line_number; } } diff --git a/tests/test_applier.cpp b/tests/test_applier.cpp index cc5b0df..b6b79e3 100644 --- a/tests/test_applier.cpp +++ b/tests/test_applier.cpp @@ -90,6 +90,22 @@ int another() EXPECT_FILE_EQ("a.rej", patch); } +static void write_context_past_end_of_file_patch() +{ + Patch::File file("diff.patch", std::ios_base::out); + file << R"(--- a ++++ a +@@ -1,5 +1,5 @@ + a +-b ++B + c + ZZZ + QQQ +)"; + file.close(); +} + static void write_three_line_file() { Patch::File file("a", std::ios_base::out); @@ -119,6 +135,43 @@ COMPAT_TEST(applier_insertion_past_end_of_file) EXPECT_FILE_EQ("a", "a\nb\nc\ninserted\n"); } +COMPAT_TEST(applier_fuzz_matches_context_past_end_of_file) +{ + write_context_past_end_of_file_patch(); + write_three_line_file(); + + Process process(patch_path, { patch_path, "--force", "--fuzz=3", "-i", "diff.patch", nullptr }); + + EXPECT_EQ(process.stdout_data(), R"(patching file a +Hunk #1 succeeded at 1 with fuzz 2. +)"); + EXPECT_EQ(process.stderr_data(), ""); + EXPECT_EQ(process.return_code(), 0); + EXPECT_FILE_EQ("a", "a\nB\nc\n"); +} + +COMPAT_TEST(applier_fuzz_matches_context_past_end_of_file_with_define) +{ + write_context_past_end_of_file_patch(); + write_three_line_file(); + + Process process(patch_path, { patch_path, "--force", "--fuzz=3", "-D", "FOO", "-i", "diff.patch", nullptr }); + + EXPECT_EQ(process.stdout_data(), R"(patching file a +Hunk #1 succeeded at 1 with fuzz 2. +)"); + EXPECT_EQ(process.stderr_data(), ""); + EXPECT_EQ(process.return_code(), 0); + EXPECT_FILE_EQ("a", R"(a +#ifndef FOO +b +#else +B +#endif +c +)"); +} + COMPAT_TEST(applier_end_of_file_hunk_requires_fuzz_when_no_longer_at_end) { const std::string patch = R"(--- a