From: Sergey Poznyakoff Date: Mon, 2 Nov 2015 10:51:19 +0000 (+0200) Subject: New options: --owner-map and --group-map. X-Git-Tag: release_1_29~42 X-Git-Url: https://git.gag.com/?a=commitdiff_plain;h=1a615a41f5e1c84b8386d2b524af9126ebb810b1;p=debian%2Ftar 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. --- diff --git a/NEWS b/NEWS index aa6d4739..2dd9f195 100644 --- a/NEWS +++ b/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 @@ -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 diff --git a/doc/tar.1 b/doc/tar.1 index 0b673553..6e2aa745 100644 --- a/doc/tar.1 +++ b/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 . -.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) diff --git a/doc/tar.texi b/doc/tar.texi index fe750b6d..9713cb09 100644 --- a/doc/tar.texi +++ b/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 diff --git a/src/Makefile.am b/src/Makefile.am index 82b2d462..a0bacfd9 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -33,6 +33,7 @@ tar_SOURCES = \ xheader.c\ incremen.c\ list.c\ + map.c\ misc.c\ names.c\ sparse.c\ diff --git a/src/common.h b/src/common.h index a75e1148..72ad4c13 100644 --- a/src/common.h +++ b/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 diff --git a/src/create.c b/src/create.c index 7cdc9782..b7c19ab5 100644 --- a/src/create.c +++ b/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); diff --git a/src/map.c b/src/map.c new file mode 100644 index 00000000..36216ed4 --- /dev/null +++ b/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 . */ + +#include +#include "common.h" +#include "wordsplit.h" +#include +#include + +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; +} diff --git a/src/tar.c b/src/tar.c index 0a5e2ee7..83c72888 100644 --- a/src/tar.c +++ b/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); diff --git a/tests/Makefile.am b/tests/Makefile.am index 70aa8747..3d5addf0 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -140,6 +140,7 @@ TESTSUITE_AT = \ lustar01.at\ lustar02.at\ lustar03.at\ + map.at\ multiv01.at\ multiv02.at\ multiv03.at\ diff --git a/tests/map.at b/tests/map.at new file mode 100644 index 00000000..c6fade82 --- /dev/null +++ b/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 . + +# 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 < gid.map < 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 diff --git a/tests/testsuite.at b/tests/testsuite.at index b1e7d3b8..4b1c8054 100644 --- a/tests/testsuite.at +++ b/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])