utimens: add lutimens interface
authorEric Blake <ebb9@byu.net>
Wed, 7 Oct 2009 19:36:45 +0000 (13:36 -0600)
committerEric Blake <ebb9@byu.net>
Sat, 10 Oct 2009 14:29:53 +0000 (08:29 -0600)
Wraps utimensat(,AT_SYMLINK_NOFOLLOW) or lutimes, when supported;
otherwise fail with ENOSYS.  Allows coreutils' copy.c to preserve
symlink timestamps on more systems.  Note that cygwin's lstat
changes atime of symlinks, but mtime can reliably be set.

* lib/utimens.c (lutimens): New function.
* m4/utimens.m4 (gl_UTIMENS): Check for lutimes.
* lib/utimens.h (lutimens): Declare new interface.
* tests/test-utimens.c (main): Enhance test.
* tests/test-lutimens.h (test_lutimens): New file.
* modules/utimens-tests (Files): Distribute it.
(Depends-on): Add symlink.
(configure.ac): Check for usleep.

Signed-off-by: Eric Blake <ebb9@byu.net>
ChangeLog
lib/utimens.c
lib/utimens.h
m4/utimens.m4
modules/utimens-tests
tests/test-lutimens.h [new file with mode: 0644]
tests/test-utimens.c

index ffbfe70..39cc9bd 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,15 @@
 2009-10-10  Eric Blake  <ebb9@byu.net>
 
+       utimens: add lutimens interface
+       * lib/utimens.c (lutimens): New function.
+       * m4/utimens.m4 (gl_UTIMENS): Check for lutimes.
+       * lib/utimens.h (lutimens): Declare new interface.
+       * tests/test-utimens.c (main): Enhance test.
+       * tests/test-lutimens.h (test_lutimens): New file.
+       * modules/utimens-tests (Files): Distribute it.
+       (Depends-on): Add symlink.
+       (configure.ac): Check for usleep.
+
        utimens: validate futimens usage
        * lib/utimens.c (gl_futimens): Require valid fd up front, using
        fewer syscalls on failure later on.  Avoid compiler warning on
index b1ac350..eb5102e 100644 (file)
@@ -207,3 +207,63 @@ utimens (char const *file, struct timespec const timespec[2])
 {
   return gl_futimens (-1, file, timespec);
 }
+
+/* Set the access and modification time stamps of the symlink FILE to
+   be TIMESPEC[0] and TIMESPEC[1], respectively.  Fail with ENOSYS if
+   the platform does not support changing symlink timestamps.  */
+int
+lutimens (char const *file _UNUSED_PARAMETER_,
+          struct timespec const timespec[2] _UNUSED_PARAMETER_)
+{
+  /* The Linux kernel did not support symlink timestamps until
+     utimensat, in version 2.6.22, so we don't need to mimic
+     gl_futimens' worry about buggy NFS clients.  But we do have to
+     worry about bogus return values.  */
+
+#if HAVE_UTIMENSAT
+  {
+    int result = utimensat (AT_FDCWD, file, timespec, AT_SYMLINK_NOFOLLOW);
+# ifdef __linux__
+    /* Work around a kernel bug:
+       http://bugzilla.redhat.com/442352
+       http://bugzilla.redhat.com/449910
+       It appears that utimensat can mistakenly return 280 rather
+       than -1 upon ENOSYS failure.
+       FIXME: remove in 2010 or whenever the offending kernels
+       are no longer in common use.  */
+    if (0 < result)
+      errno = ENOSYS;
+# endif
+
+    if (result == 0 || errno != ENOSYS)
+      return result;
+  }
+#endif /* HAVE_UTIMENSAT */
+
+  /* The platform lacks an interface to set file timestamps with
+     nanosecond resolution, so do the best we can, discarding any
+     fractional part of the timestamp.  */
+#if HAVE_LUTIMES
+  {
+    struct timeval timeval[2];
+    struct timeval const *t;
+    if (timespec)
+      {
+        timeval[0].tv_sec = timespec[0].tv_sec;
+        timeval[0].tv_usec = timespec[0].tv_nsec / 1000;
+        timeval[1].tv_sec = timespec[1].tv_sec;
+        timeval[1].tv_usec = timespec[1].tv_nsec / 1000;
+        t = timeval;
+      }
+    else
+      t = NULL;
+
+    return lutimes (file, t);
+  }
+#endif /* HAVE_LUTIMES */
+
+  /* Out of luck.  Symlink timestamps can't be changed.  We won't
+     bother changing the timestamps if FILE was not a symlink.  */
+  errno = ENOSYS;
+  return -1;
+}
index 169521d..8b3ccef 100644 (file)
@@ -1,3 +1,4 @@
 #include <time.h>
 int gl_futimens (int, char const *, struct timespec const [2]);
 int utimens (char const *, struct timespec const [2]);
