1 /* remove.c -- core functions for removing files and directories
2    Copyright (C) 1988-2023 Free Software Foundation, Inc.
3 
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8 
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13 
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
16 
17 /* Extracted from rm.c, librarified, then rewritten twice by Jim Meyering.  */
18 
19 #include <config.h>
20 #include <stdio.h>
21 #include <sys/types.h>
22 
23 #include "system.h"
24 #include "assure.h"
25 #include "file-type.h"
26 #include "filenamecat.h"
27 #include "ignore-value.h"
28 #include "remove.h"
29 #include "root-dev-ino.h"
30 #include "stat-time.h"
31 #include "write-any-file.h"
32 #include "xfts.h"
33 #include "yesno.h"
34 
35 /* The prompt function may be called twice for a given directory.
36    The first time, we ask whether to descend into it, and the
37    second time, we ask whether to remove it.  */
38 enum Prompt_action
39   {
40     PA_DESCEND_INTO_DIR = 2,
41     PA_REMOVE_DIR
42   };
43 
44 /* D_TYPE(D) is the type of directory entry D if known, DT_UNKNOWN
45    otherwise.  */
46 #if ! HAVE_STRUCT_DIRENT_D_TYPE
47 /* Any int values will do here, so long as they're distinct.
48    Undef any existing macros out of the way.  */
49 # undef DT_UNKNOWN
50 # undef DT_DIR
51 # undef DT_LNK
52 # define DT_UNKNOWN 0
53 # define DT_DIR 1
54 # define DT_LNK 2
55 #endif
56 
57 /* Like fstatat, but cache on POSIX-compatible systems.  */
58 static int
cache_fstatat(int fd,char const * file,struct stat * st,int flag)59 cache_fstatat (int fd, char const *file, struct stat *st, int flag)
60 {
61 #if HAVE_STRUCT_STAT_ST_ATIM_TV_NSEC
62   /* If ST->st_atim.tv_nsec is -1, the status has not been gotten yet.
63      If less than -1, fstatat failed with errno == ST->st_ino.
64      Otherwise, the status has already been gotten, so return 0.  */
65   if (0 <= st->st_atim.tv_nsec)
66     return 0;
67   if (st->st_atim.tv_nsec == -1)
68     {
69       if (fstatat (fd, file, st, flag) == 0)
70         return 0;
71       st->st_atim.tv_nsec = -2;
72       st->st_ino = errno;
73     }
74   errno = st->st_ino;
75   return -1;
76 #else
77   return fstatat (fd, file, st, flag);
78 #endif
79 }
80 
81 /* Initialize a fstatat cache *ST.  Return ST for convenience.  */
82 static inline struct stat *
cache_stat_init(struct stat * st)83 cache_stat_init (struct stat *st)
84 {
85 #if HAVE_STRUCT_STAT_ST_ATIM_TV_NSEC
86   st->st_atim.tv_nsec = -1;
87 #endif
88   return st;
89 }
90 
91 /* Return 1 if FILE is an unwritable non-symlink,
92    0 if it is writable or some other type of file,
93    -1 and set errno if there is some problem in determining the answer.
94    Set *BUF to the file status.  */
95 static int
write_protected_non_symlink(int fd_cwd,char const * file,struct stat * buf)96 write_protected_non_symlink (int fd_cwd,
97                              char const *file,
98                              struct stat *buf)
99 {
100   if (can_write_any_file ())
101     return 0;
102   if (cache_fstatat (fd_cwd, file, buf, AT_SYMLINK_NOFOLLOW) != 0)
103     return -1;
104   if (S_ISLNK (buf->st_mode))
105     return 0;
106   /* Here, we know FILE is not a symbolic link.  */
107 
108   /* In order to be reentrant -- i.e., to avoid changing the working
109      directory, and at the same time to be able to deal with alternate
110      access control mechanisms (ACLs, xattr-style attributes) and
111      arbitrarily deep trees -- we need a function like eaccessat, i.e.,
112      like Solaris' eaccess, but fd-relative, in the spirit of openat.  */
113 
114   /* In the absence of a native eaccessat function, here are some of
115      the implementation choices [#4 and #5 were suggested by Paul Eggert]:
116      1) call openat with O_WRONLY|O_NOCTTY
117         Disadvantage: may create the file and doesn't work for directory,
118         may mistakenly report 'unwritable' for EROFS or ACLs even though
119         perm bits say the file is writable.
120 
121      2) fake eaccessat (save_cwd, fchdir, call euidaccess, restore_cwd)
122         Disadvantage: changes working directory (not reentrant) and can't
123         work if save_cwd fails.
124 
125      3) if (euidaccess (full_name, W_OK) == 0)
126         Disadvantage: doesn't work if full_name is too long.
127         Inefficient for very deep trees (O(Depth^2)).
128 
129      4) If the full pathname is sufficiently short (say, less than
130         PATH_MAX or 8192 bytes, whichever is shorter):
131         use method (3) (i.e., euidaccess (full_name, W_OK));
132         Otherwise: vfork, fchdir in the child, run euidaccess in the
133         child, then the child exits with a status that tells the parent
134         whether euidaccess succeeded.
135 
136         This avoids the O(N**2) algorithm of method (3), and it also avoids
137         the failure-due-to-too-long-file-names of method (3), but it's fast
138         in the normal shallow case.  It also avoids the lack-of-reentrancy
139         and the save_cwd problems.
140         Disadvantage; it uses a process slot for very-long file names,
141         and would be very slow for hierarchies with many such files.
142 
143      5) If the full file name is sufficiently short (say, less than
144         PATH_MAX or 8192 bytes, whichever is shorter):
145         use method (3) (i.e., euidaccess (full_name, W_OK));
146         Otherwise: look just at the file bits.  Perhaps issue a warning
147         the first time this occurs.
148 
149         This is like (4), except for the "Otherwise" case where it isn't as
150         "perfect" as (4) but is considerably faster.  It conforms to current
151         POSIX, and is uniformly better than what Solaris and FreeBSD do (they
152         mess up with long file names). */
153 
154   {
155     if (faccessat (fd_cwd, file, W_OK, AT_EACCESS) == 0)
156       return 0;
157 
158     return errno == EACCES ? 1 : -1;
159   }
160 }
161 
162 /* Return the status of the directory identified by FTS and ENT.
163    This is -1 if the directory is empty, 0 if it is nonempty,
164    and a positive error number if there was trouble determining the status,
165    e.g., it is not a directory, or permissions problems, or I/O errors.
166    Use *DIR_STATUS as a cache for the status.  */
167 static int
get_dir_status(FTS const * fts,FTSENT const * ent,int * dir_status)168 get_dir_status (FTS const *fts, FTSENT const *ent, int *dir_status)
169 {
170   if (*dir_status == DS_UNKNOWN)
171     *dir_status = directory_status (fts->fts_cwd_fd, ent->fts_accpath);
172   return *dir_status;
173 }
174 
175 /* Prompt whether to remove FILENAME, if required via a combination of
176    the options specified by X and/or file attributes.  If the file may
177    be removed, return RM_OK or RM_USER_ACCEPTED, the latter if the user
178    was prompted and accepted.  If the user declines to remove the file,
179    return RM_USER_DECLINED.  If not ignoring missing files and we
180    cannot lstat FILENAME, then return RM_ERROR.
181 
182    IS_DIR is true if ENT designates a directory, false otherwise.
183 
184    Depending on MODE, ask whether to 'descend into' or to 'remove' the
185    directory FILENAME.  MODE is ignored when FILENAME is not a directory.
186    Use and update *DIR_STATUS as needed, via the conventions of
187    get_dir_status.  */
188 static enum RM_status
prompt(FTS const * fts,FTSENT const * ent,bool is_dir,struct rm_options const * x,enum Prompt_action mode,int * dir_status)189 prompt (FTS const *fts, FTSENT const *ent, bool is_dir,
190         struct rm_options const *x, enum Prompt_action mode,
191         int *dir_status)
192 {
193   int fd_cwd = fts->fts_cwd_fd;
194   char const *full_name = ent->fts_path;
195   char const *filename = ent->fts_accpath;
196   struct stat st;
197   struct stat *sbuf = &st;
198   cache_stat_init (sbuf);
199 
200   int dirent_type = is_dir ? DT_DIR : DT_UNKNOWN;
201   int write_protected = 0;
202 
203   /* When nonzero, this indicates that we failed to remove a child entry,
204      either because the user declined an interactive prompt, or due to
205      some other failure, like permissions.  */
206   if (ent->fts_number)
207     return RM_USER_DECLINED;
208 
209   if (x->interactive == RMI_NEVER)
210     return RM_OK;
211 
212   int wp_errno = 0;
213   if (!x->ignore_missing_files
214       && (x->interactive == RMI_ALWAYS || x->stdin_tty)
215       && dirent_type != DT_LNK)
216     {
217       write_protected = write_protected_non_symlink (fd_cwd, filename, sbuf);
218       wp_errno = errno;
219     }
220 
221   if (write_protected || x->interactive == RMI_ALWAYS)
222     {
223       if (0 <= write_protected && dirent_type == DT_UNKNOWN)
224         {
225           if (cache_fstatat (fd_cwd, filename, sbuf, AT_SYMLINK_NOFOLLOW) == 0)
226             {
227               if (S_ISLNK (sbuf->st_mode))
228                 dirent_type = DT_LNK;
229               else if (S_ISDIR (sbuf->st_mode))
230                 dirent_type = DT_DIR;
231               /* Otherwise it doesn't matter, so leave it DT_UNKNOWN.  */
232             }
233           else
234             {
235               /* This happens, e.g., with 'rm '''.  */
236               write_protected = -1;
237               wp_errno = errno;
238             }
239         }
240 
241       if (0 <= write_protected)
242         switch (dirent_type)
243           {
244           case DT_LNK:
245             /* Using permissions doesn't make sense for symlinks.  */
246             if (x->interactive != RMI_ALWAYS)
247               return RM_OK;
248             break;
249 
250           case DT_DIR:
251              /* Unless we're either deleting directories or deleting
252                 recursively, we want to raise an EISDIR error rather than
253                 prompting the user  */
254             if ( ! (x->recursive
255                     || (x->remove_empty_directories
256                         && get_dir_status (fts, ent, dir_status) != 0)))
257               {
258                 write_protected = -1;
259                 wp_errno = *dir_status <= 0 ? EISDIR : *dir_status;
260               }
261             break;
262           }
263 
264       char const *quoted_name = quoteaf (full_name);
265 
266       if (write_protected < 0)
267         {
268           error (0, wp_errno, _("cannot remove %s"), quoted_name);
269           return RM_ERROR;
270         }
271 
272       /* Issue the prompt.  */
273       if (dirent_type == DT_DIR
274           && mode == PA_DESCEND_INTO_DIR
275           && get_dir_status (fts, ent, dir_status) == DS_NONEMPTY)
276         fprintf (stderr,
277                  (write_protected
278                   ? _("%s: descend into write-protected directory %s? ")
279                   : _("%s: descend into directory %s? ")),
280                  program_name, quoted_name);
281       else if (0 < *dir_status)
282         {
283           if ( ! (x->remove_empty_directories && *dir_status == EACCES))
284             {
285               error (0, *dir_status, _("cannot remove %s"), quoted_name);
286               return RM_ERROR;
287             }
288 
289           /* The following code can lead to a successful deletion only with
290              the --dir (-d) option (remove_empty_directories) and an empty
291              inaccessible directory. In the first prompt call for a directory,
292              we'd normally ask whether to descend into it, but in this case
293              (it's inaccessible), that is not possible, so don't prompt.  */
294           if (mode == PA_DESCEND_INTO_DIR)
295             return RM_OK;
296 
297           fprintf (stderr,
298                _("%s: attempt removal of inaccessible directory %s? "),
299                    program_name, quoted_name);
300         }
301       else
302         {
303           if (cache_fstatat (fd_cwd, filename, sbuf, AT_SYMLINK_NOFOLLOW) != 0)
304             {
305               error (0, errno, _("cannot remove %s"), quoted_name);
306               return RM_ERROR;
307             }
308 
309           fprintf (stderr,
310                    (write_protected
311                     /* TRANSLATORS: In the next two strings the second %s is
312                        replaced by the type of the file.  To avoid grammatical
313                        problems, it may be more convenient to translate these
314                        strings instead as: "%1$s: %3$s is write-protected and
315                        is of type '%2$s' -- remove it? ".  */
316                     ? _("%s: remove write-protected %s %s? ")
317                     : _("%s: remove %s %s? ")),
318                    program_name, file_type (sbuf), quoted_name);
319         }
320 
321       return yesno () ? RM_USER_ACCEPTED : RM_USER_DECLINED;
322     }
323   return RM_OK;
324 }
325 
326 /* When a function like unlink, rmdir, or fstatat fails with an errno
327    value of ERRNUM, return true if the specified file system object
328    is guaranteed not to exist;  otherwise, return false.  */
329 static inline bool
nonexistent_file_errno(int errnum)330 nonexistent_file_errno (int errnum)
331 {
332   /* Do not include ELOOP here, since the specified file may indeed
333      exist, but be (in)accessible only via too long a symlink chain.
334      Likewise for ENAMETOOLONG, since rm -f ./././.../foo may fail
335      if the "..." part expands to a long enough sequence of "./"s,
336      even though ./foo does indeed exist.
337 
338      Another case to consider is when a particular name is invalid for
339      a given file system.  In 2011, smbfs returns EINVAL, but the next
340      revision of POSIX will require EILSEQ for that situation:
341      https://austingroupbugs.net/view.php?id=293
342   */
343 
344   switch (errnum)
345     {
346     case EILSEQ:
347     case EINVAL:
348     case ENOENT:
349     case ENOTDIR:
350       return true;
351     default:
352       return false;
353     }
354 }
355 
356 /* Encapsulate the test for whether the errno value, ERRNUM, is ignorable.  */
357 static inline bool
ignorable_missing(struct rm_options const * x,int errnum)358 ignorable_missing (struct rm_options const *x, int errnum)
359 {
360   return x->ignore_missing_files && nonexistent_file_errno (errnum);
361 }
362 
363 /* Tell fts not to traverse into the hierarchy at ENT.  */
364 static void
fts_skip_tree(FTS * fts,FTSENT * ent)365 fts_skip_tree (FTS *fts, FTSENT *ent)
366 {
367   fts_set (fts, ent, FTS_SKIP);
368   /* Ensure that we do not process ENT a second time.  */
369   ignore_value (fts_read (fts));
370 }
371 
372 /* Upon unlink failure, or when the user declines to remove ENT, mark
373    each of its ancestor directories, so that we know not to prompt for
374    its removal.  */
375 static void
mark_ancestor_dirs(FTSENT * ent)376 mark_ancestor_dirs (FTSENT *ent)
377 {
378   FTSENT *p;
379   for (p = ent->fts_parent; FTS_ROOTLEVEL <= p->fts_level; p = p->fts_parent)
380     {
381       if (p->fts_number)
382         break;
383       p->fts_number = 1;
384     }
385 }
386 
387 /* Remove the file system object specified by ENT.  IS_DIR specifies
388    whether it is expected to be a directory or non-directory.
389    Return RM_OK upon success, else RM_ERROR.  */
390 static enum RM_status
excise(FTS * fts,FTSENT * ent,struct rm_options const * x,bool is_dir)391 excise (FTS *fts, FTSENT *ent, struct rm_options const *x, bool is_dir)
392 {
393   int flag = is_dir ? AT_REMOVEDIR : 0;
394   if (unlinkat (fts->fts_cwd_fd, ent->fts_accpath, flag) == 0)
395     {
396       if (x->verbose)
397         {
398           printf ((is_dir
399                    ? _("removed directory %s\n")
400                    : _("removed %s\n")), quoteaf (ent->fts_path));
401         }
402       return RM_OK;
403     }
404 
405   /* The unlinkat from kernels like linux-2.6.32 reports EROFS even for
406      nonexistent files.  When the file is indeed missing, map that to ENOENT,
407      so that rm -f ignores it, as required.  Even without -f, this is useful
408      because it makes rm print the more precise diagnostic.  */
409   if (errno == EROFS)
410     {
411       struct stat st;
412       if ( ! (fstatat (fts->fts_cwd_fd, ent->fts_accpath, &st,
413                        AT_SYMLINK_NOFOLLOW)
414               && errno == ENOENT))
415         errno = EROFS;
416     }
417 
418   if (ignorable_missing (x, errno))
419     return RM_OK;
420 
421   /* When failing to rmdir an unreadable directory, we see errno values
422      like EISDIR or ENOTDIR (or, on Solaris 10, EEXIST), but they would be
423      meaningless in a diagnostic.  When that happens, use the earlier, more
424      descriptive errno value.  */
425   if (ent->fts_info == FTS_DNR
426       && (errno == ENOTEMPTY || errno == EISDIR || errno == ENOTDIR
427           || errno == EEXIST)
428       && ent->fts_errno != 0)
429     errno = ent->fts_errno;
430   error (0, errno, _("cannot remove %s"), quoteaf (ent->fts_path));
431   mark_ancestor_dirs (ent);
432   return RM_ERROR;
433 }
434 
435 /* This function is called once for every file system object that fts
436    encounters.  fts performs a depth-first traversal.
437    A directory is usually processed twice, first with fts_info == FTS_D,
438    and later, after all of its entries have been processed, with FTS_DP.
439    Return RM_ERROR upon error, RM_USER_DECLINED for a negative response
440    to an interactive prompt, and otherwise, RM_OK.  */
441 static enum RM_status
rm_fts(FTS * fts,FTSENT * ent,struct rm_options const * x)442 rm_fts (FTS *fts, FTSENT *ent, struct rm_options const *x)
443 {
444   int dir_status = DS_UNKNOWN;
445 
446   switch (ent->fts_info)
447     {
448     case FTS_D:			/* preorder directory */
449       if (! x->recursive
450           && !(x->remove_empty_directories
451                && get_dir_status (fts, ent, &dir_status) != 0))
452         {
453           /* This is the first (pre-order) encounter with a directory
454              that we cannot delete.
455              Not recursive, and it's not an empty directory (if we're removing
456              them) so arrange to skip contents.  */
457           int err = x->remove_empty_directories ? ENOTEMPTY : EISDIR;
458           error (0, err, _("cannot remove %s"), quoteaf (ent->fts_path));
459           mark_ancestor_dirs (ent);
460           fts_skip_tree (fts, ent);
461           return RM_ERROR;
462         }
463 
464       /* Perform checks that can apply only for command-line arguments.  */
465       if (ent->fts_level == FTS_ROOTLEVEL)
466         {
467           /* POSIX says:
468              If the basename of a command line argument is "." or "..",
469              diagnose it and do nothing more with that argument.  */
470           if (dot_or_dotdot (last_component (ent->fts_accpath)))
471             {
472               error (0, 0,
473                      _("refusing to remove %s or %s directory: skipping %s"),
474                      quoteaf_n (0, "."), quoteaf_n (1, ".."),
475                      quoteaf_n (2, ent->fts_path));
476               fts_skip_tree (fts, ent);
477               return RM_ERROR;
478             }
479 
480           /* POSIX also says:
481              If a command line argument resolves to "/" (and --preserve-root
482              is in effect -- default) diagnose and skip it.  */
483           if (ROOT_DEV_INO_CHECK (x->root_dev_ino, ent->fts_statp))
484             {
485               ROOT_DEV_INO_WARN (ent->fts_path);
486               fts_skip_tree (fts, ent);
487               return RM_ERROR;
488             }
489 
490           /* If a command line argument is a mount point and
491              --preserve-root=all is in effect, diagnose and skip it.
492              This doesn't handle "/", but that's handled above.  */
493           if (x->preserve_all_root)
494             {
495               bool failed = false;
496               char *parent = file_name_concat (ent->fts_accpath, "..", nullptr);
497               struct stat statbuf;
498 
499               if (!parent || lstat (parent, &statbuf))
500                 {
501                   error (0, 0,
502                          _("failed to stat %s: skipping %s"),
503                          quoteaf_n (0, parent),
504                          quoteaf_n (1, ent->fts_accpath));
505                   failed = true;
506                 }
507 
508               free (parent);
509 
510               if (failed || fts->fts_dev != statbuf.st_dev)
511                 {
512                   if (! failed)
513                     {
514                       error (0, 0,
515                              _("skipping %s, since it's on a different device"),
516                              quoteaf (ent->fts_path));
517                       error (0, 0, _("and --preserve-root=all is in effect"));
518                     }
519                   fts_skip_tree (fts, ent);
520                   return RM_ERROR;
521                 }
522             }
523         }
524 
525       {
526         enum RM_status s = prompt (fts, ent, true /*is_dir*/, x,
527                                    PA_DESCEND_INTO_DIR, &dir_status);
528 
529         if (s == RM_USER_ACCEPTED && dir_status == DS_EMPTY)
530           {
531             /* When we know (from prompt when in interactive mode)
532                that this is an empty directory, don't prompt twice.  */
533             s = excise (fts, ent, x, true);
534             if (s == RM_OK)
535               fts_skip_tree (fts, ent);
536           }
537 
538         if (! (s == RM_OK || s == RM_USER_ACCEPTED))
539           {
540             mark_ancestor_dirs (ent);
541             fts_skip_tree (fts, ent);
542           }
543 
544         return s;
545       }
546 
547     case FTS_F:			/* regular file */
548     case FTS_NS:		/* stat(2) failed */
549     case FTS_SL:		/* symbolic link */
550     case FTS_SLNONE:		/* symbolic link without target */
551     case FTS_DP:		/* postorder directory */
552     case FTS_DNR:		/* unreadable directory */
553     case FTS_NSOK:		/* e.g., dangling symlink */
554     case FTS_DEFAULT:		/* none of the above */
555       {
556         /* With --one-file-system, do not attempt to remove a mount point.
557            fts' FTS_XDEV ensures that we don't process any entries under
558            the mount point.  */
559         if (ent->fts_info == FTS_DP
560             && x->one_file_system
561             && FTS_ROOTLEVEL < ent->fts_level
562             && ent->fts_statp->st_dev != fts->fts_dev)
563           {
564             mark_ancestor_dirs (ent);
565             error (0, 0, _("skipping %s, since it's on a different device"),
566                    quoteaf (ent->fts_path));
567             return RM_ERROR;
568           }
569 
570         bool is_dir = ent->fts_info == FTS_DP || ent->fts_info == FTS_DNR;
571         enum RM_status s = prompt (fts, ent, is_dir, x, PA_REMOVE_DIR,
572                                    &dir_status);
573         if (! (s == RM_OK || s == RM_USER_ACCEPTED))
574           return s;
575         return excise (fts, ent, x, is_dir);
576       }
577 
578     case FTS_DC:		/* directory that causes cycles */
579       emit_cycle_warning (ent->fts_path);
580       fts_skip_tree (fts, ent);
581       return RM_ERROR;
582 
583     case FTS_ERR:
584       /* Various failures, from opendir to ENOMEM, to failure to "return"
585          to preceding directory, can provoke this.  */
586       error (0, ent->fts_errno, _("traversal failed: %s"),
587              quotef (ent->fts_path));
588       fts_skip_tree (fts, ent);
589       return RM_ERROR;
590 
591     default:
592       error (0, 0, _("unexpected failure: fts_info=%d: %s\n"
593                      "please report to %s"),
594              ent->fts_info,
595              quotef (ent->fts_path),
596              PACKAGE_BUGREPORT);
597       abort ();
598     }
599 }
600 
601 /* Remove FILEs, honoring options specified via X.
602    Return RM_OK if successful.  */
603 enum RM_status
rm(char * const * file,struct rm_options const * x)604 rm (char *const *file, struct rm_options const *x)
605 {
606   enum RM_status rm_status = RM_OK;
607 
608   if (*file)
609     {
610       int bit_flags = (FTS_CWDFD
611                        | FTS_NOSTAT
612                        | FTS_PHYSICAL);
613 
614       if (x->one_file_system)
615         bit_flags |= FTS_XDEV;
616 
617       FTS *fts = xfts_open (file, bit_flags, nullptr);
618 
619       while (true)
620         {
621           FTSENT *ent;
622 
623           ent = fts_read (fts);
624           if (ent == nullptr)
625             {
626               if (errno != 0)
627                 {
628                   error (0, errno, _("fts_read failed"));
629                   rm_status = RM_ERROR;
630                 }
631               break;
632             }
633 
634           enum RM_status s = rm_fts (fts, ent, x);
635 
636           affirm (VALID_STATUS (s));
637           UPDATE_STATUS (rm_status, s);
638         }
639 
640       if (fts_close (fts) != 0)
641         {
642           error (0, errno, _("fts_close failed"));
643           rm_status = RM_ERROR;
644         }
645     }
646 
647   return rm_status;
648 }
649