/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsClipboard.h"

#include <shlobj.h>
#include <intshcut.h>

// shellapi.h is needed to build with WIN32_LEAN_AND_MEAN
#include <shellapi.h>

#include <thread>

#ifdef ACCESSIBILITY
#  include "mozilla/a11y/Compatibility.h"
#endif
#include "mozilla/CheckedInt.h"
#include "mozilla/Logging.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/StaticPrefs_clipboard.h"
#include "mozilla/StaticPrefs_widget.h"
#include "mozilla/WindowsVersion.h"
#include "mozilla/widget/WebCustomFormatUtils.h"
#include "SpecialSystemDirectory.h"

#include "nsArrayUtils.h"
#include "nsCOMPtr.h"
#include "nsComponentManagerUtils.h"
#include "nsDataObj.h"
#include "nsISupportsPrimitives.h"
#include "nsString.h"
#include "nsNativeCharsetUtils.h"
#include "nsIInputStream.h"
#include "nsITransferable.h"
#include "nsXPCOM.h"
#include "nsReadableUtils.h"
#include "nsUnicharUtils.h"
#include "nsPrimitiveHelpers.h"
#include "nsIWidget.h"
#include "nsWidgetsCID.h"
#include "nsCRT.h"
#include "nsNetUtil.h"
#include "nsIFileProtocolHandler.h"
#include "nsEscape.h"
#include "nsIObserverService.h"
#include "nsMimeTypes.h"
#include "imgITools.h"
#include "imgIContainer.h"
#include "WinOLELock.h"
#include "WinUtils.h"

/* static */
UINT nsClipboard::GetClipboardFileDescriptorFormatA() {
  static UINT format = ::RegisterClipboardFormatW(CFSTR_FILEDESCRIPTORA);
  MOZ_ASSERT(format);
  return format;
}

/* static */
UINT nsClipboard::GetClipboardFileDescriptorFormatW() {
  static UINT format = ::RegisterClipboardFormatW(CFSTR_FILEDESCRIPTORW);
  MOZ_ASSERT(format);
  return format;
}

/* static */
UINT nsClipboard::GetHtmlClipboardFormat() {
  static UINT format = ::RegisterClipboardFormatW(L"HTML Format");
  return format;
}

/* static */
UINT nsClipboard::GetCustomClipboardFormat() {
  static UINT format =
      ::RegisterClipboardFormatW(L"application/x-moz-custom-clipdata");
  return format;
}

/* static */
UINT nsClipboard::GetWebCustomFormatMapClipboardFormat() {
  static UINT format = ::RegisterClipboardFormatW(L"Web Custom Format Map");
  MOZ_ASSERT(format);
  return format;
}

static inline nsresult CheckClipboardByteSize(HGLOBAL aHGlobal,
                                              uint64_t aByteThreshold) {
  // GlobalSize returns the size of the heap allocation backing aHGlobal,
  // which may be slightly larger than the actual clipboard data.
  if (::GlobalSize(aHGlobal) > aByteThreshold) {
    return NS_ERROR_CLIPBOARD_TOO_BIG;
  }

  return NS_OK;
}

// Reads the "Web Custom Format Map" clipboard format and decodes its JSON
// into the supplied map. Fetches via aDataObject (OLE) when provided; falls
// back to the legacy Windows clipboard API rooted at aWindow when aDataObject
// is null, matching the same primary/fallback path other flavors use in
// GetDataFromDataObject(). Returns false if the format is missing or the
// payload fails to parse.
static bool GetWebCustomFormatMapFromClipboard(
    IDataObject* aDataObject, nsIWidget* aWindow,
    mozilla::widget::WebCustomFormatMap& aMap) {
  const UINT format = nsClipboard::GetWebCustomFormatMapClipboardFormat();
  void* data = nullptr;
  uint32_t dataLen = 0;
  nsresult rv = NS_ERROR_FAILURE;
  if (aDataObject) {
    rv = nsClipboard::GetNativeDataOffClipboard(
        aDataObject, 0, format, /* aMIMEFlavor */ nullptr, &data, &dataLen);
  } else if (aWindow) {
    rv = nsClipboard::GetNativeDataOffClipboard(aWindow, 0, format, &data,
                                                &dataLen);
  }
  if (NS_FAILED(rv) || !data) {
    return false;
  }
  bool ok = mozilla::widget::JSONToWebCustomFormatMap(
      nsDependentCSubstring(static_cast<const char*>(data), dataLen), aMap);
  free(data);
  return ok;
}

//-------------------------------------------------------------------------
//
// nsClipboard constructor
//
//-------------------------------------------------------------------------
nsClipboard::nsClipboard()
    : nsBaseClipboard(mozilla::dom::ClipboardCapabilities(
          false /* supportsSelectionClipboard */,
          false /* supportsFindClipboard */,
          false /* supportsSelectionCache */)) {
  mWindow = nullptr;

  // Register for a shutdown notification so that we can flush data
  // to the OS clipboard.
  nsCOMPtr<nsIObserverService> observerService =
      do_GetService("@mozilla.org/observer-service;1");
  if (observerService) {
    observerService->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID,
                                 false);
  }
}

//-------------------------------------------------------------------------
// nsClipboard destructor
//-------------------------------------------------------------------------
nsClipboard::~nsClipboard() {}

NS_IMPL_ISUPPORTS_INHERITED(nsClipboard, nsBaseClipboard, nsIObserver)

NS_IMETHODIMP
nsClipboard::Observe(nsISupports* aSubject, const char* aTopic,
                     const char16_t* aData) {
  // This will be called on shutdown.
  ::OleFlushClipboard();
  ::CloseClipboard();

  return NS_OK;
}

//-------------------------------------------------------------------------
UINT nsClipboard::GetFormat(const char* aMimeStr, bool aMapHTMLMime) {
  UINT format;

  if (strcmp(aMimeStr, kTextMime) == 0) {
    format = CF_UNICODETEXT;
  } else if (strcmp(aMimeStr, kRTFMime) == 0) {
    format = ::RegisterClipboardFormat(L"Rich Text Format");
  } else if (strcmp(aMimeStr, kJPEGImageMime) == 0 ||
             strcmp(aMimeStr, kJPGImageMime) == 0) {
    format = CF_DIBV5;
  } else if (strcmp(aMimeStr, kPNGImageMime) == 0) {
    format = ::RegisterClipboardFormat(TEXT("PNG"));
  } else if (strcmp(aMimeStr, kFileMime) == 0 ||
             strcmp(aMimeStr, kFilePromiseMime) == 0) {
    format = CF_HDROP;
  } else if ((strcmp(aMimeStr, kNativeHTMLMime) == 0) ||
             (aMapHTMLMime && strcmp(aMimeStr, kHTMLMime) == 0)) {
    format = GetHtmlClipboardFormat();
  } else if (strcmp(aMimeStr, kCustomTypesMime) == 0) {
    format = GetCustomClipboardFormat();
  } else if (strcmp(aMimeStr, kWebCustomFormatMapType) == 0) {
    format = GetWebCustomFormatMapClipboardFormat();
  } else {
    format = ::RegisterClipboardFormatW(NS_ConvertASCIItoUTF16(aMimeStr).get());
  }

  return format;
}

mozilla::Maybe<UINT> nsClipboard::GetSecondaryFormat(const char* aMimeStr) {
  if (strcmp(aMimeStr, kPNGImageMime) == 0) {
    // Fall back to DIBV5 format
    return mozilla::Some(CF_DIBV5);
  }
  return mozilla::Nothing();
}

template <typename GroupDesc>
/* static */
bool nsClipboard::FileGroupDescriptorHasItems(HGLOBAL aHGlobal,
                                              uint64_t aItemCount) {
  if (!aHGlobal) {
    return false;
  }
  using FileDesc = std::remove_extent_t<decltype(GroupDesc::fgd)>;
  mozilla::CheckedInt<size_t> requiredSize(offsetof(GroupDesc, fgd));
  requiredSize += mozilla::CheckedInt<size_t>(aItemCount) * sizeof(FileDesc);
  return requiredSize.isValid() &&
         ::GlobalSize(aHGlobal) >= requiredSize.value();
}

template bool nsClipboard::FileGroupDescriptorHasItems<FILEGROUPDESCRIPTORW>(
    HGLOBAL, uint64_t);
template bool nsClipboard::FileGroupDescriptorHasItems<FILEGROUPDESCRIPTORA>(
    HGLOBAL, uint64_t);

//-------------------------------------------------------------------------
// static
nsresult nsClipboard::CreateNativeDataObject(
    nsITransferable* aTransferable, IDataObject** aDataObj, nsIURI* aUri,
    MightNeedToFlush* aMightNeedToFlush) {
  MOZ_ASSERT(aTransferable);
  if (!aTransferable) {
    return NS_ERROR_FAILURE;
  }

  // Create our native DataObject that implements the OLE IDataObject interface
  auto dataObj = mozilla::MakeRefPtr<nsDataObj>(aUri);

  // Now set it up with all the right data flavors & enums
  nsresult res =
      SetupNativeDataObject(aTransferable, dataObj, aMightNeedToFlush);
  if (NS_SUCCEEDED(res)) {
    dataObj.forget(aDataObj);
  }
  return res;
}

