chromium/ui/base/clipboard/clipboard_util_win.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "ui/base/clipboard/clipboard_util_win.h"

#include <shellapi.h>
#include <wininet.h>  // For INTERNET_MAX_URL_LENGTH.
#include <wrl/client.h>

#include <limits>
#include <optional>
#include <string_view>
#include <utility>

#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/ranges/algorithm.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/win/scoped_hglobal.h"
#include "base/win/shlwapi.h"
#include "net/base/filename_util.h"
#include "ui/base/clipboard/clipboard_format_type.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "url/gurl.h"

namespace ui {

namespace {

constexpr STGMEDIUM kNullStorageMedium = {.tymed = TYMED_NULL,
                                          .pUnkForRelease = nullptr};

bool HasData(IDataObject* data_object, const ClipboardFormatType& format) {
  FORMATETC format_etc = format.ToFormatEtc();
  return SUCCEEDED(data_object->QueryGetData(&format_etc));
}

bool GetData(IDataObject* data_object,
             const ClipboardFormatType& format,
             STGMEDIUM* medium) {
  FORMATETC format_etc = format.ToFormatEtc();
  return SUCCEEDED(data_object->GetData(&format_etc, medium));
}

bool GetUrlFromHDrop(IDataObject* data_object,
                     GURL* url,
                     std::u16string* title) {
  DCHECK(data_object && url && title);

  bool success = false;
  STGMEDIUM medium;
  if (!GetData(data_object, ClipboardFormatType::CFHDropType(), &medium))
    return false;

  {
    base::win::ScopedHGlobal<HDROP> hdrop(medium.hGlobal);

    if (!hdrop.data()) {
      return false;
    }

    wchar_t filename[MAX_PATH];
    if (DragQueryFileW(hdrop.data(), 0, filename, std::size(filename))) {
      wchar_t url_buffer[INTERNET_MAX_URL_LENGTH];
      if (0 == _wcsicmp(PathFindExtensionW(filename), L".url") &&
          GetPrivateProfileStringW(L"InternetShortcut", L"url", 0, url_buffer,
                                   std::size(url_buffer), filename)) {
        *url = GURL(base::AsStringPiece16(url_buffer));
        PathRemoveExtension(filename);
        title->assign(base::as_u16cstr(PathFindFileName(filename)));
        success = url->is_valid();
      }
    }
  }

  ReleaseStgMedium(&medium);
  return success;
}

void SplitUrlAndTitle(const std::u16string& str,
                      GURL* url,
                      std::u16string* title) {
  DCHECK(url && title);
  size_t newline_pos = str.find('\n');
  if (newline_pos != std::u16string::npos) {
    *url = GURL(std::u16string(str, 0, newline_pos));
    title->assign(str, newline_pos + 1, std::u16string::npos);
  } else {
    *url = GURL(str);
    title->assign(str);
  }
}

// Performs a case-insensitive search for a file path in a vector of existing
// filepaths. Case-insensivity is needed for file systems such as Windows where
// A.txt and a.txt are considered the same file name.
bool ContainsFilePathCaseInsensitive(
    const std::vector<base::FilePath>& existing_filenames,
    const base::FilePath& candidate_path) {
  return base::ranges::any_of(existing_filenames,
                              [&candidate_path](const base::FilePath& elem) {
                                return base::FilePath::CompareEqualIgnoreCase(
                                    elem.value(), candidate_path.value());
                              });
}

// Returns a unique display name for a virtual file, as it is possible that the
// filenames found in the file group descriptor are not unique (e.g. multiple
// emails with the same subject line are dragged out of Outlook.exe).
// |uniquifier| is incremented on encountering a non-unique file name.
base::FilePath GetUniqueVirtualFilename(
    const std::wstring& candidate_name,
    const std::vector<base::FilePath>& existing_filenames,
    unsigned int* uniquifier) {
  // Remove any possible filepath components/separators that drag source may
  // have included in virtual file name.
  base::FilePath unique_name = base::FilePath(candidate_name).BaseName();

  // To mitigate against running up against MAX_PATH limitations (temp files
  // failing to be created), truncate the display name.
  const size_t kTruncatedDisplayNameLength = 128;
  const std::wstring extension = unique_name.Extension();
  unique_name = unique_name.RemoveExtension();
  std::wstring truncated = unique_name.value();
  if (truncated.length() > kTruncatedDisplayNameLength) {
    truncated.erase(kTruncatedDisplayNameLength);
    unique_name = base::FilePath(truncated);
  }
  unique_name = unique_name.AddExtension(extension);

  // Replace any file name illegal characters.
  unique_name = net::GenerateFileName(GURL(), std::string(), std::string(),
                                      base::WideToUTF8(unique_name.value()),
                                      std::string(), std::string());

  // Make the file name unique. This is more involved than just marching through
  // |existing_filenames|, finding the first match, uniquifying, then breaking
  // out of the loop. For example, consider an array of candidate display names
  // {"A (1) (2)", "A", "A (1) ", "A"}. In the first three iterations of the
  // outer loop in GetVirtualFilenames, the candidate names are already unique
  // and so simply pushed to the vector of |filenames|. On the fourth iteration
  // of the outer loop and second iteration of the inner loop (that in
  // GetUniqueVirtualFilename), the duplicate name is encountered and the fourth
  // item is tentatively uniquified to "A (1)". If this inner loop were exited
  // now, the final |filenames| would be {"A (1) (2)", "A", "A (1) ", "A (1)"}
  // and would contain duplicate entries. So try not breaking out of the
  // inner loop. In that case on the third iteration of the inner loop, the
  // tentative unique name encounters another duplicate, so now gets uniquefied
  // to "A (1) (2)" and if we then don't restart the loop, we would end up with
  // the final |filenames| being {"A (1) (2)", "A", "A (1) ", "A (1) (2)"} and
  // we still have duplicate entries. Instead we need to test against the
  // entire collection of existing names on each uniquification attempt.

  // Same value used in base::GetUniquePathNumber.
  static const int kMaxUniqueFiles = 100;
  int count = 1;
  for (; count <= kMaxUniqueFiles; ++count) {
    if (!ContainsFilePathCaseInsensitive(existing_filenames, unique_name))
      break;

    unique_name = unique_name.InsertBeforeExtensionASCII(
        base::StringPrintf(" (%d)", (*uniquifier)++));
  }
  if (count > kMaxUniqueFiles)
    unique_name = base::FilePath();

  return unique_name;
}

// Creates a uniquely-named temporary file based on the suggested filename, or
// an empty path on error. The file will be empty and all handles closed after
// this function returns.
base::FilePath CreateTemporaryFileWithSuggestedName(
    const base::FilePath& suggested_name) {
  base::FilePath temp_path1;
  if (!base::CreateTemporaryFile(&temp_path1))
    return base::FilePath();

  base::FilePath temp_path2 = temp_path1.DirName().Append(suggested_name);

  // Make filename unique.
  temp_path2 = base::GetUniquePath(temp_path2);
  if (temp_path2.empty())
    return base::FilePath();  // Failed to make a unique path.

  base::File::Error replace_file_error = base::File::FILE_OK;
  if (!ReplaceFile(temp_path1, temp_path2, &replace_file_error))
    return base::FilePath();

  return temp_path2;
}

// This method performs file I/O and thus is executed on a worker thread. An
// empty FilePath for the temp file is returned on failure.
base::FilePath WriteFileContentsToTempFile(const base::FilePath& suggested_name,
                                           HGLOBAL hdata) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  if (!hdata)
    return base::FilePath();

