* 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.
-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 <bug-tar@gnu.org>
\f
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
.\"
.\" 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
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.
\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)
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}
@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}
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
xheader.c\
incremen.c\
list.c\
+ map.c\
misc.c\
names.c\
sparse.c\
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
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)
}
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);
--- /dev/null
+/* 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")));
+}
+\f
+/* 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;
+}
+\f
+/* 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;
+}
FORCE_LOCAL_OPTION,
FULL_TIME_OPTION,
GROUP_OPTION,
+ GROUP_MAP_OPTION,
IGNORE_CASE_OPTION,
IGNORE_COMMAND_ERROR_OPTION,
IGNORE_FAILED_READ_OPTION,
OVERWRITE_DIR_OPTION,
OVERWRITE_OPTION,
OWNER_OPTION,
+ OWNER_MAP_OPTION,
PAX_OPTION,
POSIX_OPTION,
PRESERVE_OPTION,
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,
}
break;
+ case GROUP_MAP_OPTION:
+ group_map_read (arg);
+ break;
+
case MODE_OPTION:
mode_option = mode_compile (arg);
if (!mode_option)
}
break;
+ case OWNER_MAP_OPTION:
+ owner_map_read (arg);
+ break;
+
case QUOTE_CHARS_OPTION:
for (;*arg; arg++)
set_char_quoting (NULL, *arg, 1);
lustar01.at\
lustar02.at\
lustar03.at\
+ map.at\
multiv01.at\
multiv02.at\
multiv03.at\
--- /dev/null
+# 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
AT_BANNER([Owner and Groups])
m4_include([owner.at])
+m4_include([map.at])
AT_BANNER([Sparse files])
m4_include([sparse01.at])