From c3842a6aa27f9e4417959beb604382469bd50a15 Mon Sep 17 00:00:00 2001
From: Dan Fandrich <dan@coneharvesters.com>
Date: Fri, 3 Feb 2023 17:14:31 -0800
Subject: [PATCH] Add the --max-offset option

This will reject chunks that require more than the given number of lines
of offset in order to be applied. Using a value of 0 combined with
--fuzz=0 reduces the chance of patching a file that was modified after
the patch was generated.

Fixes: https://savannah.gnu.org/bugs/?34031
Signed-off-by: Dan Fandrich <dan@coneharvesters.com>
---
 patch.man         |  7 +++++
 src/patch.c       |  7 +++++
 tests/Makefile.am |  1 +
 tests/max-offset  | 74 +++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 89 insertions(+)
 create mode 100644 tests/max-offset

diff --git a/patch.man b/patch.man
index e919eae..aa9fbf1 100644
--- a/patch.man
+++ b/patch.man
@@ -413,6 +413,13 @@ in the original file, and sequences of blanks at the ends of lines are ignored.
 Normal characters must still match exactly.
 Each line of the context must still match a line in the original file.
 .TP
+\fB\*=max-offset=\fP\fInum\fP
+Set the maximum number of offset lines.
+This option only applies to diffs that have context, and causes
+.B patch
+to reject patches that require an offset more than the given number of lines.
+A negative value means any offset is allowed.
+.TP
 \fB\*=merge\fP or \fB\*=merge=merge\fP or \fB\*=merge=diff3\fP
 Merge a patch file into the original files similar to \fBdiff3\fP(1) or
 \fBmerge\fP(1).  If a conflict is found, \fBpatch\fP outputs a warning and
diff --git a/src/patch.c b/src/patch.c
index d5f456c..63154df 100644
--- a/src/patch.c
+++ b/src/patch.c
@@ -105,6 +105,7 @@ static char const * TMPREJNAME;
 static bool TMPREJNAME_needs_removal;
 
 static lin maxfuzz = 2;
+static lin maxoffset = -1;
 
 static char serrbuf[BUFSIZ];
 
@@ -483,6 +484,7 @@ main (int argc, char **argv)
 		    && ((where == 1 && pch_says_nonexistent (reverse) == 2
 			 && instat.st_size)
 			|| ! where
+			|| (maxoffset >= 0 && in_offset > maxoffset)
 			|| ! apply_hunk (&outstate, where))))
 	      {
 		if (! skip_reject_file)
@@ -787,6 +789,7 @@ static struct option const longopts[] =
   {"reject-format", required_argument, NULL, CHAR_MAX + 9},
   {"read-only", required_argument, NULL, CHAR_MAX + 10},
   {"follow-symlinks", no_argument, NULL, CHAR_MAX + 11},
+  {"max-offset", required_argument, NULL, CHAR_MAX + 12},
   {NULL, no_argument, NULL, 0}
 };
 
@@ -796,6 +799,7 @@ static char const *const option_help[] =
 "",
 "  -p NUM  --strip=NUM  Strip NUM leading components from file names.",
 "  -F LINES  --fuzz LINES  Set the fuzz factor to LINES for inexact matching.",
+"  --max-offset LINES Reject patches needing at offset more than LINES.",
 "  -l  --ignore-whitespace  Ignore white space changes between patch and input.",
 "",
 "  -c  --context  Interpret the patch as a context difference.",
@@ -1081,6 +1085,9 @@ get_some_switches (void)
 	    case CHAR_MAX + 11:
 		follow_symlinks = true;
 		break;
+	    case CHAR_MAX + 12:
+		maxoffset = numeric_string (optarg, false, "max offset");
+		break;
 	    default:
 		usage (stderr, 2);
 	}
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 16f8693..7e72ebc 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -45,6 +45,7 @@ TESTS = \
 	global-reject-files \
 	inname \
 	line-numbers \
+	max-offset \
 	merge \
 	mangled-numbers-abort \
 	mixed-patch-types \
diff --git a/tests/max-offset b/tests/max-offset
new file mode 100644
index 0000000..71602e9
--- /dev/null
+++ b/tests/max-offset
@@ -0,0 +1,74 @@
+# Copyright (C) 2023 Free Software Foundation, Inc.
+#
+# Copying and distribution of this file, with or without modification,
+# in any medium, are permitted without royalty provided the copyright
+# notice and this notice are preserved.
+
+# Test the --max-offset=LINES option
+
+. $srcdir/test-lib.sh
+
+require cat
+require sed
+require gnu_diff
+use_local_patch
+use_tmpdir
+
+# ==============================================================
+
+cat > a.diff <<EOF
+--- a
++++ a
+@@ -1,4 +1,5 @@
+ 1
+ 2
++2a
+ 3
+ 4
+EOF
+
+seq 0 5 > a
+
+check 'patch --max-offset 1 < a.diff' <<EOF
+patching file a
+Hunk #1 succeeded at 2 (offset 1 line).
+EOF
+
+seq 0 5 > a
+
+check 'patch --max-offset 0 < a.diff || echo "Status: $?"' <<EOF
+patching file a
+Hunk #1 FAILED at 2.
+1 out of 1 hunk FAILED -- saving rejects to file a.rej
+Status: 1
+EOF
+
+# ==============================================================
+
+# Two hunks, only one of which requires an offset
+
+cat > a.diff <<EOF
+--- a
++++ b
+@@ -1,4 +1,5 @@
+ 1
+ 2
++2a
+ 3
+ 4
+@@ -6,4 +7,5 @@
+ 7
+ 8
++8a
+ 9
+ 10
+EOF
+
+seq 1 10 > a
+
+check 'patch --max-offset 0 < a.diff || echo "Status: $?"' <<EOF
+patching file a
+Hunk #2 FAILED at 8.
+1 out of 2 hunks FAILED -- saving rejects to file a.rej
+Status: 1
+EOF
-- 
2.30.6

