Jonathan Nieder wrote:

> Maybe the space-plus-date-plus-newline sequence should be used as a
> delimiter.

And here’s how that would work.

Patches are attached, because debbugs seems to handle that best.

Still to do:

 * add tests for ‘-p’ value detection
 * skip tests that require ‘diff -u’ and ‘pr’ when those commands
   are not present.  Or find some other workaround, like
   including output from those commands with the patch.

Thoughts?

Jonathan Nieder (3):
  apply: Split quoted filename handling into new function
  tests: Add test for handling of weird filenames
  apply: Handle traditional patches with space in filename

 builtin/apply.c                  |  251 ++++++++++++++++++++++++++++++++------
 t/t4135-apply-weird-filenames.sh |   73 +++++++++++
 2 files changed, 285 insertions(+), 39 deletions(-)
 create mode 100755 t/t4135-apply-weird-filenames.sh
From: Jonathan Nieder <[email protected]>
Date: Mon, 5 Jul 2010 19:34:17 -0500
Subject: apply: Split quoted filename handling into new function

The new find_name_gnu() function handles new-style ‘--- "a/foo"’
patch header lines, leaving find_name() itself a bit less
daunting.

Functional change: do not clobber the p-value when there
are not enough path components in the detected file name
to honor it.

Signed-off-by: Jonathan Nieder <[email protected]>
---
 builtin/apply.c |   70 ++++++++++++++++++++++++++++++------------------------
 1 files changed, 39 insertions(+), 31 deletions(-)

diff --git a/builtin/apply.c b/builtin/apply.c
index 12ef9ea..efc109e 100644
--- a/builtin/apply.c
+++ b/builtin/apply.c
@@ -416,44 +416,52 @@ static char *squash_slash(char *name)
        return name;
 }
 
