]> git.ipfire.org Git - thirdparty/tar.git/commitdiff
Skip file or archive member if its transformed name is empty.
authorSergey Poznyakoff <gray@gnu.org>
Tue, 6 May 2025 12:23:03 +0000 (15:23 +0300)
committerSergey Poznyakoff <gray@gnu.org>
Tue, 6 May 2025 12:32:17 +0000 (15:32 +0300)
* NEWS: Document changes.
* doc/tar.texi: Document changes.
* src/common.h (transform_stat_info): Change return value.
(transform_name_fp): Change signature.
(WARN_EMPTY_TRANSFORM): New constant.
* src/create.c: Check return from transform_name.  Skip file, if it
is false.
* src/list.c (transform_stat_info): Return bool.
(read_and): Skip member if transform_stat_info returns false.
* src/transform.c (_transform_name_to_obstack): Change return type.
Always allocate result in obstack.
(transform_name_fp): Change arguments.  Return true on
success (transformed string not empty).  Otherwise return false and
don't change the source string.
* src/warning.c: New warning class: empty-transform.
* tests/extrac17.at: Use --warning=empty-transform.

NEWS
doc/tar.texi
src/common.h
src/create.c
src/list.c
src/transform.c
src/warning.c
tests/extrac17.at

diff --git a/NEWS b/NEWS
index dd1e2ae9123b77cdcaa89da401d42d819ec45f78..39986f23d5f09d39c3e303135492f50c491b5262 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,4 @@
-GNU tar NEWS - User visible changes. 2024-11-02
+GNU tar NEWS - User visible changes. 2025-05-06
 Please send GNU tar bug reports to <bug-tar@gnu.org>
 \f
 version TBD
