bug-gnulib
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: renameat


From: Eric Blake
Subject: Re: renameat
Date: Fri, 02 Oct 2009 06:06:03 -0600
User-agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.23) Gecko/20090812 Thunderbird/2.0.0.23 Mnenhy/0.7.6.666

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

According to Jim Meyering on 10/1/2009 1:28 PM:
>> Here's the current state of the series, finally ready for review.  If we
>> check it in as-is, then coreutils will have everything it needs to ensure
>> consistent behavior of 'mv -T a b/' on every platform it already supports
>> except for cygwin 1.5 (which has never been a show-stopper for coreutils
> 
> I've skimmed through all of that.
> Considering I spent only 20 minutes, I can't have
> done it justice, but didn't spot any problems, either.

I've completed these three additional patches to fix the outstanding
issues, and am now doing a sanity check that my rebasing worked before
pushing to savannah.  Expect it soon.

- --
Don't work too hard, make some time for fun as well!

Eric Blake             address@hidden
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Public key at home.comcast.net/~ericblake/eblake.gpg
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkrF7KsACgkQ84KuGfSFAYCiGwCgg3QugG63xzO7xIA6oI75dFs4
F5cAn2BNyPeBEiXrItydYMCW01SIunqb
=AvdX
-----END PGP SIGNATURE-----
>From 0eb9ddbf689cb195763848c52507685ebb07834d Mon Sep 17 00:00:00 2001
From: Eric Blake <address@hidden>
Date: Thu, 1 Oct 2009 11:57:47 -0600
Subject: [PATCH 1/3] rename: fix another cygwin 1.5 bug

Cygwin 1.5 sometimes, but not always, failed on rename("dir1","dir2")
when "dir2" exists.  Beef up the m4 tests to more reliably
detect at least one of cygwin's bugs, in spite of not knowing
why the rename only failed sporadically (thankfully, cygwin 1.7
does not have the bug).  Meanwhile, although NetBSD needs to
share the hard link workarounds, it does not need the trailing
dot and directory workarounds, so split the m4 test into two
separate feature checks.

* m4/rename.m4 (gl_FUNC_RENAME): Split cygwin bugs into two
checks.
* lib/rename.c (rpl_rename): Don't penalize NetBSD with
unnecessary cygwin workarounds.  Also work around bug with moving
full directory onto an empty one.
* modules/rename (Depends-on): Add canonicalize-lgpl, rmdir.

Signed-off-by: Eric Blake <address@hidden>
---
 ChangeLog      |    8 +++++++
 lib/rename.c   |   54 ++++++++++++++++++++++++++++++++++++++++++++------
 m4/rename.m4   |   59 +++++++++++++++++++++++++++++++++++++++----------------
 modules/rename |    2 +
 4 files changed, 99 insertions(+), 24 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index db0c482..dacff6f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,13 @@
 2009-10-01  Eric Blake  <address@hidden>

+       rename: fix another cygwin 1.5 bug
+       * m4/rename.m4 (gl_FUNC_RENAME): Split cygwin bugs into two
+       checks.
+       * lib/rename.c (rpl_rename): Don't penalize NetBSD with
+       unnecessary cygwin workarounds.  Also work around bug with moving
+       full directory onto an empty one.
+       * modules/rename (Depends-on): Add canonicalize-lgpl, rmdir.
+
        renameat: new module
        * modules/renameat: New file.
        * lib/renameat.c (renameat): Likewise.
diff --git a/lib/rename.c b/lib/rename.c
index a59328e..b6a5dab 100644
--- a/lib/rename.c
+++ b/lib/rename.c
@@ -139,6 +139,7 @@ rpl_rename (char const *src, char const *dst)
 # include <stdlib.h>
 # include <string.h>
 # include <sys/stat.h>
+# include <unistd.h>

 # include "dirname.h"
 # include "same-inode.h"
@@ -154,6 +155,7 @@ rpl_rename (char const *src, char const *dst)
   char *dst_temp = (char *) dst;
   bool src_slash;
   bool dst_slash;
+  bool dst_exists;
   int ret_val = -1;
   int rename_errno = ENOTDIR;
   struct stat src_st;
@@ -190,13 +192,13 @@ rpl_rename (char const *src, char const *dst)
   src_slash = src[src_len - 1] == '/';
   dst_slash = dst[dst_len - 1] == '/';

-# if !RENAME_DEST_EXISTS_BUG
+# if !RENAME_HARD_LINK_BUG && !RENAME_DEST_EXISTS_BUG
   /* If there are no trailing slashes, then trust the native
      implementation unless we also suspect issues with hard link
-     detection.  */
+     detection or file/directory conflicts.  */
   if (!src_slash && !dst_slash)
     return rename (src, dst);
-# endif /* !RENAME_DEST_EXISTS_BUG */
+# endif /* !RENAME_HARD_LINK_BUG && !RENAME_DEST_EXISTS_BUG */

   /* Presence of a trailing slash requires directory semantics.  If
      the source does not exist, or if the destination cannot be turned
@@ -208,6 +210,7 @@ rpl_rename (char const *src, char const *dst)
     {
       if (errno != ENOENT || (!S_ISDIR (src_st.st_mode) && dst_slash))
         return -1;
+      dst_exists = false;
     }
   else
     {
@@ -216,13 +219,15 @@ rpl_rename (char const *src, char const *dst)
          errno = S_ISDIR (dst_st.st_mode) ? EISDIR : ENOTDIR;
          return -1;
        }
-# if RENAME_DEST_EXISTS_BUG
+# if RENAME_HARD_LINK_BUG
       if (SAME_INODE (src_st, dst_st))
        return 0;
-# endif /* RENAME_DEST_EXISTS_BUG */
+# endif /* RENAME_HARD_LINK_BUG */
+      dst_exists = true;
     }