  base::FilePath temp_path =
      CreateTemporaryFileWithSuggestedName(suggested_name);

  if (!temp_path.empty()) {
    base::win::ScopedHGlobal<char*> data(hdata);
    // Don't write to the temp file for empty content--leave it at 0-bytes.
    if (!(data.size() == 1 && data.data()[0] == '\0')) {
      if (!base::WriteFile(temp_path,
                           std::string_view(data.data(), data.size()))) {
        base::DeleteFile(temp_path);
        return base::FilePath();
      }
    }
  }

  ::GlobalFree(hdata);

  return temp_path;
}

std::vector<
    std::pair</*temp path*/ base::FilePath, /*display name*/ base::FilePath>>
WriteAllFileContentsToTempFiles(
    const std::vector<base::FilePath>& display_names,
    const std::vector<HGLOBAL>& memory_backed_contents) {
  DCHECK_EQ(display_names.size(), memory_backed_contents.size());

  std::vector<std::pair<base::FilePath, base::FilePath>> filepaths_and_names;
  for (size_t i = 0; i < display_names.size(); i++) {
    base::FilePath temp_path = WriteFileContentsToTempFile(
        display_names[i], memory_backed_contents[i]);

    filepaths_and_names.push_back({temp_path, display_names[i]});
  }

  return filepaths_and_names;
}

// Caller's responsibility to call GlobalFree on returned HGLOBAL when done with
// the data. This method must be performed on main thread as it is using the
// IDataObject marshalled there.
HGLOBAL CopyFileContentsToHGlobal(IDataObject* data_object, LONG index) {
  DCHECK(data_object);
  HGLOBAL hdata = nullptr;

  if (!HasData(data_object, ClipboardFormatType::FileContentAtIndexType(index)))
    return hdata;

  STGMEDIUM content;
  if (!GetData(data_object, ClipboardFormatType::FileContentAtIndexType(index),
               &content))
    return hdata;

  HRESULT hr = S_OK;

  if (content.tymed == TYMED_ISTORAGE) {
    // For example, messages dragged out of Outlook.exe.

    Microsoft::WRL::ComPtr<ILockBytes> lock_bytes;
    hr = ::CreateILockBytesOnHGlobal(nullptr, /* fDeleteOnRelease*/ FALSE,
                                     &lock_bytes);

    Microsoft::WRL::ComPtr<IStorage> storage;
    if (SUCCEEDED(hr)) {
      hr = ::StgCreateDocfileOnILockBytes(
          lock_bytes.Get(), STGM_READWRITE | STGM_SHARE_EXCLUSIVE | STGM_CREATE,
          0, &storage);
    }

    if (SUCCEEDED(hr))
      hr = content.pstg->CopyTo(0, nullptr, nullptr, storage.Get());

    if (SUCCEEDED(hr))
      hr = storage->Commit(STGC_OVERWRITE);

    if (SUCCEEDED(hr))
      hr = ::GetHGlobalFromILockBytes(lock_bytes.Get(), &hdata);

    if (FAILED(hr))
      hdata = nullptr;
  } else if (content.tymed == TYMED_ISTREAM) {
    // For example, attachments dragged out of messages in Outlook.exe.

    Microsoft::WRL::ComPtr<IStream> stream;
    hr =
        ::CreateStreamOnHGlobal(nullptr, /* fDeleteOnRelease */ FALSE, &stream);
    if (SUCCEEDED(hr)) {
      // A properly implemented IDataObject::GetData moves the stream pointer to
      // the end. Need to seek to the beginning before copying the data then
      // seek back to the original position.
      const LARGE_INTEGER zero_displacement = {};
      ULARGE_INTEGER original_position = {};
      // Obtain the original stream pointer position. If the stream doesn't
      // support seek, will still attempt to copy the data unless the failure is
      // due to access being denied (enterprise protected data e.g.).
      HRESULT hr_seek = content.pstm->Seek(zero_displacement, STREAM_SEEK_CUR,
                                           &original_position);
      if (hr_seek != E_ACCESSDENIED) {
        if (SUCCEEDED(hr_seek)) {
          // Seek to the beginning.
          hr_seek =
              content.pstm->Seek(zero_displacement, STREAM_SEEK_SET, nullptr);
        }

        // Copy all data to the file stream.
        ULARGE_INTEGER max_bytes;
        max_bytes.QuadPart = std::numeric_limits<uint64_t>::max();
        hr = content.pstm->CopyTo(stream.Get(), max_bytes, nullptr, nullptr);

        if (SUCCEEDED(hr_seek)) {
          // Restore the stream pointer to its original position.
          LARGE_INTEGER original_offset;
          original_offset.QuadPart = original_position.QuadPart;
          content.pstm->Seek(original_offset, STREAM_SEEK_SET, nullptr);
        }
      } else {
        // Access was denied.
        hr = hr_seek;
      }

      if (SUCCEEDED(hr))
        hr = ::GetHGlobalFromStream(stream.Get(), &hdata);

      if (FAILED(hr))
        hdata = nullptr;
    }
  } else if (content.tymed == TYMED_HGLOBAL) {
    // For example, anchor (internet shortcut) dragged out of Spartan Edge.
    // Copy the data as it will be written to a file on a worker thread and we
    // need to call ReleaseStgMedium to free the memory allocated by the drag
    // source.
    base::win::ScopedHGlobal<char*> data_source(content.hGlobal);
    hdata = ::GlobalAlloc(GHND, data_source.size());
    if (hdata) {
      base::win::ScopedHGlobal<char*> data_destination(hdata);
      memcpy(data_destination.data(), data_source.data(), data_source.size());
    }
  }

  // Safe to release the medium now since all the data has been copied.
  ReleaseStgMedium(&content);

  return hdata;
}

std::wstring ConvertString(const char* string) {
  return base::UTF8ToWide(string);
}

std::wstring ConvertString(const wchar_t* string) {
  return string;
}

template <typename FileGroupDescriptorType>
struct FileGroupDescriptorData;

template <>
struct FileGroupDescriptorData<FILEGROUPDESCRIPTORW> {
  static bool get(IDataObject* data_object, STGMEDIUM* medium) {
    return GetData(data_object, ClipboardFormatType::FileDescriptorType(),
                   medium);
  }
};

template <>
struct FileGroupDescriptorData<FILEGROUPDESCRIPTORA> {
  static bool get(IDataObject* data_object, STGMEDIUM* medium) {
    return GetData(data_object, ClipboardFormatType::FileDescriptorAType(),
                   medium);
  }
};

// Retrieves display names of virtual files, making sure they are unique.
// Use template parameter of FILEGROUPDESCRIPTORW for retrieving Unicode data
// and FILEGROUPDESCRIPTORA for ascii.
template <typename FileGroupDescriptorType>
std::optional<std::vector<base::FilePath>> GetVirtualFilenames(
    IDataObject* data_object) {
  STGMEDIUM medium;

  if (!FileGroupDescriptorData<FileGroupDescriptorType>::get(data_object,
                                                             &medium)) {
    return std::nullopt;
  }

  std::vector<base::FilePath> filenames;

  {
    base::win::ScopedHGlobal<FileGroupDescriptorType*> fgd(medium.hGlobal);
    if (!fgd.data()) {
      return std::nullopt;
    }

    unsigned int num_files = fgd->cItems;
    // We expect there to be at least one file in here.
    DCHECK_GE(num_files, 1u);

    // Value to be incremented to ensure a unique display name, as it is
    // possible that the filenames found in the file group descriptor are not
    // unique (e.g. multiple emails with the same subject line are dragged out
    // of Outlook.exe).
    unsigned int uniquifier = 1;

    for (size_t i = 0; i < num_files; i++) {
      // Folder entries not currently supported--skip this item.
      if ((fgd->fgd[i].dwFlags & FD_ATTRIBUTES) &&
          (fgd->fgd[i].dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
        DLOG(WARNING) << "GetVirtualFilenames: display name '"
                      << ConvertString(fgd->fgd[i].cFileName)
                      << "' refers to a directory (not supported).";
        continue;
      }
      base::FilePath display_name = GetUniqueVirtualFilename(
          ConvertString(fgd->fgd[i].cFileName), filenames, &uniquifier);

      filenames.push_back(display_name);
    }
  }

  ReleaseStgMedium(&medium);
  return filenames;
}

template <typename FileGroupDescriptorType>
bool GetFileNameFromFirstDescriptor(IDataObject* data_object,
                                    std::wstring* filename) {
  STGMEDIUM medium;

  if (!FileGroupDescriptorData<FileGroupDescriptorType>::get(data_object,
                                                             &medium))
    return false;

  {
    base::win::ScopedHGlobal<FileGroupDescriptorType*> fgd(medium.hGlobal);
    // We expect there to be at least one file in here.
    DCHECK_GE(fgd->cItems, 1u);
    filename->assign(ConvertString(fgd->fgd[0].cFileName));
  }
  ReleaseStgMedium(&medium);
  return true;
}

}  // namespace

namespace clipboard_util {

bool HasUrl(IDataObject* data_object, bool convert_filenames) {
  DCHECK(data_object);
  return HasData(data_object, ClipboardFormatType::MozUrlType()) ||
         HasData(data_object, ClipboardFormatType::UrlType()) ||
         HasData(data_object, ClipboardFormatType::UrlAType()) ||
         (convert_filenames && HasFilenames(data_object));
}

bool HasFilenames(IDataObject* data_object) {
  DCHECK(data_object);
  return HasData(data_object, ClipboardFormatType::CFHDropType()) ||
         HasData(data_object, ClipboardFormatType::FilenameType()) ||
         HasData(data_object, ClipboardFormatType::FilenameAType());
}

bool HasVirtualFilenames(IDataObject* data_object) {
  DCHECK(data_object);
  // Favor real files on the file system over virtual files.
  return !HasFilenames(data_object) &&
         HasData(data_object, ClipboardFormatType::FileContentAtIndexType(0)) &&
         (HasData(data_object, ClipboardFormatType::FileDescriptorType()) ||
          HasData(data_object, ClipboardFormatType::FileDescriptorAType()));
}

bool HasFileContents(IDataObject* data_object) {
  DCHECK(data_object);
  return HasData(data_object, ClipboardFormatType::FileContentZeroType()) &&
         (HasData(data_object, ClipboardFormatType::FileDescriptorType()) ||
          HasData(data_object, ClipboardFormatType::FileDescriptorAType()));
}

bool HasHtml(IDataObject* data_object) {
  DCHECK(data_object);
  return HasData(data_object, ClipboardFormatType::HtmlType()) ||
         HasData(data_object, ClipboardFormatType::TextHtmlType());
}

bool HasPlainText(IDataObject* data_object) {
  DCHECK(data_object);
  return HasData(data_object, ClipboardFormatType::PlainTextType()) ||
         HasData(data_object, ClipboardFormatType::PlainTextAType());
}

bool GetUrl(IDataObject* data_object,
            GURL* url,
            std::u16string* title,
            bool convert_filenames) {
  DCHECK(data_object && url && title);
  if (!HasUrl(data_object, convert_filenames))
    return false;

  // Try to extract a URL from |data_object| in a variety of formats.
  STGMEDIUM store;
  if (GetUrlFromHDrop(data_object, url, title))
    return true;

  if (GetData(data_object, ClipboardFormatType::MozUrlType(), &store) ||
      GetData(data_object, ClipboardFormatType::UrlType(), &store)) {
    {
      // Mozilla URL format or Unicode URL
      base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
      SplitUrlAndTitle(base::WideToUTF16(data.data()), url, title);
    }
    ReleaseStgMedium(&store);
    return url->is_valid();
  }

  if (GetData(data_object, ClipboardFormatType::UrlAType(), &store)) {
    {
      // URL using ASCII
      base::win::ScopedHGlobal<char*> data(store.hGlobal);
      SplitUrlAndTitle(base::UTF8ToUTF16(data.data()), url, title);
    }
    ReleaseStgMedium(&store);
    return url->is_valid();
  }

  if (convert_filenames) {
    std::vector<std::wstring> filenames;
    if (!GetFilenames(data_object, &filenames))
      return false;
    DCHECK_GT(filenames.size(), 0U);
    *url = net::FilePathToFileURL(base::FilePath(filenames[0]));
    return url->is_valid();
  }

  return false;
}

bool GetFilenames(IDataObject* data_object,
                  std::vector<std::wstring>* filenames) {
  DCHECK(data_object && filenames);
  if (!HasFilenames(data_object))
    return false;

  STGMEDIUM medium;
  if (GetData(data_object, ClipboardFormatType::CFHDropType(), &medium)) {
    {
      base::win::ScopedHGlobal<HDROP> hdrop(medium.hGlobal);
      if (!hdrop.data()) {
        return false;
      }

      const int kMaxFilenameLen = 4096;
      const unsigned num_files = DragQueryFileW(hdrop.data(), 0xffffffff, 0, 0);
      for (unsigned int i = 0; i < num_files; ++i) {
        wchar_t filename[kMaxFilenameLen];
        if (!DragQueryFileW(hdrop.data(), i, filename, kMaxFilenameLen)) {
          continue;
        }
        filenames->push_back(filename);
      }
    }
    ReleaseStgMedium(&medium);
    return !filenames->empty();
  }

  if (GetData(data_object, ClipboardFormatType::FilenameType(), &medium)) {
    {
      // filename using Unicode
      base::win::ScopedHGlobal<wchar_t*> data(medium.hGlobal);
      if (data.data() && data.data()[0]) {
        filenames->push_back(data.data());
      }
    }
    ReleaseStgMedium(&medium);
    return true;
  }

  if (GetData(data_object, ClipboardFormatType::FilenameAType(), &medium)) {
    {
      // filename using ASCII
      base::win::ScopedHGlobal<char*> data(medium.hGlobal);
      if (data.data() && data.data()[0]) {
        filenames->push_back(base::SysNativeMBToWide(data.data()));
      }
    }
    ReleaseStgMedium(&medium);
    return true;
  }

  return false;
}

STGMEDIUM CreateStorageForFileNames(const std::vector<FileInfo>& filenames) {
  // CF_HDROP clipboard format consists of DROPFILES structure, a series of file
  // names including the terminating null character and the additional null
  // character at the tail to terminate the array.
  // For example,
  //| DROPFILES | FILENAME 1 | NULL | ... | FILENAME n | NULL | NULL |
  // For more details, please refer to
  // https://docs.microsoft.com/en-us/windows/desktop/shell/clipboard#cf_hdrop

  if (filenames.empty())
    return kNullStorageMedium;

  const size_t kDropFilesHeaderSizeInBytes = sizeof(DROPFILES);
  size_t total_bytes = kDropFilesHeaderSizeInBytes;
  for (const auto& filename : filenames) {
    // Allocate memory of the filename's length including the null
    // character.
    total_bytes += (filename.path.value().length() + 1) * sizeof(wchar_t);
  }
  // |data| needs to be terminated by an additional null character.
  total_bytes += sizeof(wchar_t);

  // GHND combines GMEM_MOVEABLE and GMEM_ZEROINIT, and GMEM_ZEROINIT
  // initializes memory contents to zero.
  HANDLE hdata = GlobalAlloc(GHND, total_bytes);

  base::win::ScopedHGlobal<DROPFILES*> locked_mem(hdata);
  DROPFILES* drop_files = locked_mem.data();
  drop_files->pFiles = sizeof(DROPFILES);
  drop_files->fWide = TRUE;

  wchar_t* data = reinterpret_cast<wchar_t*>(
      reinterpret_cast<BYTE*>(drop_files) + kDropFilesHeaderSizeInBytes);

  size_t next_filename_offset = 0;
  for (const auto& filename : filenames) {
    wcscpy(data + next_filename_offset, filename.path.value().c_str());
    // Skip the terminating null character of the filename.
    next_filename_offset += filename.path.value().length() + 1;
  }

  STGMEDIUM storage = {
      .tymed = TYMED_HGLOBAL, .hGlobal = hdata, .pUnkForRelease = nullptr};
  return storage;
}

std::optional<std::vector<base::FilePath>> GetVirtualFilenames(
    IDataObject* data_object) {
  DCHECK(data_object);
  if (!HasVirtualFilenames(data_object))
    return std::nullopt;

  // Nothing prevents the drag source app from using the CFSTR_FILEDESCRIPTORA
  // ANSI format (e.g., it could be that it doesn't support Unicode). So need to
  // check for both the ANSI and Unicode file group descriptors.

  // Unicode.
  std::optional<std::vector<base::FilePath>> filenames =
      ui::GetVirtualFilenames<FILEGROUPDESCRIPTORW>(data_object);
  if (filenames) {
    return filenames;
  }

  // ASCII.
  return ui::GetVirtualFilenames<FILEGROUPDESCRIPTORA>(data_object);
}

void GetVirtualFilesAsTempFiles(
    IDataObject* data_object,
    base::OnceCallback<
        void(const std::vector<std::pair</*temp path*/ base::FilePath,
                                         /*display name*/ base::FilePath>>&)>
        callback) {
  // Retrieve the display names of the virtual files.
  std::optional<std::vector<base::FilePath>> display_names =
      GetVirtualFilenames(data_object);
  if (!display_names) {
    std::move(callback).Run({});
    return;
  }

  // Write the file contents to global memory.
  std::vector<HGLOBAL> memory_backed_contents;
  for (size_t i = 0; i < display_names.value().size(); i++) {
    HGLOBAL hdata = CopyFileContentsToHGlobal(data_object, i);
    memory_backed_contents.push_back(hdata);
  }

  // Queue a task to actually write the temp files on a worker thread.
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING},
      base::BindOnce(&WriteAllFileContentsToTempFiles, display_names.value(),
                     memory_backed_contents),
      std::move(callback));  // callback on the UI thread
}

