diff --git a/test/filesystem.cc b/test/filesystem.cc index a9a73f6a..c1a912d3 100644 --- a/test/filesystem.cc +++ b/test/filesystem.cc @@ -15,7 +15,9 @@ #include "test/filesystem.h" #include +#include #include +#include #include #include "base/logging.h" @@ -25,6 +27,7 @@ #include "test/scoped_temp_dir.h" #include "util/file/file_io.h" #include "util/file/filesystem.h" +#include "util/misc/time.h" #if defined(OS_POSIX) #include @@ -116,6 +119,63 @@ bool PathExists(const base::FilePath& path) { #endif } +bool SetFileModificationTime(const base::FilePath& path, + const timespec& mtime) { +#if defined(OS_MACOSX) + // utimensat() isn't available on macOS until 10.13, so lutimes() is used + // instead. + struct stat st; + if (lstat(path.value().c_str(), &st) != 0) { + PLOG(ERROR) << "lstat " << path.value(); + return false; + } + timeval times[2]; + EXPECT_TRUE(TimespecToTimeval(st.st_atimespec, ×[0])); + EXPECT_TRUE(TimespecToTimeval(mtime, ×[1])); + if (lutimes(path.value().c_str(), times) != 0) { + PLOG(ERROR) << "lutimes " << path.value(); + return false; + } + return true; +#elif defined(OS_POSIX) + timespec times[2]; + times[0].tv_sec = 0; + times[0].tv_nsec = UTIME_OMIT; + times[1] = mtime; + if (utimensat(AT_FDCWD, path.value().c_str(), times, AT_SYMLINK_NOFOLLOW) != + 0) { + PLOG(ERROR) << "utimensat " << path.value(); + return false; + } + return true; +#elif defined(OS_WIN) + DWORD flags = FILE_FLAG_OPEN_REPARSE_POINT; + if (IsDirectory(path, true)) { + // required for directory handles + flags |= FILE_FLAG_BACKUP_SEMANTICS; + } + + ScopedFileHandle handle(::CreateFile(path.value().c_str(), + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + flags, + nullptr)); + if (!handle.is_valid()) { + PLOG(ERROR) << "CreateFile " << base::UTF16ToUTF8(path.value()); + return false; + } + + FILETIME filetime = TimespecToFiletimeEpoch(mtime); + if (!SetFileTime(handle.get(), nullptr, nullptr, &filetime)) { + PLOG(ERROR) << "SetFileTime " << base::UTF16ToUTF8(path.value()); + return false; + } + return true; +#endif // OS_MACOSX +} + #if !defined(OS_FUCHSIA) bool CanCreateSymbolicLinks() { diff --git a/test/filesystem.h b/test/filesystem.h index 7e658456..516335bb 100644 --- a/test/filesystem.h +++ b/test/filesystem.h @@ -17,15 +17,26 @@ #include "base/files/file_path.h" +#include + #include "build/build_config.h" namespace crashpad { namespace test { -bool CreateFile(const base::FilePath& file); +//! \brief Creates an empty file at path \a filepath. +bool CreateFile(const base::FilePath& filepath); +//! \brief Returns `true` if a filesystem node exists at path \a path. bool PathExists(const base::FilePath& path); +//! \brief Sets the modification time for a file, directory, or symbolic link. +//! +//! \param[in] path The path to the file to set the modification time for. +//! \param[in] mtime The new modification time for the file. +//! \return `true` on success. Otherwise `false` with a message logged. +bool SetFileModificationTime(const base::FilePath& path, const timespec& mtime); + #if !defined(OS_FUCHSIA) || DOXYGEN // There are no symbolic links on Fuchsia. Don’t bother declaring or defining // symbolic link-related functions at all, because it’s an error to even pretend @@ -48,6 +59,11 @@ bool PathExists(const base::FilePath& path); //! in Windows 10! bool CanCreateSymbolicLinks(); +//! \brief Creates a new symbolic link. +//! +//! \param[in] target_path The target for the link. +//! \param[in] symlink_path The name for the new link. +//! \return `true` on success. Otherwise `false` with a message logged. bool CreateSymbolicLink(const base::FilePath& target_path, const base::FilePath& symlink_path); diff --git a/util/file/filesystem.h b/util/file/filesystem.h index acdd8d81..27a4d1de 100644 --- a/util/file/filesystem.h +++ b/util/file/filesystem.h @@ -15,12 +15,21 @@ #ifndef CRASHPAD_UTIL_FILE_FILESYSTEM_H_ #define CRASHPAD_UTIL_FILE_FILESYSTEM_H_ -#include "base/files/file_path.h" +#include +#include "base/files/file_path.h" #include "util/file/file_io.h" namespace crashpad { +//! \brief Determines the modification time for a file, directory, or symbolic +//! link, logging a message on failure. +//! +//! \param[in] path The file to get the modification time for. +//! \param[out] mtime The modification time as seconds since the POSIX Epoch. +//! \return `true` on success. `false` on failure with a message logged. +bool FileModificationTime(const base::FilePath& path, timespec* mtime); + //! \brief Creates a directory, logging a message on failure. //! //! \param[in] path The path to the directory to create. diff --git a/util/file/filesystem_posix.cc b/util/file/filesystem_posix.cc index a35db1cd..888dfb7f 100644 --- a/util/file/filesystem_posix.cc +++ b/util/file/filesystem_posix.cc @@ -21,9 +21,25 @@ #include #include "base/logging.h" +#include "build/build_config.h" namespace crashpad { +bool FileModificationTime(const base::FilePath& path, timespec* mtime) { + struct stat st; + if (lstat(path.value().c_str(), &st) != 0) { + PLOG(ERROR) << "lstat " << path.value(); + return false; + } + +#if defined(OS_MACOSX) + *mtime = st.st_mtimespec; +#else + *mtime = st.st_mtim; +#endif + return true; +} + bool LoggingCreateDirectory(const base::FilePath& path, FilePermissions permissions, bool may_reuse) { diff --git a/util/file/filesystem_test.cc b/util/file/filesystem_test.cc index 7e28892e..593b478d 100644 --- a/util/file/filesystem_test.cc +++ b/util/file/filesystem_test.cc @@ -17,6 +17,7 @@ #include "base/logging.h" #include "build/build_config.h" #include "gtest/gtest.h" +#include "test/errors.h" #include "test/filesystem.h" #include "test/gtest_disabled.h" #include "test/scoped_temp_dir.h" @@ -25,6 +26,109 @@ namespace crashpad { namespace test { namespace { +bool CurrentTime(timespec* now) { +#if defined(OS_POSIX) + int res = clock_gettime(CLOCK_REALTIME, now); + if (res != 0) { + EXPECT_EQ(res, 0) << ErrnoMessage("clock_gettime"); + return false; + } + return true; +#else + int res = timespec_get(now, TIME_UTC); + if (res != TIME_UTC) { + EXPECT_EQ(res, TIME_UTC); + return false; + } + return true; +#endif +} + +TEST(Filesystem, FileModificationTime) { + timespec expected_time_start, expected_time_end; + + ASSERT_TRUE(CurrentTime(&expected_time_start)); + ScopedTempDir temp_dir; + ASSERT_TRUE(CurrentTime(&expected_time_end)); + + timespec dir_mtime; + ASSERT_TRUE(FileModificationTime(temp_dir.path(), &dir_mtime)); + EXPECT_GE(dir_mtime.tv_sec, expected_time_start.tv_sec - 2); + EXPECT_LE(dir_mtime.tv_sec, expected_time_end.tv_sec + 2); + + base::FilePath file(temp_dir.path().Append(FILE_PATH_LITERAL("file"))); + ASSERT_TRUE(CurrentTime(&expected_time_start)); + ASSERT_TRUE(CreateFile(file)); + ASSERT_TRUE(CurrentTime(&expected_time_end)); + + timespec file_mtime; + ASSERT_TRUE(FileModificationTime(file, &file_mtime)); + EXPECT_GE(file_mtime.tv_sec, expected_time_start.tv_sec - 2); + EXPECT_LE(file_mtime.tv_sec, expected_time_end.tv_sec + 2); + + timespec file_mtime_again; + ASSERT_TRUE(FileModificationTime(file, &file_mtime_again)); + EXPECT_EQ(file_mtime.tv_sec, file_mtime_again.tv_sec); + EXPECT_EQ(file_mtime.tv_nsec, file_mtime_again.tv_nsec); + + timespec mtime; + EXPECT_FALSE(FileModificationTime(base::FilePath(), &mtime)); + EXPECT_FALSE(FileModificationTime( + temp_dir.path().Append(FILE_PATH_LITERAL("notafile")), &mtime)); +} + +#if !defined(OS_FUCHSIA) + +TEST(Filesystem, FileModificationTime_SymbolicLinks) { + if (!CanCreateSymbolicLinks()) { + DISABLED_TEST(); + } + + ScopedTempDir temp_dir; + + const base::FilePath dir_link( + temp_dir.path().Append(FILE_PATH_LITERAL("dir_link"))); + + timespec expected_time_start, expected_time_end; + ASSERT_TRUE(CurrentTime(&expected_time_start)); + ASSERT_TRUE(CreateSymbolicLink(temp_dir.path(), dir_link)); + ASSERT_TRUE(CurrentTime(&expected_time_end)); + + timespec mtime, mtime2; + ASSERT_TRUE(FileModificationTime(temp_dir.path(), &mtime)); + mtime.tv_sec -= 100; + ASSERT_TRUE(SetFileModificationTime(temp_dir.path(), mtime)); + ASSERT_TRUE(FileModificationTime(temp_dir.path(), &mtime2)); + EXPECT_GE(mtime2.tv_sec, mtime.tv_sec - 2); + EXPECT_LE(mtime2.tv_sec, mtime.tv_sec + 2); + + ASSERT_TRUE(FileModificationTime(dir_link, &mtime)); + EXPECT_GE(mtime.tv_sec, expected_time_start.tv_sec - 2); + EXPECT_LE(mtime.tv_sec, expected_time_end.tv_sec + 2); + + base::FilePath file(temp_dir.path().Append(FILE_PATH_LITERAL("file"))); + ASSERT_TRUE(CreateFile(file)); + + base::FilePath link(temp_dir.path().Append(FILE_PATH_LITERAL("link"))); + + ASSERT_TRUE(CurrentTime(&expected_time_start)); + ASSERT_TRUE(CreateSymbolicLink(file, link)); + ASSERT_TRUE(CurrentTime(&expected_time_end)); + + ASSERT_TRUE(FileModificationTime(file, &mtime)); + mtime.tv_sec -= 100; + ASSERT_TRUE(SetFileModificationTime(file, mtime)); + ASSERT_TRUE(FileModificationTime(file, &mtime2)); + EXPECT_GE(mtime2.tv_sec, mtime.tv_sec - 2); + EXPECT_LE(mtime2.tv_sec, mtime.tv_sec + 2); + + ASSERT_TRUE(FileModificationTime(link, &mtime)); + EXPECT_GE(mtime.tv_sec, expected_time_start.tv_sec - 2); + EXPECT_LE(mtime.tv_sec, expected_time_end.tv_sec + 2); +} + +#endif // !OS_FUCHSIA + TEST(Filesystem, CreateDirectory) { ScopedTempDir temp_dir; diff --git a/util/file/filesystem_win.cc b/util/file/filesystem_win.cc index b874ed8c..a465eacc 100644 --- a/util/file/filesystem_win.cc +++ b/util/file/filesystem_win.cc @@ -14,10 +14,12 @@ #include "util/file/filesystem.h" +#include #include #include "base/logging.h" #include "base/strings/utf_string_conversions.h" +#include "util/misc/time.h" namespace crashpad { @@ -50,6 +52,35 @@ bool LoggingRemoveDirectoryImpl(const base::FilePath& path) { } // namespace +bool FileModificationTime(const base::FilePath& path, timespec* mtime) { + DWORD flags = FILE_FLAG_OPEN_REPARSE_POINT; + if (IsDirectory(path, true)) { + // required for directory handles + flags |= FILE_FLAG_BACKUP_SEMANTICS; + } + + ScopedFileHandle handle( + ::CreateFile(path.value().c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, + OPEN_EXISTING, + flags, + nullptr)); + if (!handle.is_valid()) { + PLOG(ERROR) << "CreateFile " << base::UTF16ToUTF8(path.value()); + return false; + } + + FILETIME file_mtime; + if (!GetFileTime(handle.get(), nullptr, nullptr, &file_mtime)) { + PLOG(ERROR) << "GetFileTime " << base::UTF16ToUTF8(path.value()); + return false; + } + *mtime = FiletimeToTimespecEpoch(file_mtime); + return true; +} + bool LoggingCreateDirectory(const base::FilePath& path, FilePermissions permissions, bool may_reuse) {