| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/installer/util/unbuffered_file_writer.h" |
| |
| #include <windows.h> |
| |
| #include <algorithm> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| |
| #include "base/byte_count.h" |
| #include "base/containers/heap_array.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/logging.h" |
| #include "base/test/gmock_expected_support.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace installer { |
| |
| namespace { |
| |
| using base::test::ErrorIs; |
| using base::test::HasValue; |
| using base::test::ValueIs; |
| |
| class UnbufferedFileWriterTest : public testing::Test { |
| protected: |
| void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); } |
| |
| const base::FilePath temp_dir() const { return temp_dir_.GetPath(); } |
| |
| private: |
| base::ScopedTempDir temp_dir_; |
| }; |
| |
| // Tests that a file with no contents works. |
| TEST_F(UnbufferedFileWriterTest, EmptyFile) { |
| base::FilePath path = temp_dir().Append(FILE_PATH_LITERAL("empty")); |
| ASSERT_OK_AND_ASSIGN(UnbufferedFileWriter writer, |
| UnbufferedFileWriter::Create(path)); |
| ASSERT_THAT(writer.Commit(std::nullopt), HasValue()); |
| ASSERT_THAT(base::GetFileSize(path), testing::Optional(0LL)); |
| } |
| |
| // Tests a small file; smaller than a physical sector. |
| TEST_F(UnbufferedFileWriterTest, SmallFile) { |
| base::FilePath path = temp_dir().Append(FILE_PATH_LITERAL("empty")); |
| std::string_view contents = "hi mom"; |
| ASSERT_OK_AND_ASSIGN(UnbufferedFileWriter writer, |
| UnbufferedFileWriter::Create(path)); |
| writer.write_buffer().copy_prefix_from(base::as_byte_span(contents)); |
| writer.Advance(contents.size()); |
| ASSERT_THAT(writer.Commit(std::nullopt), HasValue()); |
| ASSERT_THAT(base::GetFileSize(path), testing::Optional(contents.size())); |
| std::string result; |
| ASSERT_PRED2(base::ReadFileToString, path, &result); |
| ASSERT_EQ(contents, result); |
| } |
| |
| // Tests writing to a file in multiple small chunks. |
| TEST_F(UnbufferedFileWriterTest, CommitChunks) { |
| base::FilePath path = temp_dir().Append(FILE_PATH_LITERAL("chunked")); |
| ASSERT_OK_AND_ASSIGN(UnbufferedFileWriter writer, |
| UnbufferedFileWriter::Create(path)); |
| |
| // The write buffer will be the size of one sector. |
| const size_t sector_size = writer.write_buffer().size(); |
| // Alignment guarantees that this will hold. |
| const size_t number_count = sector_size / sizeof(uint64_t); |
| ASSERT_EQ(number_count * sizeof(uint64_t), sector_size); |
| |
| // Write four sectors' worth of increasing integers. |
| uint64_t index = 0; |
| for (int i = 0; i < 4; ++i) { |
| base::span write_buffer = writer.write_buffer(); |
| ASSERT_EQ(write_buffer.size(), sector_size); |
| // SAFETY: The write buffer is large enough to hold `number_count` numbers. |
| std::ranges::generate( |
| UNSAFE_BUFFERS(base::span( |
| reinterpret_cast<uint64_t*>(write_buffer.data()), number_count)), |
| [&index] { return index++; }); |
| writer.Advance(sector_size); |
| // The write buffer is now full. |
| ASSERT_EQ(writer.write_buffer().size(), 0U); |
| // Send it to the disk. |
| ASSERT_THAT(writer.Checkpoint(), HasValue()); |
| } |
| ASSERT_THAT(writer.Commit(std::nullopt), HasValue()); |
| ASSERT_THAT(base::GetFileSize(path), testing::Optional(4 * sector_size)); |
| std::string result; |
| ASSERT_PRED2(base::ReadFileToString, path, &result); |
| ASSERT_EQ(result.size(), 4 * sector_size); |
| // Confirm that the data is all there, and all in the correct order. |
| index = 0; |
| ASSERT_TRUE(std::ranges::all_of( |
| // SAFETY: The string buffer is large enough to hold the numbers. |
| UNSAFE_BUFFERS(base::span(reinterpret_cast<uint64_t*>( |
| base::as_writable_byte_span(result).data()), |
| result.size() / sizeof(uint64_t)), |
| [&index](auto& slot) { return slot == index++; }))); |
| } |
| |
| // Tests writing a very large file. |
| TEST_F(UnbufferedFileWriterTest, VeryLarge) { |
| static constexpr auto kFileSize = base::GiB(2.333); |
| base::FilePath path = temp_dir().Append(FILE_PATH_LITERAL("very_large")); |
| ASSERT_OK_AND_ASSIGN( |
| UnbufferedFileWriter writer, |
| UnbufferedFileWriter::Create(path, kFileSize.InBytesUnsigned())); |
| writer.Advance(kFileSize.InBytesUnsigned()); |
| ASSERT_THAT(writer.Commit(std::nullopt), HasValue()); |
| ASSERT_THAT(base::GetFileSize(path), testing::Optional(kFileSize.InBytes())); |
| } |
| |
| // Tests that creation fails if the target file already exists. |
| TEST_F(UnbufferedFileWriterTest, FileExists) { |
| base::FilePath path = temp_dir().Append(FILE_PATH_LITERAL("exists")); |
| ASSERT_TRUE(base::WriteFile(path, std::string_view("hi mom"))); |
| base::HistogramTester histogram_tester; |
| ASSERT_THAT(UnbufferedFileWriter::Create(path), ErrorIs(ERROR_FILE_EXISTS)); |
| histogram_tester.ExpectUniqueSample( |
| "Setup.Install.UnbufferedFileWriter.Create.Error", ERROR_FILE_EXISTS, 1); |
| } |
| |
| // Tests that the target file is deleted upon destruction of an instance without |
| // commit. |
| TEST_F(UnbufferedFileWriterTest, DeleteWithoutCommit) { |
| base::FilePath path = temp_dir().Append(FILE_PATH_LITERAL("file")); |
| |
| WIN32_FILE_ATTRIBUTE_DATA data = {}; |
| { |
| ASSERT_OK_AND_ASSIGN(UnbufferedFileWriter writer, |
| UnbufferedFileWriter::Create(path)); |
| // An attempt to check for the file will fail with ACCESS_DENIED because the |
| // file is marked for deletion. |
| ASSERT_FALSE(::GetFileAttributesEx(path.value().c_str(), |
| GetFileExInfoStandard, &data)); |
| ASSERT_EQ(::GetLastError(), static_cast<DWORD>(ERROR_ACCESS_DENIED)); |
| } |
| // Now that the file has been deleted, an attempt to get its attributes will |
| // fail with FILE_NOT_FOUND. |
| ASSERT_FALSE(::GetFileAttributesEx(path.value().c_str(), |
| GetFileExInfoStandard, &data)); |
| ASSERT_EQ(::GetLastError(), static_cast<DWORD>(ERROR_FILE_NOT_FOUND)); |
| } |
| |
| } // namespace |
| |
| } // namespace installer |