From ff59218d541edb2cc91d67c25033170308a458dc Mon Sep 17 00:00:00 2001 From: Jim Meyering Date: Thu, 22 Jan 1998 08:13:57 +0000 Subject: New file. Contains guts of old rm.c. (remove_init): New function. (remove_fini): New function. --- src/remove.c | 848 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 848 insertions(+) create mode 100644 src/remove.c (limited to 'src') diff --git a/src/remove.c b/src/remove.c new file mode 100644 index 000000000..125c5eaf5 --- /dev/null +++ b/src/remove.c @@ -0,0 +1,848 @@ +#include +#include +#include +#include + +#include "save-cwd.h" +#include "system.h" +#include "closeout.h" +#include "error.h" +#include "obstack.h" +#include "hash.h" +#include "remove.h" + +#define obstack_chunk_alloc malloc +#define obstack_chunk_free free + +#ifndef PARAMS +# if defined (__GNUC__) || __STDC__ +# define PARAMS(args) args +# else +# define PARAMS(args) () +# endif +#endif + +#ifdef D_INO_IN_DIRENT +# define D_INO(dp) ((dp)->d_ino) +# define ENABLE_CYCLE_CHECK +#else +/* Some systems don't have inodes, so fake them to avoid lots of ifdefs. */ +# define D_INO(dp) 1 +#endif + +#if !defined (S_ISLNK) +# define S_ISLNK(Mode) 0 +#endif + +#define DOT_OR_DOTDOT(Basename) \ + (Basename[0] == '.' && (Basename[1] == '\0' \ + || (Basename[1] == '.' && Basename[2] == '\0'))) + +#if defined strdupa +# define ASSIGN_STRDUPA(DEST, S) \ + do { DEST = strdupa(S); } while (0) +#else +# define ASSIGN_STRDUPA(DEST, S) \ + do \ + { \ + const char *s_ = (S); \ + size_t len_ = strlen (s_) + 1; \ + char *tmp_dest_ = (char *) alloca (len_); \ + DEST = memcpy (tmp_dest_, (s_), len_); \ + } \ + while (0) +#endif + +/* Initial capacity of per-directory hash table of entries that have + been processed but not been deleted. */ +#define HT_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 + +char *base_name (); +int euidaccess (); +int yesno (); + +extern char *program_name; + +/* state initialized by remove_init, freed by remove_fini */ + +/* An entry in the active_dir_map. */ +struct active_dir_ent +{ + ino_t inum; + unsigned int depth; +}; + +/* 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, + the top component is removed (popped). This is used to form a full + file name when necessary. */ +static struct obstack dir_stack; + +/* Stack of lengths of directory names (including trailing slash) + appended to dir_stack. We have to have a separate stack of lengths + (rather than just popping back to previous slash) because the first + element pushed onto the dir stack may contain slashes. */ +static struct obstack len_stack; + +/* Set of `active' directories from the current command-line argument + to the level in the hierarchy at which files are being removed. + A directory is added to the active set when RM begins removing it + (or its entries), and it is removed from the set just after RM has + finished processing it. + + This is actually a map (not a set), implemented with a hash table. + For each active directory, it maps the directory's inode number to the + depth of that directory relative to the root of the tree being deleted. + A directory specified on the command line has depth zero. + This construct is used to detect directory cycles so that RM can warn + about them rather than iterating endlessly. */ +#ifdef ENABLE_CYCLE_CHECK +static struct HT *active_dir_map; +#endif + +static inline unsigned int +current_depth (void) +{ + return obstack_object_size (&len_stack) / sizeof (size_t); +} + +static void +print_nth_dir (FILE *stream, unsigned int depth) +{ + size_t *length = (size_t *) obstack_base (&len_stack); + char *dir_name = (char *) obstack_base (&dir_stack); + unsigned int sum = 0; + unsigned int i; + + assert (0 <= depth && depth < current_depth ()); + + for (i = 0; i <= depth; i++) + { + sum += length[i]; + } + + fwrite (dir_name, 1, sum, stream); +} + +static inline struct active_dir_ent * +make_active_dir_ent (ino_t inum, unsigned int depth) +{ + struct active_dir_ent *ent; + ent = (struct active_dir_ent *) xmalloc (sizeof *ent); + ent->inum = inum; + ent->depth = depth; + return ent; +} + +static unsigned int +hash_active_dir_ent (void const *x, unsigned int table_size) +{ + struct active_dir_ent const *ade = x; + return ade->inum % table_size; +} + +static int +hash_compare_active_dir_ents (void const *x, void const *y) +{ + struct active_dir_ent const *a = x; + struct active_dir_ent const *b = y; + return (a->inum == b->inum ? 0 : 1); +} + +/* A hash function for null-terminated char* strings using + the method described in Aho, Sethi, & Ullman, p 436. */ + +static unsigned int +hash_pjw (const void *x, unsigned int tablesize) +{ + const char *s = x; + unsigned int h = 0; + unsigned int g; + + while (*s != 0) + { + h = (h << 4) + *s++; + if ((g = h & (unsigned int) 0xf0000000) != 0) + h = (h ^ (g >> 24)) ^ g; + } + + return (h % tablesize); +} + +static int +hash_compare_strings (void const *x, void const *y) +{ + return strcmp (x, y); +} + +static inline void +push_dir (const char *dir_name) +{ + size_t len; + + len = strlen (dir_name); + + /* Append the string onto the stack. */ + obstack_grow (&dir_stack, dir_name, len); + + /* Append a trailing slash. */ + obstack_1grow (&dir_stack, '/'); + + /* Add one for the slash. */ + ++len; + + /* Push the length (including slash) onto its stack. */ + obstack_grow (&len_stack, &len, sizeof (len)); +} + +static inline void +pop_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; + + assert (n_lengths > 0); + top_len = length[n_lengths - 1]; + assert (top_len >= 2); + + /* Pop off the specified length of pathname. */ + assert (obstack_object_size (&dir_stack) >= top_len); + obstack_blank (&dir_stack, -top_len); + + /* Pop the length stack, too. */ + assert (obstack_object_size (&len_stack) >= sizeof (size_t)); + obstack_blank (&len_stack, -(sizeof (size_t))); +} + +/* Copy the SRC_LEN bytes of data beginning at SRC into the DST_LEN-byte + buffer, DST, so that the last source byte is at the end of the destination + buffer. If SRC_LEN is longer than DST_LEN, then set *TRUNCATED to non-zero. + Set *RESULT to point to the beginning of (the portion of) the source data + in DST. Return the number of bytes remaining in the destination buffer. */ + +static size_t +right_justify (char *dst, size_t dst_len, const char *src, size_t src_len, + char **result, int *truncated) +{ + const char *sp; + char *dp; + + if (src_len <= dst_len) + { + sp = src; + dp = dst + (dst_len - src_len); + *truncated = 0; + } + else + { + sp = src + (src_len - dst_len); + dp = dst; + src_len = dst_len; + *truncated = 1; + } + + *result = memcpy (dp, sp, src_len); + return dst_len - src_len; +} + +/* Using the global directory name obstack, create the full path to 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. */ + +static char * +full_filename (const char *filename) +{ + static char *buf = NULL; + static size_t n_allocated = 0; + + int dir_len = obstack_object_size (&dir_stack); + char *dir_name = (char *) obstack_base (&dir_stack); + size_t n_bytes_needed; + size_t filename_len; + + filename_len = strlen (filename); + n_bytes_needed = dir_len + filename_len + 1; + + if (n_bytes_needed > n_allocated) + { + /* 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. */ + buf = realloc (buf, n_bytes_needed); + n_allocated = n_bytes_needed; + + if (buf == NULL) + { +#define SBUF_SIZE 512 +#define ELLIPSES_PREFIX "[...]" + static char static_buf[SBUF_SIZE]; + int truncated; + size_t len; + char *p; + + 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; + } + } + + /* Copy 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); + + return buf; +} + +void +fspec_init_file (struct File_spec *fs, const char *filename) +{ + fs->filename = (char *) filename; + fs->have_full_mode = 0; + fs->have_filetype_mode = 0; +} + +static inline void +fspec_init_dp (struct File_spec *fs, struct dirent *dp) +{ + fs->filename = dp->d_name; + fs->have_full_mode = 0; + fs->have_filetype_mode = 0; + fs->inum = 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 +} + +static inline int +fspec_get_full_mode (struct File_spec *fs, mode_t *full_mode) +{ + struct stat stat_buf; + + if (fs->have_full_mode) + { + *full_mode = fs->mode; + return 0; + } + + if (lstat (fs->filename, &stat_buf)) + return 1; + + fs->have_full_mode = 1; + fs->have_filetype_mode = 1; + fs->mode = stat_buf.st_mode; + fs->inum = stat_buf.st_ino; + + *full_mode = stat_buf.st_mode; + return 0; +} + +static inline int +fspec_get_filetype_mode (struct File_spec *fs, mode_t *filetype_mode) +{ + int fail; + + if (fs->have_filetype_mode) + { + *filetype_mode = fs->mode; + fail = 0; + } + else + { + fail = fspec_get_full_mode (fs, filetype_mode); + } + + return fail; +} + +static inline mode_t +fspec_filetype_mode (const struct File_spec *fs) +{ + assert (fs->have_filetype_mode); + return fs->mode; +} + +/* Recursively remove all of the entries in the current directory. + Return an indication of the success of the operation. */ + +enum RM_status +remove_cwd_entries (const struct rm_options *x) +{ + /* 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. */ + struct HT *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); + } + + if (dirp) + { + if (CLOSEDIR (dirp)) + { + /* FIXME-someday: but this is actually the previously opened dir. */ + error (0, errno, "%s", 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, "%s", 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_query_in_table (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; + + fspec_init_dp (&fs, dp); + + /* 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)); + + /* 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); + + /* Update status. */ + if (tmp_status > status) + status = tmp_status; + assert (VALID_STATUS (status)); + + /* 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) + { + int fail; + + if (ht == NULL) + { + ht = hash_initialize (HT_INITIAL_CAPACITY, NULL, + hash_pjw, hash_compare_strings); + if (ht == NULL) + error (1, 0, _("Memory exhausted")); + } + HASH_INSERT_NEW_ITEM (ht, entry_name, &fail); + if (fail) + error (1, 0, _("Memory exhausted")); + } + else + { + /* This entry was not saved in the hash table. Free it. */ + obstack_free (&entry_name_pool, entry_name); + } + + if (dirp == NULL) + break; + } + } + while (dirp == NULL); + + if (CLOSEDIR (dirp)) + { + error (0, errno, "%s", full_filename (".")); + status = 1; + } + 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; +} + +/* 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) +{ + 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, 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, full_filename (pathname)); + if (!yesno ()) + return RM_USER_DECLINED; + } + + if (x->verbose) + printf (_("removing non-directory %s\n"), full_filename (pathname)); + + if (unlink (pathname) && (errno != ENOENT || !x->ignore_missing_files)) + { + error (0, errno, _("cannot unlink `%s'"), full_filename (pathname)); + return RM_ERROR; + } + return RM_OK; +} + +/* 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, const struct rm_options *x) +{ + enum RM_status status; + struct saved_cwd cwd; + char *dir_name = fs->filename; + const char *fmt = NULL; + + if (!x->recursive) + { + error (0, 0, _("%s: is a directory"), full_filename (dir_name)); + return RM_ERROR; + } + + 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? "); + } + else if (x->interactive) + { + fmt = _("%s: descend into directory `%s'? "); + } + + if (fmt) + { + fprintf (stderr, fmt, program_name, full_filename (dir_name)); + if (!yesno ()) + return RM_USER_DECLINED; + } + + if (x->verbose) + printf (_("removing any entries of directory %s\n"), + full_filename (dir_name)); + + /* Save cwd if needed. */ + if (need_save_cwd && save_cwd (&cwd)) + return RM_ERROR; + + /* Make target directory the current one. */ + if (chdir (dir_name) < 0) + { + error (0, errno, _("cannot change to directory %s"), + full_filename (dir_name)); + if (need_save_cwd) + free_cwd (&cwd); + return RM_ERROR; + } + + push_dir (dir_name); + + /* 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); + + status = remove_cwd_entries (x); + + pop_dir (); + + /* Restore cwd. */ + if (need_save_cwd) + { + if (restore_cwd (&cwd, NULL, NULL)) + { + free_cwd (&cwd); + return RM_ERROR; + } + free_cwd (&cwd); + } + else if (chdir ("..") < 0) + { + error (0, errno, _("cannot change back to directory %s via `..'"), + full_filename (dir_name)); + return RM_ERROR; + } + + if (x->interactive) + { + error (0, 0, _("remove directory `%s'%s? "), full_filename (dir_name), + (status != RM_OK ? _(" (might be nonempty)") : "")); + if (!yesno ()) + { + return RM_USER_DECLINED; + } + } + + if (x->verbose) + printf (_("removing the directory itself: %s\n"), full_filename (dir_name)); + + if (rmdir (dir_name) && (errno != ENOENT || !x->ignore_missing_files)) + { + error (0, errno, _("cannot remove directory `%s'"), + full_filename (dir_name)); + return RM_ERROR; + } + + return RM_OK; +} + +/* 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). */ + +enum RM_status +rm (struct File_spec *fs, int user_specified_name, const struct rm_options *x) +{ + mode_t filetype_mode; + + if (user_specified_name) + { + char *base = base_name (fs->filename); + + if (DOT_OR_DOTDOT (base)) + { + error (0, 0, _("cannot remove `.' or `..'")); + return RM_ERROR; + } + } + + if (fspec_get_filetype_mode (fs, &filetype_mode)) + { + if (x->ignore_missing_files && errno == ENOENT) + return RM_OK; + + error (0, errno, _("cannot remove `%s'"), full_filename (fs->filename)); + return RM_ERROR; + } + +#ifdef ENABLE_CYCLE_CHECK + if (S_ISDIR (filetype_mode)) + { + int fail; + struct active_dir_ent *old_ent; + + /* Insert this directory in the active_dir_map. + If there is already a directory in the map with the same inum, + then there's *probably* a directory cycle. This test can get + a false positive if two directories have the same inode number + but different device numbers and one directory contains the + other. But since people don't often try to delete hierarchies + containing mount points, and when they do, duplicate inode + numbers are not that likely, this isn't worth detecting. */ + old_ent = hash_insert_if_absent (active_dir_map, + make_active_dir_ent (fs->inum, + current_depth ()), + &fail); + if (fail) + error (1, 0, _("Memory exhausted")); + + if (old_ent) + { + 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 two directories have the same inode number:\n")); + /* FIXME: test this!! */ + print_nth_dir (stderr, current_depth ()); + fputc ('\n', stderr); + print_nth_dir (stderr, old_ent->depth); + fputc ('\n', stderr); + fflush (stderr); + + free (old_ent); + + if (x->interactive) + { + error (0, 0, _("continue? ")); + if (yesno ()) + return RM_ERROR; + } + exit (1); + } + } +#endif + + if (!S_ISDIR (filetype_mode) || x->unlink_dirs) + { + return remove_file (fs, x); + } + else + { + int need_save_cwd = user_specified_name; + enum RM_status status; + + if (need_save_cwd) + need_save_cwd = (strchr (fs->filename, '/') != NULL); + + status = remove_dir (fs, need_save_cwd, x); + +#ifdef ENABLE_CYCLE_CHECK + { + struct active_dir_ent tmp; + struct active_dir_ent *old_ent; + + /* Remove this directory from the active_dir_map. */ + tmp.inum = fs->inum; + old_ent = hash_delete_if_present (active_dir_map, &tmp); + assert (old_ent != NULL); + free (old_ent); + } +#endif + + return status; + } +} + +void +remove_init (void) +{ + /* Initialize dir-stack obstacks. */ + obstack_init (&dir_stack); + obstack_init (&len_stack); + +#ifdef ENABLE_CYCLE_CHECK + active_dir_map = hash_initialize (ACTIVE_DIR_INITIAL_CAPACITY, free, + hash_active_dir_ent, + hash_compare_active_dir_ents); +#endif +} + +void +remove_fini (void) +{ +#ifdef ENABLE_CYCLE_CHECK + hash_free (active_dir_map); +#endif + + obstack_free (&dir_stack, NULL); + obstack_free (&len_stack, NULL); +} -- cgit v1.2.3-54-g00ecf