/* remove.c -- core functions for removing files and directories
- Copyright (C) 88, 90, 91, 1994-2005 Free Software Foundation, Inc.
+ Copyright (C) 88, 90, 91, 1994-2008 Free Software Foundation, Inc.
- This program is free software; you can redistribute it and/or modify
+ 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 2, or (at your option)
- any later version.
+ 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
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, write to the Free Software Foundation,
- Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* Extracted from rm.c and librarified, then rewritten by Jim Meyering. */
#include "system.h"
#include "cycle-check.h"
#include "dirfd.h"
-#include "dirname.h"
#include "error.h"
#include "euidaccess.h"
#include "euidaccess-stat.h"
#include "hash-pjw.h"
#include "lstat.h"
#include "obstack.h"
-#include "openat.h"
#include "quote.h"
#include "remove.h"
#include "root-dev-ino.h"
#include "unlinkdir.h"
+#include "write-any-file.h"
#include "yesno.h"
/* Avoid shadowing warnings because these are functions declared
#define dir_name rm_dir_name
#define dir_len rm_dir_len
-#define obstack_chunk_alloc malloc
-#define obstack_chunk_free free
-
-/* Define to the options that make open (or openat) fail to open
- a symlink. Define to 0 if there are no such options.
- This is useful because it permits us to skip the `fstat'
- and dev/ino comparison in AD_push. */
-#if defined O_NOFOLLOW
-# define OPEN_NO_FOLLOW_SYMLINK O_NOFOLLOW
-#else
-# define OPEN_NO_FOLLOW_SYMLINK 0
-#endif
-
/* This is the maximum number of consecutive readdir/unlink calls that
- can be made (with no intervening rewinddir or closedir/opendir)
- before triggering a bug that makes readdir return NULL even though
- some directory entries have not been processed. The bug afflicts
- SunOS's readdir when applied to ufs file systems and Darwin 6.5's
- (and OSX v.10.3.8's) HFS+. This maximum is conservative in that
- demonstrating the problem seems to require a directory containing
- at least 254 deletable entries (which doesn't count . and ..), so
- we could conceivably increase the maximum value to 254. */
+ can be made (with no intervening rewinddir or closedir/opendir) before
+ triggering a bug that makes readdir return NULL even though some
+ directory entries have not been processed. The bug afflicts SunOS's
+ readdir when applied to ufs file systems and Darwin 6.5's (and OSX
+ v.10.3.8's) HFS+. This maximum is conservative in that demonstrating
+ the problem requires a directory containing at least 16 deletable
+ entries (which doesn't count . and ..).
+ This problem also affects Darwin 7.9.0 (aka MacOS X 10.3.9) on HFS+
+ and NFS-mounted file systems, but not vfat ones. */
enum
{
- CONSECUTIVE_READDIR_UNLINK_THRESHOLD = 200
+ CONSECUTIVE_READDIR_UNLINK_THRESHOLD = 10
};
+/* If the heuristics in preprocess_dir suggest that there
+ are fewer than this many entries in a directory, then it
+ skips the preprocessing altogether. */
enum
- {
- OPENAT_CWD_RESTORE__REQUIRE = false,
- OPENAT_CWD_RESTORE__ALLOW_FAILURE = true
- };
+{
+ INODE_SORT_DIR_ENTRIES_THRESHOLD = 10000
+};
+
+/* FIXME: in 2009, or whenever Darwin 7.9.0 (aka MacOS X 10.3.9) is no
+ longer relevant, remove this work-around code. Then, there will be
+ no need to perform the extra rewinddir call, ever. */
+#define NEED_REWIND(readdir_unlink_count) \
+ (CONSECUTIVE_READDIR_UNLINK_THRESHOLD <= (readdir_unlink_count))
enum Ternary
{
struct dev_ino dev_ino;
};
-extern char *program_name;
+/* D_TYPE(D) is the type of directory entry D if known, DT_UNKNOWN
+ otherwise. */
+#if HAVE_STRUCT_DIRENT_D_TYPE
+# define D_TYPE(d) ((d)->d_type)
+#else
+# define D_TYPE(d) DT_UNKNOWN
+
+/* 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
struct dirstack_state
{
};
typedef struct dirstack_state Dirstack_state;
+/* A static buffer and its allocated size, these variables are used by
+ xfull_filename and full_filename to form full, relative file names. */
+static char *g_buf;
+static size_t g_n_allocated;
+
+/* 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 true if *ST has been statted. */
+static inline bool
+cache_statted (struct stat *st)
+{
+ return (st->st_size != -1);
+}
+
+/* Return true if *ST has been statted successfully. */
+static inline bool
+cache_stat_ok (struct stat *st)
+{
+ return (0 <= st->st_size);
+}
+
+
static void
hash_freer (void *x)
{
return STREQ (x, y) ? true : false;
}
+/* Obstack allocator: longjump on failure. */
+static void *
+rm_malloc (void *v_jumpbuf, long size)
+{
+ jmp_buf *jumpbuf = v_jumpbuf;
+ void *p = malloc (size);
+ if (p)
+ return p;
+ longjmp (*jumpbuf, 1);
+}
+
+/* With the 2-arg allocator, we must also provide a two-argument freer. */
+static void
+rm_free (void *v_jumpbuf ATTRIBUTE_UNUSED, void *ptr)
+{
+ free (ptr);
+}
+
static inline void
push_dir (Dirstack_state *ds, const char *dir_name)
{
size_t len = strlen (dir_name);
+ /* Don't copy trailing slashes. */
+ while (1 < len && dir_name[len - 1] == '/')
+ --len;
+
/* Append the string onto the stack. */
obstack_grow (&ds->dir_stack, dir_name, len);
/* Return the entry name of the directory on the top of the stack
in malloc'd storage. */
static inline char *
-top_dir (Dirstack_state const *ds)
+top_dir (Dirstack_state *ds)
{
size_t n_lengths = obstack_object_size (&ds->len_stack) / sizeof (size_t);
size_t *length = obstack_base (&ds->len_stack);
size_t top_len = length[n_lengths - 1];
char const *p = obstack_next_free (&ds->dir_stack) - top_len;
- char *q = xmalloc (top_len);
+ char *q = malloc (top_len);
+ if (q == NULL)
+ longjmp (ds->current_arg_jumpbuf, 1);
memcpy (q, p, top_len - 1);
q[top_len - 1] = 0;
return q;
return dst_len - src_len;
}
-/* Using the global directory name obstack, create the full name FILENAME.
+/* Using the global directory name obstack, create the full name of FILENAME.
Return it in sometimes-realloc'd space that should not be freed by the
- caller. Realloc as necessary. If realloc fails, use a static buffer
- and put as long a suffix in that buffer as possible. */
+ caller. Realloc as necessary. If realloc fails, return NULL. */
-#define full_filename(Filename) full_filename_ (ds, Filename)
static char *
-full_filename_ (Dirstack_state const *ds, const char *filename)
+full_filename0 (Dirstack_state const *ds, const char *filename)
{
- static char *buf = NULL;
- static size_t n_allocated = 0;
-
size_t dir_len = obstack_object_size (&ds->dir_stack);
char *dir_name = obstack_base (&ds->dir_stack);
- size_t n_bytes_needed;
- size_t filename_len;
-
- filename_len = strlen (filename);
- n_bytes_needed = dir_len + filename_len + 1;
+ size_t filename_len = strlen (filename);
+ size_t n_bytes_needed = dir_len + filename_len + 1;
- if (n_allocated < n_bytes_needed)
+ if (g_n_allocated < n_bytes_needed)
{
- /* This code requires that realloc accept NULL as the first arg.
- This function must not use xrealloc. Otherwise, an out-of-memory
- error involving a file name to be expanded here wouldn't ever
- be issued. Use realloc and fall back on using a static buffer
- if memory allocation fails. */
- char *new_buf = realloc (buf, n_bytes_needed);
- n_allocated = n_bytes_needed;
-
+ char *new_buf = realloc (g_buf, n_bytes_needed);
if (new_buf == NULL)
- {
-#define SBUF_SIZE 512
-#define ELLIPSES_PREFIX "[...]"
- static char static_buf[SBUF_SIZE];
- bool truncated;
- size_t len;
- char *p;
-
- free (buf);
- len = right_justify (static_buf, SBUF_SIZE, filename,
- filename_len + 1, &p, &truncated);
- right_justify (static_buf, len, dir_name, dir_len, &p, &truncated);
- if (truncated)
- {
- memcpy (static_buf, ELLIPSES_PREFIX,
- sizeof (ELLIPSES_PREFIX) - 1);
- }
- return p;
- }
+ return NULL;
- buf = new_buf;
+ g_buf = new_buf;
+ g_n_allocated = n_bytes_needed;
}
- if (filename_len == 1 && *filename == '.' && dir_len)
+ if (STREQ (filename, ".") && dir_len)
{
/* FILENAME is just `.' and dir_len is nonzero.
Copy the directory part, omitting the trailing slash,
and append a trailing zero byte. */
- char *p = mempcpy (buf, dir_name, dir_len - 1);
+ char *p = mempcpy (g_buf, dir_name, dir_len - 1);
*p = 0;
}
else
{
/* Copy the directory part, including trailing slash, and then
append the filename part, including a trailing zero byte. */
- memcpy (mempcpy (buf, dir_name, dir_len), filename, filename_len + 1);
- assert (strlen (buf) + 1 == n_bytes_needed);
+ memcpy (mempcpy (g_buf, dir_name, dir_len), filename, filename_len + 1);
+ assert (strlen (g_buf) + 1 == n_bytes_needed);
}
+ return g_buf;
+}
+
+/* Using the global directory name obstack, create the full name of FILENAME.
+ Return it in sometimes-realloc'd space that should not be freed by the
+ caller. Realloc as necessary. If realloc fails, die. */
+
+static char *
+xfull_filename (Dirstack_state const *ds, const char *filename)
+{
+ char *buf = full_filename0 (ds, filename);
+ if (buf == NULL)
+ xalloc_die ();
return buf;
}
-static size_t
+/* Using the global directory name obstack, create the full name FILENAME.
+ Return it in sometimes-realloc'd space that should not be freed by the
+ caller. Realloc as necessary. If realloc fails, use a static buffer
+ and put as long a suffix in that buffer as possible. Be careful not
+ to change errno. */
+
+#define full_filename(Filename) full_filename_ (ds, Filename)
+static char *
+full_filename_ (Dirstack_state const *ds, const char *filename)
+{
+ int saved_errno = errno;
+ char *full_name = full_filename0 (ds, filename);
+ if (full_name)
+ {
+ errno = saved_errno;
+ return full_name;
+ }
+
+ {
+#define SBUF_SIZE 512
+#define ELLIPSES_PREFIX "[...]"
+ static char static_buf[SBUF_SIZE];
+ bool file_truncated;
+ bool dir_truncated;
+ size_t n_bytes_remaining;
+ char *p;
+ char *dir_name = obstack_base (&ds->dir_stack);
+ size_t dir_len = obstack_object_size (&ds->dir_stack);
+
+ free (g_buf);
+ n_bytes_remaining = right_justify (static_buf, SBUF_SIZE, filename,
+ strlen (filename) + 1, &p,
+ &file_truncated);
+ right_justify (static_buf, n_bytes_remaining, dir_name, dir_len,
+ &p, &dir_truncated);
+ if (file_truncated || dir_truncated)
+ {
+ memcpy (static_buf, ELLIPSES_PREFIX,
+ sizeof (ELLIPSES_PREFIX) - 1);
+ }
+ errno = saved_errno;
+ return p;
+ }
+}
+
+static inline size_t
AD_stack_height (Dirstack_state const *ds)
{
return obstack_object_size (&ds->Active_dir) / sizeof (struct AD_ent);
}
-static struct AD_ent *
+static inline struct AD_ent *
AD_stack_top (Dirstack_state const *ds)
{
return (struct AD_ent *)
obstack_blank (&ds->Active_dir, -(int) sizeof (struct AD_ent));
}
-static Dirstack_state *
-ds_init (void)
+static void
+AD_stack_clear (Dirstack_state *ds)
+{
+ while (0 < AD_stack_height (ds))
+ {
+ AD_stack_pop (ds);
+ }
+}
+
+/* Initialize obstack O just enough so that it may be freed
+ with obstack_free. */
+static void
+obstack_init_minimal (struct obstack *o)
{
- Dirstack_state *ds = xmalloc (sizeof *ds);
- obstack_init (&ds->dir_stack);
- obstack_init (&ds->len_stack);
- obstack_init (&ds->Active_dir);
- return ds;
+ o->chunk = NULL;
+}
+
+static void
+ds_init (Dirstack_state *ds)
+{
+ unsigned int i;
+ struct obstack *o[3];
+ o[0] = &ds->dir_stack;
+ o[1] = &ds->len_stack;
+ o[2] = &ds->Active_dir;
+
+ /* Ensure each of these is NULL, in case init/allocation
+ fails and we end up calling ds_free on all three while only
+ one or two has been initialized. */
+ for (i = 0; i < 3; i++)
+ obstack_init_minimal (o[i]);
+
+ for (i = 0; i < 3; i++)
+ obstack_specify_allocation_with_arg
+ (o[i], 0, 0, rm_malloc, rm_free, &ds->current_arg_jumpbuf);
}
static void
obstack_free (&ds->dir_stack, NULL);
obstack_free (&ds->len_stack, NULL);
obstack_free (&ds->Active_dir, NULL);
- free (ds);
}
-/* Pop the active directory (AD) stack and move *DIRP `up' one level,
+/* Pop the active directory (AD) stack and prepare to move `up' one level,
safely. Moving `up' usually means opening `..', but when we've just
finished recursively processing a command-line directory argument,
- there's nothing left on the stack, so set *DIRP to NULL in that case.
- The idea is to return with *DIRP opened on the parent directory,
+ there's nothing left on the stack, so set *FDP to AT_FDCWD in that case.
+ The idea is to return with *FDP opened on the parent directory,
assuming there are entries in that directory that we need to remove.
+ Note that we must not call opendir (or fdopendir) just yet, since
+ the caller must first remove the directory we're coming from.
+ That is because some file system implementations cache readdir
+ results at opendir time; so calling opendir, rmdir, readdir would
+ return an entry for the just-removed directory.
+
Whenever using chdir '..' (virtually, now, via openat), verify
that the post-chdir dev/ino numbers for `.' match the saved ones.
- If any system call fails or if dev/ino don't match then give a
+ If any system call fails or if dev/ino don't match, then give a
diagnostic and longjump out.
- Set *PREV_DIR to the name (in malloc'd storage) of the
+ Return the name (in malloc'd storage) of the
directory (usually now empty) from which we're coming, and which
- corresponds to the input value of *DIRP. */
-static void
-AD_pop_and_chdir (DIR **dirp, Dirstack_state *ds, char **prev_dir)
+ corresponds to the input value of DIRP.
+
+ Finally, note that while this function's name is no longer as
+ accurate as it once was (it no longer calls chdir), it does open
+ the destination directory. */
+static char *
+AD_pop_and_chdir (DIR *dirp, int *fdp, Dirstack_state *ds)
{
- enum RM_status old_status = AD_stack_top(ds)->status;
+ struct AD_ent *leaf_dir_ent = AD_stack_top(ds);
+ struct dev_ino leaf_dev_ino = leaf_dir_ent->dev_ino;
+ enum RM_status old_status = leaf_dir_ent->status;
struct AD_ent *top;
/* Get the name of the current (but soon to be `previous') directory
from the top of the stack. */
- *prev_dir = top_dir (ds);
+ char *prev_dir = top_dir (ds);
AD_stack_pop (ds);
pop_dir (ds);
top = AD_stack_top (ds);
+ /* If the directory we're about to leave (and try to rmdir)
+ is the one whose dev_ino is being used to detect a cycle,
+ reset cycle_check_state.dev_ino to that of the parent.
+ Otherwise, once that directory is removed, its dev_ino
+ could be reused in the creation (by some other process)
+ of a directory that this rm process would encounter,
+ which would result in a false-positive cycle indication. */
+ CYCLE_CHECK_REFLECT_CHDIR_UP (&ds->cycle_check_state,
+ top->dev_ino, leaf_dev_ino);
+
/* Propagate any failure to parent. */
UPDATE_STATUS (top->status, old_status);
if (1 < AD_stack_height (ds))
{
struct stat sb;
- int fd = openat (dirfd (*dirp), "..", O_RDONLY);
- if (CLOSEDIR (*dirp) != 0)
+ int fd = openat (dirfd (dirp), "..", O_RDONLY);
+ if (closedir (dirp) != 0)
{
error (0, errno, _("FATAL: failed to close directory %s"),
- quote (full_filename (*prev_dir)));
- longjmp (ds->current_arg_jumpbuf, 1);
+ quote (full_filename (prev_dir)));
+ goto next_cmdline_arg;
}
- /* The above fails with EACCES when *DIRP is readable but not
+ /* The above fails with EACCES when DIRP is readable but not
searchable, when using Solaris' openat. Without this openat
call, tests/rm2 would fail to remove directories a/2 and a/3. */
if (fd < 0)
- fd = openat (AT_FDCWD, full_filename ("."), O_RDONLY);
+ fd = openat (AT_FDCWD, xfull_filename (ds, "."), O_RDONLY);
if (fd < 0)
{
error (0, errno, _("FATAL: cannot open .. from %s"),
- quote (full_filename (*prev_dir)));
- longjmp (ds->current_arg_jumpbuf, 1);
+ quote (full_filename (prev_dir)));
+ goto next_cmdline_arg;
}
if (fstat (fd, &sb))
error (0, errno,
_("FATAL: cannot ensure %s (returned to via ..) is safe"),
quote (full_filename (".")));
- close (fd);
- longjmp (ds->current_arg_jumpbuf, 1);
+ goto close_and_next;
}
/* Ensure that post-chdir dev/ino match the stored ones. */
{
error (0, 0, _("FATAL: directory %s changed dev/ino"),
quote (full_filename (".")));
+ close_and_next:;
close (fd);
- longjmp (ds->current_arg_jumpbuf, 1);
- }
- *dirp = fdopendir (fd);
- if (*dirp == NULL)
- {
- error (0, errno, _("FATAL: cannot return to .. from %s"),
- quote (full_filename (".")));
- close (fd);
+ next_cmdline_arg:;
+ free (prev_dir);
longjmp (ds->current_arg_jumpbuf, 1);
}
+ *fdp = fd;
}
else
{
- if (CLOSEDIR (*dirp) != 0)
+ if (closedir (dirp) != 0)
{
error (0, errno, _("FATAL: failed to close directory %s"),
- quote (full_filename (*prev_dir)));
- longjmp (ds->current_arg_jumpbuf, 1);
+ quote (full_filename (prev_dir)));
+ goto next_cmdline_arg;
}
- *dirp = NULL;
+ *fdp = AT_FDCWD;
}
+
+ return prev_dir;
}
-/* Initialize *HT if it is NULL.
- Insert FILENAME into HT. */
-static void
-AD_mark_helper (Hash_table **ht, char const *filename)
+/* Initialize *HT if it is NULL. Return *HT. */
+static Hash_table *
+AD_ensure_initialized (Hash_table **ht)
{
if (*ht == NULL)
{
if (*ht == NULL)
xalloc_die ();
}
- if (! hash_insert (*ht, filename))
+
+ return *ht;
+}
+
+/* Initialize *HT if it is NULL.
+ Insert FILENAME into HT. */
+static void
+AD_mark_helper (Hash_table **ht, char *filename)
+{
+ void *ent = hash_insert (AD_ensure_initialized (ht), filename);
+ if (ent == NULL)
xalloc_die ();
+ else
+ {
+ if (ent != filename)
+ free (filename);
+ }
}
/* Mark FILENAME (in current directory) as unremovable. */
AD_mark_current_as_unremovable (Dirstack_state *ds)
{
struct AD_ent *top = AD_stack_top (ds);
- char const *curr = top_dir (ds);
+ char *curr = top_dir (ds);
assert (1 < AD_stack_height (ds));
/* If our uses of openat are guaranteed not to
follow a symlink, then we can skip this check. */
- if ( ! OPEN_NO_FOLLOW_SYMLINK)
+ if (! HAVE_WORKING_O_NOFOLLOW)
{
struct stat sb;
if (fstat (fd_cwd, &sb) != 0)
}
}
+ if (cycle_check (&ds->cycle_check_state, dir_sb_from_parent))
+ {
+ error (0, 0, _("\
+WARNING: Circular directory structure.\n\
+This almost certainly means that you have a corrupted file system.\n\
+NOTIFY YOUR SYSTEM MANAGER.\n\
+The following directory is part of the cycle:\n %s\n"),
+ quote (full_filename (".")));
+ longjmp (ds->current_arg_jumpbuf, 1);
+ }
+
/* Extend the stack. */
obstack_blank (&ds->Active_dir, sizeof (struct AD_ent));
- {
- size_t n_lengths = obstack_object_size (&ds->len_stack) / sizeof (size_t);
- assert (AD_stack_height (ds) == n_lengths + 1);
- }
+ /* The active directory stack must be one larger than the length stack. */
+ assert (AD_stack_height (ds) ==
+ 1 + obstack_object_size (&ds->len_stack) / sizeof (size_t));
/* Fill in the new values. */
top = AD_stack_top (ds);
top->unremovable = NULL;
}
-static bool
+static inline bool
AD_is_removable (Dirstack_state const *ds, char const *file)
{
struct AD_ent *top = AD_stack_top (ds);
return ! (top->unremovable && hash_lookup (top->unremovable, file));
}
-/* Return true if DIR is determined to be an empty directory
- or if fdopendir or readdir fails. */
-static bool
-is_empty_dir (int fd_cwd, char const *dir)
-{
- DIR *dirp;
- struct dirent const *dp;
- int saved_errno;
- int fd = openat (fd_cwd, dir, O_RDONLY);
-
- if (fd < 0)
- return false;
-
- dirp = fdopendir (fd);
- if (dirp == NULL)
- {
- close (fd);
- return false;
- }
-
- errno = 0;
- dp = readdir_ignoring_dot_and_dotdot (dirp);
- saved_errno = errno;
- closedir (dirp);
- if (dp != NULL)
- return false;
- return saved_errno == 0 ? true : false;
-}
-
-/* Return true if FILE is determined to be an unwritable non-symlink.
- Otherwise, return false (including when lstat'ing it fails).
- If lstat (aka fstatat) succeeds, set *BUF_P to BUF.
+/* 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.
This is to avoid calling euidaccess when FILE is a symlink. */
-static bool
+static int
write_protected_non_symlink (int fd_cwd,
char const *file,
Dirstack_state const *ds,
- struct stat **buf_p,
struct stat *buf)
{
- if (fstatat (fd_cwd, file, buf, AT_SYMLINK_NOFOLLOW) != 0)
- return false;
- *buf_p = 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 false;
+ return 0;
/* Here, we know FILE is not a symbolic link. */
/* In order to be reentrant -- i.e., to avoid changing the working
Disadvantage: changes working directory (not reentrant) and can't
work if save_cwd fails.
- 3) if (euidaccess (full_filename (file), W_OK) == 0)
- Disadvantage: doesn't work if full_filename is too long.
+ 3) if (euidaccess (xfull_filename (file), W_OK) == 0)
+ Disadvantage: doesn't work if xfull_filename 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_filename (file), W_OK));
+ use method (3) (i.e., euidaccess (xfull_filename (file), 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.
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_filename (file), W_OK));
+ use method (3) (i.e., euidaccess (xfull_filename (file), W_OK));
Otherwise: look just at the file bits. Perhaps issue a warning
the first time this occurs.
size_t file_name_len
= obstack_object_size (&ds->dir_stack) + strlen (file);
- return (file_name_len < MIN (PATH_MAX, 8192)
- ? euidaccess (full_filename (file), W_OK) != 0 && errno == EACCES
- : euidaccess_stat (buf, W_OK) != 0);
+ if (MIN (PATH_MAX, 8192) <= file_name_len)
+ return ! euidaccess_stat (buf, W_OK);
+ if (euidaccess (xfull_filename (ds, file), W_OK) == 0)
+ return 0;
+ if (errno == EACCES)
+ {
+ errno = 0;
+ return 1;
+ }
+
+ /* Perhaps some other process has removed the file, or perhaps this
+ is a buggy NFS client. */
+ return -1;
}
}
return RM_USER_DECLINED. If not ignoring missing files and we
cannot lstat FILENAME, then return RM_ERROR.
+ *PDIRENT_TYPE is the type of the directory entry; update it to DT_DIR
+ or DT_LNK as needed. *SBUF is the file's status.
+
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 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 when MODE == PA_REMOVE_DIR.
- Set *IS_DIR to T_YES or T_NO if we happen to determine whether
- FILENAME is a directory. */
+ Don't even try to set *IS_EMPTY when MODE == PA_REMOVE_DIR. */
static enum RM_status
prompt (int fd_cwd, Dirstack_state const *ds, char const *filename,
+ int *pdirent_type, struct stat *sbuf,
struct rm_options const *x, enum Prompt_action mode,
- Ternary *is_dir, Ternary *is_empty)
+ Ternary *is_empty)
{
- bool write_protected = false;
- struct stat *sbuf = NULL;
- struct stat buf;
+ int write_protected = 0;
+ int dirent_type = *pdirent_type;
*is_empty = T_UNKNOWN;
- *is_dir = T_UNKNOWN;
- if (((!x->ignore_missing_files & (x->interactive | x->stdin_tty))
- && (write_protected = write_protected_non_symlink (fd_cwd, filename,
- ds, &sbuf, &buf)))
- || x->interactive)
+ 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, ds, sbuf);
+ wp_errno = errno;
+ }
+
+ if (write_protected || x->interactive == RMI_ALWAYS)
{
- if (sbuf == NULL)
+ if (0 <= write_protected && dirent_type == DT_UNKNOWN)
{
- sbuf = &buf;
- if (fstatat (fd_cwd, filename, sbuf, AT_SYMLINK_NOFOLLOW))
+ if (cache_fstatat (fd_cwd, filename, sbuf, AT_SYMLINK_NOFOLLOW) == 0)
{
- /* lstat failed. This happens e.g., with `rm '''. */
- error (0, errno, _("cannot remove %s"),
- quote (full_filename (filename)));
- return RM_ERROR;
+ 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. */
+ *pdirent_type = dirent_type;
+ }
+ else
+ {
+ /* This happens, e.g., with `rm '''. */
+ write_protected = -1;
+ wp_errno = errno;
}
}
- if (S_ISDIR (sbuf->st_mode) && !x->recursive)
+ 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:
+ if (!x->recursive)
+ {
+ write_protected = -1;
+ wp_errno = EISDIR;
+ }
+ break;
+ }
+
+ char const *quoted_name = quote (full_filename (filename));
+
+ if (write_protected < 0)
{
- error (0, EISDIR, _("cannot remove directory %s"),
- quote (full_filename (filename)));
+ error (0, wp_errno, _("cannot remove %s"), quoted_name);
return RM_ERROR;
}
- /* Using permissions doesn't make sense for symlinks. */
- if (S_ISLNK (sbuf->st_mode))
+ /* Issue the prompt. */
+ /* FIXME: use a variant of error (instead of fprintf) that doesn't
+ append a newline. Then we won't have to declare program_name in
+ this file. */
+ if (dirent_type == DT_DIR
+ && mode == PA_DESCEND_INTO_DIR
+ && ((*is_empty = (is_empty_dir (fd_cwd, filename) ? T_YES : T_NO))
+ == T_NO))
+ fprintf (stderr,
+ (write_protected
+ ? _("%s: descend into write-protected directory %s? ")
+ : _("%s: descend into directory %s? ")),
+ program_name, quoted_name);
+ else
{
- if ( ! x->interactive)
- return RM_OK;
- write_protected = false;
- }
+ if (cache_fstatat (fd_cwd, filename, sbuf, AT_SYMLINK_NOFOLLOW) != 0)
+ {
+ error (0, errno, _("cannot remove %s"), quoted_name);
+ return RM_ERROR;
+ }
- /* Issue the prompt. */
- {
- char const *quoted_name = quote (full_filename (filename));
-
- *is_dir = (S_ISDIR (sbuf->st_mode) ? T_YES : T_NO);
-
- /* FIXME: use a variant of error (instead of fprintf) that doesn't
- append a newline. Then we won't have to declare program_name in
- this file. */
- if (S_ISDIR (sbuf->st_mode)
- && x->recursive
- && mode == PA_DESCEND_INTO_DIR
- && ((*is_empty = (is_empty_dir (fd_cwd, filename) ? T_YES : T_NO))
- == T_NO))
fprintf (stderr,
(write_protected
- ? _("%s: descend into write-protected directory %s? ")
- : _("%s: descend into directory %s? ")),
- program_name, quoted_name);
- else
- {
- /* TRANSLATORS: You may find it more convenient to translate
- the equivalent of _("%s: remove %s (write-protected) %s? ").
- It should avoid grammatical problems with the output
- of file_type. */
- fprintf (stderr,
- (write_protected
- ? _("%s: remove write-protected %s %s? ")
- : _("%s: remove %s %s? ")),
- program_name, file_type (sbuf), quoted_name);
- }
+ /* TRANSLATORS: You may find it more convenient to
+ translate "%s: remove %s (write-protected) %s? "
+ instead. It should avoid grammatical problems
+ with the output of file_type. */
+ ? _("%s: remove write-protected %s %s? ")
+ : _("%s: remove %s %s? ")),
+ program_name, file_type (sbuf), quoted_name);
+ }
- if (!yesno ())
- return RM_USER_DECLINED;
- }
+ if (!yesno ())
+ return RM_USER_DECLINED;
}
return RM_OK;
}
/* Return true if FILENAME is a directory (and not a symlink to a directory).
Otherwise, including the case in which lstat fails, return false.
+ *ST is FILENAME's tstatus.
Do not modify errno. */
static inline bool
-is_dir_lstat (char const *filename)
+is_dir_lstat (int fd_cwd, char const *filename, struct stat *st)
{
- struct stat sbuf;
int saved_errno = errno;
- bool is_dir = lstat (filename, &sbuf) == 0 && S_ISDIR (sbuf.st_mode);
+ bool is_dir =
+ (cache_fstatat (fd_cwd, filename, st, AT_SYMLINK_NOFOLLOW) == 0
+ && S_ISDIR (st->st_mode));
errno = saved_errno;
return is_dir;
}
-#if HAVE_STRUCT_DIRENT_D_TYPE
-
-/* True if the type of the directory entry D is known. */
-# define DT_IS_KNOWN(d) ((d)->d_type != DT_UNKNOWN)
-
-/* True if the type of the directory entry D must be T. */
-# define DT_MUST_BE(d, t) ((d)->d_type == (t))
-
-#else
-# define DT_IS_KNOWN(d) false
-# define DT_MUST_BE(d, t) false
-#endif
+/* Return true if FILENAME is a non-directory.
+ Otherwise, including the case in which lstat fails, return false.
+ *ST is FILENAME's tstatus.
+ Do not modify errno. */
+static inline bool
+is_nondir_lstat (int fd_cwd, char const *filename, struct stat *st)
+{
+ int saved_errno = errno;
+ bool is_non_dir =
+ (cache_fstatat (fd_cwd, filename, st, AT_SYMLINK_NOFOLLOW) == 0
+ && !S_ISDIR (st->st_mode));
+ errno = saved_errno;
+ return is_non_dir;
+}
#define DO_UNLINK(Fd_cwd, Filename, X) \
do \
return RM_OK; \
} \
\
- if (errno == ENOENT && (X)->ignore_missing_files) \
+ if (ignorable_missing (X, errno)) \
return RM_OK; \
} \
while (0)
return RM_OK; \
} \
\
- if (errno == ENOENT && (X)->ignore_missing_files) \
+ if (ignorable_missing (X, errno)) \
return RM_OK; \
\
if (errno == ENOTEMPTY || errno == EEXIST) \
} \
while (0)
+/* 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. */
+
+ switch (errnum)
+ {
+ 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);
+}
+
/* Remove the file or directory specified by FILENAME.
Return RM_OK if it is removed, and RM_ERROR or RM_USER_DECLINED if not.
But if FILENAME specifies a non-empty directory, return RM_NONEMPTY_DIR. */
static enum RM_status
remove_entry (int fd_cwd, Dirstack_state const *ds, char const *filename,
- struct rm_options const *x, struct dirent const *dp)
+ int dirent_type_arg, struct stat *st,
+ struct rm_options const *x)
{
- Ternary is_dir;
Ternary is_empty_directory;
- enum RM_status s = prompt (fd_cwd, ds, filename, x, PA_DESCEND_INTO_DIR,
- &is_dir, &is_empty_directory);
-
+ enum RM_status s = prompt (fd_cwd, ds, filename, &dirent_type_arg, st, x,
+ PA_DESCEND_INTO_DIR,
+ &is_empty_directory);
+ int dirent_type = dirent_type_arg;
if (s != RM_OK)
return s;
if (cannot_unlink_dir ())
{
- if (is_dir == T_YES && ! x->recursive)
+ if (dirent_type == DT_DIR && ! x->recursive)
{
- error (0, EISDIR, _("cannot remove directory %s"),
+ error (0, EISDIR, _("cannot remove %s"),
quote (full_filename (filename)));
return RM_ERROR;
}
/* If we happen to know that FILENAME is a directory, return now
and let the caller remove it -- this saves the overhead of a failed
- unlink call. If FILENAME is a command-line argument, then dp is NULL,
- so we'll first try to unlink it. Using unlink here is ok, because it
- cannot remove a directory. */
- if ((dp && DT_MUST_BE (dp, DT_DIR)) || is_dir == T_YES)
+ unlink call. If FILENAME is a command-line argument, then
+ DIRENT_TYPE is DT_UNKNOWN so we'll first try to unlink it.
+ Using unlink here is ok, because it cannot remove a
+ directory. */
+ if (dirent_type == DT_DIR)
return RM_NONEMPTY_DIR;
DO_UNLINK (fd_cwd, filename, x);
/* Upon a failed attempt to unlink a directory, most non-Linux systems
set errno to the POSIX-required value EPERM. In that case, change
errno to EISDIR so that we emit a better diagnostic. */
- if (! x->recursive && errno == EPERM && is_dir_lstat (filename))
+ if (! x->recursive && errno == EPERM && is_dir_lstat (fd_cwd,
+ filename, st))
errno = EISDIR;
if (! x->recursive
- || errno == ENOENT || errno == ENOTDIR
- || errno == ELOOP || errno == ENAMETOOLONG)
+ || (cache_stat_ok (st) && !S_ISDIR (st->st_mode))
+ || ((errno == EACCES || errno == EPERM)
+ && is_nondir_lstat (fd_cwd, filename, st))
+ )
{
+ if (ignorable_missing (x, errno))
+ return RM_OK;
+
/* Either --recursive is not in effect, or the file cannot be a
directory. Report the unlink problem and fail. */
error (0, errno, _("cannot remove %s"),
quote (full_filename (filename)));
return RM_ERROR;
}
+ assert (!cache_stat_ok (st) || S_ISDIR (st->st_mode));
}
else
{
- /* If we don't already know whether FILENAME is a directory, find out now.
- Then, if it's a non-directory, we can use unlink on it. */
- if (is_dir == T_UNKNOWN)
+ /* If we don't already know whether FILENAME is a directory,
+ find out now. Then, if it's a non-directory, we can use
+ unlink on it. */
+
+ if (dirent_type == DT_UNKNOWN)
{
- if (dp && DT_IS_KNOWN (dp))
- is_dir = DT_MUST_BE (dp, DT_DIR) ? T_YES : T_NO;
- else
+ if (fstatat (fd_cwd, filename, st, AT_SYMLINK_NOFOLLOW))
{
- struct stat sbuf;
- if (fstatat (fd_cwd, filename, &sbuf, AT_SYMLINK_NOFOLLOW))
- {
- if (errno == ENOENT && x->ignore_missing_files)
- return RM_OK;
-
- error (0, errno, _("cannot remove %s"),
- quote (full_filename (filename)));
- return RM_ERROR;
- }
-
- is_dir = S_ISDIR (sbuf.st_mode) ? T_YES : T_NO;
+ if (ignorable_missing (x, errno))
+ return RM_OK;
+
+ error (0, errno, _("cannot remove %s"),
+ quote (full_filename (filename)));
+ return RM_ERROR;
}
+
+ if (S_ISDIR (st->st_mode))
+ dirent_type = DT_DIR;
}
- if (is_dir == T_NO)
+ if (dirent_type != DT_DIR)
{
/* At this point, barring race conditions, FILENAME is known
to be a non-directory, so it's ok to try to unlink it. */
if (! x->recursive)
{
- error (0, EISDIR, _("cannot remove directory %s"),
+ error (0, EISDIR, _("cannot remove %s"),
quote (full_filename (filename)));
return RM_ERROR;
}
unlink- or rmdir-like system call -- use that value instead of ENOTDIR
if an opened file turns out not to be a directory. This is important
when the preceding non-dir-unlink failed due to e.g., EPERM or EACCES.
- The caller must set OPENAT_CWD_RESTORE_ALLOW_FAILURE to true the first
+ The caller must use a nonnnull CWD_ERRNO the first
time this function is called for each command-line-specified directory.
- Set *CWD_RESTORE_FAILED if OPENAT_CWD_RESTORE_ALLOW_FAILURE is true
- and openat_ro fails to restore the initial working directory.
- CWD_RESTORE_FAILED may be NULL. */
+ If CWD_ERRNO is not null, set *CWD_ERRNO to the appropriate error number
+ if this function fails to restore the initial working directory.
+ If it is null, report an error and exit if the working directory
+ isn't restored. */
static DIR *
fd_to_subdirp (int fd_cwd, char const *f,
- struct rm_options const *x, int prev_errno,
- struct stat *subdir_sb, Dirstack_state *ds,
- bool openat_cwd_restore_allow_failure,
- bool *cwd_restore_failed)
+ int prev_errno,
+ struct stat *subdir_sb,
+ int *cwd_errno ATTRIBUTE_UNUSED)
{
- int fd_sub;
- bool dummy;
+ int open_flags = O_RDONLY | O_NOCTTY | O_NOFOLLOW | O_NONBLOCK;
+ int fd_sub = openat_permissive (fd_cwd, f, open_flags, 0, cwd_errno);
+ int saved_errno;
/* Record dev/ino of F. We may compare them against saved values
to thwart any attempt to subvert the traversal. They are also used
to detect directory cycles. */
- if ((fd_sub = (openat_cwd_restore_allow_failure
- ? openat_ro (fd_cwd, f, O_RDONLY | OPEN_NO_FOLLOW_SYMLINK,
- (cwd_restore_failed
- ? cwd_restore_failed : &dummy))
- : openat (fd_cwd, f, O_RDONLY | OPEN_NO_FOLLOW_SYMLINK))) < 0
- || fstat (fd_sub, subdir_sb) != 0)
+ if (fd_sub < 0)
+ return NULL;
+ else if (fstat (fd_sub, subdir_sb) != 0)
+ saved_errno = errno;
+ else if (S_ISDIR (subdir_sb->st_mode))
{
- if (errno != ENOENT || !x->ignore_missing_files)
- error (0, errno,
- _("cannot remove %s"), quote (full_filename (f)));
- if (0 <= fd_sub)
- close (fd_sub);
- return NULL;
+ DIR *subdir_dirp = fdopendir (fd_sub);
+ if (subdir_dirp)
+ return subdir_dirp;
+ saved_errno = errno;
}
+ else
+ saved_errno = (prev_errno ? prev_errno : ENOTDIR);
+
+ close (fd_sub);
+ errno = saved_errno;
+ return NULL;
+}
+
+struct readdir_data
+{
+ ino_t ino;
+ char name[FLEXIBLE_ARRAY_MEMBER];
+};
+
+#if HAVE_STRUCT_DIRENT_D_TYPE
+/* A comparison function to sort on increasing inode number. */
+static int
+compare_ino (void const *av, void const *bv)
+{
+ struct readdir_data const *const *a = av;
+ struct readdir_data const *const *b = bv;
+ return (a[0]->ino < b[0]->ino ? -1
+ : b[0]->ino < a[0]->ino ? 1 : 0);
+}
+
+/* Return an approximation of the maximum number of dirent entries
+ in a directory with stat data *ST. */
+static size_t
+dirent_count (struct stat const *st)
+{
+ return st->st_size / 16;
+}
- if (! S_ISDIR (subdir_sb->st_mode))
+# if HAVE_SYS_VFS_H && HAVE_FSTATFS && HAVE_STRUCT_STATFS_F_TYPE
+# include <sys/vfs.h>
+# include "fs.h"
+
+/* Return false if it is easy to determine the file system type of
+ the directory on which DIR_FD is open, and sorting dirents on
+ inode numbers is known not to improve traversal performance with
+ that type of file system. Otherwise, return true. */
+static bool
+dirent_inode_sort_may_be_useful (int dir_fd)
+{
+ /* Skip the sort only if we can determine efficiently
+ that skipping it is the right thing to do.
+ The cost of performing an unnecessary sort is negligible,
+ while the cost of *not* performing it can be O(N^2) with
+ a very large constant. */
+ struct statfs fs_buf;
+
+ /* If fstatfs fails, assume sorting would be useful. */
+ if (fstatfs (dir_fd, &fs_buf) != 0)
+ return true;
+
+ /* FIXME: what about when f_type is not an integral type?
+ deal with that if/when it's encountered. */
+ switch (fs_buf.f_type)
{
- error (0, prev_errno ? prev_errno : ENOTDIR, _("cannot remove %s"),
- quote (full_filename (f)));
- close (fd_sub);
- return NULL;
+ case S_MAGIC_TMPFS:
+ case S_MAGIC_NFS:
+ /* On a file system of any of these types, sorting
+ is unnecessary, and hence wasteful. */
+ return false;
+
+ default:
+ return true;
}
+}
+# else /* !HAVE_STRUCT_STATFS_F_TYPE */
+static bool dirent_inode_sort_may_be_useful (int dir_fd) { return true; }
+# endif /* !HAVE_STRUCT_STATFS_F_TYPE */
+#endif /* HAVE_STRUCT_DIRENT_D_TYPE */
+
+/* When a directory contains very many entries, operating on N entries in
+ readdir order can be very seek-intensive (be it to unlink or even to
+ merely stat each entry), to the point that it results in O(N^2) work.
+ This is file-system-specific: ext3 and ext4 (as of 2008) are susceptible,
+ but tmpfs is not. The general solution is to process entries in inode
+ order. That means reading all entries, and sorting them before operating
+ on any. As such, it is useful only on systems with useful dirent.d_ino.
+ Since 'rm -r's removal process must traverse into directories and since
+ this preprocessing phase can allocate O(N) storage, here we store and
+ sort only non-directory entries, and then remove all of those, so that we
+ can free all allocated storage before traversing into any subdirectory.
+ Perform this optimization only when not interactive and not in verbose
+ mode, to keep the implementation simple and to minimize duplication.
+ Upon failure, simply free any resources and return. */
+static void
+preprocess_dir (DIR **dirp, struct rm_options const *x)
+{
+#if HAVE_STRUCT_DIRENT_D_TYPE
+
+ struct stat st;
+ /* There are many reasons to return right away, skipping this
+ optimization. The penalty for being wrong is that we will
+ perform a small amount of extra work.
- DIR *subdir_dirp = fdopendir (fd_sub);
- if (subdir_dirp == NULL)
+ Skip this optimization if... */
+
+ int dir_fd = dirfd (*dirp);
+ if (/* - there is a chance of interactivity */
+ x->interactive != RMI_NEVER
+
+ /* - we're in verbose mode */
+ || x->verbose
+
+ /* - privileged users can unlink nonempty directories.
+ Otherwise, there'd be a race condition between the readdir
+ call (in which we learn dirent.d_type) and the unlink, by
+ which time the non-directory may be replaced with a directory. */
+ || ! cannot_unlink_dir ()
+
+ /* - we can't fstat the file descriptor */
+ || fstat (dir_fd, &st) != 0
+
+ /* - the directory is smaller than some threshold.
+ Estimate the number of inodes with a heuristic.
+ There's no significant benefit to sorting if there are
+ too few inodes. */
+ || dirent_count (&st) < INODE_SORT_DIR_ENTRIES_THRESHOLD
+
+ /* Sort only if it might help. */
+ || ! dirent_inode_sort_may_be_useful (dir_fd))
+ return;
+
+ /* FIXME: maybe test file system type, too; skip if it's tmpfs: see fts.c */
+
+ struct obstack o_readdir_data; /* readdir data: inode,name pairs */
+ struct obstack o_p; /* an array of pointers to each inode,name pair */
+
+ /* Arrange to longjmp upon obstack allocation failure. */
+ jmp_buf readdir_jumpbuf;
+ if (setjmp (readdir_jumpbuf))
+ goto cleanup;
+
+ obstack_init_minimal (&o_readdir_data);
+ obstack_init_minimal (&o_p);
+
+ obstack_specify_allocation_with_arg (&o_readdir_data, 0, 0,
+ rm_malloc, rm_free, &readdir_jumpbuf);
+ obstack_specify_allocation_with_arg (&o_p, 0, 0,
+ rm_malloc, rm_free, &readdir_jumpbuf);
+
+ /* Read all entries, storing <d_ino, d_name> for each non-dir one.
+ Maintain a parallel list of pointers into the primary buffer. */
+ while (1)
+ {
+ struct dirent const *dp;
+ dp = readdir_ignoring_dot_and_dotdot (*dirp);
+ /* no need to distinguish EOF from failure */
+ if (dp == NULL)
+ break;
+
+ /* Skip known-directory and type-unknown entries. */
+ if (D_TYPE (dp) == DT_UNKNOWN || D_TYPE (dp) == DT_DIR)
+ break;
+
+ size_t name_len = strlen (dp->d_name);
+ size_t ent_len = offsetof (struct readdir_data, name) + name_len + 1;
+ struct readdir_data *v = obstack_alloc (&o_readdir_data, ent_len);
+ v->ino = D_INO (dp);
+ memcpy (v->name, dp->d_name, name_len + 1);
+
+ /* Append V to the list of pointers. */
+ obstack_ptr_grow (&o_p, v);
+ }
+
+ /* Compute size and finalize VV. */
+ size_t n = obstack_object_size (&o_p) / sizeof (void *);
+ struct readdir_data **vv = obstack_finish (&o_p);
+
+ /* Sort on inode number. */
+ qsort(vv, n, sizeof *vv, compare_ino);
+
+ /* Iterate through those pointers, unlinking each name. */
+ for (size_t i = 0; i < n; i++)
{
- if (errno != ENOENT || !x->ignore_missing_files)
- error (0, errno, _("cannot open directory %s"),
- quote (full_filename (f)));
- close (fd_sub);
- return NULL;
+ /* ignore failure */
+ (void) unlinkat (dir_fd, vv[i]->name, 0);
}
- return subdir_dirp;
+ cleanup:
+ obstack_free (&o_readdir_data, NULL);
+ obstack_free (&o_p, NULL);
+ rewinddir (*dirp);
+#endif
}
/* Remove entries in the directory open on DIRP
assert (VALID_STATUS (status));
*subdir = NULL;
+ /* This is just an optimization.
+ It's not a fatal problem if it fails. */
+ preprocess_dir (dirp, x);
+
while (1)
{
struct dirent const *dp;
{
/* fall through */
}
- else if (CONSECUTIVE_READDIR_UNLINK_THRESHOLD
- < n_unlinked_since_opendir_or_last_rewind)
+ else if (NEED_REWIND (n_unlinked_since_opendir_or_last_rewind))
{
/* Call rewinddir if we've called unlink or rmdir so many times
(since the opendir or the previous rewinddir) that this
case can decide whether to use unlink or chdir.
Systems without the d_type member will have to endure
the performance hit of first calling lstat F. */
- tmp_status = remove_entry (dirfd (*dirp), ds, f, x, dp);
+ cache_stat_init (subdir_sb);
+ tmp_status = remove_entry (dirfd (*dirp), ds, f,
+ D_TYPE (dp), subdir_sb, x);
switch (tmp_status)
{
case RM_OK:
case RM_NONEMPTY_DIR:
{
DIR *subdir_dirp = fd_to_subdirp (dirfd (*dirp), f,
- x, errno, subdir_sb, ds,
- OPENAT_CWD_RESTORE__REQUIRE,
- NULL);
+ errno, subdir_sb, NULL);
if (subdir_dirp == NULL)
{
- AD_mark_as_unremovable (ds, f);
status = RM_ERROR;
- break;
- }
- if (cycle_check (&ds->cycle_check_state, subdir_sb))
- {
- error (0, 0, _("\
-WARNING: Circular directory structure.\n\
-This almost certainly means that you have a corrupted file system.\n\
-NOTIFY YOUR SYSTEM MANAGER.\n\
-The following directory is part of the cycle:\n %s\n"),
- quote (full_filename (".")));
- longjmp (ds->current_arg_jumpbuf, 1);
+ /* CAUTION: this test and diagnostic are identical to
+ those following the other use of fd_to_subdirp. */
+ if (ignorable_missing (x, errno))
+ {
+ /* With -f, don't report "file not found". */
+ }
+ else
+ {
+ /* Upon fd_to_subdirp failure, try to remove F directly,
+ in case it's just an empty directory. */
+ int saved_errno = errno;
+ if (unlinkat (dirfd (*dirp), f, AT_REMOVEDIR) == 0)
+ status = RM_OK;
+ else
+ error (0, saved_errno,
+ _("cannot remove %s"), quote (full_filename (f)));
+ }
+
+ if (status == RM_ERROR)
+ AD_mark_as_unremovable (ds, f);
+ break;
}
*subdir = xstrdup (f);
- if (CLOSEDIR (*dirp) != 0)
+ if (closedir (*dirp) != 0)
{
error (0, 0, _("failed to close directory %s"),
quote (full_filename (".")));
static enum RM_status
remove_dir (int fd_cwd, Dirstack_state *ds, char const *dir,
- struct rm_options const *x, bool *cwd_restore_failed)
+ struct stat *dir_st,
+ struct rm_options const *x, int *cwd_errno)
{
enum RM_status status;
- struct stat dir_sb;
+ dev_t current_dev = dir_st->st_dev;
/* There is a race condition in that an attacker could replace the nonempty
directory, DIR, with a symlink between the preceding call to rmdir
fd_to_subdirp's fstat, along with the `fstat' and the dev/ino
comparison in AD_push ensure that we detect it and fail. */
- DIR *dirp = fd_to_subdirp (fd_cwd, dir, x, 0, &dir_sb, ds,
- OPENAT_CWD_RESTORE__ALLOW_FAILURE,
- cwd_restore_failed);
+ DIR *dirp = fd_to_subdirp (fd_cwd, dir, 0, dir_st, cwd_errno);
if (dirp == NULL)
- return RM_ERROR;
+ {
+ /* CAUTION: this test and diagnostic are identical to
+ those following the other use of fd_to_subdirp. */
+ if (ignorable_missing (x, errno))
+ {
+ /* With -f, don't report "file not found". */
+ }
+ else
+ {
+ /* Upon fd_to_subdirp failure, try to remove DIR directly,
+ in case it's just an empty directory. */
+ int saved_errno = errno;
+ if (unlinkat (fd_cwd, dir, AT_REMOVEDIR) == 0)
+ return RM_OK;
+
+ error (0, saved_errno,
+ _("cannot remove %s"), quote (full_filename (dir)));
+ }
- if (ROOT_DEV_INO_CHECK (x->root_dev_ino, &dir_sb))
+ return RM_ERROR;
+ }
+
+ if (ROOT_DEV_INO_CHECK (x->root_dev_ino, dir_st))
{
ROOT_DEV_INO_WARN (full_filename (dir));
status = RM_ERROR;
goto closedir_and_return;
}
- AD_push (dirfd (dirp), ds, dir, &dir_sb);
+ AD_push (dirfd (dirp), ds, dir, dir_st);
AD_INIT_OTHER_MEMBERS ();
status = RM_OK;
}
if (subdir)
{
- AD_push (dirfd (dirp), ds, subdir, &subdir_sb);
- AD_INIT_OTHER_MEMBERS ();
+ if ( ! x->one_file_system
+ || subdir_sb.st_dev == current_dev)
+ {
+ AD_push (dirfd (dirp), ds, subdir, &subdir_sb);
+ AD_INIT_OTHER_MEMBERS ();
+ free (subdir);
+ continue;
+ }
+ /* Here, --one-file-system is in effect, and with remove_cwd_entries'
+ traversal into the current directory, (known as SUBDIR, from ..),
+ DIRP's device number is different from CURRENT_DEV. Arrange not
+ to do anything more with this hierarchy. */
+ error (0, 0, _("skipping %s, since it's on a different device"),
+ quote (full_filename (subdir)));
free (subdir);
- continue;
+ AD_mark_current_as_unremovable (ds);
+ tmp_status = RM_ERROR;
+ UPDATE_STATUS (status, tmp_status);
}
/* Execution reaches this point when we've removed the last
- removable entry from the current directory. */
+ removable entry from the current directory -- or, with
+ --one-file-system, when the current directory is on a
+ different file system. */
{
+ int fd;
/* The name of the directory that we have just processed,
nominally removing all of its contents. */
- char *empty_dir;
-
- AD_pop_and_chdir (&dirp, ds, &empty_dir);
- int fd = (dirp != NULL ? dirfd (dirp) : AT_FDCWD);
- assert (dirp != NULL || AD_stack_height (ds) == 1);
+ char *empty_dir = AD_pop_and_chdir (dirp, &fd, ds);
+ dirp = NULL;
+ assert (fd != AT_FDCWD || AD_stack_height (ds) == 1);
/* Try to remove EMPTY_DIR only if remove_cwd_entries succeeded. */
if (tmp_status == RM_OK)
{
- /* This does a little more work than necessary when it actually
- prompts the user. E.g., we already know that D is a directory
- and that it's almost certainly empty, yet we lstat it.
- But that's no big deal since we're interactive. */
- Ternary is_dir;
+ struct stat empty_st;
Ternary is_empty;
- enum RM_status s = prompt (fd, ds, empty_dir, x,
- PA_REMOVE_DIR, &is_dir, &is_empty);
+ int dirent_type = DT_DIR;
+ enum RM_status s = prompt (fd, ds, empty_dir, &dirent_type,
+ cache_stat_init (&empty_st), x,
+ PA_REMOVE_DIR, &is_empty);
if (s != RM_OK)
{
free (empty_dir);
status = s;
+ if (fd != AT_FDCWD)
+ close (fd);
goto closedir_and_return;
}
free (empty_dir);
- if (AD_stack_height (ds) == 1)
+ if (fd == AT_FDCWD)
break;
+
+ dirp = fdopendir (fd);
+ if (dirp == NULL)
+ {
+ error (0, errno, _("FATAL: cannot return to .. from %s"),
+ quote (full_filename (".")));
+ close (fd);
+ longjmp (ds->current_arg_jumpbuf, 1);
+ }
}
}
AD_stack_pop (ds);
closedir_and_return:;
- if (dirp != NULL && CLOSEDIR (dirp) != 0)
+ if (dirp != NULL && closedir (dirp) != 0)
{
error (0, 0, _("failed to close directory %s"),
quote (full_filename (".")));
static enum RM_status
rm_1 (Dirstack_state *ds, char const *filename,
- struct rm_options const *x, bool *cwd_restore_failed)
+ struct rm_options const *x, int *cwd_errno)
{
- char const *base = base_name (filename);
- if (DOT_OR_DOTDOT (base))
+ char const *base = last_component (filename);
+ if (dot_or_dotdot (base))
{
- error (0, 0, _("cannot remove `.' or `..'"));
+ error (0, 0, _(base == filename
+ ? N_("cannot remove directory %s")
+ : N_("cannot remove %s directory %s")),
+ quote_n (0, base), quote_n (1, filename));
return RM_ERROR;
}
+ struct stat st;
+ cache_stat_init (&st);
+ cycle_check_init (&ds->cycle_check_state);
+ if (x->root_dev_ino)
+ {
+ if (cache_fstatat (AT_FDCWD, filename, &st, AT_SYMLINK_NOFOLLOW) != 0)
+ {
+ if (ignorable_missing (x, errno))
+ return RM_OK;
+ error (0, errno, _("cannot remove %s"), quote (filename));
+ return RM_ERROR;
+ }
+ if (SAME_INODE (st, *(x->root_dev_ino)))
+ {
+ error (0, 0, _("cannot remove root directory %s"), quote (filename));
+ return RM_ERROR;
+ }
+ }
+
AD_push_initial (ds);
AD_INIT_OTHER_MEMBERS ();
- /* Put `status' in static storage, so it can't be clobbered
- by the potential longjmp into this function. */
- static enum RM_status status;
- int fd_cwd = AT_FDCWD;
- status = remove_entry (fd_cwd, ds, filename, x, NULL);
+ enum RM_status status = remove_entry (AT_FDCWD, ds, filename,
+ DT_UNKNOWN, &st, x);
if (status == RM_NONEMPTY_DIR)
{
/* In the event that remove_dir->remove_cwd_entries detects
if (setjmp (ds->current_arg_jumpbuf))
status = RM_ERROR;
else
- {
- bool t_cwd_restore_failed = false;
- status = remove_dir (fd_cwd, ds, filename, x, &t_cwd_restore_failed);
- *cwd_restore_failed |= t_cwd_restore_failed;
- }
+ status = remove_dir (AT_FDCWD, ds, filename, &st, x, cwd_errno);
+
+ AD_stack_clear (ds);
}
ds_clear (ds);
-
return status;
}
rm (size_t n_files, char const *const *file, struct rm_options const *x)
{
enum RM_status status = RM_OK;
- Dirstack_state *ds = ds_init ();
- bool cwd_restore_failed = false;
+ Dirstack_state ds;
+ int cwd_errno = 0;
size_t i;
+ /* Arrange for obstack allocation failure to longjmp. */
+ if (setjmp (ds.current_arg_jumpbuf))
+ {
+ status = RM_ERROR;
+ goto cleanup;
+ }
+
+ ds_init (&ds);
+
for (i = 0; i < n_files; i++)
{
- if (cwd_restore_failed && IS_RELATIVE_FILE_NAME (file[i]))
+ if (cwd_errno && IS_RELATIVE_FILE_NAME (file[i]))
{
error (0, 0, _("cannot remove relative-named %s"), quote (file[i]));
status = RM_ERROR;
- continue;
}
-
- cycle_check_init (&ds->cycle_check_state);
- enum RM_status s = rm_1 (ds, file[i], x, &cwd_restore_failed);
- assert (VALID_STATUS (s));
- UPDATE_STATUS (status, s);
+ else
+ {
+ enum RM_status s = rm_1 (&ds, file[i], x, &cwd_errno);
+ assert (VALID_STATUS (s));
+ UPDATE_STATUS (status, s);
+ }
}
- if (x->require_restore_cwd && cwd_restore_failed)
+ if (x->require_restore_cwd && cwd_errno)
{
- error (0, 0,
+ error (0, cwd_errno,
_("cannot restore current working directory"));
status = RM_ERROR;
}
- ds_free (ds);
+ cleanup:;
+ ds_free (&ds);
return status;
}