From 941f62b24db232c58a41a0a621e08451f7f97ed8 Mon Sep 17 00:00:00 2001 From: Paul Eggert Date: Wed, 22 Jul 2026 16:48:45 -0700 Subject: [PATCH] --one-top-level clarifications Bring back documentation for --one-top-level absolute file names, and clarify its behavior. Allow --one-top-level only with -x, -d, -t. * src/misc.c (chdir_do): Do not pass -1 to openat. Simplify now that open_searchdir_how no longer has RESOLVE_BENEATH when one_top_level_dir is absolute. Fix errno confusion in messages. * src/tar.c (decode_options): Allow --one-top-level only with -x, -d, and -t. Do not use RESOLVE_BENEATH if --one-top-level=/foo is used; this simplifies chdir_do. --- NEWS | 6 ++--- doc/tar.texi | 17 ++++++++------ src/misc.c | 64 ++++++++++++++++++++++++++-------------------------- src/tar.c | 5 ++++ 4 files changed, 50 insertions(+), 42 deletions(-) diff --git a/NEWS b/NEWS index 8e18040d..a7193430 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,4 @@ -GNU tar NEWS - User visible changes. 2026-06-20 +GNU tar NEWS - User visible changes. 2026-07-22 Please send GNU tar bug reports to version 1.35.90 (git) @@ -35,8 +35,8 @@ empty string that file or member is skipped and a warning is printed. The warning can be suppressed using the --warning=empty-transform option. -** --one-top-level=DIR now requires DIR to be relative. - Previously this restriction was alluded to in the manual but not enforced. +** The --one-top-level option now requires either -x or -d mode. + Previously the behavior was unspecified in other operation modes. * Bug fixes diff --git a/doc/tar.texi b/doc/tar.texi index f5b09cab..4441cc7b 100644 --- a/doc/tar.texi +++ b/doc/tar.texi @@ -3287,14 +3287,14 @@ directory. @opsummary{one-top-level} @item --one-top-level[=@var{dir}] -Tells @command{tar} to create a new directory beneath the extraction directory -(or the one passed to @option{-C}) and use it to prevent @command{tar} -from modifying files outside that directory. -If @var{dir} is present, it must be a relative file name. -If it is absent, the name of the new directory +Tells @command{tar} to use a directory beneath the extraction directory +(or the one passed to @option{-C}) to prevent @command{tar} +from accessing files outside that directory. +If @var{dir} is absent, the name of the directory is the base name of the archive minus any recognized archive suffix. -If multiple @option{-C} options are present, -each has its own subdirectory with the same name. +If @var{dir} is an absolute file name, that is the only such directory; +otherwise, the working directory and the directories specified by +any @option{-C} options each have the named subdirectory. Any member names that do not begin with that directory name (after transformations from @option{--transform} and @@ -3302,6 +3302,9 @@ transformations from @option{--transform} and file name suffixes are @samp{.tar}, and any compression suffixes recognizable by @xref{--auto-compress}. +This option can be used only when extracting, comparing, or reading +from the archive. + @opsummary{overwrite} @item --overwrite diff --git a/src/misc.c b/src/misc.c index c5b73e31..30a04537 100644 --- a/src/misc.c +++ b/src/misc.c @@ -1079,7 +1079,8 @@ idx_t chdir_current; /* Value suitable for use as the first argument to openat, and in similar locations for fstatat, etc. This is an open file - descriptor, or AT_FDCWD if the working directory is current. It is + descriptor, or AT_FDCWD if the working directory is current, + or BADFD if the directory has not been opened yet. It is valid until the next invocation of chdir_do. */ static int chdir_fd = AT_FDCWD; @@ -1095,7 +1096,7 @@ chdir_do (idx_t i, bool create) int fd = curr->fd; bool one_top_level = !!one_top_level_dir; - /* Nothing to create unless we are at the one_top_level dir that has + /* Nothing to create unless we are at a one_top_level dir that has not been created yet. */ create &= i & one_top_level & (fd == BADFD || fd == 0); @@ -1103,42 +1104,41 @@ chdir_do (idx_t i, bool create) { if (! fd || create) { - if (! IS_ABSOLUTE_FILE_NAME (curr->name)) - chdir_do ((i - 1) & ~+one_top_level, false); + int dfd; + if (IS_ABSOLUTE_FILE_NAME (curr->name)) + dfd = AT_FDCWD; + else + { + chdir_do ((i - 1) & ~+one_top_level, false); + dfd = chdir_fd; + } - fd = openat (chdir_fd, curr->name, + fd = openat (dfd, curr->name, open_searchdir_how.flags & ~O_NOFOLLOW); if (fd < 0) { - if (create) + if (errno == ENOENT) { - 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 likely exists now; 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 ((i & one_top_level) && errno == ENOENT) - { - /* 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); + if (create) + { + if (!create_dir (curr->name)) + fatal_exit (); + /* Directory likely exists now; retry. */ + fd = openat (dfd, curr->name, + open_searchdir_how.flags & ~O_NOFOLLOW); + } + else if (i & one_top_level) + { + /* Mark it to be created later if called with CREATE. */ + chdir_fd = curr->fd = BADFD; + chdir_current = i; + /* Do not add it to the cache. */ + return; + } } + + if (fd < 0) + open_fatal (curr->name); } curr->fd = fd; diff --git a/src/tar.c b/src/tar.c index 7a95147d..6690fff1 100644 --- a/src/tar.c +++ b/src/tar.c @@ -2656,6 +2656,10 @@ decode_options (int argc, char **argv) if (optloc_lookup (OC_ONE_TOP_LEVEL)) { + if (!is_subcommand_class (SUBCL_READ)) + option_conflict_error ("--one-top-level", + subcommand_string (subcommand_option)); + if (absolute_names_option) { struct option_locus *one_top_level_loc = @@ -2712,6 +2716,7 @@ decode_options (int argc, char **argv) open_searchdir_how.flags = (search_flags | nofollow_flag | O_BINARY | O_CLOEXEC | O_DIRECTORY); if (!absolute_names_option && !dereference_option + && ! (one_top_level_dir && IS_ABSOLUTE_FILE_NAME (one_top_level_dir)) && (subcommand_option == EXTRACT_SUBCOMMAND || subcommand_option == DIFF_SUBCOMMAND)) open_searchdir_how.resolve = RESOLVE_BENEATH;