summaryrefslogtreecommitdiff
path: root/src/remove.c
blob: c76bffaa883ebe659eb2124bb286974f343662ab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
/* remove.c -- core functions for removing files and directories
   Copyright (C) 88, 90, 91, 1994-2001 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>

#if HAVE_STDBOOL_H
# include <stdbool.h>
#else
typedef enum {false = 0, true = 1} bool;
#endif

#include "save-cwd.h"
#include "system.h"
#include "dirname.h"
#include "error.h"
#include "obstack.h"
#include "hash.h"
#include "quote.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

/* 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  */

/* An entry in the active_dir_map.  */
struct active_dir_ent
{
  ino_t st_ino;
  dev_t st_dev;
  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 hash_table *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 (depth < current_depth ());

  for (i = 0; i <= depth; i++)
    {
      sum += length[i];
    }

  fwrite (dir_name, 1, sum - 1, stream);
}

static inline struct active_dir_ent *
make_active_dir_ent (ino_t inum, dev_t device, unsigned int depth)
{
  struct active_dir_ent *ent;
  ent = (struct active_dir_ent *) xmalloc (sizeof *ent);
  ent->st_ino = inum;
  ent->st_dev = device;
  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;

  /* Ignoring the device number here should be fine.  */
  return ade->st_ino % table_size;
}

static bool
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 SAME_INODE (*a, *b) ? true : false;
}

/* 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 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.  */

static 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 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);

	  /* 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;
}

/* 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"), 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,
	       _("ERROR: the directory %s initially had device/inode\n\
numbers %lu/%lu, but now (after a chdir into it), the numbers for `.'\n\
are %lu/%lu.  That means that while rm was running, the directory\n\
was replaced with either another directory or a link to another directory."),
	       quote (full_filename (dir_name)),
	       (unsigned long)(fs->st_dev),
	       (unsigned long)(fs->st_ino),
	       (unsigned long)(sb.st_dev),
	       (unsigned long)(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);

  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 `..'"),
	     quote (full_filename (dir_name)));
      return RM_ERROR;
    }

  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).  */

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)
    {
      /* 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;
	}
    }

  if (fspec_get_filetype_mode (fs, &filetype_mode))
    {
      if (x->ignore_missing_files && errno == ENOENT)
	return RM_OK;

      error (0, errno, _("cannot remove %s"),
	     quote (full_filename (fs->filename)));
      return RM_ERROR;
    }

#ifdef ENABLE_CYCLE_CHECK
  if (S_ISDIR (filetype_mode))
    {
      struct active_dir_ent *old_ent;
      struct active_dir_ent *new_ent;

      /* If there is already a directory in the map with the same device
	 and inode numbers, then there is a directory cycle.  */

      if (fspec_get_device_number (fs))
	{
	  error (0, errno, _("cannot stat %s"),
		 quote (full_filename (fs->filename)));
	  return RM_ERROR;
	}
      new_ent = make_active_dir_ent (fs->st_ino, fs->st_dev, current_depth ());
      old_ent = hash_lookup (active_dir_map, new_ent);
      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"));
	  print_nth_dir (stderr, old_ent->depth);
	  fprintf (stderr, "\n%s\n", quote (full_filename (fs->filename)));
	  fflush (stderr);

	  if (x->interactive)
	    {
	      error (0, 0, _("continue? "));
	      if (yesno ())
		return RM_ERROR;
	    }
	  exit (1);
	}

      /* Put this directory in the active_dir_map.  */
      if (! hash_insert (active_dir_map, new_ent))
	xalloc_die ();
    }
#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.st_ino = fs->st_ino;
	assert (fs->have_device);
	tmp.st_dev = fs->st_dev;
	old_ent = hash_delete (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, NULL,
				    hash_active_dir_ent,
				    hash_compare_active_dir_ents, free);
#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);
}