summaryrefslogtreecommitdiffstats
path: root/src/remove.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/remove.c')
-rw-r--r--src/remove.c623
1 files changed, 623 insertions, 0 deletions
diff --git a/src/remove.c b/src/remove.c
new file mode 100644
index 0000000..b5d1ea8
--- /dev/null
+++ b/src/remove.c
@@ -0,0 +1,623 @@
+/* remove.c -- core functions for removing files and directories
+ Copyright (C) 1988-2022 Free Software Foundation, Inc.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>. */
+
+/* Extracted from rm.c, librarified, then rewritten twice by Jim Meyering. */
+
+#include <config.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <assert.h>
+
+#include "system.h"
+#include "error.h"
+#include "file-type.h"
+#include "filenamecat.h"
+#include "ignore-value.h"
+#include "remove.h"
+#include "root-dev-ino.h"
+#include "write-any-file.h"
+#include "xfts.h"
+#include "yesno.h"
+
+enum Ternary
+ {
+ T_UNKNOWN = 2,
+ T_NO,
+ T_YES
+ };
+typedef enum Ternary Ternary;
+
+/* The prompt function may be called twice for a given directory.
+ The first time, we ask whether to descend into it, and the
+ second time, we ask whether to remove it. */
+enum Prompt_action
+ {
+ PA_DESCEND_INTO_DIR = 2,
+ PA_REMOVE_DIR
+ };
+
+/* D_TYPE(D) is the type of directory entry D if known, DT_UNKNOWN
+ otherwise. */
+#if ! HAVE_STRUCT_DIRENT_D_TYPE
+/* Any int values will do here, so long as they're distinct.
+ Undef any existing macros out of the way. */
+# undef DT_UNKNOWN
+# undef DT_DIR
+# undef DT_LNK
+# define DT_UNKNOWN 0
+# define DT_DIR 1
+# define DT_LNK 2
+#endif
+
+/* Like fstatat, but cache the result. If ST->st_size is -1, the
+ status has not been gotten yet. If less than -1, fstatat failed
+ with errno == ST->st_ino. Otherwise, the status has already
+ been gotten, so return 0. */
+static int
+cache_fstatat (int fd, char const *file, struct stat *st, int flag)
+{
+ if (st->st_size == -1 && fstatat (fd, file, st, flag) != 0)
+ {
+ st->st_size = -2;
+ st->st_ino = errno;
+ }
+ if (0 <= st->st_size)
+ return 0;
+ errno = (int) st->st_ino;
+ return -1;
+}
+
+/* Initialize a fstatat cache *ST. Return ST for convenience. */
+static inline struct stat *
+cache_stat_init (struct stat *st)
+{
+ st->st_size = -1;
+ return st;
+}
+
+/* Return 1 if FILE is an unwritable non-symlink,
+ 0 if it is writable or some other type of file,
+ -1 and set errno if there is some problem in determining the answer.
+ Set *BUF to the file status. */
+static int
+write_protected_non_symlink (int fd_cwd,
+ char const *file,
+ struct stat *buf)
+{
+ if (can_write_any_file ())
+ return 0;
+ if (cache_fstatat (fd_cwd, file, buf, AT_SYMLINK_NOFOLLOW) != 0)
+ return -1;
+ if (S_ISLNK (buf->st_mode))
+ return 0;
+ /* Here, we know FILE is not a symbolic link. */
+
+ /* In order to be reentrant -- i.e., to avoid changing the working
+ directory, and at the same time to be able to deal with alternate
+ access control mechanisms (ACLs, xattr-style attributes) and
+ arbitrarily deep trees -- we need a function like eaccessat, i.e.,
+ like Solaris' eaccess, but fd-relative, in the spirit of openat. */
+
+ /* In the absence of a native eaccessat function, here are some of
+ the implementation choices [#4 and #5 were suggested by Paul Eggert]:
+ 1) call openat with O_WRONLY|O_NOCTTY
+ Disadvantage: may create the file and doesn't work for directory,
+ may mistakenly report 'unwritable' for EROFS or ACLs even though
+ perm bits say the file is writable.
+
+ 2) fake eaccessat (save_cwd, fchdir, call euidaccess, restore_cwd)
+ Disadvantage: changes working directory (not reentrant) and can't
+ work if save_cwd fails.
+
+ 3) if (euidaccess (full_name, W_OK) == 0)
+ Disadvantage: doesn't work if full_name is too long.
+ Inefficient for very deep trees (O(Depth^2)).
+
+ 4) If the full pathname is sufficiently short (say, less than
+ PATH_MAX or 8192 bytes, whichever is shorter):
+ use method (3) (i.e., euidaccess (full_name, W_OK));
+ Otherwise: vfork, fchdir in the child, run euidaccess in the
+ child, then the child exits with a status that tells the parent
+ whether euidaccess succeeded.
+
+ This avoids the O(N**2) algorithm of method (3), and it also avoids
+ the failure-due-to-too-long-file-names of method (3), but it's fast
+ in the normal shallow case. It also avoids the lack-of-reentrancy
+ and the save_cwd problems.
+ Disadvantage; it uses a process slot for very-long file names,
+ and would be very slow for hierarchies with many such files.
+
+ 5) If the full file name is sufficiently short (say, less than
+ PATH_MAX or 8192 bytes, whichever is shorter):
+ use method (3) (i.e., euidaccess (full_name, W_OK));
+ Otherwise: look just at the file bits. Perhaps issue a warning
+ the first time this occurs.
+
+ This is like (4), except for the "Otherwise" case where it isn't as
+ "perfect" as (4) but is considerably faster. It conforms to current
+ POSIX, and is uniformly better than what Solaris and FreeBSD do (they
+ mess up with long file names). */
+
+ {
+ if (faccessat (fd_cwd, file, W_OK, AT_EACCESS) == 0)
+ return 0;
+
+ return errno == EACCES ? 1 : -1;
+ }
+}
+
+/* Prompt whether to remove FILENAME (ent->, if required via a combination of
+ the options specified by X and/or file attributes. If the file may
+ be removed, return RM_OK. If the user declines to remove the file,
+ return RM_USER_DECLINED. If not ignoring missing files and we
+ cannot lstat FILENAME, then return RM_ERROR.
+
+ IS_DIR is true if ENT designates a directory, false otherwise.
+
+ Depending on MODE, ask whether to 'descend into' or to 'remove' the
+ directory FILENAME. MODE is ignored when FILENAME is not a directory.
+ Set *IS_EMPTY_P to T_YES if FILENAME is an empty directory, and it is
+ appropriate to try to remove it with rmdir (e.g. recursive mode).
+ Don't even try to set *IS_EMPTY_P when MODE == PA_REMOVE_DIR. */
+static enum RM_status
+prompt (FTS const *fts, FTSENT const *ent, bool is_dir,
+ struct rm_options const *x, enum Prompt_action mode,
+ Ternary *is_empty_p)
+{
+ int fd_cwd = fts->fts_cwd_fd;
+ char const *full_name = ent->fts_path;
+ char const *filename = ent->fts_accpath;
+ if (is_empty_p)
+ *is_empty_p = T_UNKNOWN;
+
+ struct stat st;
+ struct stat *sbuf = &st;
+ cache_stat_init (sbuf);
+
+ int dirent_type = is_dir ? DT_DIR : DT_UNKNOWN;
+ int write_protected = 0;
+
+ bool is_empty = false;
+ if (is_empty_p)
+ {
+ is_empty = is_empty_dir (fd_cwd, filename);
+ *is_empty_p = is_empty ? T_YES : T_NO;
+ }
+
+ /* When nonzero, this indicates that we failed to remove a child entry,
+ either because the user declined an interactive prompt, or due to
+ some other failure, like permissions. */
+ if (ent->fts_number)
+ return RM_USER_DECLINED;
+
+ if (x->interactive == RMI_NEVER)
+ return RM_OK;
+
+ int wp_errno = 0;
+ if (!x->ignore_missing_files
+ && ((x->interactive == RMI_ALWAYS) || x->stdin_tty)
+ && dirent_type != DT_LNK)
+ {
+ write_protected = write_protected_non_symlink (fd_cwd, filename, sbuf);
+ wp_errno = errno;
+ }
+
+ if (write_protected || x->interactive == RMI_ALWAYS)
+ {
+ if (0 <= write_protected && dirent_type == DT_UNKNOWN)
+ {
+ if (cache_fstatat (fd_cwd, filename, sbuf, AT_SYMLINK_NOFOLLOW) == 0)
+ {
+ if (S_ISLNK (sbuf->st_mode))
+ dirent_type = DT_LNK;
+ else if (S_ISDIR (sbuf->st_mode))
+ dirent_type = DT_DIR;
+ /* Otherwise it doesn't matter, so leave it DT_UNKNOWN. */
+ }
+ else
+ {
+ /* This happens, e.g., with 'rm '''. */
+ write_protected = -1;
+ wp_errno = errno;
+ }
+ }
+
+ if (0 <= write_protected)
+ switch (dirent_type)
+ {
+ case DT_LNK:
+ /* Using permissions doesn't make sense for symlinks. */
+ if (x->interactive != RMI_ALWAYS)
+ return RM_OK;
+ break;
+
+ case DT_DIR:
+ /* Unless we're either deleting directories or deleting
+ recursively, we want to raise an EISDIR error rather than
+ prompting the user */
+ if ( ! (x->recursive || (x->remove_empty_directories && is_empty)))
+ {
+ write_protected = -1;
+ wp_errno = EISDIR;
+ }
+ break;
+ }
+
+ char const *quoted_name = quoteaf (full_name);
+
+ if (write_protected < 0)
+ {
+ error (0, wp_errno, _("cannot remove %s"), quoted_name);
+ return RM_ERROR;
+ }
+
+ /* Issue the prompt. */
+ if (dirent_type == DT_DIR
+ && mode == PA_DESCEND_INTO_DIR
+ && !is_empty)
+ fprintf (stderr,
+ (write_protected
+ ? _("%s: descend into write-protected directory %s? ")
+ : _("%s: descend into directory %s? ")),
+ program_name, quoted_name);
+ else
+ {
+ if (cache_fstatat (fd_cwd, filename, sbuf, AT_SYMLINK_NOFOLLOW) != 0)
+ {
+ error (0, errno, _("cannot remove %s"), quoted_name);
+ return RM_ERROR;
+ }
+
+ fprintf (stderr,
+ (write_protected
+ /* TRANSLATORS: In the next two strings the second %s is
+ replaced by the type of the file. To avoid grammatical
+ problems, it may be more convenient to translate these
+ strings instead as: "%1$s: %3$s is write-protected and
+ is of type '%2$s' -- remove it? ". */
+ ? _("%s: remove write-protected %s %s? ")
+ : _("%s: remove %s %s? ")),
+ program_name, file_type (sbuf), quoted_name);
+ }
+
+ if (!yesno ())
+ return RM_USER_DECLINED;
+ }
+ return RM_OK;
+}
+
+/* When a function like unlink, rmdir, or fstatat fails with an errno
+ value of ERRNUM, return true if the specified file system object
+ is guaranteed not to exist; otherwise, return false. */
+static inline bool
+nonexistent_file_errno (int errnum)
+{
+ /* Do not include ELOOP here, since the specified file may indeed
+ exist, but be (in)accessible only via too long a symlink chain.
+ Likewise for ENAMETOOLONG, since rm -f ./././.../foo may fail
+ if the "..." part expands to a long enough sequence of "./"s,
+ even though ./foo does indeed exist.
+
+ Another case to consider is when a particular name is invalid for
+ a given file system. In 2011, smbfs returns EINVAL, but the next
+ revision of POSIX will require EILSEQ for that situation:
+ http://austingroupbugs.net/view.php?id=293
+ */
+
+ switch (errnum)
+ {
+ case EILSEQ:
+ case EINVAL:
+ case ENOENT:
+ case ENOTDIR:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/* Encapsulate the test for whether the errno value, ERRNUM, is ignorable. */
+static inline bool
+ignorable_missing (struct rm_options const *x, int errnum)
+{
+ return x->ignore_missing_files && nonexistent_file_errno (errnum);
+}
+
+/* Tell fts not to traverse into the hierarchy at ENT. */
+static void
+fts_skip_tree (FTS *fts, FTSENT *ent)
+{
+ fts_set (fts, ent, FTS_SKIP);
+ /* Ensure that we do not process ENT a second time. */
+ ignore_value (fts_read (fts));
+}
+
+/* Upon unlink failure, or when the user declines to remove ENT, mark
+ each of its ancestor directories, so that we know not to prompt for
+ its removal. */
+static void
+mark_ancestor_dirs (FTSENT *ent)
+{
+ FTSENT *p;
+ for (p = ent->fts_parent; FTS_ROOTLEVEL <= p->fts_level; p = p->fts_parent)
+ {
+ if (p->fts_number)
+ break;
+ p->fts_number = 1;
+ }
+}
+
+/* Remove the file system object specified by ENT. IS_DIR specifies
+ whether it is expected to be a directory or non-directory.
+ Return RM_OK upon success, else RM_ERROR. */
+static enum RM_status
+excise (FTS *fts, FTSENT *ent, struct rm_options const *x, bool is_dir)
+{
+ int flag = is_dir ? AT_REMOVEDIR : 0;
+ if (unlinkat (fts->fts_cwd_fd, ent->fts_accpath, flag) == 0)
+ {
+ if (x->verbose)
+ {
+ printf ((is_dir
+ ? _("removed directory %s\n")
+ : _("removed %s\n")), quoteaf (ent->fts_path));
+ }
+ return RM_OK;
+ }
+
+ /* The unlinkat from kernels like linux-2.6.32 reports EROFS even for
+ nonexistent files. When the file is indeed missing, map that to ENOENT,
+ so that rm -f ignores it, as required. Even without -f, this is useful
+ because it makes rm print the more precise diagnostic. */
+ if (errno == EROFS)
+ {
+ struct stat st;
+ if ( ! (fstatat (fts->fts_cwd_fd, ent->fts_accpath, &st,
+ AT_SYMLINK_NOFOLLOW)
+ && errno == ENOENT))
+ errno = EROFS;
+ }
+
+ if (ignorable_missing (x, errno))
+ return RM_OK;
+
+ /* When failing to rmdir an unreadable directory, we see errno values
+ like EISDIR or ENOTDIR (or, on Solaris 10, EEXIST), but they would be
+ meaningless in a diagnostic. When that happens and the errno value
+ from the failed open is EPERM or EACCES, use the earlier, more
+ descriptive errno value. */
+ if (ent->fts_info == FTS_DNR
+ && (errno == ENOTEMPTY || errno == EISDIR || errno == ENOTDIR
+ || errno == EEXIST)
+ && (ent->fts_errno == EPERM || ent->fts_errno == EACCES))
+ errno = ent->fts_errno;
+ error (0, errno, _("cannot remove %s"), quoteaf (ent->fts_path));
+ mark_ancestor_dirs (ent);
+ return RM_ERROR;
+}
+
+/* This function is called once for every file system object that fts
+ encounters. fts performs a depth-first traversal.
+ A directory is usually processed twice, first with fts_info == FTS_D,
+ and later, after all of its entries have been processed, with FTS_DP.
+ Return RM_ERROR upon error, RM_USER_DECLINED for a negative response
+ to an interactive prompt, and otherwise, RM_OK. */
+static enum RM_status
+rm_fts (FTS *fts, FTSENT *ent, struct rm_options const *x)
+{
+ switch (ent->fts_info)
+ {
+ case FTS_D: /* preorder directory */
+ if (! x->recursive
+ && !(x->remove_empty_directories
+ && is_empty_dir (fts->fts_cwd_fd, ent->fts_accpath)))
+ {
+ /* This is the first (pre-order) encounter with a directory
+ that we cannot delete.
+ Not recursive, and it's not an empty directory (if we're removing
+ them) so arrange to skip contents. */
+ int err = x->remove_empty_directories ? ENOTEMPTY : EISDIR;
+ error (0, err, _("cannot remove %s"), quoteaf (ent->fts_path));
+ mark_ancestor_dirs (ent);
+ fts_skip_tree (fts, ent);
+ return RM_ERROR;
+ }
+
+ /* Perform checks that can apply only for command-line arguments. */
+ if (ent->fts_level == FTS_ROOTLEVEL)
+ {
+ /* POSIX says:
+ If the basename of a command line argument is "." or "..",
+ diagnose it and do nothing more with that argument. */
+ if (dot_or_dotdot (last_component (ent->fts_accpath)))
+ {
+ error (0, 0,
+ _("refusing to remove %s or %s directory: skipping %s"),
+ quoteaf_n (0, "."), quoteaf_n (1, ".."),
+ quoteaf_n (2, ent->fts_path));
+ fts_skip_tree (fts, ent);
+ return RM_ERROR;
+ }
+
+ /* POSIX also says:
+ If a command line argument resolves to "/" (and --preserve-root
+ is in effect -- default) diagnose and skip it. */
+ if (ROOT_DEV_INO_CHECK (x->root_dev_ino, ent->fts_statp))
+ {
+ ROOT_DEV_INO_WARN (ent->fts_path);
+ fts_skip_tree (fts, ent);
+ return RM_ERROR;
+ }
+
+ /* If a command line argument is a mount point and
+ --preserve-root=all is in effect, diagnose and skip it.
+ This doesn't handle "/", but that's handled above. */
+ if (x->preserve_all_root)
+ {
+ bool failed = false;
+ char *parent = file_name_concat (ent->fts_accpath, "..", NULL);
+ struct stat statbuf;
+
+ if (!parent || lstat (parent, &statbuf))
+ {
+ error (0, 0,
+ _("failed to stat %s: skipping %s"),
+ quoteaf_n (0, parent),
+ quoteaf_n (1, ent->fts_accpath));
+ failed = true;
+ }
+
+ free (parent);
+
+ if (failed || fts->fts_dev != statbuf.st_dev)
+ {
+ if (! failed)
+ {
+ error (0, 0,
+ _("skipping %s, since it's on a different device"),
+ quoteaf (ent->fts_path));
+ error (0, 0, _("and --preserve-root=all is in effect"));
+ }
+ fts_skip_tree (fts, ent);
+ return RM_ERROR;
+ }
+ }
+ }
+
+ {
+ Ternary is_empty_directory;
+ enum RM_status s = prompt (fts, ent, true /*is_dir*/, x,
+ PA_DESCEND_INTO_DIR, &is_empty_directory);
+
+ if (s == RM_OK && is_empty_directory == T_YES)
+ {
+ /* When we know (from prompt when in interactive mode)
+ that this is an empty directory, don't prompt twice. */
+ s = excise (fts, ent, x, true);
+ if (s == RM_OK)
+ fts_skip_tree (fts, ent);
+ }
+
+ if (s != RM_OK)
+ {
+ mark_ancestor_dirs (ent);
+ fts_skip_tree (fts, ent);
+ }
+
+ return s;
+ }
+
+ case FTS_F: /* regular file */
+ case FTS_NS: /* stat(2) failed */
+ case FTS_SL: /* symbolic link */
+ case FTS_SLNONE: /* symbolic link without target */
+ case FTS_DP: /* postorder directory */
+ case FTS_DNR: /* unreadable directory */
+ case FTS_NSOK: /* e.g., dangling symlink */
+ case FTS_DEFAULT: /* none of the above */
+ {
+ /* With --one-file-system, do not attempt to remove a mount point.
+ fts' FTS_XDEV ensures that we don't process any entries under
+ the mount point. */
+ if (ent->fts_info == FTS_DP
+ && x->one_file_system
+ && FTS_ROOTLEVEL < ent->fts_level
+ && ent->fts_statp->st_dev != fts->fts_dev)
+ {
+ mark_ancestor_dirs (ent);
+ error (0, 0, _("skipping %s, since it's on a different device"),
+ quoteaf (ent->fts_path));
+ return RM_ERROR;
+ }
+
+ bool is_dir = ent->fts_info == FTS_DP || ent->fts_info == FTS_DNR;
+ enum RM_status s = prompt (fts, ent, is_dir, x, PA_REMOVE_DIR, NULL);
+ if (s != RM_OK)
+ return s;
+ return excise (fts, ent, x, is_dir);
+ }
+
+ case FTS_DC: /* directory that causes cycles */
+ emit_cycle_warning (ent->fts_path);
+ fts_skip_tree (fts, ent);
+ return RM_ERROR;
+
+ case FTS_ERR:
+ /* Various failures, from opendir to ENOMEM, to failure to "return"
+ to preceding directory, can provoke this. */
+ error (0, ent->fts_errno, _("traversal failed: %s"),
+ quotef (ent->fts_path));
+ fts_skip_tree (fts, ent);
+ return RM_ERROR;
+
+ default:
+ error (0, 0, _("unexpected failure: fts_info=%d: %s\n"
+ "please report to %s"),
+ ent->fts_info,
+ quotef (ent->fts_path),
+ PACKAGE_BUGREPORT);
+ abort ();
+ }
+}
+
+/* Remove FILEs, honoring options specified via X.
+ Return RM_OK if successful. */
+enum RM_status
+rm (char *const *file, struct rm_options const *x)
+{
+ enum RM_status rm_status = RM_OK;
+
+ if (*file)
+ {
+ int bit_flags = (FTS_CWDFD
+ | FTS_NOSTAT
+ | FTS_PHYSICAL);
+
+ if (x->one_file_system)
+ bit_flags |= FTS_XDEV;
+
+ FTS *fts = xfts_open (file, bit_flags, NULL);
+
+ while (true)
+ {
+ FTSENT *ent;
+
+ ent = fts_read (fts);
+ if (ent == NULL)
+ {
+ if (errno != 0)
+ {
+ error (0, errno, _("fts_read failed"));
+ rm_status = RM_ERROR;
+ }
+ break;
+ }
+
+ enum RM_status s = rm_fts (fts, ent, x);
+
+ assert (VALID_STATUS (s));
+ UPDATE_STATUS (rm_status, s);
+ }
+
+ if (fts_close (fts) != 0)
+ {
+ error (0, errno, _("fts_close failed"));
+ rm_status = RM_ERROR;
+ }
+ }
+
+ return rm_status;
+}