]> git.ipfire.org Git - thirdparty/tar.git/commitdiff
Draft patch for openat2 changes vs --one-top-level
authorPavel Cahyna <pcahyna@redhat.com>
Tue, 12 May 2026 15:56:44 +0000 (08:56 -0700)
committerPaul Eggert <eggert@cs.ucla.edu>
Thu, 30 Jul 2026 06:12:46 +0000 (23:12 -0700)
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

src/common.h
src/extract.c
src/list.c
src/misc.c
src/names.c
src/tar.c
src/unlink.c
src/update.c

index e1332a512f3aac09730c1e6999f6b7c8025c6314..84d54114c3840361bde808ee903115248bb05593 100644 (file)
@@ -558,6 +558,7 @@ void verify_volume (void);
 extern dev_t root_device;
 
 void extr_init (void);
+bool create_dir (char const *file_name);
 void extract_archive (void);
 void extract_finish (void);
 bool rename_directory (char *src, char *dst);
@@ -687,6 +688,8 @@ void namebuf_free (namebuf_t buf);
 char *namebuf_name (namebuf_t buf, const char *name);
 
 const char *tar_dirname (void);
+char *transform_top_level (const char *name)
+  _GL_ATTRIBUTE_MALLOC _GL_ATTRIBUTE_DEALLOC_FREE;
 
 /* intmax (N) is like ((intmax_t) (N)) except without a cast so
    that it is an error if N is a pointer.  */
@@ -779,8 +782,8 @@ idx_t blocking_write (int fd, void const *buf, idx_t count);
 enum { BADFD = AT_FDCWD == -1 ? -2 : -1 };
 
 extern idx_t chdir_current;
-idx_t chdir_arg (char const *dir);
-void chdir_do (idx_t dir);
+idx_t chdir_arg (char const *dir, bool one_top_level);
+void chdir_do (idx_t dir, bool create);
 struct chdir_id { int err; dev_t st_dev; ino_t st_ino; } chdir_id (void);
 struct fdbase fdbase (char const *);
 struct fdbase fdbase1 (char const *);
index 5cc3ccd6e8567df5b17ea4709d8b1eb815ca836e..2ad7806d407f15cb70792e8a9109415cd2d88627 100644 (file)
@@ -216,6 +216,8 @@ static Hash_table *delayed_link_table;
 static struct delayed_link *delayed_link_head;
 static struct delayed_link **delayed_link_tail = &delayed_link_head;
 
+static bool one_top_level_prepare = false;
+
 struct string_list
   {
     struct string_list *next;
@@ -815,9 +817,10 @@ make_directories (char *file_name, bool *interdir_made)
          /* Create a struct delayed_set_stat even if
             mode == desired_mode, because
             repair_delayed_set_stat may need to update the struct.  */
-         delay_set_stat (file_name,
-                         NULL, mode & ~ current_umask, MODE_RWX,
-                         desired_mode, AT_SYMLINK_NOFOLLOW);
+         if (! one_top_level_prepare)
+           delay_set_stat (file_name,
+                           NULL, mode & ~ current_umask, MODE_RWX,
+                           desired_mode, AT_SYMLINK_NOFOLLOW);
          if (interdir_made)
            *interdir_made = true;
          print_for_mkdir (file_name, desired_mode);
@@ -1049,7 +1052,7 @@ apply_nonancestor_delayed_set_stat (char const *file_name, bool metadata_set)
              && memeq (file_name, data->file_name, data->file_name_len)))
        break;
 
-      chdir_do (data->change_dir);
+      chdir_do (data->change_dir, false);
 
       if (check_for_renamed_directories)
        {
@@ -1129,6 +1132,73 @@ safe_dir_mode (struct stat const *st)
          | (we_are_root ? 0 : MODE_WXUSR));
 }
 