static nsresult StoreValueInDataObject(nsDataObj* aObj,
                                       LPCWSTR aClipboardFormat, DWORD value) {
  ScopedOLEMemory<DWORD> hGlobalMemory;
  if (!hGlobalMemory) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  *hGlobalMemory.lock() = value;

  STGMEDIUM stg;
  stg.tymed = TYMED_HGLOBAL;
  stg.pUnkForRelease = nullptr;
  stg.hGlobal = hGlobalMemory.forget();

  FORMATETC fe;
  SET_FORMATETC(fe, ::RegisterClipboardFormat(aClipboardFormat), 0,
                DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
  aObj->SetData(&fe, &stg, TRUE);

  return NS_OK;
}

//-------------------------------------------------------------------------
nsresult nsClipboard::SetupNativeDataObject(
    nsITransferable* aTransferable, IDataObject* aDataObj,
    MightNeedToFlush* aMightNeedToFlush) {
  MOZ_ASSERT(aTransferable);
  MOZ_ASSERT(aDataObj);
  if (!aTransferable || !aDataObj) {
    return NS_ERROR_FAILURE;
  }

  auto* dObj = static_cast<nsDataObj*>(aDataObj);
  if (aMightNeedToFlush) {
    *aMightNeedToFlush = MightNeedToFlush::No;
  }

  // Now give the Transferable to the DataObject
  // for getting the data out of it
  dObj->SetTransferable(aTransferable);

  // Get the transferable list of data flavors
  nsTArray<nsCString> flavors;
  aTransferable->FlavorsTransferableCanExport(flavors);

  // Track web custom format flavors so we can publish a single JSON map
  // alongside the per-clipboard-format data. The map essence (the part after
  // "web ") maps to the clipboard format name we registered under
  // "Web Custom FormatN".
  mozilla::widget::WebCustomFormatMap webCustomFormatMap;
  uint32_t webCustomFormatIndex = 0;

  // Walk through flavors that contain data and register them
  // into the DataObj as supported flavors
  for (uint32_t i = 0; i < flavors.Length(); i++) {
    nsCString& flavorStr = flavors[i];

    UINT format;
    if (StringBeginsWith(flavorStr, nsLiteralCString(kWebCustomFormatPrefix))) {
      if (!nsBaseClipboard::IsValidFlavor(flavorStr)) {
        continue;
      }
      // Each "web foo/bar" gets its own per-slot clipboard format published
      // under the "Web Custom FormatN" name; the essence -> slot mapping is
      // recorded so the map JSON below can advertise it.
      nsAutoCString clipboardFormatName;
      clipboardFormatName.AppendLiteral("Web Custom Format");
      clipboardFormatName.AppendInt(webCustomFormatIndex);
      format = GetFormat(clipboardFormatName.get(), false);
      nsDependentCSubstring essence(
          Substring(flavorStr, strlen(kWebCustomFormatPrefix)));
      webCustomFormatMap.InsertOrUpdate(essence, clipboardFormatName);
      webCustomFormatIndex++;
    } else {
      // When putting data onto the clipboard, we want to maintain kHTMLMime
      // ("text/html") and not map it to CF_HTML here since this will be done
      // below.
      format = GetFormat(flavorStr.get(), false);
    }

    // Now tell the native IDataObject about both our mime type and
    // the native data format
    FORMATETC fe;
    SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
    dObj->AddDataFlavor(flavorStr.get(), &fe);

    // Do various things internal to the implementation, like map one
    // flavor to another or add additional flavors based on what's required
    // for the win32 impl.
    if (flavorStr.EqualsLiteral(kTextMime)) {
      // if we find text/plain, also add CF_TEXT, but we can add it for
      // text/plain as well.
      FORMATETC textFE;
      SET_FORMATETC(textFE, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
      dObj->AddDataFlavor(kTextMime, &textFE);
      if (aMightNeedToFlush) {
        *aMightNeedToFlush = MightNeedToFlush::Yes;
      }
    } else if (flavorStr.EqualsLiteral(kHTMLMime)) {
      // if we find text/html, also advertise win32's html flavor (which we will
      // convert on our own in nsDataObj::GetText().
      FORMATETC htmlFE;
      SET_FORMATETC(htmlFE, GetHtmlClipboardFormat(), 0, DVASPECT_CONTENT, -1,
                    TYMED_HGLOBAL);
      dObj->AddDataFlavor(kHTMLMime, &htmlFE);
    } else if (flavorStr.EqualsLiteral(kURLMime)) {
      // if we're a url, in addition to also being text, we need to register
      // the "file" flavors so that the win32 shell knows to create an internet
      // shortcut when it sees one of these beasts.
      FORMATETC shortcutFE;
      SET_FORMATETC(shortcutFE,
                    ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA), 0,
                    DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
      dObj->AddDataFlavor(kURLMime, &shortcutFE);
      SET_FORMATETC(shortcutFE,
                    ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW), 0,
                    DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
      dObj->AddDataFlavor(kURLMime, &shortcutFE);
      SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_FILECONTENTS),
                    0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
      dObj->AddDataFlavor(kURLMime, &shortcutFE);
      SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_INETURLA), 0,
                    DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
      dObj->AddDataFlavor(kURLMime, &shortcutFE);
      SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_INETURLW), 0,
                    DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
      dObj->AddDataFlavor(kURLMime, &shortcutFE);
    } else if (flavorStr.EqualsLiteral(kPNGImageMime) ||
               flavorStr.EqualsLiteral(kJPEGImageMime) ||
               flavorStr.EqualsLiteral(kJPGImageMime) ||
               flavorStr.EqualsLiteral(kGIFImageMime) ||
               flavorStr.EqualsLiteral(kNativeImageMime)) {
      // if we're an image, register the relevant bitmap flavors
      FORMATETC imageFE;

      // Add PNG, depending on prefs
      if (mozilla::StaticPrefs::clipboard_copy_image_as_png()) {
        static const CLIPFORMAT CF_PNG = ::RegisterClipboardFormat(TEXT("PNG"));
        SET_FORMATETC(imageFE, CF_PNG, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
        dObj->AddDataFlavor(flavorStr.get(), &imageFE);
      }

      // Add DIBv5
      SET_FORMATETC(imageFE, CF_DIBV5, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
      dObj->AddDataFlavor(flavorStr.get(), &imageFE);

      // Add DIBv3
      SET_FORMATETC(imageFE, CF_DIB, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
      dObj->AddDataFlavor(flavorStr.get(), &imageFE);
    } else if (flavorStr.EqualsLiteral(kFilePromiseMime)) {
      // if we're a file promise flavor, also register the
      // CFSTR_PREFERREDDROPEFFECT format.  The data object
      // returns a value of DROPEFFECTS_MOVE to the drop target
      // when it asks for the value of this format.  This causes
      // the file to be moved from the temporary location instead
      // of being copied.  The right thing to do here is to call
      // SetData() on the data object and set the value of this format
      // to DROPEFFECTS_MOVE on this particular data object.  But,
      // since all the other clipboard formats follow the model of setting
      // data on the data object only when the drop object calls GetData(),
      // I am leaving this format's value hard coded in the data object.
      // We can change this if other consumers of this format get added to this
      // codebase and they need different values.
      FORMATETC shortcutFE;
      SET_FORMATETC(shortcutFE,
                    ::RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT), 0,
                    DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
      dObj->AddDataFlavor(kFilePromiseMime, &shortcutFE);
    }
  }

  if (!webCustomFormatMap.IsEmpty()) {
    // Publish the web-custom-format map alongside the per-slot payloads.
    // The JSON is held on the data object so nsDataObj::GetText can serve
    // it without consulting the transferable.
    nsAutoCString mapJson;
    mozilla::widget::WebCustomFormatMapToJSON(webCustomFormatMap, mapJson);
    dObj->SetWebCustomFormatMapJson(mapJson);
    FORMATETC mapFE;
    SET_FORMATETC(mapFE, GetFormat(kWebCustomFormatMapType, false), 0,
                  DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
    dObj->AddDataFlavor(kWebCustomFormatMapType, &mapFE);
  }

  if (!mozilla::StaticPrefs::
          clipboard_copyPrivateDataToClipboardCloudOrHistory()) {
    // Let Clipboard know that data is sensitive and must not be copied to
    // the Cloud Clipboard, Clipboard History and similar.
    // https://docs.microsoft.com/en-us/windows/win32/dataxchg/clipboard-formats#cloud-clipboard-and-clipboard-history-formats
    if (aTransferable->GetIsPrivateData()) {
      nsresult rv =
          StoreValueInDataObject(dObj, TEXT("CanUploadToCloudClipboard"), 0);
      NS_ENSURE_SUCCESS(rv, rv);
      rv =
          StoreValueInDataObject(dObj, TEXT("CanIncludeInClipboardHistory"), 0);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = StoreValueInDataObject(
          dObj, TEXT("ExcludeClipboardContentFromMonitorProcessing"), 0);
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }

  return NS_OK;
}

// See methods listed at
// <https://docs.microsoft.com/en-us/windows/win32/api/objidl/nn-objidl-idataobject#methods>.
static void IDataObjectMethodResultToString(const HRESULT aHres,
                                            nsACString& aResult) {
  switch (aHres) {
    case E_INVALIDARG:
      aResult = "E_INVALIDARG";
      break;
    case E_UNEXPECTED:
      aResult = "E_UNEXPECTED";
      break;
    case E_OUTOFMEMORY:
      aResult = "E_OUTOFMEMORY";
      break;
    case DV_E_LINDEX:
      aResult = "DV_E_LINDEX";
      break;
    case DV_E_FORMATETC:
      aResult = "DV_E_FORMATETC";
      break;
    case DV_E_TYMED:
      aResult = "DV_E_TYMED";
      break;
    case DV_E_DVASPECT:
      aResult = "DV_E_DVASPECT";
      break;
    case OLE_E_NOTRUNNING:
      aResult = "OLE_E_NOTRUNNING";
      break;
    case STG_E_MEDIUMFULL:
      aResult = "STG_E_MEDIUMFULL";
      break;
    case DV_E_CLIPFORMAT:
      aResult = "DV_E_CLIPFORMAT";
      break;
    case S_OK:
      aResult = "S_OK";
      break;
    default:
      // Explicit template instantiaton, because otherwise the call is
      // ambiguous.
      constexpr int kRadix = 16;
      aResult = IntToCString<int32_t>(aHres, kRadix);
      break;
  }
}

// See
// <https://docs.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-olegetclipboard>.
static void OleGetClipboardResultToString(const HRESULT aHres,
                                          nsACString& aResult) {
  switch (aHres) {
    case S_OK:
      aResult = "S_OK";
      break;
    case CLIPBRD_E_CANT_OPEN:
      aResult = "CLIPBRD_E_CANT_OPEN";
      break;
    case CLIPBRD_E_CANT_CLOSE:
      aResult = "CLIPBRD_E_CANT_CLOSE";
      break;
    default:
      // Explicit template instantiaton, because otherwise the call is
      // ambiguous.
      constexpr int kRadix = 16;
      aResult = IntToCString<int32_t>(aHres, kRadix);
      break;
  }
}

static void MaybeLogClipboardCurrentOwner(
    const HRESULT aHres, const mozilla::StaticString& aMethodName) {
  if (!MOZ_CLIPBOARD_LOG_ENABLED()) {
    return;
  }

  if (aHres != CLIPBRD_E_CANT_OPEN) {
    return;
  }
  auto hwnd = ::GetOpenClipboardWindow();
  if (!hwnd) {
    MOZ_CLIPBOARD_LOG(
        "IDataObject::%s | Clipboard already opened by unknown process",
        aMethodName.get());
    return;
  }
  DWORD procId;
  DWORD threadId = ::GetWindowThreadProcessId(hwnd, &procId);
  NS_ENSURE_TRUE_VOID(threadId);
  nsAutoString procName;
  NS_ENSURE_SUCCESS_VOID(
      mozilla::widget::WinUtils::GetProcessImageName(procId, procName));
  MOZ_CLIPBOARD_LOG(
      "IDataObject::%s | Clipboard already opened by HWND: %p | "
      "Process ID: %lu | Thread ID: %lu | App name: %s",
      aMethodName.get(), hwnd, procId, threadId,
      NS_ConvertUTF16toUTF8(procName).get());
}

// See
// <https://docs.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-olegetclipboard>.
static void LogOleGetClipboardResult(const HRESULT aHres) {
  if (MOZ_CLIPBOARD_LOG_ENABLED()) {
    nsAutoCString hresString;
    OleGetClipboardResultToString(aHres, hresString);
    MOZ_CLIPBOARD_LOG("OleGetClipboard result: %s", hresString.get());
    MaybeLogClipboardCurrentOwner(aHres, "OleGetClipboard");
  }
}

// See
// <https://docs.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-olesetclipboard>.
static void OleSetClipboardResultToString(HRESULT aHres, nsACString& aResult) {
  switch (aHres) {
    case S_OK:
      aResult = "S_OK";
      break;
    case CLIPBRD_E_CANT_OPEN:
      aResult = "CLIPBRD_E_CANT_OPEN";
      break;
    case CLIPBRD_E_CANT_EMPTY:
      aResult = "CLIPBRD_E_CANT_EMPTY";
      break;
    case CLIPBRD_E_CANT_CLOSE:
      aResult = "CLIPBRD_E_CANT_CLOSE";
      break;
    case CLIPBRD_E_CANT_SET:
      aResult = "CLIPBRD_E_CANT_SET";
      break;
    default:
      // Explicit template instantiaton, because otherwise the call is
      // ambiguous.
      constexpr int kRadix = 16;
      aResult = IntToCString<int32_t>(aHres, kRadix);
      break;
  }
}

// See
// <https://docs.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-olesetclipboard>.
static void LogOleSetClipboardResult(const HRESULT aHres) {
  if (MOZ_CLIPBOARD_LOG_ENABLED()) {
    nsAutoCString hresString;
    OleSetClipboardResultToString(aHres, hresString);
    MOZ_CLIPBOARD_LOG("OleSetClipboard result: %s", hresString.get());
    MaybeLogClipboardCurrentOwner(aHres, "OleSetClipboard");
  }
}

template <typename Function, typename LogFunction, typename... Args>
static HRESULT RepeatedlyTry(Function aFunction, LogFunction aLogFunction,
                             Args... aArgs) {
  // These are magic values based on local testing. They are chosen not higher
  // to avoid jank (<https://developer.mozilla.org/en-US/docs/Glossary/Jank>).
  // When changing them, be careful.
  static constexpr int kNumberOfTries = 3;
  static constexpr int kDelayInMs = 3;

  HRESULT hres;
  for (int i = 0; i < kNumberOfTries; ++i) {
    hres = aFunction(aArgs...);
    aLogFunction(hres);

    if (hres == S_OK) {
      break;
    }

    // TODO: This was formerly std::sleep_for, which wasn't actually sleeping
    // in tests (bug 1927664).
    ::SleepEx(kDelayInMs, TRUE);
  }

  return hres;
}

// Other apps can block access to the clipboard. This repeatedly
// calls `::OleSetClipboard` for a fixed number of times and should be called
// instead of `::OleSetClipboard`.
static void RepeatedlyTryOleSetClipboard(IDataObject* aDataObj) {
  RepeatedlyTry(::OleSetClipboard, LogOleSetClipboardResult, aDataObj);
}

//-------------------------------------------------------------------------
NS_IMETHODIMP nsClipboard::SetNativeClipboardData(
    nsITransferable* aTransferable, ClipboardType aWhichClipboard) {
  MOZ_CLIPBOARD_LOG("%s", __FUNCTION__);

  if (aWhichClipboard != kGlobalClipboard) {
    return NS_ERROR_FAILURE;
  }

  // make sure we have a good transferable
  if (!aTransferable) {
    return NS_ERROR_FAILURE;
  }

#ifdef ACCESSIBILITY
  mozilla::a11y::Compatibility::SuppressA11yForClipboardCopy();
#endif

  RefPtr<IDataObject> dataObj;
  auto mightNeedToFlush = MightNeedToFlush::No;
  if (NS_SUCCEEDED(CreateNativeDataObject(aTransferable,
                                          getter_AddRefs(dataObj), nullptr,
                                          &mightNeedToFlush))) {
    RepeatedlyTryOleSetClipboard(dataObj);

    const bool doFlush = [&] {
      switch (mozilla::StaticPrefs::widget_windows_sync_clipboard_flush()) {
        case 0:
          return false;
        case 1:
          return true;
        default:
          // Bug 1774285: Windows Suggested Actions (introduced in Windows 11
          // 22H2) walks the entire a11y tree using UIA if something is placed
          // on the clipboard using delayed rendering. (The OLE clipboard always
          // uses delayed rendering.) This a11y tree walk causes an unacceptable
          // hang, particularly when the a11y cache is disabled. We choose the
          // lesser of the two performance/memory evils here and force immediate
          // rendering as part of our workaround.
          return mightNeedToFlush == MightNeedToFlush::Yes &&
                 mozilla::IsWin1122H2OrLater();
      }
    }();
    if (doFlush) {
      RepeatedlyTry(::OleFlushClipboard, [](HRESULT) {});
    }
  } else {
    // Clear the native clipboard
    RepeatedlyTryOleSetClipboard(nullptr);
  }

  return NS_OK;
}

//-------------------------------------------------------------------------
nsresult nsClipboard::GetGlobalData(HGLOBAL aHGBL, void** aData,
                                    uint32_t* aLen) {
  MOZ_CLIPBOARD_LOG("%s", __FUNCTION__);

  // Allocate a new memory buffer and copy the data from global memory.
  //
  // Some callers of this function call `NS_strlen(char16_t*)` on the returned
  // data buffer -- even though there's no guarantee that the data is a wide
  // string, let alone NUL-terminated. As a safety precaution, allocate a
  // slightly longer buffer than necessary, and append three bytes' worth of
  // NUL.
  //
  // (These bytes are not reported in *aLen, so callers which sensibly use that
  // as a limit will not need to worry about stray trailing bytes.)

  if (aHGBL != nullptr) {
    ScopedOLELock<CHAR[]> lpStr(aHGBL);
    mozilla::CheckedInt<uint32_t> allocSize =
        mozilla::CheckedInt<uint32_t>(lpStr.size()) + 3;
    if (!allocSize.isValid()) {
      return NS_ERROR_INVALID_ARG;
    }
    char* data = static_cast<char*>(malloc(allocSize.value()));
    if (!data) return NS_ERROR_FAILURE;

    std::copy(lpStr.begin(), lpStr.end(), data);

    // null terminate for safety
    std::fill_n(data + lpStr.size(), 3, '\0');

    *aData = data;
    *aLen = lpStr.size();

    return NS_OK;
  }

  // We really shouldn't ever get here
  // but just in case
  *aData = nullptr;
  *aLen = 0;

  LPVOID lpMsgBuf;
  ::FormatMessageW(
      FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, nullptr,
      GetLastError(),
      MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),  // Default language
      (LPWSTR)&lpMsgBuf, 0, nullptr);

  // Display the string.
  ::MessageBoxW(nullptr, (LPCWSTR)lpMsgBuf, L"GetLastError",
                MB_OK | MB_ICONINFORMATION);

  // Free the buffer.
  ::LocalFree(lpMsgBuf);

  return NS_ERROR_FAILURE;
}

//-------------------------------------------------------------------------
nsresult nsClipboard::GetNativeDataOffClipboard(nsIWidget* aWidget,
                                                UINT /*aIndex*/, UINT aFormat,
                                                void** aData, uint32_t* aLen,
                                                uint64_t aThreshold) {
  MOZ_CLIPBOARD_LOG("%s: overload taking nsIWidget*.", __FUNCTION__);

  HGLOBAL hglb;
  nsresult result = NS_ERROR_FAILURE;

  HWND nativeWin = nullptr;
  if (::OpenClipboard(nativeWin)) {
    hglb = ::GetClipboardData(aFormat);

    if (aThreshold) {
      if (aFormat == CF_TEXT) {
        MOZ_TRY(CheckClipboardByteSize(hglb, aThreshold + 1));
      } else if (aFormat == CF_UNICODETEXT) {
        MOZ_TRY(CheckClipboardByteSize(hglb, aThreshold + sizeof(wchar_t)));
      }
    }

    result = GetGlobalData(hglb, aData, aLen);
    ::CloseClipboard();
  }
  return result;
}

// See methods listed at
// <https://docs.microsoft.com/en-us/windows/win32/api/objidl/nn-objidl-idataobject#methods>.
static void LogIDataObjectMethodResult(const HRESULT aHres,
                                       mozilla::StaticString aMethodName) {
  if (MOZ_CLIPBOARD_LOG_ENABLED()) {
    nsAutoCString hresString;
    IDataObjectMethodResultToString(aHres, hresString);
    MOZ_CLIPBOARD_LOG("IDataObject::%s result : %s", aMethodName.get(),
                      hresString.get());
    MaybeLogClipboardCurrentOwner(aHres, aMethodName);
  }
}

// Other apps can block access to the clipboard. This repeatedly calls
// `GetData` for a fixed number of times and should be called instead of
// `GetData`. See
// <https://docs.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-idataobject-getdata>.
// While Microsoft's documentation doesn't include `CLIPBRD_E_CANT_OPEN`
// explicitly, it allows it implicitly and in local experiments it was indeed
// returned.
static HRESULT RepeatedlyTryGetData(IDataObject& aDataObject, LPFORMATETC pFE,
                                    LPSTGMEDIUM pSTM) {
  return RepeatedlyTry(
      [&aDataObject, &pFE, &pSTM]() { return aDataObject.GetData(pFE, pSTM); },
      [](HRESULT hres) { LogIDataObjectMethodResult(hres, "GetData"); });
}

//-------------------------------------------------------------------------
// static
HRESULT nsClipboard::FillSTGMedium(IDataObject* aDataObject, UINT aFormat,
                                   LPFORMATETC pFE, LPSTGMEDIUM pSTM,
                                   DWORD aTymed) {
  SET_FORMATETC(*pFE, aFormat, 0, DVASPECT_CONTENT, -1, aTymed);

  // Starting by querying for the data to see if we can get it as from global
  // memory
  HRESULT hres = S_FALSE;
  hres = aDataObject->QueryGetData(pFE);
  LogIDataObjectMethodResult(hres, "QueryGetData");
  if (S_OK == hres) {
    hres = RepeatedlyTryGetData(*aDataObject, pFE, pSTM);
  }
  return hres;
}

//-------------------------------------------------------------------------
// aMIMEFlavor lets the caller hint which transferable flavor is being
// fetched. It must be a type for which we have an image encoder (e.g.
// image/png) when aFormat is CF_DIBV5, and it is consulted to detect web
// custom format payloads ("web foo/bar") so the trailing UTF-16 null pad is
// suppressed. May be null otherwise.
nsresult nsClipboard::GetNativeDataOffClipboard(IDataObject* aDataObject,
                                                UINT aIndex, UINT aFormat,
                                                const char* aMIMEFlavor,
                                                void** aData, uint32_t* aLen,
                                                uint64_t aThreshold) {
  MOZ_CLIPBOARD_LOG("%s: overload taking IDataObject*.", __FUNCTION__);

  *aData = nullptr;
  *aLen = 0;

  if (!aDataObject) {
    return NS_ERROR_FAILURE;
  }

  UINT const format = aFormat;

  FORMATETC fe;
  STGMEDIUM stm;
  HRESULT hres = FillSTGMedium(aDataObject, format, &fe, &stm, TYMED_HGLOBAL);

  // If the format is CF_HDROP and we haven't found any files we can try looking
  // for virtual files with FILEDESCRIPTOR.
  if (FAILED(hres) && format == CF_HDROP) {
    hres = FillSTGMedium(aDataObject,
                         nsClipboard::GetClipboardFileDescriptorFormatW(), &fe,
                         &stm, TYMED_HGLOBAL);
    if (FAILED(hres)) {
      hres = FillSTGMedium(aDataObject,
                           nsClipboard::GetClipboardFileDescriptorFormatA(),
                           &fe, &stm, TYMED_HGLOBAL);
    }
  }

  // N.B.: not `FAILED(hres)`, as this can be `S_FALSE`!
  if (hres != S_OK) {
    return NS_ERROR_FAILURE;
  }

  // otherwise, there is something in stm; make sure we delete it on exit
  auto const _release_stm =
      mozilla::MakeScopeExit([&stm] { ::ReleaseStgMedium(&stm); });

  static CLIPFORMAT fileDescriptorFlavorA =
      ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA);
  static CLIPFORMAT fileDescriptorFlavorW =
      ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW);
  static CLIPFORMAT fileFlavor = ::RegisterClipboardFormat(CFSTR_FILECONTENTS);
  static CLIPFORMAT preferredDropEffect =
      ::RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT);
  static CLIPFORMAT pngFlavor = ::RegisterClipboardFormat(TEXT("PNG"));

  // Historical note: when this code was first written (bug #9367, 1999-07-09),
  // it was believed we would need to handle other values of stm.tymed. As of
  // 2024-01-09, such a need has not yet materialized.
  if (stm.tymed != TYMED_HGLOBAL) {
    MOZ_CLIPBOARD_LOG("unhandled TYMED_* value: %lu", stm.tymed);
    return NS_ERROR_FAILURE;
  }

  // compile-time-constant format indicators:
  switch (fe.cfFormat) {
    case CF_TEXT: {
      if (aThreshold > 0) {
        MOZ_TRY(CheckClipboardByteSize(stm.hGlobal, aThreshold + 1));
      }

      // Get the data out of the global data handle. The size we
      // return should not include the null because the other
      // platforms don't use nulls, so just return the length we get
      // back from strlen(), since we know CF_TEXT is null
      // terminated. Recall that GetGlobalData() returns the size of
      // the allocated buffer, not the size of the data (on 98, these
      // are not the same) so we can't use that.
      uint32_t allocLen = 0;
      MOZ_TRY(GetGlobalData(stm.hGlobal, aData, &allocLen));
      *aLen = strlen(reinterpret_cast<char*>(*aData));
      return NS_OK;
    }

    case CF_UNICODETEXT: {
      if (aThreshold > 0) {
        MOZ_TRY(
            CheckClipboardByteSize(stm.hGlobal, aThreshold + sizeof(wchar_t)));
      }

      // Get the data out of the global data handle. The size we
      // return should not include the null because the other
      // platforms don't use nulls, so just return the length we get
      // back from strlen(), since we know CF_UNICODETEXT is null
      // terminated. Recall that GetGlobalData() returns the size of
      // the allocated buffer, not the size of the data (on 98, these
      // are not the same) so we can't use that.
      uint32_t allocLen = 0;
      MOZ_TRY(GetGlobalData(stm.hGlobal, aData, &allocLen));
      *aLen = NS_strlen(reinterpret_cast<char16_t*>(*aData)) * 2;
      return NS_OK;
    }

    case CF_DIBV5: {
      if (!aMIMEFlavor) {
        return NS_ERROR_FAILURE;
      }
      uint32_t allocLen = 0;
      const char* clipboardData = nullptr;
      auto const _freeClipboardData =
          mozilla::MakeScopeExit([&]() { free((void*)clipboardData); });

      MOZ_TRY(GetGlobalData(stm.hGlobal, (void**)&clipboardData, &allocLen));
      nsCOMPtr<imgIContainer> container;
      nsCOMPtr<imgITools> imgTools =
          do_CreateInstance("@mozilla.org/image/tools;1");
      MOZ_TRY(imgTools->DecodeImageFromBuffer(
          clipboardData, allocLen, nsLiteralCString(IMAGE_BMP_MS_CLIPBOARD),
          getter_AddRefs(container)));

      nsAutoCString mimeType;
      if (strcmp(aMIMEFlavor, kJPGImageMime) == 0) {
        mimeType.Assign(IMAGE_JPEG);
      } else {
        mimeType.Assign(aMIMEFlavor);
      }

      nsCOMPtr<nsIInputStream> inputStream;
      MOZ_TRY(imgTools->EncodeImage(container, mimeType, u""_ns,
                                    getter_AddRefs(inputStream)));

      if (!inputStream) {
        return NS_ERROR_FAILURE;
      }

      *aData = inputStream.forget().take();
      *aLen = sizeof(nsIInputStream*);
      return NS_OK;
    }

    case CF_HDROP: {
      // in the case of a file drop, multiple files are stashed within a
      // single data object. In order to match mozilla's D&D apis, we
      // just pull out the file at the requested index, pretending as
      // if there really are multiple drag items.
      ScopedOLELock<HDROP> dropFiles(stm.hGlobal);

      UINT numFiles = ::DragQueryFileW(dropFiles.get(), 0xFFFFFFFF, nullptr, 0);

      if (numFiles == 0) {
        NS_WARNING("CF_HDROP received with empty file list");
        return NS_ERROR_FAILURE;
      }

      // Note that (partly for historical reasons) we do not consider it to be
      // an error on Gecko's part to request index 0 of a data object that turns
      // out to be empty. That case is handled above.
      if (aIndex >= numFiles) {
        MOZ_ASSERT(false, "Asked for a file index out of range of list");
        return NS_ERROR_INVALID_ARG;
      }

      UINT fileNameLen = ::DragQueryFileW(dropFiles.get(), aIndex, nullptr, 0);
      wchar_t* buffer = reinterpret_cast<wchar_t*>(
          moz_xmalloc((fileNameLen + 1) * sizeof(wchar_t)));
      ::DragQueryFileW(dropFiles.get(), aIndex, buffer, fileNameLen + 1);
      *aData = buffer;
      *aLen = fileNameLen * sizeof(char16_t);
      return NS_OK;
    }

    default: /* fallthrough */;
  }  // switch (fe.cfFormat)

  // non-compile-time-constant format indicators:

  if (fe.cfFormat == fileDescriptorFlavorA ||
      fe.cfFormat == fileDescriptorFlavorW) {
    nsAutoString tempPath;

    if (fe.cfFormat == fileDescriptorFlavorW) {
      ScopedOLELock<LPFILEGROUPDESCRIPTORW> fgdesc(stm.hGlobal);
      if (fgdesc) {
        if (aIndex >= fgdesc->cItems ||
            !FileGroupDescriptorHasItems<FILEGROUPDESCRIPTORW>(
                stm.hGlobal, static_cast<uint64_t>(aIndex) + 1)) {
          return NS_ERROR_INVALID_ARG;
        }
        const WCHAR* fileName = fgdesc->fgd[aIndex].cFileName;
        size_t nameLen = ::wcsnlen(fileName, MAX_PATH);
        if (nameLen == MAX_PATH) {
          return NS_ERROR_INVALID_ARG;
        }
        MOZ_TRY(
            GetTempFilePath(nsDependentString(fileName, nameLen), tempPath));
      }
    } else {
      ScopedOLELock<LPFILEGROUPDESCRIPTORA> fgdesc(stm.hGlobal);
      if (fgdesc) {
        if (aIndex >= fgdesc->cItems ||
            !FileGroupDescriptorHasItems<FILEGROUPDESCRIPTORA>(
                stm.hGlobal, static_cast<uint64_t>(aIndex) + 1)) {
          return NS_ERROR_INVALID_ARG;
        }
        const CHAR* fileName = fgdesc->fgd[aIndex].cFileName;
        size_t nameLen = ::strnlen(fileName, MAX_PATH);
        if (nameLen == MAX_PATH) {
          return NS_ERROR_INVALID_ARG;
        }
        nsAutoString wideName;
        MOZ_TRY(NS_CopyNativeToUnicode(nsDependentCSubstring(fileName, nameLen),
                                       wideName));
        MOZ_TRY(GetTempFilePath(wideName, tempPath));
      }
    }

    MOZ_TRY(SaveStorageOrStream(aDataObject, aIndex, tempPath));

    wchar_t* buffer = reinterpret_cast<wchar_t*>(
        moz_xmalloc((tempPath.Length() + 1) * sizeof(wchar_t)));
    wcscpy(buffer, tempPath.get());
    *aData = buffer;
    *aLen = tempPath.Length() * sizeof(wchar_t);
    return NS_OK;
  }

  if (fe.cfFormat == pngFlavor) {
    MOZ_ASSERT(!strcmp(aMIMEFlavor, kPNGImageMime));
    uint32_t allocLen = 0;
    const char* clipboardData = nullptr;
    auto const _freeClipboardData =
        mozilla::MakeScopeExit([&]() { free((void*)clipboardData); });

    MOZ_TRY(GetGlobalData(stm.hGlobal, (void**)&clipboardData, &allocLen));
    nsCOMPtr<imgIContainer> container;
    nsCOMPtr<imgITools> imgTools =
        do_CreateInstance("@mozilla.org/image/tools;1");
    MOZ_TRY(imgTools->DecodeImageFromBuffer(clipboardData, allocLen,
                                            nsLiteralCString(kPNGImageMime),
                                            getter_AddRefs(container)));

    nsCOMPtr<nsIInputStream> inputStream;
    MOZ_TRY(imgTools->EncodeImage(container, nsLiteralCString(kPNGImageMime),
                                  u""_ns, getter_AddRefs(inputStream)));

    if (!inputStream) {
      return NS_ERROR_FAILURE;
    }

    *aData = inputStream.forget().take();
    *aLen = sizeof(nsIInputStream*);
    return NS_OK;
  }

  if (fe.cfFormat == fileFlavor) {
    NS_WARNING(
        "Mozilla doesn't yet understand how to read this type of "
        "file flavor");
    return NS_ERROR_FAILURE;
  }

  // Get the data out of the global data handle. The size we
  // return should not include the null because the other
  // platforms don't use nulls, so just return the length we get
  // back from strlen(), since we know CF_UNICODETEXT is null
  // terminated. Recall that GetGlobalData() returns the size of
  // the allocated buffer, not the size of the data (on 98, these
  // are not the same) so we can't use that.
  //
  // NOTE: we are assuming that anything that falls into this
  //        default case is unicode. As we start to get more
  //        kinds of binary data, this may become an incorrect
  //        assumption. Stay tuned.
  uint32_t allocLen = 0;
  MOZ_TRY(GetGlobalData(stm.hGlobal, aData, &allocLen));
  if (fe.cfFormat == GetHtmlClipboardFormat()) {
    // CF_HTML is actually UTF8, not unicode, so disregard the
    // assumption above. We have to check the header for the
    // actual length, and we'll do that in FindPlatformHTML().
    // For now, return the allocLen. This case is mostly to
    // ensure we don't try to call strlen on the buffer.
    *aLen = allocLen;
  } else if (fe.cfFormat == GetCustomClipboardFormat() ||
             fe.cfFormat == GetWebCustomFormatMapClipboardFormat() ||
             (aMIMEFlavor && !strncmp(aMIMEFlavor, kWebCustomFormatPrefix,
                                      strlen(kWebCustomFormatPrefix)))) {
    // Binary data: legacy application/x-moz-custom-clipdata, the web custom
    // format map (JSON, no terminator), or one of the "Web Custom FormatN"
    // clipboard formats backing a web custom format payload. The writer
    // stores the exact byte length without a trailing terminator, so we
    // mustn't fall through to the default UTF-16 strlen interpretation
    // (which would over-read into the zero-padded HGLOBAL tail).
    *aLen = allocLen;
  } else if (fe.cfFormat == preferredDropEffect) {
    // As per the MSDN doc entitled: "Shell Clipboard Formats"
    // CFSTR_PREFERREDDROPEFFECT should return a DWORD
    // Reference:
    // http://msdn.microsoft.com/en-us/library/bb776902(v=vs.85).aspx
    NS_ASSERTION(allocLen == sizeof(DWORD),
                 "CFSTR_PREFERREDDROPEFFECT should return a DWORD");
    *aLen = allocLen;
  } else {
    *aLen = NS_strlen(reinterpret_cast<char16_t*>(*aData)) * sizeof(char16_t);
  }
  return NS_OK;
}
//-------------------------------------------------------------------------
mozilla::Result<nsCOMPtr<nsISupports>, nsresult>
nsClipboard::GetDataFromDataObject(IDataObject* aDataObject, UINT anIndex,
                                   nsIWidget* aWindow, const nsCString& aFlavor,
                                   uint64_t aThreshold) {
  MOZ_CLIPBOARD_LOG("%s", __FUNCTION__);

  // kWebCustomFormatMapType is a synthetic flavor: its payload isn't on the
  // clipboard as bytes-and-flavor; it's the "Web Custom Format Map" JSON. Use
  // the helper to fetch and parse it, then surface the per-essence flavors
  // as an nsIMutableArray. The rest of GetDataFromDataObject doesn't apply
  // to this case.
  if (aFlavor.EqualsLiteral(kWebCustomFormatMapType)) {
    mozilla::widget::WebCustomFormatMap map;
    if (!GetWebCustomFormatMapFromClipboard(aDataObject, aWindow, map)) {
      return nsCOMPtr<nsISupports>{};
    }
    nsCOMPtr<nsIMutableArray> customFormats =
        do_CreateInstance(NS_ARRAY_CONTRACTID);
    for (const auto& essence : map.Keys()) {
      nsCOMPtr<nsISupportsCString> customFormat =
          do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID);
      customFormat->SetData(nsLiteralCString(kWebCustomFormatPrefix) + essence);
      customFormats->AppendElement(customFormat);
    }
    return nsCOMPtr<nsISupports>(std::move(customFormats));
  }

  // Resolve the Windows clipboard format for aFlavor. Standard flavors go
  // through GetFormat(); a "web foo/bar" flavor is dynamic and resolved via
  // a lookup in the published "Web Custom Format Map" JSON.
  const bool isWebFormat =
      StringBeginsWith(aFlavor, nsLiteralCString(kWebCustomFormatPrefix));
  UINT format;
  if (isWebFormat) {
    mozilla::widget::WebCustomFormatMap map;
    if (!GetWebCustomFormatMapFromClipboard(aDataObject, aWindow, map)) {
      return nsCOMPtr<nsISupports>{};
    }
    nsDependentCSubstring essence(
        Substring(aFlavor, strlen(kWebCustomFormatPrefix)));
    auto entry = map.Lookup(essence);
    if (!entry) {
      return nsCOMPtr<nsISupports>{};
    }
    format = GetFormat(entry.Data().get());
    if (!format) {
      return nsCOMPtr<nsISupports>{};
    }
  } else {
    format = GetFormat(aFlavor.get());
  }

  // Try to get the data using the desired flavor. This might fail, but all is
  // not lost.
  void* data = nullptr;
  uint32_t dataLen = 0;
  bool dataFound = false;
  if (nullptr != aDataObject) {
    nsresult rv =
        GetNativeDataOffClipboard(aDataObject, anIndex, format, aFlavor.get(),
                                  &data, &dataLen, aThreshold);
    if (rv == NS_ERROR_CLIPBOARD_TOO_BIG) {
      return mozilla::Err(NS_ERROR_CLIPBOARD_TOO_BIG);
    }
    dataFound = NS_SUCCEEDED(rv);
  } else if (nullptr != aWindow) {
    nsresult rv = GetNativeDataOffClipboard(aWindow, anIndex, format, &data,
                                            &dataLen, aThreshold);
    if (rv == NS_ERROR_CLIPBOARD_TOO_BIG) {
      return mozilla::Err(NS_ERROR_CLIPBOARD_TOO_BIG);
    }
    dataFound = NS_SUCCEEDED(rv);
  }

  // This is our second chance to try to find some data, having not found it
  // when directly asking for the flavor. Let's try digging around in other
  // flavors to help satisfy our craving for data.
  if (!dataFound) {
    if (aFlavor.EqualsLiteral(kTextMime)) {
      dataFound =
          FindUnicodeFromPlainText(aDataObject, anIndex, &data, &dataLen);
    } else if (aFlavor.EqualsLiteral(kURLMime)) {
      // drags from other windows apps expose the native
      // CFSTR_INETURL{A,W} flavor
      dataFound = FindURLFromNativeURL(aDataObject, anIndex, &data, &dataLen);
      if (!dataFound) {
        dataFound = FindURLFromLocalFile(aDataObject, anIndex, &data, &dataLen);
      }
    } else {
      mozilla::Maybe<UINT> secondaryFormat = GetSecondaryFormat(aFlavor.get());
      if (secondaryFormat) {
        // Fall back to secondary format
        dataFound = NS_SUCCEEDED(GetNativeDataOffClipboard(
            aDataObject, anIndex, secondaryFormat.value(), aFlavor.get(), &data,
            &dataLen));
      }
    }
  }  // if we try one last ditch effort to find our data

  if (!dataFound) {
    return nsCOMPtr<nsISupports>{};
  }

  // Hopefully by this point we've found it and can go about our business
  nsCOMPtr<nsISupports> genericDataWrapper;
  if (aFlavor.EqualsLiteral(kFileMime)) {
    // we have a file path in |data|. Create an nsLocalFile object.
    nsDependentString filepath(reinterpret_cast<char16_t*>(data));
    nsCOMPtr<nsIFile> file;
    if (NS_SUCCEEDED(NS_NewLocalFile(filepath, getter_AddRefs(file)))) {
      genericDataWrapper = do_QueryInterface(file);
    }
    free(data);
  } else if (aFlavor.EqualsLiteral(kNativeHTMLMime)) {
    uint32_t dummy;
    // the editor folks want CF_HTML exactly as it's on the clipboard, no
    // conversions, no fancy stuff. Pull it off the clipboard, stuff it into
    // a wrapper and hand it back to them.
    if (FindPlatformHTML(aDataObject, anIndex, &data, &dummy, &dataLen)) {
      nsPrimitiveHelpers::CreatePrimitiveForData(
          aFlavor, data, dataLen, getter_AddRefs(genericDataWrapper));
    }
    free(data);
  } else if (aFlavor.EqualsLiteral(kHTMLMime)) {
    uint32_t startOfData = 0;
    // The JS folks want CF_HTML exactly as it is on the clipboard, but
    // minus the CF_HTML header index information.
    // It also needs to be converted to UTF16 and have linebreaks changed.
    if (FindPlatformHTML(aDataObject, anIndex, &data, &startOfData, &dataLen)) {
      dataLen -= startOfData;
      nsPrimitiveHelpers::CreatePrimitiveForCFHTML(
          static_cast<char*>(data) + startOfData, &dataLen,
          getter_AddRefs(genericDataWrapper));
    }
    free(data);
  } else if (aFlavor.EqualsLiteral(kJPEGImageMime) ||
             aFlavor.EqualsLiteral(kJPGImageMime) ||
             aFlavor.EqualsLiteral(kPNGImageMime)) {
    nsIInputStream* imageStream = reinterpret_cast<nsIInputStream*>(data);
    genericDataWrapper = do_QueryInterface(imageStream);
    NS_IF_RELEASE(imageStream);
  } else {
    // Treat custom types and web custom format payloads as raw bytes
    // (nsISupportsCString); CreatePrimitiveForData below picks that up
    // from the flavor. Skip the Win32 -> DOM linebreak conversion for
    // these so the bytes survive verbatim.
    if (!aFlavor.EqualsLiteral(kCustomTypesMime) && !isWebFormat) {
      bool isRTF = aFlavor.EqualsLiteral(kRTFMime);
      // we probably have some form of text. The DOM only wants LF, so
      // convert from Win32 line endings to DOM line endings.
      int32_t signedLen = static_cast<int32_t>(dataLen);
      nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks(isRTF, &data,
                                                         &signedLen);
      dataLen = signedLen;

      if (isRTF) {
        // RTF on Windows is known to sometimes deliver an extra null byte.
        if (dataLen > 0 && static_cast<char*>(data)[dataLen - 1] == '\0') {
          dataLen--;
        }
      }
    }

    nsPrimitiveHelpers::CreatePrimitiveForData(
        aFlavor, data, dataLen, getter_AddRefs(genericDataWrapper));
    free(data);
  }

  return std::move(genericDataWrapper);
}