bool GetPlainText(IDataObject* data_object, std::u16string* plain_text) {
  DCHECK(data_object && plain_text);
  if (!HasPlainText(data_object))
    return false;

  STGMEDIUM store;
  if (GetData(data_object, ClipboardFormatType::PlainTextType(), &store)) {
    {
      // Unicode text
      base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
      plain_text->assign(base::as_u16cstr(data.data()));
    }
    ReleaseStgMedium(&store);
    return true;
  }

  if (GetData(data_object, ClipboardFormatType::PlainTextAType(), &store)) {
    {
      // ASCII text
      base::win::ScopedHGlobal<char*> data(store.hGlobal);
      plain_text->assign(base::UTF8ToUTF16(data.data()));
    }
    ReleaseStgMedium(&store);
    return true;
  }

  // If a file is dropped on the window, it does not provide either of the
  // plain text formats, so here we try to forcibly get a url.
  GURL url;
  std::u16string title;
  if (GetUrl(data_object, &url, &title, false)) {
    *plain_text = base::UTF8ToUTF16(url.spec());
    return true;
  }
  return false;
}

bool GetHtml(IDataObject* data_object,
             std::u16string* html,
             std::string* base_url) {
  DCHECK(data_object && html && base_url);

  STGMEDIUM store;
  if (HasData(data_object, ClipboardFormatType::HtmlType()) &&
      GetData(data_object, ClipboardFormatType::HtmlType(), &store)) {
    {
      // MS CF html
      base::win::ScopedHGlobal<char*> data(store.hGlobal);

      std::string html_utf8;
      CFHtmlToHtml(std::string_view(data.data(), data.size()), &html_utf8,
                   base_url);
      html->assign(base::UTF8ToUTF16(html_utf8));
    }
    ReleaseStgMedium(&store);
    return true;
  }

  if (!HasData(data_object, ClipboardFormatType::TextHtmlType()))
    return false;

  if (!GetData(data_object, ClipboardFormatType::TextHtmlType(), &store))
    return false;

  {
    // text/html
    base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
    html->assign(base::as_u16cstr(data.data()));
  }
  ReleaseStgMedium(&store);
  return true;
}

