// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/base/dragdrop/os_exchange_data.h"

#include <objbase.h>

#include <algorithm>
#include <memory>
#include <optional>
#include <vector>

#include "base/byte_size.h"
#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/sequence_checker.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/run_until.h"
#include "base/test/task_environment.h"
#include "base/threading/thread.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/scoped_hglobal.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/platform_test.h"
#include "ui/base/clipboard/clipboard_format_type.h"
#include "ui/base/clipboard/clipboard_url_info.h"
#include "ui/base/clipboard/file_info.h"
#include "ui/base/dragdrop/os_exchange_data_provider_win.h"
#include "url/gurl.h"

namespace ui {

namespace {

// Creates a dummy `STGMEDIUM` containing 4 bytes of data ("AAAA").
STGMEDIUM CreateHGlobalMedium() {
  STGMEDIUM stgm = {};
  stgm.tymed = TYMED_HGLOBAL;
  stgm.hGlobal = GlobalAlloc(GPTR, 4);
  base::win::ScopedHGlobal<std::array<char, 4>*> lock(stgm.hGlobal);
  *lock.data() = {'A', 'A', 'A', 'A'};
  return stgm;
}

const std::vector<DWORD> kStorageMediaTypesForVirtualFiles = {
    TYMED_ISTORAGE,
    TYMED_ISTREAM,
    TYMED_HGLOBAL,
};

class RefCountMockStream : public IStream {
 public:
  RefCountMockStream() = default;
  ~RefCountMockStream() = default;

  ULONG GetRefCount() const {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    return ref_count_;
  }

  // Overridden from IUnknown:
  IFACEMETHODIMP QueryInterface(REFIID iid, void** object) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (iid == IID_IUnknown || iid == IID_ISequentialStream ||
        iid == IID_IStream) {
      *object = static_cast<IStream*>(this);
      AddRef();
      return S_OK;
    }

    *object = nullptr;
    return E_NOINTERFACE;
  }

  IFACEMETHODIMP_(ULONG) AddRef(void) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    return ++ref_count_;
  }

  IFACEMETHODIMP_(ULONG) Release(void) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    EXPECT_GT(ref_count_, 0u);
    return --ref_count_;
  }
  // Overridden from ISequentialStream:
  MOCK_METHOD3_WITH_CALLTYPE(STDMETHODCALLTYPE,
                             Read,
                             HRESULT(void* pv, ULONG cb, ULONG* pcbRead));

  MOCK_METHOD3_WITH_CALLTYPE(STDMETHODCALLTYPE,
                             Write,
                             HRESULT(void const* pv, ULONG cb, ULONG* pcbW));

  // Overridden from IStream:
  MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE,
                             SetSize,
                             HRESULT(ULARGE_INTEGER));

  MOCK_METHOD4_WITH_CALLTYPE(
      STDMETHODCALLTYPE,
      CopyTo,
      HRESULT(IStream*, ULARGE_INTEGER, ULARGE_INTEGER*, ULARGE_INTEGER*));

  MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, Commit, HRESULT(DWORD));

  MOCK_METHOD0_WITH_CALLTYPE(STDMETHODCALLTYPE, Revert, HRESULT());

  MOCK_METHOD3_WITH_CALLTYPE(STDMETHODCALLTYPE,
                             LockRegion,
                             HRESULT(ULARGE_INTEGER, ULARGE_INTEGER, DWORD));

  MOCK_METHOD3_WITH_CALLTYPE(STDMETHODCALLTYPE,
                             UnlockRegion,
                             HRESULT(ULARGE_INTEGER, ULARGE_INTEGER, DWORD));

  // Clone is not supported; returning E_NOTIMPL forces the production code to
  // use the AddRef() fallback, keeping ref-count assertions meaningful.
  IFACEMETHODIMP Clone(IStream** ppstm) override {
    if (ppstm) {
      *ppstm = nullptr;
    }
    return E_NOTIMPL;
  }

  MOCK_METHOD3_WITH_CALLTYPE(STDMETHODCALLTYPE,
                             Seek,
                             HRESULT(LARGE_INTEGER liDistanceToMove,
                                     DWORD dwOrigin,
                                     ULARGE_INTEGER* lpNewFilePointer));

  MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE,
                             Stat,
                             HRESULT(STATSTG* pStatstg, DWORD grfStatFlag));

 private:
  ULONG ref_count_ = 0u;
  SEQUENCE_CHECKER(sequence_checker_);
};

}  // namespace

class OSExchangeDataWinTest : public ::testing::Test {
 public:
  OSExchangeDataWinTest()
      : task_environment_(base::test::TaskEnvironment::MainThreadType::UI) {}

  void OnGotVirtualFilesAsTempFiles(
      const std::vector<std::pair<base::FilePath, base::FilePath>>&
          filepaths_and_names) {
    on_got_virtual_files_as_temp_files_called_ = true;

    // Clear any previous results and cache a vector of FileInfo objects for
    // verification.
    retrieved_virtual_files_.clear();

    for (const auto& filepath_and_name : filepaths_and_names) {
      retrieved_virtual_files_.push_back(
          FileInfo(filepath_and_name.first, filepath_and_name.second));
    }
  }

 protected:
  class OnGotVirtualFilesAsTempFilesCalledChecker {
   public:
    OnGotVirtualFilesAsTempFilesCalledChecker(OSExchangeDataWinTest* test)
        : test_(test) {
      test_->on_got_virtual_files_as_temp_files_called_ = false;
    }
    ~OnGotVirtualFilesAsTempFilesCalledChecker() {
      EXPECT_TRUE(test_->on_got_virtual_files_as_temp_files_called_);
    }

   private:
    raw_ptr<OSExchangeDataWinTest> test_;
  };

  std::vector<FileInfo> retrieved_virtual_files_;
  base::test::TaskEnvironment task_environment_;
  bool on_got_virtual_files_as_temp_files_called_ = false;
};

