This patch add a module 'utime'. The original _utime function on native Windows has a behaviour that depends on the time zone, which is nonsense (see https://lists.gnu.org/archive/html/bug-gnulib/2017-04/msg00164.html ).
2017-04-29 Bruno Haible <br...@clisp.org> utime: New module. * lib/utime.in.h: Add comment for snippets. (utime): New declaration. * lib/utime.c: New file. * m4/utime.m4: New file. * m4/utime_h.m4 (gl_UTIME_H): Test for utime declaration. (gl_UTIME_H_DEFAULTS): Initialize GNULIB_UTIME, HAVE_UTIME, REPLACE_UTIME. * modules/utime-h (Depends-on): Add snippets. (Makefile.am): Substitute GNULIB_UTIME, HAVE_UTIME, REPLACE_UTIME. Insert snippets. * modules/utime: New file. * doc/posix-functions/utime.texi: Mention the new module. diff --git a/doc/posix-functions/utime.texi b/doc/posix-functions/utime.texi index 9cfe373..a77e614 100644 --- a/doc/posix-functions/utime.texi +++ b/doc/posix-functions/utime.texi @@ -4,10 +4,18 @@ POSIX specification:@* @url{http://www.opengroup.org/onlinepubs/9699919799/functions/utime.html} -Gnulib module: --- +Gnulib module: utime Portability problems fixed by Gnulib: @itemize +@item +The times that are set on the file are affected by the current time zone and +by the DST flag of the current time zone on some platforms: +mingw, MSVC 14 (when the environment variable @code{TZ} is set). +@item +On some platforms, the prototype for @code{utime} omits @code{const} +for the second argument: +mingw, MSVC 9. @end itemize Portability problems not fixed by Gnulib: @@ -22,9 +30,4 @@ Solaris 9. This function cannot set full timestamp resolution. Use @code{utimensat(AT_FDCWD,file,times,0)}, or the gnulib module utimens, instead. -@item -On some platforms, the prototype for @code{utime} omits @code{const} -for the second argument. Fortunately, the argument is not modified, -so it is safe to cast away const: -mingw, MSVC 9. @end itemize diff --git a/lib/utime.c b/lib/utime.c new file mode 100644 index 0000000..ac5c78b --- /dev/null +++ b/lib/utime.c @@ -0,0 +1,240 @@ +/* Work around platform bugs in utime. + Copyright (C) 2017 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/>. */ + +/* Written by Bruno Haible. */ + +#include <config.h> + +/* Specification. */ +#include <utime.h> + +#if (defined _WIN32 || defined __WIN32__) && ! defined __CYGWIN__ + +# include <errno.h> +# include <stdbool.h> +# include <windows.h> +# include "filename.h" +# include "malloca.h" + +int +utime (const char *name, const struct utimbuf *ts) +{ + /* POSIX <http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13> + specifies: "More than two leading <slash> characters shall be treated as + a single <slash> character." */ + if (ISSLASH (name[0]) && ISSLASH (name[1]) && ISSLASH (name[2])) + { + name += 2; + while (ISSLASH (name[1])) + name++; + } + + size_t len = strlen (name); + size_t drive_prefix_len = (HAS_DEVICE (name) ? 2 : 0); + + /* Remove trailing slashes (except the very first one, at position + drive_prefix_len), but remember their presence. */ + size_t rlen; + bool check_dir = false; + + rlen = len; + while (rlen > drive_prefix_len && ISSLASH (name[rlen-1])) + { + check_dir = true; + if (rlen == drive_prefix_len + 1) + break; + rlen--; + } + + const char *rname; + char *malloca_rname; + if (rlen == len) + { + rname = name; + malloca_rname = NULL; + } + else + { + malloca_rname = malloca (rlen + 1); + if (malloca_rname == NULL) + { + errno = ENOMEM; + return -1; + } + memcpy (malloca_rname, name, rlen); + malloca_rname[rlen] = '\0'; + rname = malloca_rname; + } + + DWORD error; + + /* Open a handle to the file. + CreateFile + <https://msdn.microsoft.com/en-us/library/aa363858.aspx> + <https://msdn.microsoft.com/en-us/library/aa363874.aspx> */ + HANDLE handle = + CreateFile (rname, + FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + /* FILE_FLAG_POSIX_SEMANTICS (treat file names that differ only + in case as different) makes sense only when applied to *all* + filesystem operations. */ + FILE_FLAG_BACKUP_SEMANTICS /* | FILE_FLAG_POSIX_SEMANTICS */, + NULL); + if (handle == INVALID_HANDLE_VALUE) + { + error = GetLastError (); + goto failed; + } + + if (check_dir) + { + /* GetFileAttributes + <https://msdn.microsoft.com/en-us/library/aa364944.aspx> */ + DWORD attributes = GetFileAttributes (rname); + if (attributes == INVALID_FILE_ATTRIBUTES) + { + error = GetLastError (); + CloseHandle (handle); + goto failed; + } + if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) + { + CloseHandle (handle); + if (malloca_rname != NULL) + freea (malloca_rname); + errno = ENOTDIR; + return -1; + } + } + + { + /* Use SetFileTime(). See + <https://msdn.microsoft.com/en-us/library/ms724933.aspx> + <https://msdn.microsoft.com/en-us/library/ms724284.aspx> */ + FILETIME last_access_time; + FILETIME last_write_time; + if (ts == NULL) + { + /* GetSystemTimeAsFileTime is the same as + GetSystemTime followed by SystemTimeToFileTime. + <https://msdn.microsoft.com/en-us/library/ms724397.aspx>. + It would be overkill to use + GetSystemTimePreciseAsFileTime + <https://msdn.microsoft.com/en-us/library/hh706895.aspx>. */ + FILETIME current_time; + GetSystemTimeAsFileTime (¤t_time); + last_access_time = current_time; + last_write_time = current_time; + } + else + { + { + ULONGLONG time_since_16010101 = + (ULONGLONG) ts->actime * 10000000 + 116444736000000000LL; + last_access_time.dwLowDateTime = (DWORD) time_since_16010101; + last_access_time.dwHighDateTime = time_since_16010101 >> 32; + } + { + ULONGLONG time_since_16010101 = + (ULONGLONG) ts->modtime * 10000000 + 116444736000000000LL; + last_write_time.dwLowDateTime = (DWORD) time_since_16010101; + last_write_time.dwHighDateTime = time_since_16010101 >> 32; + } + } + if (SetFileTime (handle, NULL, &last_access_time, &last_write_time)) + { + CloseHandle (handle); + if (malloca_rname != NULL) + freea (malloca_rname); + return 0; + } + else + { + #if 0 + DWORD sft_error = GetLastError (); + fprintf (stderr, "utime SetFileTime error 0x%x\n", (unsigned int) sft_error); + #endif + CloseHandle (handle); + if (malloca_rname != NULL) + freea (malloca_rname); + errno = EINVAL; + return -1; + } + } + + failed: + { + #if 0 + fprintf (stderr, "utime CreateFile/GetFileAttributes error 0x%x\n", (unsigned int) error); + #endif + if (malloca_rname != NULL) + freea (malloca_rname); + + switch (error) + { + /* Some of these errors probably cannot happen with the specific flags + that we pass to CreateFile. But who knows... */ + case ERROR_FILE_NOT_FOUND: /* The last component of rname does not exist. */ + case ERROR_PATH_NOT_FOUND: /* Some directory component in rname does not exist. */ + case ERROR_BAD_PATHNAME: /* rname is such as '\\server'. */ + case ERROR_BAD_NET_NAME: /* rname is such as '\\server\nonexistentshare'. */ + case ERROR_INVALID_NAME: /* rname contains wildcards, misplaced colon, etc. */ + case ERROR_DIRECTORY: + errno = ENOENT; + break; + + case ERROR_ACCESS_DENIED: /* rname is such as 'C:\System Volume Information\foo'. */ + case ERROR_SHARING_VIOLATION: /* rname is such as 'C:\pagefile.sys'. */ + /* XXX map to EACCESS or EPERM? */ + errno = (ts != NULL ? EPERM : EACCES); + break; + + case ERROR_OUTOFMEMORY: + errno = ENOMEM; + break; + + case ERROR_WRITE_PROTECT: + errno = EROFS; + break; + + case ERROR_WRITE_FAULT: + case ERROR_READ_FAULT: + case ERROR_GEN_FAILURE: + errno = EIO; + break; + + case ERROR_BUFFER_OVERFLOW: + case ERROR_FILENAME_EXCED_RANGE: + errno = ENAMETOOLONG; + break; + + case ERROR_DELETE_PENDING: /* XXX map to EACCESS or EPERM? */ + errno = EPERM; + break; + + default: + errno = EINVAL; + break; + } + + return -1; + } +} + +#endif diff --git a/lib/utime.in.h b/lib/utime.in.h index 26a1cea..8847e72 100644 --- a/lib/utime.in.h +++ b/lib/utime.in.h @@ -33,6 +33,13 @@ # include <sys/utime.h> #endif +/* The definitions of _GL_FUNCDECL_RPL etc. are copied here. */ + +/* The definition of _GL_ARG_NONNULL is copied here. */ + +/* The definition of _GL_WARN_ON_USE is copied here. */ + + #if (defined _WIN32 || defined __WIN32__) && ! defined __CYGWIN__ /* Define 'struct utimbuf' as an alias of 'struct _utimbuf' @@ -41,5 +48,32 @@ #endif + +#if @GNULIB_UTIME@ +# if @REPLACE_UTIME@ +# if !(defined __cplusplus && defined GNULIB_NAMESPACE) +# define utime rpl_utime +# endif +_GL_FUNCDECL_RPL (utime, int, (const char *filename, const struct utimbuf *ts) + _GL_ARG_NONNULL ((1))); +_GL_CXXALIAS_RPL (utime, int, (const char *filename, const struct utimbuf *ts)); +# else +# if !@HAVE_UTIME@ +_GL_FUNCDECL_SYS (utime, int, (const char *filename, const struct utimbuf *ts) + _GL_ARG_NONNULL ((1))); +# endif +_GL_CXXALIAS_SYS (utime, int, (const char *filename, const struct utimbuf *ts)); +# endif +_GL_CXXALIASWARN (utime); +#elif defined GNULIB_POSIXCHECK +# undef utime +# if HAVE_RAW_DECL_UTIME +_GL_WARN_ON_USE (utime, + "utime is unportable - " + "use gnulib module canonicalize-lgpl for portability"); +# endif +#endif + + #endif /* _@GUARD_PREFIX@_UTIME_H */ #endif /* _@GUARD_PREFIX@_UTIME_H */ diff --git a/m4/utime.m4 b/m4/utime.m4 new file mode 100644 index 0000000..7d4a603 --- /dev/null +++ b/m4/utime.m4 @@ -0,0 +1,26 @@ +# utime.m4 serial 1 +dnl Copyright (C) 2017 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. + +AC_DEFUN([gl_FUNC_UTIME], +[ + AC_REQUIRE([gl_UTIME_H_DEFAULTS]) + AC_REQUIRE([AC_CANONICAL_HOST]) + AC_CHECK_FUNCS_ONCE([utime]) + if test $ac_cv_func_utime = no; then + HAVE_UTIME=0 + else + case "$host_os" in + mingw*) + dnl On this platform, the original utime() or _utime() produces + dnl timestamps that are affected by the time zone. + REPLACE_UTIME=1 + ;; + esac + fi +]) + +# Prerequisites of lib/utime.c. +AC_DEFUN([gl_PREREQ_UTIME], [:]) diff --git a/m4/utime_h.m4 b/m4/utime_h.m4 index 6b0ac5c..550f764 100644 --- a/m4/utime_h.m4 +++ b/m4/utime_h.m4 @@ -33,6 +33,12 @@ AC_DEFUN([gl_UTIME_H], fi AC_SUBST([UTIME_H]) AM_CONDITIONAL([GL_GENERATE_UTIME_H], [test -n "$UTIME_H"]) + + dnl Check for declarations of anything we want to poison if the + dnl corresponding gnulib module is not in use. + gl_WARN_ON_USE_PREPARE([[#include <utime.h> + ]], + [utime]) ]) AC_DEFUN([gl_UTIME_MODULE_INDICATOR], @@ -46,5 +52,8 @@ AC_DEFUN([gl_UTIME_MODULE_INDICATOR], AC_DEFUN([gl_UTIME_H_DEFAULTS], [ + GNULIB_UTIME=0; AC_SUBST([GNULIB_UTIME]) dnl Assume POSIX behavior unless another module says otherwise. + HAVE_UTIME=1; AC_SUBST([HAVE_UTIME]) + REPLACE_UTIME=0; AC_SUBST([REPLACE_UTIME]) ]) diff --git a/modules/utime b/modules/utime new file mode 100644 index 0000000..545a24c --- /dev/null +++ b/modules/utime @@ -0,0 +1,30 @@ +Description: +utime() function: set access and modification times of a file. + +Files: +lib/utime.c +m4/utime.m4 + +Depends-on: +utime-h +filename [test $HAVE_UTIME = 0 || test $REPLACE_UTIME = 1] +malloca [test $HAVE_UTIME = 0 || test $REPLACE_UTIME = 1] + +configure.ac: +gl_FUNC_UTIME +if test $HAVE_UTIME = 0 || test $REPLACE_UTIME = 1; then + AC_LIBOBJ([utime]) + gl_PREREQ_UTIME +fi +gl_UTIME_MODULE_INDICATOR([utime]) + +Makefile.am: + +Include: +<utime.h> + +License: +LGPL + +Maintainer: +Bruno Haible diff --git a/modules/utime-h b/modules/utime-h index 0e02d5b..a60f45a 100644 --- a/modules/utime-h +++ b/modules/utime-h @@ -7,6 +7,9 @@ m4/utime_h.m4 Depends-on: include_next +snippet/arg-nonnull +snippet/c++defs +snippet/warn-on-use configure.ac: gl_UTIME_H @@ -26,6 +29,12 @@ utime.h: utime.in.h $(top_builddir)/config.status -e 's|@''PRAGMA_SYSTEM_HEADER''@|@PRAGMA_SYSTEM_HEADER@|g' \ -e 's|@''PRAGMA_COLUMNS''@|@PRAGMA_COLUMNS@|g' \ -e 's|@''NEXT_UTIME_H''@|$(NEXT_UTIME_H)|g' \ + -e 's/@''GNULIB_UTIME''@/$(GNULIB_UTIME)/g' \ + -e 's|@''HAVE_UTIME''@|$(HAVE_UTIME)|g' \ + -e 's|@''REPLACE_UTIME''@|$(REPLACE_UTIME)|g' \ + -e '/definitions of _GL_FUNCDECL_RPL/r $(CXXDEFS_H)' \ + -e '/definition of _GL_ARG_NONNULL/r $(ARG_NONNULL_H)' \ + -e '/definition of _GL_WARN_ON_USE/r $(WARN_ON_USE_H)' \ < $(srcdir)/utime.in.h; \ } > $@-t && \ mv $@-t $@