nsresult nsClipboard::GetDataFromDataObject(IDataObject* aDataObject,
                                            UINT anIndex, nsIWidget* aWindow,
                                            nsITransferable* aTransferable,
                                            uint64_t aThreshold) {
  MOZ_CLIPBOARD_LOG("%s", __FUNCTION__);

  // make sure we have a good transferable
  if (!aTransferable) {
    return NS_ERROR_INVALID_ARG;
  }

  // get flavor list that includes all flavors that can be written (including
  // ones obtained through conversion)
  nsTArray<nsCString> flavors;
  nsresult res = aTransferable->FlavorsTransferableCanImport(flavors);
  if (NS_FAILED(res)) {
    return NS_ERROR_FAILURE;
  }

  // Walk through flavors and see which flavor is on the clipboard them on the
  // native clipboard,
  for (uint32_t i = 0; i < flavors.Length(); i++) {
    const nsCString& flavorStr = flavors[i];

    auto dataOrError = GetDataFromDataObject(aDataObject, anIndex, aWindow,
                                             flavorStr, aThreshold);
    if (dataOrError.isErr() || !dataOrError.inspect()) {
      continue;
    }

    NS_ASSERTION(dataOrError.inspect(),
                 "About to put null data into the transferable");
    aTransferable->SetTransferData(flavorStr.get(), dataOrError.inspect());
    // we found one, get out of the loop
    break;
  }  // foreach flavor

  return NS_OK;
}