// Test getting using the IDataObject COM API
TEST_F(OSExchangeDataWinTest, StringDataAccessViaCOM) {
  OSExchangeData data;
  std::wstring input = L"O hai googlz.";
  data.SetString(base::AsString16(input));
  Microsoft::WRL::ComPtr<IDataObject> com_data(
      OSExchangeDataProviderWin::GetIDataObject(data));

  FORMATETC format_etc =
      { CF_UNICODETEXT, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
  EXPECT_EQ(S_OK, com_data->QueryGetData(&format_etc));

  STGMEDIUM medium;
  EXPECT_EQ(S_OK, com_data->GetData(&format_etc, &medium));
  std::wstring output =
      base::win::ScopedHGlobal<wchar_t*>(medium.hGlobal).data();
  EXPECT_EQ(input, output);
  ReleaseStgMedium(&medium);
}

// Test setting using the IDataObject COM API
TEST_F(OSExchangeDataWinTest, StringDataWritingViaCOM) {
  OSExchangeData data;
  std::wstring input = L"http://www.google.com/";

  Microsoft::WRL::ComPtr<IDataObject> com_data(
      OSExchangeDataProviderWin::GetIDataObject(data));

  // Store data in the object using the COM SetData API.
  CLIPFORMAT cfstr_ineturl = RegisterClipboardFormat(CFSTR_INETURL);
  FORMATETC format_etc =
      { cfstr_ineturl, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
  STGMEDIUM medium;
  medium.tymed = TYMED_HGLOBAL;
  HGLOBAL glob = GlobalAlloc(GPTR, sizeof(wchar_t) * (input.size() + 1));
  base::win::ScopedHGlobal<wchar_t*> global_lock(glob);
  wchar_t* buffer_handle = global_lock.data();
  UNSAFE_TODO(wcscpy_s(buffer_handle, input.size() + 1, input.c_str()));
  medium.hGlobal = glob;
  medium.pUnkForRelease = NULL;
  EXPECT_EQ(S_OK, com_data->SetData(&format_etc, &medium, TRUE));

  // Construct a new object with the old object so that we can use our access
  // APIs.
  OSExchangeData data2(data.provider().Clone());
  EXPECT_TRUE(data2.HasURL(FilenameToURLPolicy::CONVERT_FILENAMES));
  std::vector<ui::ClipboardUrlInfo> url_infos =
      data2.GetURLs(FilenameToURLPolicy::CONVERT_FILENAMES);
  ASSERT_FALSE(url_infos.empty());
  const ui::ClipboardUrlInfo& url_info = url_infos.front();
  GURL reference_url(base::AsStringPiece16(input));
  EXPECT_EQ(reference_url, url_info.url);
}

// Verifies SetData invoked twice with the same data clobbers existing data.
TEST_F(OSExchangeDataWinTest, RemoveData) {
  OSExchangeData data;
  std::wstring input = L"http://www.google.com/";
  std::wstring input2 = L"http://www.google2.com/";

  Microsoft::WRL::ComPtr<IDataObject> com_data(
      OSExchangeDataProviderWin::GetIDataObject(data));

  // Store data in the object using the COM SetData API.
  CLIPFORMAT cfstr_ineturl = RegisterClipboardFormat(CFSTR_INETURL);
  FORMATETC format_etc =
      { cfstr_ineturl, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
  STGMEDIUM medium;
  medium.tymed = TYMED_HGLOBAL;
  {
    HGLOBAL glob = GlobalAlloc(GPTR, sizeof(wchar_t) * (input.size() + 1));
    base::win::ScopedHGlobal<wchar_t*> global_lock(glob);
    wchar_t* buffer_handle = global_lock.data();
    UNSAFE_TODO(wcscpy_s(buffer_handle, input.size() + 1, input.c_str()));
    medium.hGlobal = glob;
    medium.pUnkForRelease = NULL;
    EXPECT_EQ(S_OK, com_data->SetData(&format_etc, &medium, TRUE));
  }
  // This should clobber the existing data.
  {
    HGLOBAL glob = GlobalAlloc(GPTR, sizeof(wchar_t) * (input2.size() + 1));
    base::win::ScopedHGlobal<wchar_t*> global_lock(glob);
    wchar_t* buffer_handle = global_lock.data();
    UNSAFE_TODO(wcscpy_s(buffer_handle, input2.size() + 1, input2.c_str()));
    medium.hGlobal = glob;
    medium.pUnkForRelease = NULL;
    EXPECT_EQ(S_OK, com_data->SetData(&format_etc, &medium, TRUE));
  }
  EXPECT_EQ(1u, static_cast<DataObjectImpl*>(com_data.Get())->size());

  // Construct a new object with the old object so that we can use our access
  // APIs.
  OSExchangeData data2(data.provider().Clone());
  EXPECT_TRUE(data2.HasURL(FilenameToURLPolicy::CONVERT_FILENAMES));
  std::vector<ui::ClipboardUrlInfo> url_infos =
      data2.GetURLs(FilenameToURLPolicy::CONVERT_FILENAMES);
  ASSERT_FALSE(url_infos.empty());
  EXPECT_EQ(GURL(base::AsStringPiece16(input2)), url_infos.front().url);
}

TEST_F(OSExchangeDataWinTest, URLDataAccessViaCOM) {
  OSExchangeData data;
  GURL url("http://www.google.com/");
  data.SetURL(url, std::u16string());
  Microsoft::WRL::ComPtr<IDataObject> com_data(
      OSExchangeDataProviderWin::GetIDataObject(data));

  CLIPFORMAT cfstr_ineturl = RegisterClipboardFormat(CFSTR_INETURL);
  FORMATETC format_etc =
      { cfstr_ineturl, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
  EXPECT_EQ(S_OK, com_data->QueryGetData(&format_etc));

  STGMEDIUM medium;
  EXPECT_EQ(S_OK, com_data->GetData(&format_etc, &medium));
  std::wstring output =
      base::win::ScopedHGlobal<wchar_t*>(medium.hGlobal).data();
  EXPECT_EQ(url.spec(), base::WideToUTF8(output));
  ReleaseStgMedium(&medium);
}

TEST_F(OSExchangeDataWinTest, MultipleFormatsViaCOM) {
  OSExchangeData data;
  std::string url_spec = "http://www.google.com/";
  GURL url(url_spec);
  std::u16string text = u"O hai googlz.";
  data.SetURL(url, u"Google");
  data.SetString(text);

  Microsoft::WRL::ComPtr<IDataObject> com_data(
      OSExchangeDataProviderWin::GetIDataObject(data));

  CLIPFORMAT cfstr_ineturl = RegisterClipboardFormat(CFSTR_INETURL);
  FORMATETC url_format_etc =
      { cfstr_ineturl, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
  EXPECT_EQ(S_OK, com_data->QueryGetData(&url_format_etc));
  FORMATETC text_format_etc =
      { CF_UNICODETEXT, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
  EXPECT_EQ(S_OK, com_data->QueryGetData(&text_format_etc));

  STGMEDIUM medium;
  EXPECT_EQ(S_OK, com_data->GetData(&url_format_etc, &medium));
  std::wstring output_url =
      base::win::ScopedHGlobal<wchar_t*>(medium.hGlobal).data();
  EXPECT_EQ(url.spec(), base::WideToUTF8(output_url));
  ReleaseStgMedium(&medium);

  // The text is supposed to be the raw text of the URL, _NOT_ the value of
  // |text|! This is because the URL is added first and thus takes precedence!
  EXPECT_EQ(S_OK, com_data->GetData(&text_format_etc, &medium));
  std::wstring output_text =
      base::win::ScopedHGlobal<wchar_t*>(medium.hGlobal).data();
  EXPECT_EQ(url_spec, base::WideToUTF8(output_text));
  ReleaseStgMedium(&medium);
}

TEST_F(OSExchangeDataWinTest, EnumerationViaCOM) {
  OSExchangeData data;
  data.SetURL(GURL("http://www.google.com/"), std::u16string());
  data.SetString(u"O hai googlz.");

  CLIPFORMAT cfstr_file_group_descriptor =
      RegisterClipboardFormat(CFSTR_FILEDESCRIPTOR);
  CLIPFORMAT text_x_moz_url = RegisterClipboardFormat(L"text/x-moz-url");

  Microsoft::WRL::ComPtr<IDataObject> com_data(
      OSExchangeDataProviderWin::GetIDataObject(data));
  Microsoft::WRL::ComPtr<IEnumFORMATETC> enumerator;
  EXPECT_EQ(S_OK, com_data.Get()->EnumFormatEtc(DATADIR_GET, &enumerator));

  // Test that we can get one item.
  {
    // Explictly don't reset the first time, to verify the creation state is
    // OK.
    ULONG retrieved = 0;
    FORMATETC elements_array[1];
    EXPECT_EQ(S_OK, enumerator->Next(1,
        reinterpret_cast<FORMATETC*>(&elements_array), &retrieved));
    EXPECT_EQ(1u, retrieved);
    EXPECT_EQ(text_x_moz_url, elements_array[0].cfFormat);
  }

  // Test that we can get one item with a NULL retrieved value.
  {
    EXPECT_EQ(S_OK, enumerator->Reset());
    FORMATETC elements_array[1];
    EXPECT_EQ(S_OK, enumerator->Next(1,
        reinterpret_cast<FORMATETC*>(&elements_array), NULL));
    EXPECT_EQ(text_x_moz_url, elements_array[0].cfFormat);
  }

  // Test that we can get two items.
  {
    EXPECT_EQ(S_OK, enumerator->Reset());
    ULONG retrieved = 0;
    FORMATETC elements_array[2];
    EXPECT_EQ(S_OK, enumerator->Next(2,
        reinterpret_cast<FORMATETC*>(&elements_array), &retrieved));
    EXPECT_EQ(2u, retrieved);
    EXPECT_EQ(text_x_moz_url, elements_array[0].cfFormat);
    EXPECT_EQ(cfstr_file_group_descriptor, elements_array[1].cfFormat);
  }

  // Test that we can skip the first item.
  {
    EXPECT_EQ(S_OK, enumerator->Reset());
    EXPECT_EQ(S_OK, enumerator->Skip(1));
    ULONG retrieved = 0;
    FORMATETC elements_array[1];
    EXPECT_EQ(S_OK, enumerator->Next(1,
        reinterpret_cast<FORMATETC*>(&elements_array), &retrieved));
    EXPECT_EQ(1u, retrieved);
    EXPECT_EQ(cfstr_file_group_descriptor, elements_array[0].cfFormat);
  }

  // Test that we can skip the first item, and create a clone that matches in
  // this state, and modify the original without affecting the clone.
  {
    EXPECT_EQ(S_OK, enumerator->Reset());
    EXPECT_EQ(S_OK, enumerator->Skip(1));
    Microsoft::WRL::ComPtr<IEnumFORMATETC> cloned_enumerator;
    EXPECT_EQ(S_OK, enumerator.Get()->Clone(&cloned_enumerator));
    EXPECT_EQ(S_OK, enumerator.Get()->Reset());

    {
      ULONG retrieved = 0;
      FORMATETC elements_array[1];
      EXPECT_EQ(S_OK, cloned_enumerator->Next(1,
          reinterpret_cast<FORMATETC*>(&elements_array), &retrieved));
      EXPECT_EQ(1u, retrieved);
      EXPECT_EQ(cfstr_file_group_descriptor, elements_array[0].cfFormat);
    }

    {
      ULONG retrieved = 0;
      FORMATETC elements_array[1];
      EXPECT_EQ(S_OK, enumerator->Next(1,
          reinterpret_cast<FORMATETC*>(&elements_array), &retrieved));
      EXPECT_EQ(1u, retrieved);
      EXPECT_EQ(text_x_moz_url, elements_array[0].cfFormat);
    }
  }
}

TEST_F(OSExchangeDataWinTest, TestURLExchangeFormatsViaCOM) {
  OSExchangeData data;
  std::string url_spec = "http://www.google.com/";
  GURL url(url_spec);
  std::u16string url_title = u"www.google.com";
  data.SetURL(url, url_title);

  // File contents access via COM. SetFileContents stores data as TYMED_ISTREAM
  // (backed by SHCreateMemStream), so request TYMED_ISTREAM accordingly.
  Microsoft::WRL::ComPtr<IDataObject> com_data(
      OSExchangeDataProviderWin::GetIDataObject(data));
  {
    CLIPFORMAT cfstr_file_contents =
        RegisterClipboardFormat(CFSTR_FILECONTENTS);
    // format_etc.lindex value 0 used for file drop.
    FORMATETC format_etc = {cfstr_file_contents, nullptr, DVASPECT_CONTENT, 0,
                            TYMED_ISTREAM};
    EXPECT_EQ(S_OK, com_data->QueryGetData(&format_etc));

    STGMEDIUM medium = {};
    ASSERT_EQ(S_OK, com_data->GetData(&format_etc, &medium));
    ASSERT_EQ(TYMED_ISTREAM, medium.tymed);

    std::string output;
    char buf[4096];
    ULONG nread = 0;
    while (medium.pstm->Read(buf, sizeof(buf), &nread) == S_OK && nread > 0) {
      output.append(buf, nread);
    }

    std::string file_contents = "[InternetShortcut]\r\nURL=";
    file_contents += url_spec;
    file_contents += "\r\n";
    EXPECT_EQ(file_contents, output);
    ReleaseStgMedium(&medium);
  }
}

TEST_F(OSExchangeDataWinTest, FileContents) {
  OSExchangeData data;
  base::span<const uint8_t> file_contents =
      base::byte_span_from_cstring("data\0with\0nulls");
  data.SetFileContents(base::FilePath(L"filename.txt"), file_contents);

  OSExchangeData copy(data.provider().Clone());
  std::optional<OSExchangeData::FileContentsInfo> file_contents_info =
      copy.GetFileContents();
  EXPECT_TRUE(file_contents_info.has_value());
  EXPECT_EQ(L"filename.txt", file_contents_info->filename.value());
  EXPECT_EQ(file_contents, file_contents_info->file_contents);
}

TEST_F(OSExchangeDataWinTest, VirtualFiles) {
  const base::FilePath kPathPlaceholder(kVirtualFileTempPlaceholderPath);

  const std::vector<std::pair<base::FilePath, base::span<const uint8_t>>>
      kTestFilenamesAndContents = {
          {base::FilePath(FILE_PATH_LITERAL("filename.txt")),
           base::byte_span_from_cstring("just some data")},
          {base::FilePath(FILE_PATH_LITERAL("another filename.txt")),
           base::byte_span_from_cstring("just some data\0with\0nulls")},
          {base::FilePath(FILE_PATH_LITERAL("and another filename.txt")),
           base::byte_span_from_cstring("just some more data")},
      };

  for (const auto& tymed : kStorageMediaTypesForVirtualFiles) {
    OSExchangeData data;
    data.provider().SetVirtualFileContentsForTesting(kTestFilenamesAndContents,
                                                     tymed);

    OSExchangeData copy(data.provider().Clone());
    std::optional<std::vector<FileInfo>> file_infos =
        copy.GetVirtualFilenames();
    ASSERT_TRUE(file_infos.has_value());
    EXPECT_EQ(kTestFilenamesAndContents.size(), file_infos.value().size());
    for (size_t i = 0; i < file_infos.value().size(); i++) {
      EXPECT_EQ(kTestFilenamesAndContents[i].first,
                file_infos.value()[i].display_name);
      EXPECT_EQ(kPathPlaceholder, file_infos.value()[i].path);
    }

    base::FilePath temp_dir;
    EXPECT_TRUE(base::GetTempDir(&temp_dir));

    // Callback for GetVirtualFilesAsTempFiles is executed when all virtual
    // files are backed by temp files.
    auto callback =
        base::BindOnce(&OSExchangeDataWinTest::OnGotVirtualFilesAsTempFiles,
                       base::Unretained(this));

    OnGotVirtualFilesAsTempFilesCalledChecker checker(this);
    copy.GetVirtualFilesAsTempFiles(std::move(callback));

    // RunUntilIdle assures all async tasks are run.
    task_environment_.RunUntilIdle();

    EXPECT_EQ(kTestFilenamesAndContents.size(),
              retrieved_virtual_files_.size());
    for (size_t i = 0; i < retrieved_virtual_files_.size(); i++) {
      EXPECT_EQ(kTestFilenamesAndContents[i].first,
                retrieved_virtual_files_[i].display_name);
      // Check if the temp files that back the virtual files are actually
      // created in the temp directory. Need to compare long file paths here
      // because GetTempDir can return a short ("8.3") path if the test is run
      // under a username that is too long.
      EXPECT_EQ(
          base::MakeLongFilePath(temp_dir),
          base::MakeLongFilePath(retrieved_virtual_files_[i].path.DirName()));
      EXPECT_EQ(kTestFilenamesAndContents[i].first.Extension(),
                retrieved_virtual_files_[i].path.Extension());
      std::optional<std::vector<uint8_t>> read_contents =
          base::ReadFileToBytes(retrieved_virtual_files_[i].path);
      ASSERT_TRUE(read_contents.has_value());
      if (tymed != TYMED_ISTORAGE) {
        EXPECT_EQ(kTestFilenamesAndContents[i].second, read_contents.value());
      } else {
        // IStorage uses compound files, so temp files won't be flat text files.
        // Just make sure the original contents appears in the compound files.
        EXPECT_FALSE(std::ranges::search(read_contents.value(),
                                         kTestFilenamesAndContents[i].second)
                         .empty());
      }
    }
  }
}

TEST_F(OSExchangeDataWinTest, VirtualFilesWithLeadingDirectoryEntry) {
  // A descriptor whose first entry is a directory (as produced by dropping a
  // folder out of a Windows ZIP "compressed folder"). The directory has no
  // content stream and must be skipped without shifting the file entries that
  // follow.
  const std::vector<std::pair<base::FilePath, base::span<const uint8_t>>>
      kTestFilenamesAndContents = {
          {base::FilePath(FILE_PATH_LITERAL("folder")),
           base::span<const uint8_t>()},
          {base::FilePath(FILE_PATH_LITERAL("first.txt")),
           base::byte_span_from_cstring("contents of first")},
          {base::FilePath(FILE_PATH_LITERAL("second.txt")),
           base::byte_span_from_cstring("contents of second")},
      };
  const std::vector<size_t> kDirectoryIndices = {0};

  // Only the non-directory entries should be surfaced as files.
  const std::vector<std::pair<base::FilePath, base::span<const uint8_t>>>
      kExpectedFiles = {kTestFilenamesAndContents[1],
                        kTestFilenamesAndContents[2]};

  for (const auto& tymed : kStorageMediaTypesForVirtualFiles) {
    OSExchangeData data;
    static_cast<OSExchangeDataProviderWin*>(&data.provider())
        ->SetVirtualFileContentsWithDirectoriesForTesting(
            kTestFilenamesAndContents, kDirectoryIndices, tymed);

    OSExchangeData copy(data.provider().Clone());
    std::optional<std::vector<FileInfo>> file_infos =
        copy.GetVirtualFilenames();
    ASSERT_TRUE(file_infos.has_value());
    EXPECT_EQ(kExpectedFiles.size(), file_infos.value().size());

    auto callback =
        base::BindOnce(&OSExchangeDataWinTest::OnGotVirtualFilesAsTempFiles,
                       base::Unretained(this));
    OnGotVirtualFilesAsTempFilesCalledChecker checker(this);
    copy.GetVirtualFilesAsTempFiles(std::move(callback));
    task_environment_.RunUntilIdle();

    ASSERT_EQ(kExpectedFiles.size(), retrieved_virtual_files_.size());
    for (size_t i = 0; i < retrieved_virtual_files_.size(); i++) {
      EXPECT_EQ(kExpectedFiles[i].first,
                retrieved_virtual_files_[i].display_name);
      std::optional<std::vector<uint8_t>> read_contents =
          base::ReadFileToBytes(retrieved_virtual_files_[i].path);
      ASSERT_TRUE(read_contents.has_value());
      if (tymed != TYMED_ISTORAGE) {
        EXPECT_EQ(kExpectedFiles[i].second, read_contents.value());
      } else {
        EXPECT_FALSE(
            std::ranges::search(read_contents.value(), kExpectedFiles[i].second)
                .empty());
      }
    }
  }
}

TEST_F(OSExchangeDataWinTest, VirtualFilesAsyncChunkedCopy) {
  const base::FilePath kPathPlaceholder(kVirtualFileTempPlaceholderPath);

  // Create a large file (50MB) to exercise the chunked copy code path.
  // The chunk size is 16MB, so this will require multiple iterations.
  constexpr base::ByteSize kLargeFileSizeBytes = base::MiB(50);
  const std::vector<uint8_t> large_content(kLargeFileSizeBytes.InBytes(), 'X');

  const std::vector<std::pair<base::FilePath, base::span<const uint8_t>>>
      kTestFilenamesAndContents = {
          {base::FilePath(FILE_PATH_LITERAL("large_file.bin")), large_content},
      };

  OSExchangeData data;
  data.provider().SetVirtualFileContentsForTesting(kTestFilenamesAndContents,
                                                   TYMED_ISTREAM);

  auto* provider = static_cast<OSExchangeDataProviderWin*>(&data.provider());
  provider->async_operation()->SetAsyncMode(TRUE);

  BOOL is_async = FALSE;
  EXPECT_EQ(S_OK, provider->async_operation()->GetAsyncMode(&is_async));
  EXPECT_TRUE(is_async);
  // Track whether we observe InOperation() returning TRUE at any point,
  // which verifies StartOperation() was called.
  bool saw_in_operation_true = false;
  BOOL in_operation = FALSE;

  std::optional<std::vector<FileInfo>> file_infos = data.GetVirtualFilenames();
  ASSERT_TRUE(file_infos.has_value());
  EXPECT_EQ(1u, file_infos.value().size());
  EXPECT_EQ(kTestFilenamesAndContents[0].first,
            file_infos.value()[0].display_name);
  EXPECT_EQ(kPathPlaceholder, file_infos.value()[0].path);

  base::FilePath temp_dir;
  EXPECT_TRUE(base::GetTempDir(&temp_dir));

  auto callback =
      base::BindOnce(&OSExchangeDataWinTest::OnGotVirtualFilesAsTempFiles,
                     base::Unretained(this));
  OnGotVirtualFilesAsTempFilesCalledChecker checker(this);
  data.GetVirtualFilesAsTempFiles(std::move(callback));

  // Wait for the async chunked copy to complete, checking InOperation on each
  // iteration.
  ASSERT_TRUE(base::test::RunUntil([&]() {
    provider->async_operation()->InOperation(&in_operation);
    if (in_operation) {
      saw_in_operation_true = true;
    }
    return on_got_virtual_files_as_temp_files_called_;
  }));
  EXPECT_TRUE(saw_in_operation_true);

  // After async extraction completes, assert EndOperation was called.
  EXPECT_EQ(S_OK, provider->async_operation()->InOperation(&in_operation));
  EXPECT_FALSE(in_operation);

  ASSERT_EQ(1u, retrieved_virtual_files_.size());
  EXPECT_EQ(kTestFilenamesAndContents[0].first,
            retrieved_virtual_files_[0].display_name);
  EXPECT_EQ(base::MakeLongFilePath(temp_dir),
            base::MakeLongFilePath(retrieved_virtual_files_[0].path.DirName()));
  EXPECT_EQ(kTestFilenamesAndContents[0].first.Extension(),
            retrieved_virtual_files_[0].path.Extension());

  // Verify the full content was copied correctly despite chunking.
  std::optional<std::vector<uint8_t>> read_contents =
      base::ReadFileToBytes(retrieved_virtual_files_[0].path);
  ASSERT_TRUE(read_contents.has_value());
  EXPECT_EQ(large_content.size(), read_contents->size());
  EXPECT_EQ(large_content, read_contents.value());
}

TEST_F(OSExchangeDataWinTest, VirtualFilesRealFilesPreferred) {
  // Verify that no virtual files retrieved if there is real file data.
  const std::vector<FileInfo> kTestFilenames = {
      {base::FilePath(FILE_PATH_LITERAL("C:\\tmp\\test_file1")),
       base::FilePath()},
      {base::FilePath(FILE_PATH_LITERAL("C:\\tmp\\test_file2")),
       base::FilePath()},
  };

  const std::vector<std::pair<base::FilePath, base::span<const uint8_t>>>
      kTestFilenamesAndContents = {
          {base::FilePath(FILE_PATH_LITERAL("filename.txt")),
           base::byte_span_from_cstring("just some data")},
          {base::FilePath(FILE_PATH_LITERAL("another filename.txt")),
           base::byte_span_from_cstring("just some data\0with\0nulls")},
          {base::FilePath(FILE_PATH_LITERAL("and another filename.txt")),
           base::byte_span_from_cstring("just some more data")},
      };

  for (const auto& tymed : kStorageMediaTypesForVirtualFiles) {
    OSExchangeData data;
    data.SetFilenames(kTestFilenames);
    data.provider().SetVirtualFileContentsForTesting(kTestFilenamesAndContents,
                                                     tymed);

    OSExchangeData copy(data.provider().Clone());

    std::optional<std::vector<FileInfo>> real_filenames = copy.GetFilenames();
    ASSERT_TRUE(real_filenames.has_value());
    EXPECT_EQ(kTestFilenames.size(), real_filenames.value().size());
    EXPECT_EQ(kTestFilenames, real_filenames.value());

    std::optional<std::vector<FileInfo>> file_infos =
        copy.GetVirtualFilenames();
    EXPECT_FALSE(file_infos.has_value());

    // Callback for GetVirtualFilesAsTempFiles is executed when all virtual
    // files are backed by temp files.
    auto callback =
        base::BindOnce(&OSExchangeDataWinTest::OnGotVirtualFilesAsTempFiles,
                       base::Unretained(this));

    OnGotVirtualFilesAsTempFilesCalledChecker checker(this);
    copy.GetVirtualFilesAsTempFiles(std::move(callback));

    // RunUntilIdle assures all async tasks are run.
    task_environment_.RunUntilIdle();

    EXPECT_EQ(static_cast<size_t>(0), retrieved_virtual_files_.size());
  }
}

TEST_F(OSExchangeDataWinTest, VirtualFilesDuplicateNames) {
  const std::vector<std::pair<base::FilePath, base::span<const uint8_t>>>
      kTestFilenamesAndContents = {
          {base::FilePath(FILE_PATH_LITERAL("A (1) (2).txt")),
           base::byte_span_from_cstring("just some data")},
          {base::FilePath(FILE_PATH_LITERAL("A.txt")),
           base::byte_span_from_cstring("just some more data")},
          {base::FilePath(FILE_PATH_LITERAL("A (1).txt")),
           base::byte_span_from_cstring("just some more more data")},
          {base::FilePath(FILE_PATH_LITERAL("A.txt")),
           base::byte_span_from_cstring("just some more more more data")},
      };

  for (const auto& tymed : kStorageMediaTypesForVirtualFiles) {
    OSExchangeData data;
    data.provider().SetVirtualFileContentsForTesting(kTestFilenamesAndContents,
                                                     tymed);

    OSExchangeData copy(data.provider().Clone());
    std::optional<std::vector<FileInfo>> file_infos =
        copy.GetVirtualFilenames();
    ASSERT_TRUE(file_infos.has_value());
    EXPECT_EQ(kTestFilenamesAndContents.size(), file_infos.value().size());
    for (size_t i = 0; i < file_infos.value().size(); i++) {
      // Check that display name is unique.
      for (size_t j = 0; j < i; j++) {
        EXPECT_FALSE(base::FilePath::CompareEqualIgnoreCase(
            file_infos.value()[j].display_name.value(),
            file_infos.value()[i].display_name.value()));
      }
    }

    base::FilePath temp_dir;
    EXPECT_TRUE(base::GetTempDir(&temp_dir));

    // Callback for GetVirtualFilesAsTempFiles is executed when all virtual
    // files are backed by temp files.
    auto callback =
        base::BindOnce(&OSExchangeDataWinTest::OnGotVirtualFilesAsTempFiles,
                       base::Unretained(this));

    OnGotVirtualFilesAsTempFilesCalledChecker checker(this);
    copy.GetVirtualFilesAsTempFiles(std::move(callback));

    // RunUntilIdle assures all async tasks are run.
    task_environment_.RunUntilIdle();

    EXPECT_EQ(kTestFilenamesAndContents.size(), file_infos.value().size());
    for (size_t i = 0; i < retrieved_virtual_files_.size(); i++) {
      // Check that display name is unique.
      for (size_t j = 0; j < i; j++) {
        EXPECT_FALSE(base::FilePath::CompareEqualIgnoreCase(
            retrieved_virtual_files_[j].display_name.value(),
            retrieved_virtual_files_[i].display_name.value()));
      }

      // Check that temp file path is unique.
      for (size_t j = 0; j < i; j++) {
        EXPECT_FALSE(base::FilePath::CompareEqualIgnoreCase(
            retrieved_virtual_files_[j].path.value(),
            retrieved_virtual_files_[i].path.value()));
      }

      // Check if the temp files that back the virtual files are actually
      // created in the temp directory. Need to compare long file paths here
      // because GetTempDir can return a short ("8.3") path if the test is run
      // under a username that is too long.
      EXPECT_EQ(
          base::MakeLongFilePath(temp_dir),
          base::MakeLongFilePath(retrieved_virtual_files_[i].path.DirName()));
      EXPECT_EQ(kTestFilenamesAndContents[i].first.Extension(),
                retrieved_virtual_files_[i].path.Extension());
      std::optional<std::vector<uint8_t>> read_contents =
          base::ReadFileToBytes(retrieved_virtual_files_[i].path);
      ASSERT_TRUE(read_contents.has_value());
      if (tymed != TYMED_ISTORAGE) {
        EXPECT_EQ(kTestFilenamesAndContents[i].second, read_contents.value());
      } else {
        // IStorage uses compound files, so temp files won't be flat text files.
        // Just make sure the original contents appears in the compound files.
        EXPECT_FALSE(std::ranges::search(read_contents.value(),
                                         kTestFilenamesAndContents[i].second)
                         .empty());
      }
    }
  }
}  // namespace ui

TEST_F(OSExchangeDataWinTest, VirtualFilesDuplicateNamesCaseInsensitivity) {
  const std::vector<std::pair<base::FilePath, base::span<const uint8_t>>>
      kTestFilenamesAndContents = {
          {base::FilePath(FILE_PATH_LITERAL("a.txt")),
           base::byte_span_from_cstring("just some data")},
          {base::FilePath(FILE_PATH_LITERAL("B.txt")),
           base::byte_span_from_cstring("just some more data")},
          {base::FilePath(FILE_PATH_LITERAL("A.txt")),
           base::byte_span_from_cstring("just some more more data")},
      };

  for (const auto& tymed : kStorageMediaTypesForVirtualFiles) {
    OSExchangeData data;
    data.provider().SetVirtualFileContentsForTesting(kTestFilenamesAndContents,
                                                     tymed);

    OSExchangeData copy(data.provider().Clone());
    std::optional<std::vector<FileInfo>> file_infos =
        copy.GetVirtualFilenames();
    ASSERT_TRUE(file_infos.has_value());
    EXPECT_EQ(kTestFilenamesAndContents.size(), file_infos.value().size());
    for (size_t i = 0; i < file_infos.value().size(); i++) {
      // Check that display name is unique.
      for (size_t j = 0; j < i; j++) {
        EXPECT_FALSE(base::FilePath::CompareEqualIgnoreCase(
            file_infos.value()[j].display_name.value(),
            file_infos.value()[i].display_name.value()));
      }
    }

    base::FilePath temp_dir;
    EXPECT_TRUE(base::GetTempDir(&temp_dir));

    // Callback for GetVirtualFilesAsTempFiles is executed when all virtual
    // files are backed by temp files.
    auto callback =
        base::BindOnce(&OSExchangeDataWinTest::OnGotVirtualFilesAsTempFiles,
                       base::Unretained(this));

    OnGotVirtualFilesAsTempFilesCalledChecker checker(this);
    copy.GetVirtualFilesAsTempFiles(std::move(callback));

    // RunUntilIdle assures all async tasks are run.
    task_environment_.RunUntilIdle();

    EXPECT_EQ(kTestFilenamesAndContents.size(), file_infos.value().size());
    for (size_t i = 0; i < retrieved_virtual_files_.size(); i++) {
      // Check that display name is unique.
      for (size_t j = 0; j < i; j++) {
        EXPECT_FALSE(base::FilePath::CompareEqualIgnoreCase(
            retrieved_virtual_files_[j].display_name.value(),
            retrieved_virtual_files_[i].display_name.value()));
      }

      // Check that temp file path is unique.
      for (size_t j = 0; j < i; j++) {
        EXPECT_FALSE(base::FilePath::CompareEqualIgnoreCase(
            retrieved_virtual_files_[j].path.value(),
            retrieved_virtual_files_[i].path.value()));
      }

      // Check if the temp files that back the virtual files are actually
      // created in the temp directory. Need to compare long file paths here
      // because GetTempDir can return a short ("8.3") path if the test is run
      // under a username that is too long.
      EXPECT_EQ(
          base::MakeLongFilePath(temp_dir),
          base::MakeLongFilePath(retrieved_virtual_files_[i].path.DirName()));
      EXPECT_EQ(kTestFilenamesAndContents[i].first.Extension(),
                retrieved_virtual_files_[i].path.Extension());
      std::optional<std::vector<uint8_t>> read_contents =
          base::ReadFileToBytes(retrieved_virtual_files_[i].path);
      ASSERT_TRUE(read_contents.has_value());
      if (tymed != TYMED_ISTORAGE) {
        EXPECT_EQ(kTestFilenamesAndContents[i].second, read_contents.value());
      } else {
        // IStorage uses compound files, so temp files won't be flat text files.
        // Just make sure the original contents appears in the compound files.
        EXPECT_FALSE(std::ranges::search(read_contents.value(),
                                         kTestFilenamesAndContents[i].second)
                         .empty());
      }
    }
  }
}

TEST_F(OSExchangeDataWinTest, VirtualFilesInvalidAndDuplicateNames) {
  const std::wstring kInvalidFileNameCharacters(
      FILE_PATH_LITERAL("\\/:*?\"<>|"));
  const std::wstring kInvalidFilePathCharacters(FILE_PATH_LITERAL("/*?\"<>|"));
  const base::FilePath kPathWithInvalidFileNameCharacters =
      base::FilePath(kInvalidFileNameCharacters)
          .AddExtension(FILE_PATH_LITERAL("txt"));
  const base::FilePath kEmptyDisplayName(FILE_PATH_LITERAL(""));
  const base::FilePath kMaxPathDisplayName =
      base::FilePath(std::wstring(MAX_PATH - 5, L'a'))
          .AddExtension(FILE_PATH_LITERAL("txt"));

  const std::vector<std::pair<base::FilePath, base::span<const uint8_t>>>
      kTestFilenamesAndContents = {
          {kPathWithInvalidFileNameCharacters,
           base::byte_span_from_cstring("just some data")},
          {kPathWithInvalidFileNameCharacters,
           base::byte_span_from_cstring("just some data\0with\0nulls")},
          {// Test that still get a unique name if a previous uniquified
           // name is a duplicate of this one.
           kPathWithInvalidFileNameCharacters.InsertBeforeExtension(
               FILE_PATH_LITERAL(" (1)")),
           base::byte_span_from_cstring("just some more data")},
          // Expect a default display name to be generated ("download" if it
          // matters).
          {kEmptyDisplayName,
           base::byte_span_from_cstring("data for an empty display name")},
          {kEmptyDisplayName,
           base::byte_span_from_cstring("data for another empty display name")},
          // Expect good behavior if the display name length exceeds MAX_PATH.
          {kMaxPathDisplayName,
           base::byte_span_from_cstring("data for a >MAX_PATH display name")},
          {kMaxPathDisplayName, base::byte_span_from_cstring(
                                    "data for another >MAX_PATH display name")},
      };

  for (const auto& tymed : kStorageMediaTypesForVirtualFiles) {
    OSExchangeData data;
    data.provider().SetVirtualFileContentsForTesting(kTestFilenamesAndContents,
                                                     tymed);

    OSExchangeData copy(data.provider().Clone());
    std::optional<std::vector<FileInfo>> file_infos =
        copy.GetVirtualFilenames();
    ASSERT_TRUE(file_infos.has_value());
    EXPECT_EQ(kTestFilenamesAndContents.size(), file_infos.value().size());
    for (size_t i = 0; i < file_infos.value().size(); i++) {
      // Check that display name does not contain invalid characters.
      EXPECT_EQ(std::wstring::npos,
                file_infos.value()[i].display_name.value().find_first_of(
                    kInvalidFileNameCharacters));
      // Check that display name is unique.
      for (size_t j = 0; j < i; j++) {
        EXPECT_FALSE(base::FilePath::CompareEqualIgnoreCase(
            file_infos.value()[j].display_name.value(),
            file_infos.value()[i].display_name.value()));
      }
    }

    base::FilePath temp_dir;
    EXPECT_TRUE(base::GetTempDir(&temp_dir));

    // Callback for GetVirtualFilesAsTempFiles is executed when all virtual
    // files are backed by temp files.
    auto callback =
        base::BindOnce(&OSExchangeDataWinTest::OnGotVirtualFilesAsTempFiles,
                       base::Unretained(this));

    OnGotVirtualFilesAsTempFilesCalledChecker checker(this);
    copy.GetVirtualFilesAsTempFiles(std::move(callback));

    // RunUntilIdle assures all async tasks are run.
    task_environment_.RunUntilIdle();

    EXPECT_EQ(kTestFilenamesAndContents.size(), file_infos.value().size());
    for (size_t i = 0; i < retrieved_virtual_files_.size(); i++) {
      // Check that display name does not contain invalid characters.
      EXPECT_EQ(std::wstring::npos,
                retrieved_virtual_files_[i].display_name.value().find_first_of(
                    kInvalidFileNameCharacters));
      // Check that display name is unique.
      for (size_t j = 0; j < i; j++) {
        EXPECT_FALSE(base::FilePath::CompareEqualIgnoreCase(
            retrieved_virtual_files_[j].display_name.value(),
            retrieved_virtual_files_[i].display_name.value()));
      }
      // Check that temp file path does not contain invalid characters (except
      // for separator).
      EXPECT_EQ(std::wstring::npos,
                retrieved_virtual_files_[i].path.value().find_first_of(
                    kInvalidFilePathCharacters));
      // Check that temp file path is unique.
      for (size_t j = 0; j < i; j++) {
        EXPECT_FALSE(base::FilePath::CompareEqualIgnoreCase(
            retrieved_virtual_files_[j].path.value(),
            retrieved_virtual_files_[i].path.value()));
      }

      // Check if the temp files that back the virtual files are actually
      // created in the temp directory. Need to compare long file paths here
      // because GetTempDir can return a short ("8.3") path if the test is run
      // under a username that is too long.
      EXPECT_EQ(
          base::MakeLongFilePath(temp_dir),
          base::MakeLongFilePath(retrieved_virtual_files_[i].path.DirName()));
      EXPECT_EQ(kTestFilenamesAndContents[i].first.Extension(),
                retrieved_virtual_files_[i].path.Extension());
      // Ability to read the contents implies a temp file was successfully
      // created on the file system even though the original suggested display
      // name had invalid filename characters.
      std::optional<std::vector<uint8_t>> read_contents =
          base::ReadFileToBytes(retrieved_virtual_files_[i].path);
      ASSERT_TRUE(read_contents.has_value());
      if (tymed != TYMED_ISTORAGE) {
        EXPECT_EQ(kTestFilenamesAndContents[i].second, read_contents.value());
      } else {
        // IStorage uses compound files, so temp files won't be flat text files.
        // Just make sure the original contents appears in the compound files.
        EXPECT_FALSE(std::ranges::search(read_contents.value(),
                                         kTestFilenamesAndContents[i].second)
                         .empty());
      }
    }
  }
}

TEST_F(OSExchangeDataWinTest, VirtualFilesEmptyContents) {
  const std::vector<std::pair<base::FilePath, base::span<const uint8_t>>>
      kTestFilenamesAndContents = {
          {base::FilePath(FILE_PATH_LITERAL("file_with_no_contents.txt")),
           std::vector<uint8_t>()},
      };

  for (const auto& tymed : kStorageMediaTypesForVirtualFiles) {
    OSExchangeData data;
    data.provider().SetVirtualFileContentsForTesting(kTestFilenamesAndContents,
                                                     tymed);

    OSExchangeData copy(data.provider().Clone());
    std::optional<std::vector<FileInfo>> file_infos =
        copy.GetVirtualFilenames();
    ASSERT_TRUE(file_infos.has_value());
    EXPECT_EQ(kTestFilenamesAndContents.size(), file_infos.value().size());
    for (size_t i = 0; i < file_infos.value().size(); i++) {
      EXPECT_EQ(kTestFilenamesAndContents[i].first,
                file_infos.value()[i].display_name);
    }

    base::FilePath temp_dir;
    EXPECT_TRUE(base::GetTempDir(&temp_dir));

    // Callback for GetVirtualFilesAsTempFiles is executed when all virtual
    // files are backed by temp files.
    auto callback =
        base::BindOnce(&OSExchangeDataWinTest::OnGotVirtualFilesAsTempFiles,
                       base::Unretained(this));

    OnGotVirtualFilesAsTempFilesCalledChecker checker(this);
    copy.GetVirtualFilesAsTempFiles(std::move(callback));

    // RunUntilIdle assures all async tasks are run.
    task_environment_.RunUntilIdle();

    EXPECT_EQ(kTestFilenamesAndContents.size(),
              retrieved_virtual_files_.size());
    for (size_t i = 0; i < retrieved_virtual_files_.size(); i++) {
      EXPECT_EQ(kTestFilenamesAndContents[i].first,
                retrieved_virtual_files_[i].display_name);

      // Check if the temp files that back the virtual files are actually
      // created in the temp directory. Need to compare long file paths here
      // because GetTempDir can return a short ("8.3") path if the test is run
      // under a username that is too long.
      EXPECT_EQ(
          base::MakeLongFilePath(temp_dir),
          base::MakeLongFilePath(retrieved_virtual_files_[i].path.DirName()));
      EXPECT_EQ(kTestFilenamesAndContents[i].first.Extension(),
                retrieved_virtual_files_[i].path.Extension());
      std::optional<std::vector<uint8_t>> read_contents =
          base::ReadFileToBytes(retrieved_virtual_files_[i].path);
      EXPECT_TRUE(read_contents.has_value());
      // IStorage uses compound files, so temp files won't be flat text files.
      // Just make sure the original contents appear in the compound files.
      if (tymed != TYMED_ISTORAGE) {
        EXPECT_EQ(kTestFilenamesAndContents[i].second, read_contents.value());
        EXPECT_EQ(static_cast<size_t>(0), read_contents->size());
      }
    }
  }
}

TEST_F(OSExchangeDataWinTest, CFHtml) {
  OSExchangeData data;
  GURL url("http://www.google.com/");
  std::u16string html(
      u"<HTML>\n<BODY>\n"
      u"<b>bold.</b> <i><b>This is bold italic.</b></i>\n"
      u"</BODY>\n</HTML>");
  data.SetHtml(html, url);

  // Check the CF_HTML too.
  std::string expected_cf_html(
      "Version:0.9\r\nStartHTML:0000000139\r\nEndHTML:0000000288\r\n"
      "StartFragment:0000000175\r\nEndFragment:0000000252\r\n"
      "SourceURL:http://www.google.com/\r\n<html>\r\n<body>\r\n"
      "<!--StartFragment-->");
  expected_cf_html += base::UTF16ToUTF8(html);
  expected_cf_html.append("<!--EndFragment-->\r\n</body>\r\n</html>");

  FORMATETC format = ClipboardFormatType::HtmlType().ToFormatEtc();
  STGMEDIUM medium;
  IDataObject* data_object = OSExchangeDataProviderWin::GetIDataObject(data);
  EXPECT_EQ(S_OK, data_object->GetData(&format, &medium));
  base::win::ScopedHGlobal<char*> glob(medium.hGlobal);
  std::string output(glob.data(), glob.size());
  EXPECT_EQ(expected_cf_html, output);
  ReleaseStgMedium(&medium);
}

TEST_F(OSExchangeDataWinTest, SetURLWithMaxPath) {
  OSExchangeData data;
  std::u16string long_title(MAX_PATH + 1, u'a');
  data.SetURL(GURL("http://google.com"), long_title);
}

TEST_F(OSExchangeDataWinTest, ProvideURLForPlainTextURL) {
  OSExchangeData data;
  data.SetString(u"http://google.com");

  OSExchangeData data2(data.provider().Clone());
  ASSERT_TRUE(data2.HasURL(FilenameToURLPolicy::CONVERT_FILENAMES));
  std::vector<ui::ClipboardUrlInfo> url_infos =
      data2.GetURLs(FilenameToURLPolicy::CONVERT_FILENAMES);
  ASSERT_FALSE(url_infos.empty());
  EXPECT_EQ(GURL("http://google.com"), url_infos.front().url);
}

class MockDownloadFileProvider : public DownloadFileProvider {
 public:
  MockDownloadFileProvider() = default;
  ~MockDownloadFileProvider() override = default;
  base::WeakPtr<MockDownloadFileProvider> GetWeakPtr() {
    return weak_ptr_factory_.GetWeakPtr();
  }

  MOCK_METHOD1(Start, void(DownloadFileObserver* observer));
  MOCK_METHOD0(Wait, bool());
  MOCK_METHOD0(Stop, void());

 private:
  base::WeakPtrFactory<MockDownloadFileProvider> weak_ptr_factory_{this};
};

// Verifies that DataObjectImpl::OnDownloadCompleted() doesn't delete
// the DownloadFileProvider instance.
TEST_F(OSExchangeDataWinTest, OnDownloadCompleted) {
  OSExchangeData data;
  Microsoft::WRL::ComPtr<IDataObject> com_data(
      OSExchangeDataProviderWin::GetIDataObject(data));

  OSExchangeDataProviderWin provider(com_data.Get());

  auto download_file_provider = std::make_unique<MockDownloadFileProvider>();
  auto weak_ptr = download_file_provider->GetWeakPtr();
  DownloadFileInfo file_info(
      base::FilePath(FILE_PATH_LITERAL("file_with_no_contents.txt")),
      std::move(download_file_provider));
  provider.SetDownloadFileInfo(&file_info);

  OSExchangeDataProviderWin::GetDataObjectImpl(data)->OnDownloadCompleted(
      base::FilePath());
  EXPECT_TRUE(weak_ptr);
}

// Verifies the data set by DataObjectImpl::SetData with |fRelease| is released
// correctly after the DataObjectImpl instance is destroyed.
TEST_F(OSExchangeDataWinTest, SetDataRelease) {
  RefCountMockStream stream;

  ASSERT_EQ(stream.AddRef(), 1u);
  {
    OSExchangeDataProviderWin data_provider;
    IDataObject* data_object = data_provider.data_object();

    ClipboardFormatType format(
        /* cfFormat= */ CF_TEXT, /* lindex= */ -1, /* tymed= */ TYMED_ISTREAM);
    FORMATETC format_etc = format.ToFormatEtc();

    STGMEDIUM medium;
    medium.tymed = TYMED_ISTREAM;
    medium.pstm = &stream;
    medium.pUnkForRelease = nullptr;

    // |stream| should be released when |data_object| is destroyed since it
    // takes responsibility to release |stream| after used.
    EXPECT_EQ(S_OK,
              data_object->SetData(&format_etc, &medium, /* fRelease= */ TRUE));
    ASSERT_EQ(stream.GetRefCount(), 1u);
  }

  EXPECT_EQ(stream.GetRefCount(), 0u);
}

// Verifies the data duplicated by DataObjectImpl::SetData without |fRelease|
// is released correctly after the DataObjectImpl instance destroyed.
TEST_F(OSExchangeDataWinTest, SetDataNoRelease) {
  RefCountMockStream stream;

  ASSERT_EQ(stream.GetRefCount(), 0u);
  {
    OSExchangeDataProviderWin data_provider;
    IDataObject* data_object = data_provider.data_object();

    ClipboardFormatType format(
        /* cfFormat= */ CF_TEXT, /* lindex= */ -1, /* tymed= */ TYMED_ISTREAM);
    FORMATETC format_etc = format.ToFormatEtc();

    STGMEDIUM medium;
    medium.tymed = TYMED_ISTREAM;
    medium.pstm = &stream;
    medium.pUnkForRelease = nullptr;

    EXPECT_EQ(S_OK, data_object->SetData(&format_etc, &medium,
                                         /* fRelease= */ FALSE));
    ASSERT_EQ(stream.GetRefCount(), 1u);
  }

  // Reference count should be the same as before if |data_object| is
  // destroyed.
  EXPECT_EQ(stream.GetRefCount(), 0u);
}

// Verifies that calling `DataObjectImpl::SetData` with a `FORMATETC` containing
// a target device (`ptd`) correctly performs a deep copy of the variable-length
// `DVTARGETDEVICE` structure. This prevents Use-After-Free of the COM-stub
// owned `ptd` pointer, and Out-of-Bounds heap read when the data object's
// formats are later cloned and marshalled back to the caller.
TEST_F(OSExchangeDataWinTest, SetDataTargetDeviceUAFAndOOB) {
  base::win::ScopedCOMInitializer com_initializer;

  // Create the data provider on the main thread and marshal the `IDataObject`
  // pointer into a stream so it can be unmarshalled and accessed by a
  // background thread.
  OSExchangeDataProviderWin data_provider;
  IDataObject* data_object = data_provider.data_object();

  IStream* stream = nullptr;
  ASSERT_EQ(CoMarshalInterThreadInterfaceInStream(IID_IDataObject, data_object,
                                                  &stream),
            S_OK);

  base::RunLoop run_loop;

  // Run a background thread representing the cross-process COM client.
  base::Thread background_thread("UAFBackgroundThread");
  background_thread.Start();

  background_thread.task_runner()->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](uintptr_t stream_ptr_val, base::OnceClosure quit_closure) {
            base::ScopedClosureRunner quit_runner(std::move(quit_closure));
            {
              base::win::ScopedCOMInitializer com_initializer;

              // Unmarshal the data object proxy.
              IStream* stream_ptr = reinterpret_cast<IStream*>(stream_ptr_val);
              Microsoft::WRL::ComPtr<IDataObject> proxy;
              ASSERT_EQ(CoGetInterfaceAndReleaseStream(stream_ptr,
                                                       IID_PPV_ARGS(&proxy)),
                        S_OK);

              // Call `SetData` with a `FORMATETC` containing a target device.
              const DWORD kTdSize = sizeof(DVTARGETDEVICE);
              base::win::ScopedCoMem<DVTARGETDEVICE> dv;
              dv.Reset(static_cast<DVTARGETDEVICE*>(CoTaskMemAlloc(kTdSize)));
              ASSERT_NE(dv.get(), nullptr);

              dv->tdSize = kTdSize;
              dv->tdDriverNameOffset = 0;
              dv->tdDeviceNameOffset = 0;
              dv->tdPortNameOffset = 0;
              dv->tdExtDevmodeOffset = 0;
              dv->tdData[0] = 0;

              FORMATETC fe =
                  ClipboardFormatType(RegisterClipboardFormatW(L"ChromePtdPoC"),
                                      -1, TYMED_HGLOBAL)
                      .ToFormatEtc();
              fe.ptd = dv.get();

              STGMEDIUM stgm = CreateHGlobalMedium();
              // `DataObjectImpl` takes ownership of `stgm.hGlobal` when calling
              // `SetData` with `fRelease=TRUE`.
              ASSERT_EQ(proxy->SetData(&fe, &stgm, TRUE), S_OK);

              // Free the local target device buffer by resetting the
              // `ScopedCoMem`. The COM stub on the main thread will also free
              // its temporary buffer when `SetData` returns.
              dv.Reset(nullptr);

              // Call `EnumFormatEtc` and retrieve the cloned format. If Chrome
              // shallow-copied `ptd`, it will read a dangling pointer,
              // corrupting the `tdSize` value. During marshalling of `Next()`,
              // COM will crash attempting to read an invalid `tdSize` out of
              // bounds of the 16-byte header, causing `next_hr` to return
              // `RPC_E_SERVERFAULT`. If Chrome correctly performs a deep copy
              // of `ptd` during `SetData` and clones it with the correct size,
              // `Next()` will succeed and return the duplicated `ptd`.
              Microsoft::WRL::ComPtr<IEnumFORMATETC> enum_fmt;
              ASSERT_EQ(proxy->EnumFormatEtc(DATADIR_GET, &enum_fmt), S_OK);
              FORMATETC out_fe = {};
              ULONG fetched = 0;
              EXPECT_EQ(enum_fmt->Next(1, &out_fe, &fetched), S_OK);
              EXPECT_EQ(fetched, 1ul);

              base::win::ScopedCoMem<DVTARGETDEVICE> out_ptd;
              out_ptd.Reset(out_fe.ptd);
              ASSERT_NE(out_ptd.get(), nullptr);
              EXPECT_EQ(out_ptd->tdSize, kTdSize);
            }
          },
          reinterpret_cast<uintptr_t>(stream), run_loop.QuitClosure()));

  run_loop.Run();
}

// Verifies that calling `DataObjectImpl::SetData` with a `FORMATETC` containing
// a `null` target device (`ptd`) is handled safely.
TEST_F(OSExchangeDataWinTest, SetDataTargetDeviceNull) {
  base::win::ScopedCOMInitializer com_initializer;

  OSExchangeDataProviderWin data_provider;
  IDataObject* data_object = data_provider.data_object();

  IStream* stream = nullptr;
  ASSERT_EQ(CoMarshalInterThreadInterfaceInStream(IID_IDataObject, data_object,
                                                  &stream),
            S_OK);

  base::RunLoop run_loop;

  base::Thread background_thread("BackgroundThread");
  background_thread.Start();

  background_thread.task_runner()->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](uintptr_t stream_ptr_val, base::OnceClosure quit_closure) {
            base::ScopedClosureRunner quit_runner(std::move(quit_closure));
            {
              base::win::ScopedCOMInitializer com_initializer;

              IStream* stream_ptr = reinterpret_cast<IStream*>(stream_ptr_val);
              Microsoft::WRL::ComPtr<IDataObject> proxy;
              ASSERT_EQ(CoGetInterfaceAndReleaseStream(stream_ptr,
                                                       IID_PPV_ARGS(&proxy)),
                        S_OK);

              FORMATETC fe = ClipboardFormatType(
                                 RegisterClipboardFormatW(L"ChromePtdNull"), -1,
                                 TYMED_HGLOBAL)
                                 .ToFormatEtc();
              fe.ptd = nullptr;

              STGMEDIUM stgm = CreateHGlobalMedium();
              // `DataObjectImpl` takes ownership of `stgm.hGlobal` when calling
              // `SetData` with `fRelease=TRUE`.
              ASSERT_EQ(proxy->SetData(&fe, &stgm, TRUE), S_OK);

              Microsoft::WRL::ComPtr<IEnumFORMATETC> enum_fmt;
              ASSERT_EQ(proxy->EnumFormatEtc(DATADIR_GET, &enum_fmt), S_OK);
              FORMATETC out_fe = {};
              ULONG fetched = 0;
              EXPECT_EQ(enum_fmt->Next(1, &out_fe, &fetched), S_OK);
              EXPECT_EQ(fetched, 1ul);
              EXPECT_EQ(out_fe.ptd, nullptr);
            }
          },
          reinterpret_cast<uintptr_t>(stream), run_loop.QuitClosure()));

  run_loop.Run();
}