bool GetFileContents(IDataObject* data_object,
                     std::wstring* filename,
                     std::string* file_contents) {
  DCHECK(data_object && filename && file_contents);
  if (!HasFileContents(data_object))
    return false;

  STGMEDIUM content;
  // The call to GetData can be very slow depending on what is in
  // |data_object|.
  if (GetData(data_object, ClipboardFormatType::FileContentZeroType(),
              &content)) {
    if (TYMED_HGLOBAL == content.tymed) {
      base::win::ScopedHGlobal<char*> data(content.hGlobal);
      file_contents->assign(data.data(), data.size());
    }
    ReleaseStgMedium(&content);
  }

  // Nothing prevents the drag source app from using the CFSTR_FILEDESCRIPTORA
  // ANSI format (e.g., it could be that it doesn't support Unicode). So need to
  // check for both the ANSI and Unicode file group descriptors.
  if (GetFileNameFromFirstDescriptor<FILEGROUPDESCRIPTORW>(data_object,
                                                           filename)) {
    // file group descriptor using Unicode.
    return true;
  }

  if (GetFileNameFromFirstDescriptor<FILEGROUPDESCRIPTORA>(data_object,
                                                           filename)) {
    // file group descriptor using ASCII.
    return true;
  }

  return false;
}

bool GetDataTransferCustomData(
    IDataObject* data_object,
    std::unordered_map<std::u16string, std::u16string>* custom_data) {
  DCHECK(data_object && custom_data);

  if (!HasData(data_object, ClipboardFormatType::DataTransferCustomType())) {
    return false;
  }

  STGMEDIUM store;
  if (GetData(data_object, ClipboardFormatType::DataTransferCustomType(),
              &store)) {
    {
      base::win::ScopedHGlobal<const uint8_t*> data(store.hGlobal);
      if (std::optional<std::unordered_map<std::u16string, std::u16string>>
              maybe_custom_data = ReadCustomDataIntoMap(data);
          maybe_custom_data) {
        *custom_data = std::move(*maybe_custom_data);
        return true;
      }
    }
    ReleaseStgMedium(&store);
  }
  return false;
}