-# if RENAME_TRAILING_SLASH_SOURCE_BUG || RENAME_DEST_EXISTS_BUG
+# if (RENAME_TRAILING_SLASH_SOURCE_BUG || RENAME_DEST_EXISTS_BUG        \
+      || RENAME_HARD_LINK_BUG)
   /* If the only bug was that a trailing slash was allowed on a
      non-existing file destination, as in Solaris 10, then we've
      already covered that situation.  But if there is any problem with
@@ -281,7 +286,42 @@ rpl_rename (char const *src, char const *dst)
       else if (S_ISLNK (dst_st.st_mode))
         goto out;
     }
-# endif /* RENAME_TRAILING_SLASH_SOURCE_BUG || RENAME_DEST_EXISTS_BUG */
+# endif /* RENAME_TRAILING_SLASH_SOURCE_BUG || RENAME_DEST_EXISTS_BUG
+           || RENAME_HARD_LINK_BUG */
+
+# if RENAME_DEST_EXISTS_BUG
+  /* Cygwin 1.5 sometimes behaves oddly when moving a non-empty
+     directory on top of an empty one (the old directory name can
+     reappear if the new directory tree is removed).  Work around this
+     by removing the target first, but don't remove the target if it
+     is a subdirectory of the source.  */
+  if (dst_exists && S_ISDIR (dst_st.st_mode))
+    {
+      if (src_temp != src)
+        free (src_temp);
+      src_temp = canonicalize_file_name (src);
+      if (dst_temp != dst)
+        free (dst_temp);
+      dst_temp = canonicalize_file_name (dst);
+      if (!src_temp || !dst_temp)
+        {
+          rename_errno = ENOMEM;
+          goto out;
+        }
+      src_len = strlen (src_temp);
+      if (strncmp (src_temp, dst_temp, src_len) == 0
+          && dst_temp[src_len] == '/')
+        {
+          rename_errno = EINVAL;
+          goto out;
+        }
+      if (rmdir (dst))
+        {
+          rename_errno = errno;
+          goto out;
+        }
+    }
+# endif /* RENAME_DEST_EXISTS_BUG */

   ret_val = rename (src_temp, dst_temp);
   rename_errno = errno;