+/* Trimmed version of extract_dir, to create a dir that is not in the
+   archive, including parents.  Should behave like extract_dir when
+   NO_OVERWRITE_DIR_OLD_FILES is set in order to avoid changing existing
+   paths if they are in the way.
+*/
+bool
+create_dir (char const *file_name)
+{
+  int status;
+  mode_t mode;
+  bool interdir_made = false;
+  /* exists only to avoid passing a const pointer to make_directories */
+  char *unconst_file_name;
+
+  mode = MODE_RWX & ~ newdir_umask;
+
+  for (;;)
+    {
+      struct fdbase f = fdbase (file_name);
+      status = f.fd == BADFD ? -1 : mkdirat (f.fd, f.base, mode);
+      if (status == 0)
+       {
+         return true;
+       }
+
+      if (errno == EEXIST)
+       {
+         struct stat st;
+         st.st_mode = 0;
+
+         if (is_directory_link (file_name, &st))
+           return true;
+
+         if ((st.st_mode != 0 && fstatat_flags == 0)
+             || deref_stat (file_name, &st) == 0)
+           {
+             if (S_ISDIR (st.st_mode))
+               {
+                 return true;
+               }
+           }
+         errno = EEXIST;
+         break;
+       }
+      else if (errno != ENOENT || interdir_made)
+       {
+         /* The error is not due to missing parent, or we already
+            tried to make the parent directories and succeeded, so
+            there must be another problem. No point in retrying. */
+         break;
+       }
+      unconst_file_name = xstrdup (file_name);
+      if (make_directories (unconst_file_name, &interdir_made) == 0)
+       {
+         free (unconst_file_name);
+         continue;
+       }
+      else
+       {
+         free (unconst_file_name);
+         break;
+       }
+    }
+  mkdir_error (file_name);
+  return false;
+}
+
 /* Extractor functions for various member types */
 
 static bool
@@ -1895,7 +1965,7 @@ extract_archive (void)
     {
       idx_t dir = chdir_current;
       apply_nonancestor_delayed_set_stat (current_stat_info.file_name, false);
-      chdir_do (dir);
+      chdir_do (dir, false);
     }
 
   /* Take a safety backup of a previously existing file.  */
@@ -1916,7 +1986,16 @@ extract_archive (void)
 
   tar_extractor_t fun = prepare_to_extract (current_stat_info.file_name,
                                            typeflag);
-  bool ok = fun && fun (current_stat_info.file_name, typeflag);
+  bool ok = false;
+  if (fun)
+    {
+      /* Create one_top_level dir if it does not exist.  */
+      one_top_level_prepare = true;
+      chdir_do (chdir_current, !!one_top_level_dir);
+      one_top_level_prepare = false;
+      if (fun (current_stat_info.file_name, typeflag))
+       ok = true;
+    }
   skip_member ();
   if (!ok && backup_option)
     undo_last_backup ();
@@ -1928,7 +2007,7 @@ apply_delayed_link (struct delayed_link *ds)
 {
   char const *valid_source = NULL;
 
-  chdir_do (ds->change_dir);
+  chdir_do (ds->change_dir, false);
 
   for (struct string_list *sources = ds->sources;
        sources;
index 1f44136451b0ea3296b08e8945bcd9e8d56905ff..b0da0cecd4f9de3a31a66cc1a940a48861fd2545 100644 (file)
@@ -136,16 +136,28 @@ enforce_one_top_level (char **pfile_name)
       idx_t pos = strlen (one_top_level_dir);
       if (strncmp (p, one_top_level_dir, pos) == 0)
        {
-         if (ISSLASH (p[pos]) || p[pos] == 0)
-           return;
+         /* Remove the one_top_level_dir prefix if it ends at
+            component boundary.  */
+         if (ISSLASH (p[pos]))
+           {
+             *pfile_name = xstrdup (p[pos+1] ? &p[pos+1] : ".");
+             free (file_name);
+             return;
+           }
+         else if (p[pos] == 0)
+           {
+             *pfile_name = xstrdup (".");
+             free (file_name);
+             return;
+           }
        }
-
-      *pfile_name = make_file_name (one_top_level_dir, file_name);
-      normalize_filename_x (*pfile_name);
+      /* If the prefix does not match, do nothing.  */
     }
   else
-    *pfile_name = xstrdup (one_top_level_dir);
-  free (file_name);
+    {
+      *pfile_name = xstrdup (".");
+      free (file_name);
+    }
 }
 
 bool
