frexpl: Update regarding AIX.
[gnulib.git] / lib / utimens.c
index 10d2c82..35d082d 100644 (file)
@@ -1,7 +1,6 @@
 /* Set file access and modification times.
 
-   Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008, 2009 Free
-   Software Foundation, Inc.
+   Copyright (C) 2003-2010 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
 
 #include "utimens.h"
 
+#include <assert.h>
 #include <errno.h>
 #include <fcntl.h>
+#include <stdbool.h>
 #include <sys/stat.h>
 #include <sys/time.h>
 #include <unistd.h>
 
+#include "stat-time.h"
+#include "timespec.h"
+
 #if HAVE_UTIME_H
 # include <utime.h>
 #endif
@@ -44,6 +48,111 @@ struct utimbuf
 };
 #endif
 
+/* Avoid recursion with rpl_futimens or rpl_utimensat.  */
+#undef futimens
+#undef utimensat
+
+/* Solaris 9 mistakenly succeeds when given a non-directory with a
+   trailing slash.  Force the use of rpl_stat for a fix.  */
+#ifndef REPLACE_FUNC_STAT_FILE
+# define REPLACE_FUNC_STAT_FILE 0
+#endif
+
+#if HAVE_UTIMENSAT || HAVE_FUTIMENS
+/* Cache variables for whether the utimensat syscall works; used to
+   avoid calling the syscall if we know it will just fail with ENOSYS,
+   and to avoid unnecessary work in massaging timestamps if the
+   syscall will work.  Multiple variables are needed, to distinguish
+   between the following scenarios on Linux:
+   utimensat doesn't exist, or is in glibc but kernel 2.6.18 fails with ENOSYS
+   kernel 2.6.22 and earlier rejects AT_SYMLINK_NOFOLLOW
+   kernel 2.6.25 and earlier reject UTIME_NOW/UTIME_OMIT with non-zero tv_sec
+   kernel 2.6.32 used with xfs or ntfs-3g fail to honor UTIME_OMIT
+   utimensat completely works
+   For each cache variable: 0 = unknown, 1 = yes, -1 = no.  */
+static int utimensat_works_really;
+static int lutimensat_works_really;
+#endif /* HAVE_UTIMENSAT || HAVE_FUTIMENS */
+
+/* Validate the requested timestamps.  Return 0 if the resulting
+   timespec can be used for utimensat (after possibly modifying it to
+   work around bugs in utimensat).  Return a positive value if the
+   timespec needs further adjustment based on stat results: 1 if any
+   adjustment is needed for utimes, and 2 if any adjustment is needed
+   for Linux utimensat.  Return -1, with errno set to EINVAL, if
+   timespec is out of range.  */
+static int
+validate_timespec (struct timespec timespec[2])
+{
+  int result = 0;
+  int utime_omit_count = 0;
+  assert (timespec);
+  if ((timespec[0].tv_nsec != UTIME_NOW
+       && timespec[0].tv_nsec != UTIME_OMIT
+       && (timespec[0].tv_nsec < 0 || 1000000000 <= timespec[0].tv_nsec))
+      || (timespec[1].tv_nsec != UTIME_NOW
+          && timespec[1].tv_nsec != UTIME_OMIT
+          && (timespec[1].tv_nsec < 0 || 1000000000 <= timespec[1].tv_nsec)))
+    {
+      errno = EINVAL;
+      return -1;
+    }
+  /* Work around Linux kernel 2.6.25 bug, where utimensat fails with
+     EINVAL if tv_sec is not 0 when using the flag values of tv_nsec.
+     Flag a Linux kernel 2.6.32 bug, where an mtime of UTIME_OMIT
+     fails to bump ctime.  */
+  if (timespec[0].tv_nsec == UTIME_NOW
+      || timespec[0].tv_nsec == UTIME_OMIT)
+    {
+      timespec[0].tv_sec = 0;
+      result = 1;
+      if (timespec[0].tv_nsec == UTIME_OMIT)
+        utime_omit_count++;
+    }
+  if (timespec[1].tv_nsec == UTIME_NOW
+      || timespec[1].tv_nsec == UTIME_OMIT)
+    {
+      timespec[1].tv_sec = 0;
+      result = 1;
+      if (timespec[1].tv_nsec == UTIME_OMIT)
+        utime_omit_count++;
+    }
+  return result + (utime_omit_count == 1);
+}
+
+/* Normalize any UTIME_NOW or UTIME_OMIT values in *TS, using stat
+   buffer STATBUF to obtain the current timestamps of the file.  If
+   both times are UTIME_NOW, set *TS to NULL (as this can avoid some
+   permissions issues).  If both times are UTIME_OMIT, return true
+   (nothing further beyond the prior collection of STATBUF is
+   necessary); otherwise return false.  */
+static bool
+update_timespec (struct stat const *statbuf, struct timespec *ts[2])
+{
+  struct timespec *timespec = *ts;
+  if (timespec[0].tv_nsec == UTIME_OMIT
+      && timespec[1].tv_nsec == UTIME_OMIT)
+    return true;
+  if (timespec[0].tv_nsec == UTIME_NOW
+      && timespec[1].tv_nsec == UTIME_NOW)
+    {
+      *ts = NULL;
+      return false;
+    }
+
+  if (timespec[0].tv_nsec == UTIME_OMIT)
+    timespec[0] = get_stat_atime (statbuf);
+  else if (timespec[0].tv_nsec == UTIME_NOW)
+    gettime (&timespec[0]);
+
+  if (timespec[1].tv_nsec == UTIME_OMIT)
+    timespec[1] = get_stat_mtime (statbuf);
+  else if (timespec[1].tv_nsec == UTIME_NOW)
+    gettime (&timespec[1]);
+
+  return false;
+}
+
 /* Set the access and modification time stamps of FD (a.k.a. FILE) to be
    TIMESPEC[0] and TIMESPEC[1], respectively.
    FD must be either negative -- in which case it is ignored --
@@ -55,9 +164,36 @@ struct utimbuf
    Return 0 on success, -1 (setting errno) on failure.  */
 
 int
