// 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/clipboard/clipboard_util_win.h"

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

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

#include "base/compiler_specific.h"
#include "base/containers/to_vector.h"
#include "base/feature_list.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/numerics/checked_math.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 "ui/base/ui_base_features.h"
#include "url/gurl.h"

namespace ui {

namespace {

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

// Returns the locked HGLOBAL contents as UTF-16, bounded by size() and
// truncated at the first embedded NUL. Untrusted external sources may omit a
// NUL terminator, so size() is authoritative; reading via data() alone would
// scan past the allocation.
std::u16string HGlobalAsWideString(base::win::ScopedHGlobal<wchar_t*>& data) {
  if (!data.data()) {
    return std::u16string();
  }
  std::u16string out = base::WideToUTF16(
      std::wstring_view(data.data(), data.size() / sizeof(wchar_t)));
  if (size_t pos = out.find(u'\0'); pos != std::u16string::npos) {
    out.resize(pos);
  }
  return out;
}

// Returns the locked HGLOBAL contents as UTF-16, decoding the bytes as UTF-8
// and bounded by size(), truncated at the first embedded NUL. Untrusted
// external sources may omit a NUL terminator, so size() is authoritative;
// reading via data() alone would scan past the allocation.
std::u16string HGlobalAsString(base::win::ScopedHGlobal<char*>& data) {
  if (!data.data()) {
    return std::u16string();
  }
  std::string out(data.data(), data.size());
  if (size_t pos = out.find('\0'); pos != std::string::npos) {
    out.resize(pos);
  }
  return base::UTF8ToUTF16(out);
}

// Result type for virtual file extraction: pairs of (temp_file_path,
// display_name).
using VirtualFileResults =
    std::vector<std::pair<base::FilePath, base::FilePath>>;

// A surviving (non-directory) virtual file: its unique display name plus the
// original FILEGROUPDESCRIPTOR index, used as the CFSTR_FILECONTENTS lindex.
// Skipping directory descriptors makes the filtered position diverge from the
// descriptor index, so the index must be carried explicitly.
struct VirtualFileNameWithIndex {
  base::FilePath display_name;
  LONG content_index;
};

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);
  }
}

// Parses a BookmarkListType clipboard payload. Accepts a JSON array of
// objects with a required "url" string and an optional "title" string.
// On success, appends each entry to url_infos as {GURL(url), title}.
//
// Example JSON payload:
// [
//   { "url": "https://chromium.org", "title": "Chromium Project" },
//   { "url": "https://www.mozilla.org", "title": "Mozilla Home" }
// ]
void ParseBookmarkListData(const std::u16string& str,
                           std::vector<ClipboardUrlInfo>& url_infos) {
  const std::string utf8_payload = base::UTF16ToUTF8(str);

  std::optional<base::Value> json_payload =
      base::JSONReader::Read(utf8_payload, base::JSON_PARSE_RFC);

  if (json_payload && json_payload->is_list()) {
    for (const base::Value& item : json_payload->GetList()) {
      if (!item.is_dict()) {
        continue;
      }

      const auto& dict = item.GetDict();
      const std::string* url_str = dict.FindString("url");
      if (!url_str) {
        continue;
      }
      GURL url(*url_str);
      if (!url.is_valid()) {
        continue;
      }

      std::u16string title;
      if (const std::string* title_str = dict.FindString("title")) {
        title = base::UTF8ToUTF16(*title_str);
      }

      url_infos.emplace_back(std::move(url), std::move(title));
    }
  }
}

// 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 std::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;
}

