# HG changeset patch # User Eric Blake # Date 1255387737 21600 # Node ID 73f2681e0524775850b54ca4a1199b9d8196415f # Parent efdb2ab5743fb713c7af2794e57394b7594e77c8 utimensat: new module Provide utimensat where it is missing, and rpl_utimensat to work around ENOSYS and EINVAL bugs in older Linux kernels. * modules/utimensat: New file. * lib/utimensat.c (utimensat): Likewise. * m4/utimensat.m4 (gl_FUNC_UTIMENSAT): Likewise. * lib/utimens.c (utimensat): Avoid recursion into rpl_utimensat, so we can work around Linux bugs. * m4/sys_stat_h.m4 (gl_SYS_STAT_H_DEFAULTS): Add witnesses. * modules/sys_stat (Makefile.am): Substitute them. * lib/sys_stat.in.h (utimensat): Declare it. * MODULES.html.sh (systems lacking POSIX:2008): Mention module. * doc/posix-functions/utimensat.texi (utimensat): Likewise. * modules/utimensat-tests: New test. * tests/test-utimensat.c: Likewise. Signed-off-by: Eric Blake diff --git a/ChangeLog b/ChangeLog --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,19 @@ 2009-10-16 Eric Blake + utimensat: new module + * modules/utimensat: New file. + * lib/utimensat.c (utimensat): Likewise. + * m4/utimensat.m4 (gl_FUNC_UTIMENSAT): Likewise. + * lib/utimens.c (utimensat): Avoid recursion into rpl_utimensat, + so we can work around Linux bugs. + * m4/sys_stat_h.m4 (gl_SYS_STAT_H_DEFAULTS): Add witnesses. + * modules/sys_stat (Makefile.am): Substitute them. + * lib/sys_stat.in.h (utimensat): Declare it. + * MODULES.html.sh (systems lacking POSIX:2008): Mention module. + * doc/posix-functions/utimensat.texi (utimensat): Likewise. + * modules/utimensat-tests: New test. + * tests/test-utimensat.c: Likewise. + utimens: let lutimens work on non-symlinks * lib/utimens.c (lutimens): Fall back to utimens rather than failing with ENOSYS, when file is not a symlink. diff --git a/MODULES.html.sh b/MODULES.html.sh --- a/MODULES.html.sh +++ b/MODULES.html.sh @@ -2366,6 +2366,7 @@ func_module unistd func_module unlink func_module utime + func_module utimensat func_module vasnprintf-posix func_module vasprintf-posix func_module vfprintf-posix diff --git a/doc/posix-functions/utimensat.texi b/doc/posix-functions/utimensat.texi --- a/doc/posix-functions/utimensat.texi +++ b/doc/posix-functions/utimensat.texi @@ -4,14 +4,10 @@ POSIX specification: @url{http://www.opengroup.org/onlinepubs/9699919799/functions/utimensat.html} -Gnulib module: --- +Gnulib module: utimensat Portability problems fixed by Gnulib: @itemize -@end itemize - -Portability problems not fixed by Gnulib: -@itemize @item This function is missing on some platforms: glibc 2.3.6, MacOS X 10.3, FreeBSD 6.0, NetBSD 3.0, OpenBSD 3.8, AIX @@ -26,10 +22,14 @@ the @code{tv_sec} argument to be 0, and don't necessarily handle all file permissions in the manner required by POSIX: Linux kernel 2.6.25. +@end itemize + +Portability problems not fixed by Gnulib: +@itemize @item On some platforms, timestamps of symbolic links cannot be modified, so -this function fails with @code{ENOSYS} if passed the flag -@code{AT_SYMLINK_NOFOLLOW}. +the replacement fails with @code{ENOSYS} if passed the flag +@code{AT_SYMLINK_NOFOLLOW} on a symlink. @item The mere act of using @code{lstat} modifies the access time of symlinks on some platforms, so @code{utimensat} with diff --git a/lib/sys_stat.in.h b/lib/sys_stat.in.h --- a/lib/sys_stat.in.h +++ b/lib/sys_stat.in.h @@ -468,6 +468,24 @@ #endif +#if @GNULIB_UTIMENSAT@ +# if @REPLACE_UTIMENSAT@ +# undef utimensat +# define utimensat rpl_utimensat +# endif +# if !@HAVE_UTIMENSAT@ || @REPLACE_UTIMENSAT@ + extern int utimensat (int fd, char const *name, + struct timespec const times[2], int flag); +# endif +#elif defined GNULIB_POSIXCHECK +# undef utimensat +# define utimensat(d,n,t,f) \ + (GL_LINK_WARNING ("utimensat is not portable - " \ + "use gnulib module utimensat for portability"), \ + utimensat (d, n, t, f)) +#endif + + #ifdef __cplusplus } #endif diff --git a/lib/utimens.c b/lib/utimens.c --- a/lib/utimens.c +++ b/lib/utimens.c @@ -49,8 +49,9 @@ }; #endif -/* Avoid recursion with rpl_futimens. */ +/* Avoid recursion with rpl_futimens or rpl_utimensat. */ #undef futimens +#undef utimensat #if HAVE_UTIMENSAT || HAVE_FUTIMENS /* Cache variable for whether syscall works; used to avoid calling the diff --git a/lib/utimensat.c b/lib/utimensat.c new file mode 100644 --- /dev/null +++ b/lib/utimensat.c @@ -0,0 +1,102 @@ +/* Set the access and modification time of a file relative to directory fd. + 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 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 . */ + +/* written by Eric Blake */ + +#include + +#include + +#include +#include + +#include "utimens.h" + +#if HAVE_UTIMENSAT + +# undef utimensat + +/* If we have a native utimensat, but are compiling this file, then + utimensat was defined to rpl_utimensat by our replacement + sys/stat.h. We assume the native version might fail with ENOSYS + (as is the case when using newer glibc but older Linux kernel). In + this scenario, rpl_utimensat checks whether the native version is + usable, and local_utimensat provides the fallback manipulation. */ + +static int local_utimensat (int, char const *, struct timespec const[2], int); +# define AT_FUNC_NAME local_utimensat + +/* Like utimensat, but work around native bugs. */ + +int +rpl_utimensat (int fd, char const *file, struct timespec const times[2], + int flag) +{ + static int utimensat_works_really; /* 0 = unknown, 1 = yes, -1 = no. */ + if (0 <= utimensat_works_really) + { + int result = utimensat (fd, file, times, flag); + /* Linux kernel 2.6.25 has a bug where it returns EINVAL for + UTIME_NOW or UTIME_OMIT with non-zero tv_sec, which + local_utimensat works around. Meanwhile, EINVAL for a bad + flag is indeterminate whether the native utimensat works, but + local_utimensat will also reject it. */ + if (result == -1 && errno == EINVAL && (flag & ~AT_SYMLINK_NOFOLLOW)) + return result; + if (result == 0 || (errno != ENOSYS && errno != EINVAL)) + { + utimensat_works_really = 1; + return result; + } + } + /* No point in trying openat/futimens, since on Linux, futimens is + implemented with the same syscall as utimensat. Only avoid the + native utimensat due to an ENOSYS failure; an EINVAL error was + data-dependent, and the next caller may pass valid data. */ + if (0 <= utimensat_works_really && errno == ENOSYS) + utimensat_works_really = -1; + return local_utimensat (fd, file, times, flag); +} + +#else /* !HAVE_UTIMENSAT */ + +# define AT_FUNC_NAME utimensat + +#endif /* !HAVE_UTIMENSAT */ + +/* Set the access and modification time stamps of FILE to be + TIMESPEC[0] and TIMESPEC[1], respectively; relative to directory + FD. If flag is AT_SYMLINK_NOFOLLOW, change the times of a symlink, + or fail with ENOSYS if not possible. If TIMESPEC is null, set the + time stamps to the current time. If possible, do it without + changing the working directory. Otherwise, resort to using + save_cwd/fchdir, then utimens/restore_cwd. If either the save_cwd + or the restore_cwd fails, then give a diagnostic and exit nonzero. + Return 0 on success, -1 (setting errno) on failure. */ + +/* AT_FUNC_NAME is now utimensat or local_utimensat. */ +#define AT_FUNC_F1 lutimens +#define AT_FUNC_F2 utimens +#define AT_FUNC_USE_F1_COND AT_SYMLINK_NOFOLLOW +#define AT_FUNC_POST_FILE_PARAM_DECLS , struct timespec const ts[2], int flag +#define AT_FUNC_POST_FILE_ARGS , ts +#include "at-func.c" +#undef AT_FUNC_NAME +#undef AT_FUNC_F1 +#undef AT_FUNC_F2 +#undef AT_FUNC_USE_F1_COND +#undef AT_FUNC_POST_FILE_PARAM_DECLS +#undef AT_FUNC_POST_FILE_ARGS diff --git a/m4/sys_stat_h.m4 b/m4/sys_stat_h.m4 --- a/m4/sys_stat_h.m4 +++ b/m4/sys_stat_h.m4 @@ -1,4 +1,4 @@ -# sys_stat_h.m4 serial 18 -*- Autoconf -*- +# sys_stat_h.m4 serial 19 -*- Autoconf -*- dnl Copyright (C) 2006-2009 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, @@ -39,28 +39,31 @@ AC_DEFUN([gl_SYS_STAT_H_DEFAULTS], [ AC_REQUIRE([gl_UNISTD_H_DEFAULTS]) dnl for REPLACE_FCHDIR - GNULIB_FCHMODAT=0; AC_SUBST([GNULIB_FCHMODAT]) - GNULIB_FSTATAT=0; AC_SUBST([GNULIB_FSTATAT]) - GNULIB_FUTIMENS=0; AC_SUBST([GNULIB_FUTIMENS]) - GNULIB_LCHMOD=0; AC_SUBST([GNULIB_LCHMOD]) - GNULIB_LSTAT=0; AC_SUBST([GNULIB_LSTAT]) - GNULIB_MKDIRAT=0; AC_SUBST([GNULIB_MKDIRAT]) - GNULIB_MKFIFOAT=0; AC_SUBST([GNULIB_MKFIFOAT]) - GNULIB_MKNODAT=0; AC_SUBST([GNULIB_MKNODAT]) - GNULIB_STAT=0; AC_SUBST([GNULIB_STAT]) + GNULIB_FCHMODAT=0; AC_SUBST([GNULIB_FCHMODAT]) + GNULIB_FSTATAT=0; AC_SUBST([GNULIB_FSTATAT]) + GNULIB_FUTIMENS=0; AC_SUBST([GNULIB_FUTIMENS]) + GNULIB_LCHMOD=0; AC_SUBST([GNULIB_LCHMOD]) + GNULIB_LSTAT=0; AC_SUBST([GNULIB_LSTAT]) + GNULIB_MKDIRAT=0; AC_SUBST([GNULIB_MKDIRAT]) + GNULIB_MKFIFOAT=0; AC_SUBST([GNULIB_MKFIFOAT]) + GNULIB_MKNODAT=0; AC_SUBST([GNULIB_MKNODAT]) + GNULIB_STAT=0; AC_SUBST([GNULIB_STAT]) + GNULIB_UTIMENSAT=0; AC_SUBST([GNULIB_UTIMENSAT]) dnl Assume proper GNU behavior unless another module says otherwise. - HAVE_FCHMODAT=1; AC_SUBST([HAVE_FCHMODAT]) - HAVE_FSTATAT=1; AC_SUBST([HAVE_FSTATAT]) - HAVE_FUTIMENS=1; AC_SUBST([HAVE_FUTIMENS]) - HAVE_LCHMOD=1; AC_SUBST([HAVE_LCHMOD]) - HAVE_LSTAT=1; AC_SUBST([HAVE_LSTAT]) - HAVE_MKDIRAT=1; AC_SUBST([HAVE_MKDIRAT]) - HAVE_MKFIFOAT=1; AC_SUBST([HAVE_MKFIFOAT]) - HAVE_MKNODAT=1; AC_SUBST([HAVE_MKNODAT]) - REPLACE_FSTAT=0; AC_SUBST([REPLACE_FSTAT]) - REPLACE_FSTATAT=0; AC_SUBST([REPLACE_FSTATAT]) - REPLACE_FUTIMENS=0; AC_SUBST([REPLACE_FUTIMENS]) - REPLACE_LSTAT=0; AC_SUBST([REPLACE_LSTAT]) - REPLACE_MKDIR=0; AC_SUBST([REPLACE_MKDIR]) - REPLACE_STAT=0; AC_SUBST([REPLACE_STAT]) + HAVE_FCHMODAT=1; AC_SUBST([HAVE_FCHMODAT]) + HAVE_FSTATAT=1; AC_SUBST([HAVE_FSTATAT]) + HAVE_FUTIMENS=1; AC_SUBST([HAVE_FUTIMENS]) + HAVE_LCHMOD=1; AC_SUBST([HAVE_LCHMOD]) + HAVE_LSTAT=1; AC_SUBST([HAVE_LSTAT]) + HAVE_MKDIRAT=1; AC_SUBST([HAVE_MKDIRAT]) + HAVE_MKFIFOAT=1; AC_SUBST([HAVE_MKFIFOAT]) + HAVE_MKNODAT=1; AC_SUBST([HAVE_MKNODAT]) + HAVE_UTIMENSAT=1; AC_SUBST([HAVE_UTIMENSAT]) + REPLACE_FSTAT=0; AC_SUBST([REPLACE_FSTAT]) + REPLACE_FSTATAT=0; AC_SUBST([REPLACE_FSTATAT]) + REPLACE_FUTIMENS=0; AC_SUBST([REPLACE_FUTIMENS]) + REPLACE_LSTAT=0; AC_SUBST([REPLACE_LSTAT]) + REPLACE_MKDIR=0; AC_SUBST([REPLACE_MKDIR]) + REPLACE_STAT=0; AC_SUBST([REPLACE_STAT]) + REPLACE_UTIMENSAT=0; AC_SUBST([REPLACE_UTIMENSAT]) ]) diff --git a/m4/utimensat.m4 b/m4/utimensat.m4 new file mode 100644 --- /dev/null +++ b/m4/utimensat.m4 @@ -0,0 +1,40 @@ +# serial 1 +# See if we need to provide utimensat replacement. + +dnl Copyright (C) 2009 Free 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. + +# Written by Eric Blake. + +AC_DEFUN([gl_FUNC_UTIMENSAT], +[ + AC_REQUIRE([gl_SYS_STAT_H_DEFAULTS]) + AC_REQUIRE([gl_USE_SYSTEM_EXTENSIONS]) + AC_CHECK_FUNCS_ONCE([utimensat]) + if test $ac_cv_func_utimensat = no; then + HAVE_UTIMENSAT=0 + AC_LIBOBJ([utimensat]) + else + AC_CACHE_CHECK([whether utimensat works], + [gl_cv_func_utimensat_works], + [AC_RUN_IFELSE([AC_LANG_PROGRAM([[ +#include +#include +#ifdef __linux__ +/* The Linux kernel added utimensat in 2.6.22, but it had bugs until 2.6.26. + Always replace utimensat to support older kernels. */ +choke me +#endif +]], [[struct timespec ts[2] = { { 1, UTIME_OMIT }, { 1, UTIME_OMIT } }; + return utimensat (AT_FDCWD, ".", NULL, AT_SYMLINK_NOFOLLOW);]])], + [gl_cv_func_utimensat_works=yes], + [gl_cv_func_utimensat_works="needs runtime check"], + [gl_cv_func_utimensat_works="guessing no"])]) + if test "$gl_cv_func_utimensat_works" != yes; then + REPLACE_UTIMENSAT=1 + AC_LIBOBJ([utimensat]) + fi + fi +]) diff --git a/modules/sys_stat b/modules/sys_stat --- a/modules/sys_stat +++ b/modules/sys_stat @@ -36,6 +36,7 @@ -e 's|@''GNULIB_MKFIFOAT''@|$(GNULIB_MKFIFOAT)|g' \ -e 's|@''GNULIB_MKNODAT''@|$(GNULIB_MKNODAT)|g' \ -e 's|@''GNULIB_STAT''@|$(GNULIB_STAT)|g' \ + -e 's|@''GNULIB_UTIMENSAT''@|$(GNULIB_UTIMENSAT)|g' \ -e 's|@''HAVE_FCHMODAT''@|$(HAVE_FCHMODAT)|g' \ -e 's|@''HAVE_FSTATAT''@|$(HAVE_FSTATAT)|g' \ -e 's|@''HAVE_FUTIMENS''@|$(HAVE_FUTIMENS)|g' \ @@ -44,12 +45,14 @@ -e 's|@''HAVE_MKDIRAT''@|$(HAVE_MKDIRAT)|g' \ -e 's|@''HAVE_MKFIFOAT''@|$(HAVE_MKFIFOAT)|g' \ -e 's|@''HAVE_MKNODAT''@|$(HAVE_MKNODAT)|g' \ + -e 's|@''HAVE_UTIMENSAT''@|$(HAVE_UTIMENSAT)|g' \ -e 's|@''REPLACE_FSTAT''@|$(REPLACE_FSTAT)|g' \ -e 's|@''REPLACE_FSTATAT''@|$(REPLACE_FSTATAT)|g' \ -e 's|@''REPLACE_FUTIMENS''@|$(REPLACE_FUTIMENS)|g' \ -e 's|@''REPLACE_LSTAT''@|$(REPLACE_LSTAT)|g' \ -e 's|@''REPLACE_MKDIR''@|$(REPLACE_MKDIR)|g' \ -e 's|@''REPLACE_STAT''@|$(REPLACE_STAT)|g' \ + -e 's|@''REPLACE_UTIMENSAT''@|$(REPLACE_UTIMENSAT)|g' \ -e '/definition of GL_LINK_WARNING/r $(LINK_WARNING_H)' \ < $(srcdir)/sys_stat.in.h; \ } > $@-t && \ diff --git a/modules/utimensat b/modules/utimensat new file mode 100644 --- /dev/null +++ b/modules/utimensat @@ -0,0 +1,29 @@ +Description: +Set file access and modification times of a file relative to a directory fd. + +Files: +lib/utimensat.c +m4/utimensat.m4 + +Depends-on: +openat +sys_stat +utimens + +configure.ac: +gl_FUNC_UTIMENSAT +gl_SYS_STAT_MODULE_INDICATOR([utimensat]) + +Makefile.am: + +Include: + + +Link: +$(LIB_CLOCK_GETTIME) + +License: +GPL + +Maintainer: +Eric Blake diff --git a/modules/utimensat-tests b/modules/utimensat-tests new file mode 100644 --- /dev/null +++ b/modules/utimensat-tests @@ -0,0 +1,18 @@ +Files: +tests/test-lutimens.h +tests/test-utimens.h +tests/test-utimens-common.h +tests/test-utimensat.c + +Depends-on: +progname +timespec +utimecmp + +configure.ac: +AC_CHECK_FUNCS_ONCE([usleep]) + +Makefile.am: +TESTS += test-utimensat +check_PROGRAMS += test-utimensat +test_utimensat_LDADD = $(LDADD) $(LIB_CLOCK_GETTIME) @LIBINTL@ diff --git a/tests/test-utimensat.c b/tests/test-utimensat.c new file mode 100644 --- /dev/null +++ b/tests/test-utimensat.c @@ -0,0 +1,117 @@ +/* Tests of utimensat. + 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 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 . */ + +/* Written by Eric Blake , 2009. */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "stat-time.h" +#include "timespec.h" +#include "utimecmp.h" + +#define ASSERT(expr) \ + do \ + { \ + if (!(expr)) \ + { \ + fprintf (stderr, "%s:%d: assertion failed\n", __FILE__, __LINE__); \ + fflush (stderr); \ + abort (); \ + } \ + } \ + while (0) + +#define BASE "test-utimensat.t" + +#include "test-lutimens.h" +#include "test-utimens.h" + +static int dfd = AT_FDCWD; + +/* Wrap utimensat to behave like utimens. */ +static int +do_utimensat (char const *name, struct timespec const times[2]) +{ + return utimensat (dfd, name, times, 0); +} + +/* Wrap utimensat to behave like lutimens. */ +static int +do_lutimensat (char const *name, struct timespec const times[2]) +{ + return utimensat (dfd, name, times, AT_SYMLINK_NOFOLLOW); +} + +int +main () +{ + int result1; /* Skip because of no symlink support. */ + int result2; /* Skip because of no lutimens support. */ + int fd; + + /* Clean up any trash from prior testsuite runs. */ + ASSERT (system ("rm -rf " BASE "*") == 0); + + /* Basic tests. */ + result1 = test_utimens (do_utimensat, true); + result2 = test_lutimens (do_lutimensat, result1 == 0); + dfd = open (".", O_RDONLY); + ASSERT (0 <= dfd); + ASSERT (test_utimens (do_utimensat, false) == result1); + ASSERT (test_lutimens (do_lutimensat, false) == result2); + /* We expect 0/0, 0/77, or 77/77, but not 77/0. */ + ASSERT (result1 <= result2); + + /* Directory-relative tests. */ + ASSERT (mkdir (BASE "dir", 0700) == 0); + ASSERT (chdir (BASE "dir") == 0); + fd = creat ("file", 0600); + ASSERT (0 <= fd); + errno = 0; + ASSERT (utimensat (fd, ".", NULL, 0) == -1); + ASSERT (errno == ENOTDIR); + { + struct timespec ts[2] = { { Y2K, 0 }, { Y2K, 0 } }; + struct stat st; + ASSERT (utimensat (dfd, BASE "dir/file", ts, AT_SYMLINK_NOFOLLOW) == 0); + ASSERT (stat ("file", &st) == 0); + ASSERT (st.st_atime == Y2K); + ASSERT (get_stat_atime_ns (&st) == 0); + ASSERT (st.st_mtime == Y2K); + ASSERT (get_stat_mtime_ns (&st) == 0); + } + ASSERT (close (fd) == 0); + ASSERT (close (dfd) == 0); + errno = 0; + ASSERT (utimensat (dfd, ".", NULL, 0) == -1); + ASSERT (errno == EBADF); + + /* Cleanup. */ + ASSERT (chdir ("..") == 0); + ASSERT (unlink (BASE "dir/file") == 0); + ASSERT (rmdir (BASE "dir") == 0); + return result1 | result2; +}