diff --git a/m4/rename.m4 b/m4/rename.m4
index db08dd5..48ba0e7 100644
--- a/m4/rename.m4
+++ b/m4/rename.m4
@@ -1,4 +1,4 @@
-# serial 19
+# serial 20

 # Copyright (C) 2001, 2003, 2005, 2006, 2009 Free Software Foundation, Inc.
 # This file is free software; the Free Software Foundation
@@ -76,7 +76,39 @@ AC_DEFUN([gl_FUNC_RENAME],
        argument, such as on Solaris 9 or cygwin 1.5.])
   fi

-  dnl NetBSD 1.6 mistakenly reduces hard link count on rename("h1,"h2").
+  dnl NetBSD 1.6 and cygwin 1.5.x mistakenly reduce hard link count
+  dnl on rename("h1,"h2").
+  dnl This bug requires stat'ting targets prior to attempting rename.
+  AC_CACHE_CHECK([whether rename manages hard links correctly],
+    [gl_cv_func_rename_link_works],
+    [rm -rf conftest.f conftest.f1
+    if touch conftest.f && ln conftest.f conftest.f1 &&
+        set x `ls -i conftest.f conftest.f1` && test "$2" = "$4"; then
+      AC_RUN_IFELSE([AC_LANG_PROGRAM([[
+#       include <stdio.h>
+#       include <stdlib.h>
+#       include <unistd.h>
+]], [if (rename ("conftest.f", "conftest.f1")) return 1;
+     if (unlink ("conftest.f1")) return 2;
+     if (rename ("conftest.f", "conftest.f")) return 3;
+     if (rename ("conftest.f1", "conftest.f1") == 0) return 4;])],
+        [gl_cv_func_rename_link_works=yes],
+        [gl_cv_func_rename_link_works=no],
+        dnl When crosscompiling, assume rename is broken.
+        [gl_cv_func_rename_link_works="guessing no"])
+    else
+      gl_cv_func_rename_link_works="guessing no"
+    fi
+    rm -rf conftest.f conftest.f1
+  ])
+  if test "x$gl_cv_func_rename_link_works" != xyes; then
+    AC_LIBOBJ([rename])
+    REPLACE_RENAME=1
+    AC_DEFINE([RENAME_HARD_LINK_BUG], [1],
+      [Define if rename fails to leave hard links alone, as on NetBSD 1.6
+       or Cygwin 1.5.])
+  fi
+
   dnl Cygwin 1.5.x mistakenly allows rename("dir","file").
   dnl mingw mistakenly forbids rename("dir1","dir2").
   dnl These bugs require stripping trailing slash to avoid corrupting
@@ -86,29 +118,22 @@ AC_DEFUN([gl_FUNC_RENAME],
     [rm -rf conftest.f conftest.d1 conftest.d2
     touch conftest.f && mkdir conftest.d1 conftest.d2 ||
       AC_MSG_ERROR([cannot create temporary files])
-    if ln conftest.f conftest.f1 && set x `ls -i conftest.f conftest.f1` &&
-        test "$2" = "$4"; then
-      AC_RUN_IFELSE([AC_LANG_PROGRAM([[
+    AC_RUN_IFELSE([AC_LANG_PROGRAM([[
 #       include <stdio.h>
 #       include <stdlib.h>
 ]], [if (rename ("conftest.d1", "conftest.d2") != 0) return 1;
-     if (rename ("conftest.d2", "conftest.f") == 0) return 2;
-     if (rename ("conftest.f", "conftest.f1")
-         || rename ("conftest.f", "conftest.f")) return 3;])],
-        [gl_cv_func_rename_dest_works=yes],
-        [gl_cv_func_rename_dest_works=no],
-        dnl When crosscompiling, assume rename is broken.
-        [gl_cv_func_rename_dest_works="guessing no"])
-    else
-      gl_cv_func_rename_dest_works="guessing no"
-    fi
-    rm -rf conftest.f conftest.f1 conftest.d1 conftest.d2
+     if (rename ("conftest.d2", "conftest.f") == 0) return 2;])],
+      [gl_cv_func_rename_dest_works=yes],
+      [gl_cv_func_rename_dest_works=no],
+      dnl When crosscompiling, assume rename is broken.
+      [gl_cv_func_rename_dest_works="guessing no"])
+    rm -rf conftest.f conftest.d1 conftest.d2
   ])
   if test "x$gl_cv_func_rename_dest_works" != xyes; then
     AC_LIBOBJ([rename])
     REPLACE_RENAME=1
     AC_DEFINE([RENAME_DEST_EXISTS_BUG], [1],
       [Define if rename does not work when the destination file exists,
-       as with NetBSD 1.6 on hard links, or Windows on directories.])
+       as on Cygwin 1.5 or Windows.])
   fi
 ])