// HtmlToCFHtml and CFHtmlToHtml are based on similar methods in
// WebCore/platform/win/ClipboardUtilitiesWin.cpp.
/*
 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

// Helper method for converting from text/html to MS CF_HTML.
// Documentation for the CF_HTML format is available at
// http://msdn.microsoft.com/en-us/library/aa767917(VS.85).aspx
std::string HtmlToCFHtml(std::string_view html, std::string_view base_url) {
  if (html.empty()) {
    return std::string();
  }

#define MAX_DIGITS 10
#define MAKE_NUMBER_FORMAT_1(digits) MAKE_NUMBER_FORMAT_2(digits)
#define MAKE_NUMBER_FORMAT_2(digits) "%0" #digits "zu"
#define NUMBER_FORMAT MAKE_NUMBER_FORMAT_1(MAX_DIGITS)

  static constexpr char kHeader[] =
      "Version:0.9\r\n"
      "StartHTML:" NUMBER_FORMAT
      "\r\n"
      "EndHTML:" NUMBER_FORMAT
      "\r\n"
      "StartFragment:" NUMBER_FORMAT
      "\r\n"
      "EndFragment:" NUMBER_FORMAT "\r\n";
  static const char kSourceUrlPrefix[] = "SourceURL:";
  static const char kStartMarkup[] = "<html>\r\n<body>\r\n";
  static const char kEndMarkup[] = "\r\n</body>\r\n</html>";
  static const char kStartFragment[] = "<!--StartFragment-->";
  static const char kEndFragment[] = "<!--EndFragment-->";

  // Windows apps expect HTML in the clipboard to be in the text format CF_HTML
  // so that they can figure out the length of the HTML document and extract
  // fragments of the content out if needed. `content_type` describes the
  // sanitization of the markup that will be converted to CF_HTML.

  // Given the following unsanitized HTML string
  // <html>
  //   <head> <style>p {color:blue}</style> </head>
  //   <body>
  //     <p>Hello World</p>
  //     <script> alert("Hello World!"); </script>
  //   </body>
  // </html>

  // Windows apps may extract the content from the headers to know where the
  // HTML or fragment starts. If we wrap the content by simply "sticking" the
  // headers (like we do with sanitized HTML), then it may result in double
  // tags.

  // Sticking the headers using the previous unsanitized HTML string (shortened
  // for brevity):
  // Version:0.9
  // StartHTML:0000000132
  // EndHTML:0000000637
  // ...
  // <html>
  // <body>
  //   <!--StartFragment-->
  //   <html>
  //     <head> <style>p {color:blue}</style> </head>
  //     <body> <p>...</p> <script>...</script> </body>
  //   </html>
  //   <!--EndFragment-->
  // </body>
  // </html>

  // Wrapping the unsanitized HTML string (shortened for brevity):
  // Version:0.9
  // StartHTML:0000000132
  // EndHTML:0000000274
  // ...
  // <!--StartFragment-->
  //   <html>
  //     <head> <style>p {color:blue}</style> </head>
  //     <body> <p>...</p> <script>...</script> </body>
  //   </html>
  // <!--EndFragment-->

  // The only way to write unsanitized HTML is by using the Async Clipboard API
  // write pipeline.

  // We don't want to regress the behavior of current DataTransfer APIs and
  // getData calls for apps that rely on markup with duplicate tags (e.g. Excel
  // Online expects this type of markup). As a result, if the HTML is sanitized,
  // we only "stick" the CF_HTML headers to the HTML string.
  std::string markup = kStartMarkup;
  base::StrAppend(&markup, {kStartFragment, html, kEndFragment});
  markup += kEndMarkup;

  // Calculate the offsets required for the HTML headers. This is used by Apps
  // on Windows to figure out the length of the HTML document and fragments.
  // Additionally, Apps can process specific parts of the HTML document. e.g.,
  // if they choose to process fragments of the HTML document, then they can use
  // the start and end fragments offsets to extract the content out.
  size_t headers_offset =
      strlen(kHeader) - strlen(NUMBER_FORMAT) * 4 + MAX_DIGITS * 4;
  if (!base_url.empty()) {
    headers_offset +=
        strlen(kSourceUrlPrefix) + base_url.length() + 2;  // Add 2 for \r\n.
  }

  size_t start_html_offset = headers_offset;
  size_t start_fragment_offset = headers_offset + strlen(kStartFragment);
  start_fragment_offset += strlen(kStartMarkup);
  size_t end_fragment_offset = start_fragment_offset + html.length();
  size_t end_html_offset = end_fragment_offset + strlen(kEndFragment);
  end_html_offset += strlen(kEndMarkup);

  std::string result =
      base::StringPrintf(kHeader, start_html_offset, end_html_offset,
                         start_fragment_offset, end_fragment_offset);
  if (!base_url.empty()) {
    base::StrAppend(&result, {kSourceUrlPrefix, base_url, "\r\n"});
  }
  result += markup;

#undef MAX_DIGITS
#undef MAKE_NUMBER_FORMAT_1
#undef MAKE_NUMBER_FORMAT_2
#undef NUMBER_FORMAT

  return result;
}

// Helper method for converting from MS CF_HTML to text/html.
void CFHtmlToHtml(std::string_view cf_html,
                  std::string* html,
                  std::string* base_url) {
  size_t fragment_start = std::string::npos;
  size_t fragment_end = std::string::npos;

  CFHtmlExtractMetadata(cf_html, base_url, nullptr, &fragment_start,
                        &fragment_end);

  if (html &&
      fragment_start != std::string::npos &&
      fragment_end != std::string::npos) {
    *html = cf_html.substr(fragment_start, fragment_end - fragment_start);
    base::TrimWhitespaceASCII(*html, base::TRIM_ALL, html);
  }
}

void CFHtmlExtractMetadata(std::string_view cf_html,
                           std::string* base_url,
                           size_t* html_start,
                           size_t* fragment_start,
                           size_t* fragment_end) {
  // Obtain base_url if present.
  if (base_url) {
    static constexpr char kSrcUrlStr[] = "SourceURL:";
    size_t line_start = cf_html.find(kSrcUrlStr);
    if (line_start != std::string::npos) {
      size_t src_end = cf_html.find("\n", line_start);
      size_t src_start = line_start + strlen(kSrcUrlStr);
      if (src_end != std::string::npos && src_start != std::string::npos) {
        *base_url = cf_html.substr(src_start, src_end - src_start);
        base::TrimWhitespaceASCII(*base_url, base::TRIM_ALL, base_url);
      }
    }
  }

  // Find the markup between "<!--StartFragment-->" and "<!--EndFragment-->".
  // If the comments cannot be found, like copying from OpenOffice Writer,
  // we simply fall back to using StartFragment/EndFragment bytecount values
  // to determine the fragment indexes.
  std::string cf_html_lower = base::ToLowerASCII(cf_html);
  size_t markup_start = cf_html_lower.find("<html", 0);
  if (html_start) {
    *html_start = markup_start;
  }
  size_t tag_start = cf_html.find("<!--StartFragment", markup_start);
  if (tag_start == std::string::npos) {
    static constexpr char kStartFragmentStr[] = "StartFragment:";
    size_t start_fragment_start = cf_html.find(kStartFragmentStr);
    if (start_fragment_start != std::string::npos) {
      *fragment_start = static_cast<size_t>(atoi(
          cf_html.data() + start_fragment_start + strlen(kStartFragmentStr)));
    }

    static constexpr char kEndFragmentStr[] = "EndFragment:";
    size_t end_fragment_start = cf_html.find(kEndFragmentStr);
    if (end_fragment_start != std::string::npos) {
      *fragment_end = static_cast<size_t>(
          atoi(cf_html.data() + end_fragment_start + strlen(kEndFragmentStr)));
    }
  } else {
    *fragment_start = cf_html.find('>', tag_start) + 1;
    size_t tag_end = cf_html.rfind("<!--EndFragment", std::string::npos);
    *fragment_end = cf_html.rfind('<', tag_end);
  }
}

}  // namespace clipboard_util

}  // namespace ui