Files
tar/src/unlink.c
T
Pavel CahynaandPaul Eggert 1b91f5f66f Draft patch for openat2 changes vs --one-top-level
The patch leverages the existing -C code. In order to do that, every
entry in the wd[] table gets another companion entry in the table that
represents the --one-top-level directory. There is one additional field
in each entry that allows skipping the companion entries if they are not
desired.

The actual directory is created lazily by chdir_do() if needed, as you
requested, to avoid empty "a/foo" after --one-top-level=foo -C a -C b.

The patch "by the way" fixes also extraction of hardlinks with
--one-top-level which currently is broken in the typical case (the
transform is not applied to the target, so the hardlink is wrong).

The patch does not yet handle the --show-transformed case with
--one-top-level that you discussed in another subthread. As a result, two
tests now fail (onetop02.at and onetop04.at). I suppose that this would
be quite easy to fix.

Another issue that I am aware of is that I am not sure whether to call
repair_delayed_set_stat and/or delay_set_stat on the newly created
directories like extract_dir() does (the whole delay_set code is abit
mysterious to me).

Use of --create together with --one-top-level should probably be
forbidden, as unlink.c uses wd[] in a way that will likely break in the
presence of companion entries (the chdir_do call in
flush_deferred_unlinks).

Show the one-top-level arg as a transformation
2026-07-29 23:12:46 -07:00

234 lines
5.2 KiB
C

/* Unlink files.
Copyright 2009-2026 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, false);
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, false);
if (p->dir_idx && is_cwd (p))
{
fname = tar_dirname ();
chdir_do (p->dir_idx - 1, false);
}
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, false);
}
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);
}