From db84fbacbe43fcb9144f523e4a965150ed7dcc3a Mon Sep 17 00:00:00 2001 From: Kamil Dudka Date: Mon, 16 Feb 2009 10:13:45 +0100 Subject: [PATCH] install: add --compare (-C) option to install file only when necessary * src/install.c (have_same_content): New function to compare files content. (need_copy): New function to check if copy is necessary. (main): Handle new option --compare (-C). (copy_file): Skip file copying if not necessary. (usage): Show new option --compare (-C) in --help. * tests/install/install-C: Basic tests for install --compare (-C). * tests/install/install-C-root: Tests requiring root privileges. * tests/install/install-C-selinux: Tests requiring SELinux. * tests/Makefile.am: Add new tests for install --compare (-C). * doc/coreutils.texi: Document new install option --compare (-C). * NEWS: Mention the change. --- NEWS | 5 ++ doc/coreutils.texi | 8 +++ src/install.c | 129 ++++++++++++++++++++++++++++++++++++++- tests/Makefile.am | 3 + tests/install/install-C | 94 ++++++++++++++++++++++++++++ tests/install/install-C-root | 80 ++++++++++++++++++++++++ tests/install/install-C-selinux | 56 +++++++++++++++++ 7 files changed, 374 insertions(+), 1 deletions(-) create mode 100755 tests/install/install-C create mode 100755 tests/install/install-C-root create mode 100755 tests/install/install-C-selinux diff --git a/NEWS b/NEWS index 9de4f25..4f80813 100644 --- a/NEWS +++ b/NEWS @@ -16,6 +16,11 @@ GNU coreutils NEWS -*- outline -*- dd accepts iflag=cio and oflag=cio to open the file in CIO (concurrent I/O) mode where this feature is available. + install accepts a new option, --compare (-C): compare each pair of source + and destination files, and if the destination has identical content and + any specified owner, group, permissions, and possibly SELinux context, then + do not modify the destination at all. + ls --color now highlights hard linked files, too stat -f recognizes the Lustre file system type diff --git a/doc/coreutils.texi b/doc/coreutils.texi index 57497e9..ba1e74e 100644 --- a/doc/coreutils.texi +++ b/doc/coreutils.texi @@ -2123,6 +2123,14 @@ The program accepts the following options. Also see @ref{Common options}. @table @samp address@hidden -C address@hidden --compare address@hidden -C address@hidden --compare +Compare each pair of source and destination files, and if the destination has +identical content and any specified owner, group, permissions, and possibly +SELinux context, then do not modify the destination at all. + @item -c @itemx --crown-margin @opindex -c diff --git a/src/install.c b/src/install.c index 9bf9eee..0a102bd 100644 --- a/src/install.c +++ b/src/install.c @@ -31,6 +31,7 @@ #include "cp-hash.h" #include "copy.h" #include "filenamecat.h" +#include "full-read.h" #include "mkancesdirs.h" #include "mkdir-p.h" #include "modechange.h" @@ -111,6 +112,8 @@ static char *group_name; static gid_t group_id; #define DEFAULT_MODE (S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH) +#define EXTRA_MODE(mode) ((S_ISUID & (mode)) || (S_ISGID & (mode)) \ + || (S_ISVTX & (mode))) /* The file mode bits to which non-directory files will be set. The umask has no effect. */ @@ -125,6 +128,9 @@ static mode_t dir_mode = DEFAULT_MODE; or S_ISGID bits. */ static mode_t dir_mode_bits = CHMOD_MODE_BITS; +/* Compare files before installing (-C) */ +static bool copy_only_if_needed; + /* If true, strip executable files after copying them. */ static bool strip_files; @@ -145,6 +151,7 @@ enum static struct option const long_options[] = { {"backup", optional_argument, NULL, 'b'}, + {"compare", no_argument, NULL, 'C'}, {GETOPT_SELINUX_CONTEXT_OPTION_DECL}, {"directory", no_argument, NULL, 'd'}, {"group", required_argument, NULL, 'g'}, @@ -167,6 +174,100 @@ static struct option const long_options[] = {NULL, 0, NULL, 0} }; +/* Compare content of opened files using file descriptors A_FD and B_FD. Return + true if files are equal. */ +static bool +have_same_content (int a_fd, int b_fd) +{ +#define CMP_BLOCK_SIZE 65536 + char a_buff[CMP_BLOCK_SIZE]; + char b_buff[CMP_BLOCK_SIZE]; + + size_t size; + while (0 < (size = full_read (a_fd, a_buff, CMP_BLOCK_SIZE))) { + if (size != full_read (b_fd, b_buff, CMP_BLOCK_SIZE)) + return false; + + if (memcmp (a_buff, b_buff, size) != 0) + return false; + } + + return size == 0; +#undef CMP_BLOCK_SIZE +} + +/* Return true if copy of file SRC_NAME to file DEST_NAME is necessary. */ +static bool +need_copy (const char *src_name, const char *dest_name, + const struct cp_options *x) +{ + struct stat src_sb, dest_sb; + int src_fd, dest_fd; + bool content_match; + + if (EXTRA_MODE (mode)) + return true; + + /* compare files using stat */ + if (lstat (src_name, &src_sb) != 0) + return true; + + if (lstat (dest_name, &dest_sb) != 0) + return true; + + if (!S_ISREG (src_sb.st_mode) || !S_ISREG (dest_sb.st_mode) + || EXTRA_MODE (src_sb.st_mode) || EXTRA_MODE (dest_sb.st_mode)) + return true; + + if (src_sb.st_size != dest_sb.st_size + || (dest_sb.st_mode & CHMOD_MODE_BITS) != mode + || dest_sb.st_uid != (owner_id == (uid_t) -1 ? getuid () : owner_id) + || dest_sb.st_gid != (group_id == (gid_t) -1 ? getgid () : group_id)) + return true; + + /* compare SELinux context if preserving */ + if (selinux_enabled && x->preserve_security_context) + { + security_context_t file_scontext = NULL; + security_context_t to_scontext = NULL; + bool scontext_match; + + if (getfilecon (src_name, &file_scontext) == -1) + return true; + + if (getfilecon (dest_name, &to_scontext) == -1) + { + freecon (file_scontext); + return true; + } + + scontext_match = STREQ (file_scontext, to_scontext); + + freecon (file_scontext); + freecon (to_scontext); + if (!scontext_match) + return true; + } + + /* compare files content */ + src_fd = open (src_name, O_RDONLY); + if (src_fd < 0) + return true; + + dest_fd = open (dest_name, O_RDONLY); + if (dest_fd < 0) + { + close (src_fd); + return true; + } + + content_match = have_same_content (src_fd, dest_fd); + + close (src_fd); + close (dest_fd); + return !content_match; +} + static void cp_option_init (struct cp_options *x) { @@ -361,7 +462,7 @@ main (int argc, char **argv) we'll actually use backup_suffix_string. */ backup_suffix_string = getenv ("SIMPLE_BACKUP_SUFFIX"); - while ((optc = getopt_long (argc, argv, "bcsDdg:m:o:pt:TvS:Z:", long_options, + while ((optc = getopt_long (argc, argv, "bcCsDdg:m:o:pt:TvS:Z:", long_options, NULL)) != -1) { switch (optc) @@ -373,6 +474,9 @@ main (int argc, char **argv) break; case 'c': break; + case 'C': + copy_only_if_needed = true; + break; case 's': strip_files = true; #ifdef SIGCHLD @@ -529,6 +633,23 @@ main (int argc, char **argv) error (0, 0, _("WARNING: ignoring --strip-program option as -s option was " "not specified")); + if (copy_only_if_needed && x.preserve_timestamps) + { + error (0, 0, _("options -C and --preserve-timestamps are mutually " + "exclusive")); + usage (EXIT_FAILURE); + } + + if (copy_only_if_needed && strip_files) + { + error (0, 0, _("options -C and --strip are mutually exclusive")); + usage (EXIT_FAILURE); + } + + if (copy_only_if_needed && EXTRA_MODE (mode)) + error (0, 0, _("options -C is ignored if any non-permission mode should " + "be set")); + get_ids (); if (dir_arg) @@ -645,6 +766,9 @@ copy_file (const char *from, const char *to, const struct cp_options *x) { bool copy_into_self; + if (copy_only_if_needed && !need_copy (from, to, x)) + return true; + /* Allow installing from non-regular files like /dev/null. Charles Karney reported that some Sun version of install allows that and that sendmail's installation process relies on the behavior. @@ -835,6 +959,9 @@ Mandatory arguments to long options are mandatory for short options too.\n\ --backup[=CONTROL] make a backup of each existing destination file\n\ -b like --backup but does not accept an argument\n\ -c (ignored)\n\ + -C, --compare install file, unless target already exists and is\n\ + the same as the new file, in which case the\n\ + modification time won't be changed\n\ -d, --directory treat all arguments as directory names; create all\n\ components of the specified directories\n\ "), stdout); diff --git a/tests/Makefile.am b/tests/Makefile.am index 024eb48..07e9473 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -25,6 +25,7 @@ root_tests = \ cp/preserve-gid \ cp/special-bits \ dd/skip-seek-past-dev \ + install/install-C-root \ ls/capability \ ls/nameless-uid \ misc/chcon \ @@ -318,6 +319,8 @@ TESTS = \ install/basic-1 \ install/create-leading \ install/d-slashdot \ + install/install-C \ + install/install-C-selinux \ install/strip-program \ install/trap \ ln/backup-1 \ diff --git a/tests/install/install-C b/tests/install/install-C new file mode 100755 index 0000000..129286d --- /dev/null +++ b/tests/install/install-C @@ -0,0 +1,94 @@ +#!/bin/sh +# Ensure "install -C" works. (basic tests) + +# Copyright (C) 2008 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 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 . + +if test "$VERBOSE" = yes; then + set -x + ginstall --version +fi + +. $srcdir/test-lib.sh + +mode1=0644 +mode2=0755 +mode3=1755 + +fail=0 + +echo test > a || framework_failure +echo "\`a' -> \`b'" > out_installed_first +echo "removed \`b' +\`a' -> \`b'" > out_installed_second +> out_empty + +# destination file does not exist +ginstall -Cv -m$mode1 a b > out || fail=1 +compare out out_installed_first || fail=1 + +# destination file exists +ginstall -Cv -m$mode1 a b > out || fail=1 +compare out out_empty || fail=1 + +# destination file exists (long option) +ginstall -v --compare -m$mode1 a b > out || fail=1 +compare out out_empty || fail=1 + +# destination file exists but -C is not given +ginstall -v -m$mode1 a b > out || fail=1 +compare out out_installed_second || fail=1 + +# option -C ignored if any non-permission mode should be set +ginstall -Cv -m$mode3 a b > out || fail=1 +compare out out_installed_second || fail=1 +ginstall -Cv -m$mode3 a b > out || fail=1 +compare out out_installed_second || fail=1 + +# files are not regular files +ln -s a c || framework_failure +ln -s b d || framework_failure +ginstall -Cv -m$mode1 c d > out || fail=1 +echo "removed \`d' +\`c' -> \`d'" > out_installed_second_cd +compare out out_installed_second_cd || fail=1 + +# destination file exists but content differs +echo test1 > a || framework_failure +ginstall -Cv -m$mode1 a b > out || fail=1 +compare out out_installed_second || fail=1 +ginstall -Cv -m$mode1 a b > out || fail=1 +compare out out_empty || fail=1 + +# destination file exists but content differs (same size) +echo test2 > a || framework_failure +ginstall -Cv -m$mode1 a b > out || fail=1 +compare out out_installed_second || fail=1 +ginstall -Cv -m$mode1 a b > out || fail=1 +compare out out_empty || fail=1 + +# destination file exists but mode differs +ginstall -Cv -m$mode2 a b > out || fail=1 +compare out out_installed_second || fail=1 +ginstall -Cv -m$mode2 a b > out || fail=1 +compare out out_empty || fail=1 + +# options -C and --preserve-timestamps are mutually exclusive +ginstall -C --preserve-timestamps a b && fail=1 + +# options -C and --strip are mutually exclusive +ginstall -C --strip --strip-program=echo a b && fail=1 + +Exit $fail diff --git a/tests/install/install-C-root b/tests/install/install-C-root new file mode 100755 index 0000000..1a07dbe --- /dev/null +++ b/tests/install/install-C-root @@ -0,0 +1,80 @@ +#!/bin/sh +# Ensure "install -C" compares owner and group. + +# Copyright (C) 2008 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 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 . + +if test "$VERBOSE" = yes; then + set -x + ginstall --version +fi + +. $srcdir/test-lib.sh +require_root_ + +u1=1 +u2=2 +g1=1 +g2=2 + +fail=0 + +echo test > a || framework_failure +echo "\`a' -> \`b'" > out_installed_first +echo "removed \`b' +\`a' -> \`b'" > out_installed_second +> out_empty + +# destination file does not exist +ginstall -Cv -o$u1 -g$g1 a b > out || fail=1 +compare out out_installed_first || fail=1 + +# destination file exists +ginstall -Cv -o$u1 -g$g1 a b > out || fail=1 +compare out out_empty || fail=1 + +# destination file exists but -C is not given +ginstall -v -o$u1 -g$g1 a b > out || fail=1 +compare out out_installed_second || fail=1 + +# destination file exists but owner differs +ginstall -Cv -o$u2 -g$g1 a b > out || fail=1 +compare out out_installed_second || fail=1 +ginstall -Cv -o$u2 -g$g1 a b > out || fail=1 +compare out out_empty || fail=1 + +# destination file exists but group differs +ginstall -Cv -o$u2 -g$g2 a b > out || fail=1 +compare out out_installed_second || fail=1 +ginstall -Cv -o$u2 -g$g2 a b > out || fail=1 +compare out out_empty || fail=1 + +# destination file exists but owner differs from getuid () +ginstall -Cv -o$u2 a b > out || fail=1 +compare out out_installed_second || fail=1 +ginstall -Cv a b > out || fail=1 +compare out out_installed_second || fail=1 +ginstall -Cv a b > out || fail=1 +compare out out_empty || fail=1 + +# destination file exists but group differs from getgid () +ginstall -Cv -g$g2 a b > out || fail=1 +compare out out_installed_second || fail=1 +ginstall -Cv a b > out || fail=1 +compare out out_installed_second || fail=1 +ginstall -Cv a b > out || fail=1 +compare out out_empty || fail=1 + +Exit $fail diff --git a/tests/install/install-C-selinux b/tests/install/install-C-selinux new file mode 100755 index 0000000..d1d9540 --- /dev/null +++ b/tests/install/install-C-selinux @@ -0,0 +1,56 @@ +#!/bin/sh +# Ensure "install -C" compares SELinux context. + +# Copyright (C) 2008 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 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 . + +if test "$VERBOSE" = yes; then + set -x + ginstall --version +fi + +. $srcdir/test-lib.sh +require_selinux_ + +fail=0 + +echo test > a || framework_failure +chcon -u system_u a || skip_test_ "chcon doesn't work" + +echo "\`a' -> \`b'" > out_installed_first +echo "removed \`b' +\`a' -> \`b'" > out_installed_second +> out_empty + +# destination file does not exist +ginstall -Cv --preserve-context a b > out || fail=1 +compare out out_installed_first || fail=1 + +# destination file exists +ginstall -Cv --preserve-context a b > out || fail=1 +compare out out_empty || fail=1 + +# destination file exists but -C is not given +ginstall -v --preserve-context a b > out || fail=1 +compare out out_installed_second || fail=1 + +# destination file exists but SELinux context differs +chcon -u unconfined_u a || skip_test_ "chcon doesn't work" +ginstall -Cv --preserve-context a b > out || fail=1 +compare out out_installed_second || fail=1 +ginstall -Cv --preserve-context a b > out || fail=1 +compare out out_empty || fail=1 + +Exit $fail -- 1.6.1.2