diff --git a/src/lib/platform/MSWindowsClipboardBitmapConverter.cpp b/src/lib/platform/MSWindowsClipboardBitmapConverter.cpp index 29462bfaa..eefff5050 100644 --- a/src/lib/platform/MSWindowsClipboardBitmapConverter.cpp +++ b/src/lib/platform/MSWindowsClipboardBitmapConverter.cpp @@ -10,6 +10,47 @@ #include "base/Log.h" +#include + +#include +#include + +namespace { +bool normaliseMalformedMacDib(const std::string &data, std::string &normalisedData) +{ + if (data.size() < sizeof(BITMAPINFOHEADER)) { + return false; + } + + const auto *header = reinterpret_cast(data.data()); + if (header->biWidth <= 0) { + return false; + } + + const auto width = static_cast(header->biWidth); + const auto height = static_cast(std::abs(static_cast(header->biHeight))); + if (height == 0 || width > (std::numeric_limits::max() - sizeof(BITMAPINFOHEADER)) / 4 / height) { + return false; + } + const auto expectedSize = sizeof(BITMAPINFOHEADER) + width * height * 4; + + // macOS can describe an INFOHEADER-sized 32-bit pixel payload as a V5 DIB. + // Windows then interprets the first pixels as V5 colour masks. The pixel + // bytes are ordinary BGRA, so publish a canonical BI_RGB DIB instead. + if (header->biSize <= sizeof(BITMAPINFOHEADER) || header->biPlanes != 1 || header->biBitCount != 32 || + header->biCompression != BI_BITFIELDS || expectedSize != data.size()) { + return false; + } + + normalisedData = data.substr(0, sizeof(BITMAPINFOHEADER)); + qToLittleEndian(sizeof(BITMAPINFOHEADER), reinterpret_cast(&normalisedData[0])); + qToLittleEndian(BI_RGB, reinterpret_cast(&normalisedData[0]) + 16); + normalisedData += data.substr(sizeof(BITMAPINFOHEADER)); + LOG_INFO("normalised malformed macOS clipboard image to BI_RGB"); + return true; +} +} // namespace + // // MSWindowsClipboardBitmapConverter // @@ -27,13 +68,19 @@ UINT MSWindowsClipboardBitmapConverter::getWin32Format() const HANDLE MSWindowsClipboardBitmapConverter::fromIClipboard(const std::string &data) const { + std::string normalisedData; + const auto *clipboardData = &data; + if (normaliseMalformedMacDib(data, normalisedData)) { + clipboardData = &normalisedData; + } + // copy to memory handle - HGLOBAL gData = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, data.size()); + HGLOBAL gData = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, clipboardData->size()); if (gData != nullptr) { // get a pointer to the allocated memory char *dst = (char *)GlobalLock(gData); if (dst != nullptr) { - memcpy(dst, data.data(), data.size()); + memcpy(dst, clipboardData->data(), clipboardData->size()); GlobalUnlock(gData); } else { GlobalFree(gData); diff --git a/src/unittests/platform/MSWindowsClipboardTests.cpp b/src/unittests/platform/MSWindowsClipboardTests.cpp index 79628cb6e..ae8a8e0f7 100644 --- a/src/unittests/platform/MSWindowsClipboardTests.cpp +++ b/src/unittests/platform/MSWindowsClipboardTests.cpp @@ -9,6 +9,9 @@ #include "MSWindowsClipboardTests.h" #include "platform/MSWindowsClipboard.h" +#include "platform/MSWindowsClipboardBitmapConverter.h" + +#include void MSWindowsClipboardTests::initTestCase() { @@ -136,4 +139,63 @@ void MSWindowsClipboardTests::isOwnedByDeskflow() QVERIFY(clipboard.isOwnedByDeskflow()); } +void MSWindowsClipboardTests::normalisesMalformedMacBitmap() +{ + // A 1x1 top-down macOS DIB that incorrectly declares a V5 header. + constexpr qsizetype headerSize = sizeof(BITMAPINFOHEADER); + std::string dib(headerSize + 4, '\0'); + auto *raw = reinterpret_cast(&dib[0]); + qToLittleEndian(sizeof(BITMAPV5HEADER), raw); // claims V5 but only has an INFOHEADER + qToLittleEndian(1, raw + 4); + qToLittleEndian(-1, raw + 8); + qToLittleEndian(1, raw + 12); + qToLittleEndian(32, raw + 14); + qToLittleEndian(BI_BITFIELDS, raw + 16); + raw[headerSize + 3] = 0xff; + + MSWindowsClipboardBitmapConverter converter; + const auto handle = converter.fromIClipboard(dib); + QVERIFY(handle != nullptr); + QCOMPARE(GlobalSize(handle), SIZE_T(headerSize + 4)); + const auto *result = static_cast(GlobalLock(handle)); + QVERIFY(result != nullptr); + QCOMPARE(qFromLittleEndian(result), quint32(headerSize)); + QCOMPARE(qFromLittleEndian(result + 16), quint32(BI_RGB)); + QCOMPARE(result[headerSize + 3], quint8(0xff)); + GlobalUnlock(handle); + GlobalFree(handle); +} + +void MSWindowsClipboardTests::preservesHealthyMacV5Bitmap() +{ + // A complete 1x1 top-down macOS V5 DIB with BGRA colour masks and one pixel. + constexpr qsizetype headerSize = sizeof(BITMAPV5HEADER); + std::string dib(headerSize + 4, '\0'); + auto *raw = reinterpret_cast(&dib[0]); + qToLittleEndian(headerSize, raw); + qToLittleEndian(1, raw + 4); + qToLittleEndian(-1, raw + 8); + qToLittleEndian(1, raw + 12); + qToLittleEndian(32, raw + 14); + qToLittleEndian(BI_BITFIELDS, raw + 16); + qToLittleEndian(0x00ff0000, raw + 40); + qToLittleEndian(0x0000ff00, raw + 44); + qToLittleEndian(0x000000ff, raw + 48); + qToLittleEndian(0xff000000, raw + 52); + raw[headerSize] = 0x12; + raw[headerSize + 1] = 0x34; + raw[headerSize + 2] = 0x56; + raw[headerSize + 3] = 0x78; + + MSWindowsClipboardBitmapConverter converter; + const auto handle = converter.fromIClipboard(dib); + QVERIFY(handle != nullptr); + QCOMPARE(GlobalSize(handle), SIZE_T(dib.size())); + const auto *result = static_cast(GlobalLock(handle)); + QVERIFY(result != nullptr); + QCOMPARE(std::string(result, GlobalSize(handle)), dib); + GlobalUnlock(handle); + GlobalFree(handle); +} + QTEST_MAIN(MSWindowsClipboardTests) diff --git a/src/unittests/platform/MSWindowsClipboardTests.h b/src/unittests/platform/MSWindowsClipboardTests.h index 483ffdc0a..d0b4b689c 100644 --- a/src/unittests/platform/MSWindowsClipboardTests.h +++ b/src/unittests/platform/MSWindowsClipboardTests.h @@ -28,6 +28,8 @@ private Q_SLOTS: void has_withNoFormatAdded(); void getNonEmptyText(); void isOwnedByDeskflow(); + void normalisesMalformedMacBitmap(); + void preservesHealthyMacV5Bitmap(); private: Log m_log;