// Verifies that calling `DataObjectImpl::SetData` directly (in-process) with a
// `FORMATETC` containing a target device of an abnormally small `tdSize`
// (smaller than `sizeof(DVTARGETDEVICE)`) is handled safely without
// out-of-bounds access.
TEST_F(OSExchangeDataWinTest, SetDataTargetDeviceSmallSize) {
  OSExchangeDataProviderWin data_provider;
  IDataObject* data_object = data_provider.data_object();

  // Call `SetData` directly on the main thread with a target device size of
  // only 4 bytes (enough for `tdSize` itself, but smaller than
  // `sizeof(DVTARGETDEVICE)`).
  const DWORD kTdSize = 4;
  base::win::ScopedCoMem<DVTARGETDEVICE> dv;
  dv.Reset(static_cast<DVTARGETDEVICE*>(CoTaskMemAlloc(kTdSize)));
  ASSERT_NE(dv.get(), nullptr);
  dv->tdSize = kTdSize;

  FORMATETC fe =
      ClipboardFormatType(RegisterClipboardFormatW(L"ChromePtdSmall"), -1,
                          TYMED_HGLOBAL)
          .ToFormatEtc();
  fe.ptd = dv.get();

  STGMEDIUM stgm = CreateHGlobalMedium();
  // `DataObjectImpl` takes ownership of `stgm.hGlobal` when calling `SetData`
  // with `fRelease=TRUE`.
  ASSERT_EQ(data_object->SetData(&fe, &stgm, TRUE), S_OK);
  dv.Reset(nullptr);

  Microsoft::WRL::ComPtr<IEnumFORMATETC> enum_fmt;
  ASSERT_EQ(data_object->EnumFormatEtc(DATADIR_GET, &enum_fmt), S_OK);
  FORMATETC out_fe = {};
  ULONG fetched = 0;
  EXPECT_EQ(enum_fmt->Next(1, &out_fe, &fetched), S_OK);
  EXPECT_EQ(fetched, 1ul);

  EXPECT_EQ(out_fe.ptd, nullptr);
}