@@ -24,6 +24,15 @@ as argument to --mtime option (see GNU tar manual, chapter 4
 Defines output format for the COMMAND set by the above option.  If
 used, command output will be parsed using strptime(3).
 
+* Skip file or archive member if transformed name is empty
+
+If applying file name transformations (--transform and
+--strip-component options) to a file or member name results in an
+empty string that file or member is skipped and a warning is printed.
+
+The warning can be suppressed using the --warning=empty-transform
+option.
+
 * Bug fixes
 
 ** Fixed O(n^2) time complexity bug for large numbers of directories when
index 8fb8c542f544edadae41c5bae5458afcbceb25a2..64d08cdb596aaccf4fd5f9db10fd4d46d9836f27 100644 (file)
@@ -4639,7 +4639,7 @@ The subsections below discuss allowed values for @var{keyword} along with the
 warning messages they control.
 
 @menu
-* General Warnings::  Keywords applicable for @command{tar --create}.
+* General Warnings::  Keywords controlling @command{tar} operation.
 * Archive Creation Warnings:: Keywords applicable for @command{tar --create}.
 * Archive Extraction Warnings:: Keywords applicable for @command{tar --extract}.
 * Incremental Extraction Warnings:: Keywords controlling incremental extraction.
@@ -4670,6 +4670,11 @@ suppressed if @option{--ignore-zeros} is in effect (@pxref{Ignore
 Zeros}).
 @end defvr
 
+@defvr {warning} empty-transform
+@cindex @samp{transforms to empty name}, warning message.
+@samp{transforms to empty name}.  @xref{transform}.
+@end defvr
+
 @defvr {warning} missing-zero-blocks
 @cindex @samp{Terminating zero blocks missing}, warning message.
 @samp{Terminating zero blocks missing at %s}.  This warning is
@@ -8906,7 +8911,9 @@ $ @kbd{tar -xf usr.tar --strip=2 usr/include/stdlib.h}
 
 The option @option{--strip=2} instructs @command{tar} to strip the
 two leading components (@file{usr/} and @file{include/}) off the file
-name.
+name, before extracting it.  Notice, that archive members to extract are
+searched before that modification, hence the file name is specified in
+full.
 
 If you add the @option{--verbose} (@option{-v}) option to the invocation
 above, you will note that the verbose listing still contains the
@@ -9022,7 +9029,6 @@ follows the GNU @command{sed} implementation in this regard, so
 the interaction is defined to be: ignore matches before the
 @var{number}th, and then match and replace all matches from the
 @var{number}th on.
-
 @end table
 
 In addition, several @dfn{transformation scope} flags are supported,
@@ -9162,6 +9168,42 @@ $ @kbd{tar -cf arch.tar \
                --transform='s,/usr/var,/var/;s,/usr/local,/usr/,'}
 @end smallexample
 
+Applying transformations to some file names may produce empty
+strings.  This may indicate errors in regular expressions, but it may
+as well happen during normal operation, for instance, when using
+@option{--strip-components} option with a value which is greater then
+or equal to the number of directory components in the topmost
+directory stored in the archive.  Consider the following example.  Let
+the archive @file{usr.tar} contain:
+
+@example
+@group
+$ @kbd{tar tf usr.tar}
+usr/
+usr/local/
+...
+@end group
+@end example
+
+Extracting from this archive with the @option{--strip=1} option will
+transform the name of its first entry (@samp{usr/}) to an empty
+string.  When this happens, @GNUTAR{} prints a warning and skips this
+member:
+
+@example
+@group
+$ @kbd{tar --strip=1 -xf usr.tar}
+tar: usr: transforms to empty name
+@end group
+@end example
+
+If an empty name results from transformations applied when creating or
+updating the archive, the warning is output and the file is not
+archived.
+
+If this is intended, you can suppress the warning using the
+@option{--warning=no-empty-transform} option (@pxref{warnings}).
+
 @node after
 @section Operating Only on New Files
 
index 7fba6325c60b2a263a9402a25f31d32f5889ee04..5d4f83bf8e776f23bc11a33bfd1956525b898de7 100644 (file)
@@ -611,7 +611,7 @@ extern idx_t recent_long_link_blocks;
 
 void decode_header (union block *header, struct tar_stat_info *stat_info,
                    enum archive_format *format_pointer, bool do_user_group);
-void transform_stat_info (char typeflag, struct tar_stat_info *stat_info);
+bool transform_stat_info (char typeflag, struct tar_stat_info *stat_info);
 char const *tartime (struct timespec t, bool full_time);
 
 #define OFF_FROM_HEADER(where) off_from_header (where, sizeof (where))
@@ -972,7 +972,7 @@ enum
 void set_transform_expr (const char *expr);
 bool transform_name (char **pinput, int type);
 bool transform_name_fp (char **pinput, int type,
-                       char *(*fun) (char *, int), int dat);
+                       char const *(*fun) (char const *, int));
 bool transform_program_p (void);
 
 /* Module suffix.c */
@@ -1014,7 +1014,8 @@ enum
     WARN_XATTR_WRITE           = 1 << 21,
     WARN_RECORD_SIZE           = 1 << 22,
     WARN_FAILED_READ           = 1 << 23,
-    WARN_MISSING_ZERO_BLOCKS   = 1 << 24
+    WARN_MISSING_ZERO_BLOCKS   = 1 << 24,
+    WARN_EMPTY_TRANSFORM        = 1 << 25
   };
 /* These warnings are enabled by default in verbose mode: */
 enum
index 8564e992a0101d06d80b1454ff0665ff611f1018..8458f2154344a9ed33d818df66476aa381a1dec9 100644 (file)
@@ -1478,7 +1478,11 @@ file_count_links (struct tar_stat_info *st)
 
       assign_string (&linkname, safer_name_suffix (st->orig_file_name, true,
                                                   absolute_names_option));
-      transform_name (&linkname, XFORM_LINK);
+      if (!transform_name (&linkname, XFORM_LINK))
+       {
+         free (linkname);
+         return;
+       }
 
       lp = xmalloc (FLEXNSIZEOF (struct link, name, strlen (linkname) + 1));
       lp->ino = st->stat.st_ino;