diff --git a/modules/rename b/modules/rename
index 09baa83..fbd081c 100644
--- a/modules/rename
+++ b/modules/rename
@@ -6,8 +6,10 @@ lib/rename.c
 m4/rename.m4

 Depends-on:
+canonicalize-lgpl
 dirname
 lstat
+rmdir
 same-inode
 stdbool
 stdio
-- 
1.6.5.rc1


>From 984235b49cd5dd2f2bcfe26a4988c1c8a2253b97 Mon Sep 17 00:00:00 2001
From: Eric Blake <address@hidden>
Date: Thu, 1 Oct 2009 15:31:32 -0600
Subject: [PATCH 2/3] renameat: fix Solaris bugs

renameat(fd,"file",fd,"name/") failed, just like rename.

* m4/renameat.m4 (gl_FUNC_RENAMEAT): Replace renameat if rename
needed fixing.
* m4/stdio_h.m4 (gl_STDIO_H_DEFAULTS): New witness.
* modules/stdio (Makefile.am): Substitute it.
* lib/stdio.in.h (renameat): Declare replacement.
* lib/renameat.c (rpl_renameat): Implement fix.

Signed-off-by: Eric Blake <address@hidden>
---
 ChangeLog      |    8 ++++
 lib/renameat.c |  124 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 lib/stdio.in.h |    6 ++-
 m4/renameat.m4 |    7 +++-
 m4/stdio_h.m4  |    1 +
 modules/stdio  |    1 +
 6 files changed, 144 insertions(+), 3 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index dacff6f..dd5378d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,13 @@
 2009-10-01  Eric Blake  <address@hidden>

+       renameat: fix Solaris bugs
+       * m4/renameat.m4 (gl_FUNC_RENAMEAT): Replace renameat if rename
+       needed fixing.
+       * m4/stdio_h.m4 (gl_STDIO_H_DEFAULTS): New witness.
+       * modules/stdio (Makefile.am): Substitute it.
+       * lib/stdio.in.h (renameat): Declare replacement.
+       * lib/renameat.c (rpl_renameat): Implement fix.
+
        rename: fix another cygwin 1.5 bug
        * m4/rename.m4 (gl_FUNC_RENAME): Split cygwin bugs into two
        checks.
diff --git a/lib/renameat.c b/lib/renameat.c
index 244ee38..375dcf8 100644
--- a/lib/renameat.c
+++ b/lib/renameat.c
@@ -20,7 +20,127 @@

 #include <stdio.h>

