aboutsummaryrefslogtreecommitdiff
path: root/src/port/path.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/port/path.c')
-rw-r--r--src/port/path.c258
1 files changed, 205 insertions, 53 deletions
diff --git a/src/port/path.c b/src/port/path.c
index 69bb8fe40b7..ea1d8f6ff58 100644
--- a/src/port/path.c
+++ b/src/port/path.c
@@ -46,8 +46,9 @@
static void make_relative_path(char *ret_path, const char *target_path,
const char *bin_path, const char *my_exec_path);
-static void trim_directory(char *path);
+static char *trim_directory(char *path);
static void trim_trailing_separator(char *path);
+static char *append_subdir_to_path(char *path, char *subdir);
/*
@@ -222,13 +223,9 @@ join_path_components(char *ret_path,
strlcpy(ret_path, head, MAXPGPATH);
/*
- * Remove any leading "." in the tail component.
- *
- * Note: we used to try to remove ".." as well, but that's tricky to get
- * right; now we just leave it to be done by canonicalize_path() later.
+ * We used to try to simplify some cases involving "." and "..", but now
+ * we just leave that to be done by canonicalize_path() later.
*/
- while (tail[0] == '.' && IS_DIR_SEP(tail[1]))
- tail += 2;
if (*tail)
{
@@ -241,14 +238,27 @@ join_path_components(char *ret_path,
}
+/* State-machine states for canonicalize_path */
+typedef enum
+{
+ ABSOLUTE_PATH_INIT, /* Just past the leading '/' (and Windows
+ * drive name if any) of an absolute path */
+ ABSOLUTE_WITH_N_DEPTH, /* We collected 'pathdepth' directories in an
+ * absolute path */
+ RELATIVE_PATH_INIT, /* At start of a relative path */
+ RELATIVE_WITH_N_DEPTH, /* We collected 'pathdepth' directories in a
+ * relative path */
+ RELATIVE_WITH_PARENT_REF /* Relative path containing only double-dots */
+} canonicalize_state;
+
/*
* Clean up path by:
* o make Win32 path use Unix slashes
* o remove trailing quote on Win32
* o remove trailing slash
- * o remove duplicate adjacent separators
- * o remove trailing '.'
- * o process trailing '..' ourselves
+ * o remove duplicate (adjacent) separators
+ * o remove '.' (unless path reduces to only '.')
+ * o process '..' ourselves, removing it if possible
*/
void
canonicalize_path(char *path)
@@ -256,8 +266,11 @@ canonicalize_path(char *path)
char *p,
*to_p;
char *spath;
+ char *parsed;
+ char *unparse;
bool was_sep = false;
- int pending_strips;
+ canonicalize_state state;
+ int pathdepth = 0; /* counts collected regular directory names */
#ifdef WIN32
@@ -308,60 +321,173 @@ canonicalize_path(char *path)
*to_p = '\0';
/*
- * Remove any trailing uses of "." and process ".." ourselves
+ * Remove any uses of "." and process ".." ourselves
*
* Note that "/../.." should reduce to just "/", while "../.." has to be
- * kept as-is. In the latter case we put back mistakenly trimmed ".."
- * components below. Also note that we want a Windows drive spec to be
- * visible to trim_directory(), but it's not part of the logic that's
- * looking at the name components; hence distinction between path and
- * spath.
+ * kept as-is. Also note that we want a Windows drive spec to be visible
+ * to trim_directory(), but it's not part of the logic that's looking at
+ * the name components; hence distinction between path and spath.
+ *
+ * This loop overwrites the path in-place. This is safe since we'll never
+ * make the path longer. "unparse" points to where we are reading the
+ * path, "parse" to where we are writing.
*/
spath = skip_drive(path);
- pending_strips = 0;
- for (;;)
+ if (*spath == '\0')
+ return; /* empty path is returned as-is */
+
+ if (*spath == '/')
{
- int len = strlen(spath);
+ state = ABSOLUTE_PATH_INIT;
+ /* Skip the leading slash for absolute path */
+ parsed = unparse = (spath + 1);
+ }
+ else
+ {
+ state = RELATIVE_PATH_INIT;
+ parsed = unparse = spath;
+ }
- if (len >= 2 && strcmp(spath + len - 2, "/.") == 0)
- trim_directory(path);
- else if (strcmp(spath, ".") == 0)
+ while (*unparse != '\0')
+ {
+ char *unparse_next;
+ bool is_double_dot;
+
+ /* Split off this dir name, and set unparse_next to the next one */
+ unparse_next = unparse;
+ while (*unparse_next && *unparse_next != '/')
+ unparse_next++;
+ if (*unparse_next != '\0')
+ *unparse_next++ = '\0';
+
+ /* Identify type of this dir name */
+ if (strcmp(unparse, ".") == 0)
{
- /* Want to leave "." alone, but "./.." has to become ".." */
- if (pending_strips > 0)
- *spath = '\0';
- break;
+ /* We can ignore "." components in all cases */
+ unparse = unparse_next;
+ continue;
}
- else if ((len >= 3 && strcmp(spath + len - 3, "/..") == 0) ||
- strcmp(spath, "..") == 0)
+
+ if (strcmp(unparse, "..") == 0)
+ is_double_dot = true;
+ else
{
- trim_directory(path);
- pending_strips++;
+ /* adjacent separators were eliminated above */
+ Assert(*unparse != '\0');
+ is_double_dot = false;
}
- else if (pending_strips > 0 && *spath != '\0')
+
+ switch (state)
{
- /* trim a regular directory name canceled by ".." */
- trim_directory(path);
- pending_strips--;
- /* foo/.. should become ".", not empty */
- if (*spath == '\0')
- strcpy(spath, ".");
+ case ABSOLUTE_PATH_INIT:
+ /* We can ignore ".." immediately after / */
+ if (!is_double_dot)
+ {
+ /* Append first dir name (we already have leading slash) */
+ parsed = append_subdir_to_path(parsed, unparse);
+ state = ABSOLUTE_WITH_N_DEPTH;
+ pathdepth++;
+ }
+ break;
+ case ABSOLUTE_WITH_N_DEPTH:
+ if (is_double_dot)
+ {
+ /* Remove last parsed dir */
+ /* (trim_directory won't remove the leading slash) */
+ *parsed = '\0';
+ parsed = trim_directory(path);
+ if (--pathdepth == 0)
+ state = ABSOLUTE_PATH_INIT;
+ }
+ else
+ {
+ /* Append normal dir */
+ *parsed++ = '/';
+ parsed = append_subdir_to_path(parsed, unparse);
+ pathdepth++;
+ }
+ break;
+ case RELATIVE_PATH_INIT:
+ if (is_double_dot)
+ {
+ /* Append irreducible double-dot (..) */
+ parsed = append_subdir_to_path(parsed, unparse);
+ state = RELATIVE_WITH_PARENT_REF;
+ }
+ else
+ {
+ /* Append normal dir */
+ parsed = append_subdir_to_path(parsed, unparse);
+ state = RELATIVE_WITH_N_DEPTH;
+ pathdepth++;
+ }
+ break;
+ case RELATIVE_WITH_N_DEPTH:
+ if (is_double_dot)
+ {
+ /* Remove last parsed dir */
+ *parsed = '\0';
+ parsed = trim_directory(path);
+ if (--pathdepth == 0)
+ {
+ /*
+ * If the output path is now empty, we're back to the
+ * INIT state. However, we could have processed a
+ * path like "../dir/.." and now be down to "..", in
+ * which case enter the correct state for that.
+ */
+ if (parsed == spath)
+ state = RELATIVE_PATH_INIT;
+ else
+ state = RELATIVE_WITH_PARENT_REF;
+ }
+ }
+ else
+ {
+ /* Append normal dir */
+ *parsed++ = '/';
+ parsed = append_subdir_to_path(parsed, unparse);
+ pathdepth++;
+ }
+ break;
+ case RELATIVE_WITH_PARENT_REF:
+ if (is_double_dot)
+ {
+ /* Append next irreducible double-dot (..) */
+ *parsed++ = '/';
+ parsed = append_subdir_to_path(parsed, unparse);
+ }
+ else
+ {
+ /* Append normal dir */
+ *parsed++ = '/';
+ parsed = append_subdir_to_path(parsed, unparse);
+
+ /*
+ * We can now start counting normal dirs. But if later
+ * double-dots make us remove this dir again, we'd better
+ * revert to RELATIVE_WITH_PARENT_REF not INIT state.
+ */
+ state = RELATIVE_WITH_N_DEPTH;
+ pathdepth = 1;
+ }
+ break;
}
- else
- break;
- }
- if (pending_strips > 0)
- {
- /*
- * We could only get here if path is now totally empty (other than a
- * possible drive specifier on Windows). We have to put back one or
- * more ".."'s that we took off.
- */
- while (--pending_strips > 0)
- strcat(path, "../");
- strcat(path, "..");
+ unparse = unparse_next;
}
+
+ /*
+ * If our output path is empty at this point, insert ".". We don't want
+ * to do this any earlier because it'd result in an extra dot in corner
+ * cases such as "../dir/..". Since we rejected the wholly-empty-path
+ * case above, there is certainly room.
+ */
+ if (parsed == spath)
+ *parsed++ = '.';
+
+ /* And finally, ensure the output path is nul-terminated. */
+ *parsed = '\0';
}
/*
@@ -865,8 +991,11 @@ get_parent_directory(char *path)
* Trim trailing directory from path, that is, remove any trailing slashes,
* the last pathname component, and the slash just ahead of it --- but never
* remove a leading slash.
+ *
+ * For the convenience of canonicalize_path, the path's new end location
+ * is returned.
*/
-static void
+static char *
trim_directory(char *path)
{
char *p;
@@ -874,7 +1003,7 @@ trim_directory(char *path)
path = skip_drive(path);
if (path[0] == '\0')
- return;
+ return path;
/* back up over trailing slash(es) */
for (p = path + strlen(path) - 1; IS_DIR_SEP(*p) && p > path; p--)
@@ -889,6 +1018,7 @@ trim_directory(char *path)
if (p == path && IS_DIR_SEP(*p))
p++;
*p = '\0';
+ return p;
}
@@ -908,3 +1038,25 @@ trim_trailing_separator(char *path)
for (p--; p > path && IS_DIR_SEP(*p); p--)
*p = '\0';
}
+
+/*
+ * append_subdir_to_path
+ *
+ * Append the currently-considered subdirectory name to the output
+ * path in canonicalize_path. Return the new end location of the
+ * output path.
+ *
+ * Since canonicalize_path updates the path in-place, we must use
+ * memmove not memcpy, and we don't yet terminate the path with '\0'.
+ */
+static char *
+append_subdir_to_path(char *path, char *subdir)
+{
+ size_t len = strlen(subdir);
+
+ /* No need to copy data if path and subdir are the same. */
+ if (path != subdir)
+ memmove(path, subdir, len);
+
+ return path + len;
+}