//
// FindPlatformHTML
//
// Someone asked for the OS CF_HTML flavor. We give it back to them exactly
// as-is.
//
bool nsClipboard ::FindPlatformHTML(IDataObject* inDataObject, UINT inIndex,
                                    void** outData, uint32_t* outStartOfData,
                                    uint32_t* outDataLen) {
  // Reference: MSDN doc entitled "HTML Clipboard Format"
  // http://msdn.microsoft.com/en-us/library/aa767917(VS.85).aspx#unknown_854
  // CF_HTML is UTF8, not unicode. We also can't rely on it being
  // null-terminated so we have to check the CF_HTML header for the correct
  // length. The length we return is the bytecount from the beginning of the
  // selected data to the end of the selected data, without the null
  // termination. Because it's UTF8, we're guaranteed the header is ASCII.

  if (!outData || !*outData) {
    return false;
  }

  char version[8] = {0};
  int32_t startOfData = 0;
  int32_t endOfData = 0;
  int numFound =
      sscanf((char*)*outData, "Version:%7s\nStartHTML:%d\nEndHTML:%d", version,
             &startOfData, &endOfData);

  if (numFound != 3 || startOfData < -1 || endOfData < -1) {
    return false;
  }

  // Fixup the start and end markers if they have no context (set to -1)
  if (startOfData == -1) {
    startOfData = 0;
  }
  if (endOfData == -1) {
    endOfData = *outDataLen;
  }

  // Make sure we were passed sane values within our buffer size.
  // (Note that we've handled all cases of negative endOfData above, so we can
  // safely cast it to be unsigned here.)
  if (!endOfData || startOfData >= endOfData ||
      static_cast<uint32_t>(endOfData) > *outDataLen) {
    return false;
  }

  // We want to return the buffer not offset by startOfData because it will be
  // parsed out later (probably by HTMLEditor::ParseCFHTML) when it is still
  // in CF_HTML format.

  // We return the byte offset from the start of the data buffer to where the
  // HTML data starts. The caller might want to extract the HTML only.
  *outStartOfData = startOfData;
  *outDataLen = endOfData;
  return true;
}

