From 4bc18b09557747baf03cdc412102b4166627eba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Schu=CC=88mann?= Date: Sun, 12 Jul 2026 13:12:35 +0200 Subject: [PATCH] Preserve subsecond precision for `last_write_time()` and related filesystem metadata (#208) --- README.md | 2 ++ include/ghc/filesystem.hpp | 42 +++++++++++++++++++++++--------------- test/filesystem_test.cpp | 11 +++++++++- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 393712f..688a381 100644 --- a/README.md +++ b/README.md @@ -667,6 +667,8 @@ to the expected behavior. formatting now handles unknown error codes safely. * Fix for [#207](https://github.com/gulrak/filesystem/issues/207), `last_write_time()` now consistently follows symlinks. +* Fix for [#208](https://github.com/gulrak/filesystem/issues/208), filesystem + metadata now preserves subsecond modification times. * Fix for [#185](https://github.com/gulrak/filesystem/issues/185), `lexically_normal()` now preserves unresolved parent components in relative paths. diff --git a/include/ghc/filesystem.hpp b/include/ghc/filesystem.hpp index 3334dd9..25f270e 100644 --- a/include/ghc/filesystem.hpp +++ b/include/ghc/filesystem.hpp @@ -890,7 +890,7 @@ private: #ifndef GHC_OS_WINDOWS uintmax_t _hard_link_count = static_cast(-1); #endif - time_t _last_write_time = 0; + file_time_type _last_write_time = (file_time_type::min)(); }; // [fs.class.directory.iterator] Class directory_iterator @@ -2237,12 +2237,14 @@ GHC_INLINE path resolveSymlink(const path& p, std::error_code& ec) } #ifdef GHC_OS_WINDOWS -GHC_INLINE time_t timeFromFILETIME(const FILETIME& ft) +GHC_INLINE file_time_type timeFromFILETIME(const FILETIME& ft) { ULARGE_INTEGER ull; ull.LowPart = ft.dwLowDateTime; ull.HighPart = ft.dwHighDateTime; - return static_cast(ull.QuadPart / 10000000ULL - 11644473600ULL); + const int64_t ticks = static_cast(ull.QuadPart) - 116444736000000000LL; + const auto duration = std::chrono::duration>(ticks); + return file_time_type(std::chrono::duration_cast(duration)); } GHC_INLINE void timeToFILETIME(time_t t, FILETIME& ft) @@ -2292,7 +2294,7 @@ GHC_INLINE bool is_symlink_from_INFO(const path &, const WIN32_FIND_DATAW* info, } template -GHC_INLINE file_status status_from_INFO(const path& p, const INFO* info, std::error_code& ec, uintmax_t* sz = nullptr, time_t* lwt = nullptr) +GHC_INLINE file_status status_from_INFO(const path& p, const INFO* info, std::error_code& ec, uintmax_t* sz = nullptr, file_time_type* lwt = nullptr) { file_type ft = file_type::unknown; if (is_symlink_from_INFO(p, info, ec)) { @@ -2331,7 +2333,7 @@ GHC_INLINE bool is_not_found_error(std::error_code& ec) #endif } -GHC_INLINE file_status symlink_status_ex(const path& p, std::error_code& ec, uintmax_t* sz = nullptr, uintmax_t* nhl = nullptr, time_t* lwt = nullptr) noexcept +GHC_INLINE file_status symlink_status_ex(const path& p, std::error_code& ec, uintmax_t* sz = nullptr, uintmax_t* nhl = nullptr, file_time_type* lwt = nullptr) noexcept { #ifdef GHC_OS_WINDOWS file_status fs; @@ -2369,7 +2371,7 @@ GHC_INLINE file_status symlink_status_ex(const path& p, std::error_code& ec, uin #endif } -GHC_INLINE file_status status_ex(const path& p, std::error_code& ec, file_status* sls = nullptr, uintmax_t* sz = nullptr, uintmax_t* nhl = nullptr, time_t* lwt = nullptr, int recurse_count = 0) noexcept +GHC_INLINE file_status status_ex(const path& p, std::error_code& ec, file_status* sls = nullptr, uintmax_t* sz = nullptr, uintmax_t* nhl = nullptr, file_time_type* lwt = nullptr, int recurse_count = 0) noexcept { ec.clear(); #ifdef GHC_OS_WINDOWS @@ -2435,7 +2437,13 @@ GHC_INLINE file_status status_ex(const path& p, std::error_code& ec, file_status *nhl = st.st_nlink; } if (lwt) { - *lwt = st.st_mtime; +#ifdef GHC_OS_APPLE + const auto subsecond = std::chrono::nanoseconds(st.st_mtimespec.tv_nsec); +#else + const auto subsecond = std::chrono::nanoseconds(st.st_mtim.tv_nsec); +#endif + const auto duration = std::chrono::seconds(st.st_mtime) + subsecond; + *lwt = file_time_type(std::chrono::duration_cast(duration)); } return fs; } @@ -4655,10 +4663,10 @@ GHC_INLINE file_time_type last_write_time(const path& p) GHC_INLINE file_time_type last_write_time(const path& p, std::error_code& ec) noexcept { - time_t result = 0; + file_time_type result = (file_time_type::min)(); ec.clear(); file_status fs = detail::status_ex(p, ec, nullptr, nullptr, nullptr, &result); - return ec ? (file_time_type::min)() : std::chrono::system_clock::from_time_t(result); + return ec ? (file_time_type::min)() : result; } #ifdef GHC_WITH_EXCEPTIONS @@ -5270,7 +5278,7 @@ GHC_INLINE directory_entry::directory_entry(const filesystem::path& p) #ifndef GHC_OS_WINDOWS , _hard_link_count(static_cast(-1)) #endif - , _last_write_time(0) + , _last_write_time((file_time_type::min)()) { refresh(); } @@ -5282,7 +5290,7 @@ GHC_INLINE directory_entry::directory_entry(const filesystem::path& p, std::erro #ifndef GHC_OS_WINDOWS , _hard_link_count(static_cast(-1)) #endif - , _last_write_time(0) + , _last_write_time((file_time_type::min)()) { refresh(ec); } @@ -5530,8 +5538,8 @@ GHC_INLINE uintmax_t directory_entry::hard_link_count(std::error_code& ec) const #ifdef GHC_WITH_EXCEPTIONS GHC_INLINE file_time_type directory_entry::last_write_time() const { - if (_last_write_time != 0) { - return std::chrono::system_clock::from_time_t(_last_write_time); + if (_last_write_time != (file_time_type::min)()) { + return _last_write_time; } return filesystem::last_write_time(path()); } @@ -5539,9 +5547,9 @@ GHC_INLINE file_time_type directory_entry::last_write_time() const GHC_INLINE file_time_type directory_entry::last_write_time(std::error_code& ec) const noexcept { - if (_last_write_time != 0) { + if (_last_write_time != (file_time_type::min)()) { ec.clear(); - return std::chrono::system_clock::from_time_t(_last_write_time); + return _last_write_time; } return filesystem::last_write_time(path(), ec); } @@ -5716,7 +5724,7 @@ public: } else { _dir_entry._file_size = static_cast(-1); - _dir_entry._last_write_time = 0; + _dir_entry._last_write_time = (file_time_type::min)(); } } } @@ -5805,7 +5813,7 @@ public: } _dir_entry._file_size = static_cast(-1); _dir_entry._hard_link_count = static_cast(-1); - _dir_entry._last_write_time = 0; + _dir_entry._last_write_time = (file_time_type::min)(); } path _base; directory_options _options; diff --git a/test/filesystem_test.cpp b/test/filesystem_test.cpp index 50caacf..8244d37 100644 --- a/test/filesystem_test.cpp +++ b/test/filesystem_test.cpp @@ -1820,8 +1820,11 @@ TEST_CASE("fs.op.copy_file - copy_file", "[filesystem][operations][fs.op.copy_fi CHECK(fs::file_size("foo") == fs::file_size("bar")); CHECK(fs::copy_file("foo", "bar2", ec)); CHECK(!ec); - std::this_thread::sleep_for(std::chrono::seconds(1)); generateFile("foo2", 200); + const auto copyTime = fs::file_time_type(std::chrono::seconds(1445401800)); + fs::last_write_time("foo", copyTime); + fs::last_write_time("bar", copyTime + std::chrono::milliseconds(100)); + fs::last_write_time("foo2", copyTime + std::chrono::milliseconds(200)); CHECK(fs::copy_file("foo2", "bar", fs::copy_options::update_existing)); CHECK(fs::file_size("bar") == 200); CHECK(!fs::copy_file("foo", "bar", fs::copy_options::update_existing)); @@ -2568,6 +2571,12 @@ TEST_CASE("fs.op.last_write_time - last_write_time", "[filesystem][operations][f CHECK_NOTHROW(fs::last_write_time("foo", nt, ec)); CHECK(std::abs(std::chrono::duration_cast(fs::last_write_time("foo") - nt).count()) < 1); CHECK(!ec); + const auto preciseTime = nt + std::chrono::milliseconds(123); + CHECK_NOTHROW(fs::last_write_time("foo", preciseTime, ec)); + REQUIRE(!ec); + CHECK(fs::last_write_time("foo") == preciseTime); + const fs::directory_entry preciseEntry("foo"); + CHECK(preciseEntry.last_write_time() == preciseTime); if (is_symlink_creation_supported()) { #if !defined(GHC_OS_WINDOWS) && !defined(GHC_OS_WEB) struct ::stat linkStatBefore;