unix,win: accept NAN/INFINITY as file timestamps (#4702)

Extend uv_fs_utime, uv_fs_futime and uv_fs_lutime to accept NAN and
INFINITY, with NAN meaning "don't touch the timestamp" and INFINITY
meaning "set to the current timestamp."

Ugly, but it avoids having to add uv_fs_utime2, etc.

UV_FS_UTIME_NOW and UV_FS_UTIME_OMIT constants have been added to make
it more palatable.

Fixes: https://github.com/libuv/libuv/issues/4665
This commit is contained in:
Ben Noordhuis 2025-02-21 23:08:15 +01:00 committed by GitHub
parent 8a94b7b2ec
commit 85b526f56a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 273 additions and 106 deletions

View File

@ -430,6 +430,12 @@ API
Equivalent to :man:`utime(2)`, :man:`futimes(3)` and :man:`lutimes(3)` respectively. Equivalent to :man:`utime(2)`, :man:`futimes(3)` and :man:`lutimes(3)` respectively.
Passing `UV_FS_UTIME_NOW` as the atime or mtime sets the timestamp to the
current time.
Passing `UV_FS_UTIME_OMIT` as the atime or mtime leaves the timestamp
untouched.
.. note:: .. note::
z/OS: `uv_fs_lutime()` is not implemented for z/OS. It can still be called but will return z/OS: `uv_fs_lutime()` is not implemented for z/OS. It can still be called but will return
``UV_ENOSYS``. ``UV_ENOSYS``.

View File

@ -58,6 +58,7 @@ extern "C" {
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
#include <stdint.h> #include <stdint.h>
#include <math.h>
/* Internal type, do not use. */ /* Internal type, do not use. */
struct uv__queue { struct uv__queue {
@ -1585,6 +1586,8 @@ UV_EXTERN int uv_fs_chmod(uv_loop_t* loop,
const char* path, const char* path,
int mode, int mode,
uv_fs_cb cb); uv_fs_cb cb);
#define UV_FS_UTIME_NOW (INFINITY)
#define UV_FS_UTIME_OMIT (NAN)
UV_EXTERN int uv_fs_utime(uv_loop_t* loop, UV_EXTERN int uv_fs_utime(uv_loop_t* loop,
uv_fs_t* req, uv_fs_t* req,
const char* path, const char* path,

View File

@ -203,8 +203,23 @@ static ssize_t uv__fs_fdatasync(uv_fs_t* req) {
} }
UV_UNUSED(static struct timespec uv__fs_to_timespec(double time)) { #if defined(__APPLE__) \
|| defined(_AIX71) \
|| defined(__DragonFly__) \
|| defined(__FreeBSD__) \
|| defined(__HAIKU__) \
|| defined(__NetBSD__) \
|| defined(__OpenBSD__) \
|| defined(__linux__) \
|| defined(__sun)
static struct timespec uv__fs_to_timespec(double time) {
struct timespec ts; struct timespec ts;
if (uv__isinf(time))
return (struct timespec){UTIME_NOW, UTIME_NOW};
if (uv__isnan(time))
return (struct timespec){UTIME_OMIT, UTIME_OMIT};
ts.tv_sec = time; ts.tv_sec = time;
ts.tv_nsec = (time - ts.tv_sec) * 1e9; ts.tv_nsec = (time - ts.tv_sec) * 1e9;
@ -221,41 +236,23 @@ UV_UNUSED(static struct timespec uv__fs_to_timespec(double time)) {
} }
return ts; return ts;
} }
#endif
UV_UNUSED(static struct timeval uv__fs_to_timeval(double time)) {
struct timeval tv;
tv.tv_sec = time;
tv.tv_usec = (time - tv.tv_sec) * 1e6;
if (tv.tv_usec < 0) {
tv.tv_usec += 1e6;
tv.tv_sec -= 1;
}
return tv;
}
static ssize_t uv__fs_futime(uv_fs_t* req) { static ssize_t uv__fs_futime(uv_fs_t* req) {
#if defined(__linux__) \ #if defined(__APPLE__) \
|| defined(_AIX71) \ || defined(_AIX71) \
|| defined(__DragonFly__) \
|| defined(__FreeBSD__) \
|| defined(__HAIKU__) \ || defined(__HAIKU__) \
|| defined(__GNU__) || defined(__NetBSD__) \
|| defined(__OpenBSD__) \
|| defined(__linux__) \
|| defined(__sun)
struct timespec ts[2]; struct timespec ts[2];
ts[0] = uv__fs_to_timespec(req->atime); ts[0] = uv__fs_to_timespec(req->atime);
ts[1] = uv__fs_to_timespec(req->mtime); ts[1] = uv__fs_to_timespec(req->mtime);
return futimens(req->file, ts); return futimens(req->file, ts);
#elif defined(__APPLE__) \
|| defined(__DragonFly__) \
|| defined(__FreeBSD__) \
|| defined(__NetBSD__) \
|| defined(__OpenBSD__) \
|| defined(__sun)
struct timeval tv[2];
tv[0] = uv__fs_to_timeval(req->atime);
tv[1] = uv__fs_to_timeval(req->mtime);
# if defined(__sun)
return futimesat(req->file, NULL, tv);
# else
return futimes(req->file, tv);
# endif
#elif defined(__MVS__) #elif defined(__MVS__)
attrib_t atr; attrib_t atr;
memset(&atr, 0, sizeof(atr)); memset(&atr, 0, sizeof(atr));
@ -1142,25 +1139,20 @@ static ssize_t uv__fs_sendfile(uv_fs_t* req) {
static ssize_t uv__fs_utime(uv_fs_t* req) { static ssize_t uv__fs_utime(uv_fs_t* req) {
#if defined(__linux__) \ #if defined(__APPLE__) \
|| defined(_AIX71) \ || defined(_AIX71) \
|| defined(__sun) \ || defined(__DragonFly__) \
|| defined(__HAIKU__) || defined(__FreeBSD__) \
|| defined(__HAIKU__) \
|| defined(__NetBSD__) \
|| defined(__OpenBSD__) \
|| defined(__linux__) \
|| defined(__sun)
struct timespec ts[2]; struct timespec ts[2];
ts[0] = uv__fs_to_timespec(req->atime); ts[0] = uv__fs_to_timespec(req->atime);
ts[1] = uv__fs_to_timespec(req->mtime); ts[1] = uv__fs_to_timespec(req->mtime);
return utimensat(AT_FDCWD, req->path, ts, 0); return utimensat(AT_FDCWD, req->path, ts, 0);
#elif defined(__APPLE__) \ #elif defined(_AIX) && !defined(_AIX71)
|| defined(__DragonFly__) \
|| defined(__FreeBSD__) \
|| defined(__NetBSD__) \
|| defined(__OpenBSD__)
struct timeval tv[2];
tv[0] = uv__fs_to_timeval(req->atime);
tv[1] = uv__fs_to_timeval(req->mtime);
return utimes(req->path, tv);
#elif defined(_AIX) \
&& !defined(_AIX71)
struct utimbuf buf; struct utimbuf buf;
buf.actime = req->atime; buf.actime = req->atime;
buf.modtime = req->mtime; buf.modtime = req->mtime;
@ -1181,24 +1173,19 @@ static ssize_t uv__fs_utime(uv_fs_t* req) {
static ssize_t uv__fs_lutime(uv_fs_t* req) { static ssize_t uv__fs_lutime(uv_fs_t* req) {
#if defined(__linux__) || \ #if defined(__APPLE__) \
defined(_AIX71) || \ || defined(_AIX71) \
defined(__sun) || \ || defined(__DragonFly__) \
defined(__HAIKU__) || \ || defined(__FreeBSD__) \
defined(__GNU__) || \ || defined(__HAIKU__) \
defined(__OpenBSD__) || defined(__NetBSD__) \
|| defined(__OpenBSD__) \
|| defined(__linux__) \
|| defined(__sun)
struct timespec ts[2]; struct timespec ts[2];
ts[0] = uv__fs_to_timespec(req->atime); ts[0] = uv__fs_to_timespec(req->atime);
ts[1] = uv__fs_to_timespec(req->mtime); ts[1] = uv__fs_to_timespec(req->mtime);
return utimensat(AT_FDCWD, req->path, ts, AT_SYMLINK_NOFOLLOW); return utimensat(AT_FDCWD, req->path, ts, AT_SYMLINK_NOFOLLOW);
#elif defined(__APPLE__) || \
defined(__DragonFly__) || \
defined(__FreeBSD__) || \
defined(__NetBSD__)
struct timeval tv[2];
tv[0] = uv__fs_to_timeval(req->atime);
tv[1] = uv__fs_to_timeval(req->mtime);
return lutimes(req->path, tv);
#else #else
errno = ENOSYS; errno = ENOSYS;
return -1; return -1;

View File

@ -31,6 +31,7 @@
#include <stdarg.h> #include <stdarg.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <string.h>
#include "uv.h" #include "uv.h"
#include "uv/tree.h" #include "uv/tree.h"
@ -448,4 +449,22 @@ struct uv__loop_internal_fields_s {
# define UV_PTHREAD_MAX_NAMELEN_NP 16 # define UV_PTHREAD_MAX_NAMELEN_NP 16
#endif #endif
/* Open-coded so downstream users don't have to link libm. */
static inline int uv__isinf(double d) {
uint64_t v;
STATIC_ASSERT(sizeof(v) == sizeof(d));
memcpy(&v, &d, sizeof(v));
return (v << 1 >> 53) == 2047 && !(v << 12);
}
/* Open-coded so downstream users don't have to link libm. */
static inline int uv__isnan(double d) {
uint64_t v;
STATIC_ASSERT(sizeof(v) == sizeof(d));
memcpy(&v, &d, sizeof(v));
return (v << 1 >> 53) == 2047 && !!(v << 12);
}
#endif /* UV_COMMON_H_ */ #endif /* UV_COMMON_H_ */

View File

@ -2580,14 +2580,29 @@ fchmod_cleanup:
INLINE static int fs__utime_handle(HANDLE handle, double atime, double mtime) { INLINE static int fs__utime_handle(HANDLE handle, double atime, double mtime) {
FILETIME filetime_a, filetime_m; FILETIME filetime_as, *filetime_a = &filetime_as;
FILETIME filetime_ms, *filetime_m = &filetime_ms;
FILETIME now;
TIME_T_TO_FILETIME(atime, &filetime_a); if (uv__isinf(atime) || uv__isinf(mtime))
TIME_T_TO_FILETIME(mtime, &filetime_m); GetSystemTimeAsFileTime(&now);
if (!SetFileTime(handle, NULL, &filetime_a, &filetime_m)) { if (uv__isinf(atime))
filetime_a = &now;
else if (uv__isnan(atime))
filetime_a = NULL;
else
TIME_T_TO_FILETIME(atime, filetime_a);
if (uv__isinf(mtime))
filetime_m = &now;
else if (uv__isnan(mtime))
filetime_m = NULL;
else
TIME_T_TO_FILETIME(mtime, filetime_m);
if (!SetFileTime(handle, NULL, filetime_a, filetime_m))
return -1; return -1;
}
return 0; return 0;
} }

View File

@ -59,6 +59,18 @@
#define TOO_LONG_NAME_LENGTH 65536 #define TOO_LONG_NAME_LENGTH 65536
#define PATHMAX 4096 #define PATHMAX 4096
#ifdef _WIN32
static const int is_win32 = 1;
#else
static const int is_win32 = 0;
#endif
#if defined(__APPLE__) || defined(__SUNPRO_C)
static const int is_apple_or_sunpro_c = 1;
#else
static const int is_apple_or_sunpro_c = 0;
#endif
typedef struct { typedef struct {
const char* path; const char* path;
double atime; double atime;
@ -827,43 +839,70 @@ static void check_utime(const char* path,
ASSERT_OK(req.result); ASSERT_OK(req.result);
s = &req.statbuf; s = &req.statbuf;
if (s->st_atim.tv_nsec == 0 && s->st_mtim.tv_nsec == 0) { if (isfinite(atime)) {
/* /* Test sub-second timestamps only when supported (such as Windows with
* Test sub-second timestamps only when supported (such as Windows with
* NTFS). Some other platforms support sub-second timestamps, but that * NTFS). Some other platforms support sub-second timestamps, but that
* support is filesystem-dependent. Notably OS X (HFS Plus) does NOT * support is filesystem-dependent. Notably OS X (HFS Plus) does NOT
* support sub-second timestamps. But kernels may round or truncate in * support sub-second timestamps. But kernels may round or truncate in
* either direction, so we may accept either possible answer. * either direction, so we may accept either possible answer.
*/ */
#ifdef _WIN32 if (s->st_atim.tv_nsec == 0) {
ASSERT_DOUBLE_EQ(atime, (long) atime); if (is_win32)
ASSERT_DOUBLE_EQ(mtime, (long) atime); ASSERT_DOUBLE_EQ(atime, (long) atime);
#endif if (atime > 0 || (long) atime == atime)
if (atime > 0 || (long) atime == atime) ASSERT_EQ(s->st_atim.tv_sec, (long) atime);
ASSERT_EQ(s->st_atim.tv_sec, (long) atime); ASSERT_GE(s->st_atim.tv_sec, (long) atime - 1);
if (mtime > 0 || (long) mtime == mtime) ASSERT_LE(s->st_atim.tv_sec, (long) atime);
ASSERT_EQ(s->st_mtim.tv_sec, (long) mtime); } else {
ASSERT_GE(s->st_atim.tv_sec, (long) atime - 1); double st_atim;
ASSERT_GE(s->st_mtim.tv_sec, (long) mtime - 1); /* TODO(vtjnash): would it be better to normalize this? */
ASSERT_LE(s->st_atim.tv_sec, (long) atime); if (!is_apple_or_sunpro_c)
ASSERT_LE(s->st_mtim.tv_sec, (long) mtime); ASSERT_DOUBLE_GE(s->st_atim.tv_nsec, 0);
} else { st_atim = s->st_atim.tv_sec + s->st_atim.tv_nsec / 1e9;
double st_atim; /* Linux does not allow reading reliably the atime of a symlink
double st_mtim; * since readlink() can update it
#if !defined(__APPLE__) && !defined(__SUNPRO_C) */
/* TODO(vtjnash): would it be better to normalize this? */ if (!test_lutime)
ASSERT_DOUBLE_GE(s->st_atim.tv_nsec, 0); ASSERT_DOUBLE_EQ(st_atim, atime);
ASSERT_DOUBLE_GE(s->st_mtim.tv_nsec, 0); }
#endif } else if (isinf(atime)) {
st_atim = s->st_atim.tv_sec + s->st_atim.tv_nsec / 1e9; /* We test with timestamps that are in the distant past
st_mtim = s->st_mtim.tv_sec + s->st_mtim.tv_nsec / 1e9; * (if you're a Gen Z-er) so check it's more recent than that.
/*
* Linux does not allow reading reliably the atime of a symlink
* since readlink() can update it
*/ */
if (!test_lutime) ASSERT_GT(s->st_atim.tv_sec, 1739710000);
ASSERT_DOUBLE_EQ(st_atim, atime); } else {
ASSERT_DOUBLE_EQ(st_mtim, mtime); ASSERT_OK(0);
}
if (isfinite(mtime)) {
/* Test sub-second timestamps only when supported (such as Windows with
* NTFS). Some other platforms support sub-second timestamps, but that
* support is filesystem-dependent. Notably OS X (HFS Plus) does NOT
* support sub-second timestamps. But kernels may round or truncate in
* either direction, so we may accept either possible answer.
*/
if (s->st_mtim.tv_nsec == 0) {
if (is_win32)
ASSERT_DOUBLE_EQ(mtime, (long) atime);
if (mtime > 0 || (long) mtime == mtime)
ASSERT_EQ(s->st_mtim.tv_sec, (long) mtime);
ASSERT_GE(s->st_mtim.tv_sec, (long) mtime - 1);
ASSERT_LE(s->st_mtim.tv_sec, (long) mtime);
} else {
double st_mtim;
/* TODO(vtjnash): would it be better to normalize this? */
if (!is_apple_or_sunpro_c)
ASSERT_DOUBLE_GE(s->st_mtim.tv_nsec, 0);
st_mtim = s->st_mtim.tv_sec + s->st_mtim.tv_nsec / 1e9;
ASSERT_DOUBLE_EQ(st_mtim, mtime);
}
} else if (isinf(mtime)) {
/* We test with timestamps that are in the distant past
* (if you're a Gen Z-er) so check it's more recent than that.
*/
ASSERT_GT(s->st_mtim.tv_sec, 1739710000);
} else {
ASSERT_OK(0);
} }
uv_fs_req_cleanup(&req); uv_fs_req_cleanup(&req);
@ -2728,13 +2767,46 @@ TEST_IMPL(fs_utime) {
atime = mtime = 400497753.25; /* 1982-09-10 11:22:33.25 */ atime = mtime = 400497753.25; /* 1982-09-10 11:22:33.25 */
r = uv_fs_utime(NULL, &req, path, atime, mtime, NULL); ASSERT_OK(uv_fs_utime(NULL, &req, path, atime, mtime, NULL));
ASSERT_OK(r);
ASSERT_OK(req.result); ASSERT_OK(req.result);
uv_fs_req_cleanup(&req); uv_fs_req_cleanup(&req);
check_utime(path, atime, mtime, /* test_lutime */ 0); check_utime(path, atime, mtime, /* test_lutime */ 0);
ASSERT_OK(uv_fs_utime(NULL,
&req,
path,
UV_FS_UTIME_OMIT,
UV_FS_UTIME_OMIT,
NULL));
ASSERT_OK(req.result);
uv_fs_req_cleanup(&req);
check_utime(path, atime, mtime, /* test_lutime */ 0);
ASSERT_OK(uv_fs_utime(NULL,
&req,
path,
UV_FS_UTIME_NOW,
UV_FS_UTIME_OMIT,
NULL));
ASSERT_OK(req.result);
uv_fs_req_cleanup(&req);
check_utime(path, UV_FS_UTIME_NOW, mtime, /* test_lutime */ 0);
ASSERT_OK(uv_fs_utime(NULL, &req, path, atime, mtime, NULL));
ASSERT_OK(req.result);
uv_fs_req_cleanup(&req);
check_utime(path, atime, mtime, /* test_lutime */ 0);
ASSERT_OK(uv_fs_utime(NULL,
&req,
path,
UV_FS_UTIME_OMIT,
UV_FS_UTIME_NOW,
NULL));
ASSERT_OK(req.result);
uv_fs_req_cleanup(&req);
check_utime(path, atime, UV_FS_UTIME_NOW, /* test_lutime */ 0);
atime = mtime = 1291404900.25; /* 2010-12-03 20:35:00.25 - mees <3 */ atime = mtime = 1291404900.25; /* 2010-12-03 20:35:00.25 - mees <3 */
checkme.path = path; checkme.path = path;
checkme.atime = atime; checkme.atime = atime;
@ -2868,9 +2940,43 @@ TEST_IMPL(fs_futime) {
ASSERT_OK(req.result); ASSERT_OK(req.result);
#endif #endif
uv_fs_req_cleanup(&req); uv_fs_req_cleanup(&req);
check_utime(path, atime, mtime, /* test_lutime */ 0); check_utime(path, atime, mtime, /* test_lutime */ 0);
ASSERT_OK(uv_fs_futime(NULL,
&req,
file,
UV_FS_UTIME_OMIT,
UV_FS_UTIME_OMIT,
NULL));
ASSERT_OK(req.result);
uv_fs_req_cleanup(&req);
check_utime(path, atime, mtime, /* test_lutime */ 0);
ASSERT_OK(uv_fs_futime(NULL,
&req,
file,
UV_FS_UTIME_NOW,
UV_FS_UTIME_OMIT,
NULL));
ASSERT_OK(req.result);
uv_fs_req_cleanup(&req);
check_utime(path, UV_FS_UTIME_NOW, mtime, /* test_lutime */ 0);
ASSERT_OK(uv_fs_futime(NULL, &req, file, atime, mtime, NULL));
ASSERT_OK(req.result);
uv_fs_req_cleanup(&req);
check_utime(path, atime, mtime, /* test_lutime */ 0);
ASSERT_OK(uv_fs_futime(NULL,
&req,
file,
UV_FS_UTIME_OMIT,
UV_FS_UTIME_NOW,
NULL));
ASSERT_OK(req.result);
uv_fs_req_cleanup(&req);
check_utime(path, atime, UV_FS_UTIME_NOW, /* test_lutime */ 0);
atime = mtime = 1291404900; /* 2010-12-03 20:35:00 - mees <3 */ atime = mtime = 1291404900; /* 2010-12-03 20:35:00 - mees <3 */
checkme.atime = atime; checkme.atime = atime;
@ -2932,20 +3038,50 @@ TEST_IMPL(fs_lutime) {
/* Test the synchronous version. */ /* Test the synchronous version. */
atime = mtime = 400497753.25; /* 1982-09-10 11:22:33.25 */ atime = mtime = 400497753.25; /* 1982-09-10 11:22:33.25 */
checkme.atime = atime;
checkme.mtime = mtime;
checkme.path = symlink_path;
req.data = &checkme;
r = uv_fs_lutime(NULL, &req, symlink_path, atime, mtime, NULL); r = uv_fs_lutime(NULL, &req, symlink_path, atime, mtime, NULL);
#if (defined(_AIX) && !defined(_AIX71)) || \ #if (defined(_AIX) && !defined(_AIX71)) || defined(__MVS__)
defined(__MVS__)
ASSERT_EQ(r, UV_ENOSYS); ASSERT_EQ(r, UV_ENOSYS);
RETURN_SKIP("lutime is not implemented for z/OS and AIX versions below 7.1"); RETURN_SKIP("lutime is not implemented for z/OS and AIX versions below 7.1");
#endif #endif
ASSERT_OK(r); ASSERT_OK(r);
lutime_cb(&req); ASSERT_OK(req.result);
ASSERT_EQ(1, lutime_cb_count); uv_fs_req_cleanup(&req);
check_utime(symlink_path, atime, mtime, /* test_lutime */ 1);
ASSERT_OK(uv_fs_lutime(NULL,
&req,
symlink_path,
UV_FS_UTIME_OMIT,
UV_FS_UTIME_OMIT,
NULL));
ASSERT_OK(req.result);
uv_fs_req_cleanup(&req);
check_utime(symlink_path, atime, mtime, /* test_lutime */ 1);
ASSERT_OK(uv_fs_lutime(NULL,
&req,
symlink_path,
UV_FS_UTIME_NOW,
UV_FS_UTIME_OMIT,
NULL));
ASSERT_OK(req.result);
uv_fs_req_cleanup(&req);
check_utime(symlink_path, UV_FS_UTIME_NOW, mtime, /* test_lutime */ 1);
ASSERT_OK(uv_fs_lutime(NULL, &req, symlink_path, atime, mtime, NULL));
ASSERT_OK(req.result);
uv_fs_req_cleanup(&req);
check_utime(symlink_path, atime, mtime, /* test_lutime */ 1);
ASSERT_OK(uv_fs_lutime(NULL,
&req,
symlink_path,
UV_FS_UTIME_OMIT,
UV_FS_UTIME_NOW,
NULL));
ASSERT_OK(req.result);
uv_fs_req_cleanup(&req);
check_utime(symlink_path, atime, UV_FS_UTIME_NOW, /* test_lutime */ 1);
/* Test the asynchronous version. */ /* Test the asynchronous version. */
atime = mtime = 1291404900; /* 2010-12-03 20:35:00 */ atime = mtime = 1291404900; /* 2010-12-03 20:35:00 */
@ -2953,11 +3089,12 @@ TEST_IMPL(fs_lutime) {
checkme.atime = atime; checkme.atime = atime;
checkme.mtime = mtime; checkme.mtime = mtime;
checkme.path = symlink_path; checkme.path = symlink_path;
req.data = &checkme;
r = uv_fs_lutime(loop, &req, symlink_path, atime, mtime, lutime_cb); r = uv_fs_lutime(loop, &req, symlink_path, atime, mtime, lutime_cb);
ASSERT_OK(r); ASSERT_OK(r);
uv_run(loop, UV_RUN_DEFAULT); uv_run(loop, UV_RUN_DEFAULT);
ASSERT_EQ(2, lutime_cb_count); ASSERT_EQ(1, lutime_cb_count);
/* Cleanup. */ /* Cleanup. */
unlink(path); unlink(path);