-#include "openat-priv.h"
+#if HAVE_RENAMEAT
+
+# include <errno.h>
+# include <stdbool.h>
+# include <stdlib.h>
+# include <string.h>
+# include <sys/stat.h>
+
+# include "dirname.h"
+# include "openat.h"
+
+# undef renameat
+
+/* renameat does not honor trailing / on Solaris 10.  Solve it in a
+   similar manner to rename.  No need to worry about bugs not present
+   on Solaris, since all other systems either lack renameat or honor
+   trailing slash correctly.  */
+
+int
+rpl_renameat (int fd1, char const *src, int fd2, char const *dst)
+{
+  size_t src_len = strlen (src);
+  size_t dst_len = strlen (dst);
+  char *src_temp = (char *) src;
+  char *dst_temp = (char *) dst;
+  bool src_slash;
+  bool dst_slash;
+  int ret_val = -1;
+  int rename_errno = ENOTDIR;
+  struct stat src_st;
+  struct stat dst_st;
+
+  /* Let strace see any ENOENT failure.  */
+  if (!src_len || !dst_len)
+    return renameat (fd1, src, fd2, dst);
+
+  src_slash = src[src_len - 1] == '/';
+  dst_slash = dst[dst_len - 1] == '/';
+  if (!src_slash && !dst_slash)
+    return renameat (fd1, src, fd2, dst);
+
+  /* Presence of a trailing slash requires directory semantics.  If
+     the source does not exist, or if the destination cannot be turned
+     into a directory, give up now.  Otherwise, strip trailing slashes
+     before calling rename.  */
+  if (lstatat (fd1, src, &src_st))
+    return -1;
+  if (lstatat (fd2, dst, &dst_st))
+    {
+      if (errno != ENOENT || !S_ISDIR (src_st.st_mode))
+        return -1;
+    }
+  else if (!S_ISDIR (dst_st.st_mode))
+    {
+      errno = ENOTDIR;
+      return -1;
+    }
+  else if (!S_ISDIR (src_st.st_mode))
+    {
+      errno = EISDIR;
+      return -1;
+    }
+
+# if RENAME_TRAILING_SLASH_SOURCE_BUG
+  /* See the lengthy comment in rename.c why Solaris 9 is forced to
+     GNU behavior, while Solaris 10 is left with POSIX behavior,
+     regarding symlinks with trailing slash.  */
+  if (src_slash)
+    {
+      src_temp = strdup (src);
+      if (!src_temp)
+        {
+          /* Rather than rely on strdup-posix, we set errno ourselves.  */
+          rename_errno = ENOMEM;
+          goto out;
+        }
+      strip_trailing_slashes (src_temp);
+      if (lstatat (fd1, src_temp, &src_st))
+        {
+          rename_errno = errno;
+          goto out;
+        }
+      if (S_ISLNK (src_st.st_mode))
+        goto out;
+    }
+  if (dst_slash)
+    {
+      dst_temp = strdup (dst);
+      if (!dst_temp)
+        {
+          rename_errno = ENOMEM;
+          goto out;
+        }
+      strip_trailing_slashes (dst_temp);
+      if (lstatat (fd2, dst_temp, &dst_st))
+        {
+          if (errno != ENOENT)
+            {
+              rename_errno = errno;
+              goto out;
+            }
+        }
+      else if (S_ISLNK (dst_st.st_mode))
+        goto out;
+    }
+# endif /* RENAME_TRAILING_SLASH_SOURCE_BUG */
+
+  ret_val = renameat (fd1, src_temp, fd2, dst_temp);
+  rename_errno = errno;
+ out:
+  if (src_temp != src)
+    free (src_temp);
+  if (dst_temp != dst)
+    free (dst_temp);
+  errno = rename_errno;
+  return ret_val;
+}
+
+#else /* !HAVE_RENAMEAT */
+
+# include "openat-priv.h"

 /* Rename FILE1, in the directory open on descriptor FD1, to FILE2, in
    the directory open on descriptor FD2.  If possible, do it without
@@ -33,3 +153,5 @@ renameat (int fd1, char const *file1, int fd2, char const 
*file2)
 {
   return at_func2 (fd1, file1, fd2, file2, rename);
 }
+
+#endif /* !HAVE_RENAMEAT */
diff --git a/lib/stdio.in.h b/lib/stdio.in.h
index 3777e85..7a0bc12 100644
--- a/lib/stdio.in.h
+++ b/lib/stdio.in.h
@@ -445,7 +445,11 @@ extern int rename (const char *old, const char *new);
 #endif

 #if @GNULIB_RENAMEAT@
