Browse Source

New options: --owner-map and --group-map.

* NEWS: Update.
* doc/tar.1: Document --owner-map and --group-map
* doc/tar.texi: Likewise.

* src/map.c: New file.
* src/Makefile.am: Add map.c
* src/common.h (owner_map_read, owner_map_translate)
(group_map_read, group_map_translate): New protos.
* src/create.c (start_header): Use owner_map_translate
and group_map_translate to optionally translate user/group names/ids.
* src/tar.c: New options --owner-map and --group-map.

* tests/map.at: New file.
* tests/Makefile.am: Add map.at
* tests/testsuite.at: Include map.at.
Sergey Poznyakoff 9 years ago
parent
commit
1a615a41f5
11 changed files with 550 additions and 16 deletions
  1. 19 1
      NEWS
  2. 55 5
      doc/tar.1
  3. 89 1
      doc/tar.texi
  4. 1 0
      src/Makefile.am
  5. 7 0
      src/common.h
  6. 9 9
      src/create.c
  7. 283 0
      src/map.c
  8. 14 0
      src/tar.c
  9. 1 0
      tests/Makefile.am
  10. 71 0
      tests/map.at
  11. 1 0
      tests/testsuite.at

+ 19 - 1
NEWS

@@ -1,4 +1,4 @@
-GNU tar NEWS - User visible changes. 2015-08-03
+GNU tar NEWS - User visible changes. 2015-11-02
 Please send GNU tar bug reports to <[email protected]>
 
 
@@ -22,6 +22,24 @@ This option affects all --files-from options that occur after it in
 the command line.  Its effect is reverted by the
 --no-verbatim-files-from option.
 
+* New options: --owner-map=FILE and --group-map=FILE
+
+These two options provide fine-grained control over what user/group
+names (or IDs) should be mapped when adding files to archive.
+
+For both options, FILE is a plain text file with user or group
+mappings.  Empty lines are ignored.  Comments are introduced with
+# sign (unless quoted) and extend to the end of the corresponding
+line.  Each non-empty line defines translation for a single UID (GID).
+It must consist of two fields, delimited by any amount of whitespace:
+
+     OLDNAME NEWNAME[:NEWID]
+
+OLDNAME is either a valid user (group) name or a ID prefixed with +.  Unless
+NEWID is supplied, NEWNAME must also be either a valid name or a
++ID.  Otherwise, both NEWNAME and NEWID need not be listed in the
+system user database.
+
 * --null option reads file names verbatim
 
 The --null option implies --verbatim-files-from.  I.e. each line 

+ 55 - 5
doc/tar.1

@@ -13,7 +13,7 @@
 .\"
 .\" You should have received a copy of the GNU General Public License
 .\" along with this program.  If not, see <http://www.gnu.org/licenses/>.
-.TH TAR 1 "August 24, 2015" "TAR" "GNU TAR Manual"
+.TH TAR 1 "November 2, 2015" "TAR" "GNU TAR Manual"
 .SH NAME
 tar \- an archiving utility
 .SH SYNOPSIS
@@ -467,8 +467,33 @@ Delay setting modification times and permissions of extracted
 directories until the end of extraction.  Use this option when
 extracting from an archive which has unusual member ordering.
 .TP
-\fB\-\-group\fR=\fINAME\fR
-Force \fINAME\fR as group for added files.
+\fB\-\-group\fR=\fINAME\fR[:\fIGID\fR]
+Force \fINAME\fR as group for added files.  If \fIGID\fR is not
+supplied, \fINAME\fR can be either a user name or numeric GID.  In
+this case the missing part (GID or name) will be inferred from the
+current host's group database.
+
+When used with \fB\-\-group\-map\fR=\fIFILE\fR, affects only those
+files whose owner group is not listed in \fIFILE\fR.
+.TP
+\fB\-\-group\-map\fR=\fIFILE\fR
+Read group translation map from \fIFILE\fR.  Empty lines are ignored.
+Comments are introduced with \fB#\fR sign and extend to the end of line.
+Each non-empty line in \fIFILE\fR defines translation for a single
+group.  It must consist of two fields, delimited by any amount of whitespace:
+
+.EX
+\fIOLDGRP\fR \fINEWGRP\fR[\fB:\fINEWGID\fR]
+.EE
+
+\fIOLDGRP\fR is either a valid group name or a GID prefixed with
+\fB+\fR.  Unless \fINEWGID\fR is supplied, \fINEWGRP\fR must also be
+either a valid group name or a \fB+\fIGID\fR.  Otherwise, both
+\fINEWGRP\fR and \fINEWGID\fR need not be listed in the system group
+database.
+
+As a result, each input file with owner group \fIOLDGRP\fR will be
+stored in archive with owner group \fINEWGRP\fR and GID \fINEWGID\fR.
 .TP
 \fB\-\-mode\fR=\fICHANGES\fR
 Force symbolic mode \fICHANGES\fR for added files.