@@ -1612,7 +1616,8 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
   assign_string (&st->file_name,
                  safer_name_suffix (p, false, absolute_names_option));
 
-  transform_name (&st->file_name, XFORM_REGFILE);
+  if (!transform_name (&st->file_name, XFORM_REGFILE))
+    return NULL;
 
   if (parentfd < 0 && ! top_level)
     {
@@ -1797,7 +1802,8 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
 
          if (!ok)
            {
-             warnopt (WARN_FILE_CHANGED, 0, _("%s: file changed as we read it"),
+             warnopt (WARN_FILE_CHANGED, 0,
+                      _("%s: file changed as we read it"),
                       quotearg_colon (p));
              if (! ignore_failed_read_option)
                set_exit_status (TAREXIT_DIFFERS);
@@ -1824,7 +1830,8 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
          file_removed_diag (p, top_level, readlink_diag);
          return allocated;
        }
-      transform_name (&st->link_name, XFORM_SYMLINK);
+      if (!transform_name (&st->link_name, XFORM_SYMLINK))
+       return allocated;
       if (NAME_FIELD_SIZE - (archive_format == OLDGNU_FORMAT)
          < strlen (st->link_name))
        write_long_link (st);
index 68ee35764e3646f2bcee491250e233c8e5c7e758..ed90190910d1312eff50227b5d6a53b705ba2381 100644 (file)
@@ -75,8 +75,8 @@ static char const base64_map[UCHAR_MAX + 1] = {
   ['+'] = 62 + 1, ['/'] = 63 + 1,
 };
 
-static char *
-decode_xform (char *file_name, int type)
+static char const *
+decode_xform (char const *file_name, int type)
 {
   switch (type)
     {
@@ -111,7 +111,7 @@ decode_xform (char *file_name, int type)
 static bool
 transform_member_name (char **pinput, int type)
 {
-  return transform_name_fp (pinput, type, decode_xform, type);
+  return transform_name_fp (pinput, type, decode_xform);
 }
 
 static void
@@ -140,26 +140,31 @@ enforce_one_top_level (char **pfile_name)
   free (file_name);
 }
 
-void
+bool
 transform_stat_info (char typeflag, struct tar_stat_info *stat_info)
 {
   if (typeflag == GNUTYPE_VOLHDR)
     /* Name transformations don't apply to volume headers. */
-    return;
+    return true;
 
-  transform_member_name (&stat_info->file_name, XFORM_REGFILE);
+  if (!transform_member_name (&stat_info->file_name, XFORM_REGFILE))
+    return false;
   switch (typeflag)
     {
     case SYMTYPE:
-      transform_member_name (&stat_info->link_name, XFORM_SYMLINK);
+      if (!transform_member_name (&stat_info->link_name, XFORM_SYMLINK))
+       return false;
       break;
 
     case LNKTYPE:
-      transform_member_name (&stat_info->link_name, XFORM_LINK);
+      if (!transform_member_name (&stat_info->link_name, XFORM_LINK))
+       return false;
+      break;
     }
 
   if (one_top_level_option)
     enforce_one_top_level (&stat_info->file_name);
+  return true;
 }
 
 /* Main loop for reading an archive.  */
@@ -223,9 +228,11 @@ read_and (void (*do_something) (void))
                }
            }
 
-         transform_stat_info (current_header->header.typeflag,
-                              &current_stat_info);
-         (*do_something) ();
+         if (transform_stat_info (current_header->header.typeflag,
+                                  &current_stat_info))
+           (*do_something) ();
+         else
+           skip_member ();
          continue;
 
        case HEADER_ZERO_BLOCK:
index 5e22d630512ed8add78339f78ac6ee0e53983bcc..2e6d718710751f9dc09041b1c7f157b74e9639f9 100644 (file)
@@ -17,6 +17,7 @@
 #include <system.h>
 #include <regex.h>
 #include <mcel.h>
+#include <quotearg.h>
 #include "common.h"
 
 enum transform_type
@@ -561,11 +562,11 @@ _single_transform_name_to_obstack (struct transform *tf, char *input)
   free (rmp);
 }
 
