From 85b526f56ac0ca5e76de5cbc0e1e9452f73a01d5 Mon Sep 17 00:00:00 2001 From: Ben Noordhuis Date: Fri, 21 Feb 2025 23:08:15 +0100 Subject: [PATCH] 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 --- docs/src/fs.rst | 6 ++ include/uv.h | 3 + src/unix/fs.c | 99 +++++++++------------ src/uv-common.h | 19 ++++ src/win/fs.c | 25 ++++-- test/test-fs.c | 227 ++++++++++++++++++++++++++++++++++++++---------- 6 files changed, 273 insertions(+), 106 deletions(-) diff --git a/docs/src/fs.rst b/docs/src/fs.rst index 7bc8d0cb..01a48e8e 100644 --- a/docs/src/fs.rst +++ b/docs/src/fs.rst @@ -430,6 +430,12 @@ API 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:: z/OS: `uv_fs_lutime()` is not implemented for z/OS. It can still be called but will return ``UV_ENOSYS``. diff --git a/include/uv.h b/include/uv.h index f0ec376b..46dedc9e 100644 --- a/include/uv.h +++ b/include/uv.h @@ -58,6 +58,7 @@ extern "C" { #include #include #include +#include /* Internal type, do not use. */ struct uv__queue { @@ -1585,6 +1586,8 @@ UV_EXTERN int uv_fs_chmod(uv_loop_t* loop, const char* path, int mode, 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_fs_t* req, const char* path, diff --git a/src/unix/fs.c b/src/unix/fs.c index 1631d934..717f3fab 100644 --- a/src/unix/fs.c +++ b/src/unix/fs.c @@ -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; + + 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_nsec = (time - ts.tv_sec) * 1e9; @@ -221,41 +236,23 @@ UV_UNUSED(static struct timespec uv__fs_to_timespec(double time)) { } 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) { -#if defined(__linux__) \ +#if defined(__APPLE__) \ || defined(_AIX71) \ + || defined(__DragonFly__) \ + || defined(__FreeBSD__) \ || defined(__HAIKU__) \ - || defined(__GNU__) + || defined(__NetBSD__) \ + || defined(__OpenBSD__) \ + || defined(__linux__) \ + || defined(__sun) struct timespec ts[2]; ts[0] = uv__fs_to_timespec(req->atime); ts[1] = uv__fs_to_timespec(req->mtime); 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__) attrib_t 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) { -#if defined(__linux__) \ - || defined(_AIX71) \ - || defined(__sun) \ - || defined(__HAIKU__) +#if defined(__APPLE__) \ + || defined(_AIX71) \ + || defined(__DragonFly__) \ + || defined(__FreeBSD__) \ + || defined(__HAIKU__) \ + || defined(__NetBSD__) \ + || defined(__OpenBSD__) \ + || defined(__linux__) \ + || defined(__sun) struct timespec ts[2]; ts[0] = uv__fs_to_timespec(req->atime); ts[1] = uv__fs_to_timespec(req->mtime); return utimensat(AT_FDCWD, req->path, ts, 0); -#elif defined(__APPLE__) \ - || 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) +#elif defined(_AIX) && !defined(_AIX71) struct utimbuf buf; buf.actime = req->atime; 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) { -#if defined(__linux__) || \ - defined(_AIX71) || \ - defined(__sun) || \ - defined(__HAIKU__) || \ - defined(__GNU__) || \ - defined(__OpenBSD__) +#if defined(__APPLE__) \ + || defined(_AIX71) \ + || defined(__DragonFly__) \ + || defined(__FreeBSD__) \ + || defined(__HAIKU__) \ + || defined(__NetBSD__) \ + || defined(__OpenBSD__) \ + || defined(__linux__) \ + || defined(__sun) struct timespec ts[2]; ts[0] = uv__fs_to_timespec(req->atime); ts[1] = uv__fs_to_timespec(req->mtime); 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 errno = ENOSYS; return -1; diff --git a/src/uv-common.h b/src/uv-common.h index 372f0c4b..8e779ba5 100644 --- a/src/uv-common.h +++ b/src/uv-common.h @@ -31,6 +31,7 @@ #include #include #include +#include #include "uv.h" #include "uv/tree.h" @@ -448,4 +449,22 @@ struct uv__loop_internal_fields_s { # define UV_PTHREAD_MAX_NAMELEN_NP 16 #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_ */ diff --git a/src/win/fs.c b/src/win/fs.c index b4ed760e..27248f64 100644 --- a/src/win/fs.c +++ b/src/win/fs.c @@ -2580,14 +2580,29 @@ fchmod_cleanup: 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); - TIME_T_TO_FILETIME(mtime, &filetime_m); + if (uv__isinf(atime) || uv__isinf(mtime)) + 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 0; } diff --git a/test/test-fs.c b/test/test-fs.c index 2519e44c..4761b15b 100644 --- a/test/test-fs.c +++ b/test/test-fs.c @@ -59,6 +59,18 @@ #define TOO_LONG_NAME_LENGTH 65536 #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 { const char* path; double atime; @@ -827,43 +839,70 @@ static void check_utime(const char* path, ASSERT_OK(req.result); s = &req.statbuf; - if (s->st_atim.tv_nsec == 0 && s->st_mtim.tv_nsec == 0) { - /* - * Test sub-second timestamps only when supported (such as Windows with + if (isfinite(atime)) { + /* 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. */ -#ifdef _WIN32 - ASSERT_DOUBLE_EQ(atime, (long) atime); - ASSERT_DOUBLE_EQ(mtime, (long) atime); -#endif - if (atime > 0 || (long) atime == atime) - ASSERT_EQ(s->st_atim.tv_sec, (long) atime); - if (mtime > 0 || (long) mtime == mtime) - ASSERT_EQ(s->st_mtim.tv_sec, (long) mtime); - ASSERT_GE(s->st_atim.tv_sec, (long) atime - 1); - ASSERT_GE(s->st_mtim.tv_sec, (long) mtime - 1); - ASSERT_LE(s->st_atim.tv_sec, (long) atime); - ASSERT_LE(s->st_mtim.tv_sec, (long) mtime); - } else { - double st_atim; - double st_mtim; -#if !defined(__APPLE__) && !defined(__SUNPRO_C) - /* TODO(vtjnash): would it be better to normalize this? */ - ASSERT_DOUBLE_GE(s->st_atim.tv_nsec, 0); - ASSERT_DOUBLE_GE(s->st_mtim.tv_nsec, 0); -#endif - st_atim = s->st_atim.tv_sec + s->st_atim.tv_nsec / 1e9; - st_mtim = s->st_mtim.tv_sec + s->st_mtim.tv_nsec / 1e9; - /* - * Linux does not allow reading reliably the atime of a symlink - * since readlink() can update it + if (s->st_atim.tv_nsec == 0) { + if (is_win32) + ASSERT_DOUBLE_EQ(atime, (long) atime); + if (atime > 0 || (long) atime == atime) + ASSERT_EQ(s->st_atim.tv_sec, (long) atime); + ASSERT_GE(s->st_atim.tv_sec, (long) atime - 1); + ASSERT_LE(s->st_atim.tv_sec, (long) atime); + } else { + double st_atim; + /* TODO(vtjnash): would it be better to normalize this? */ + if (!is_apple_or_sunpro_c) + ASSERT_DOUBLE_GE(s->st_atim.tv_nsec, 0); + st_atim = s->st_atim.tv_sec + s->st_atim.tv_nsec / 1e9; + /* Linux does not allow reading reliably the atime of a symlink + * since readlink() can update it + */ + if (!test_lutime) + ASSERT_DOUBLE_EQ(st_atim, atime); + } + } else if (isinf(atime)) { + /* 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. */ - if (!test_lutime) - ASSERT_DOUBLE_EQ(st_atim, atime); - ASSERT_DOUBLE_EQ(st_mtim, mtime); + ASSERT_GT(s->st_atim.tv_sec, 1739710000); + } else { + 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); @@ -2728,13 +2767,46 @@ TEST_IMPL(fs_utime) { atime = mtime = 400497753.25; /* 1982-09-10 11:22:33.25 */ - r = uv_fs_utime(NULL, &req, path, atime, mtime, NULL); - ASSERT_OK(r); + 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_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 */ checkme.path = path; checkme.atime = atime; @@ -2868,9 +2940,43 @@ TEST_IMPL(fs_futime) { ASSERT_OK(req.result); #endif 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_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 */ checkme.atime = atime; @@ -2932,20 +3038,50 @@ TEST_IMPL(fs_lutime) { /* Test the synchronous version. */ 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); -#if (defined(_AIX) && !defined(_AIX71)) || \ - defined(__MVS__) +#if (defined(_AIX) && !defined(_AIX71)) || defined(__MVS__) ASSERT_EQ(r, UV_ENOSYS); RETURN_SKIP("lutime is not implemented for z/OS and AIX versions below 7.1"); #endif ASSERT_OK(r); - lutime_cb(&req); - ASSERT_EQ(1, lutime_cb_count); + 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_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. */ atime = mtime = 1291404900; /* 2010-12-03 20:35:00 */ @@ -2953,11 +3089,12 @@ TEST_IMPL(fs_lutime) { checkme.atime = atime; checkme.mtime = mtime; checkme.path = symlink_path; + req.data = &checkme; r = uv_fs_lutime(loop, &req, symlink_path, atime, mtime, lutime_cb); ASSERT_OK(r); uv_run(loop, UV_RUN_DEFAULT); - ASSERT_EQ(2, lutime_cb_count); + ASSERT_EQ(1, lutime_cb_count); /* Cleanup. */ unlink(path);