VirtualFileResults WriteAllFileContentsToTempFiles(
    const std::vector<base::FilePath>& display_names,
    const std::vector<HGLOBAL>& memory_backed_contents) {
  DCHECK_EQ(display_names.size(), memory_backed_contents.size());

  VirtualFileResults 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]);
    // Ignore file if write failed.
    if (temp_path.empty()) {
      continue;
    }

    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);
        }

        if (base::FeatureList::IsEnabled(features::kVirtualFileChunkedRead)) {
          // Read in chunks and write to the destination stream
          constexpr ULONG kChunkSize = 16 * 1024 * 1024;  // 16 MB
          auto buffer = std::make_unique<char[]>(kChunkSize);
          ULONG bytes_read = 0;
          while (SUCCEEDED(hr = content.pstm->Read(buffer.get(), kChunkSize,
                                                   &bytes_read)) &&
                 bytes_read > 0) {
            ULONG bytes_written = 0;
            hr = stream->Write(buffer.get(), bytes_read, &bytes_written);
            if (FAILED(hr) || bytes_written != bytes_read) {
              hr = E_FAIL;
              break;
            }
          }
          if (hr == S_OK && bytes_read == 0) {
            LOG(WARNING) << "Source stream returned S_OK with zero bytes read.";
          }
        } else {
          // 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);
      UNSAFE_TODO(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;
}

// Extracts virtual file contents and writes them to temp files asynchronously
// on a worker thread.
VirtualFileResults ExtractVirtualFiles(
    Microsoft::WRL::ComPtr<IStream> marshaled_data_object_stream,
    const std::vector<VirtualFileNameWithIndex>& files) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  // Unmarshal the IDataObject from the stream.
  Microsoft::WRL::ComPtr<IDataObject> data_object;
  HRESULT hr = ::CoGetInterfaceAndReleaseStream(
      marshaled_data_object_stream.Get(), IID_PPV_ARGS(&data_object));
  // CoGetInterfaceAndReleaseStream already released the stream;
  // Detach() prevents ComPtr destructor from double-releasing.
  marshaled_data_object_stream.Detach();

  if (FAILED(hr) || !data_object) {
    LOG(WARNING) << "CoGetInterfaceAndReleaseStream failed: "
                 << (FAILED(hr) ? hr : E_UNEXPECTED);
    return {};
  }

  std::vector<base::FilePath> display_names;
  display_names.reserve(files.size());
  std::vector<HGLOBAL> memory_backed_contents;
  memory_backed_contents.reserve(files.size());
  for (const auto& file : files) {
    display_names.push_back(file.display_name);
    memory_backed_contents.push_back(
        CopyFileContentsToHGlobal(data_object.Get(), file.content_index));
  }

  return WriteAllFileContentsToTempFiles(display_names, memory_backed_contents);
}

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<VirtualFileNameWithIndex>> GetVirtualFilenames(
    IDataObject* data_object) {
  STGMEDIUM medium;

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

  std::vector<VirtualFileNameWithIndex> filenames;
  // Display names already added, used only for uniquification (the
  // case-insensitive de-dup must see the prior names, not the indices).
  std::vector<base::FilePath> unique_names;

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

    unsigned int num_files = descriptor->cItems;
    // We expect there to be at least one file in here.
    if (num_files < 1u) {
      return std::nullopt;
    }
    // We expect the medium to contain enough data for at least cItems file
    // group descriptor.
    const auto required_size =
        base::CheckMul(num_files, sizeof(decltype(descriptor->fgd[0])));
    const auto end_offset =
        required_size + offsetof(FileGroupDescriptorType, fgd);
    if (end_offset.IsInvalidOr([&descriptor](size_t result) {
          return descriptor.size() < result;
        })) {
      return std::nullopt;
    }

    // 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 ((UNSAFE_TODO(descriptor->fgd[i]).dwFlags & FD_ATTRIBUTES) &&
          (UNSAFE_TODO(descriptor->fgd[i]).dwFileAttributes &
           FILE_ATTRIBUTE_DIRECTORY)) {
        DLOG(WARNING) << "GetVirtualFilenames: display name '"
                      << ConvertString(
                             UNSAFE_TODO(descriptor->fgd[i]).cFileName)
                      << "' refers to a directory (not supported).";
        continue;
      }
      base::FilePath display_name = GetUniqueVirtualFilename(
          ConvertString(UNSAFE_TODO(descriptor->fgd[i]).cFileName),
          unique_names, &uniquifier);

      unique_names.push_back(display_name);
      filenames.push_back({display_name, static_cast<LONG>(i)});
    }
  }

  ReleaseStgMedium(&medium);
  return filenames;
}

// Tries the Unicode (W) descriptor first, then the ASCII (A) descriptor,
// preserving each surviving entry's original descriptor index.
std::optional<std::vector<VirtualFileNameWithIndex>>
GetVirtualFilenamesWithIndices(IDataObject* data_object) {
  std::optional<std::vector<VirtualFileNameWithIndex>> filenames =
      GetVirtualFilenames<FILEGROUPDESCRIPTORW>(data_object);
  if (filenames) {
    return filenames;
  }
  return GetVirtualFilenames<FILEGROUPDESCRIPTORA>(data_object);
}

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*> descriptor(
        medium.hGlobal);
    // We expect valid data in the file group descriptor.
    if (!descriptor.data()) {
      return false;
    }
    // We expect there to be at least one file in here.
    if (descriptor->cItems < 1u) {
      return false;
    }
    // We expect the medium to contain enough data for at least cItems file
    // group descriptor.

    if (base::CheckAdd(offsetof(FileGroupDescriptorType, fgd),
                       base::CheckMul(descriptor->cItems,
                                      sizeof(decltype(descriptor->fgd[0]))))
            .IsInvalidOr(
                [&](size_t result) { return descriptor.size() < result; })) {
      return false;
    }

    filename->assign(ConvertString(descriptor->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()) ||
         HasData(data_object, ClipboardFormatType::BookmarkListType()) ||
         (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());
}

