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
@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
#include "cp-hash.h"
#include "copy.h"
#include "filenamecat.h"
+#include "full-read.h"
#include "mkancesdirs.h"
#include "mkdir-p.h"
#include "modechange.h"
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;
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'},
{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)
{
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)
break;
case 'c':
break;
+ case 'C':
+ copy_only_if_needed = true;
+ break;
case 's':
strip_files = true;
#ifdef SIGCHLD
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)
{
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.
--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);
cp/preserve-gid \
cp/special-bits \
dd/skip-seek-past-dev \
+ install/install-C-root \
ls/capability \
ls/nameless-uid \
misc/chcon \
install/basic-1 \
install/create-leading \
install/d-slashdot \
+ install/install-C \
+ install/install-C-selinux \
install/strip-program \
install/trap \
ln/backup-1 \
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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