diff: fix a bug in diffing against stdin. Closes 7784
[oweals/busybox.git] / editors / diff.c
index 8d91b83bfc8a448779008c5d88125852ab3f465d..c3ad31bf30cc8fd7b85a542ab6f8cb127638ba1b 100644 (file)
@@ -10,7 +10,7 @@
  * Agency (DARPA) and Air Force Research Laboratory, Air Force
  * Materiel Command, USAF, under agreement number F39502-99-1-0512.
  *
- * Licensed under GPLv2 or later, see file LICENSE in this tarball for details.
+ * Licensed under GPLv2 or later, see file LICENSE in this source tree.
  */
 
 /*
  * 6n words for files of length n.
  */
 
+//config:config DIFF
+//config:      bool "diff"
+//config:      default y
+//config:      help
+//config:        diff compares two files or directories and outputs the
+//config:        differences between them in a form that can be given to
+//config:        the patch command.
+//config:
+//config:config FEATURE_DIFF_LONG_OPTIONS
+//config:      bool "Enable long options"
+//config:      default y
+//config:      depends on DIFF && LONG_OPTS
+//config:      help
+//config:        Enable use of long options.
+//config:
+//config:config FEATURE_DIFF_DIR
+//config:      bool "Enable directory support"
+//config:      default y
+//config:      depends on DIFF
+//config:      help
+//config:        This option enables support for directory and subdirectory
+//config:        comparison.
+
+//kbuild:lib-$(CONFIG_DIFF) += diff.o
+
+//applet:IF_DIFF(APPLET(diff, BB_DIR_USR_BIN, BB_SUID_DROP))
+
+//usage:#define diff_trivial_usage
+//usage:       "[-abBdiNqrTstw] [-L LABEL] [-S FILE] [-U LINES] FILE1 FILE2"
+//usage:#define diff_full_usage "\n\n"
+//usage:       "Compare files line by line and output the differences between them.\n"
+//usage:       "This implementation supports unified diffs only.\n"
+//usage:     "\n       -a      Treat all files as text"
+//usage:     "\n       -b      Ignore changes in the amount of whitespace"
+//usage:     "\n       -B      Ignore changes whose lines are all blank"
+//usage:     "\n       -d      Try hard to find a smaller set of changes"
+//usage:     "\n       -i      Ignore case differences"
+//usage:     "\n       -L      Use LABEL instead of the filename in the unified header"
+//usage:     "\n       -N      Treat absent files as empty"
+//usage:     "\n       -q      Output only whether files differ"
+//usage:     "\n       -r      Recurse"
+//usage:     "\n       -S      Start with FILE when comparing directories"
+//usage:     "\n       -T      Make tabs line up by prefixing a tab when necessary"
+//usage:     "\n       -s      Report when two files are the same"
+//usage:     "\n       -t      Expand tabs to spaces in output"
+//usage:     "\n       -U      Output LINES lines of context"
+//usage:     "\n       -w      Ignore all whitespace"
+
 #include "libbb.h"
 
 #if 0
-//#define dbg_error_msg(...) bb_error_msg(__VA_ARGS__)
+define dbg_error_msg(...) bb_error_msg(__VA_ARGS__)
 #else