TEST_F(OSExchangeDataWinTest, ScopedTargetDeviceLifecycle) {
  ScopedTargetDevice empty;
  EXPECT_EQ(empty.get(), nullptr);

  const DWORD kTdSize = 64;
  base::win::ScopedCoMem<DVTARGETDEVICE> raw;
  raw.Reset(static_cast<DVTARGETDEVICE*>(CoTaskMemAlloc(kTdSize)));
  ASSERT_NE(raw.get(), nullptr);
  raw->tdSize = kTdSize;
  raw->tdDriverNameOffset = 0;
  raw->tdDeviceNameOffset = 0;
  raw->tdPortNameOffset = 0;
  raw->tdExtDevmodeOffset = 0;

  ScopedTargetDevice device(raw.get());
  EXPECT_NE(device.get(), nullptr);
  EXPECT_NE(device.get(), raw.get());
  EXPECT_EQ(device.get()->tdSize, kTdSize);

  ScopedTargetDevice copy(device);
  EXPECT_NE(copy.get(), nullptr);
  EXPECT_NE(copy.get(), device.get());
  EXPECT_EQ(copy.get()->tdSize, kTdSize);

  DVTARGETDEVICE* copy_raw = copy.get();
  ScopedTargetDevice moved(std::move(copy));
  EXPECT_EQ(copy.get(), nullptr);
  EXPECT_EQ(moved.get(), copy_raw);

  ScopedTargetDevice assigned;
  assigned = device;
  EXPECT_NE(assigned.get(), nullptr);
  EXPECT_NE(assigned.get(), device.get());
  EXPECT_EQ(assigned.get()->tdSize, kTdSize);

  ScopedTargetDevice moved_assigned;
  DVTARGETDEVICE* moved_raw = moved.get();
  moved_assigned = std::move(moved);
  EXPECT_EQ(moved.get(), nullptr);
  EXPECT_EQ(moved_assigned.get(), moved_raw);

  base::win::ScopedCoMem<DVTARGETDEVICE> released;
  released.Reset(moved_assigned.release());
  EXPECT_EQ(moved_assigned.get(), nullptr);
  EXPECT_EQ(released.get(), moved_raw);

  // Test rejection of abnormally small target devices.
  {
    const DWORD kSmallTdSize = sizeof(DVTARGETDEVICE) - 1;
    base::win::ScopedCoMem<DVTARGETDEVICE> small_raw;
    small_raw.Reset(static_cast<DVTARGETDEVICE*>(CoTaskMemAlloc(kSmallTdSize)));
    ASSERT_NE(small_raw.get(), nullptr);
    small_raw->tdSize = kSmallTdSize;

    ScopedTargetDevice small_device(small_raw.get());
    EXPECT_EQ(small_device.get(), nullptr);
  }
}

