mirror of
git://git.sv.gnu.org/coreutils.git
synced 2026-03-30 16:56:33 +02:00
858 lines
23 KiB
C
858 lines
23 KiB
C
/* remove.c -- core functions for removing files and directories
|
|
Copyright (C) 88, 90, 91, 1994-2002 Free Software Foundation, Inc.
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2, or (at your option)
|
|
any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software Foundation,
|
|
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */
|
|
|
|
/* Extracted from rm.c and librarified by Jim Meyering. */
|
|
|
|
#ifdef _AIX
|
|
#pragma alloca
|
|
#endif
|
|
|
|
#include <config.h>
|
|
#include <stdio.h>
|
|
#include <sys/types.h>
|
|
#include <assert.h>
|
|
|
|
#include "save-cwd.h"
|
|
#include "system.h"
|
|
#include "dirname.h"
|
|
#include "error.h"
|
|
#include "obstack.h"
|
|
#include "hash.h"
|
|
#include "hash-pjw.h"
|
|
#include "quote.h"
|
|
#include "remove.h"
|
|
|
|
/* Avoid shadowing warnings because these are functions declared
|
|
in dirname.h as well as locals used below. */
|
|
#define dir_name rm_dir_name
|
|
#define dir_len rm_dir_len
|
|
|
|
#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
|
|
|
|
/* 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
|
|
int rpl_lstat PARAMS((const char *, struct stat *));
|
|
# define lstat(Name, Stat_buf) rpl_lstat(Name, Stat_buf)
|
|
#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
|
|
|
|
/* 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
|
|
|
|
int euidaccess ();
|
|
int yesno ();
|
|
|
|
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,
|
|
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;
|
|
|
|
static inline unsigned int
|
|
current_depth (void)
|
|
{
|
|
return obstack_object_size (&len_stack) / sizeof (size_t);
|
|
}
|
|
|
|
static bool
|
|
hash_compare_strings (void const *x, void const *y)
|
|
{
|
|
return STREQ (x, y) ? true : false;
|
|
}
|
|
|
|
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, (int) -(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;
|
|
}
|
|
|
|
static inline void
|
|
fspec_init_common (struct File_spec *fs)
|
|
{
|
|
fs->have_full_mode = 0;
|
|
fs->have_filetype_mode = 0;
|
|
fs->have_device = 0;
|
|
}
|
|
|
|
void
|
|
fspec_init_file (struct File_spec *fs, const char *filename)
|
|
{
|
|
fs->filename = (char *) filename;
|
|
fspec_init_common (fs);
|
|
}
|
|
|
|
static inline void
|
|
fspec_init_dp (struct File_spec *fs, struct dirent *dp)
|
|
{
|
|
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
|
|
}
|
|
|
|
static inline int
|
|
fspec_get_full_mode (struct File_spec *fs)
|
|
{
|
|
struct stat stat_buf;
|
|
|
|
if (fs->have_full_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->st_ino = stat_buf.st_ino;
|
|
fs->have_device = 1;
|
|
fs->st_dev = stat_buf.st_dev;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static inline int
|
|
fspec_get_device_number (struct File_spec *fs)
|
|
{
|
|
struct stat stat_buf;
|
|
|
|
if (fs->have_device)
|
|
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->st_ino = stat_buf.st_ino;
|
|
fs->have_device = 1;
|
|
fs->st_dev = stat_buf.st_dev;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static inline int
|
|
fspec_get_filetype_mode (struct File_spec *fs, mode_t *filetype_mode)
|
|
{
|
|
int fail;
|
|
|
|
fail = fs->have_filetype_mode ? 0 : fspec_get_full_mode (fs);
|
|
if (!fail)
|
|
*filetype_mode = fs->mode;
|
|
|
|
return fail;
|
|
}
|
|
|
|
static inline mode_t
|
|
fspec_filetype_mode (const struct File_spec *fs)
|
|
{
|
|
assert (fs->have_filetype_mode);
|
|
return fs->mode;
|
|
}
|
|
|
|
static int
|
|
same_file (const char *file_1, const char *file_2)
|
|
{
|
|
struct stat sb1, sb2;
|
|
return (lstat (file_1, &sb1) == 0
|
|
&& lstat (file_2, &sb2) == 0
|
|
&& SAME_INODE (sb1, sb2));
|
|
}
|
|
|
|
|
|
/* 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. */
|
|
|
|
static enum RM_status
|
|
remove_cwd_entries (const struct rm_options *x,
|
|
struct dev_ino const *cwd_dev_ino)
|
|
{
|
|
/* 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);
|
|
}
|
|
|
|
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;
|
|
|
|
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, cwd_dev_ino);
|
|
|
|
/* 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)
|
|
{
|
|
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 (dirp == NULL)
|
|
break;
|
|
}
|
|
}
|
|
while (dirp == NULL);
|
|
|
|
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;
|
|
}
|
|
|
|
/* 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, 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;
|
|
}
|
|
|
|
static inline bool
|
|
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)
|
|
{
|
|
#ifdef ENABLE_CYCLE_CHECK
|
|
/* If there is a directory cycle, detect it (lazily) and die. */
|
|
static struct dev_ino dir_cycle_detect_dev_ino;
|
|
static unsigned int chdir_counter;
|
|
|
|
/* If the current directory ever happens to be the same
|
|
as the one we last recorded for the cycle detection,
|
|
then it's obviously part of a cycle. */
|
|
if (chdir_counter && SAME_INODE (*sb, dir_cycle_detect_dev_ino))
|
|
{
|
|
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 (".")));
|
|
exit (EXIT_FAILURE);
|
|
}
|
|
|
|
/* If the number of `descending' chdir calls is a power of two,
|
|
record the dev/ino of the current directory. */
|
|
if (is_power_of_two (++chdir_counter))
|
|
{
|
|
dir_cycle_detect_dev_ino.st_dev = sb->st_dev;
|
|
dir_cycle_detect_dev_ino.st_ino = sb->st_ino;
|
|
}
|
|
#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)
|
|
{
|
|
enum RM_status status;
|
|
struct saved_cwd cwd;
|
|
char *dir_name = fs->filename;
|
|
const char *fmt = NULL;
|
|
struct dev_ino tmp_cwd_dev_ino;
|
|
|
|
if (!x->recursive)
|
|
{
|
|
error (0, 0, _("%s is a directory"), quote (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, quote (full_filename (dir_name)));
|
|
if (!yesno ())
|
|
return RM_USER_DECLINED;
|
|
}
|
|
|
|
if (x->verbose)
|
|
printf (_("removing all entries of directory %s\n"),
|
|
quote (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"),
|
|
quote (full_filename (dir_name)));
|
|
if (need_save_cwd)
|
|
free_cwd (&cwd);
|
|
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)));
|
|
|
|
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)));
|
|
}
|
|
|
|
cycle_check (&sb);
|
|
|
|
tmp_cwd_dev_ino.st_dev = sb.st_dev;
|
|
tmp_cwd_dev_ino.st_ino = sb.st_ino;
|
|
}
|
|
|
|
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, &tmp_cwd_dev_ino);
|
|
|
|
pop_dir ();
|
|
|
|
/* Restore cwd. */
|
|
if (need_save_cwd)
|
|
{
|
|
if (restore_cwd (&cwd, NULL, NULL))
|
|
{
|
|
free_cwd (&cwd);
|
|
return RM_ERROR;
|
|
}
|
|
free_cwd (&cwd);
|
|
}
|
|
else
|
|
{
|
|
struct stat sb;
|
|
if (chdir ("..") < 0)
|
|
{
|
|
error (0, errno, _("cannot change back to directory %s via `..'"),
|
|
quote (full_filename (dir_name)));
|
|
return RM_ERROR;
|
|
}
|
|
|
|
if (lstat (".", &sb))
|
|
error (EXIT_FAILURE, errno,
|
|
_("cannot lstat `.' in %s"), quote (full_filename (".")));
|
|
|
|
if (!SAME_INODE (sb, *cwd_dev_ino))
|
|
{
|
|
error (EXIT_FAILURE, 0,
|
|
_("subdirectory of %s was moved while being removed"),
|
|
quote (full_filename (".")));
|
|
}
|
|
}
|
|
|
|
if (x->interactive)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (x->verbose)
|
|
printf (_("removing the directory itself: %s\n"),
|
|
quote (full_filename (dir_name)));
|
|
|
|
if (rmdir (dir_name) && (errno != ENOENT || !x->ignore_missing_files))
|
|
{
|
|
int saved_errno = errno;
|
|
|
|
#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))
|
|
{
|
|
error (0, 0, _("cannot remove current directory %s"),
|
|
quote (full_filename (dir_name)));
|
|
}
|
|
else
|
|
{
|
|
error (0, saved_errno, _("cannot remove directory %s"),
|
|
quote (full_filename (dir_name)));
|
|
}
|
|
return RM_ERROR;
|
|
}
|
|
|
|
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. */
|
|
|
|
enum RM_status
|
|
rm (struct File_spec *fs, int user_specified_name,
|
|
struct rm_options const *x, struct dev_ino const *cwd_dev_ino)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
/* 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 (x->ignore_missing_files && errno == ENOENT)
|
|
return RM_OK;
|
|
|
|
error (0, errno, _("cannot remove %s"),
|
|
quote (full_filename (fs->filename)));
|
|
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_dir (fs, need_save_cwd, x, cwd_dev_ino);
|
|
|
|
return status;
|
|
}
|
|
}
|
|
|
|
void
|
|
remove_init (void)
|
|
{
|
|
/* Initialize dir-stack obstacks. */
|
|
obstack_init (&dir_stack);
|
|
obstack_init (&len_stack);
|
|
}
|
|
|
|
void
|
|
remove_fini (void)
|
|
{
|
|
obstack_free (&dir_stack, NULL);
|
|
obstack_free (&len_stack, NULL);
|
|
}
|