-#define dbg_error_msg(...) ((void)0)
+# define dbg_error_msg(...) ((void)0)
 #endif
 
 enum {                  /* print_status() and diffreg() return values */
@@ -121,6 +169,7 @@ typedef struct FILE_and_pos_t {
 struct globals {
        smallint exit_status;
        int opt_U_context;
+       const char *other_dir;
        char *label[2];
        struct stat stb[2];
 };
@@ -229,7 +278,7 @@ static int search(const int *c, int k, int y, const struct cand *list)
 {
        int i, j;
 
-       if (list[c[k]].y < y)   /* quick look for typical case */
+       if (list[c[k]].y < y)  /* quick look for typical case */
                return k + 1;
 
        for (i = 0, j = k + 1;;) {
@@ -478,7 +527,7 @@ start:
        for (; suff < nlen[0] - pref && suff < nlen[1] - pref &&
               nfile[0][nlen[0] - suff].value == nfile[1][nlen[1] - suff].value;
               suff++);
-       /* Arrays are pruned by the suffix and prefix lenght,
+       /* Arrays are pruned by the suffix and prefix length,
         * the result being sorted and stored in sfile[fileno],
         * and their sizes are stored in slen[fileno]
         */
@@ -671,10 +720,12 @@ static bool diff(FILE* fp[2], char *file[2])
 
 static int diffreg(char *file[2])
 {
-       FILE *fp[2] = { stdin, stdin };
+       FILE *fp[2];
        bool binary = false, differ = false;
        int status = STATUS_SAME, i;
 
+       fp[0] = stdin;
+       fp[1] = stdin;
        for (i = 0; i < 2; i++) {
                int fd = open_or_warn_stdin(file[i]);
                if (fd == -1)
@@ -684,15 +735,15 @@ static int diffreg(char *file[2])
                 */
                if (lseek(fd, 0, SEEK_SET) == -1 && errno == ESPIPE) {
                        char name[] = "/tmp/difXXXXXX";
-                       int fd_tmp = mkstemp(name);
-                       if (fd_tmp < 0)
-                               bb_perror_msg_and_die("mkstemp");
+                       int fd_tmp = xmkstemp(name);
+
                        unlink(name);
                        if (bb_copyfd_eof(fd, fd_tmp) < 0)
                                xfunc_die();
-                       if (fd) /* Prevents closing of stdin */
+                       if (fd != STDIN_FILENO)
                                close(fd);
                        fd = fd_tmp;
+                       xlseek(fd, 0, SEEK_SET);
                }
                fp[i] = fdopen(fd, "r");
        }
@@ -761,9 +812,9 @@ static int FAST_FUNC add_to_dirlist(const char *filename,
 {
        struct dlist *const l = userdata;
        const char *file = filename + l->len;
-       l->dl = xrealloc_vector(l->dl, 6, l->e);
-       while(*file == '/')
+       while (*file == '/')
                file++;
+       l->dl = xrealloc_vector(l->dl, 6, l->e);
        l->dl[l->e] = xstrdup(file);
        l->e++;
        return TRUE;
@@ -780,6 +831,27 @@ static int FAST_FUNC skip_dir(const char *filename,
                add_to_dirlist(filename, sb, userdata, depth);
                return SKIP;
        }
+       if (!(option_mask32 & FLAG(N))) {
+               /* -r without -N: no need to recurse into dirs
+                * which do not exist on the "other side".
+                * Testcase: diff -r /tmp /
+                * (it would recurse deep into /proc without this code) */
+               struct dlist *const l = userdata;
+               filename += l->len;
+               if (filename[0]) {
+                       struct stat osb;
+                       char *othername = concat_path_file(G.other_dir, filename);
+                       int r = stat(othername, &osb);
+                       free(othername);
+                       if (r != 0 || !S_ISDIR(osb.st_mode)) {
+                               /* other dir doesn't have similarly named
+                                * directory, don't recurse; return 1 upon
+                                * exit, just like diffutils' diff */
+                               exit_status |= 1;
+                               return SKIP;
+                       }
+               }
+       }
        return TRUE;
 }
 
@@ -793,12 +865,13 @@ static void diffdir(char *p[2], const char *s_start)
                /*list[i].s = list[i].e = 0; - memset did it */
                /*list[i].dl = NULL; */
 
+               G.other_dir = p[1 - i];
                /* We need to trim root directory prefix.
                 * Using list.len to specify its length,
                 * add_to_dirlist will remove it. */
                list[i].len = strlen(p[i]);
                recursive_action(p[i], ACTION_RECURSE | ACTION_FOLLOWLINKS,
-                                add_to_dirlist, skip_dir, &list[i], 0);
+                               add_to_dirlist, skip_dir, &list[i], 0);
                /* Sort dl alphabetically.
                 * GNU diff does this ignoring any number of trailing dots.
                 * We don't, so for us dotted files almost always are
@@ -826,9 +899,10 @@ static void diffdir(char *p[2], const char *s_start)
                        break;
                pos = !dp[0] ? 1 : (!dp[1] ? -1 : strcmp(dp[0], dp[1]));
                k = pos > 0;
-               if (pos && !(option_mask32 & FLAG(N)))
+               if (pos && !(option_mask32 & FLAG(N))) {
                        printf("Only in %s: %s\n", p[k], dp[k]);
-               else {
+                       exit_status |= 1;
+               } else {
                        char *fullpath[2], *path[2]; /* if -N */
 
                        for (i = 0; i < 2; i++) {
@@ -929,6 +1003,31 @@ int diff_main(int argc UNUSED_PARAM, char **argv)
        if (gotstdin && (S_ISDIR(stb[0].st_mode) || S_ISDIR(stb[1].st_mode)))
                bb_error_msg_and_die("can't compare stdin to a directory");
 
+       /* Compare metadata to check if the files are the same physical file.
+        *
+        * Comment from diffutils source says:
+        * POSIX says that two files are identical if st_ino and st_dev are
+        * the same, but many file systems incorrectly assign the same (device,
+        * inode) pair to two distinct files, including:
+        * GNU/Linux NFS servers that export all local file systems as a
+        * single NFS file system, if a local device number (st_dev) exceeds
+        * 255, or if a local inode number (st_ino) exceeds 16777215.
+        */
+       if (ENABLE_DESKTOP
+        && stb[0].st_ino == stb[1].st_ino
+        && stb[0].st_dev == stb[1].st_dev
+        && stb[0].st_size == stb[1].st_size
+        && stb[0].st_mtime == stb[1].st_mtime
+        && stb[0].st_ctime == stb[1].st_ctime
+        && stb[0].st_mode == stb[1].st_mode
+        && stb[0].st_nlink == stb[1].st_nlink
+        && stb[0].st_uid == stb[1].st_uid
+        && stb[0].st_gid == stb[1].st_gid
+       ) {
+               /* files are physically the same; no need to compare them */
+               return STATUS_SAME;
+       }
+
        if (S_ISDIR(stb[0].st_mode) && S_ISDIR(stb[1].st_mode)) {
 #if ENABLE_FEATURE_DIFF_DIR
                diffdir(file, s_start);