TEST_F(OSExchangeDataWinTest, ScopedFormatEtcLifecycle) {
  ScopedFormatEtc empty;
  EXPECT_EQ(empty.format_etc.ptd, nullptr);

  const DWORD kTdSize = 64;
  base::win::ScopedCoMem<DVTARGETDEVICE> raw_td;
  raw_td.Reset(static_cast<DVTARGETDEVICE*>(CoTaskMemAlloc(kTdSize)));
  ASSERT_NE(raw_td.get(), nullptr);
  raw_td->tdSize = kTdSize;
  raw_td->tdDriverNameOffset = 0;
  raw_td->tdDeviceNameOffset = 0;
  raw_td->tdPortNameOffset = 0;
  raw_td->tdExtDevmodeOffset = 0;

  FORMATETC raw_fe = {};
  raw_fe.cfFormat = CF_TEXT;
  raw_fe.dwAspect = DVASPECT_CONTENT;
  raw_fe.lindex = -1;
  raw_fe.tymed = TYMED_HGLOBAL;
  raw_fe.ptd = raw_td.get();

  ScopedFormatEtc format(raw_fe);
  EXPECT_EQ(format->cfFormat, CF_TEXT);
  EXPECT_NE(format.format_etc.ptd, nullptr);
  EXPECT_NE(format.format_etc.ptd, raw_td.get());
  EXPECT_EQ(format.format_etc.ptd, format.target_device.get());

  ScopedFormatEtc copy(format);
  EXPECT_EQ(copy->cfFormat, CF_TEXT);
  EXPECT_NE(copy.format_etc.ptd, nullptr);
  EXPECT_NE(copy.format_etc.ptd, format.format_etc.ptd);
  EXPECT_EQ(copy.format_etc.ptd, copy.target_device.get());

  DVTARGETDEVICE* copy_td_raw = copy.target_device.get();
  ScopedFormatEtc moved(std::move(copy));
  EXPECT_EQ(copy.format_etc.ptd, nullptr);
  EXPECT_EQ(moved.format_etc.ptd, copy_td_raw);
  EXPECT_EQ(moved.format_etc.ptd, moved.target_device.get());

  ScopedFormatEtc assigned;
  assigned = format;
  EXPECT_EQ(assigned->cfFormat, CF_TEXT);
  EXPECT_NE(assigned.format_etc.ptd, nullptr);
  EXPECT_NE(assigned.format_etc.ptd, format.format_etc.ptd);
  EXPECT_EQ(assigned.format_etc.ptd, assigned.target_device.get());

  ScopedFormatEtc moved_assigned;
  DVTARGETDEVICE* moved_td_raw = moved.target_device.get();
  moved_assigned = std::move(moved);
  EXPECT_EQ(moved.format_etc.ptd, nullptr);
  EXPECT_EQ(moved_assigned.format_etc.ptd, moved_td_raw);
  EXPECT_EQ(moved_assigned.format_etc.ptd, moved_assigned.target_device.get());
}

}  // namespace ui