+static char *find_name_gnu(const char *line, char *def, int p_value)
+{
+       struct strbuf name = STRBUF_INIT;
+       char *cp;
+
+       /*
+        * Proposed "new-style" GNU patch/diff format; see
+        * http://marc.theaimsgroup.com/?l=git&m=112927316408690&w=2
+        */
+       if (unquote_c_style(&name, line, NULL)) {
+               strbuf_release(&name);
+               return NULL;
+       }
+
+       for (cp = name.buf; p_value; p_value--) {
+               cp = strchr(cp, '/');
+               if (!cp) {
+                       strbuf_release(&name);
+                       return NULL;
+               }
+               cp++;
+       }
+
+       /* name can later be freed, so we need
+        * to memmove, not just return cp
+        */
+       strbuf_remove(&name, 0, cp - name.buf);
+       free(def);
+       if (root)
+               strbuf_insert(&name, 0, root, root_len);
+       return squash_slash(strbuf_detach(&name, NULL));
+}
+
 static char *find_name(const char *line, char *def, int p_value, int terminate)
 {
        int len;
        const char *start = NULL;
 
+       if (*line == '"') {
+               char *name = find_name_gnu(line, def, p_value);
+               if (name)
+                       return name;
+       }
+
        if (p_value == 0)
                start = line;
-
-       if (*line == '"') {
-               struct strbuf name = STRBUF_INIT;
-
-               /*
-                * Proposed "new-style" GNU patch/diff format; see
-                * http://marc.theaimsgroup.com/?l=git&m=112927316408690&w=2
-                */
-               if (!unquote_c_style(&name, line, NULL)) {
-                       char *cp;
-
-                       for (cp = name.buf; p_value; p_value--) {
-                               cp = strchr(cp, '/');
-                               if (!cp)
-                                       break;
-                               cp++;
-                       }
-                       if (cp) {
-                               /* name can later be freed, so we need
-                                * to memmove, not just return cp
-                                */
-                               strbuf_remove(&name, 0, cp - name.buf);
-                               free(def);
-                               if (root)
-                                       strbuf_insert(&name, 0, root, root_len);
-                               return squash_slash(strbuf_detach(&name, NULL));
-                       }
-               }
-               strbuf_release(&name);
-       }
-
        for (;;) {
                char c = *line;
 
-- 
1.7.2.rc2

From: Jonathan Nieder <[email protected]>
Date: Mon, 12 Jul 2010 19:19:17 -0500
Subject: tests: Add test for handling of weird filenames

These tests rely on the local ‘diff’ implementation to produce
interesting patches, instead of including our own interesting
examples.

Portability nightmares: diff -u, pr.

Signed-off-by: Jonathan Nieder <[email protected]>
---
 t/t4135-apply-weird-filenames.sh |   75 ++++++++++++++++++++++++++++++++++++++
 1 files changed, 75 insertions(+), 0 deletions(-)
 create mode 100755 t/t4135-apply-weird-filenames.sh

diff --git a/t/t4135-apply-weird-filenames.sh b/t/t4135-apply-weird-filenames.sh
new file mode 100755
index 0000000..c3050a2
--- /dev/null
+++ b/t/t4135-apply-weird-filenames.sh
@@ -0,0 +1,75 @@
+#!/bin/sh
+
+test_description='git apply with weird postimage filenames'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+       test_tick &&
+       git commit --allow-empty -m preimage &&
+       git tag preimage &&
+
+       reset_preimage() {
+               git checkout -f preimage^0 &&
+               git read-tree -u --reset HEAD &&
+               git update-index --refresh
+       } &&
+
+       reset_subdirs() {
+               rm -fr a b &&
+               mkdir a b
+       }
+'
+
+try_filename() {
+       desc=$1
+       postimage=$2
+       exp1=$3
+       exp2=$4
+
+       test_expect_$exp1 "$desc, git-style file creation patch" "
+               reset_preimage &&
+               echo postimage >'$postimage' &&
+               git add -N '$postimage' &&
+               git diff HEAD >'git-$desc.diff' &&
+
+               git rm -f --cached '$postimage' &&
+               mv '$postimage' postimage.saved &&
+               git apply -v 'git-$desc.diff' &&
+
+               test_cmp postimage.saved '$postimage'
+       "
+
+       test_expect_$exp2 "$desc, traditional file creation patch" "
+               reset_preimage &&
+               reset_subdirs &&
+               echo postimage >b/'$postimage' &&
+               ! diff -pruN a b >'diff-$desc.diff' &&
+
+               mv '$postimage' postimage.saved &&
+               git apply -v 'diff-$desc.diff' &&
+
+               test_cmp postimage.saved '$postimage'
+       "
+}
+
+try_filename 'plain'            'postimage.txt'         success success
+try_filename 'with spaces'      'post image.txt'        success failure
+try_filename 'with tab'         'post  image.txt'      success failure
+try_filename 'with backslash'   'post\image.txt'        success success
+try_filename 'with quote'       '"postimage".txt'       success success
+
+test_expect_success 'whitespace-damaged traditional patch' '
+       reset_preimage &&
+       reset_subdirs &&
+       echo postimage >b/postimage.txt &&
+       ! diff -pruN a b >diff-plain.txt &&
+       pr -tT -e8 diff-plain.diff >damaged.diff &&
+
+       mv postimage.txt postimage.saved &&
+       git apply -v damaged.diff &&
+
+       test_cmp postimage.saved postimage.txt
+'
+
+test_done
-- 
1.7.2.rc2

From: Jonathan Nieder <[email protected]>
Date: Mon, 12 Jul 2010 20:01:29 -0500
Subject: apply: Handle traditional patches with space in filename

To discover filenames from the

 --- foo
 +++ bar

lines in a traditional unified diff, currently ‘git apply’ scans
forward for a whitespace character on each line and stops there.
It can’t use the whole line because ‘diff -u’ likes to include
timestamps, like so:

 --- foo        2000-07-12 16:56:50.020000414 -0500
 +++ bar        2010-07-12 16:56:50.020000414 -0500

The whitespace-seeking heuristic works great, even when the tab
has been converted to spaces by some email + copy-and-paste
related corruption.

Except for one problem: if the actual filename contains whitespace,
the inferred filename will be too short.

When Giuseppe ran into this problem, it was for a file creation
patch (filename ‘debian/licenses/LICENSE.global BSD-style Chromium’).
So one can’t even use the list of files present in the index to
deduce an appropriate filename.

Instead, look for a timestamp and use that if present to mark the end
of the filename.  If no timestamp is present, the old heuristic is
used, with one exception: the space character \040 is not considered
terminating whitespace any more unless it is followed by a timestamp.

Reported-by: Giuseppe Iuculano <[email protected]>
Analysis-by: Guido Günther <[email protected]>
Signed-off-by: Jonathan Nieder <[email protected]>
---
 builtin/apply.c                  |  193 +++++++++++++++++++++++++++++++++++---
 t/t4135-apply-weird-filenames.sh |   16 ++--
 2 files changed, 186 insertions(+), 23 deletions(-)

diff --git a/builtin/apply.c b/builtin/apply.c
index efc109e..4874eca 100644
--- a/builtin/apply.c
+++ b/builtin/apply.c
@@ -449,23 +449,157 @@ static char *find_name_gnu(const char *line, char *def, 
int p_value)
        return squash_slash(strbuf_detach(&name, NULL));
 }
 
-static char *find_name(const char *line, char *def, int p_value, int terminate)
+static size_t tz_len(const char *line, size_t len)
+{
+       const char *tz, *p;
+
+       if (len < strlen(" +0500") || line[len-strlen(" +0500")] != ' ')
+               return 0;
+       tz = line + len - strlen(" +0500");
+
+       if (tz[1] != '+' && tz[1] != '-')
+               return 0;
+
+       for (p = tz + 2; p != line + len; p++)
+               if (!isdigit(*p))
+                       return 0;
+
+       return line + len - tz;
+}
+
+static size_t date_len(const char *line, size_t len)
+{
+       const char *date, *p;
+
+       if (len < strlen("72-02-05") || line[len-strlen("-05")] != '-')
+               return 0;
+       p = date = line + len - strlen("72-02-05");
+
+       if (!isdigit(*p++) || !isdigit(*p++) || *p++ != '-' ||
+           !isdigit(*p++) || !isdigit(*p++) || *p++ != '-' ||
+           !isdigit(*p++) || !isdigit(*p++))   /* Not a date. */
+               return 0;
+
+       if (date - line >= strlen("19") &&
+           isdigit(date[-1]) && isdigit(date[-2]))     /* 4-digit year */
+               date -= strlen("19");
+
+       return line + len - date;
+}
+
+static size_t whole_time_len(const char *line, size_t len)
+{
+       const char *time, *p;
+
+       if (len < strlen(" 07:01:32") || line[len-strlen(":32")] != ':')
+               return 0;
+       p = time = line + len - strlen(" 07:01:32");
+
+       /* Permit 1-digit hours? */
+       if (*p++ != ' ' ||
+           !isdigit(*p++) || !isdigit(*p++) || *p++ != ':' ||
+           !isdigit(*p++) || !isdigit(*p++) || *p++ != ':' ||
+           !isdigit(*p++) || !isdigit(*p++))   /* Not a time. */
+               return 0;
+
+       return line + len - time;
+}
+
+static size_t fractional_time_len(const char *line, size_t len)
+{
+       const char *p;
+       size_t n;
+
+       /* Expected format: 19:41:17.620000023 */
+       if (!len || !isdigit(line[len - 1]))
+               return 0;
+       p = line + len - 1;
+
+       /* Fractional seconds. */
+       while (p > line && isdigit(*p))
+               p--;
+       if (*p != '.')
+               return 0;
+
+       /* Hours, minutes, and whole seconds. */
+       n = whole_time_len(line, p - line);
+       if (!n)
+               return 0;
+
+       return line + len - p + n;
+}
+
+static size_t trailing_spaces_len(const char *line, size_t len)
+{
+       const char *p;
+
+       /* Expected format: ' ' x (1 or more)  */
+       if (!len || line[len - 1] != ' ')
+               return 0;
+
+       p = line + len;
+       while (p != line) {
+               p--;
+               if (*p != ' ')
+                       return line + len - (p + 1);
+       }
+
+       /* All spaces! */
+       return len;
+}
+
+static size_t name_trailer_len(const char *line, size_t len)
+{
+       const char *end = line + len;
+       size_t n;
+
+       /*
+        * Posix: 2010-07-05 19:41:17
+        * GNU: 2010-07-05 19:41:17.620000023 -0500
+        */
+
+       if (!isdigit(end[-1]))
+               return 0;
+
+       n = tz_len(line, end - line);
+       end -= n;
+
+       n = whole_time_len(line, end - line);
+       if (!n)
+               n = fractional_time_len(line, end - line);
+       end -= n;
+
+       n = date_len(line, end - line);
+       if (!n) /* No date.  Too bad. */
+               return 0;
+       end -= n;
+
+       if (end == line)        /* No space before date. */
+               return 0;
+       if (end[-1] == '\t') {  /* Success! */
+               end--;
+               return line + len - end;
+       }
+       if (end[-1] != ' ')     /* No space before date. */
+               return 0;
+
+       /* Whitespace damage. */
+       end -= trailing_spaces_len(line, end - line);
+       return line + len - end;
+}
+
+static char *find_name_common(const char *line, char *def, int p_value,
+                               const char *end, int terminate)
 {
        int len;
        const char *start = NULL;
 
-       if (*line == '"') {
-               char *name = find_name_gnu(line, def, p_value);
-               if (name)
-                       return name;
-       }
-
        if (p_value == 0)
                start = line;
-       for (;;) {
+       while (line != end) {
                char c = *line;
 
-               if (isspace(c)) {
+               if (!end && isspace(c)) {
                        if (c == '\n')
                                break;
                        if (name_terminate(start, line-start, c, terminate))
@@ -505,6 +639,37 @@ static char *find_name(const char *line, char *def, int 
p_value, int terminate)
        return squash_slash(xmemdupz(start, len));
 }
 
+static char *find_name(const char *line, char *def, int p_value, int terminate)
+{
+       if (*line == '"') {
+               char *name = find_name_gnu(line, def, p_value);
+               if (name)
+                       return name;
+       }
+
+       return find_name_common(line, def, p_value, NULL, terminate);
+}
+
+static char *find_name_traditional(const char *line, char *def, int p_value)
+{
+       size_t len = strlen(line);
+       size_t date_len;
+
+       if (*line == '"') {
+               char *name = find_name_gnu(line, def, p_value);
+               if (name)
+                       return name;
+       }
+
+       len = strchrnul(line, '\n') - line;
+       date_len = name_trailer_len(line, len);
+       if (!date_len)
+               return find_name(line, def, p_value, TERM_TAB);
+       len -= date_len;
+
+       return find_name_common(line, def, p_value, line + len, 0);
+}
+
 static int count_slashes(const char *cp)
 {
        int cnt = 0;
@@ -527,7 +692,7 @@ static int guess_p_value(const char *nameline)
 
        if (is_dev_null(nameline))
                return -1;
-       name = find_name(nameline, NULL, 0, TERM_SPACE | TERM_TAB);
+       name = find_name_traditional(nameline, NULL, 0);
        if (!name)
                return -1;
        cp = strchr(name, '/');
@@ -646,16 +811,16 @@ static void parse_traditional_patch(const char *first, 
const char *second, struc
        if (is_dev_null(first)) {
                patch->is_new = 1;
                patch->is_delete = 0;
-               name = find_name(second, NULL, p_value, TERM_SPACE | TERM_TAB);
+               name = find_name_traditional(second, NULL, p_value);
                patch->new_name = name;
        } else if (is_dev_null(second)) {
                patch->is_new = 0;
                patch->is_delete = 1;
-               name = find_name(first, NULL, p_value, TERM_SPACE | TERM_TAB);
+               name = find_name_traditional(first, NULL, p_value);
                patch->old_name = name;
        } else {
-               name = find_name(first, NULL, p_value, TERM_SPACE | TERM_TAB);
-               name = find_name(second, name, p_value, TERM_SPACE | TERM_TAB);
+               name = find_name_traditional(first, NULL, p_value);
+               name = find_name_traditional(second, name, p_value);
                if (has_epoch_timestamp(first)) {
                        patch->is_new = 1;
                        patch->is_delete = 0;
diff --git a/t/t4135-apply-weird-filenames.sh b/t/t4135-apply-weird-filenames.sh
index c3050a2..b0c0426 100755
--- a/t/t4135-apply-weird-filenames.sh
+++ b/t/t4135-apply-weird-filenames.sh
@@ -24,10 +24,8 @@ test_expect_success setup '
 try_filename() {
        desc=$1
        postimage=$2
-       exp1=$3
-       exp2=$4
 
-       test_expect_$exp1 "$desc, git-style file creation patch" "
+       test_expect_success "$desc, git-style file creation patch" "
                reset_preimage &&
                echo postimage >'$postimage' &&
                git add -N '$postimage' &&
@@ -40,7 +38,7 @@ try_filename() {
                test_cmp postimage.saved '$postimage'
        "
 
-       test_expect_$exp2 "$desc, traditional file creation patch" "
+       test_expect_success "$desc, traditional file creation patch" "
                reset_preimage &&
                reset_subdirs &&
                echo postimage >b/'$postimage' &&
@@ -53,11 +51,11 @@ try_filename() {
        "
 }
 
-try_filename 'plain'            'postimage.txt'         success success
-try_filename 'with spaces'      'post image.txt'        success failure
-try_filename 'with tab'         'post  image.txt'      success failure
-try_filename 'with backslash'   'post\image.txt'        success success
-try_filename 'with quote'       '"postimage".txt'       success success
+try_filename 'plain'            'postimage.txt'
+try_filename 'with spaces'      'post image.txt'
+try_filename 'with tab'         'post  image.txt'
+try_filename 'with backslash'   'post\image.txt'
+try_filename 'with quote'       '"postimage".txt'
 
 test_expect_success 'whitespace-damaged traditional patch' '
        reset_preimage &&
-- 
1.7.2.rc2

Reply via email to