Re: drop tablespace failed when location contains .. on win32

Поиск
Список
Период
Сортировка
От Tom Lane
Тема Re: drop tablespace failed when location contains .. on win32
Дата
Msg-id 462031.1643579403@sss.pgh.pa.us
обсуждение исходный текст
Ответ на Re: drop tablespace failed when location contains .. on win32  (Michael Paquier <michael@paquier.xyz>)
Ответы Re: drop tablespace failed when location contains .. on win32  (Michael Paquier <michael@paquier.xyz>)
Re: drop tablespace failed when location contains .. on win32  (Andrew Dunstan <andrew@dunslane.net>)
Список pgsql-hackers
Michael Paquier <michael@paquier.xyz> writes:
> In order to make the tests cheap, there is no need to have a separate
> module in src/test/modules/ as your patch 0002 is doing.  Instead, you
> should move the C code of your SQL function test_canonicalize_path()
> to src/test/regress/regress.c, then add some tests in
> src/test/regress/sql/, with a SQL function created in the test script
> that feeds from what would be added to regress.so.

Here's a revised patch version that does it like that.  I also
reviewed and simplified the canonicalize_path logic.  I think
this is committable.

(I suspect that adminpack's checks for unsafe file names could
now be simplified substantially, because many of the corner cases
it worries about are no longer possible, as evidenced by the change
in error message there.  I've not pursued that, however.)

            regards, tom lane

diff --git a/contrib/adminpack/expected/adminpack.out b/contrib/adminpack/expected/adminpack.out
index edf3ebfcba..76aafe6316 100644
--- a/contrib/adminpack/expected/adminpack.out
+++ b/contrib/adminpack/expected/adminpack.out
@@ -51,7 +51,7 @@ SELECT pg_file_write(current_setting('data_directory') || '/test_file4', 'test4'
 (1 row)

 SELECT pg_file_write(current_setting('data_directory') || '/../test_file4', 'test4', false);
-ERROR:  reference to parent directory ("..") not allowed
+ERROR:  absolute path not allowed
 RESET ROLE;
 REVOKE EXECUTE ON FUNCTION pg_file_write(text,text,bool) FROM regress_user1;
 REVOKE pg_read_all_settings FROM regress_user1;
diff --git a/src/port/path.c b/src/port/path.c
index 69bb8fe40b..e8b0364b13 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
  */
 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,172 @@ 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 (IS_DIR_SEP(*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 && !IS_DIR_SEP(*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)
+                {
+                    /* 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 +990,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 +1002,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 +1017,7 @@ trim_directory(char *path)
     if (p == path && IS_DIR_SEP(*p))
         p++;
     *p = '\0';
+    return p;
 }


@@ -908,3 +1037,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;
+}
diff --git a/src/test/regress/expected/create_function_1.out b/src/test/regress/expected/create_function_1.out
index 5345ed0840..6141b7060b 100644
--- a/src/test/regress/expected/create_function_1.out
+++ b/src/test/regress/expected/create_function_1.out
@@ -28,3 +28,7 @@ CREATE FUNCTION int44out(city_budget)
    AS :'regresslib'
    LANGUAGE C STRICT IMMUTABLE;
 NOTICE:  argument type city_budget is only a shell
+CREATE FUNCTION test_canonicalize_path(text)
+   RETURNS text
+   AS :'regresslib'
+   LANGUAGE C STRICT IMMUTABLE;
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index e2c77d0ac8..cce92b014f 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -133,6 +133,129 @@ ERROR:  function num_nulls() does not exist
 LINE 1: SELECT num_nulls();
                ^
 HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+--
+-- canonicalize_path()
+--
+SELECT * FROM test_canonicalize_path('/');
+ test_canonicalize_path
+------------------------
+ /
+(1 row)
+
+SELECT * FROM test_canonicalize_path('/./abc/def/');
+ test_canonicalize_path
+------------------------
+ /abc/def
+(1 row)
+
+SELECT * FROM test_canonicalize_path('/./../abc/def');
+ test_canonicalize_path
+------------------------
+ /abc/def
+(1 row)
+
+SELECT * FROM test_canonicalize_path('/./../../abc/def/');
+ test_canonicalize_path
+------------------------
+ /abc/def
+(1 row)
+
+SELECT * FROM test_canonicalize_path('/abc/.././def/ghi');
+ test_canonicalize_path
+------------------------
+ /def/ghi
+(1 row)
+
+SELECT * FROM test_canonicalize_path('/abc/./../def/ghi//');
+ test_canonicalize_path
+------------------------
+ /def/ghi
+(1 row)
+
+SELECT * FROM test_canonicalize_path('/abc/def/../../../../ghi/jkl');
+ test_canonicalize_path
+------------------------
+ /ghi/jkl
+(1 row)
+
+SELECT * FROM test_canonicalize_path('.');
+ test_canonicalize_path
+------------------------
+ .
+(1 row)
+
+SELECT * FROM test_canonicalize_path('./');
+ test_canonicalize_path
+------------------------
+ .
+(1 row)
+
+SELECT * FROM test_canonicalize_path('./abc/..');
+ test_canonicalize_path
+------------------------
+ .
+(1 row)
+
+SELECT * FROM test_canonicalize_path('abc/../');
+ test_canonicalize_path
+------------------------
+ .
+(1 row)
+
+SELECT * FROM test_canonicalize_path('abc/../def');
+ test_canonicalize_path
+------------------------
+ def
+(1 row)
+
+SELECT * FROM test_canonicalize_path('..');
+ test_canonicalize_path
+------------------------
+ ..
+(1 row)
+
+SELECT * FROM test_canonicalize_path('../abc/def');
+ test_canonicalize_path
+------------------------
+ ../abc/def
+(1 row)
+
+SELECT * FROM test_canonicalize_path('../abc/..');
+ test_canonicalize_path
+------------------------
+ ..
+(1 row)
+
+SELECT * FROM test_canonicalize_path('../abc/../def');
+ test_canonicalize_path
+------------------------
+ ../def
+(1 row)
+
+SELECT * FROM test_canonicalize_path('../abc/../../def/ghi');
+ test_canonicalize_path
+------------------------
+ ../../def/ghi
+(1 row)
+
+SELECT * FROM test_canonicalize_path('./abc/./def/.');
+ test_canonicalize_path
+------------------------
+ abc/def
+(1 row)
+
+SELECT * FROM test_canonicalize_path('./abc/././def/.');
+ test_canonicalize_path
+------------------------
+ abc/def
+(1 row)
+
+SELECT * FROM test_canonicalize_path('./abc/./def/.././ghi/../../../jkl/mno');
+ test_canonicalize_path
+------------------------
+ ../jkl/mno
+(1 row)
+
 --
 -- pg_log_backend_memory_contexts()
 --
diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c
index eefbd0f0df..0802fb9136 100644
--- a/src/test/regress/regress.c
+++ b/src/test/regress/regress.c
@@ -531,6 +531,16 @@ int44out(PG_FUNCTION_ARGS)
     PG_RETURN_CSTRING(result);
 }

+PG_FUNCTION_INFO_V1(test_canonicalize_path);
+Datum
+test_canonicalize_path(PG_FUNCTION_ARGS)
+{
+    char       *path = text_to_cstring(PG_GETARG_TEXT_PP(0));
+
+    canonicalize_path(path);
+    PG_RETURN_TEXT_P(cstring_to_text(path));
+}
+
 PG_FUNCTION_INFO_V1(make_tuple_indirect);
 Datum
 make_tuple_indirect(PG_FUNCTION_ARGS)
diff --git a/src/test/regress/sql/create_function_1.sql b/src/test/regress/sql/create_function_1.sql
index 4170b16fe6..34cc7c6efc 100644
--- a/src/test/regress/sql/create_function_1.sql
+++ b/src/test/regress/sql/create_function_1.sql
@@ -29,3 +29,8 @@ CREATE FUNCTION int44out(city_budget)
    RETURNS cstring
    AS :'regresslib'
    LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION test_canonicalize_path(text)
+   RETURNS text
+   AS :'regresslib'
+   LANGUAGE C STRICT IMMUTABLE;
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index 1159f6b585..448114808d 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -30,6 +30,31 @@ SELECT num_nulls(VARIADIC '{}'::int[]);
 SELECT num_nonnulls();
 SELECT num_nulls();

+--
+-- canonicalize_path()
+--
+
+SELECT * FROM test_canonicalize_path('/');
+SELECT * FROM test_canonicalize_path('/./abc/def/');
+SELECT * FROM test_canonicalize_path('/./../abc/def');
+SELECT * FROM test_canonicalize_path('/./../../abc/def/');
+SELECT * FROM test_canonicalize_path('/abc/.././def/ghi');
+SELECT * FROM test_canonicalize_path('/abc/./../def/ghi//');
+SELECT * FROM test_canonicalize_path('/abc/def/../../../../ghi/jkl');
+SELECT * FROM test_canonicalize_path('.');
+SELECT * FROM test_canonicalize_path('./');
+SELECT * FROM test_canonicalize_path('./abc/..');
+SELECT * FROM test_canonicalize_path('abc/../');
+SELECT * FROM test_canonicalize_path('abc/../def');
+SELECT * FROM test_canonicalize_path('..');
+SELECT * FROM test_canonicalize_path('../abc/def');
+SELECT * FROM test_canonicalize_path('../abc/..');
+SELECT * FROM test_canonicalize_path('../abc/../def');
+SELECT * FROM test_canonicalize_path('../abc/../../def/ghi');
+SELECT * FROM test_canonicalize_path('./abc/./def/.');
+SELECT * FROM test_canonicalize_path('./abc/././def/.');
+SELECT * FROM test_canonicalize_path('./abc/./def/.././ghi/../../../jkl/mno');
+
 --
 -- pg_log_backend_memory_contexts()
 --

В списке pgsql-hackers по дате отправления:

Предыдущее
От: Andres Freund
Дата:
Сообщение: Re: plperl on windows
Следующее
От: Andres Freund
Дата:
Сообщение: Re: plperl on windows