-# if address@hidden@
+# if @REPLACE_RENAMEAT@
+#  undef renameat
+#  define renameat rpl_renameat
+# endif
+# if address@hidden@ || @REPLACE_RENAMEAT@
 extern int renameat (int fd1, char const *file1, int fd2, char const *file2);
 # endif
 #elif defined GNULIB_POSIXCHECK
diff --git a/m4/renameat.m4 b/m4/renameat.m4
index bda7660..6dc7ff3 100644
--- a/m4/renameat.m4
+++ b/m4/renameat.m4
@@ -1,4 +1,4 @@
-# serial 1
+# serial 2
 # See if we need to provide renameat replacement.

 dnl Copyright (C) 2009 Free Software Foundation, Inc.
@@ -19,5 +19,10 @@ AC_DEFUN([gl_FUNC_RENAMEAT],
     HAVE_RENAMEAT=0
     AC_LIBOBJ([renameat])
     AC_LIBOBJ([at-func2])
+  elif test $REPLACE_RENAME = 1; then
+    dnl Solaris 9 and 10 have the same bugs in renameat as in rename.
+    REPLACE_RENAMEAT=1
+    AC_LIBOBJ([renameat])
+    AC_LIBOBJ([at-func2])
   fi
 ])
diff --git a/m4/stdio_h.m4 b/m4/stdio_h.m4
index 6f8c035..b94d267 100644
--- a/m4/stdio_h.m4
+++ b/m4/stdio_h.m4
@@ -112,6 +112,7 @@ AC_DEFUN([gl_STDIO_H_DEFAULTS],
   REPLACE_PRINTF=0;              AC_SUBST([REPLACE_PRINTF])
   REPLACE_REMOVE=0;              AC_SUBST([REPLACE_REMOVE])
   REPLACE_RENAME=0;              AC_SUBST([REPLACE_RENAME])
+  REPLACE_RENAMEAT=0;            AC_SUBST([REPLACE_RENAMEAT])
   REPLACE_SNPRINTF=0;            AC_SUBST([REPLACE_SNPRINTF])
   REPLACE_SPRINTF=0;             AC_SUBST([REPLACE_SPRINTF])
   REPLACE_STDIO_WRITE_FUNCS=0;   AC_SUBST([REPLACE_STDIO_WRITE_FUNCS])
diff --git a/modules/stdio b/modules/stdio
index 8f75e19..8b6e050 100644
--- a/modules/stdio
+++ b/modules/stdio
@@ -94,6 +94,7 @@ stdio.h: stdio.in.h
              -e 's|@''REPLACE_PRINTF''@|$(REPLACE_PRINTF)|g' \
              -e 's|@''REPLACE_REMOVE''@|$(REPLACE_REMOVE)|g' \
              -e 's|@''REPLACE_RENAME''@|$(REPLACE_RENAME)|g' \
+             -e 's|@''REPLACE_RENAMEAT''@|$(REPLACE_RENAMEAT)|g' \
              -e 's|@''REPLACE_SNPRINTF''@|$(REPLACE_SNPRINTF)|g' \
              -e 's|@''REPLACE_SPRINTF''@|$(REPLACE_SPRINTF)|g' \
              -e 
's|@''REPLACE_STDIO_WRITE_FUNCS''@|$(REPLACE_STDIO_WRITE_FUNCS)|g' \
-- 
1.6.5.rc1


>From 9b631e0590832e7a1739fe053c30c97fa4d1aa77 Mon Sep 17 00:00:00 2001
From: Eric Blake <address@hidden>
Date: Thu, 1 Oct 2009 16:46:08 -0600
Subject: [PATCH 3/3] rename: fix mingw bugs

Copy various workarounds from cygwin 1.5: rename("dir/.","name"),
rename("dir","file"), rename("dir1","dir2").  Amazingly,
even though mingw stat() has no way to identify hard linked
files, and even though rename("hard1","hard2") destroys the
hard link, the lower-level MoveFileEx does the right thing!
So the only testsuite relaxation is for rename("dir","dir/sub")
giving EACCES instead of EINVAL when "dir/sub" does not exist.

* lib/rename.c (rpl_rename) [W32]: Fix trailing slash and
directory overwrite bugs.
* tests/test-rename.h (test_rename): Relax test.

Signed-off-by: Eric Blake <address@hidden>
---
 ChangeLog           |    5 ++
 lib/rename.c        |  137 +++++++++++++++++++++++++++++++++++++++++++++++++--
 tests/test-rename.h |    2 +-
 3 files changed, 138 insertions(+), 6 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index dd5378d..2d8f638 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,10 @@
 2009-10-01  Eric Blake  <address@hidden>

+       rename: fix mingw bugs
+       * lib/rename.c (rpl_rename) [W32]: Fix trailing slash and
+       directory overwrite bugs.
+       * tests/test-rename.h (test_rename): Relax test.
+
        renameat: fix Solaris bugs
        * m4/renameat.m4 (gl_FUNC_RENAMEAT): Replace renameat if rename
        needed fixing.
diff --git a/lib/rename.c b/lib/rename.c
index b6a5dab..abd1ec5 100644
--- a/lib/rename.c
+++ b/lib/rename.c
@@ -30,10 +30,16 @@
    existing files.  */

 # include <errno.h>
+# include <stdbool.h>
+# include <stdlib.h>
+# include <sys/stat.h>
+# include <unistd.h>

 # define WIN32_LEAN_AND_MEAN
 # include <windows.h>

+# include "dirname.h"
+
 /* Rename the file SRC to DST.  This replacement is necessary on
    Windows, on which the system rename function will not replace
    an existing DST.  */
@@ -41,15 +47,136 @@ int
 rpl_rename (char const *src, char const *dst)
 {
   int error;
+  size_t src_len = strlen (src);
+  size_t dst_len = strlen (dst);
+  char *src_base = last_component (src);
+  char *dst_base = last_component (dst);
+  bool src_slash;
+  bool dst_slash;
+  bool dst_exists;
+  struct stat src_st;
+  struct stat dst_st;
+
+  /* Filter out dot as last component.  */
+  if (!src_len || !dst_len)
+    {
+      errno = ENOENT;
+      return -1;
+    }
+  if (*src_base == '.')
+    {
+      size_t len = base_len (src_base);
+      if (len == 1 || (len == 2 && src_base[1] == '.'))
+        {
+          errno = EINVAL;
+          return -1;
+        }
+    }
+  if (*dst_base == '.')
+    {
+      size_t len = base_len (dst_base);
+      if (len == 1 || (len == 2 && dst_base[1] == '.'))
+        {
+          errno = EINVAL;
+          return -1;
+        }
+    }
+
+  /* Presence of a trailing slash requires directory semantics.  If
+     the source does not exist, or if the destination cannot be turned
+     into a directory, give up now.  Otherwise, strip trailing slashes
+     before calling rename.  There are no symlinks on mingw, so stat
+     works instead of lstat.  */
+  src_slash = ISSLASH (src[src_len - 1]);
+  dst_slash = ISSLASH (dst[dst_len - 1]);
+  if (stat (src, &src_st))
+    return -1;
+  if (stat (dst, &dst_st))
+    {
+      if (errno != ENOENT || (!S_ISDIR (src_st.st_mode) && dst_slash))
+        return -1;
+      dst_exists = false;
+    }
+  else
+    {
+      if (S_ISDIR (dst_st.st_mode) != S_ISDIR (src_st.st_mode))
+       {
+         errno = S_ISDIR (dst_st.st_mode) ? EISDIR : ENOTDIR;
+         return -1;
+       }
+      dst_exists = true;
+    }
+
+  /* There are no symlinks, so if a file existed with a trailing
+     slash, it must be a directory, and we don't have to worry about
+     stripping strip trailing slash.  However, mingw refuses to
+     replace an existing empty directory, so we have to help it out.
+     And canonicalize_file_name is not yet ported to mingw; however,
+     for directories, getcwd works as a viable alternative.  Ensure
+     that we can get back to where we started before using it.  */
+  if (dst_exists && S_ISDIR (dst_st.st_mode))
+    {
+      char *cwd = getcwd (NULL, 0);
+      char *src_temp;
+      char *dst_temp;
+      if (chdir (cwd))
+        return -1;
+      if (IS_ABSOLUTE_FILE_NAME (src))
+        {
+          dst_temp = chdir (dst) ? NULL : getcwd (NULL, 0);
+          src_temp = chdir (src) ? NULL : getcwd (NULL, 0);
+        }
+      else
+        {
+          src_temp = chdir (src) ? NULL : getcwd (NULL, 0);
+          if (!IS_ABSOLUTE_FILE_NAME (dst))
+            chdir (cwd);
+          dst_temp = chdir (dst) ? NULL : getcwd (NULL, 0);
+        }
+      chdir (cwd);
+      free (cwd);
+      if (!src_temp || !dst_temp)
+        {
+          free (src_temp);
+          free (dst_temp);
+          errno = ENOMEM;
+          return -1;
+        }
+      src_len = strlen (src_temp);
+      if (strncmp (src_temp, dst_temp, src_len) == 0
+          && (ISSLASH (dst_temp[src_len]) || dst_temp[src_len] == '\0'))
+        {
+          error = dst_temp[src_len];
+          free (src_temp);
+          free (dst_temp);
+          if (error)
+            {
+              errno = EINVAL;
+              return -1;
+            }
+          return 0;
+        }
+      if (rmdir (dst))
+        {
+          error = errno;
+          free (src_temp);
+          free (dst_temp);
+          errno = error;
+          return -1;
+        }
+      free (src_temp);
+      free (dst_temp);
+    }

-  /* MoveFileEx works if SRC is a directory without any flags,
-     but fails with MOVEFILE_REPLACE_EXISTING, so try without
-     flags first. */
+  /* MoveFileEx works if SRC is a directory without any flags, but
+     fails with MOVEFILE_REPLACE_EXISTING, so try without flags first.
+     Thankfully, MoveFileEx handles hard links correctly, even though
+     rename() does not.  */
   if (MoveFileEx (src, dst, 0))
     return 0;

   /* Retry with MOVEFILE_REPLACE_EXISTING if the move failed
-   * due to the destination already existing. */
+     due to the destination already existing.  */
   error = GetLastError ();
   if (error == ERROR_FILE_EXISTS || error == ERROR_ALREADY_EXISTS)
     {
@@ -117,7 +244,7 @@ rpl_rename (char const *src, char const *dst)
       break;

 # ifndef ERROR_FILE_TOO_LARGE
-/* This value is documented but not defined in all versions of windows.h. */
+/* This value is documented but not defined in all versions of windows.h.  */
 #  define ERROR_FILE_TOO_LARGE 223
 # endif
     case ERROR_FILE_TOO_LARGE:
diff --git a/tests/test-rename.h b/tests/test-rename.h
index 44c6962..34a6a8a 100644
--- a/tests/test-rename.h
+++ b/tests/test-rename.h
@@ -159,7 +159,7 @@ test_rename (int (*func) (char const *, char const *), bool 
print)
   ASSERT (rmdir (BASE "dir2") == 0);
   errno = 0; /* Move into subdir.  */
   ASSERT (func (BASE "dir", BASE "dir/sub") == -1);
-  ASSERT (errno == EINVAL);
+  ASSERT (errno == EINVAL || errno == EACCES);
   errno = 0;
   ASSERT (stat (BASE "dir/sub", &st) == -1);
   ASSERT (errno == ENOENT);
-- 
1.6.5.rc1


reply via email to

[Prev in Thread] Current Thread [Next in Thread]