Squashed commit of the following:

commit 16e1157d4e0a484c5b0045551ce0e083e566d582
Author: Steffen Schümann <s.schuemann@pobox.com>
Date:   Sun Jul 12 14:53:28 2026 +0200

    Improve file time precision checks and initialize `directory_entry`'s `_last_write_time`

commit 4bc18b09557747baf03cdc412102b4166627eba9
Author: Steffen Schümann <s.schuemann@pobox.com>
Date:   Sun Jul 12 13:12:35 2026 +0200

    Preserve subsecond precision for `last_write_time()` and related filesystem metadata (#208)
This commit is contained in:
Steffen Schümann 2026-07-12 15:11:57 +02:00
parent 38f91bd470
commit 73212968f6
3 changed files with 42 additions and 20 deletions

View File

@ -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.

View File

@ -805,7 +805,10 @@ class GHC_FS_API_CLASS directory_entry
{
public:
// [fs.dir.entry.cons] constructors and destructor
directory_entry() noexcept = default;
directory_entry() noexcept
: _last_write_time((file_time_type::min)())
{
}
directory_entry(const directory_entry&) = default;
directory_entry(directory_entry&&) noexcept = default;
#ifdef GHC_WITH_EXCEPTIONS
@ -890,7 +893,7 @@ private:
#ifndef GHC_OS_WINDOWS
uintmax_t _hard_link_count = static_cast<uintmax_t>(-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 +2240,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<time_t>(ull.QuadPart / 10000000ULL - 11644473600ULL);
const int64_t ticks = static_cast<int64_t>(ull.QuadPart) - 116444736000000000LL;
const auto duration = std::chrono::duration<int64_t, std::ratio<1, 10000000>>(ticks);
return file_time_type(std::chrono::duration_cast<file_time_type::duration>(duration));
}
GHC_INLINE void timeToFILETIME(time_t t, FILETIME& ft)
@ -2292,7 +2297,7 @@ GHC_INLINE bool is_symlink_from_INFO(const path &, const WIN32_FIND_DATAW* info,
}
template <typename INFO>
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 +2336,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 +2374,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 +2440,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<file_time_type::duration>(duration));
}
return fs;
}
@ -4655,10 +4666,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
@ -4679,7 +4690,7 @@ GHC_INLINE void last_write_time(const path& p, file_time_type new_time, std::err
#ifdef GHC_OS_WINDOWS
detail::unique_handle file(::CreateFileW(GHC_NATIVEWP(p), FILE_WRITE_ATTRIBUTES, FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL));
FILETIME ft;
auto tt = std::chrono::duration_cast<std::chrono::microseconds>(d).count() * 10 + 116444736000000000;
const auto tt = std::chrono::duration_cast<std::chrono::duration<int64_t, std::ratio<1, 10000000>>>(d).count() + 116444736000000000LL;
ft.dwLowDateTime = static_cast<DWORD>(tt);
ft.dwHighDateTime = static_cast<DWORD>(tt >> 32);
if (!::SetFileTime(file.get(), 0, 0, &ft)) {
@ -5270,7 +5281,7 @@ GHC_INLINE directory_entry::directory_entry(const filesystem::path& p)
#ifndef GHC_OS_WINDOWS
, _hard_link_count(static_cast<uintmax_t>(-1))
#endif
, _last_write_time(0)
, _last_write_time((file_time_type::min)())
{
refresh();
}
@ -5282,7 +5293,7 @@ GHC_INLINE directory_entry::directory_entry(const filesystem::path& p, std::erro
#ifndef GHC_OS_WINDOWS
, _hard_link_count(static_cast<uintmax_t>(-1))
#endif
, _last_write_time(0)
, _last_write_time((file_time_type::min)())
{
refresh(ec);
}
@ -5530,8 +5541,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 +5550,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 +5727,7 @@ public:
}
else {
_dir_entry._file_size = static_cast<uintmax_t>(-1);
_dir_entry._last_write_time = 0;
_dir_entry._last_write_time = (file_time_type::min)();
}
}
}
@ -5805,7 +5816,7 @@ public:
}
_dir_entry._file_size = static_cast<uintmax_t>(-1);
_dir_entry._hard_link_count = static_cast<uintmax_t>(-1);
_dir_entry._last_write_time = 0;
_dir_entry._last_write_time = (file_time_type::min)();
}
path _base;
directory_options _options;

View File

@ -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<std::chrono::seconds>(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(std::abs(std::chrono::duration_cast<std::chrono::nanoseconds>(fs::last_write_time("foo") - preciseTime).count()) < 1000000);
const fs::directory_entry preciseEntry("foo");
CHECK(std::abs(std::chrono::duration_cast<std::chrono::nanoseconds>(preciseEntry.last_write_time() - preciseTime).count()) < 1000000);
if (is_symlink_creation_supported()) {
#if !defined(GHC_OS_WINDOWS) && !defined(GHC_OS_WEB)
struct ::stat linkStatBefore;