@@ -171,7 +183,14 @@ transform_stat_info (char typeflag, struct tar_stat_info *stat_info)
     }
 
   if (one_top_level_dir)
-    enforce_one_top_level (&stat_info->file_name);
+    {
+      enforce_one_top_level (&stat_info->file_name);
+      /* Hard links are interpreted relative to cwd, and --one-top-level
+        works by means of a hidden change of cwd to the requested directory.
+        Adjust hard link targets as well.  */
+      if (typeflag == LNKTYPE)
+       enforce_one_top_level (&stat_info->link_name);
+    }
   return true;
 }
 
@@ -1129,10 +1148,10 @@ static void
 simple_print_header (struct tar_stat_info *st, union block *blk,
                     off_t block_ordinal)
 {
-  char *temp_name
-    (show_transformed_names_option
-       ? (st->file_name ? st->file_name : st->orig_file_name)
-       : (st->orig_file_name ? st->orig_file_name : st->file_name));
+  char *temp_name =
+    (show_transformed_names_option
+     ? transform_top_level (st->file_name ? st->file_name : st->orig_file_name)
+     : xstrdup (st->orig_file_name ? st->orig_file_name : st->file_name));
 
   if (block_number_option)
     {
@@ -1331,6 +1350,7 @@ simple_print_header (struct tar_stat_info *st, union block *blk,
     }
   fflush (stdlis);
   xattrs_print (st);
+  free (temp_name);
 }
 
 
index e7b13719fec943a100fc5e39e692b3f08e04b6af..bdb458a34ef40ed6c0bafde3a412726b58c181e2 100644 (file)
@@ -24,6 +24,7 @@
 #include <xgetcwd.h>
 #include <unlinkdir.h>
 #include <utimens.h>
+#include <assert.h>
 
 #ifndef DOUBLE_SLASH_IS_DISTINCT_ROOT
 # define DOUBLE_SLASH_IS_DISTINCT_ROOT 0
@@ -969,6 +970,7 @@ struct wd
      to be used.  */
   int fd;
 
+  bool one_top_level;
   /* If ID.err is zero, the directory's identity;
      if positive, a failure indication with errno = ID.err;
      if negative, no attempt has been made yet to get the identity.  */
@@ -1000,7 +1002,17 @@ static idx_t wdcache_count;
 idx_t
 chdir_count (void)
 {
-  return wd_count - !!wd_count;
+  idx_t count = 0;
+  if (wd_count)
+    {
+      /* Do not count the initial CWD entry -> start at 1.  */
+      for (idx_t i = 1; i < wd_count; i++)
+       {
+         if (! wd[i].one_top_level)
+           count++;
+       }
+    }
+  return count;
 }
 
 /* Grow the WD table by at least one entry.  */
@@ -1015,15 +1027,27 @@ grow_wd (void)
       wd[wd_count].abspath = NULL;
       wd[wd_count].fd = AT_FDCWD;
       wd[wd_count].id.err = -1;
+      wd[wd_count].one_top_level = false;
       wd_count++;
+      if (one_top_level_dir)
+       {
+         wd[wd_count].name = one_top_level_dir;
+         wd[wd_count].abspath = NULL;
+         wd[wd_count].fd = 0;
+         wd[wd_count].id.err = -1;
+         wd[wd_count].one_top_level = true;
+         wd_count++;
+       }
     }
 }
 
 /* DIR is the operand of a -C option; add it to vector of chdir targets,
    and return the index of its location.  */
 idx_t
