diff options
Diffstat (limited to '')
-rw-r--r-- | src/divertcmd.c | 876 |
1 files changed, 876 insertions, 0 deletions
diff --git a/src/divertcmd.c b/src/divertcmd.c new file mode 100644 index 0000000..5095e2b --- /dev/null +++ b/src/divertcmd.c @@ -0,0 +1,876 @@ +/* + * dpkg-divert - override a package's version of a file + * + * Copyright © 1995 Ian Jackson + * Copyright © 2000, 2001 Wichert Akkerman + * Copyright © 2006-2015, 2017-2018 Guillem Jover <guillem@debian.org> + * Copyright © 2011 Linaro Limited + * Copyright © 2011 Raphaël Hertzog <hertzog@debian.org> + * + * 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 2 of the License, 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 <https://www.gnu.org/licenses/>. + */ + +#include <config.h> +#include <compat.h> + +#include <sys/types.h> +#include <sys/stat.h> + +#include <errno.h> +#if HAVE_LOCALE_H +#include <locale.h> +#endif +#include <fcntl.h> +#include <fnmatch.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> + +#include <dpkg/i18n.h> +#include <dpkg/dpkg.h> +#include <dpkg/dpkg-db.h> +#include <dpkg/arch.h> +#include <dpkg/file.h> +#include <dpkg/glob.h> +#include <dpkg/buffer.h> +#include <dpkg/options.h> +#include <dpkg/db-fsys.h> + + +static const char printforhelp[] = N_( +"Use --help for help about diverting files."); + +static const char *admindir; +const char *instdir; + +static bool opt_pkgname_match_any = true; +static const char *opt_pkgname = NULL; +static const char *opt_divertto = NULL; + +static int opt_verbose = 1; +static int opt_test = 0; +static int opt_rename = -1; + + +static void +printversion(const struct cmdinfo *cip, const char *value) +{ + printf(_("Debian %s version %s.\n"), dpkg_get_progname(), + PACKAGE_RELEASE); + + printf(_( +"This is free software; see the GNU General Public License version 2 or\n" +"later for copying conditions. There is NO warranty.\n")); + + m_output(stdout, _("<standard output>")); + + exit(0); +} + +static void +usage(const struct cmdinfo *cip, const char *value) +{ + printf(_( +"Usage: %s [<option>...] <command>\n" +"\n"), dpkg_get_progname()); + + printf(_( +"Commands:\n" +" [--add] <file> add a diversion.\n" +" --remove <file> remove the diversion.\n" +" --list [<glob-pattern>] show file diversions.\n" +" --listpackage <file> show what package diverts the file.\n" +" --truename <file> return the diverted file.\n" +"\n")); + + printf(_( +"Options:\n" +" --package <package> name of the package whose copy of <file> will not\n" +" be diverted.\n" +" --local all packages' versions are diverted.\n" +" --divert <divert-to> the name used by other packages' versions.\n" +" --rename actually move the file aside (or back).\n" +" --no-rename do not move the file aside (or back) (default).\n" +" --admindir <directory> set the directory with the diversions file.\n" +" --instdir <directory> set the root directory, but not the admin dir.\n" +" --root <directory> set the directory of the root filesystem.\n" +" --test don't do anything, just demonstrate.\n" +" --quiet quiet operation, minimal output.\n" +" --help show this help message.\n" +" --version show the version.\n" +"\n")); + + printf(_( +"When adding, default is --local and --divert <original>.distrib.\n" +"When removing, --package or --local and --divert must match if specified.\n" +"Package preinst/postrm scripts should always specify --package and --divert.\n")); + + m_output(stdout, _("<standard output>")); + + exit(0); +} + +static void +opt_rename_setup(void) +{ + if (opt_rename >= 0) + return; + + opt_rename = 0; + warning(_("please specify --no-rename explicitly, the default " + "will change to --rename in 1.20.x")); +} + +struct file { + char *name; + enum { + FILE_STAT_INVALID, + FILE_STAT_VALID, + FILE_STAT_NOFILE, + } stat_state; + struct stat stat; +}; + +static void +file_init(struct file *f, const char *filename) +{ + struct varbuf usefilename = VARBUF_INIT; + + varbuf_add_str(&usefilename, instdir); + varbuf_add_str(&usefilename, filename); + varbuf_end_str(&usefilename); + + f->name = varbuf_detach(&usefilename); + f->stat_state = FILE_STAT_INVALID; +} + +static void +file_destroy(struct file *f) +{ + free(f->name); +} + +static void +file_stat(struct file *f) +{ + int ret; + + if (f->stat_state != FILE_STAT_INVALID) + return; + + ret = lstat(f->name, &f->stat); + if (ret && errno != ENOENT) + ohshite(_("cannot stat file '%s'"), f->name); + + if (ret == 0) + f->stat_state = FILE_STAT_VALID; + else + f->stat_state = FILE_STAT_NOFILE; +} + +static void +check_writable_dir(struct file *f) +{ + char *tmpname; + int tmpfd; + + tmpname = str_fmt("%s%s", f->name, ".dpkg-divert.tmp"); + + tmpfd = creat(tmpname, 0600); + if (tmpfd < 0) + ohshite(_("error checking '%s'"), f->name); + close(tmpfd); + (void)unlink(tmpname); + + free(tmpname); +} + +static bool +check_rename(struct file *src, struct file *dst) +{ + file_stat(src); + + /* If the source file is not present and we are not going to do + * the rename anyway there's no point in checking any further. */ + if (src->stat_state == FILE_STAT_NOFILE) + return false; + + file_stat(dst); + + /* + * Unfortunately we have to check for write access in both places, + * just having +w is not enough, since people do mount things RO, + * and we need to fail before we start mucking around with things. + * So we open a file with the same name as the diversions but with + * an extension that (hopefully) won't overwrite anything. If it + * succeeds, we assume a writable filesystem. + */ + + check_writable_dir(src); + check_writable_dir(dst); + + if (src->stat_state == FILE_STAT_VALID && + dst->stat_state == FILE_STAT_VALID && + !(src->stat.st_dev == dst->stat.st_dev && + src->stat.st_ino == dst->stat.st_ino)) + ohshit(_("rename involves overwriting '%s' with\n" + " different file '%s', not allowed"), + dst->name, src->name); + + return true; +} + +static void +file_copy(const char *src, const char *dst) +{ + struct dpkg_error err; + char *tmp; + int srcfd, dstfd; + + srcfd = open(src, O_RDONLY); + if (srcfd < 0) + ohshite(_("unable to open file '%s'"), src); + + tmp = str_fmt("%s%s", dst, ".dpkg-divert.tmp"); + dstfd = creat(tmp, 0600); + if (dstfd < 0) + ohshite(_("unable to create file '%s'"), tmp); + + push_cleanup(cu_filename, ~ehflag_normaltidy, 1, tmp); + + if (fd_fd_copy(srcfd, dstfd, -1, &err) < 0) + ohshit(_("cannot copy '%s' to '%s': %s"), src, tmp, err.str); + + close(srcfd); + + if (fsync(dstfd)) + ohshite(_("unable to sync file '%s'"), tmp); + if (close(dstfd)) + ohshite(_("unable to close file '%s'"), tmp); + + file_copy_perms(src, tmp); + + if (rename(tmp, dst) != 0) + ohshite(_("cannot rename '%s' to '%s'"), tmp, dst); + + free(tmp); + + pop_cleanup(ehflag_normaltidy); +} + +static void +file_rename(struct file *src, struct file *dst) +{ + if (src->stat_state == FILE_STAT_NOFILE) + return; + + if (dst->stat_state == FILE_STAT_VALID) { + if (unlink(src->name)) + ohshite(_("rename: remove duplicate old link '%s'"), + src->name); + } else { + if (rename(src->name, dst->name) == 0) + return; + + /* If a rename didn't work try moving the file instead. */ + file_copy(src->name, dst->name); + + if (unlink(src->name)) + ohshite(_("unable to remove copied source file '%s'"), + src->name); + } +} + +static void +diversion_check_filename(const char *filename) +{ + if (filename[0] != '/') + badusage(_("filename \"%s\" is not absolute"), filename); + if (strchr(filename, '\n') != NULL) + badusage(_("file may not contain newlines")); +} + +static const char * +diversion_pkg_name(struct fsys_diversion *d) +{ + if (d->pkgset == NULL) + return ":"; + else + return d->pkgset->name; +} + +static const char * +varbuf_diversion(struct varbuf *str, const char *pkgname, + const char *filename, const char *divertto) +{ + varbuf_reset(str); + + if (pkgname == NULL) { + if (divertto == NULL) + varbuf_printf(str, _("local diversion of %s"), filename); + else + varbuf_printf(str, _("local diversion of %s to %s"), + filename, divertto); + } else { + if (divertto == NULL) + varbuf_printf(str, _("diversion of %s by %s"), + filename, pkgname); + else + varbuf_printf(str, _("diversion of %s to %s by %s"), + filename, divertto, pkgname); + } + + return str->buf; +} + +static const char * +diversion_current(const char *filename) +{ + static struct varbuf str = VARBUF_INIT; + + if (opt_pkgname_match_any) { + varbuf_reset(&str); + + if (opt_divertto == NULL) + varbuf_printf(&str, _("any diversion of %s"), filename); + else + varbuf_printf(&str, _("any diversion of %s to %s"), + filename, opt_divertto); + } else { + return varbuf_diversion(&str, opt_pkgname, filename, opt_divertto); + } + + return str.buf; +} + +static const char * +diversion_describe(struct fsys_diversion *d) +{ + static struct varbuf str = VARBUF_INIT; + const char *pkgname; + const char *name_from, *name_to; + + if (d->camefrom) { + name_from = d->camefrom->name; + name_to = d->camefrom->divert->useinstead->name; + } else { + name_from = d->useinstead->divert->camefrom->name; + name_to = d->useinstead->name; + } + + if (d->pkgset == NULL) + pkgname = NULL; + else + pkgname = d->pkgset->name; + + return varbuf_diversion(&str, pkgname, name_from, name_to); +} + +static void +divertdb_write(void) +{ + char *dbname; + struct atomic_file *file; + struct fsys_hash_iter *iter; + struct fsys_namenode *namenode; + + dbname = dpkg_db_get_path(DIVERSIONSFILE); + + file = atomic_file_new(dbname, ATOMIC_FILE_BACKUP); + atomic_file_open(file); + + iter = fsys_hash_iter_new(); + while ((namenode = fsys_hash_iter_next(iter))) { + struct fsys_diversion *d = namenode->divert; + + if (d == NULL || d->useinstead == NULL) + continue; + + fprintf(file->fp, "%s\n%s\n%s\n", + d->useinstead->divert->camefrom->name, + d->useinstead->name, + diversion_pkg_name(d)); + } + fsys_hash_iter_free(iter); + + atomic_file_sync(file); + atomic_file_close(file); + atomic_file_commit(file); + atomic_file_free(file); + + free(dbname); +} + +static bool +diversion_is_essential(struct fsys_namenode *namenode) +{ + struct pkginfo *pkg; + struct pkg_hash_iter *pkg_iter; + struct fsys_node_pkgs_iter *iter; + bool essential = false; + + pkg_iter = pkg_hash_iter_new(); + while ((pkg = pkg_hash_iter_next_pkg(pkg_iter))) { + if (pkg->installed.essential) + ensure_packagefiles_available(pkg); + } + pkg_hash_iter_free(pkg_iter); + + iter = fsys_node_pkgs_iter_new(namenode); + while ((pkg = fsys_node_pkgs_iter_next(iter))) { + if (pkg->installed.essential) { + essential = true; + break; + } + } + fsys_node_pkgs_iter_free(iter); + + return essential; +} + +static bool +diversion_is_owned_by_self(struct pkgset *set, struct fsys_namenode *namenode) +{ + struct pkginfo *pkg; + struct fsys_node_pkgs_iter *iter; + bool owned = false; + + if (set == NULL) + return false; + + for (pkg = &set->pkg; pkg; pkg = pkg->arch_next) + ensure_packagefiles_available(pkg); + + iter = fsys_node_pkgs_iter_new(namenode); + while ((pkg = fsys_node_pkgs_iter_next(iter))) { + if (pkg->set == set) { + owned = true; + break; + } + } + fsys_node_pkgs_iter_free(iter); + + return owned; +} + +static int +diversion_add(const char *const *argv) +{ + const char *filename = argv[0]; + struct file file_from, file_to; + struct fsys_diversion *contest, *altname; + struct fsys_namenode *fnn_from, *fnn_to; + struct pkgset *pkgset; + + opt_pkgname_match_any = false; + opt_rename_setup(); + + /* Handle filename. */ + if (!filename || argv[1]) + badusage(_("--%s needs a single argument"), cipaction->olong); + + diversion_check_filename(filename); + + file_init(&file_from, filename); + file_stat(&file_from); + + if (file_from.stat_state == FILE_STAT_VALID && + S_ISDIR(file_from.stat.st_mode)) + badusage(_("cannot divert directories")); + + fnn_from = fsys_hash_find_node(filename, 0); + + /* Handle divertto. */ + if (opt_divertto == NULL) + opt_divertto = str_fmt("%s.distrib", filename); + + if (strcmp(filename, opt_divertto) == 0) + badusage(_("cannot divert file '%s' to itself"), filename); + + file_init(&file_to, opt_divertto); + + fnn_to = fsys_hash_find_node(opt_divertto, 0); + + /* Handle package name. */ + if (opt_pkgname == NULL) + pkgset = NULL; + else + pkgset = pkg_hash_find_set(opt_pkgname); + + /* Check we are not stomping over an existing diversion. */ + if (fnn_from->divert || fnn_to->divert) { + if (fnn_to->divert && fnn_to->divert->camefrom && + strcmp(fnn_to->divert->camefrom->name, filename) == 0 && + fnn_from->divert && fnn_from->divert->useinstead && + strcmp(fnn_from->divert->useinstead->name, opt_divertto) == 0 && + fnn_from->divert->pkgset == pkgset) { + if (opt_verbose > 0) + printf(_("Leaving '%s'\n"), + diversion_describe(fnn_from->divert)); + + file_destroy(&file_from); + file_destroy(&file_to); + + return 0; + } + + ohshit(_("'%s' clashes with '%s'"), + diversion_current(filename), + fnn_from->divert ? + diversion_describe(fnn_from->divert) : + diversion_describe(fnn_to->divert)); + } + + /* Create new diversion. */ + contest = nfmalloc(sizeof(*contest)); + altname = nfmalloc(sizeof(*altname)); + + altname->camefrom = fnn_from; + altname->camefrom->divert = contest; + altname->useinstead = NULL; + altname->pkgset = pkgset; + + contest->useinstead = fnn_to; + contest->useinstead->divert = altname; + contest->camefrom = NULL; + contest->pkgset = pkgset; + + /* Update database and file system if needed. */ + if (opt_verbose > 0) + printf(_("Adding '%s'\n"), diversion_describe(contest)); + if (opt_rename) + opt_rename = check_rename(&file_from, &file_to); + /* Check we are not renaming a file owned by the diverting pkgset. */ + if (opt_rename && diversion_is_owned_by_self(pkgset, fnn_from)) { + if (opt_verbose > 0) + printf(_("Ignoring request to rename file '%s' " + "owned by diverting package '%s'\n"), + filename, pkgset->name); + opt_rename = false; + } + if (opt_rename && diversion_is_essential(fnn_from)) + warning(_("diverting file '%s' from an Essential package with " + "rename is dangerous, use --no-rename"), filename); + if (!opt_test) { + divertdb_write(); + if (opt_rename) + file_rename(&file_from, &file_to); + } + + file_destroy(&file_from); + file_destroy(&file_to); + + return 0; +} + +static bool +diversion_is_shared(struct pkgset *set, struct fsys_namenode *namenode) +{ + const char *archname; + struct pkginfo *pkg; + struct dpkg_arch *arch; + struct fsys_node_pkgs_iter *iter; + bool shared = false; + + if (set == NULL) + return false; + + archname = getenv("DPKG_MAINTSCRIPT_ARCH"); + arch = dpkg_arch_find(archname); + if (arch->type == DPKG_ARCH_NONE || arch->type == DPKG_ARCH_EMPTY) + return false; + + for (pkg = &set->pkg; pkg; pkg = pkg->arch_next) + ensure_packagefiles_available(pkg); + + iter = fsys_node_pkgs_iter_new(namenode); + while ((pkg = fsys_node_pkgs_iter_next(iter))) { + if (pkg->set == set && pkg->installed.arch != arch) { + shared = true; + break; + } + } + fsys_node_pkgs_iter_free(iter); + + return shared; +} + +static int +diversion_remove(const char *const *argv) +{ + const char *filename = argv[0]; + struct fsys_namenode *namenode; + struct fsys_diversion *contest, *altname; + struct file file_from, file_to; + struct pkgset *pkgset; + + opt_rename_setup(); + + if (!filename || argv[1]) + badusage(_("--%s needs a single argument"), cipaction->olong); + + diversion_check_filename(filename); + + namenode = fsys_hash_find_node(filename, FHFF_NONE); + + if (namenode == NULL || namenode->divert == NULL || + namenode->divert->useinstead == NULL) { + if (opt_verbose > 0) + printf(_("No diversion '%s', none removed.\n"), + diversion_current(filename)); + return 0; + } + + if (opt_pkgname == NULL) + pkgset = NULL; + else + pkgset = pkg_hash_find_set(opt_pkgname); + + contest = namenode->divert; + altname = contest->useinstead->divert; + + if (opt_divertto != NULL && + strcmp(opt_divertto, contest->useinstead->name) != 0) + ohshit(_("mismatch on divert-to\n" + " when removing '%s'\n" + " found '%s'"), + diversion_current(filename), + diversion_describe(contest)); + + if (!opt_pkgname_match_any && pkgset != contest->pkgset) + ohshit(_("mismatch on package\n" + " when removing '%s'\n" + " found '%s'"), + diversion_current(filename), + diversion_describe(contest)); + + /* Ignore removal request if the diverted file is still owned + * by another package in the same set. */ + if (diversion_is_shared(pkgset, namenode)) { + if (opt_verbose > 0) + printf(_("Ignoring request to remove shared diversion '%s'.\n"), + diversion_describe(contest)); + return 0; + } + + if (opt_verbose > 0) + printf(_("Removing '%s'\n"), diversion_describe(contest)); + + file_init(&file_from, altname->camefrom->name); + file_init(&file_to, contest->useinstead->name); + + /* Remove entries from database. */ + contest->useinstead->divert = NULL; + altname->camefrom->divert = NULL; + + if (opt_rename) + opt_rename = check_rename(&file_to, &file_from); + if (opt_rename && !opt_test) + file_rename(&file_to, &file_from); + + if (!opt_test) + divertdb_write(); + + file_destroy(&file_from); + file_destroy(&file_to); + + return 0; +} + +static int +diversion_list(const char *const *argv) +{ + struct fsys_hash_iter *iter; + struct fsys_namenode *namenode; + struct glob_node *glob_list = NULL; + const char *pattern; + + while ((pattern = *argv++)) + glob_list_prepend(&glob_list, m_strdup(pattern)); + + if (glob_list == NULL) + glob_list_prepend(&glob_list, m_strdup("*")); + + iter = fsys_hash_iter_new(); + while ((namenode = fsys_hash_iter_next(iter))) { + struct glob_node *g; + struct fsys_diversion *contest = namenode->divert; + struct fsys_diversion *altname; + const char *pkgname; + + if (contest == NULL || contest->useinstead == NULL) + continue; + + altname = contest->useinstead->divert; + + pkgname = diversion_pkg_name(contest); + + for (g = glob_list; g; g = g->next) { + if (fnmatch(g->pattern, pkgname, 0) == 0 || + fnmatch(g->pattern, contest->useinstead->name, 0) == 0 || + fnmatch(g->pattern, altname->camefrom->name, 0) == 0) { + printf("%s\n", diversion_describe(contest)); + break; + } + } + } + fsys_hash_iter_free(iter); + + glob_list_free(glob_list); + + return 0; +} + +static int +diversion_truename(const char *const *argv) +{ + const char *filename = argv[0]; + struct fsys_namenode *namenode; + + if (!filename || argv[1]) + badusage(_("--%s needs a single argument"), cipaction->olong); + + diversion_check_filename(filename); + + namenode = fsys_hash_find_node(filename, FHFF_NONE); + + /* Print the given name if file is not diverted. */ + if (namenode && namenode->divert && namenode->divert->useinstead) + printf("%s\n", namenode->divert->useinstead->name); + else + printf("%s\n", filename); + + return 0; +} + +static int +diversion_listpackage(const char *const *argv) +{ + const char *filename = argv[0]; + struct fsys_namenode *namenode; + + if (!filename || argv[1]) + badusage(_("--%s needs a single argument"), cipaction->olong); + + diversion_check_filename(filename); + + namenode = fsys_hash_find_node(filename, FHFF_NONE); + + /* Print nothing if file is not diverted. */ + if (namenode == NULL || namenode->divert == NULL) + return 0; + + if (namenode->divert->pkgset == NULL) + /* Indicate package is local using something not in package + * namespace. */ + printf("LOCAL\n"); + else + printf("%s\n", namenode->divert->pkgset->name); + + return 0; +} + +static void +set_package(const struct cmdinfo *cip, const char *value) +{ + opt_pkgname_match_any = false; + + /* If value is NULL we are being called from --local. */ + opt_pkgname = value; + + if (opt_pkgname && strchr(opt_pkgname, '\n') != NULL) + badusage(_("package may not contain newlines")); +} + +static void +set_divertto(const struct cmdinfo *cip, const char *value) +{ + opt_divertto = value; + + if (opt_divertto[0] != '/') + badusage(_("filename \"%s\" is not absolute"), opt_divertto); + if (strchr(opt_divertto, '\n') != NULL) + badusage(_("divert-to may not contain newlines")); +} + +static void +set_instdir(const struct cmdinfo *cip, const char *value) +{ + instdir = dpkg_fsys_set_dir(value); +} + +static void +set_root(const struct cmdinfo *cip, const char *value) +{ + instdir = dpkg_fsys_set_dir(value); + admindir = dpkg_fsys_get_path(ADMINDIR); +} + +static const struct cmdinfo cmdinfo_add = + ACTION("add", 0, 0, diversion_add); + +static const struct cmdinfo cmdinfos[] = { + ACTION("add", 0, 0, diversion_add), + ACTION("remove", 0, 0, diversion_remove), + ACTION("list", 0, 0, diversion_list), + ACTION("listpackage", 0, 0, diversion_listpackage), + ACTION("truename", 0, 0, diversion_truename), + + { "admindir", 0, 1, NULL, &admindir, NULL }, + { "instdir", 0, 1, NULL, NULL, set_instdir, 0 }, + { "root", 0, 1, NULL, NULL, set_root, 0 }, + { "divert", 0, 1, NULL, NULL, set_divertto }, + { "package", 0, 1, NULL, NULL, set_package }, + { "local", 0, 0, NULL, NULL, set_package }, + { "quiet", 0, 0, &opt_verbose, NULL, NULL, 0 }, + { "rename", 0, 0, &opt_rename, NULL, NULL, 1 }, + { "no-rename", 0, 0, &opt_rename, NULL, NULL, 0 }, + { "test", 0, 0, &opt_test, NULL, NULL, 1 }, + { "help", '?', 0, NULL, NULL, usage }, + { "version", 0, 0, NULL, NULL, printversion }, + { NULL, 0 } +}; + +int +main(int argc, const char * const *argv) +{ + const char *env_pkgname; + int ret; + + dpkg_locales_init(PACKAGE); + dpkg_program_init("dpkg-divert"); + dpkg_options_parse(&argv, cmdinfos, printforhelp); + + admindir = dpkg_db_set_dir(admindir); + instdir = dpkg_fsys_set_dir(instdir); + + env_pkgname = getenv("DPKG_MAINTSCRIPT_PACKAGE"); + if (opt_pkgname_match_any && env_pkgname) + set_package(NULL, env_pkgname); + + if (!cipaction) + setaction(&cmdinfo_add, NULL); + + modstatdb_open(msdbrw_readonly); + ensure_diversions(); + + ret = cipaction->action(argv); + + modstatdb_shutdown(); + + dpkg_program_done(); + dpkg_locales_done(); + + return ret; +} |