diff options
author | Jim Meyering <jim@meyering.net> | 2002-06-02 20:49:03 +0000 |
---|---|---|
committer | Jim Meyering <jim@meyering.net> | 2002-06-02 20:49:03 +0000 |
commit | fb205872c1038139e118022014db0d6d817c34ec (patch) | |
tree | 6c69bc525f6a88ecb5c22791dd995b611858909a /src | |
parent | a4ff1d036daabb6d6a9f96543b308167cdf23550 (diff) | |
download | coreutils-fb205872c1038139e118022014db0d6d817c34ec.tar.xz |
Merge in all changes from no-recursion branch.
* src/remove.c (enum Ternary): Define type.
(prompt): Add a parameter. Adjust callers.
(remove_entry): Attempt rmdir here, only if a directory is
`known' to be empty. Significant rework.
(remove_dir): Propagate failure `up' also when rmdir fails.
In interactive mode, prompt only once about an empty directory.
* src/remove.c (enum Prompt_action): Define.
(prompt): Two new parameters. Adjust all callers.
Performance.
* src/remove.c (remove_entry) [!ROOT_CAN_UNLINK_DIRS]:
Don't call rmdir here.
* src/remove.c (AD_pop_and_chdir): Don't use errno (it's not valid)
in diagnostic for changed dev/ino.
(remove_entry): Tweak diagnostic.
* src/remove.c (ROOT_CAN_UNLINK_DIRS): Define.
(AD_pop_and_chdir): Propagate status as we traverse back `up' the tree.
(DO_UNLINK, DO_RMDIR): Define.
(remove_entry) [ROOT_CAN_UNLINK_DIRS]: Add code so this works also on
systems where root can use `unlink' to remove directories.
* src/remove.c: Include file-type.h.
Include file type in prompt when asking whether to remove file.
Based on a patch from Paul Eggert.
* src/remove.c (prompt): Add comment.
* src/remove.c (remove_dir): Fix another (known) leak.
* src/remove.c (hash_freer): New function.
(AD_mark_helper): Use it.
(AD_mark_as_unremovable): xstrdup the filename argument.
(remove_dir): Free directory name.
* src/remove.c (remove_entry): Fail also when trying to remove a
directory without the --recursive option.
Change a diagnostic, s/unlink/remove/, now that it can apply also
to a directory.
* src/remove.c (is_empty_dir): New function.
(prompt): New function, factored out of...
(remove_entry): ...here. Call it.
(remove_dir): Call prompt before rmdir.
* src/remove.c (remove_entry): Add support for prompting (e.g., -i).
* src/remove.h (UPDATE_STATUS): New macro.
* src/remove.c [AD_ent] (status): New member. This lets us propagate
the status from a subdirectory to its parent via AD_pop_and_chdir.
(AD_push_initial): Set it.
(AD_push): Likewise.
(remove_cwd_entries): Change return type to enum RM_status, and
adjust all callers.
(rm): Use UPDATE_STATUS rather than open-coding it.
* src/remove.c (remove_entry): New function, factored out of...
(remove_cwd_entries): ...here, and...
(rm_1): ...here.
* src/remove.c (remove_cwd_entries): Add support for --verbose.
(remove_dir): Likewise.
(rm_1): Likewise.
* src/remove.c (rm): Free cwd_state, if necessary.
* src/remove.c (rm_1): Remove now useless (always true)
user_specified_name parameter. Adjust sole caller.
* src/remove.c (rm): New function. This interface allows
one to remove multiple arguments at a time. This is important in
that it allows us to hide the remove_init/remove_fini functions and
the cwd_state parameter.
(rm_1): Renamed from rm.
(remove_init, remove_fini): Remove functions. Each body is now
part of `rm'.
Diffstat (limited to 'src')
-rw-r--r-- | src/remove.c | 1129 |
1 files changed, 649 insertions, 480 deletions
diff --git a/src/remove.c b/src/remove.c index affd397ef..35a31d0aa 100644 --- a/src/remove.c +++ b/src/remove.c @@ -30,9 +30,10 @@ #include "system.h" #include "dirname.h" #include "error.h" -#include "obstack.h" +#include "file-type.h" #include "hash.h" #include "hash-pjw.h" +#include "obstack.h" #include "quote.h" #include "remove.h" @@ -52,6 +53,30 @@ # endif #endif +/* FIXME: if possible, use autoconf... */ +#ifdef __GLIBC__ +# define ROOT_CAN_UNLINK_DIRS 0 +#else +# define ROOT_CAN_UNLINK_DIRS 1 +#endif + +enum Ternary + { + T_UNKNOWN = 2, + T_NO, + T_YES + }; +typedef enum Ternary Ternary; + +/* The prompt function may be called twice 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 + }; + /* On systems with an lstat function that accepts the empty string, arrange to make lstat calls go through the wrapper function. */ #if HAVE_LSTAT_EMPTY_STRING_BUG @@ -73,24 +98,41 @@ int rpl_lstat PARAMS((const char *, struct stat *)); /* Initial capacity of per-directory hash table of entries that have been processed but not been deleted. */ -#define HT_INITIAL_CAPACITY 13 +#define HT_UNREMOVABLE_INITIAL_CAPACITY 13 -/* Initial capacity of the active directory hash table. This table will - be resized only for hierarchies more than about 45 levels deep. */ -#define ACTIVE_DIR_INITIAL_CAPACITY 53 +/* An entry in the active directory stack. + Each entry corresponds to an `active' directory. */ +struct AD_ent +{ + /* For a given active directory, this is the set of names of + entries in that directory that could/should not be removed. + For example, `.' and `..', as well as files/dirs for which + unlink/rmdir failed e.g., due to access restrictions. */ + Hash_table *unremovable; + + /* Record the status for a given active directory; we need to know + whether an entry was not removed, either because of an error or + because the user declined. */ + enum RM_status status; + + union + { + /* The directory's dev/ino. Used to ensure that `chdir some-subdir', then + `chdir ..' takes us back to the same directory from which we started). + (valid for all but the bottommost entry on the stack. */ + struct dev_ino a; + + /* Enough information to restore the initial working directory. + (valid only for the bottommost entry on the stack) */ + struct saved_cwd saved_cwd; + } u; +}; int euidaccess (); int yesno (); -/* FIXME: remove this declaration and instead write a new function, - error_no_prog, that saves error.c's error_print_progname, sets it to - point to function that does nothing, calls error, then restores - the original value of error_print_progname. Use error_no_prog in - place of `fprintf (stderr,'. */ extern char *program_name; -/* state initialized by remove_init, freed by remove_fini */ - /* The name of the directory (starting with and relative to a command line argument) being processed. When a subdirectory is entered, a new component is appended (pushed). When RM chdir's out of a directory, @@ -104,10 +146,17 @@ static struct obstack dir_stack; element pushed onto the dir stack may contain slashes. */ static struct obstack len_stack; -static inline unsigned int -current_depth (void) +/* Stack of active directory entries. + The first `active' directory is the initial working directory. + Additional active dirs are pushed onto the stack as rm `chdir's + into each nonempty directory it must remove. When rm has finished + removing the hierarchy under a directory, it pops the active dir stack. */ +static struct obstack Active_dir; + +static void +hash_freer (void *x) { - return obstack_object_size (&len_stack) / sizeof (size_t); + free (x); } static bool @@ -136,6 +185,21 @@ push_dir (const char *dir_name) obstack_grow (&len_stack, &len, sizeof (len)); } +/* Return the entry name of the directory on the top of the stack + in malloc'd storage. */ +static inline char * +top_dir (void) +{ + int n_lengths = obstack_object_size (&len_stack) / sizeof (size_t); + size_t *length = (size_t *) obstack_base (&len_stack); + size_t top_len = length[n_lengths - 1]; + char const *p = obstack_next_free (&dir_stack) - top_len; + char *q = xmalloc (top_len); + memcpy (q, p, top_len - 1); + q[top_len - 1] = 0; + return q; +} + static inline void pop_dir (void) { @@ -246,334 +310,175 @@ full_filename (const char *filename) return buf; } -static inline void -fspec_init_common (struct File_spec *fs) +static size_t +AD_stack_height (void) { - fs->have_full_mode = 0; - fs->have_filetype_mode = 0; - fs->have_device = 0; + return obstack_object_size (&Active_dir) / sizeof (struct AD_ent); } -void -fspec_init_file (struct File_spec *fs, const char *filename) +static struct AD_ent * +AD_stack_top (void) { - fs->filename = (char *) filename; - fspec_init_common (fs); + return (struct AD_ent *) + ((char *) obstack_next_free (&Active_dir) - sizeof (struct AD_ent)); } -static inline void -fspec_init_dp (struct File_spec *fs, struct dirent *dp) +static void +AD_stack_pop (void) { - fs->filename = dp->d_name; - fspec_init_common (fs); - fs->st_ino = D_INO (dp); - -#if D_TYPE_IN_DIRENT && defined DT_UNKNOWN && defined DTTOIF - if (dp->d_type != DT_UNKNOWN) - { - fs->have_filetype_mode = 1; - fs->mode = DTTOIF (dp->d_type); - } -#endif + /* operate on Active_dir. pop and free top entry */ + struct AD_ent *top = AD_stack_top (); + if (top->unremovable) + hash_free (top->unremovable); + obstack_blank (&Active_dir, -sizeof (struct AD_ent)); + pop_dir (); } -static inline int -fspec_get_full_mode (struct File_spec *fs) +/* chdir `up' one level. + Whenever using chdir '..', verify that the post-chdir + dev/ino numbers for `.' match the saved ones. + Return the name (in malloc'd storage) of the + directory (usually now empty) from which we're coming. */ +static char * +AD_pop_and_chdir (void) { - struct stat stat_buf; + /* Get the name of the current directory from the top of the stack. */ + char *dir = top_dir (); + enum RM_status old_status = AD_stack_top()->status; + struct stat sb; + struct AD_ent *top; - if (fs->have_full_mode) - return 0; + AD_stack_pop (); - if (lstat (fs->filename, &stat_buf)) - return 1; + /* Propagate any failure to parent. */ + UPDATE_STATUS (AD_stack_top()->status, old_status); - fs->have_full_mode = 1; - fs->have_filetype_mode = 1; - fs->mode = stat_buf.st_mode; - fs->st_ino = stat_buf.st_ino; - fs->have_device = 1; - fs->st_dev = stat_buf.st_dev; + assert (AD_stack_height ()); - return 0; -} - -static inline int -fspec_get_device_number (struct File_spec *fs) -{ - struct stat stat_buf; - - if (fs->have_device) - return 0; + top = AD_stack_top (); + if (1 < AD_stack_height ()) + { + /* We can give a better diagnostic here, since the target is relative. */ + if (chdir ("..")) + { + error (EXIT_FAILURE, errno, + _("cannot chdir from %s to .."), + quote (full_filename ("."))); + } + } + else + { + if (restore_cwd (&top->u.saved_cwd, NULL, NULL)) + exit (EXIT_FAILURE); + } - if (lstat (fs->filename, &stat_buf)) - return 1; + if (lstat (".", &sb)) + error (EXIT_FAILURE, errno, + _("cannot lstat `.' in %s"), quote (full_filename ("."))); - fs->have_full_mode = 1; - fs->have_filetype_mode = 1; - fs->mode = stat_buf.st_mode; - fs->st_ino = stat_buf.st_ino; - fs->have_device = 1; - fs->st_dev = stat_buf.st_dev; + if (1 < AD_stack_height ()) + { + /* Ensure that post-chdir dev/ino match the stored ones. */ + if ( ! SAME_INODE (sb, top->u.a)) + error (EXIT_FAILURE, 0, + _("%s changed dev/ino"), quote (full_filename ("."))); + } - return 0; + return dir; } -static inline int -fspec_get_filetype_mode (struct File_spec *fs, mode_t *filetype_mode) +/* Initialize *HT if it is NULL. + Insert FILENAME into HT. */ +static void +AD_mark_helper (Hash_table **ht, char const *filename) { - int fail; - - fail = fs->have_filetype_mode ? 0 : fspec_get_full_mode (fs); - if (!fail) - *filetype_mode = fs->mode; - - return fail; + if (*ht == NULL) + *ht = hash_initialize (HT_UNREMOVABLE_INITIAL_CAPACITY, NULL, hash_pjw, + hash_compare_strings, hash_freer); + if (*ht == NULL) + xalloc_die (); + if (! hash_insert (*ht, filename)) + xalloc_die (); } -static inline mode_t -fspec_filetype_mode (const struct File_spec *fs) +/* Mark FILENAME (in current directory) as unremovable. */ +static void +AD_mark_as_unremovable (char const *filename) { - assert (fs->have_filetype_mode); - return fs->mode; + AD_mark_helper (&AD_stack_top()->unremovable, xstrdup (filename)); } -static int -same_file (const char *file_1, const char *file_2) +/* Mark the current directory as unremovable. I.e., mark the entry + in the parent directory corresponding to `.'. + This happens e.g., when an opendir fails and the only name + the caller has conveniently at hand is `.'. */ +static void +AD_mark_current_as_unremovable (void) { - struct stat sb1, sb2; - return (lstat (file_1, &sb1) == 0 - && lstat (file_2, &sb2) == 0 - && SAME_INODE (sb1, sb2)); -} + struct AD_ent *top = AD_stack_top (); + const char *curr = top_dir (); + /* FIXME: assert this? */ + if (AD_stack_height () <= 1) + return; -/* Recursively remove all of the entries in the current directory. - Return an indication of the success of the operation. - CWD_DEV_INO must store the device and inode numbers of the - current working directory. */ + --top; + AD_mark_helper (&top->unremovable, curr); +} -static enum RM_status -remove_cwd_entries (const struct rm_options *x, - struct dev_ino const *cwd_dev_ino) +/* Push the initial cwd info onto the stack. + This will always be the bottommost entry on the stack. */ +static void +AD_push_initial (struct saved_cwd const *cwd) { - /* NOTE: this is static. */ - static DIR *dirp = NULL; - - /* NULL or a malloc'd and initialized hash table of entries in the - current directory that have been processed but not removed -- - due either to an error or to an interactive `no' response. */ - Hash_table *ht = NULL; - - /* FIXME: describe */ - static struct obstack entry_name_pool; - static int first_call = 1; - - enum RM_status status = RM_OK; - - if (first_call) - { - first_call = 0; - obstack_init (&entry_name_pool); - } + struct AD_ent *top; - if (dirp) - { - if (CLOSEDIR (dirp)) - { - /* FIXME-someday: but this is actually the previously opened dir. */ - error (0, errno, "%s", quote (full_filename ("."))); - status = RM_ERROR; - } - dirp = NULL; - } - - do - { - /* FIXME: why do this? */ - errno = 0; - - dirp = opendir ("."); - if (dirp == NULL) - { - if (errno != ENOENT || !x->ignore_missing_files) - { - error (0, errno, _("cannot open directory %s"), - quote (full_filename ("."))); - status = RM_ERROR; - } - break; - } - - while (1) - { - char *entry_name; - struct File_spec fs; - enum RM_status tmp_status; - struct dirent *dp; - -/* FILE should be skipped if it is `.' or `..', or if it is in - the table, HT, of entries we've already processed. */ -#define SKIPPABLE(Ht, File) \ - (DOT_OR_DOTDOT(File) || (Ht && hash_lookup (Ht, File))) - - /* FIXME: use readdir_r directly into an obstack to avoid - the obstack_copy0 below -- - Suggestion from Uli. Be careful -- there are different - prototypes on e.g. Solaris. - - Do something like this: - #define NAME_MAX_FOR(Parent_dir) pathconf ((Parent_dir), - _PC_NAME_MAX); - dp = obstack_alloc (sizeof (struct dirent) - + NAME_MAX_FOR (".") + 1); - fail = xreaddir (dirp, dp); - where xreaddir is ... - - But what about systems like the hurd where NAME_MAX is supposed - to be effectively unlimited. We don't want to have to allocate - a huge buffer to accommodate maximum possible entry name. */ - - dp = readdir (dirp); - -#if ! HAVE_WORKING_READDIR - if (dp == NULL) - { - /* Since we have probably modified the directory since it - was opened, readdir returning NULL does not necessarily - mean we have read the last entry. Rewind it and check - again. This happens on SunOS4.1.4 with 254 or more files - in a directory. */ - rewinddir (dirp); - while ((dp = readdir (dirp)) && SKIPPABLE (ht, dp->d_name)) - { - /* empty */ - } - } -#endif - - if (dp == NULL) - break; - - if (SKIPPABLE (ht, dp->d_name)) - continue; + /* Extend the stack. */ + obstack_blank (&Active_dir, sizeof (struct AD_ent)); - fspec_init_dp (&fs, dp); + /* Fill in the new values. */ + top = AD_stack_top (); + top->u.saved_cwd = *cwd; + top->status = RM_OK; + top->unremovable = NULL; +} - /* Save a copy of the name of this entry, in case we have - to add it to the set of unremoved entries below. */ - entry_name = obstack_copy0 (&entry_name_pool, - dp->d_name, NLENGTH (dp)); +/* Push info about the current working directory (".") onto the + active directory stack. DIR is the ./-relative name through + which we've just `chdir'd to this directory. DIR_SB_FROM_PARENT + is the result of calling lstat on DIR from the parent of DIR. */ +static void +AD_push (char const *dir, struct stat const *dir_sb_from_parent) +{ + struct stat sb; + struct AD_ent *top; - /* CAUTION: after this call to rm, DP may not be valid -- - it may have been freed due to a close in a recursive call - (through rm and remove_dir) to this function. */ - tmp_status = rm (&fs, 0, x, cwd_dev_ino); + push_dir (dir); - /* Update status. */ - if (tmp_status > status) - status = tmp_status; - assert (VALID_STATUS (status)); + if (lstat (".", &sb)) + error (EXIT_FAILURE, errno, + _("cannot lstat `.' in %s"), quote (full_filename ("."))); - /* If this entry was not removed (due either to an error or to - an interactive `no' response), record it in the hash table so - we don't consider it again if we reopen this directory later. */ - if (status != RM_OK) - { - if (ht == NULL) - { - ht = hash_initialize (HT_INITIAL_CAPACITY, NULL, hash_pjw, - hash_compare_strings, NULL); - if (ht == NULL) - xalloc_die (); - } - if (! hash_insert (ht, entry_name)) - xalloc_die (); - } - else - { - /* This entry was not saved in the hash table. Free it. */ - obstack_free (&entry_name_pool, entry_name); - } + if ( ! SAME_INODE (sb, *dir_sb_from_parent)) + error (EXIT_FAILURE, errno, + _("%s changed dev/ino"), quote (full_filename ("."))); - if (dirp == NULL) - break; - } - } - while (dirp == NULL); + /* Extend the stack. */ + obstack_blank (&Active_dir, sizeof (struct AD_ent)); - if (dirp) - { - if (CLOSEDIR (dirp)) - { - error (0, errno, _("closing directory %s"), - quote (full_filename ("."))); - status = RM_ERROR; - } - dirp = NULL; - } - - if (ht) - { - hash_free (ht); - } - - if (obstack_object_size (&entry_name_pool) > 0) - obstack_free (&entry_name_pool, obstack_base (&entry_name_pool)); - - return status; + /* Fill in the new values. */ + top = AD_stack_top (); + top->u.a.st_dev = sb.st_dev; + top->u.a.st_ino = sb.st_ino; + top->status = RM_OK; + top->unremovable = NULL; } -/* Query the user if appropriate, and if ok try to remove the - file or directory specified by FS. Return RM_OK if it is removed, - and RM_ERROR or RM_USER_DECLINED if not. */ - -static enum RM_status -remove_file (struct File_spec *fs, const struct rm_options *x) +static int +AD_is_removable (char const *file) { - int asked = 0; - char *pathname = fs->filename; - - if (!x->ignore_missing_files && (x->interactive || x->stdin_tty) - && euidaccess (pathname, W_OK)) - { - if (!S_ISLNK (fspec_filetype_mode (fs))) - { - fprintf (stderr, - (S_ISDIR (fspec_filetype_mode (fs)) - ? _("%s: remove write-protected directory %s? ") - : _("%s: remove write-protected file %s? ")), - program_name, quote (full_filename (pathname))); - if (!yesno ()) - return RM_USER_DECLINED; - - asked = 1; - } - } - - if (!asked && x->interactive) - { - /* 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. */ - fprintf (stderr, - (S_ISDIR (fspec_filetype_mode (fs)) - ? _("%s: remove directory %s? ") - : _("%s: remove %s? ")), - program_name, quote (full_filename (pathname))); - if (!yesno ()) - return RM_USER_DECLINED; - } - - if (x->verbose) - printf (_("removing %s\n"), quote (full_filename (pathname))); - - if (unlink (pathname) && (errno != ENOENT || !x->ignore_missing_files)) - { - error (0, errno, _("cannot unlink %s"), quote (full_filename (pathname))); - return RM_ERROR; - } - return RM_OK; + struct AD_ent *top = AD_stack_top (); + return ! (top->unremovable && hash_lookup (top->unremovable, file)); } static inline bool @@ -582,8 +487,6 @@ is_power_of_two (unsigned int i) return (i & (i - 1)) == 0; } -/* Test whether the current dev/ino (from SB) is the same as the saved one. - Periodically squirrel away the dev/ino of a current directory. */ static void cycle_check (struct stat const *sb) { @@ -616,247 +519,513 @@ The following directory is part of the cycle:\n %s\n"), #endif } -/* If not in recursive mode, print an error message and return RM_ERROR. - Otherwise, query the user if appropriate, then try to recursively - remove the directory specified by FS. Return RM_OK if it is removed, - and RM_ERROR or RM_USER_DECLINED if not. - FIXME: describe need_save_cwd parameter. */ - -static enum RM_status -remove_dir (struct File_spec *fs, int need_save_cwd, - struct rm_options const *x, struct dev_ino const *cwd_dev_ino) +static bool +is_empty_dir (char const *dir) { - enum RM_status status; - struct saved_cwd cwd; - char *dir_name = fs->filename; - const char *fmt = NULL; - struct dev_ino tmp_cwd_dev_ino; + DIR *dirp = opendir (dir); + if (dirp == NULL) + return false; - if (!x->recursive) + while (1) { - error (0, 0, _("%s is a directory"), quote (full_filename (dir_name))); - return RM_ERROR; - } + struct dirent *dp = readdir (dirp); + const char *f; - if (!x->ignore_missing_files && (x->interactive || x->stdin_tty) - && euidaccess (dir_name, W_OK)) - { - fmt = _("%s: directory %s is write protected; descend into it anyway? "); + if (dp == NULL) + return true; + + f = dp->d_name; + if ( ! DOT_OR_DOTDOT (f)) + return false; } - else if (x->interactive) +} + +/* Prompt whether to remove FILENAME, 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. + + 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. */ +static enum RM_status +prompt (char const *filename, struct rm_options const *x, + enum Prompt_action mode, Ternary *is_dir, Ternary *is_empty) +{ + int write_protected = 0; + *is_empty = T_UNKNOWN; + *is_dir = T_UNKNOWN; + + if ((!x->ignore_missing_files && (x->interactive || x->stdin_tty) + && (write_protected = euidaccess (filename, W_OK))) + || x->interactive) { - fmt = _("%s: descend into directory %s? "); + struct stat sbuf; + if (lstat (filename, &sbuf)) + { + /* lstat failed. This happens e.g., with `rm '''. */ + error (0, errno, _("cannot lstat %s"), + quote (full_filename (filename))); + return RM_ERROR; + } + + /* Using permissions doesn't make sense for symlinks. */ + if (S_ISLNK (sbuf.st_mode)) + write_protected = 0; + + /* 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 (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 + fprintf (stderr, + (write_protected + ? _("%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; +} + +#define DT_IS_DIR(D) ((D)->d_type == DT_DIR) + +#define DO_UNLINK(Filename, X) \ + do \ + { \ + if (unlink (Filename) == 0) \ + { \ + if ((X)->verbose) \ + printf (_("removed %s\n"), quote (full_filename (Filename))); \ + return RM_OK; \ + } \ + \ + if (errno == ENOENT && (X)->ignore_missing_files) \ + return RM_OK; \ + } \ + while (0) + +#define DO_RMDIR(Filename, X) \ + do \ + { \ + if (rmdir (Filename) == 0) \ + { \ + if ((X)->verbose) \ + printf (_("removed directory: %s\n"), \ + quote (full_filename (Filename))); \ + return RM_OK; \ + } \ + \ + if (errno == ENOENT && (X)->ignore_missing_files) \ + return RM_OK; \ + \ + if (errno == ENOTEMPTY || errno == EEXIST) \ + return RM_NONEMPTY_DIR; \ + } \ + while (0) + +/* 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. */ - if (fmt) +static enum RM_status +remove_entry (char const *filename, struct rm_options const *x, + struct dirent const *dp) +{ + Ternary is_dir; + Ternary is_empty_directory; + enum RM_status s = prompt (filename, x, PA_DESCEND_INTO_DIR, + &is_dir, &is_empty_directory); + + if (s != RM_OK) + return s; + + /* Why bother with the following #if/#else block? Because on systems with + an unlink function that *can* unlink directories, we must determine the + type of each entry before removing it. Otherwise, we'd risk unlinking an + entire directory tree simply by unlinking a single directory; then all + the storage associated with that hierarchy would not be freed until the + next reboot. Not nice. To avoid that, on such slightly losing systems, we + need to call lstat to determine the type of each entry, and that represents + extra overhead that -- it turns out -- we can avoid on GNU-libc-based + systems, since there, unlink will never remove a directory. */ + +#if ROOT_CAN_UNLINK_DIRS + + /* If we don't already know whether FILENAME is a directory, + find out now. */ + if (is_dir == T_UNKNOWN) { - fprintf (stderr, fmt, program_name, quote (full_filename (dir_name))); - if (!yesno ()) - return RM_USER_DECLINED; - } + if (dp) + is_dir = DT_IS_DIR (dp) ? T_YES : T_NO; + else + { + struct stat sbuf; + if (lstat (filename, &sbuf)) + { + if (errno == ENOENT && x->ignore_missing_files) + return RM_OK; - if (x->verbose) - printf (_("removing all entries of directory %s\n"), - quote (full_filename (dir_name))); + error (0, errno, + _("cannot lstat %s"), quote (full_filename (filename))); + return RM_ERROR; + } - /* Save cwd if needed. */ - if (need_save_cwd && save_cwd (&cwd)) - return RM_ERROR; + is_dir = S_ISDIR (sbuf.st_mode) ? T_YES : T_NO; + } + } - /* Make target directory the current one. */ - if (chdir (dir_name) < 0) + if (is_dir == T_NO) { - error (0, errno, _("cannot change to directory %s"), - quote (full_filename (dir_name))); - if (need_save_cwd) - free_cwd (&cwd); + /* At this point, barring race conditions, FILENAME is known + to be a non-directory, so it's ok to try to unlink it. */ + DO_UNLINK (filename, x); + + /* unlink failed with some other error code. report it. */ + error (0, errno, _("cannot remove %s"), + quote (full_filename (filename))); return RM_ERROR; } - /* Verify that the device and inode numbers of `.' are the same as - the ones we recorded for dir_name before we cd'd into it. This - detects the scenario in which an attacker tries to make Bob's rm - command remove some other directory belonging to Bob. The method - would be to replace an existing lstat'd but-not-yet-removed directory - with a symlink to the target directory. */ - { - struct stat sb; - if (lstat (".", &sb)) - error (EXIT_FAILURE, errno, - _("cannot lstat `.' in %s"), quote (full_filename (dir_name))); + if (! x->recursive) + { + error (0, EISDIR, _("cannot remove directory %s"), + quote (full_filename (filename))); + return RM_ERROR; + } - assert (fs->have_device); - if (!SAME_INODE (sb, *fs)) - { - error (EXIT_FAILURE, 0, - _("directory %s was replaced before being removed"), - quote (full_filename (dir_name))); - } + if (is_empty_directory == T_YES) + { + DO_RMDIR (filename, x); + /* Don't diagnose any failure here. + It'll be detected when the caller tries another way. */ + } - cycle_check (&sb); - tmp_cwd_dev_ino.st_dev = sb.st_dev; - tmp_cwd_dev_ino.st_ino = sb.st_ino; - } +#else - push_dir (dir_name); + /* is_empty_directory is set iff it's ok to use rmdir. + Note that it's set only in interactive mode -- in which case it's + an optimization that arranges so that the user is asked just + once whether to remove the directory. */ + if (is_empty_directory == T_YES) + DO_RMDIR (filename, x); + + /* 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_IS_DIR (dp)) || is_dir == T_YES) + return RM_NONEMPTY_DIR; + + DO_UNLINK (filename, x); + + /* Accept either EISDIR or EPERM as an indication that FILENAME may be + a directory. POSIX says that unlink must set errno to EPERM when it + fails to remove a directory, while Linux-2.4.18 sets it to EISDIR. */ + if ((errno != EISDIR && errno != EPERM) || ! x->recursive) + { + /* some other error code. Report it and fail. + Likewise, if we're trying to remove a directory without + the --recursive option. */ + error (0, errno, _("cannot remove %s"), + quote (full_filename (filename))); + return RM_ERROR; + } +#endif - /* Save a copy of dir_name. Otherwise, remove_cwd_entries may clobber - it because it is just a pointer to the dir entry's d_name field, and - remove_cwd_entries may close the directory. */ - ASSIGN_STRDUPA (dir_name, dir_name); + return RM_NONEMPTY_DIR; +} - status = remove_cwd_entries (x, &tmp_cwd_dev_ino); +/* Remove entries in `.', the current working directory (cwd). + Upon finding a directory that is both non-empty and that can be chdir'd + into, return zero and set *SUBDIR and fill in SUBDIR_SB, where + SUBDIR is the malloc'd name of the subdirectory if the chdir succeeded, + NULL otherwise (e.g., if opendir failed or if there was no subdirectory). + Likewise, SUBDIR_SB is the result of calling lstat on SUBDIR. + Return RM_OK if all entries are removed. Remove RM_ERROR if any + entry cannot be removed. Otherwise, return RM_USER_DECLINED if + the user declines to remove at least one entry. Remove as much as + possible, continuing even if we fail to remove some entries. */ +static enum RM_status +remove_cwd_entries (char **subdir, struct stat *subdir_sb, + struct rm_options const *x) +{ + DIR *dirp = opendir ("."); + struct AD_ent *top = AD_stack_top (); + enum RM_status status = top->status; - pop_dir (); + assert (VALID_STATUS (status)); + *subdir = NULL; - /* Restore cwd. */ - if (need_save_cwd) + if (dirp == NULL) { - if (restore_cwd (&cwd, NULL, NULL)) + if (errno != ENOENT || !x->ignore_missing_files) { - free_cwd (&cwd); + error (0, errno, _("cannot open directory %s"), + quote (full_filename ("."))); return RM_ERROR; } - free_cwd (&cwd); } - else + + while (1) { - struct stat sb; - if (chdir ("..") < 0) + struct dirent *dp = readdir (dirp); + enum RM_status tmp_status; + const char *f; + + if (dp == NULL) + break; + + f = dp->d_name; + if (DOT_OR_DOTDOT (f)) + continue; + + /* Skip files we've already tried/failed to remove. */ + if ( ! AD_is_removable (f)) + continue; + + /* Pass dp->d_type info to remove_entry so the non-glibc + 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 (f, x, dp); + switch (tmp_status) { - error (0, errno, _("cannot change back to directory %s via `..'"), - quote (full_filename (dir_name))); - return RM_ERROR; - } + case RM_OK: + /* do nothing */ + break; - if (lstat (".", &sb)) - error (EXIT_FAILURE, errno, - _("cannot lstat `.' in %s"), quote (full_filename ("."))); + case RM_ERROR: + case RM_USER_DECLINED: + AD_mark_as_unremovable (f); + UPDATE_STATUS (status, tmp_status); + break; - if (!SAME_INODE (sb, *cwd_dev_ino)) - { - error (EXIT_FAILURE, 0, - _("subdirectory of %s was moved while being removed"), - quote (full_filename ("."))); + case RM_NONEMPTY_DIR: + /* Record dev/ino of F so that we can compare + that with dev/ino of `.' after the chdir. + This dev/ino pair is also used in cycle detection. */ + if (lstat (f, subdir_sb)) + error (EXIT_FAILURE, errno, _("cannot lstat %s"), + quote (full_filename (f))); + + if (chdir (f)) + { + error (0, errno, _("cannot chdir from %s to %s"), + quote_n (0, full_filename (".")), quote_n (1, f)); + AD_mark_as_unremovable (f); + status = RM_ERROR; + break; + } + cycle_check (subdir_sb); + + *subdir = xstrdup (f); + break; } + + /* Record status for this directory. */ + UPDATE_STATUS (top->status, status); + + if (*subdir) + break; } - if (x->interactive) + closedir (dirp); + + return status; +} + +/* Remove the hierarchy rooted at DIR. + Do that by changing into DIR, then removing its contents, then + returning to the original working directory and removing DIR itself. + Don't use recursion. Be careful when using chdir ".." that we + return to the same directory from which we came, if necessary. + Return 1 for success, 0 if some file cannot be removed or if + a chdir fails. + If the working directory cannot be restored, exit immediately. */ + +static enum RM_status +remove_dir (char const *dir, struct saved_cwd **cwd_state, + struct rm_options const *x) +{ + enum RM_status status; + struct stat dir_sb; + + if (*cwd_state == NULL) { - fprintf (stderr, _("%s: remove directory %s%s? "), - program_name, - quote (full_filename (dir_name)), - (status != RM_OK ? _(" (might be nonempty)") : "")); - if (!yesno ()) - { - return RM_USER_DECLINED; - } + *cwd_state = XMALLOC (struct saved_cwd, 1); + if (save_cwd (*cwd_state)) + return RM_ERROR; + AD_push_initial (*cwd_state); } - if (x->verbose) - printf (_("removing the directory itself: %s\n"), - quote (full_filename (dir_name))); + /* There is a race condition in that an attacker could replace the nonempty + directory, DIR, with a symlink between the preceding call to rmdir + (in our caller) and the chdir below. However, the following lstat, + along with the `stat (".",...' and dev/ino comparison in AD_push + ensure that we detect it and fail. */ - if (rmdir (dir_name) && (errno != ENOENT || !x->ignore_missing_files)) + if (lstat (dir, &dir_sb)) { - int saved_errno = errno; + error (0, errno, + _("cannot lstat %s"), quote (full_filename (dir))); + return RM_ERROR; + } -#ifndef EINVAL -# define EINVAL 0 -#endif - /* See if rmdir just failed because DIR_NAME is the current directory. - If so, give a better diagnostic than `rm: cannot remove directory - `...': Invalid argument' */ - if (errno == EINVAL && same_file (".", dir_name)) + if (chdir (dir)) + { + error (0, errno, + _("cannot chdir from %s to %s"), + quote_n (0, full_filename (".")), quote_n (1, dir)); + return RM_ERROR; + } + + AD_push (dir, &dir_sb); + + status = RM_OK; + + while (1) + { + char *subdir = NULL; + struct stat subdir_sb; + enum RM_status tmp_status = remove_cwd_entries (&subdir, &subdir_sb, x); + if (tmp_status != RM_OK) { - error (0, 0, _("cannot remove current directory %s"), - quote (full_filename (dir_name))); + UPDATE_STATUS (status, tmp_status); + AD_mark_current_as_unremovable (); } - else + if (subdir) { - error (0, saved_errno, _("cannot remove directory %s"), - quote (full_filename (dir_name))); + AD_push (subdir, &subdir_sb); + free (subdir); + continue; } - return RM_ERROR; + + /* Execution reaches this point when we've removed the last + removable entry from the current directory. */ + { + char *d = AD_pop_and_chdir (); + + /* Try to remove D 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; + Ternary is_empty; + enum RM_status s = prompt (d, x, PA_REMOVE_DIR, &is_dir, &is_empty); + + if (s != RM_OK) + { + free (d); + return s; + } + + if (rmdir (d) == 0) + { + if (x->verbose) + printf (_("removed directory: %s\n"), + quote (full_filename (d))); + } + else + { + error (0, errno, _("cannot remove directory %s"), + quote (full_filename (d))); + AD_mark_as_unremovable (d); + status = RM_ERROR; + UPDATE_STATUS (AD_stack_top()->status, status); + } + } + + free (d); + + if (AD_stack_height () == 1) + break; + } } return status; } -/* Remove the file or directory specified by FS after checking appropriate - things. Return RM_OK if it is removed, and RM_ERROR or RM_USER_DECLINED - if not. If USER_SPECIFIED_NAME is non-zero, then the name part of FS may - be `.', `..', or may contain slashes. Otherwise, it must be a simple file - name (and hence must specify a file in the current directory). - CWD_DEV_INO must store the device and inode numbers of the - current working directory. */ +/* Remove the file or directory specified by FILENAME. + Return RM_OK if it is removed, and RM_ERROR or RM_USER_DECLINED if not. + On input, the first time this function is called, CWD_STATE should be + the address of a NULL pointer. Do not modify it for any subsequent calls. + On output, it is either that same NULL pointer or the address of + a malloc'd `struct saved_cwd' that may be freed. */ -enum RM_status -rm (struct File_spec *fs, int user_specified_name, - struct rm_options const *x, struct dev_ino const *cwd_dev_ino) +static enum RM_status +rm_1 (char const *filename, + struct rm_options const *x, struct saved_cwd **cwd_state) { - if (user_specified_name) - { - /* CAUTION: this use of base_name works only because any - trailing slashes in fs->filename have already been removed. */ - char *base = base_name (fs->filename); - - if (DOT_OR_DOTDOT (base)) - { - error (0, 0, _("cannot remove `.' or `..'")); - return RM_ERROR; - } - } + char *base = base_name (filename); + enum RM_status status; - /* FIXME: this makes fspec_get_filetype_mode unused, and in fact, - may make the whole fspec_* caching business pointless... - I'm finally coming around to Paul's way of thinking: - we need a `safe' mode (see rewrite on the no-recursion branch) - and a fast-and-unsafe mode. */ - if (fspec_get_full_mode (fs)) + if (DOT_OR_DOTDOT (base)) { - if (x->ignore_missing_files && errno == ENOENT) - return RM_OK; - - error (0, errno, _("cannot remove %s"), - quote (full_filename (fs->filename))); + error (0, 0, _("cannot remove `.' or `..'")); return RM_ERROR; } - if (!S_ISDIR (fs->mode) || x->unlink_dirs) - { - return remove_file (fs, x); - } - else - { - /* If this command line argument contains a `/' (which means - rm will chdir `into' it while removing it), then rm will - have to save/restore the current working directory, in case a - subsequent command line argument is a relative path name. */ - int need_save_cwd = user_specified_name; - enum RM_status status; - - if (need_save_cwd) - need_save_cwd = (strchr (fs->filename, '/') != NULL); + status = remove_entry (filename, x, NULL); + if (status != RM_NONEMPTY_DIR) + return status; - status = remove_dir (fs, need_save_cwd, x, cwd_dev_ino); - - return status; - } + return remove_dir (filename, cwd_state, x); } -void -remove_init (void) +/* Remove all files and/or directories specified by N_FILES and FILE. + Apply the options in X. */ +enum RM_status +rm (size_t n_files, char const *const *file, struct rm_options const *x) { - /* Initialize dir-stack obstacks. */ + struct saved_cwd *cwd_state = NULL; + enum RM_status status = RM_OK; + size_t i; + obstack_init (&dir_stack); obstack_init (&len_stack); -} + obstack_init (&Active_dir); + + for (i = 0; i < n_files; i++) + { + enum RM_status s = rm_1 (file[i], x, &cwd_state); + assert (VALID_STATUS (status)); + UPDATE_STATUS (status, s); + } -void -remove_fini (void) -{ obstack_free (&dir_stack, NULL); obstack_free (&len_stack, NULL); + obstack_free (&Active_dir, NULL); + + XFREE (cwd_state); + + return status; } |