-chdir_arg (char const *dir)
+chdir_arg (char const *dir, bool one_top_level)
 {
+  if (one_top_level)
+    chdir_arg (dir, false);
   if (wd_count == wd_alloc)
     grow_wd ();
 
@@ -1033,13 +1057,22 @@ chdir_arg (char const *dir)
     {
       dir += dotslashlen (dir);
       if (! dir[dir[0] == '.'])
-       return wd_count - 1;
+       {
+         if (wd[wd_count - 1].one_top_level == one_top_level)
+           return wd_count - 1;
+         else
+           return wd_count - 2;
+       }
     }
 
+  if (one_top_level)
+    dir = one_top_level_dir;
+
   wd[wd_count].name = dir;
   wd[wd_count].abspath = NULL;
   wd[wd_count].fd = 0;
   wd[wd_count].id.err = -1;
+  wd[wd_count].one_top_level = one_top_level;
   return wd_count++;
 }
 
@@ -1058,21 +1091,63 @@ static int chdir_fd = AT_FDCWD;
    working directory; otherwise, I must be a value returned by
    chdir_arg.  */
 void
-chdir_do (idx_t i)
+chdir_do (idx_t i, bool create)
 {
-  if (chdir_current != i)
-    {
-      struct wd *curr = &wd[i];
-      int fd = curr->fd;
+  struct wd *curr = &wd[i];
+  int fd = curr->fd;
+
+  /* Nothing to create unless we are at the one_top_level dir that has
+     not been created yet.  */
+  create = create && curr->one_top_level && (fd == BADFD || fd == 0);
 
-      if (! fd)
+  if (chdir_current != i || create)
+    {
+      if (! fd || create)
        {
          if (! IS_ABSOLUTE_FILE_NAME (curr->name))
-           chdir_do (i - 1);
+           {
+             idx_t j = i - 1;
+             if (wd[j].one_top_level)
+               {
+                 j--;
+                 assert (! wd[j].one_top_level);
+               }
+             chdir_do (j, false);
+           }
          fd = openat (chdir_fd, curr->name,
                       open_searchdir_how.flags & ~O_NOFOLLOW);
          if (fd < 0)
-           open_fatal (curr->name);
+           {
+             if (create)
+               {
+                 struct open_how saved_open_searchdir_how = open_searchdir_how;
+                 /* Don't use O_BENEATH during creation of the
+                    directory. The one-top-level directory is
+                    allowed to be given as an absolute path.  */
+                 open_searchdir_how.resolve = 0;
+                 if (create_dir (curr->name))
+                   /* Directory created, retry */
+                   fd = openat (chdir_fd, curr->name,
+                                open_searchdir_how.flags & ~O_NOFOLLOW);
+                 open_searchdir_how = saved_open_searchdir_how;
+                 /* Either the creation or open failed */
+                 if (fd < 0)
+                   open_fatal (curr->name);
+               }
+             else if (errno == ENOENT && curr->one_top_level)
+               {
+                 /* We are requested to not create the directory now. Mark it
+                    as to be created later when called with create == true. */
+                 chdir_fd = curr->fd = BADFD;
+                 chdir_current = i;
+                 /* Do not add it to the cache */
+                 return;
+               }
+             else
+               {
+                 open_fatal (curr->name);
+               }
+           }
 
          curr->fd = fd;
 
@@ -1090,7 +1165,7 @@ chdir_do (idx_t i)
            }
        }
 
-      if (0 < fd)
+      if (0 < fd && /* no assumption about sign of BADFD */ fd != BADFD)
        {
          /* Move the i value to the front of the cache.  This is
             O(CHDIR_CACHE_SIZE), but the cache is small.  */
@@ -1194,6 +1269,14 @@ fdbase_opendir (char const *file_name, bool alternate)
 {
   char const *name = file_name;
 
+  if (chdir_fd == BADFD && ! IS_ABSOLUTE_FILE_NAME (file_name))
+    {
+      /* BADFD is a sentinel value meaning that the chdir directory
+        needs to be created lazily, therefore if we encounter it, the
+        directory does not exist yet. */
+      errno = ENOENT;
+      return (struct fdbase) { .fd = chdir_fd, .base = name };
+    }
   /* Skip past leading "./"s,
      but not past the last "./" if that ends the name.  */
   idx_t dslen = dotslashlen (name);
@@ -1298,6 +1381,29 @@ tar_dirname (void)
   return wd[chdir_current].name;
 }
 
+/* Return a newly allocated string that shows NAME from the user's
+   viewpoint, given that --one-top-level may be in effect.  */
+char *
+transform_top_level (const char *name)
+{
+  if (wd[chdir_current].one_top_level)
+    {
+      if (streq (name, "."))
+       {
+         /* nothing to append - .../. is the same as ... */
+         return xstrdup (wd[chdir_current].name);
+       }
+      else
+       {
+         namebuf_t nbuf = namebuf_create (wd[chdir_current].name);
+         namebuf_add_dir (nbuf, name);
+         return namebuf_finish (nbuf);
+       }
+    }
+  else
+    return xstrdup (name);
+}
+
 /* Return the absolute path that represents the working
    directory referenced by IDX.
 
@@ -1323,12 +1429,13 @@ tar_getcdpath (idx_t idx)
   if (!wd[idx].abspath)
     {
       idx_t save_cwdi = chdir_current, i = idx;
-      while (0 < i && !wd[i - 1].abspath)
+      while (0 < i && (!wd[i - 1].abspath || wd[i - 1].one_top_level))
        i--;
 
       for (; i <= idx; i++)
        {
-         chdir_do (i);
+         if (!wd[i].one_top_level)
+           chdir_do (i, false);
          if (i == 0)
            {
              if ((wd[i].abspath = xgetcwd ()) == NULL)
@@ -1341,13 +1448,18 @@ tar_getcdpath (idx_t idx)
            wd[i].abspath = xstrdup (wd[i].name);
          else
            {
-             namebuf_t nbuf = namebuf_create (wd[i - 1].abspath);
+             idx_t j = i - 1;
+             if (wd[j].one_top_level)
+               {
+                 j--;
+                 assert (! wd[j].one_top_level);
+               }
+             namebuf_t nbuf = namebuf_create (wd[j].abspath);
              namebuf_add_dir (nbuf, wd[i].name);
              wd[i].abspath = namebuf_finish (nbuf);
            }
        }
-
-      chdir_do (save_cwdi);
+      chdir_do (save_cwdi, false);
     }
 
   return wd[idx].abspath;
index 1b8131c5b439ddcf0f535ae6566c59ad34af77d4..2ea6d7ff3d312cc7bf6e8e5f655fe24b09f3f52f 100644 (file)
@@ -875,6 +875,7 @@ static idx_t name_buffer_length; /* allocated length of name_buffer */
 void
 name_init (void)
 {
+  chdir_do (chdir_arg (".", !!one_top_level_dir), false);
   name_list_adjust ();
 }
 \f
@@ -1118,7 +1119,8 @@ name_next_elt (bool change_dirs)
        case NELT_CHDIR:
          if (change_dirs)
            {
-             chdir_do (chdir_arg (xstrdup (ep->v.name)));
+             chdir_do (chdir_arg (xstrdup (ep->v.name), !!one_top_level_dir),
+                       false);
              name_list_advance ();
              break;
            }
@@ -1181,7 +1183,7 @@ name_gather (void)
       static idx_t change_dir;
 
       while ((ep = name_next_elt (false)) && ep->type == NELT_CHDIR)
-       change_dir = chdir_arg (xstrdup (ep->v.name));
+       change_dir = chdir_arg (xstrdup (ep->v.name), !!one_top_level_dir);
 
       if (ep)
        {
@@ -1210,7 +1212,7 @@ name_gather (void)
        {
          idx_t change_dir0 = change_dir;
          while ((ep = name_next_elt (false)) && ep->type == NELT_CHDIR)
-           change_dir = chdir_arg (xstrdup (ep->v.name));
+           change_dir = chdir_arg (xstrdup (ep->v.name), !!one_top_level_dir);
 
          if (ep)
            addname (ep->v.name, change_dir, true, NULL);
@@ -1339,7 +1341,7 @@ name_match (const char *file_name)
 
       if (cursor->name[0] == 0)
        {
-         chdir_do (cursor->change_dir);
+         chdir_do (cursor->change_dir, false);
          namelist = NULL;
          nametail = NULL;
          return true;
@@ -1383,7 +1385,7 @@ name_match (const char *file_name)
            return false;
 
          /* We got a match. */
-         chdir_do (found->change_dir);
+         chdir_do (found->change_dir, false);
          return true;
        }
 
@@ -1785,7 +1787,7 @@ collect_and_sort_names (void)
        /* NOTE: EXCLUDE_ANCHORED is not relevant here */
        /* FIXME: just skip regexps for now */
        continue;
-      chdir_do (name->change_dir);
+      chdir_do (name->change_dir, false);
 
       if (name->name[0] == 0)
        continue;
@@ -1931,7 +1933,7 @@ name_from_list (void)
     {
       if (!gnu_list_name->is_wildcard)
        gnu_list_name->found_count++;
-      chdir_do (gnu_list_name->change_dir);
+      chdir_do (gnu_list_name->change_dir, false);
       return gnu_list_name;
     }
   return NULL;
index a920d9c293a84bf8aef994fa6394dd4ccf6ab91b..a7845a050c896bb832f8fc8ebb80f1f64dc5185e 100644 (file)
--- a/src/tar.c
+++ b/src/tar.c
@@ -2683,10 +2683,7 @@ decode_options (int argc, char **argv)
                        "please set it explicitly with --one-top-level=DIR"));
        }
 
-      if (one_top_level_dir
-         && ! (*one_top_level_dir
-               && IS_RELATIVE_FILE_NAME (one_top_level_dir)))
-       paxusage(_("--one-top-level=DIR must use a relative file name"));
+      normalize_filename_x (one_top_level_dir);
     }
 
   /* If ready to unlink hierarchies, so we are for simpler files.  */
index 58187415c4442728134cbff982407633430b29f3..d809ad44da3a579510481a2fa75c7811b2a5bf4d 100644 (file)
@@ -92,7 +92,7 @@ flush_deferred_unlinks (bool force)
       if (force
          || p->records_written < records_written)
        {
-         chdir_do (p->dir_idx);
+         chdir_do (p->dir_idx, false);
          if (p->is_dir)
            {
              const char *fname;
@@ -163,11 +163,11 @@ flush_deferred_unlinks (bool force)
          struct deferred_unlink *next = p->next;
          const char *fname;
 
-         chdir_do (p->dir_idx);
+         chdir_do (p->dir_idx, false);
          if (p->dir_idx && is_cwd (p))
            {
              fname = tar_dirname ();
-             chdir_do (p->dir_idx - 1);
+             chdir_do (p->dir_idx - 1, false);
            }
          else
            fname = p->file_name;
@@ -184,7 +184,7 @@ flush_deferred_unlinks (bool force)
       dunlink_head = dunlink_tail = NULL;
     }
 
-  chdir_do (saved_chdir);
+  chdir_do (saved_chdir, false);
 }
 
 void
index 872e701fc39782ae53e91c34ace0fe5f0615876b..fa4883b0018cbb0427a0291faab53a199386febe 100644 (file)
@@ -133,7 +133,7 @@ update_archive (void)
              {
                struct stat s;
 
-               chdir_do (name->change_dir);
+               chdir_do (name->change_dir, false);
                if (deref_stat (current_stat_info.file_name, &s) == 0)
                  {
                    if (S_ISDIR (s.st_mode))