@@ -494,8 +519,33 @@ Apply the user's umask when extracting permissions from the archive
 \fB\-\-numeric\-owner\fR
 Always use numbers for user/group names.
 .TP
-\fB\-\-owner\fR=\fINAME\fR
-Force \fINAME\fR as owner for added files.
+\fB\-\-owner\fR=\fINAME\fR[:\fIUID\fR]
+Force \fINAME\fR as owner for added files.  If \fIUID\fR is not
+supplied, \fINAME\fR can be either a user name or numeric UID.  In
+this case the missing part (UID or name) will be inferred from the
+current host's user database.
+
+When used with \fB\-\-owner\-map\fR=\fIFILE\fR, affects only those
+files whose owner is not listed in \fIFILE\fR.
+.TP
+\fB\-\-owner\-map\fR=\fIFILE\fR
+Read owner translation map from \fIFILE\fR.  Empty lines are ignored.
+Comments are introduced with \fB#\fR sign and extend to the end of line.
+Each non-empty line in \fIFILE\fR defines translation for a single
+UID.  It must consist of two fields, delimited by any amount of whitespace:
+
+.EX
+\fIOLDUSR\fR \fINEWUSR\fR[\fB:\fINEWUID\fR]
+.EE
+
+\fIOLDUSR\fR is either a valid user name or a UID prefixed with
+\fB+\fR.  Unless \fINEWUID\fR is supplied, \fINEWUSR\fR must also be
+either a valid user name or a \fB+\fIUID\fR.  Otherwise, both
+\fINEWUSR\fR and \fINEWUID\fR need not be listed in the system user
+database.
+
+As a result, each input file owned by \fIOLDUSR\fR will be
+stored in archive with owner name \fINEWUSR\fR and UID \fINEWUID\fR.
 .TP
 \fB\-p\fR, \fB\-\-preserve\-permissions\fR, \fB\-\-same\-permissions\fR
 extract information about file permissions (default for superuser)

+ 89 - 1
doc/tar.texi

@@ -2742,7 +2742,19 @@ rather than the group from the source file.  @var{group} can specify a
 symbolic name, or a numeric @acronym{ID}, or both as
 @var{name}:@var{id}.  @xref{override}.
 
-Also see the comments for the @option{--owner=@var{user}} option.
+Also see the @option{--group-map} option and comments for the
+@option{--owner=@var{user}} option.
+
+@opsummary{group-map}
+@item --group-map=@var{file}
+
+Read owner group translation map from @var{file}.  This option allows to
+translate only certain group names and/or UIDs.  @xref{override}, for a
+detailed description.  When used together with @option{--group}
+option, the latter affects only those files whose owner group is not listed
+in the @var{file}.
+
+This option does not affect extraction from archives.
 
 @opsummary{gzip}
 @opsummary{gunzip}
@@ -3163,6 +3175,18 @@ file.  @var{user} can specify a symbolic name, or a numeric
 @acronym{ID}, or both as @var{name}:@var{id}.
 @xref{override}.
 
+This option does not affect extraction from archives.  See also
+@option{--owner-map}, below.
+
+@opsummary{owner-map}
+@item --owner-map=@var{file}
+
+Read owner translation map from @var{file}.  This option allows to
+translate only certain owner names or UIDs.  @xref{override}, for a
+detailed description.  When used together with @option{--owner}
+option, the latter affects only those files whose owner is not listed
+in the @var{file}.
+
 This option does not affect extraction from archives.
 
 @opsummary{pax-option}
@@ -5259,6 +5283,70 @@ the argument @var{group} can be an existing group symbolic name, or a
 decimal numeric group @acronym{ID}, or @var{name}:@var{id}.
 @end table
 