//
// FindUnicodeFromPlainText
//
// Looks for CF_TEXT on the clipboard and converts it into an UTF-16 string
// if present. Returns this string in outData, and its length in outDataLen.
// XXXndeakin Windows converts between CF_UNICODE and CF_TEXT automatically
// so it doesn't seem like this is actually needed.
//
bool nsClipboard ::FindUnicodeFromPlainText(IDataObject* inDataObject,
                                            UINT inIndex, void** outData,
                                            uint32_t* outDataLen) {
  MOZ_CLIPBOARD_LOG("%s", __FUNCTION__);

  // We are looking for text/plain and we failed to find it on the clipboard
  // first, so try again with CF_TEXT. If that is present, convert it to
  // unicode.
  nsresult rv = GetNativeDataOffClipboard(inDataObject, inIndex, CF_TEXT,
                                          nullptr, outData, outDataLen);
  if (NS_FAILED(rv) || !*outData) {
    return false;
  }

  const char* castedText = static_cast<char*>(*outData);
  nsAutoString tmp;
  rv = NS_CopyNativeToUnicode(nsDependentCSubstring(castedText, *outDataLen),
                              tmp);
  if (NS_FAILED(rv)) {
    return false;
  }

  // out with the old, in with the new
  free(*outData);
  *outData = ToNewUnicode(tmp);
  *outDataLen = tmp.Length() * sizeof(char16_t);

  return true;

}  // FindUnicodeFromPlainText

