Call clean_flist() with its new "collapse_dot_dot" arg.
[rsync/rsync.git] / util.c
diff --git a/util.c b/util.c
index d896082..a2e8660 100644 (file)
--- a/util.c
+++ b/util.c
@@ -31,6 +31,7 @@ extern int verbose;
 extern int dry_run;
 extern int module_id;
 extern int modify_window;
+extern char *partial_dir;
 extern struct exclude_list_struct server_exclude_list;
 
 int sanitize_paths = 0;
@@ -361,8 +362,8 @@ int robust_unlink(char *fname)
 #endif
 }
 
-/* Returns 0 on success, -1 on most errors, and -2 if we got an error
- * trying to copy the file across file systems. */
+/* Returns 0 on successful rename, 1 if we successfully copied the file
+ * across filesystems, -2 if copy_file() failed, and -1 on other errors. */
 int robust_rename(char *from, char *to, int mode)
 {
        int tries = 4;
@@ -382,7 +383,7 @@ int robust_rename(char *from, char *to, int mode)
                        if (copy_file(from, to, mode) != 0)
                                return -2;
                        do_unlink(from);
-                       return 0;
+                       return 1;
                default:
                        return -1;
                }
@@ -522,9 +523,10 @@ static void glob_expand_one(char *s, char ***argv_ptr, int *argc_ptr,
        if (!*s)
                s = ".";
 
-       s = strdup(s);
        if (sanitize_paths)
-               sanitize_path(s, NULL);
+               s = sanitize_path(NULL, s, "", 0);
+       else
+               s = strdup(s);
 
        memset(&globbuf, 0, sizeof globbuf);
        if (!exclude_server_path(s))
@@ -648,122 +650,139 @@ size_t stringjoin(char *dest, size_t destsize, ...)
        return ret;
 }
 
-void clean_fname(char *name)
+int count_dir_elements(const char *p)
 {
-       char *p;
-       int l;
-       int modified = 1;
+       int cnt = 0, new_component = 1;
+       while (*p) {
+               if (*p++ == '/')
+                       new_component = 1;
+               else if (new_component) {
+                       new_component = 0;
+                       cnt++;
+               }
+       }
+       return cnt;
+}
 
