| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <windows.h> |
| |
| #include <shlobj.h> |
| |
| #include <iterator> |
| #include <memory> |
| #include <string> |
| #include <tuple> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/macros.h" |
| #include "base/strings/string_util.h" |
| #include "base/win/scoped_handle.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #define FPL FILE_PATH_LITERAL |
| |
| namespace base { |
| |
| // A basic test harness that creates a temporary directory during setup and |
| // deletes it during teardown. |
| class OsValidationTest : public ::testing::Test { |
| protected: |
| OsValidationTest() = default; |
| |
| // ::testing::Test: |
| void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); } |
| void TearDown() override { ASSERT_TRUE(temp_dir_.Delete()); } |
| |
| // Returns the path to the test's temporary directory. |
| const FilePath& temp_path() const { return temp_dir_.GetPath(); } |
| |
| private: |
| ScopedTempDir temp_dir_; |
| |
| DISALLOW_COPY_AND_ASSIGN(OsValidationTest); |
| }; |
| |
| // A test harness for exhaustively evaluating the conditions under which an open |
| // file may be operated on. Template parameters are used to turn off or on |
| // various bits in the access rights and sharing mode bitfields. These template |
| // parameters are: |
| // - The standard access right bits (except for WRITE_OWNER, which requires |
| // admin rights): SYNCHRONIZE, WRITE_DAC, READ_CONTROL, DELETE. |
| // - Generic file access rights: FILE_GENERIC_READ, FILE_GENERIC_WRITE, |
| // FILE_EXECUTE. |
| // - The sharing bits: FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE. |
| class OpenFileTest : public OsValidationTest, |
| public ::testing::WithParamInterface< |
| std::tuple<std::tuple<DWORD, DWORD, DWORD, DWORD>, |
| std::tuple<DWORD, DWORD, DWORD>, |
| std::tuple<DWORD, DWORD, DWORD>>> { |
| protected: |
| OpenFileTest() = default; |
| |
| // Returns a dwDesiredAccess bitmask for use with CreateFileW containing the |
| // test's access right bits. |
| static DWORD GetAccess() { |
| // Extract the two tuples of standard and generic file rights. |
| std::tuple<DWORD, DWORD, DWORD, DWORD> standard_rights; |
| std::tuple<DWORD, DWORD, DWORD> generic_rights; |
| std::tie(standard_rights, generic_rights, std::ignore) = GetParam(); |
| |
| // Extract the five standard rights bits. |
| DWORD synchronize_bit; |
| DWORD write_dac_bit; |
| DWORD read_control_bit; |
| DWORD delete_bit; |
| std::tie(synchronize_bit, write_dac_bit, read_control_bit, delete_bit) = |
| standard_rights; |
| |
| // Extract the three generic file rights masks. |
| DWORD file_generic_read_bits; |
| DWORD file_generic_write_bits; |
| DWORD file_generic_execute_bits; |
| std::tie(file_generic_read_bits, file_generic_write_bits, |
| file_generic_execute_bits) = generic_rights; |
| |
| // Combine and return the desired access rights. |
| return synchronize_bit | write_dac_bit | read_control_bit | delete_bit | |
| file_generic_read_bits | file_generic_write_bits | |
| file_generic_execute_bits; |
| } |
| |
| // Returns a dwShareMode bitmask for use with CreateFileW containing the |
| // tests's share mode bits. |
| static DWORD GetShareMode() { |
| // Extract the tuple of sharing mode bits. |
| std::tuple<DWORD, DWORD, DWORD> sharing_bits; |
| std::tie(std::ignore, std::ignore, sharing_bits) = GetParam(); |
| |
| // Extract the sharing mode bits. |
| DWORD share_read_bit; |
| DWORD share_write_bit; |
| DWORD share_delete_bit; |
| std::tie(share_read_bit, share_write_bit, share_delete_bit) = sharing_bits; |
| |
| // Combine and return the sharing mode. |
| return share_read_bit | share_write_bit | share_delete_bit; |
| } |
| |
| // Appends string representation of the access rights bits present in |access| |
| // to |result|. |
| static void AppendAccessString(DWORD access, std::string* result) { |
| #define ENTRY(a) \ |
| { a, #a } |
| static constexpr BitAndName kBitNames[] = { |
| // The standard access rights: |
| ENTRY(SYNCHRONIZE), |
| ENTRY(WRITE_OWNER), |
| ENTRY(WRITE_DAC), |
| ENTRY(READ_CONTROL), |
| ENTRY(DELETE), |
| // The file-specific access rights: |
| ENTRY(FILE_WRITE_ATTRIBUTES), |
| ENTRY(FILE_READ_ATTRIBUTES), |
| ENTRY(FILE_EXECUTE), |
| ENTRY(FILE_WRITE_EA), |
| ENTRY(FILE_READ_EA), |
| ENTRY(FILE_APPEND_DATA), |
| ENTRY(FILE_WRITE_DATA), |
| ENTRY(FILE_READ_DATA), |
| }; |
| #undef ENTRY |
| ASSERT_NO_FATAL_FAILURE(AppendBitsToString(access, std::begin(kBitNames), |
| std::end(kBitNames), result)); |
| } |
| |
| // Appends a string representation of the sharing mode bits present in |
| // |share_mode| to |result|. |
| static void AppendShareModeString(DWORD share_mode, std::string* result) { |
| #define ENTRY(a) \ |
| { a, #a } |
| static constexpr BitAndName kBitNames[] = { |
| ENTRY(FILE_SHARE_DELETE), |
| ENTRY(FILE_SHARE_WRITE), |
| ENTRY(FILE_SHARE_READ), |
| }; |
| #undef ENTRY |
| ASSERT_NO_FATAL_FAILURE(AppendBitsToString( |
| share_mode, std::begin(kBitNames), std::end(kBitNames), result)); |
| } |
| |
| // Returns true if we expect that a file opened with |access| access rights |
| // and |share_mode| sharing can be deleted via DeleteFile and/or moved via |
| // MoveFileEx. |
| static bool CanDeleteFile(DWORD access, DWORD share_mode) { |
| // A file can be deleted as long as it is opened with FILE_SHARE_DELETE or |
| // if nothing beyond the standard access rights (save DELETE) has been |
| // requested. |
| constexpr DWORD kStandardNoDelete = STANDARD_RIGHTS_ALL & ~DELETE; |
| return ((share_mode & FILE_SHARE_DELETE) != 0) || |
| ((access & ~kStandardNoDelete) == 0); |
| } |
| |
| // OsValidationTest: |
| void SetUp() override { |
| OsValidationTest::SetUp(); |
| |
| // Determine the desired access and share mode for this test. |
| access_ = GetAccess(); |
| share_mode_ = GetShareMode(); |
| |
| // Make a ScopedTrace instance for comprehensible output. |
| std::string access_string; |
| ASSERT_NO_FATAL_FAILURE(AppendAccessString(access_, &access_string)); |
| std::string share_mode_string; |
| ASSERT_NO_FATAL_FAILURE( |
| AppendShareModeString(share_mode_, &share_mode_string)); |
| scoped_trace_ = std::make_unique<::testing::ScopedTrace>( |
| __FILE__, __LINE__, access_string + ", " + share_mode_string); |
| |
| // Create a file on which to operate. |
| ASSERT_TRUE(CreateTemporaryFileInDir(temp_path(), &temp_file_path_)); |
| |
| // Open the file |
| file_handle_.Set(::CreateFileW(as_wcstr(temp_file_path_.value()), access_, |
| share_mode_, nullptr, OPEN_EXISTING, |
| FILE_ATTRIBUTE_NORMAL, nullptr)); |
| ASSERT_TRUE(file_handle_.IsValid()) << ::GetLastError(); |
| } |
| |
| void TearDown() override { file_handle_.Close(); } |
| |
| DWORD access() const { return access_; } |
| DWORD share_mode() const { return share_mode_; } |
| const FilePath& temp_file_path() const { return temp_file_path_; } |
| |
| private: |
| struct BitAndName { |
| DWORD bit; |
| StringPiece name; |
| }; |
| |
| // Appends the names of the bits present in |bitfield| to |result| based on |
| // the array of bit-to-name mappings bounded by |bits_begin| and |bits_end|. |
| static void AppendBitsToString(DWORD bitfield, |
| const BitAndName* bits_begin, |
| const BitAndName* bits_end, |
| std::string* result) { |
| while (bits_begin < bits_end) { |
| const BitAndName& bit_name = *bits_begin; |
| if (bitfield & bit_name.bit) { |
| if (!result->empty()) |
| result->append(" | "); |
| bit_name.name.AppendToString(result); |
| bitfield &= ~bit_name.bit; |
| } |
| ++bits_begin; |
| } |
| ASSERT_EQ(bitfield, DWORD{0}); |
| } |
| |
| DWORD access_ = 0; |
| DWORD share_mode_ = 0; |
| std::unique_ptr<::testing::ScopedTrace> scoped_trace_; |
| FilePath temp_file_path_; |
| win::ScopedHandle file_handle_; |
| |
| DISALLOW_COPY_AND_ASSIGN(OpenFileTest); |
| }; |
| |
| // Tests that an opened file can be deleted as expected. |
| TEST_P(OpenFileTest, DeleteFile) { |
| if (CanDeleteFile(access(), share_mode())) { |
| EXPECT_NE(::DeleteFileW(as_wcstr(temp_file_path().value())), 0) |
| << "Last error code: " << ::GetLastError(); |
| } else { |
| EXPECT_EQ(::DeleteFileW(as_wcstr(temp_file_path().value())), 0); |
| } |
| } |
| |
| // Tests that an opened file can be moved as expected. |
| TEST_P(OpenFileTest, MoveFileEx) { |
| const FilePath dest_path = temp_file_path().InsertBeforeExtension(FPL("bla")); |
| if (CanDeleteFile(access(), share_mode())) { |
| EXPECT_NE(::MoveFileExW(as_wcstr(temp_file_path().value()), |
| as_wcstr(dest_path.value()), 0), |
| 0) |
| << "Last error code: " << ::GetLastError(); |
| } else { |
| EXPECT_EQ(::MoveFileExW(as_wcstr(temp_file_path().value()), |
| as_wcstr(dest_path.value()), 0), |
| 0); |
| } |
| } |
| |
| // Tests that an open file cannot be moved after it has been marked for |
| // deletion. |
| TEST_P(OpenFileTest, DeleteThenMove) { |
| // Don't test combinations that cannot be deleted. |
| if (!CanDeleteFile(access(), share_mode())) |
| return; |
| ASSERT_NE(::DeleteFileW(as_wcstr(temp_file_path().value())), 0) |
| << "Last error code: " << ::GetLastError(); |
| const FilePath dest_path = temp_file_path().InsertBeforeExtension(FPL("bla")); |
| // Move fails with ERROR_ACCESS_DENIED (STATUS_DELETE_PENDING under the |
| // covers). |
| EXPECT_EQ(::MoveFileExW(as_wcstr(temp_file_path().value()), |
| as_wcstr(dest_path.value()), 0), |
| 0); |
| } |
| |
| INSTANTIATE_TEST_CASE_P( |
| , |
| OpenFileTest, |
| ::testing::Combine( |
| // Standard access rights except for WRITE_OWNER, which requires admin. |
| ::testing::Combine(::testing::Values(0, SYNCHRONIZE), |
| ::testing::Values(0, WRITE_DAC), |
| ::testing::Values(0, READ_CONTROL), |
| ::testing::Values(0, DELETE)), |
| // Generic file access rights. |
| ::testing::Combine(::testing::Values(0, FILE_GENERIC_READ), |
| ::testing::Values(0, FILE_GENERIC_WRITE), |
| ::testing::Values(0, FILE_GENERIC_EXECUTE)), |
| // File sharing mode. |
| ::testing::Combine(::testing::Values(0, FILE_SHARE_READ), |
| ::testing::Values(0, FILE_SHARE_WRITE), |
| ::testing::Values(0, FILE_SHARE_DELETE)))); |
| |
| } // namespace base |