//
// FindURLFromLocalFile
//
// we are looking for a URL and couldn't find it, try again with looking for
// a local file. If we have one, it may either be a normal file or an internet
// shortcut. In both cases, however, we can get a URL (it will be a file:// url
// in the local file case).
//
bool nsClipboard ::FindURLFromLocalFile(IDataObject* inDataObject, UINT inIndex,
                                        void** outData, uint32_t* outDataLen) {
  MOZ_CLIPBOARD_LOG("%s", __FUNCTION__);

  bool dataFound = false;

  nsresult loadResult =
      GetNativeDataOffClipboard(inDataObject, inIndex, GetFormat(kFileMime),
                                nullptr, outData, outDataLen);
  if (NS_SUCCEEDED(loadResult) && *outData) {
    // we have a file path in |data|. Is it an internet shortcut or a normal
    // file?
    const nsDependentString filepath(static_cast<char16_t*>(*outData));
    nsCOMPtr<nsIFile> file;
    nsresult rv = NS_NewLocalFile(filepath, getter_AddRefs(file));
    if (NS_FAILED(rv)) {
      free(*outData);
      return dataFound;
    }

    if (IsInternetShortcut(filepath)) {
      free(*outData);
      nsAutoCString url;
      ResolveShortcut(file, url);
      if (!url.IsEmpty()) {
        // convert it to unicode and pass it out
        NS_ConvertUTF8toUTF16 urlString(url);
        // the internal mozilla URL format, text/x-moz-url, contains
        // URL\ntitle.  We can guess the title from the file's name.
        nsAutoString title;
        file->GetLeafName(title);
        // We rely on IsInternetShortcut check that file has a .url extension.
        title.SetLength(title.Length() - 4);
        if (title.IsEmpty()) {
          title = urlString;
        }
        *outData = ToNewUnicode(urlString + u"\n"_ns + title);
        *outDataLen =
            NS_strlen(static_cast<char16_t*>(*outData)) * sizeof(char16_t);

        dataFound = true;
      }
    } else {
      // we have a normal file, use some Necko objects to get our file path
      nsAutoCString urlSpec;
      NS_GetURLSpecFromFile(file, urlSpec);

      // convert it to unicode and pass it out
      free(*outData);
      *outData = UTF8ToNewUnicode(urlSpec);
      *outDataLen =
          NS_strlen(static_cast<char16_t*>(*outData)) * sizeof(char16_t);
      dataFound = true;
    }  // else regular file
  }

  return dataFound;
}  // FindURLFromLocalFile

