Subject: bug#8374: cp -a [-l] sometimes does not preserve timestamps of symlinks

From: Pádraig Brady <[email-masked]>
Date: Fri, 01 Apr 2011 12:08:55 +0100

On 31/03/11 15:11, Pádraig Brady wrote:
> On 29/03/11 14:46, Ruediger Meier wrote:
>> Hi,
>>
>>
>> I see you fixed that already for for cp -a
>> http://marc.info/?t=124897089600001&r=1&w=2
>>
>> But it does not together with option -link:
>>
>> cd /tmp/
>> ln -s somewhere symlink
>> touch -h -t "197001010000"  symlink
>> cp -a symlink symlink-a
>> cp -al symlink symlink-al
>> ls -l symlink*
>> lrwxrwxrwx 1 rudi users 9 1970-01-01 00:00 symlink -> somewhere
>> lrwxrwxrwx 1 rudi users 9 1970-01-01 00:00 symlink-a -> somewhere
>> lrwxrwxrwx 1 rudi users 9 2011-03-29 13:44 symlink-al -> somewhere
>>
>>
>> I'm using core utils coreutils 8.9 (opensuse 11.4).
>>
>> BTW with coreutils 7.1 and 6.9 it behaves the other way, -al is correct 
>> but -a not.
>>
>> So probably -al is broken since you fixed -a in 7.5.
> 
> Hmm it looks now like we're creating symlinks (with wrong timestamps),
> but in fact we should be creating hardlinks to symlinks.
> 
> This seems to have been changed with:
> http://git.sv.gnu.org/gitweb/?p=coreutils.git;a=commit;h=3346c0af

That commit didn't actually change the logic as far as I can see.
It was actually ca9e212c that introduced the logic bug I think.

So I'm going to apply the attached.

cheers,
Pádraig.
>From 533b834ab52682945569e1bc1e61624a09a34273 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email-masked]>
Date: Thu, 31 Mar 2011 11:28:58 +0100
Subject: [PATCH] copy: link rather than copy symlinks, when --link used

This bug was introduced in commit ca9e212c, 2009-09-24,
"cp, mv: use linkat to guarantee semantics", which
inadvertently disabled the creation of hardlinks to symlinks.
However rather than implementing the intention of that commit
and relying on gnulib linkat emulation, we'll revert to the
previous emulation as that maintains ownership and timestamps.

* src/copy.c (copy_internal): Use our existing hardlink to
symlink emulation when we're not sure that link() will
not dereference the symlink.  Also ensure that we copy the
timestamps of the original symlink when we use the emulation.
* tests/cp/link-symlink: Add a test to ensure timestamps copied.
* tests/Makefile.am: Reference the new test.
* NEWS: Mention the fix.
Reported by Ruediger Meier
---
 NEWS                  |    4 ++++
 src/copy.c            |   27 ++++++++++++++++++++-------
 tests/Makefile.am     |    1 +
 tests/cp/link-symlink |   41 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 66 insertions(+), 7 deletions(-)
 create mode 100755 tests/cp/link-symlink

diff --git a/NEWS b/NEWS
index 6d66355..f25a696 100644
--- a/NEWS
+++ b/NEWS
@@ -20,6 +20,10 @@ GNU coreutils NEWS                                    -*- outline -*-
   wc would dereference a NULL pointer upon an early out-of-memory error
   [bug introduced in coreutils-7.1]
 
+  cp -a --link would not create a hardlink to a symlink, instead
+  copying the symlink and then not preserving its timestamp.
+  [bug introduced in coreutils-8.0]
+
 ** New features
 
   dd now accepts the 'nocache' flag to the iflag and oflag options,
diff --git a/src/copy.c b/src/copy.c
index 104652d..e839f97 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -2169,13 +2169,24 @@ copy_internal (char const *src_name, char const *dst_name,
         }
     }
 
-  /* cp, invoked with `--link --no-dereference', should not follow the
-     link; we guarantee this with gnulib's linkat module (on systems
-     where link(2) follows the link, gnulib creates a symlink with
-     identical contents, which is good enough for our purposes).  */
+  /* POSIX 2008 states that it is implementation-defined whether
+     link() on a symlink creates a hard-link to the symlink, or only
+     to the referent (effectively dereferencing the symlink) (POSIX
+     2001 required the latter behavior, although many systems provided
+     the former).  Yet cp, invoked with `--link --no-dereference',
+     should not follow the link.  We can approximate the desired
+     behavior by skipping this hard-link creating block and instead
+     copying the symlink, via the `S_ISLNK'- copying code below.
+     LINK_FOLLOWS_SYMLINKS is tri-state; if it is -1, we don't know
+     how link() behaves, so we use the fallback case for safety.
+
+     Note gnulib's linkat module, guarantees that the symlink is not
+     dereferenced.  However its emulation currently doesn't maintain
+     timestamps or ownership so we only call it when we know the
+     emulation will not be needed.  */
   else if (x->hard_link
-           && (!S_ISLNK (src_mode)
-               || x->dereference != DEREF_NEVER))
+           && !(LINK_FOLLOWS_SYMLINKS && S_ISLNK (src_mode)
+                && x->dereference == DEREF_NEVER))
     {
        if (linkat (AT_FDCWD, src_name, AT_FDCWD, dst_name, 0))
         {
@@ -2298,7 +2309,9 @@ copy_internal (char const *src_name, char const *dst_name,
 
   /* If we've just created a hard-link due to cp's --link option,
      we're done.  */
-  if (x->hard_link && ! S_ISDIR (src_mode))
+  if (x->hard_link && ! S_ISDIR (src_mode)
+      && !(LINK_FOLLOWS_SYMLINKS && S_ISLNK (src_mode)
+           && x->dereference == DEREF_NEVER))
     return delayed_ok;
 
   if (copied_as_regular)
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 99b46e8..e7f3fff 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -328,6 +328,7 @@ TESTS =						\
   cp/link					\
   cp/link-no-deref				\
   cp/link-preserve				\
+  cp/link-symlink				\
   cp/no-deref-link1				\
   cp/no-deref-link2				\
   cp/no-deref-link3				\
diff --git a/tests/cp/link-symlink b/tests/cp/link-symlink
new file mode 100755
index 0000000..443b337
--- /dev/null
+++ b/tests/cp/link-symlink
@@ -0,0 +1,41 @@
+#!/bin/sh
+# Ensure that cp -a --link maintains timestamps if possible
+
+# Copyright (C) 2011 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/>.
+
+. "${srcdir=.}/init.sh"; path_prepend_ ../src
+print_ver_ cp
+
+# Check that the timestamps of the symlink are copied
+# if we're using hardlink to symlink emulation.
+touch file
+ln -s file link || framework_failure
+touch -m -h -d 2011-01-01 link ||
+  skip_test_ "Your system doesn't support updating symlink timestamps"
+case `stat --format=%y link` in
+  2011-01-01*) ;;
+  *) skip_test_ "Your system doesn't support updating symlink timestamps" ;;
+esac
+
+# link.cp is probably a hardlink, but may also be a symlink
+# In either case the timestamp should match the original.
+cp -al link link.cp
+case `stat --format=%y link.cp` in
+  2011-01-01*) ;;
+  *) fail=1 ;;
+esac
+
+Exit $fail
-- 
1.7.4