diff options
Diffstat (limited to 'lib/rename.c')
-rw-r--r-- | lib/rename.c | 477 |
1 files changed, 477 insertions, 0 deletions
diff --git a/lib/rename.c b/lib/rename.c new file mode 100644 index 0000000..119abe5 --- /dev/null +++ b/lib/rename.c @@ -0,0 +1,477 @@ +/* Work around rename bugs in some systems. + + Copyright (C) 2001-2003, 2005-2006, 2009-2022 Free Software Foundation, Inc. + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of the + License, or (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. */ + +/* Written by Volker Borchert, Eric Blake. */ + +#include <config.h> + +#include <stdio.h> + +#undef rename + +#if defined _WIN32 && ! defined __CYGWIN__ +/* The mingw rename has problems with trailing slashes; it also + requires use of native Windows calls to allow atomic renames over + 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" + +/* Don't assume that UNICODE is not defined. */ +# undef MoveFileEx +# define MoveFileEx MoveFileExA + +/* Rename the file SRC to DST. This replacement is necessary on + Windows, on which the system rename function will not replace + an existing DST. */ +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; later + attempts to return are fatal. Note that we can end up losing a + directory if rename then fails, but it was empty, so not much + damage was done. */ + if (dst_exists && S_ISDIR (dst_st.st_mode)) + { + char *cwd = getcwd (NULL, 0); + char *src_temp; + char *dst_temp; + if (!cwd || 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)) + abort (); + dst_temp = chdir (dst) ? NULL : getcwd (NULL, 0); + } + if (chdir (cwd)) + abort (); + 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)) + { + free (src_temp); + free (dst_temp); + 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. + 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. */ + error = GetLastError (); + if (error == ERROR_FILE_EXISTS || error == ERROR_ALREADY_EXISTS) + { + if (MoveFileEx (src, dst, MOVEFILE_REPLACE_EXISTING)) + return 0; + + error = GetLastError (); + } + + switch (error) + { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_BAD_PATHNAME: + case ERROR_DIRECTORY: + errno = ENOENT; + break; + + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + errno = EACCES; + break; + + case ERROR_OUTOFMEMORY: + errno = ENOMEM; + break; + + case ERROR_CURRENT_DIRECTORY: + errno = EBUSY; + break; + + case ERROR_NOT_SAME_DEVICE: + errno = EXDEV; + break; + + case ERROR_WRITE_PROTECT: + errno = EROFS; + break; + + case ERROR_WRITE_FAULT: + case ERROR_READ_FAULT: + case ERROR_GEN_FAILURE: + errno = EIO; + break; + + case ERROR_HANDLE_DISK_FULL: + case ERROR_DISK_FULL: + case ERROR_DISK_TOO_FRAGMENTED: + errno = ENOSPC; + break; + + case ERROR_FILE_EXISTS: + case ERROR_ALREADY_EXISTS: + errno = EEXIST; + break; + + case ERROR_BUFFER_OVERFLOW: + case ERROR_FILENAME_EXCED_RANGE: + errno = ENAMETOOLONG; + break; + + case ERROR_INVALID_NAME: + case ERROR_DELETE_PENDING: + errno = EPERM; /* ? */ + break; + +# ifndef ERROR_FILE_TOO_LARGE +/* 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: + errno = EFBIG; + break; + + default: + errno = EINVAL; + break; + } + + return -1; +} + +#else /* ! W32 platform */ + +# include <errno.h> +# include <stdio.h> +# include <stdlib.h> +# include <string.h> +# include <sys/stat.h> +# include <unistd.h> + +# include "dirname.h" +# include "same-inode.h" + +/* Rename the file SRC to DST, fixing any trailing slash bugs. */ + +int +rpl_rename (char const *src, 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; + _GL_UNUSED bool dst_exists; + int ret_val = -1; + int rename_errno = ENOTDIR; + struct stat src_st; + struct stat dst_st; + + if (!src_len || !dst_len) + return rename (src, dst); /* Let strace see the ENOENT failure. */ + +# if RENAME_DEST_EXISTS_BUG + { + char *src_base = last_component (src); + char *dst_base = last_component (dst); + 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; + } + } + } +# endif /* RENAME_DEST_EXISTS_BUG */ + + src_slash = src[src_len - 1] == '/'; + dst_slash = dst[dst_len - 1] == '/'; + +# 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 or file/directory conflicts. */ + if (!src_slash && !dst_slash) + return rename (src, dst); +# 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 + into a directory, give up now. Otherwise, strip trailing slashes + before calling rename. */ + if (lstat (src, &src_st)) + return -1; + if (lstat (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; + } +# if RENAME_HARD_LINK_BUG + if (SAME_INODE (src_st, dst_st)) + return 0; +# endif /* RENAME_HARD_LINK_BUG */ + dst_exists = true; + } + +# 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 + a trailing slash on an existing source or destination, as in + Solaris 9, or if a directory can overwrite a symlink, as on + Cygwin 1.5, or if directories cannot be created with trailing + slash, as on NetBSD 1.6, then we must strip the offending slash + and check that we have not encountered a symlink instead of a + directory. + + Stripping a trailing slash interferes with POSIX semantics, where + rename behavior on a symlink with a trailing slash operates on + the corresponding target directory. We prefer the GNU semantics + of rejecting any use of a symlink with trailing slash, but do not + enforce them, since Solaris 10 is able to obey POSIX semantics + and there might be clients expecting it, as counter-intuitive as + those semantics are. + + Technically, we could also follow the POSIX behavior by chasing a + readlink trail, but that is harder to implement. */ + 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 (lstat (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 (lstat (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 || 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. Note that we can end up losing + a directory if rename then fails, but it was empty, so not much + damage was done. */ + if (dst_exists && S_ISDIR (dst_st.st_mode)) + { + if (src_st.st_dev != dst_st.st_dev) + { + rename_errno = EXDEV; + goto out; + } + 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; + + out: _GL_UNUSED_LABEL; + + if (src_temp != src) + free (src_temp); + if (dst_temp != dst) + free (dst_temp); + errno = rename_errno; + return ret_val; +} +#endif /* ! W32 platform */ |