summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS5
-rw-r--r--doc/coreutils.texi8
-rw-r--r--src/install.c134
-rw-r--r--tests/Makefile.am3
-rwxr-xr-xtests/install/install-C94
-rwxr-xr-xtests/install/install-C-root80
-rwxr-xr-xtests/install/install-C-selinux56
7 files changed, 379 insertions, 1 deletions
diff --git a/NEWS b/NEWS
index 9de4f256d..4f8081396 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 57497e960..ba1e74e30 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
+@item -C
+@itemx --compare
+@opindex -C
+@opindex --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 9bf9eee0f..669fbea7a 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"
@@ -125,6 +126,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 +149,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 +172,107 @@ 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)
+{
+ enum { CMP_BLOCK_SIZE = 4096 };
+ static char a_buff[CMP_BLOCK_SIZE];
+ static char b_buff[CMP_BLOCK_SIZE];
+
+ size_t size;
+ while (0 < (size = full_read (a_fd, a_buff, sizeof a_buff))) {
+ if (size != full_read (b_fd, b_buff, sizeof b_buff))
+ return false;
+
+ if (memcmp (a_buff, b_buff, size) != 0)
+ return false;
+ }
+
+ return size == 0;
+}
+
+/* Return true for mode with non-permission bits. */
+static bool
+extra_mode (mode_t input)
+{
+ const mode_t mask = ~S_IRWXUGO & ~S_IFMT;
+ return input & mask;
+}
+
+/* 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 +467,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 +479,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 +638,24 @@ 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 --compare (-C) and --preserve-timestamps are "
+ "mutually exclusive"));
+ usage (EXIT_FAILURE);
+ }
+
+ if (copy_only_if_needed && strip_files)
+ {
+ error (0, 0, _("options --compare (-C) and --strip are mutually "
+ "exclusive"));
+ usage (EXIT_FAILURE);
+ }
+
+ if (copy_only_if_needed && extra_mode (mode))
+ error (0, 0, _("the --compare (-C) option is ignored when you"
+ " specify a mode with non-permission bits"));
+
get_ids ();
if (dir_arg)
@@ -645,6 +772,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 +965,8 @@ 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 compare each pair of source and destination files, and\n\
+ in some cases, do not modify the destination at all\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 024eb48c6..07e947341 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 000000000..129286d2d
--- /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 <http://www.gnu.org/licenses/>.
+
+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 000000000..1a07dbe3d
--- /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 <http://www.gnu.org/licenses/>.
+
+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 000000000..d1d954085
--- /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 <http://www.gnu.org/licenses/>.
+
+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