From 9d4ae524b93e3bb2f8cb2c99e22f3f192e8dfae8 Mon Sep 17 00:00:00 2001 From: Kamil Dudka Date: Fri, 16 Jan 2009 11:58:26 +0100 Subject: [PATCH] install: add -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 -C. (copy_file): Skip file copying if not necessary. (usage): Show new option -C in --help. * tests/install/install-C: Basic tests for install -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 -C. * doc/coreutils.texi: Document new install option -C. * NEWS: Mention the change. --- NEWS | 3 + doc/coreutils.texi | 5 ++ src/install.c | 112 ++++++++++++++++++++++++++++++++++++++- tests/Makefile.am | 3 + tests/install/install-C | 75 ++++++++++++++++++++++++++ tests/install/install-C-root | 64 ++++++++++++++++++++++ tests/install/install-C-selinux | 56 +++++++++++++++++++ 7 files changed, 317 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 f1b383e..05d6644 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,9 @@ 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 -C installs file, unless target already exists and is the same file, + in which case the modification time is not changed + 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 d8df107..b1c22e0 100644 --- a/doc/coreutils.texi +++ b/doc/coreutils.texi @@ -2123,6 +2123,11 @@ The program accepts the following options. Also see @ref{Common options}. @table @samp address@hidden -C address@hidden -C +Install file, unless target already exists and is the same file, in which +case the modification time is not changed. + @item -c @itemx --crown-margin @opindex -c diff --git a/src/install.c b/src/install.c index 9dda05a..03c849a 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; @@ -167,6 +171,90 @@ 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 FILE to destination TO is necessary. */ +static bool +need_copy (const char *file, const char *to, const struct cp_options *x) +{ + struct stat file_s, to_s; + int file_fd, to_fd; + bool match; + security_context_t file_scontext = NULL; + security_context_t to_scontext = NULL; + + /* compare files using stat */ + if (stat (file, &file_s) != 0) + return true; + + if (stat (to, &to_s) != 0) + return true; + + if (file_s.st_size != to_s.st_size + || (to_s.st_mode & CHMOD_MODE_BITS) != mode + || (owner_id != (uid_t) -1 && to_s.st_uid != owner_id) + || (group_id != (gid_t) -1 && to_s.st_gid != group_id)) + return true; + + /* compare SELinux context if preserving */ + if (selinux_enabled && x->preserve_security_context) + { + if (getfilecon (file, &file_scontext) == -1) + return true; + + if (getfilecon (to, &to_scontext) == -1) + { + freecon (file_scontext); + return true; + } + + match = strcmp (file_scontext, to_scontext) == 0; + + freecon (file_scontext); + freecon (to_scontext); + if (!match) + return true; + } + + /* compare files content */ + file_fd = open (file, O_RDONLY); + if (file_fd < 0) + return true; + + to_fd = open (to, O_RDONLY); + if (to_fd < 0) + { + close (file_fd); + return true; + } + + match = have_same_content (file_fd, to_fd); + + close (file_fd); + close (to_fd); + return !match; +} + static void cp_option_init (struct cp_options *x) { @@ -360,7 +448,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) @@ -372,6 +460,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 @@ -528,6 +619,19 @@ 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); + } + get_ids (); if (dir_arg) @@ -644,6 +748,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. @@ -834,6 +941,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 Install file, unless target already exists and is\n\ + the same as the new file, in which case the modification\n\ + 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 6dce9cd..fe53286 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -24,6 +24,7 @@ root_tests = \ cp/cp-a-selinux \ cp/preserve-gid \ cp/special-bits \ + install/install-C-root \ ls/capability \ ls/nameless-uid \ misc/chcon \ @@ -314,6 +315,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..cd7ebf5 --- /dev/null +++ b/tests/install/install-C @@ -0,0 +1,75 @@ +#!/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 + +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 but -C is not given +ginstall -v -m$mode1 a b > out || fail=1 +compare out out_installed_second || 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..f2c7c2f --- /dev/null +++ b/tests/install/install-C-root @@ -0,0 +1,64 @@ +#!/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 + +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.5.4.3