//
// FindURLFromNativeURL
//
// we are looking for a URL and couldn't find it using our internal
// URL flavor, so look for it using the native URL flavor,
// CF_INETURLSTRW (We don't handle CF_INETURLSTRA currently)
//
bool nsClipboard ::FindURLFromNativeURL(IDataObject* inDataObject, UINT inIndex,
                                        void** outData, uint32_t* outDataLen) {
  MOZ_CLIPBOARD_LOG("%s", __FUNCTION__);

  bool dataFound = false;

  void* tempOutData = nullptr;
  uint32_t tempDataLen = 0;

  nsresult loadResult = GetNativeDataOffClipboard(
      inDataObject, inIndex, ::RegisterClipboardFormat(CFSTR_INETURLW), nullptr,
      &tempOutData, &tempDataLen);
  if (NS_SUCCEEDED(loadResult) && tempOutData) {
    nsDependentString urlString(static_cast<char16_t*>(tempOutData));
    // the internal mozilla URL format, text/x-moz-url, contains
    // URL\ntitle.  Since we don't actually have a title here,
    // just repeat the URL to fake it.
    *outData = ToNewUnicode(urlString + u"\n"_ns + urlString);
    *outDataLen =
        NS_strlen(static_cast<char16_t*>(*outData)) * sizeof(char16_t);
    free(tempOutData);
    dataFound = true;
  } else {
    loadResult = GetNativeDataOffClipboard(
        inDataObject, inIndex, ::RegisterClipboardFormat(CFSTR_INETURLA),
        nullptr, &tempOutData, &tempDataLen);
    if (NS_SUCCEEDED(loadResult) && tempOutData) {
      // CFSTR_INETURLA is (currently) equal to CFSTR_SHELLURL which is equal to
      // CF_TEXT which is by definition ANSI encoded.
      nsCString urlUnescapedA;
      bool unescaped =
          NS_UnescapeURL(static_cast<char*>(tempOutData), tempDataLen,
                         esc_OnlyNonASCII | esc_SkipControl, urlUnescapedA);

      nsString urlString;
      if (unescaped) {
        NS_CopyNativeToUnicode(urlUnescapedA, urlString);
      } else {
        NS_CopyNativeToUnicode(
            nsDependentCString(static_cast<char*>(tempOutData), tempDataLen),
            urlString);
      }

      // the internal mozilla URL format, text/x-moz-url, contains
      // URL\ntitle.  Since we don't actually have a title here,
      // just repeat the URL to fake it.
      *outData = ToNewUnicode(urlString + u"\n"_ns + urlString);
      *outDataLen =
          NS_strlen(static_cast<char16_t*>(*outData)) * sizeof(char16_t);
      free(tempOutData);
      dataFound = true;
    }
  }

  return dataFound;
}  // FindURLFromNativeURL

// Other apps can block access to the clipboard. This repeatedly
// calls `::OleGetClipboard` for a fixed number of times and should be called
// instead of `::OleGetClipboard`.
static HRESULT RepeatedlyTryOleGetClipboard(IDataObject** aDataObj) {
  return RepeatedlyTry(::OleGetClipboard, LogOleGetClipboardResult, aDataObj);
}

//
// ResolveShortcut
//
void nsClipboard ::ResolveShortcut(nsIFile* aFile, nsACString& outURL) {
  nsCOMPtr<nsIFileProtocolHandler> fph;
  nsresult rv = NS_GetFileProtocolHandler(getter_AddRefs(fph));
  if (NS_FAILED(rv)) {
    return;
  }

  nsCOMPtr<nsIURI> uri;
  rv = fph->ReadURLFile(aFile, getter_AddRefs(uri));
  if (NS_FAILED(rv)) {
    return;
  }

  uri->GetSpec(outURL);
}  // ResolveShortcut

//
// IsInternetShortcut
//
// A file is an Internet Shortcut if it ends with .URL
//
bool nsClipboard ::IsInternetShortcut(const nsAString& inFileName) {
  return StringEndsWith(inFileName, u".url"_ns,
                        nsCaseInsensitiveStringComparator);
}  // IsInternetShortcut