// Some virtual file providers like Windows ZIP Shell Folder
// advertise these formats (CF_HDROP, CFSTR_FILENAME) in QueryGetData but fail
// on GetData, so it must be checked if those formats can provide data to
// correctly identify real files.
bool HasRealFiles(IDataObject* data_object) {
  DCHECK(data_object);
  STGMEDIUM medium;
  if (GetData(data_object, ClipboardFormatType::CFHDropType(), &medium) ||
      GetData(data_object, ClipboardFormatType::FilenameType(), &medium) ||
      GetData(data_object, ClipboardFormatType::FilenameAType(), &medium)) {
    ReleaseStgMedium(&medium);
    return true;
  }
  return false;
}

bool HasVirtualFilenames(IDataObject* data_object) {
  DCHECK(data_object);
  const bool has_real_files = base::FeatureList::IsEnabled(
                                  features::kUseClipboardStrictVirtualFileCheck)
                                  ? HasRealFiles(data_object)
                                  : HasFilenames(data_object);
  // Favor real files on the file system over virtual files.
  return !has_real_files &&
         HasData(data_object, ClipboardFormatType::FileContentAtIndexType(0)) &&
         (HasData(data_object, ClipboardFormatType::FileDescriptorType()) ||
          HasData(data_object, ClipboardFormatType::FileDescriptorAType()));
}

