Files
tar/src/unlink.c
Paul Eggert bdd773d028 Cache parent directories
Although this might help (or hurt) performance, the main
motivation is to make it easier in future commits
to prevent tarballs from escaping the extraction directory.
* src/common.h: (BADFD): New constant.
(struct fdbase): New type.
* src/create.c (dump_file0): Use parent->fd instead of caching
it into a local, as the latter approach is now awkward.
* src/extract.c (extract_link): Don’t save errno unless needed.
* src/misc.c (safer_rmdir): New arg F.  All callers changed.
(maybe_backup_file): Construct full after_backup_name, now
that find_backup_file_name no longer does that for us.
(chdir_fd): Now static not extern, as other modules now use fdbase.
(fdbase_cache): New static var.
(fdbase_clear): New function.  Call it whenever removing
or renaming directories or symlinks to directories.
(fdbase_opendir): New static function.
(fdbase, fdbase1): New functions.  Call them whenever the
code formerly passed chdir_fd to a syscall.
2025-11-15 15:10:48 -08:00

234 lines
5.1 KiB
C

/* Unlink files.
Copyright 2009-2025 Free Software Foundation, Inc.
This file is part of GNU tar.
GNU tar 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 3 of the License, or
(at your option) any later version.
GNU tar 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, see <http://www.gnu.org/licenses/>. */
#include <system.h>
#include "common.h"
#include <quotearg.h>
struct deferred_unlink
{
struct deferred_unlink *next; /* Next unlink in the queue */
idx_t dir_idx; /* Directory index in wd */
char *file_name; /* Name of the file to unlink, relative
to dir_idx */
bool is_dir; /* True if file_name is a directory */
off_t records_written; /* Number of records written when this
entry got added to the queue */
};
static bool
is_cwd (struct deferred_unlink const *p)
{
return p->is_dir && !p->file_name[p->file_name[0] == '.'];
}
/* The unlink queue */
static struct deferred_unlink *dunlink_head, *dunlink_tail;
/* List of entries available for allocation */
static struct deferred_unlink *dunlink_avail;
static struct deferred_unlink *
dunlink_alloc (void)
{
struct deferred_unlink *p = dunlink_avail;
if (!p)
return xmalloc (sizeof *p);
dunlink_avail = p->next;
return p;
}
static void
dunlink_insert (struct deferred_unlink *anchor, struct deferred_unlink *p)
{
if (anchor)
{
p->next = anchor->next;
anchor->next = p;
}
else
{
p->next = dunlink_head;
dunlink_head = p;
}
if (!p->next)
dunlink_tail = p;
}
static void
dunlink_reclaim (struct deferred_unlink *p)
{
free (p->file_name);
p->next = dunlink_avail;
dunlink_avail = p;
}
static void
flush_deferred_unlinks (bool force)
{
struct deferred_unlink *p, *prev = NULL;
idx_t saved_chdir = chdir_current;
for (p = dunlink_head; p; )
{
struct deferred_unlink *next = p->next;
if (force
|| p->records_written < records_written)
{
chdir_do (p->dir_idx);
if (p->is_dir)
{
const char *fname;
if (p->dir_idx && is_cwd (p))
{
prev = p;
p = next;
continue;
}
else
fname = p->file_name;
struct fdbase f = fdbase (fname);
if (f.fd != BADFD && unlinkat (f.fd, f.base, AT_REMOVEDIR) == 0)
fdbase_clear ();
else
{
switch (errno)
{
case ENOENT:
/* nothing to worry about */
break;
case EEXIST:
/* OpenSolaris >=10 sets EEXIST instead of ENOTEMPTY
if trying to remove a non-empty directory */
#if defined ENOTEMPTY && ENOTEMPTY != EEXIST
case ENOTEMPTY:
#endif
/* Keep the record in list, in the hope we'll
be able to remove it later */
prev = p;
p = next;
continue;
default:
rmdir_error (fname);
}
}
}
else
{
struct fdbase f = fdbase (p->file_name);
if (f.fd != BADFD && unlinkat (f.fd, f.base, 0) == 0)
fdbase_clear ();
else if (errno != ENOENT)
unlink_error (p->file_name);
}
dunlink_reclaim (p);
p = next;
if (prev)
prev->next = p;
else
dunlink_head = p;
}
else
{
prev = p;
p = next;
}
}
if (!dunlink_head)
dunlink_tail = NULL;
else if (force)
{
for (p = dunlink_head; p; )
{
struct deferred_unlink *next = p->next;
const char *fname;
chdir_do (p->dir_idx);
if (p->dir_idx && is_cwd (p))
{
fname = tar_dirname ();
chdir_do (p->dir_idx - 1);
}
else
fname = p->file_name;
struct fdbase f = fdbase (fname);
if (f.fd != BADFD && unlinkat (f.fd, f.base, AT_REMOVEDIR) == 0)
fdbase_clear ();
else if (errno != ENOENT)
rmdir_error (fname);
dunlink_reclaim (p);
p = next;
}
dunlink_head = dunlink_tail = NULL;
}
chdir_do (saved_chdir);
}
void
finish_deferred_unlinks (void)
{
flush_deferred_unlinks (true);
while (dunlink_avail)
{
struct deferred_unlink *next = dunlink_avail->next;
free (dunlink_avail);
dunlink_avail = next;
}
}
void
queue_deferred_unlink (const char *name, bool is_dir)
{
struct deferred_unlink *p;
if (dunlink_head
&& records_written > dunlink_head->records_written)
flush_deferred_unlinks (false);
p = dunlink_alloc ();
p->next = NULL;
p->dir_idx = chdir_current;
p->file_name = xstrdup (name);
normalize_filename_x (p->file_name);
p->is_dir = is_dir;
p->records_written = records_written;
if (is_cwd (p))
{
struct deferred_unlink *q, *prev;
for (q = dunlink_head, prev = NULL; q; prev = q, q = q->next)
if (is_cwd (q) && q->dir_idx < p->dir_idx)
break;
if (q)
dunlink_insert (prev, p);
else
dunlink_insert (dunlink_tail, p);
}
else
dunlink_insert (dunlink_tail, p);
}