-       if (!name)
-               return;
+/* Turns multiple adjacent slashes into a single slash; gets rid of "./"
+ * elements; collapses ".." elements except for those at the start of the
+ * string; removes a trailing slash.  If the resulting name would be empty,
+ * change it into a ".". */
+unsigned int clean_fname(char *name)
+{
+       char *limit = name - 1, *t = name, *f = name;
+       int anchored;
 
-       while (modified) {
-               modified = 0;
+       if (!name)
+               return 0;
 
-               if ((p = strstr(name,"/./")) != NULL) {
-                       modified = 1;
-                       while (*p) {
-                               p[0] = p[2];
-                               p++;
-                       }
+       if ((anchored = *f == '/') != 0)
+               *t++ = *f++;
+       while (*f) {
+               /* discard extra slashes */
+               if (*f == '/') {
+                       f++;
+                       continue;
                }
-
-               if ((p = strstr(name,"//")) != NULL) {
-                       modified = 1;
-                       while (*p) {
-                               p[0] = p[1];
-                               p++;
+               if (*f == '.') {
+                       /* discard "." dirs (but NOT a trailing '.'!) */
+                       if (f[1] == '/') {
+                               f += 2;
+                               continue;
+                       }
+                       /* collapse ".." dirs */
+                       if (f[1] == '.' && (f[2] == '/' || !f[2])) {
+                               char *s = t - 1;
+                               if (s == name && anchored) {
+                                       f += 2;
+                                       continue;
+                               }
+                               while (s > limit && *--s != '/') {}
+                               if (s != t - 1 && (s < name || *s == '/')) {
+                                       t = s + 1;
+                                       f += 2;
+                                       continue;
+                               }
+                               *t++ = *f++;
+                               *t++ = *f++;
+                               limit = t;
                        }
                }
+               while (*f && (*t++ = *f++) != '/') {}
+       }
 
-               if (strncmp(p = name, "./", 2) == 0) {
-                       modified = 1;
-                       do {
-                               p[0] = p[2];
-                       } while (*p++);
-               }
+       if (t > name+anchored && t[-1] == '/')
+               t--;
+       if (t == name)
+               *t++ = '.';
+       *t = '\0';
 
-               l = strlen(p = name);
-               if (l > 1 && p[l-1] == '/') {
-                       modified = 1;
-                       p[l-1] = 0;
-               }
-       }
+       return t - name;
 }
 
-/**
- * Make path appear as if a chroot had occurred:
+/* Make path appear as if a chroot had occurred.  This handles a leading
+ * "/" (either removing it or expanding it) and any leading or embedded
+ * ".." components that attempt to escape past the module's top dir.
  *
- * @li 1. remove leading "/" (or replace with "." if at end)
+ * If dest is NULL, a buffer is allocated to hold the result.  It is legal
+ * to call with the dest and the path (p) pointing to the same buffer, but
+ * rootdir will be ignored to avoid expansion of the string.
  *
- * @li 2. remove leading ".." components (except those allowed by @p reldir)
+ * The rootdir string contains a value to use in place of a leading slash.
+ * Specify NULL to get the default of lp_path(module_id).
  *
- * @li 3. delete any other "<dir>/.." (recursively)
+ * If depth is > 0, it is a count of how many '..'s to allow at the start
+ * of the path.
  *
- * Can only shrink paths, so sanitizes in place.
- *
- * While we're at it, remove double slashes and "." components like
- *   clean_fname() does, but DON'T remove a trailing slash because that
- *   is sometimes significant on command line arguments.
- *
- * If @p reldir is non-null, it is a sanitized directory that the path will be
- *    relative to, so allow as many ".." at the beginning of the path as
- *    there are components in reldir.  This is used for symbolic link targets.
- *    If reldir is non-null and the path began with "/", to be completely like
- *    a chroot we should add in depth levels of ".." at the beginning of the
- *    path, but that would blow the assumption that the path doesn't grow and
- *    it is not likely to end up being a valid symlink anyway, so just do
- *    the normal removal of the leading "/" instead.
- *
- * Contributed by Dave Dykstra <dwd@bell-labs.com>
+ * We call clean_fname() to clean up the path, but we preserve a trailing
+ * slash because that is sometimes significant on command-line arguments.
  */
-void sanitize_path(char *p, char *reldir)
+char *sanitize_path(char *dest, const char *p, const char *rootdir, int depth)
 {
        char *start, *sanp;
-       int depth = 0;
-       int allowdotdot = 0;
-
-       if (reldir) {
-               int new_component = 1;
-               while (*reldir) {
-                       if (*reldir++ == '/')
-                               new_component = 1;
-                       else if (new_component) {
-                               new_component = 0;
-                               depth++;
-                       }
+       int rlen = 0;
+
+       if (dest != p) {
+               int plen = strlen(p);
+               if (*p == '/') {
+                       if (!rootdir)
+                               rootdir = lp_path(module_id);
+                       rlen = strlen(rootdir);
+                       depth = 0;
+                       p++;
+               }
+               if (dest) {
+                       if (rlen + plen + 1 >= MAXPATHLEN)
+                               return NULL;
+               } else if (!(dest = new_array(char, rlen + plen + 1)))
+                       out_of_memory("sanitize_path");
+               if (rlen) {
+                       memcpy(dest, rootdir, rlen);
+                       if (rlen > 1)
+                               dest[rlen++] = '/';
                }
        }
-       start = p;
-       sanp = p;
-       while (*p == '/') {
-               /* remove leading slashes */
-               p++;
-       }
+
+       start = sanp = dest + rlen;
        while (*p != '\0') {
+               /* discard leading or extra slashes */
+               if (*p == '/') {
+                       p++;
+                       continue;
+               }
                /* this loop iterates once per filename component in p.
                 * both p (and sanp if the original had a slash) should
                 * always be left pointing after a slash
                 */
                if (*p == '.' && (p[1] == '/' || p[1] == '\0')) {
                        /* skip "." component */
-                       while (*++p == '/') {
-                               /* skip following slashes */
-                               ;
-                       }
+                       p++;
                        continue;
                }
-               allowdotdot = 0;
                if (*p == '.' && p[1] == '.' && (p[2] == '/' || p[2] == '\0')) {
                        /* ".." component followed by slash or end */
-                       if (depth > 0 && sanp == start) {
-                               /* allow depth levels of .. at the beginning */
-                               --depth;
-                               allowdotdot = 1;
-                       } else {
+                       if (depth <= 0 || sanp != start) {
                                p += 2;
-                               if (*p == '/')
-                                       p++;
                                if (sanp != start) {
                                        /* back up sanp one level */
                                        --sanp; /* now pointing at slash */
@@ -774,68 +793,21 @@ void sanitize_path(char *p, char *reldir)
                                }
                                continue;
                        }
-               }
-               while (1) {
-                       /* copy one component through next slash */
-                       *sanp++ = *p++;
-                       if (*p == '\0' || p[-1] == '/') {
-                               while (*p == '/') {
-                                       /* skip multiple slashes */
-                                       p++;
-                               }
-                               break;
-                       }
-               }
-               if (allowdotdot) {
+                       /* allow depth levels of .. at the beginning */
+                       depth--;
                        /* move the virtual beginning to leave the .. alone */
-                       start = sanp;
+                       start = sanp + 3;
                }
+               /* copy one component through next slash */
+               while (*p && (*sanp++ = *p++) != '/') {}
        }
-       if (sanp == start && !allowdotdot) {
+       if (sanp == dest) {
                /* ended up with nothing, so put in "." component */
-               /*
-                * note that the !allowdotdot doesn't prevent this from
-                *  happening in all allowed ".." situations, but I didn't
-                *  think it was worth putting in an extra variable to ensure
-                *  it since an extra "." won't hurt in those situations.
-                */
                *sanp++ = '.';
        }
        *sanp = '\0';
-}
-
-/* Works much like sanitize_path(), with these differences:  (1) a new buffer
- * is allocated for the sanitized path rather than modifying it in-place; (2)
- * a leading slash gets transformed into the rootdir value (which can be empty
- * or NULL if you just want the slash to get dropped); (3) no "reldir" can be
- * specified. */
-char *alloc_sanitize_path(const char *path, const char *rootdir)
-{
-       char *buf;
-       int rlen, plen = strlen(path);
-
-       if (*path == '/' && rootdir) {
-               rlen = strlen(rootdir);
-               if (rlen == 1)
-                       path++;
-       } else
-               rlen = 0;
-       if (!(buf = new_array(char, rlen + plen + 1)))
-               out_of_memory("alloc_sanitize_path");
-       if (rlen)
-               memcpy(buf, rootdir, rlen);
-       memcpy(buf + rlen, path, plen + 1);
-
-       if (rlen > 1)
-               rlen++;
-       sanitize_path(buf + rlen, NULL);
-       if (rlen && buf[rlen] == '.' && buf[rlen+1] == '\0') {
-               if (rlen > 1)
-                       rlen--;
-               buf[rlen] = '\0';
-       }
 
-       return buf;
+       return dest;
 }
 
 char curr_dir[MAXPATHLEN];
@@ -879,7 +851,7 @@ int push_dir(char *dir)
                curr_dir_len += len;
        }
 
-       clean_fname(curr_dir);
+       curr_dir_len = clean_fname(curr_dir);
 
        return 1;
 }
@@ -902,17 +874,20 @@ int pop_dir(char *dir)
 
 /**
  * Return the filename, turning any newlines into '?'s.  This ensures that
- * outputting it on a line of its own cannot generate an empty line.
+ * outputting it on a line of its own cannot generate an empty line.  This
+ * function can handle only 2 names at a time!
  **/
 const char *safe_fname(const char *fname)
 {
-       static char fbuf[MAXPATHLEN];
+       static char fbuf1[MAXPATHLEN], fbuf2[MAXPATHLEN];
+       static char *fbuf = fbuf2;
        char *nl = strchr(fname, '\n');
 
        if (!nl)
                return fname;
 
-       strlcpy(fbuf, fname, sizeof fbuf);
+       fbuf = fbuf == fbuf1 ? fbuf2 : fbuf1;
+       strlcpy(fbuf, fname, MAXPATHLEN);
        nl = fbuf + (nl - (char *)fname);
        do {
                *nl = '?';
@@ -935,6 +910,7 @@ char *full_fname(const char *fn)
        if (result)
                free(result);
 
+       fn = safe_fname(fn);
        if (*fn == '/')
                p1 = p2 = "";
        else {
@@ -966,6 +942,69 @@ char *full_fname(const char *fn)
        return result;
 }
 
+static char partial_fname[MAXPATHLEN];
+
+char *partial_dir_fname(const char *fname)
+{
+       char *t = partial_fname;
+       int sz = sizeof partial_fname;
+       const char *fn;
+
+       if ((fn = strrchr(fname, '/')) != NULL) {
+               fn++;
+               if (*partial_dir != '/') {
+                       int len = fn - fname;
+                       strncpy(t, fname, len); /* safe */
+                       t += len;
+                       sz -= len;
+               }
+       } else
+               fn = fname;
+       if ((int)pathjoin(t, sz, partial_dir, fn) >= sz)
+               return NULL;
+       if (server_exclude_list.head
+           && check_exclude(&server_exclude_list, partial_fname, 0) < 0)
+               return NULL;
+
+       return partial_fname;
+}
+
+/* If no --partial-dir option was specified, we don't need to do anything
+ * (the partial-dir is essentially '.'), so just return success. */
+int handle_partial_dir(const char *fname, int create)
+{
+       char *fn, *dir;
+
+       if (fname != partial_fname)
+               return 1;
+       if (!create && *partial_dir == '/')
+               return 1;
+       if (!(fn = strrchr(partial_fname, '/')))
+               return 1;
+
+       *fn = '\0';
+       dir = partial_fname;
+       if (create) {
+               STRUCT_STAT st;
+#if SUPPORT_LINKS
+               int statret = do_lstat(dir, &st);
+#else
+               int statret = do_stat(dir, &st);
+#endif
+               if (statret == 0 && !S_ISDIR(st.st_mode)) {
+                       if (do_unlink(dir) < 0)
+                               return 0;
+                       statret = -1;
+               }
+               if (statret < 0 && do_mkdir(dir, 0700) < 0)
+                       return 0;
+       } else
+               do_rmdir(dir);
+       *fn = '/';
+
+       return 1;
+}
+
 /** We need to supply our own strcmp function for file list comparisons
    to ensure that signed/unsigned usage is consistent between machines. */
 int u_strcmp(const char *cs1, const char *cs2)