bool HasFileContents(IDataObject* data_object) {
  DCHECK(data_object);
  FORMATETC format_etc =
      ClipboardFormatType::FileContentAtIndexType(0).ToFormatEtc();
  format_etc.tymed = TYMED_HGLOBAL | TYMED_ISTREAM;
  return SUCCEEDED(data_object->QueryGetData(&format_etc)) &&
         (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 GetUrlInfos(IDataObject* data_object,
                 std::vector<ClipboardUrlInfo>& url_infos,
                 bool convert_filenames) {
  DCHECK(data_object);
  if (!HasUrl(data_object, convert_filenames))
    return false;

  // Try to extract a URL from |data_object| in a variety of formats.
  STGMEDIUM store;
  GURL url;
  std::u16string title;
  if (GetUrlFromHDrop(data_object, &url, &title)) {
    url_infos.emplace_back(url, title);
    return true;
  }

  // Check for the BookmarkListType clipboard format, which contains a JSON
  // array of URLs and titles.
  if (GetData(data_object, ClipboardFormatType::BookmarkListType(), &store)) {
    {
      base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
      ParseBookmarkListData(HGlobalAsWideString(data), url_infos);
    }
    ReleaseStgMedium(&store);
    return !url_infos.empty();
  }

  // Check for single URL formats, including CFSTR_INETURLW,
  // text/x-moz-url(a bookmark), or a text/uri-list that happens to
  // contain only one URL.
  if (GetData(data_object, ClipboardFormatType::MozUrlType(), &store) ||
      GetData(data_object, ClipboardFormatType::UrlType(), &store)) {
    {
      base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
      SplitUrlAndTitle(HGlobalAsWideString(data), &url, &title);
      url_infos.emplace_back(url, title);
    }
    ReleaseStgMedium(&store);
    return url_infos[0].url.is_valid();
  }

  // Check for single URL format(CFSTR_INETURLA),
  if (GetData(data_object, ClipboardFormatType::UrlAType(), &store)) {
    {
      base::win::ScopedHGlobal<char*> data(store.hGlobal);
      SplitUrlAndTitle(HGlobalAsString(data), &url, &title);
      url_infos.emplace_back(url, title);
    }
    ReleaseStgMedium(&store);
    return url_infos[0].url.is_valid();
  }

  if (convert_filenames) {
    std::vector<std::wstring> filenames;
    if (!GetFilenames(data_object, &filenames))
      return false;
    DCHECK_GT(filenames.size(), 0U);
    GURL file_url = net::FilePathToFileURL(base::FilePath(filenames[0]));
    if (file_url.is_valid()) {
      url_infos.emplace_back(file_url, u"");
    }
    return !url_infos.empty();
  }

  return false;
}

std::vector<std::wstring> GetFilenames(HDROP hdrop) {
  std::vector<std::wstring> filenames;
  if (!hdrop) {
    return filenames;
  }

  const unsigned num_files = DragQueryFileW(hdrop, 0xffffffff, 0, 0);
  for (unsigned int i = 0; i < num_files; ++i) {
    const UINT required_len = DragQueryFileW(hdrop, i, nullptr, 0);
    if (!required_len) {
      continue;
    }
    const UINT buffer_size = required_len + 1;
    std::wstring filename;
    if (!DragQueryFileW(hdrop, i, base::WriteInto(&filename, buffer_size),
                        buffer_size)) {
      continue;
    }
    filenames.push_back(std::move(filename));
  }
  return filenames;
}

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);
      *filenames = GetFilenames(hdrop.data());
    }
    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]) {
        std::wstring filename(data.data(), data.size() / sizeof(wchar_t));
        filename.resize(std::min(filename.size(), filename.find(L'\0')));
        filenames->push_back(std::move(filename));
      }
    }
    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]) {
        std::string mb(data.data(), data.size());
        mb.resize(std::min(mb.size(), mb.find('\0')));
        filenames->push_back(base::SysNativeMBToWide(mb));
      }
    }
    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*>(UNSAFE_TODO(
      reinterpret_cast<BYTE*>(drop_files) + kDropFilesHeaderSizeInBytes));

  size_t next_filename_offset = 0;
  for (const auto& filename : filenames) {
    UNSAFE_TODO(
        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.
  std::optional<std::vector<VirtualFileNameWithIndex>> filenames_with_indices =
      GetVirtualFilenamesWithIndices(data_object);
  if (!filenames_with_indices) {
    return std::nullopt;
  }

  std::vector<base::FilePath> filenames;
  filenames.reserve(filenames_with_indices->size());
  for (const auto& entry : *filenames_with_indices) {
    filenames.push_back(entry.display_name);
  }
  return filenames;
}

// Checks if the data object supports async operations via
// IDataObjectAsyncCapability.
Microsoft::WRL::ComPtr<IDataObjectAsyncCapability>
GetAsyncCapabilityIfSupported(IDataObject* data_object) {
  if (!base::FeatureList::IsEnabled(features::kAsyncVirtualFileExtraction)) {
    return nullptr;
  }

  Microsoft::WRL::ComPtr<IDataObjectAsyncCapability> async_capability;
  HRESULT hr = data_object->QueryInterface(IID_PPV_ARGS(&async_capability));
  if (FAILED(hr)) {
    return nullptr;
  }

  BOOL supports_async = FALSE;
  hr = async_capability->GetAsyncMode(&supports_async);
  if (FAILED(hr) || !supports_async) {
    return nullptr;
  }

  return async_capability;
}

// Marshals the IDataObject to a stream for cross-thread COM access.
Microsoft::WRL::ComPtr<IStream> MarshalDataObjectToStream(
    IDataObject* data_object) {
  Microsoft::WRL::ComPtr<IStream> marshaled_stream;
  HRESULT hr = ::CoMarshalInterThreadInterfaceInStream(
      IID_IDataObject, data_object, &marshaled_stream);
  if (FAILED(hr) || !marshaled_stream) {
    LOG(WARNING) << "CoMarshalInterThreadInterfaceInStream failed: " << hr;
    return nullptr;
  }
  return marshaled_stream;
}

// Posts the virtual file extraction work to a background thread.
void PostVirtualFileExtractionTask(
    Microsoft::WRL::ComPtr<IStream> marshaled_stream,
    Microsoft::WRL::ComPtr<IDataObjectAsyncCapability> async_capability,
    const std::vector<VirtualFileNameWithIndex>& files,
    base::OnceCallback<void(const VirtualFileResults&)> callback) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING},
      base::BindOnce(&ExtractVirtualFiles, marshaled_stream, files),
      base::BindOnce(
          [](Microsoft::WRL::ComPtr<IDataObjectAsyncCapability> async_cap,
             base::OnceCallback<void(const VirtualFileResults&)> cb,
             const VirtualFileResults& result) {
            if (async_cap) {
              HRESULT op_result = result.empty() ? E_FAIL : S_OK;
              DWORD effect = result.empty() ? DROPEFFECT_NONE : DROPEFFECT_COPY;
              async_cap->EndOperation(op_result, nullptr, effect);
            }
            std::move(cb).Run(result);
          },
          async_capability, std::move(callback)));
}