+int lutimens (char const *, struct timespec const [2]);
index 01a3184..381d087 100644 (file)
@@ -1,10 +1,10 @@
-dnl Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008 Free Software
-dnl Foundation, Inc.
+dnl Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008, 2009 Free
+dnl Software Foundation, Inc.
 dnl This file is free software; the Free Software Foundation
 dnl gives unlimited permission to copy and/or distribute it,
 dnl with or without modifications, as long as this notice is preserved.
 
-dnl serial 2
+dnl serial 3
 
 AC_DEFUN([gl_UTIMENS],
 [
@@ -14,5 +14,5 @@ AC_DEFUN([gl_UTIMENS],
   AC_REQUIRE([gl_FUNC_UTIMES])
   AC_REQUIRE([gl_CHECK_TYPE_STRUCT_TIMESPEC])
   AC_REQUIRE([gl_CHECK_TYPE_STRUCT_UTIMBUF])
-  AC_CHECK_FUNCS_ONCE([futimes futimesat futimens utimensat])
+  AC_CHECK_FUNCS_ONCE([futimes futimesat futimens utimensat lutimes])
 ])
index 9163b04..85b9f65 100644 (file)
@@ -1,15 +1,18 @@
 Files:
 tests/test-futimens.h
+tests/test-lutimens.h
 tests/test-utimens.h
 tests/test-utimens.c
 
 Depends-on:
 stat-time
 stdbool
+symlink
 timespec
 utimecmp
 
 configure.ac:
+AC_CHECK_FUNCS_ONCE([usleep])
 
 Makefile.am:
 TESTS += test-utimens
diff --git a/tests/test-lutimens.h b/tests/test-lutimens.h
new file mode 100644 (file)
index 0000000..18cf75f
--- /dev/null
@@ -0,0 +1,152 @@
+/* Test of file timestamp modification functions.
+   Copyright (C) 2009 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 2 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/>.  */
+
+/* This file assumes that BASE and ASSERT are already defined.  */
+
+#ifndef GL_TEST_UTIMENS
+# define GL_TEST_UTIMENS
+
+#include <fcntl.h>
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "stat-time.h"
+#include "timespec.h"
+#include "utimecmp.h"
+
+enum {
+  BILLION = 1000 * 1000 * 1000,
+
+  Y2K = 946684800, /* Jan 1, 2000, in seconds since epoch.  */
+
+  /* Bogus positive and negative tv_nsec values closest to valid
+     range, but without colliding with UTIME_NOW or UTIME_OMIT.  */
+  UTIME_BOGUS_POS = BILLION + ((UTIME_NOW == BILLION || UTIME_OMIT == BILLION)
+                               ? (1 + (UTIME_NOW == BILLION + 1)
+                                  + (UTIME_OMIT == BILLION + 1))
+                               : 0),
+  UTIME_BOGUS_NEG = -1 - ((UTIME_NOW == -1 || UTIME_OMIT == -1)
+                          ? (1 + (UTIME_NOW == -2) + (UTIME_OMIT == -2))
+                          : 0)
+};
+
+#endif /* GL_TEST_UTIMENS */
+
+/* This function is designed to test both lutimens(a,b) and
+   utimensat(AT_FDCWD,a,b,AT_SYMLINK_NOFOLLOW).  FUNC is the function
+   to test.  If PRINT, warn before skipping tests with status 77.  */
+static int
+test_lutimens (int (*func) (char const *, struct timespec const *), bool print)
+{
+  int result;
+  struct stat st1;
+  struct stat st2;
+  bool atime_supported = true;
+
+  if (symlink ("nowhere", BASE "link"))
+    {
+      if (print)
+        fputs ("skipping test: symlinks not supported on this file system\n",
+               stderr);
+      return 77;
+    }
+  errno = 0;
+  result = func (BASE "link", NULL);
+  if (result == -1 && errno == ENOSYS)
+    {
+      ASSERT (unlink (BASE "link") == 0);
+      if (print)
+        fputs ("skipping test: "
+               "setting symlink time not supported on this file system\n",
+               stderr);
+      return 77;
+    }
+  ASSERT (!result);
+  ASSERT (lstat (BASE "link", &st1) == 0);
+#if HAVE_USLEEP
+  /* On Cygwin, the mere act of lstat changes symlink atime, even
+     though POSIX says that only readlink is allowed to do that.
+     Sleeping for one millisecond is enough to expose this.  Platforms
+     without usleep either don't have symlinks, or are immune.  */
+  usleep (1000);
+#endif
+  ASSERT (lstat (BASE "link", &st2) == 0);
+  if (st1.st_atime != st2.st_atime
+      || get_stat_atime_ns (&st1) != get_stat_atime_ns (&st2))
+     atime_supported = false;
+
+  /* Invalid arguments.  */
+  errno = 0;
+  ASSERT (func ("no_such", NULL) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func ("", NULL) == -1);
+  ASSERT (errno == ENOENT);
+  {
+    struct timespec ts[2] = { { Y2K, UTIME_BOGUS_POS }, { Y2K, 0 } };
+    errno = 0;
+    ASSERT (func (BASE "link", ts) == -1);
+    ASSERT (errno == EINVAL);
+  }
+  {
+    struct timespec ts[2] = { { Y2K, 0 }, { Y2K, UTIME_BOGUS_NEG } };
+    errno = 0;
+    ASSERT (func (BASE "link", ts) == -1);
+    ASSERT (errno == EINVAL);
+  }
+  ASSERT (lstat (BASE "link", &st2) == 0);
+  if (atime_supported)
+    {
+      ASSERT (st1.st_atime == st2.st_atime);
+      ASSERT (get_stat_atime_ns (&st1) == get_stat_atime_ns (&st2));
+    }
+  ASSERT (utimecmp (BASE "link", &st1, &st2, 0) == 0);
+
+  /* Set both times.  */
+  {
+    struct timespec ts[2] = { { Y2K, BILLION / 2 - 1 }, { Y2K, BILLION - 1 } };
+    ASSERT (func (BASE "link", ts) == 0);
+    ASSERT (lstat (BASE "link", &st2) == 0);
+    if (atime_supported)
+      {
+        ASSERT (st2.st_atime == Y2K);
+        ASSERT (0 <= get_stat_atime_ns (&st2));
+        ASSERT (get_stat_atime_ns (&st2) < BILLION / 2);
+      }
+    ASSERT (st2.st_mtime == Y2K);
+    ASSERT (0 <= get_stat_mtime_ns (&st2));
+    ASSERT (get_stat_mtime_ns (&st2) < BILLION);
+  }
+
+  /* Play with UTIME_OMIT, UTIME_NOW.  */
+  {
+    struct timespec ts[2] = { { BILLION, UTIME_OMIT }, { 0, UTIME_NOW } };
+    ASSERT (func (BASE "link", ts) == 0);
+    ASSERT (lstat (BASE "link", &st2) == 0);
+    if (atime_supported)
+      {
+        ASSERT (st2.st_atime == Y2K);
+        ASSERT (0 <= get_stat_atime_ns (&st2));
+        ASSERT (get_stat_atime_ns (&st2) < BILLION / 2);
+      }
+    ASSERT (utimecmp (BASE "link", &st1, &st2, 0) <= 0);
+  }
+
+  /* Cleanup.  */
+  ASSERT (unlink (BASE "link") == 0);
+  return 0;
+}
index 5734a57..0b00fad 100644 (file)
@@ -39,6 +39,7 @@
 #define BASE "test-utimens.t"
 
 #include "test-futimens.h"
+#include "test-lutimens.h"
 #include "test-utimens.h"
 
 /* Wrap gl_futimens to behave like futimens.  */
@@ -70,10 +71,20 @@ do_fdutimens (char const *name, struct timespec const times[2])
 int
 main ()
 {
+  int result1;
+  int result2;
+
   /* Clean up any trash from prior testsuite runs.  */
   ASSERT (system ("rm -rf " BASE "*") == 0);
 
   ASSERT (test_utimens (utimens) == 0);
   ASSERT (test_utimens (do_fdutimens) == 0);
-  return test_futimens (do_futimens, true);
+  result1 = test_futimens (do_futimens, true);
+  if (result1)
+    ASSERT (result1 == 77);
+  /* Print only one skip message.  */
+  result2 = test_lutimens (lutimens, result1 == 0);
+  if (result2)
+    ASSERT (result2 == 77);
+  return result1 | result2;
 }