fix(win): normalise malformed macOS clipboard DIB

This commit is contained in:
kyle 2026-08-13 14:57:36 +08:00 committed by Nick Bolton
parent 9a3a299c8d
commit e856cce65b
3 changed files with 113 additions and 2 deletions

View file

@ -10,6 +10,47 @@
#include "base/Log.h" #include "base/Log.h"
#include <QtEndian>
#include <cstdlib>
#include <limits>
namespace {
bool normaliseMalformedMacDib(const std::string &data, std::string &normalisedData)
{
if (data.size() < sizeof(BITMAPINFOHEADER)) {
return false;
}
const auto *header = reinterpret_cast<const BITMAPINFOHEADER *>(data.data());
if (header->biWidth <= 0) {
return false;
}
const auto width = static_cast<size_t>(header->biWidth);
const auto height = static_cast<size_t>(std::abs(static_cast<int64_t>(header->biHeight)));
if (height == 0 || width > (std::numeric_limits<size_t>::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<quint32>(sizeof(BITMAPINFOHEADER), reinterpret_cast<quint8 *>(&normalisedData[0]));
qToLittleEndian<quint32>(BI_RGB, reinterpret_cast<quint8 *>(&normalisedData[0]) + 16);
normalisedData += data.substr(sizeof(BITMAPINFOHEADER));
LOG_INFO("normalised malformed macOS clipboard image to BI_RGB");
return true;
}
} // namespace
// //
// MSWindowsClipboardBitmapConverter // MSWindowsClipboardBitmapConverter
// //
@ -27,13 +68,19 @@ UINT MSWindowsClipboardBitmapConverter::getWin32Format() const
HANDLE HANDLE
MSWindowsClipboardBitmapConverter::fromIClipboard(const std::string &data) const MSWindowsClipboardBitmapConverter::fromIClipboard(const std::string &data) const
{ {
std::string normalisedData;
const auto *clipboardData = &data;
if (normaliseMalformedMacDib(data, normalisedData)) {
clipboardData = &normalisedData;
}
// copy to memory handle // 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) { if (gData != nullptr) {
// get a pointer to the allocated memory // get a pointer to the allocated memory
char *dst = (char *)GlobalLock(gData); char *dst = (char *)GlobalLock(gData);
if (dst != nullptr) { if (dst != nullptr) {
memcpy(dst, data.data(), data.size()); memcpy(dst, clipboardData->data(), clipboardData->size());
GlobalUnlock(gData); GlobalUnlock(gData);
} else { } else {
GlobalFree(gData); GlobalFree(gData);

View file

@ -9,6 +9,9 @@
#include "MSWindowsClipboardTests.h" #include "MSWindowsClipboardTests.h"
#include "platform/MSWindowsClipboard.h" #include "platform/MSWindowsClipboard.h"
#include "platform/MSWindowsClipboardBitmapConverter.h"
#include <QtEndian>
void MSWindowsClipboardTests::initTestCase() void MSWindowsClipboardTests::initTestCase()
{ {
@ -136,4 +139,63 @@ void MSWindowsClipboardTests::isOwnedByDeskflow()
QVERIFY(clipboard.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<quint8 *>(&dib[0]);
qToLittleEndian<quint32>(sizeof(BITMAPV5HEADER), raw); // claims V5 but only has an INFOHEADER
qToLittleEndian<quint32>(1, raw + 4);
qToLittleEndian<quint32>(-1, raw + 8);
qToLittleEndian<quint16>(1, raw + 12);
qToLittleEndian<quint16>(32, raw + 14);
qToLittleEndian<quint32>(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<const quint8 *>(GlobalLock(handle));
QVERIFY(result != nullptr);
QCOMPARE(qFromLittleEndian<quint32>(result), quint32(headerSize));
QCOMPARE(qFromLittleEndian<quint32>(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<quint8 *>(&dib[0]);
qToLittleEndian<quint32>(headerSize, raw);
qToLittleEndian<quint32>(1, raw + 4);
qToLittleEndian<quint32>(-1, raw + 8);
qToLittleEndian<quint16>(1, raw + 12);
qToLittleEndian<quint16>(32, raw + 14);
qToLittleEndian<quint32>(BI_BITFIELDS, raw + 16);
qToLittleEndian<quint32>(0x00ff0000, raw + 40);
qToLittleEndian<quint32>(0x0000ff00, raw + 44);
qToLittleEndian<quint32>(0x000000ff, raw + 48);
qToLittleEndian<quint32>(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<const char *>(GlobalLock(handle));
QVERIFY(result != nullptr);
QCOMPARE(std::string(result, GlobalSize(handle)), dib);
GlobalUnlock(handle);
GlobalFree(handle);
}
QTEST_MAIN(MSWindowsClipboardTests) QTEST_MAIN(MSWindowsClipboardTests)

View file

@ -28,6 +28,8 @@ private Q_SLOTS:
void has_withNoFormatAdded(); void has_withNoFormatAdded();
void getNonEmptyText(); void getNonEmptyText();
void isOwnedByDeskflow(); void isOwnedByDeskflow();
void normalisesMalformedMacBitmap();
void preservesHealthyMacV5Bitmap();
private: private:
Log m_log; Log m_log;