void GetVirtualFilesAsTempFiles(
    IDataObject* data_object,
    base::OnceCallback<void(const VirtualFileResults&)> callback) {
  // Favor real files on the file system over virtual files; bail out if this
  // data object should not surface virtual files.
  if (!HasVirtualFilenames(data_object)) {
    std::move(callback).Run({});
    return;
  }

  // Retrieve the display names of the virtual files along with their original
  // descriptor indices.
  std::optional<std::vector<VirtualFileNameWithIndex>> files =
      GetVirtualFilenamesWithIndices(data_object);
  if (!files) {
    std::move(callback).Run({});
    return;
  }

  // Try async extraction if supported.
  if (auto async_capability = GetAsyncCapabilityIfSupported(data_object)) {
    async_capability->StartOperation(nullptr);

    if (auto marshaled_stream = MarshalDataObjectToStream(data_object)) {
      PostVirtualFileExtractionTask(std::move(marshaled_stream),
                                    std::move(async_capability), files.value(),
                                    std::move(callback));
      return;
    }

    // Marshal failed, end the async operation.
    async_capability->EndOperation(E_FAIL, nullptr, DROPEFFECT_NONE);
  }

  // Fallback: async not supported or marshal failed. Copy file contents to
  // global memory on the UI thread, using each entry's original descriptor
  // index as the CFSTR_FILECONTENTS lindex.
  std::vector<base::FilePath> display_names;
  display_names.reserve(files->size());
  std::vector<HGLOBAL> memory_backed_contents;
  memory_backed_contents.reserve(files->size());
  for (const auto& file : *files) {
    display_names.push_back(file.display_name);
    memory_backed_contents.push_back(
        CopyFileContentsToHGlobal(data_object, file.content_index));
  }

  // Write the temp files on a worker thread.
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING},
      base::BindOnce(&WriteAllFileContentsToTempFiles, display_names,
                     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(HGlobalAsWideString(data));
    }
    ReleaseStgMedium(&store);
    return true;
  }

  if (GetData(data_object, ClipboardFormatType::PlainTextAType(), &store)) {
    {
      // ASCII text
      base::win::ScopedHGlobal<char*> data(store.hGlobal);
      plain_text->assign(HGlobalAsString(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.
  std::vector<ClipboardUrlInfo> url_infos;
  if (GetUrlInfos(data_object, url_infos, false)) {
    *plain_text = base::UTF8ToUTF16(url_infos.front().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(HGlobalAsWideString(data));
  }
  ReleaseStgMedium(&store);
  return true;
}

bool ReadStreamToVector(IStream* stream, std::vector<uint8_t>* out) {
  DCHECK(stream);
  DCHECK(out);
  STATSTG statstg;
  if (FAILED(stream->Stat(&statstg, STATFLAG_NONAME)) ||
      statstg.cbSize.QuadPart == 0) {
    return false;
  }
  const size_t total_size = static_cast<size_t>(statstg.cbSize.QuadPart);
  out->resize(total_size);
  const LARGE_INTEGER zero = {};
  stream->Seek(zero, STREAM_SEEK_SET, nullptr);
  // Loop to handle partial reads.
  size_t bytes_remaining = total_size;
  uint8_t* ptr = out->data();
  while (bytes_remaining > 0) {
    ULONG bytes_read = 0;
    HRESULT hr = stream->Read(ptr, base::checked_cast<ULONG>(bytes_remaining),
                              &bytes_read);
    if (FAILED(hr) || bytes_read == 0) {
      out->clear();
      return false;
    }
    UNSAFE_TODO(ptr += bytes_read);
    bytes_remaining -= bytes_read;
  }
  return true;
}

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

  STGMEDIUM content;

  FORMATETC format_etc =
      ClipboardFormatType::FileContentAtIndexType(0).ToFormatEtc();
  // Request only TYMED_HGLOBAL and TYMED_ISTREAM.
  // TYMED_ISTORAGE (e.g. .msg files dragged from Outlook) is excluded here;
  // it is currently handled via CopyFileContentsToHGlobal() in the
  // GetVirtualFilesAsTempFiles() path, which converts IStorage to HGLOBAL
  // and writes the result to a temp file.
  // TODO(crbug.com/41452260): Add native TYMED_ISTORAGE support on the drop
  // target side to read IStorage data directly into memory and avoid temp
  // file creation.
  format_etc.tymed = TYMED_HGLOBAL | TYMED_ISTREAM;
  // The call to GetData can be very slow depending on what is in |data_object|.
  if (SUCCEEDED(data_object->GetData(&format_etc, &content))) {
    if (TYMED_HGLOBAL == content.tymed) {
      base::win::ScopedHGlobal<uint8_t*> data(content.hGlobal);
      *file_contents = base::ToVector(data);
    } else if (TYMED_ISTREAM == content.tymed) {
      if (!ReadStreamToVector(content.pstm, file_contents)) {
        file_contents->clear();
      }
    }
    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(UNSAFE_TODO(
          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(UNSAFE_TODO(
          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