-gl_futimens (int fd _UNUSED_PARAMETER_,
-             char const *file, struct timespec const timespec[2])
+fdutimens (char const *file, int fd, struct timespec const timespec[2])
 {
+  struct timespec adjusted_timespec[2];
+  struct timespec *ts = timespec ? adjusted_timespec : NULL;
+  int adjustment_needed = 0;
+  struct stat st;
+
+  if (ts)
+    {
+      adjusted_timespec[0] = timespec[0];
+      adjusted_timespec[1] = timespec[1];
+      adjustment_needed = validate_timespec (ts);
+    }
+  if (adjustment_needed < 0)
+    return -1;
+
+  /* Require that at least one of FD or FILE are valid.  Works around
+     a Linux bug where futimens (AT_FDCWD, NULL) changes "." rather
+     than failing.  */
+  if (!file)
+    {
+      if (fd < 0)
+        {
+          errno = EBADF;
+          return -1;
+        }
+      if (dup2 (fd, fd) != fd)
+        return -1;
+    }
+
   /* Some Linux-based NFS clients are buggy, and mishandle time stamps
      of files in NFS file systems in some cases.  We have no
      configure-time test for this, but please see
@@ -75,56 +211,102 @@ gl_futimens (int fd _UNUSED_PARAMETER_,
     fsync (fd);
 #endif
 
-  /* POSIX 200x added two interfaces to set file timestamps with
-     nanosecond resolution.  We provide a fallback for ENOSYS (for
-     example, compiling against Linux 2.6.25 kernel headers and glibc
-     2.7, but running on Linux 2.6.18 kernel).  */
-#if HAVE_UTIMENSAT
-  if (fd < 0)
+  /* POSIX 2008 added two interfaces to set file timestamps with
+     nanosecond resolution; newer Linux implements both functions via
+     a single syscall.  We provide a fallback for ENOSYS (for example,
+     compiling against Linux 2.6.25 kernel headers and glibc 2.7, but
+     running on Linux 2.6.18 kernel).  */
+#if HAVE_UTIMENSAT || HAVE_FUTIMENS
+  if (0 <= utimensat_works_really)
     {
-      int result = utimensat (AT_FDCWD, file, timespec, 0);
-# ifdef __linux__
-      /* Work around what might be 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 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;
+      int result;
+# if __linux__
+      /* As recently as Linux kernel 2.6.32 (Dec 2009), several file
+         systems (xfs, ntfs-3g) have bugs with a single UTIME_OMIT,
+         but work if both times are either explicitly specified or
+         UTIME_NOW.  Work around it with a preparatory [f]stat prior
+         to calling futimens/utimensat; fortunately, there is not much
+         timing impact due to the extra syscall even on file systems
+         where UTIME_OMIT would have worked.  FIXME: Simplify this in
+         2012, when file system bugs are no longer common.  */
+      if (adjustment_needed == 2)
+        {
+          if (fd < 0 ? stat (file, &st) : fstat (fd, &st))
+            return -1;
+          if (ts[0].tv_nsec == UTIME_OMIT)
+            ts[0] = get_stat_atime (&st);
+          else if (ts[1].tv_nsec == UTIME_OMIT)
+            ts[1] = get_stat_mtime (&st);
+          /* Note that st is good, in case utimensat gives ENOSYS.  */
+          adjustment_needed++;
+        }
+# endif /* __linux__ */
+# if HAVE_UTIMENSAT
+      if (fd < 0)
+        {
+          result = utimensat (AT_FDCWD, file, ts, 0);
+#  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 /* __linux__ */
+          if (result == 0 || errno != ENOSYS)
+            {
+              utimensat_works_really = 1;
+              return result;
+            }
+        }
+# endif /* HAVE_UTIMENSAT */
+# if HAVE_FUTIMENS
+      if (0 <= fd)
+        {
+          result = futimens (fd, ts);
+#  ifdef __linux__
+          /* Work around the same bug as above.  */
+          if (0 < result)
+            errno = ENOSYS;
+#  endif /* __linux__ */
+          if (result == 0 || errno != ENOSYS)
+            {
+              utimensat_works_really = 1;
+              return result;
+            }
+        }
+# endif /* HAVE_FUTIMENS */
     }
-#endif
-#if HAVE_FUTIMENS
-  {
-    int result = futimens (fd, timespec);
-# ifdef __linux__
-    /* Work around the same bug as above.  */
-    if (0 < result)
-      errno = ENOSYS;
-# endif
-    if (result == 0 || errno != ENOSYS)
-      return result;
-  }
-#endif
+  utimensat_works_really = -1;
+  lutimensat_works_really = -1;
+#endif /* HAVE_UTIMENSAT || HAVE_FUTIMENS */
 
   /* 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 (adjustment_needed || (REPLACE_FUNC_STAT_FILE && fd < 0))
+    {
+      if (adjustment_needed != 3
+          && (fd < 0 ? stat (file, &st) : fstat (fd, &st)))
+        return -1;
+      if (ts && update_timespec (&st, &ts))
+        return 0;
+    }
+
   {
 #if HAVE_FUTIMESAT || HAVE_WORKING_UTIMES
     struct timeval timeval[2];
-    struct timeval const *t;
-    if (timespec)
+    struct timeval *t;
+    if (ts)
       {
-        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;
+        timeval[0].tv_sec = ts[0].tv_sec;
+        timeval[0].tv_usec = ts[0].tv_nsec / 1000;
+        timeval[1].tv_sec = ts[1].tv_sec;
+        timeval[1].tv_usec = ts[1].tv_nsec / 1000;
         t = timeval;
       }
     else
@@ -148,7 +330,7 @@ gl_futimens (int fd _UNUSED_PARAMETER_,
            worth optimizing, and who knows what other messed-up systems
            are out there?  So play it safe and fall back on the code
            below.  */
-# if HAVE_FUTIMESAT
+# if HAVE_FUTIMESAT && !FUTIMESAT_NULL_BUG
         if (futimesat (fd, NULL, t) == 0)
           return 0;
 # elif HAVE_FUTIMES
@@ -160,20 +342,10 @@ gl_futimens (int fd _UNUSED_PARAMETER_,
 
     if (!file)
       {
-#if ! (HAVE_FUTIMESAT || (HAVE_WORKING_UTIMES && HAVE_FUTIMES))
+#if ! ((HAVE_FUTIMESAT && !FUTIMESAT_NULL_BUG)          \
+        || (HAVE_WORKING_UTIMES && HAVE_FUTIMES))
         errno = ENOSYS;
 #endif
-
-        /* Prefer EBADF to ENOSYS if both error numbers apply.  */
-        if (errno == ENOSYS)
-          {
-            int fd2 = dup (fd);
-            int dup_errno = errno;
-            if (0 <= fd2)
-              close (fd2);
-            errno = (fd2 < 0 && dup_errno == EBADF ? EBADF : ENOSYS);
-          }
-
         return -1;
       }
 
@@ -182,11 +354,11 @@ gl_futimens (int fd _UNUSED_PARAMETER_,
 #else
     {
       struct utimbuf utimbuf;
-      struct utimbuf const *ut;
-      if (timespec)
+      struct utimbuf *ut;
+      if (ts)
         {
-          utimbuf.actime = timespec[0].tv_sec;
-          utimbuf.modtime = timespec[1].tv_sec;
+          utimbuf.actime = ts[0].tv_sec;
+          utimbuf.modtime = ts[1].tv_sec;
           ut = &utimbuf;
         }
       else
@@ -198,10 +370,144 @@ gl_futimens (int fd _UNUSED_PARAMETER_,
   }
 }
 
+/* Set the access and modification time stamps of FD (a.k.a. FILE) to be
+   TIMESPEC[0] and TIMESPEC[1], respectively.
+   FD must be either negative -- in which case it is ignored --
+   or a file descriptor that is open on FILE.
+   If FD is nonnegative, then FILE can be NULL, which means
+   use just futimes (or equivalent) instead of utimes (or equivalent),
+   and fail if on an old system without futimes (or equivalent).
+   If TIMESPEC is null, set the time stamps to the current time.
+   Return 0 on success, -1 (setting errno) on failure.  */
+
+int
+gl_futimens (int fd, char const *file, struct timespec const timespec[2])
+{
+  return fdutimens (file, fd, timespec);
+}
+
 /* Set the access and modification time stamps of FILE to be
    TIMESPEC[0] and TIMESPEC[1], respectively.  */
 int
 utimens (char const *file, struct timespec const timespec[2])
 {
-  return gl_futimens (-1, file, timespec);
+  return fdutimens (file, -1, timespec);
+}
+
+/* Set the access and modification time stamps of FILE to be
+   TIMESPEC[0] and TIMESPEC[1], respectively, without dereferencing
+   symlinks.  Fail with ENOSYS if the platform does not support
+   changing symlink timestamps, but FILE was a symlink.  */
+int
+lutimens (char const *file, struct timespec const timespec[2])
+{
+  struct timespec adjusted_timespec[2];
+  struct timespec *ts = timespec ? adjusted_timespec : NULL;
+  int adjustment_needed = 0;
+  struct stat st;
+
+  if (ts)
+    {
+      adjusted_timespec[0] = timespec[0];
+      adjusted_timespec[1] = timespec[1];
+      adjustment_needed = validate_timespec (ts);
+    }
+  if (adjustment_needed < 0)
+    return -1;
+
+  /* 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
+  if (0 <= lutimensat_works_really)
+    {
+      int result;
+# if __linux__
+      /* As recently as Linux kernel 2.6.32 (Dec 2009), several file
+         systems (xfs, ntfs-3g) have bugs with a single UTIME_OMIT,
+         but work if both times are either explicitly specified or
+         UTIME_NOW.  Work around it with a preparatory lstat prior to
+         calling utimensat; fortunately, there is not much timing
+         impact due to the extra syscall even on file systems where
+         UTIME_OMIT would have worked.  FIXME: Simplify this in 2012,
+         when file system bugs are no longer common.  */
+      if (adjustment_needed == 2)
+        {
+          if (lstat (file, &st))
+            return -1;
+          if (ts[0].tv_nsec == UTIME_OMIT)
+            ts[0] = get_stat_atime (&st);
+          else if (ts[1].tv_nsec == UTIME_OMIT)
+            ts[1] = get_stat_mtime (&st);
+          /* Note that st is good, in case utimensat gives ENOSYS.  */
+          adjustment_needed++;
+        }
+# endif /* __linux__ */
+      result = utimensat (AT_FDCWD, file, ts, 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)
+        {
+          utimensat_works_really = 1;
+          lutimensat_works_really = 1;
+          return result;
+        }
+    }
+  lutimensat_works_really = -1;
+#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 (adjustment_needed || REPLACE_FUNC_STAT_FILE)
+    {
+      if (adjustment_needed != 3 && lstat (file, &st))
+        return -1;
+      if (ts && update_timespec (&st, &ts))
+        return 0;
+    }
+
+  /* On Linux, lutimes is a thin wrapper around utimensat, so there is
+     no point trying lutimes if utimensat failed with ENOSYS.  */
+#if HAVE_LUTIMES && !HAVE_UTIMENSAT
+  {
+    struct timeval timeval[2];
+    struct timeval *t;
+    int result;
+    if (ts)
+      {
+        timeval[0].tv_sec = ts[0].tv_sec;
+        timeval[0].tv_usec = ts[0].tv_nsec / 1000;
+        timeval[1].tv_sec = ts[1].tv_sec;
+        timeval[1].tv_usec = ts[1].tv_nsec / 1000;
+        t = timeval;
+      }
+    else
+      t = NULL;
+
+    result = lutimes (file, t);
+    if (result == 0 || errno != ENOSYS)
+      return result;
+  }
+#endif /* HAVE_LUTIMES && !HAVE_UTIMENSAT */
+
+  /* Out of luck for symlinks, but we still handle regular files.  */
+  if (!(adjustment_needed || REPLACE_FUNC_STAT_FILE) && lstat (file, &st))
+    return -1;
+  if (!S_ISLNK (st.st_mode))
+    return fdutimens (file, -1, ts);
+  errno = ENOSYS;
+  return -1;
 }