-static bool
+static void
 _transform_name_to_obstack (int flags, char *input, char **output)
 {
   struct transform *tf;
-  bool alloced = false;
+  bool ok = false;
 
   if (!stk_init)
     {
@@ -579,38 +580,53 @@ _transform_name_to_obstack (int flags, char *input, char **output)
        {
          _single_transform_name_to_obstack (tf, input);
          input = obstack_finish (&stk);
-         alloced = true;
+         ok = true;
        }
     }
+  if (!ok)
+    {
+      obstack_grow0 (&stk, input, strlen (input));
+      input = obstack_finish (&stk);
+    }
   *output = input;
-  return alloced;
 }
 
+/* Transform name *PINPUT of a file or archive member of type TYPE
+   (a single XFORM_* bit).  If FUN is not NULL, call this function
+   to further transform the result.  Arguments to FUN are the transformed
+   name and type, it's return value is the new transformed name.
+
+   If transformation results in a non-empty string, store the result in
+   *PINPUT and return true.  Otherwise, if it results in an empty string,
+   issue a warning, return false and don't modify PINPUT.
+ */
 bool
-transform_name_fp (char **pinput, int flags,
-                  char *(*fun) (char *, int), int dat)
+transform_name_fp (char **pinput, int type,
+                  char const *(*fun) (char const *, int))
 {
-    char *str;
-    bool ret = _transform_name_to_obstack (flags, *pinput, &str);
-    if (ret)
-      {
-       assign_string (pinput, fun ? fun (str, dat) : str);
-       obstack_free (&stk, str);
-      }
-    else if (fun)
-      {
-       *pinput = NULL;
-       assign_string (pinput, fun (str, dat));
-       free (str);
-       ret = true;
-      }
-    return ret;
+  char *str;
+  char const *result;
+
+  _transform_name_to_obstack (type, *pinput, &str);
+  result = (str[0] != 0 && fun) ? fun (str, type) : str;
+
+  if (result[0] == 0)
+    {
+      warnopt (WARN_EMPTY_TRANSFORM, 0,
+              _("%s: transforms to empty name"), quotearg_colon (*pinput));
+      obstack_free (&stk, str);
+      return false;
+    }
+
+  assign_string (pinput, result);
+  obstack_free (&stk, str);
+  return true;
 }
 
 bool
 transform_name (char **pinput, int type)
 {
-  return transform_name_fp (pinput, type, NULL, 0);
+  return transform_name_fp (pinput, type, NULL);
 }
 
 bool
index 44e1d1e9f21b65e1b246c8a9bfb885de507da804..161ac877603a72186e63887d10767ca34b6de9cd 100644 (file)
@@ -50,6 +50,7 @@ static char const *const warning_args[] = {
   "failed-read",
   "missing-zero-blocks",
   "verbose",
+  "empty-transform",
   NULL
 };
 
@@ -81,6 +82,7 @@ static int warning_types[] = {
   WARN_FAILED_READ,
   WARN_MISSING_ZERO_BLOCKS,
   WARN_VERBOSE_WARNINGS,
+  WARN_EMPTY_TRANSFORM
 };
 
 ARGMATCH_VERIFY (warning_args, warning_types);
index 5c08ed3ae944cfae2f7b1eb8f5be3a4817e45044..e38eac7c5c1e8e2bc5fd63d10310c0859072c533 100644 (file)
@@ -38,7 +38,9 @@ genfile --file dir/subdir2/file2
 
 tar cf dir.tar dir
 
-tar -x -v -f dir.tar -C out --strip-components=2 --warning=no-timestamp \
+tar -x -v -f dir.tar -C out --strip-components=2 \
+  --warning=no-empty-transform \
+  --warning=no-timestamp \
   dir/subdir1/
 ],
 [0],