aboutsummaryrefslogtreecommitdiff
path: root/usr.bin
diff options
context:
space:
mode:
authorKyle Evans <kevans@FreeBSD.org>2019-11-04 03:07:01 +0000
committerKyle Evans <kevans@FreeBSD.org>2019-11-04 03:07:01 +0000
commit50dacbf6c2a3d32f7e3f20ca98cda23f38817e7f (patch)
tree437dcfd9f3bec678f1401ffc4a37a774a403f349 /usr.bin
parent69fac7c3af9903ba2e6bedd29a969667b0367133 (diff)
downloadsrc-50dacbf6c2a3d32f7e3f20ca98cda23f38817e7f.tar.gz
src-50dacbf6c2a3d32f7e3f20ca98cda23f38817e7f.zip
patch(1): give /dev/null patches special treatment
We have a bad habit of duplicating contents of files that are sourced from /dev/null and applied more than once... take the more sane (in most ways) GNU route and complain if the file exists and offer reversal options. This still falls short a little bit as selecting "don't reverse, apply anyway" will still give you duplicated file contents. There's probably other issues as well, but awareness is the first step to happiness. MFC after: 2 weeks Differential Revision: https://reviews.freebsd.org/D21535
Notes
Notes: svn path=/head/; revision=354328
Diffstat (limited to 'usr.bin')
-rw-r--r--usr.bin/patch/patch.18
-rw-r--r--usr.bin/patch/patch.c142
-rw-r--r--usr.bin/patch/pch.c27
-rw-r--r--usr.bin/patch/pch.h2
-rwxr-xr-xusr.bin/patch/tests/unified_patch_test.sh17
-rw-r--r--usr.bin/patch/util.c4
6 files changed, 184 insertions, 16 deletions
diff --git a/usr.bin/patch/patch.1 b/usr.bin/patch/patch.1
index e8478fc0438e..3ad5287dee36 100644
--- a/usr.bin/patch/patch.1
+++ b/usr.bin/patch/patch.1
@@ -21,7 +21,7 @@
.\"
.\" $OpenBSD: patch.1,v 1.27 2014/04/15 06:26:54 jmc Exp $
.\" $FreeBSD$
-.Dd August 15, 2015
+.Dd November 3, 2019
.Dt PATCH 1
.Os
.Sh NAME
@@ -559,8 +559,10 @@ option as needed.
.Pp
Third, you can create a file by sending out a diff that compares a
null file to the file you want to create.
-This will only work if the file you want to create does not exist already in
-the target directory.
+If the file you want to create already exists in the target directory when the
+diff is applied, then
+.Nm
+will identify the patch as potentially reversed and offer to reverse the patch.
.Pp
Fourth, take care not to send out reversed patches, since it makes people wonder
whether they already applied the patch.
diff --git a/usr.bin/patch/patch.c b/usr.bin/patch/patch.c
index 8342dba688bc..a23fc82d3d90 100644
--- a/usr.bin/patch/patch.c
+++ b/usr.bin/patch/patch.c
@@ -31,6 +31,7 @@
#include <sys/types.h>
#include <sys/stat.h>
+#include <assert.h>
#include <ctype.h>
#include <getopt.h>
#include <limits.h>
@@ -103,6 +104,7 @@ static void dump_line(LINENUM, bool);
static bool patch_match(LINENUM, LINENUM, LINENUM);
static bool similar(const char *, const char *, int);
static void usage(void);
+static bool handle_creation(bool, bool *);
/* true if -E was specified on command line. */
static bool remove_empty_files = false;
@@ -147,8 +149,10 @@ static char end_defined[128];
int
main(int argc, char *argv[])
{
+ struct stat statbuf;
int error = 0, hunk, failed, i, fd;
- bool patch_seen, reverse_seen;
+ bool out_creating, out_existed, patch_seen, remove_file;
+ bool reverse_seen;
LINENUM where = 0, newwhere, fuzz, mymaxfuzz;
const char *tmpdir;
char *v;
@@ -219,6 +223,12 @@ main(int argc, char *argv[])
reinitialize_almost_everything()) {
/* for each patch in patch file */
+ if (source_file != NULL && (diff_type == CONTEXT_DIFF ||
+ diff_type == NEW_CONTEXT_DIFF ||
+ diff_type == UNI_DIFF))
+ out_creating = strcmp(source_file, _PATH_DEVNULL) == 0;
+ else
+ out_creating = false;
patch_seen = true;
warn_on_invalid_line = true;
@@ -226,6 +236,19 @@ main(int argc, char *argv[])
if (outname == NULL)
outname = xstrdup(filearg[0]);
+ /*
+ * At this point, we know if we're supposed to be creating the
+ * file and we know if we should be trying to handle a conflict
+ * between the patch and the file already existing. We defer
+ * handling it until hunk processing because we want to swap
+ * the hunk if they opt to reverse it, but we want to make sure
+ * we *can* swap the hunk without running into memory issues
+ * before we offer it. We also want to be verbose if flags or
+ * user decision cause us to skip -- this is explained a little
+ * more later.
+ */
+ out_existed = stat(outname, &statbuf) == 0;
+
/* for ed script just up and do it and exit */
if (diff_type == ED_DIFF) {
do_ed_script();
@@ -252,9 +275,28 @@ main(int argc, char *argv[])
failed = 0;
reverse_seen = false;
out_of_mem = false;
+ remove_file = false;
while (another_hunk()) {
+ assert(!out_creating || hunk == 0);
hunk++;
fuzz = 0;
+
+ /*
+ * There are only three cases in handle_creation() that
+ * results in us skipping hunk location, in order:
+ *
+ * 1.) Potentially reversed but -f/--force'd,
+ * 2.) Potentially reversed but -N/--forward'd
+ * 3.) Reversed and the user's opted to not apply it.
+ *
+ * In all three cases, we still want to inform the user
+ * that we're ignoring it in the standard way, which is
+ * also tied to this hunk processing loop.
+ */
+ if (out_creating)
+ reverse_seen = handle_creation(out_existed,
+ &remove_file);
+
mymaxfuzz = pch_context();
if (maxfuzz < mymaxfuzz)
mymaxfuzz = maxfuzz;
@@ -372,7 +414,6 @@ main(int argc, char *argv[])
/* and put the output where desired */
ignore_signals();
if (!skip_rest_of_patch) {
- struct stat statbuf;
char *realout = outname;
if (!check_only) {
@@ -383,7 +424,18 @@ main(int argc, char *argv[])
} else
chmod(outname, filemode);
- if (remove_empty_files &&
+ /*
+ * remove_file is a per-patch flag indicating
+ * whether it's OK to remove the empty file.
+ * This is specifically set when we're reversing
+ * the creation of a file and it ends up empty.
+ * This is an exception to the global policy
+ * (remove_empty_files) because the user would
+ * likely not expect the reverse of file
+ * creation to leave an empty file laying
+ * around.
+ */
+ if ((remove_empty_files || remove_file) &&
stat(realout, &statbuf) == 0 &&
statbuf.st_size == 0) {
if (verbose)
@@ -444,6 +496,9 @@ reinitialize_almost_everything(void)
filearg[0] = NULL;
}
+ free(source_file);
+ source_file = NULL;
+
free(outname);
outname = NULL;
@@ -1084,3 +1139,84 @@ similar(const char *a, const char *b, int len)
return true; /* actually, this is not reached */
/* since there is always a \n */
}
+
+static bool
+handle_creation(bool out_existed, bool *remove)
+{
+ bool reverse_seen;
+
+ reverse_seen = false;
+ if (reverse && out_existed) {
+ /*
+ * If the patch creates the file and we're reversing the patch,
+ * then we need to indicate to the patch processor that it's OK
+ * to remove this file.
+ */
+ *remove = true;
+ } else if (!reverse && out_existed) {
+ /*
+ * Otherwise, we need to blow the horn because the patch appears
+ * to be reversed/already applied. For non-batch jobs, we'll
+ * prompt to figure out what we should be trying to do to raise
+ * awareness of the issue. batch (-t) processing suppresses the
+ * questions and just assumes that we're reversed if it looks
+ * like we are, which is always the case if we've reached this
+ * branch.
+ */
+ if (force) {
+ skip_rest_of_patch = true;
+ return (false);
+ }
+ if (noreverse) {
+ /* If -N is supplied, however, we bail out/ignore. */
+ say("Ignoring previously applied (or reversed) patch.\n");
+ skip_rest_of_patch = true;
+ return (false);
+ }
+
+ /* Unreversed... suspicious if the file existed. */
+ if (!pch_swap())
+ fatal("lost hunk on alloc error!\n");
+
+ reverse = !reverse;
+
+ if (batch) {
+ if (verbose)
+ say("Patch creates file that already exists, %s %seversed",
+ reverse ? "Assuming" : "Ignoring",
+ reverse ? "R" : "Unr");
+ } else {
+ ask("Patch creates file that already exists! %s -R? [y] ",
+ reverse ? "Assume" : "Ignore");
+
+ if (*buf == 'n') {
+ ask("Apply anyway? [n]");
+ if (*buf != 'y')
+ /* Don't apply; error out. */
+ skip_rest_of_patch = true;
+ else
+ /* Attempt to apply. */
+ reverse_seen = true;
+ reverse = !reverse;
+ if (!pch_swap())
+ fatal("lost hunk on alloc error!\n");
+ } else {
+ /*
+ * They've opted to assume -R; effectively the
+ * same as the first branch in this function,
+ * but the decision is here rather than in a
+ * prior patch/hunk as in that branch.
+ */
+ *remove = true;
+ }
+ }
+ }
+
+ /*
+ * The return value indicates if we offered a chance to reverse but the
+ * user declined. This keeps the main patch processor in the loop since
+ * we've taken this out of the normal flow of hunk processing to
+ * simplify logic a little bit.
+ */
+ return (reverse_seen);
+}
diff --git a/usr.bin/patch/pch.c b/usr.bin/patch/pch.c
index 50e1604b9b92..24e3eea5f297 100644
--- a/usr.bin/patch/pch.c
+++ b/usr.bin/patch/pch.c
@@ -70,6 +70,8 @@ static LINENUM p_bfake = -1; /* beg of faked up lines */
static FILE *pfp = NULL; /* patch file pointer */
static char *bestguess = NULL; /* guess at correct filename */
+char *source_file;
+
static void grow_hunkmax(void);
static int intuit_diff_type(void);
static void next_intuit_at(off_t, LINENUM);
@@ -218,7 +220,12 @@ there_is_another_patch(void)
bestguess = xstrdup(buf);
filearg[0] = fetchname(buf, &exists, 0);
}
- if (!exists) {
+ /*
+ * fetchname can now return buf = NULL, exists = true, to
+ * indicate to the caller that /dev/null was specified. Retain
+ * previous behavior for now until this can be better evaluted.
+ */
+ if (filearg[0] == NULL || !exists) {
int def_skip = *bestguess == '\0';
ask("No file found--skip this patch? [%c] ",
def_skip ? 'y' : 'n');
@@ -403,6 +410,24 @@ scan_exit:
names[OLD_FILE] = names[NEW_FILE];
names[NEW_FILE] = tmp;
}
+
+ /* Invalidated */
+ free(source_file);
+ source_file = NULL;
+
+ if (retval != 0) {
+ /*
+ * If we've successfully determined a diff type, stored in
+ * retval, path == NULL means _PATH_DEVNULL if exists is set.
+ * Explicitly specify it here to make it easier to detect later
+ * on that we're actually creating a file and not that we've
+ * just goofed something up.
+ */
+ if (names[OLD_FILE].path != NULL)
+ source_file = xstrdup(names[OLD_FILE].path);
+ else if (names[OLD_FILE].exists)
+ source_file = xstrdup(_PATH_DEVNULL);
+ }
if (filearg[0] == NULL) {
if (posix)
filearg[0] = posix_name(names, ok_to_create_file);
diff --git a/usr.bin/patch/pch.h b/usr.bin/patch/pch.h
index da7a08d4008b..53ed71780e26 100644
--- a/usr.bin/patch/pch.h
+++ b/usr.bin/patch/pch.h
@@ -37,6 +37,8 @@ struct file_name {
bool exists;
};
+extern char *source_file;
+
void re_patch(void);
void open_patch_file(const char *);
void set_hunkmax(void);
diff --git a/usr.bin/patch/tests/unified_patch_test.sh b/usr.bin/patch/tests/unified_patch_test.sh
index 783681e94925..afd4b457f8d2 100755
--- a/usr.bin/patch/tests/unified_patch_test.sh
+++ b/usr.bin/patch/tests/unified_patch_test.sh
@@ -103,27 +103,28 @@ file_creation_body()
# contents as many times as you apply the diff. It should instead detect that
# a source of /dev/null creates the file, so it shouldn't exist. Furthermore,
# the reverse of creation is deletion -- hence the next test, which ensures that
-# the file is removed if it's empty once the patch is reversed.
+# the file is removed if it's empty once the patch is reversed. The size checks
+# are scattered throughout to make sure that we didn't get some kind of false
+# error, and the first size check is merely a sanity check that should be
+# trivially true as this is executed in a sandbox.
atf_test_case file_nodupe
file_nodupe_body()
{
- # WIP
- atf_expect_fail "patch(1) erroneously duplicates created files"
echo "x" > foo
diff -u /dev/null foo > foo.diff
- atf_check -x "patch -s < foo.diff"
- atf_check -s not-exit:0 -x "patch -fs < foo.diff"
+ atf_check -o inline:"2\n" stat -f "%z" foo
+ atf_check -s not-exit:0 -o ignore -x "patch -Ns < foo.diff"
+ atf_check -o inline:"2\n" stat -f "%z" foo
+ atf_check -s not-exit:0 -o ignore -x "patch -fs < foo.diff"
+ atf_check -o inline:"2\n" stat -f "%z" foo
}
atf_test_case file_removal
file_removal_body()
{
- # WIP
- atf_expect_fail "patch(1) does not yet recognize /dev/null as creation"
-
echo "x" > foo
diff -u /dev/null foo > foo.diff
diff --git a/usr.bin/patch/util.c b/usr.bin/patch/util.c
index 39a2eedec357..080a3a266e3b 100644
--- a/usr.bin/patch/util.c
+++ b/usr.bin/patch/util.c
@@ -366,8 +366,10 @@ fetchname(const char *at, bool *exists, int strip_leading)
say("fetchname %s %d\n", at, strip_leading);
#endif
/* So files can be created by diffing against /dev/null. */
- if (strnEQ(at, _PATH_DEVNULL, sizeof(_PATH_DEVNULL) - 1))
+ if (strnEQ(at, _PATH_DEVNULL, sizeof(_PATH_DEVNULL) - 1)) {
+ *exists = true;
return NULL;
+ }
name = fullname = t = savestr(at);
tab = strchr(t, '\t') != NULL;