+The @option{--owner} and @option{--group} options affect all files
+added to the archive.  @GNUTAR{} provides also two options that allow
+for more detailed control over owner translation:
+
+@table @option
+@item --owner-map=@var{file}
+Read UID translation map from @var{file}.
+
+When reading, empty lines are ignored.  The @samp{#} sign, unless
+quoted, introduces a comment, which extends to the end of the line.
+Each nonempty line defines mapping for a single UID.  It must consist
+of two fields separated by any amount of whitespace.  The first field
+defines original username and UID.  It can be a valid user name or
+a valid UID prefixed with a plus sign.  In both cases the
+corresponding UID or user name is inferred from the current host's
+user database.
+
+The second field defines the UID and username to map the original one
+to.  Its format can be the same as described above.  Otherwise, it can
+have the form @var{newname}:@var{newuid}, in which case neither
+@var{newname} nor @var{newuid} are required to be valid as per the
+user database.
+
+For example, consider the following file:
+
+@example
++10     bin
+smith   root:0
+@end example
+
+@noindent
+Given this file, each input file that is owner by UID 10 will be
+stored in archive with owner name @samp{bin} and owner UID
+corresponding to @samp{bin}.  Each file owned by user @samp{smith}
+will be stored with owner name @samp{root} and owner ID 0.  Other
+files will remain unchanged.
+
+When used together with @option{--owner-map}, the @option{--owner}
+option affects only files whose owner is not listed in the map file.
+
+@item --group-map=@var{file}
+Read GID translation map from @var{file}.
+
+The format of @var{file} is the same as for @option{--owner-map}
+option:
+
+Each nonempty line defines mapping for a single GID.  It must consist
+of two fields separated by any amount of whitespace.  The first field
+defines original group name and GID.  It can be a valid group name or
+a valid GID prefixed with a plus sign.  In both cases the
+corresponding GID or user name is inferred from the current host's
+group database.
+
+The second field defines the GID and group name to map the original one
+to.  Its format can be the same as described above.  Otherwise, it can
+have the form @var{newname}:@var{newgid}, in which case neither
+@var{newname} nor @var{newgid} are required to be valid as per the
+group database.
+
+When used together with @option{--group-map}, the @option{--group}
+option affects only files whose owner group is not rewritten using the
+map file.
+@end table
+
 @node Ignore Failed Read
 @subsection Ignore Fail Read
 

+ 1 - 0
src/Makefile.am

@@ -33,6 +33,7 @@ tar_SOURCES = \
  xheader.c\
  incremen.c\
  list.c\
+ map.c\
  misc.c\
  names.c\
  sparse.c\

+ 7 - 0
src/common.h

@@ -952,4 +952,11 @@ void info_free_exclist (struct tar_stat_info *dir);
 bool excluded_name (char const *name, struct tar_stat_info *st);
 void exclude_vcs_ignores (void);
 
+/* Module map.c */
+void owner_map_read (char const *name);
+int owner_map_translate (uid_t uid, uid_t *new_uid, char const **new_name);
+void group_map_read (char const *file);
+int group_map_translate (gid_t gid, gid_t *new_gid, char const **new_name);
+
+
 _GL_INLINE_HEADER_END

+ 9 - 9
src/create.c

@@ -741,17 +741,17 @@ union block *
 start_header (struct tar_stat_info *st)
 {
   union block *header;
-
+  char const *uname = NULL;
+  char const *gname = NULL;
+  
   header = write_header_name (st);
   if (!header)
     return NULL;
 
   /* Override some stat fields, if requested to do so.  */
+  owner_map_translate (st->stat.st_uid, &st->stat.st_uid, &uname);
+  group_map_translate (st->stat.st_gid, &st->stat.st_gid, &gname);
 
-  if (owner_option != (uid_t) -1)
-    st->stat.st_uid = owner_option;
-  if (group_option != (gid_t) -1)
-    st->stat.st_gid = group_option;
   if (mode_option)
     st->stat.st_mode =
       ((st->stat.st_mode & ~MODE_ALL)
@@ -910,13 +910,13 @@ start_header (struct tar_stat_info *st)
     }
   else
     {
-      if (owner_name_option)
-	st->uname = xstrdup (owner_name_option);
+      if (uname)
+	st->uname = xstrdup (uname);
       else
 	uid_to_uname (st->stat.st_uid, &st->uname);
 
-      if (group_name_option)
-	st->gname = xstrdup (group_name_option);
+      if (gname)
+	st->gname = xstrdup (gname);
       else
 	gid_to_gname (st->stat.st_gid, &st->gname);
 

+ 283 - 0
src/map.c

@@ -0,0 +1,283 @@
+/* Owner/group mapping for tar
+
+   Copyright 2015 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 "wordsplit.h"
+#include <hash.h>
+#include <pwd.h>
+
+struct mapentry
+{
+  uintmax_t orig_id;
+  uintmax_t new_id;
+  char *new_name;
+};
+
+static size_t
+map_hash (void const *entry, size_t nbuckets)
+{
+  struct mapentry const *map = entry;
+  return map->orig_id % nbuckets;
+}
+
+static bool
+map_compare (void const *entry1, void const *entry2)
+{
+  struct mapentry const *map1 = entry1;
+  struct mapentry const *map2 = entry2;
+  return map1->orig_id == map2->orig_id;
+}
+
+static int
+parse_id (uintmax_t *retval,
+	  char const *arg, char const *what, uintmax_t maxval,
+	  char const *file, unsigned line)
+{
+  uintmax_t v;
+  char *p;
+  
+  errno = 0;
+  v = strtoumax (arg, &p, 10);
+  if (*p || errno)
+    {
+      error (0, 0, _("%s:%u: invalid %s: %s"),  file, line, what, arg);
+      return -1;
+    }
+  if (v > maxval)
+    {
+      error (0, 0, _("%s:%u: %s out of range: %s"), file, line, what, arg);
+      return -1;
+    }
+  *retval = v;
+  return 0;
+}
+
+static void
+map_read (Hash_table **ptab, char const *file,
+	  uintmax_t (*name_to_id) (char const *), char const *what,
+	  uintmax_t maxval)
+{
+  FILE *fp;
+  char *buf = NULL;
+  size_t bufsize = 0;
+  ssize_t n;
+  struct wordsplit ws;
+  int wsopt;
+  unsigned line;
+  int err = 0;
+  
+  fp = fopen (file, "r");
+  if (!fp)
+    open_fatal (file);
+
+  ws.ws_comment = "#";
+  wsopt = WRDSF_COMMENT | WRDSF_NOVAR | WRDSF_NOCMD | WRDSF_SQUEEZE_DELIMS
+          | WRDSF_QUOTE;
+  line = 0;
+  while ((n = getline (&buf, &bufsize, fp)) > 0)
+    {
+      struct mapentry *ent;
+      uintmax_t orig_id, new_id;
+      char *name = NULL;
+      char *colon;
+      
+      ++line;
+      if (wordsplit (buf, &ws, wsopt))
+	FATAL_ERROR ((0, 0, _("%s:%u: cannot split line: %s"),
+		      file, line, wordsplit_strerror (&ws)));
+      wsopt |= WRDSF_REUSE;
+      if (ws.ws_wordc == 0)
+	continue;
+      if (ws.ws_wordc != 2)
+	{
+	  error (0, 0, _("%s:%u: malformed line"), file, line);
+	  err = 1;
+	  continue;
+	}
+
+      if (ws.ws_wordv[0][0] == '+')
+	{
+	  if (parse_id (&orig_id, ws.ws_wordv[0]+1, what, maxval, file, line)) 
+	    {
+	      err = 1;
+	      continue;
+	    }
+	}
+      else if (name_to_id)
+	{
+	  orig_id = name_to_id (ws.ws_wordv[0]);
+	  if (orig_id == UINTMAX_MAX)
+	    {
+	      error (0, 0, _("%s:%u: can't obtain %s of %s"),
+		     file, line, what, ws.ws_wordv[0]);
+	      err = 1;
+	      continue;
+	    }
+	}
+
+      colon = strchr (ws.ws_wordv[1], ':');
+      if (colon)
+	{
+	  if (colon > ws.ws_wordv[1])
+	    name = ws.ws_wordv[1];
+	  *colon++ = 0;
+	  if (parse_id (&new_id, colon, what, maxval, file, line)) 
+	    {
+	      err = 1;
+	      continue;
+	    }
+	}
+      else if (ws.ws_wordv[1][0] == '+')
+	{
+	  if (parse_id (&new_id, ws.ws_wordv[1], what, maxval, file, line)) 
+	    {
+	      err = 1;
+	      continue;
+	    }
+	}
+      else
+	{
+	  name = ws.ws_wordv[1];
+	  new_id = name_to_id (ws.ws_wordv[1]);
+	  if (new_id == UINTMAX_MAX)
+	    {
+	      error (0, 0, _("%s:%u: can't obtain %s of %s"),
+		     file, line, what, ws.ws_wordv[1]);
+	      err = 1;
+	      continue;
+	    }
+	}
+
+      ent = xmalloc (sizeof (*ent));
+      ent->orig_id = orig_id;
+      ent->new_id = new_id;
+      ent->new_name = name ? xstrdup (name) : NULL;
+      
+      if (!((*ptab
+	     || (*ptab = hash_initialize (0, 0, map_hash, map_compare, 0)))
+	    && hash_insert (*ptab, ent)))
+	xalloc_die ();
+    }
+  if (wsopt & WRDSF_REUSE)
+    wordsplit_free (&ws);
+  fclose (fp);
+  if (err)
+    FATAL_ERROR ((0, 0, _("errors reading map file")));
+}
+
+/* UID translation */
+
+static Hash_table *owner_map;
+
+static uintmax_t
+name_to_uid (char const *name)
+{
+  struct passwd *pw = getpwnam (name);
+  return pw ? pw->pw_uid : UINTMAX_MAX;
+}
+
+void
+owner_map_read (char const *file)
+{
+  map_read (&owner_map, file, name_to_uid, "UID", TYPE_MAXIMUM (uid_t));
+}
+
+int
+owner_map_translate (uid_t uid, uid_t *new_uid, char const **new_name)
+{
+  int rc = 1;
+  
+  if (owner_map)
+    {
+      struct mapentry ent, *res;
+  
+      ent.orig_id = uid;
+      res = hash_lookup (owner_map, &ent);
+      if (res)
+	{
+	  *new_uid = res->new_id;
+	  *new_name = res->new_name;
+	  return 0;
+	}
+    }
+
+  if (owner_option != (uid_t) -1)
+    {
+      *new_uid = owner_option;
+      rc = 0;
+    }
+  if (owner_name_option)
+    {
+      *new_name = owner_name_option;
+      rc = 0;
+    }
+
+  return rc;
+}
+
+/* GID translation */
+
+static Hash_table *group_map;
+
+static uintmax_t
+name_to_gid (char const *name)
+{
+  struct group *gr = getgrnam (name);
+  return gr ? gr->gr_gid : UINTMAX_MAX;
+}
+
+void
+group_map_read (char const *file)
+{
+  map_read (&group_map, file, name_to_gid, "GID", TYPE_MAXIMUM (gid_t));
+}
+
+int
+group_map_translate (gid_t gid, gid_t *new_gid, char const **new_name)
+{
+  int rc = 1;
+  
+  if (group_map)
+    {
+      struct mapentry ent, *res;
+  
+      ent.orig_id = gid;
+      res = hash_lookup (group_map, &ent);
+      if (res)
+	{
+	  *new_gid = res->new_id;
+	  *new_name = res->new_name;
+	  return 0;
+	}
+    }
+
+  if (group_option != (uid_t) -1)
+    {
+      *new_gid = group_option;
+      rc = 0;
+    }
+  if (group_name_option)
+    {
+      *new_name = group_name_option;
+      rc = 0;
+    }
+  
+  return rc;
+}

+ 14 - 0
src/tar.c

@@ -298,6 +298,7 @@ enum
   FORCE_LOCAL_OPTION,
   FULL_TIME_OPTION,
   GROUP_OPTION,
+  GROUP_MAP_OPTION,
   IGNORE_CASE_OPTION,
   IGNORE_COMMAND_ERROR_OPTION,
   IGNORE_FAILED_READ_OPTION,
@@ -340,6 +341,7 @@ enum
   OVERWRITE_DIR_OPTION,
   OVERWRITE_OPTION,
   OWNER_OPTION,
+  OWNER_MAP_OPTION,
   PAX_OPTION,
   POSIX_OPTION,
   PRESERVE_OPTION,
@@ -534,6 +536,10 @@ static struct argp_option options[] = {
    N_("force NAME as owner for added files"), GRID+1 },
   {"group", GROUP_OPTION, N_("NAME"), 0,
    N_("force NAME as group for added files"), GRID+1 },
+  {"owner-map", OWNER_MAP_OPTION, N_("FILE"), 0,
+   N_("use FILE to map file owner UIDs and names"), GRID+1 },
+  {"group-map", GROUP_MAP_OPTION, N_("FILE"), 0,
+   N_("use FILE to map file owner GIDs and names"), GRID+1 },
   {"mtime", MTIME_OPTION, N_("DATE-OR-FILE"), 0,
    N_("set mtime for added files from DATE-OR-FILE"), GRID+1 },
   {"mode", MODE_OPTION, N_("CHANGES"), 0,
@@ -1996,6 +2002,10 @@ parse_opt (int key, char *arg, struct argp_state *state)
       }
       break;
 
+    case GROUP_MAP_OPTION:
+      group_map_read (arg);
+      break;
+      
     case MODE_OPTION:
       mode_option = mode_compile (arg);
       if (!mode_option)
@@ -2090,6 +2100,10 @@ parse_opt (int key, char *arg, struct argp_state *state)
       }
       break;
 
+    case OWNER_MAP_OPTION:
+      owner_map_read (arg);
+      break;
+      
     case QUOTE_CHARS_OPTION:
       for (;*arg; arg++)
 	set_char_quoting (NULL, *arg, 1);

+ 1 - 0
tests/Makefile.am

@@ -140,6 +140,7 @@ TESTSUITE_AT = \
  lustar01.at\
  lustar02.at\
  lustar03.at\
+ map.at\
  multiv01.at\
  multiv02.at\
  multiv03.at\

+ 71 - 0
tests/map.at

@@ -0,0 +1,71 @@
+# Process this file with autom4te to create testsuite. -*- Autotest -*-
+#
+# Test suite for GNU tar.
+# Copyright 2015 Free Software Foundation, Inc.
+#
+# This program 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, or (at your option)
+# any later version.
+#
+# This program 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/>.
+
+# Check the --owner-map and --group-map options.
+
+AT_SETUP([--owner-map and --group-map])
+AT_KEYWORDS([owner map])
+
+AT_TAR_CHECK([
+export TZ=UTC0
+
+genfile --file a
+set -- `genfile --stat=uid,gid a`
+cat > uid.map <<EOT
+# Owner mapping
++$1	"Joe the Plumber:1234"
+EOT
+# Group mapping
+cat > gid.map <<EOT
++$2	"Plumber's Union:5678"
+EOT
+
+tar --owner-map=uid.map\
+    --group-map=gid.map\
+    --owner="Fallback Owner:4321" \
+    --group="Fallback Group:8765" \
+    --mtime='@0' \
+    --mode='u=rw,go=r' \
+    -cf 1.tar a
+
+tar -tvf 1.tar
+tar --numeric-owner -tvf 1.tar
+    
+> uid.map
+> gid.map
+
+tar --owner-map=uid.map\
+    --group-map=gid.map\
+    --owner="Fallback Owner:4321" \
+    --group="Fallback Group:8765" \
+    --mtime='@0' \
+    --mode='u=rw,go=r' \
+    -cf 2.tar a
+
+tar -tvf 2.tar
+tar --numeric-owner -tvf 2.tar
+],
+[0],
+[-rw-r--r-- Joe the Plumber/Plumber's Union 0 1970-01-01 00:00 a
+-rw-r--r-- 1234/5678         0 1970-01-01 00:00 a
+-rw-r--r-- Fallback Owner/Fallback Group 0 1970-01-01 00:00 a
+-rw-r--r-- 4321/8765         0 1970-01-01 00:00 a
+],
+[],[],[],[gnu])
+
+AT_CLEANUP

+ 1 - 0
tests/testsuite.at

@@ -349,6 +349,7 @@ m4_include([multiv08.at])
 
 AT_BANNER([Owner and Groups])
 m4_include([owner.at])
+m4_include([map.at])
 
 AT_BANNER([Sparse files])
 m4_include([sparse01.at])