//-------------------------------------------------------------------------
mozilla::Result<nsCOMPtr<nsISupports>, nsresult>
nsClipboard::GetNativeClipboardData(const nsACString& aFlavor,
                                    ClipboardType aWhichClipboard,
                                    uint64_t aThreshold) {
  MOZ_DIAGNOSTIC_ASSERT(
      nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
  MOZ_DIAGNOSTIC_ASSERT(IsValidFlavor(aFlavor));

  MOZ_CLIPBOARD_LOG("%s aWhichClipboard=%i", __FUNCTION__, aWhichClipboard);

  // This makes sure we can use the OLE functionality for the clipboard
  IDataObject* dataObj;
  if (S_OK == RepeatedlyTryOleGetClipboard(&dataObj)) {
    auto dataObjRelease = mozilla::MakeScopeExit([&] { dataObj->Release(); });
    // Use OLE IDataObject for clipboard operations
    MOZ_CLIPBOARD_LOG("    use OLE IDataObject:");
    if (MOZ_CLIPBOARD_LOG_ENABLED()) {
      IEnumFORMATETC* pEnum = nullptr;
      if (S_OK == dataObj->EnumFormatEtc(DATADIR_GET, &pEnum)) {
        auto pEnumRelease = mozilla::MakeScopeExit([&] { pEnum->Release(); });
        FORMATETC fEtc;
        while (S_OK == pEnum->Next(1, &fEtc, nullptr)) {
          nsAutoString format;
          mozilla::widget::WinUtils::GetClipboardFormatAsString(fEtc.cfFormat,
                                                                format);
          MOZ_CLIPBOARD_LOG("        FORMAT %s",
                            NS_ConvertUTF16toUTF8(format).get());
        }
      }
    }

    return GetDataFromDataObject(dataObj, 0, nullptr,
                                 PromiseFlatCString(aFlavor), aThreshold);
  }

  // do it the old manual way
  return GetDataFromDataObject(nullptr, 0, mWindow, PromiseFlatCString(aFlavor),
                               aThreshold);
}

nsresult nsClipboard::EmptyNativeClipboardData(ClipboardType aWhichClipboard) {
  MOZ_DIAGNOSTIC_ASSERT(
      nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
  // Some programs such as ZoneAlarm monitor clipboard usage and then open the
  // clipboard to scan it.  If we i) empty and then ii) set data, then the
  // 'set data' can sometimes fail with access denied becacuse another program
  // has the clipboard open.  So to avoid this race condition for OpenClipboard
  // we do not empty the clipboard when we're setting it.
  RepeatedlyTryOleSetClipboard(nullptr);
  return NS_OK;
}

mozilla::Result<int32_t, nsresult>
nsClipboard::GetNativeClipboardSequenceNumber(ClipboardType aWhichClipboard) {
  MOZ_DIAGNOSTIC_ASSERT(kGlobalClipboard == aWhichClipboard);
  return (int32_t)::GetClipboardSequenceNumber();
}

//-------------------------------------------------------------------------
mozilla::Result<bool, nsresult>
nsClipboard::HasNativeClipboardDataMatchingFlavors(
    const nsTArray<nsCString>& aFlavorList, ClipboardType aWhichClipboard) {
  MOZ_DIAGNOSTIC_ASSERT(
      nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
  // The web custom format map is loaded lazily on the first "web " query to
  // avoid parsing JSON when the caller is only asking about standard formats.
  mozilla::widget::WebCustomFormatMap webCustomFormatMap;
  bool didLoadWebCustomFormatMap = false;
  IDataObject* webMapDataObj = nullptr;
  auto webMapDataObjRelease = mozilla::MakeScopeExit([&] {
    if (webMapDataObj) {
      webMapDataObj->Release();
    }
  });
  for (const auto& flavor : aFlavorList) {
    MOZ_DIAGNOSTIC_ASSERT(IsValidFlavor(flavor));

    if (StringBeginsWith(flavor, nsLiteralCString(kWebCustomFormatPrefix))) {
      if (!didLoadWebCustomFormatMap) {
        didLoadWebCustomFormatMap = true;
        // Try OLE first; if that fails, the helper falls back to the
        // window-rooted legacy Windows clipboard path.
        (void)RepeatedlyTryOleGetClipboard(&webMapDataObj);
        if (!GetWebCustomFormatMapFromClipboard(webMapDataObj, mWindow,
                                                webCustomFormatMap)) {
          continue;
        }
      }
      nsDependentCSubstring essence(
          Substring(flavor, strlen(kWebCustomFormatPrefix)));
      auto entry = webCustomFormatMap.Lookup(essence);
      if (!entry) {
        continue;
      }
      UINT cf = GetFormat(entry.Data().get());
      if (cf && IsClipboardFormatAvailable(cf)) {
        return true;
      }
      continue;
    }

    UINT format = GetFormat(flavor.get());
    if (IsClipboardFormatAvailable(format)) {
      return true;
    }
    mozilla::Maybe<UINT> secondaryFormat = GetSecondaryFormat(flavor.get());
    if (secondaryFormat &&
        IsClipboardFormatAvailable(secondaryFormat.value())) {
      return true;
    }
  }
  return false;
}

//-------------------------------------------------------------------------
nsresult nsClipboard::GetTempFilePath(const nsAString& aFileName,
                                      nsAString& aFilePath) {
  nsresult result = NS_OK;

  nsCOMPtr<nsIFile> tmpFile;
  result =
      GetSpecialSystemDirectory(OS_TemporaryDirectory, getter_AddRefs(tmpFile));
  NS_ENSURE_SUCCESS(result, result);

  result = tmpFile->Append(aFileName);
  NS_ENSURE_SUCCESS(result, result);

  result = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0660);
  NS_ENSURE_SUCCESS(result, result);
  result = tmpFile->GetPath(aFilePath);

  return result;
}

//-------------------------------------------------------------------------
nsresult nsClipboard::SaveStorageOrStream(IDataObject* aDataObject, UINT aIndex,
                                          const nsAString& aFileName) {
  NS_ENSURE_ARG_POINTER(aDataObject);

  FORMATETC fe = {0};
  SET_FORMATETC(fe, RegisterClipboardFormat(CFSTR_FILECONTENTS), 0,
                DVASPECT_CONTENT, aIndex, TYMED_ISTORAGE | TYMED_ISTREAM);

  STGMEDIUM stm = {0};
  HRESULT hres = aDataObject->GetData(&fe, &stm);
  if (FAILED(hres)) {
    return NS_ERROR_FAILURE;
  }

  nsPromiseFlatString flatFileName(aFileName);

  auto releaseMediumGuard =
      mozilla::MakeScopeExit([&] { ReleaseStgMedium(&stm); });

  // We do this check because, even though we *asked* for IStorage or IStream,
  // it seems that IDataObject providers can just hand us back whatever they
  // feel like. See Bug 1824644 for a fun example of that!
  if (stm.tymed != TYMED_ISTORAGE && stm.tymed != TYMED_ISTREAM) {
    return NS_ERROR_FAILURE;
  }

  if (stm.tymed == TYMED_ISTORAGE) {
    // should never happen -- but theoretically possible, given an ill-behaved
    // data-source
    if (stm.pstg == nullptr) {
      return NS_ERROR_FAILURE;
    }

    RefPtr<IStorage> file;
    hres = StgCreateStorageEx(
        flatFileName.get(), STGM_CREATE | STGM_READWRITE | STGM_SHARE_EXCLUSIVE,
        STGFMT_STORAGE, 0, NULL, NULL, IID_IStorage, getter_AddRefs(file));
    if (FAILED(hres)) {
      return NS_ERROR_FAILURE;
    }

    hres = stm.pstg->CopyTo(0, NULL, NULL, file);
    if (FAILED(hres)) {
      return NS_ERROR_FAILURE;
    }

    file->Commit(STGC_DEFAULT);

    return NS_OK;
  }

  MOZ_ASSERT(stm.tymed == TYMED_ISTREAM);
  // should never happen -- but possible given an ill-behaved data-source, and
  // has been seen in the wild (bug 1895681)
  if (stm.pstm == nullptr) {
    return NS_ERROR_FAILURE;
  }

  HANDLE handle = CreateFile(flatFileName.get(), GENERIC_WRITE, FILE_SHARE_READ,
                             NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  if (handle == INVALID_HANDLE_VALUE) {
    return NS_ERROR_FAILURE;
  }

  // Don't leave a partially-written file behind if we bail out on a read or
  // write error below.
  // NB: This must initialize before/run after fileCloseGuard.
  auto fileDeleteGuard =
      mozilla::MakeScopeExit([&] { DeleteFile(flatFileName.get()); });
  auto fileCloseGuard = mozilla::MakeScopeExit([&] { CloseHandle(handle); });

  const ULONG bufferSize = 4096;
  char buffer[bufferSize] = {0};
  ULONG bytesRead = 0;
  DWORD bytesWritten = 0;
  while (true) {
    HRESULT result = stm.pstm->Read(buffer, bufferSize, &bytesRead);
    if (FAILED(result)) {
      return NS_ERROR_FAILURE;
    }
    if (bytesRead > bufferSize) {
      // It obviously couldn't legitimately write more than the buffer holds.
      return NS_ERROR_FAILURE;
    }
    if (bytesRead == 0) {
      break;
    }
    if (!WriteFile(handle, buffer, static_cast<DWORD>(bytesRead), &bytesWritten,
                   NULL)) {
      return NS_ERROR_FAILURE;
    }
  }
  fileDeleteGuard.release();
  return NS_OK;
}
