diff --git a/client/displayservers/Windows/CMakeLists.txt b/client/displayservers/Windows/CMakeLists.txt index 108ddb0c..16a66942 100644 --- a/client/displayservers/Windows/CMakeLists.txt +++ b/client/displayservers/Windows/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.10) project(displayserver_Windows LANGUAGES C) add_library(displayserver_Windows STATIC + clipboard.c input_event.c window_size.c windows.c diff --git a/client/displayservers/Windows/clipboard.c b/client/displayservers/Windows/clipboard.c new file mode 100644 index 00000000..0ccf514d --- /dev/null +++ b/client/displayservers/Windows/clipboard.c @@ -0,0 +1,1198 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "clipboard.h" + +#include +#include +#include +#include +#include + +#include "common/debug.h" +#include "common/windebug.h" +#include "core/clipboard.h" +#include "platform.h" + +#define WINDOWS_CB_OPEN_ATTEMPTS 8 + +enum WindowsClipboardMessage +{ + WM_LG_CLIPBOARD_INIT = WM_APP + 0x100, + WM_LG_CLIPBOARD_DISPATCH = WM_APP + 0x101, +}; + +enum WindowsClipboardOperationType +{ + WINDOWS_CB_OPERATION_NOTICE, + WINDOWS_CB_OPERATION_RELEASE, + WINDOWS_CB_OPERATION_REQUEST, + WINDOWS_CB_OPERATION_DATA, +}; + +struct WindowsClipboardOperation +{ + struct WindowsClipboardOperation * next; + enum WindowsClipboardOperationType type; + uint64_t generation; + LG_ClipboardRequest request; + LG_ClipboardData dataType; + uint8_t * data; + size_t size; +}; + +struct WindowsClipboardTransfer +{ + uint64_t generation; + LG_ClipboardData type; + uint8_t * data; + size_t size; + size_t capacity; + bool started; +}; + +struct WindowsClipboardState +{ + struct WindowsClipboardOperation * queueHead; + struct WindowsClipboardOperation * queueTail; + HWND window; + HWND localOwner; + DWORD localSequence; + uint32_t localTypes; + UINT pngFormat; + UINT jfifFormat; + UINT jpegFormat; + LG_ClipboardRequest canceledRequest; + bool listener; + bool accepting; +}; + +static SRWLOCK windowsCBQueueLock = SRWLOCK_INIT; +static struct WindowsClipboardState windowsCB; +static _Atomic(uint64_t) windowsCBGeneration; + +static uint16_t readLE16(const uint8_t * data) +{ + return (uint16_t)data[0] | + (uint16_t)((uint16_t)data[1] << 8); +} + +static uint32_t readLE32(const uint8_t * data) +{ + return (uint32_t)data[0] | + (uint32_t)data[1] << 8 | + (uint32_t)data[2] << 16 | + (uint32_t)data[3] << 24; +} + +static void writeLE16(uint8_t * data, uint16_t value) +{ + data[0] = (uint8_t)value; + data[1] = (uint8_t)(value >> 8); +} + +static void writeLE32(uint8_t * data, uint32_t value) +{ + data[0] = (uint8_t)value; + data[1] = (uint8_t)(value >> 8); + data[2] = (uint8_t)(value >> 16); + data[3] = (uint8_t)(value >> 24); +} + +static void freeOperation(struct WindowsClipboardOperation * operation) +{ + if (!operation) + return; + + free(operation->data); + free(operation); +} + +static void freeOperations(struct WindowsClipboardOperation * operation) +{ + while (operation) + { + struct WindowsClipboardOperation * next = operation->next; + freeOperation(operation); + operation = next; + } +} + +static struct WindowsClipboardOperation * newOperation( + enum WindowsClipboardOperationType type) +{ + struct WindowsClipboardOperation * operation = + calloc(1, sizeof(*operation)); + if (!operation) + { + DEBUG_ERROR("Out of memory while queuing a Windows clipboard operation"); + return NULL; + } + + operation->type = type; + return operation; +} + +static bool queueOperation(struct WindowsClipboardOperation * operation) +{ + if (!operation) + return false; + + AcquireSRWLockExclusive(&windowsCBQueueLock); + if (!windowsCB.accepting || !windowsCB.window) + { + ReleaseSRWLockExclusive(&windowsCBQueueLock); + freeOperation(operation); + return false; + } + + const bool wake = !windowsCB.queueTail; + if (windowsCB.queueTail) + windowsCB.queueTail->next = operation; + else + windowsCB.queueHead = operation; + windowsCB.queueTail = operation; + + if (wake && !PostMessageW( + windowsCB.window, WM_LG_CLIPBOARD_DISPATCH, 0, 0)) + { + windowsCB.queueHead = NULL; + windowsCB.queueTail = NULL; + ReleaseSRWLockExclusive(&windowsCBQueueLock); + DEBUG_WINERROR("Failed to queue a Windows clipboard operation", + GetLastError()); + freeOperation(operation); + return false; + } + + ReleaseSRWLockExclusive(&windowsCBQueueLock); + return true; +} + +static struct WindowsClipboardOperation * takeOperation(void) +{ + AcquireSRWLockExclusive(&windowsCBQueueLock); + struct WindowsClipboardOperation * operation = windowsCB.queueHead; + if (operation) + { + windowsCB.queueHead = operation->next; + operation->next = NULL; + if (!windowsCB.queueHead) + windowsCB.queueTail = NULL; + } + ReleaseSRWLockExclusive(&windowsCBQueueLock); + return operation; +} + +static bool requestCanceled(LG_ClipboardRequest request) +{ + AcquireSRWLockShared(&windowsCBQueueLock); + const bool canceled = windowsCB.canceledRequest == request; + ReleaseSRWLockShared(&windowsCBQueueLock); + return canceled; +} + +static bool openClipboard(void) +{ + DWORD delay = 1; + for (unsigned int attempt = 0; + attempt < WINDOWS_CB_OPEN_ATTEMPTS; ++attempt) + { + if (OpenClipboard(windowsCB.window)) + return true; + + if (attempt + 1 < WINDOWS_CB_OPEN_ATTEMPTS) + { + Sleep(delay); + delay *= 2; + } + } + + DEBUG_WINERROR("Failed to open the Windows clipboard", GetLastError()); + return false; +} + +static bool checkedAdd(size_t a, size_t b, size_t * result) +{ + if (a > SIZE_MAX - b) + return false; + + *result = a + b; + return true; +} + +static bool uint64ToSize(uint64_t value, size_t * result) +{ + const size_t converted = (size_t)value; + if ((uint64_t)converted != value) + return false; + + *result = converted; + return true; +} + +static bool validateDIB(const uint8_t * data, size_t size, + size_t * pixelOffset) +{ + if (!data || size < sizeof(uint32_t)) + return false; + + const uint32_t headerSize = readLE32(data); + size_t offset; + if (headerSize == sizeof(BITMAPCOREHEADER)) + { + if (size < sizeof(BITMAPCOREHEADER) || !readLE16(data + 4) || + !readLE16(data + 6) || readLE16(data + 8) != 1) + return false; + + const uint16_t bits = readLE16(data + 10); + if (!bits || bits > 32) + return false; + + const size_t colors = bits <= 8 ? (size_t)1 << bits : 0; + if (colors > (SIZE_MAX - headerSize) / sizeof(RGBTRIPLE)) + return false; + offset = headerSize + colors * sizeof(RGBTRIPLE); + } + else + { + if (headerSize < sizeof(BITMAPINFOHEADER) || headerSize > size || + !readLE32(data + 4) || !readLE32(data + 8) || + readLE16(data + 12) != 1) + return false; + + const uint16_t bits = readLE16(data + 14); + const uint32_t compression = readLE32(data + 16); + const uint32_t imageSize = readLE32(data + 20); + const uint32_t usedColors = readLE32(data + 32); + if (bits > 32) + return false; + + size_t maskSize = 0; + if (headerSize == sizeof(BITMAPINFOHEADER)) + { + if (compression == BI_BITFIELDS) + maskSize = 3 * sizeof(uint32_t); +#ifdef BI_ALPHABITFIELDS + else if (compression == BI_ALPHABITFIELDS) + maskSize = 4 * sizeof(uint32_t); +#endif + } + + const size_t colors = usedColors ? usedColors : + (bits && bits <= 8 ? (size_t)1 << bits : 0); + if (!checkedAdd(headerSize, maskSize, &offset) || + colors > (SIZE_MAX - offset) / sizeof(RGBQUAD)) + return false; + offset += colors * sizeof(RGBQUAD); + if (imageSize && (offset > size || imageSize > size - offset)) + return false; + } + + if (offset > size) + return false; + + *pixelOffset = offset; + return true; +} + +static bool bmpToDIB(const void * source, size_t sourceSize, + uint8_t ** output, size_t * outputSize) +{ + const uint8_t * data = source; + if (!data || sourceSize < sizeof(BITMAPFILEHEADER) || + readLE16(data) != UINT16_C(0x4D42)) + return false; + + const uint32_t fileSizeValue = readLE32(data + 2); + const uint32_t fileOffset = readLE32(data + 10); + const size_t fileSize = fileSizeValue ? fileSizeValue : sourceSize; + if (fileSize > sourceSize || fileSize < sizeof(BITMAPFILEHEADER) || + fileOffset < sizeof(BITMAPFILEHEADER) || fileOffset > fileSize) + return false; + + const uint8_t * dib = data + sizeof(BITMAPFILEHEADER); + const size_t dibSize = fileSize - sizeof(BITMAPFILEHEADER); + size_t minimumOffset; + if (!validateDIB(dib, dibSize, &minimumOffset) || + fileOffset - sizeof(BITMAPFILEHEADER) < minimumOffset) + return false; + + uint8_t * copy = malloc(dibSize ? dibSize : 1); + if (!copy) + return false; + + if (dibSize) + memcpy(copy, dib, dibSize); + *output = copy; + *outputSize = dibSize; + return true; +} + +static bool dibToBMP(const void * source, size_t sourceSize, + uint8_t ** output, size_t * outputSize) +{ + const uint8_t * dib = source; + size_t dibOffset; + if (!validateDIB(dib, sourceSize, &dibOffset) || + sourceSize > UINT32_MAX - sizeof(BITMAPFILEHEADER)) + return false; + + const size_t size = sizeof(BITMAPFILEHEADER) + sourceSize; + uint8_t * bmp = malloc(size); + if (!bmp) + return false; + + memset(bmp, 0, sizeof(BITMAPFILEHEADER)); + writeLE16(bmp, UINT16_C(0x4D42)); + writeLE32(bmp + 2, (uint32_t)size); + writeLE32(bmp + 10, + (uint32_t)(sizeof(BITMAPFILEHEADER) + dibOffset)); + memcpy(bmp + sizeof(BITMAPFILEHEADER), dib, sourceSize); + *output = bmp; + *outputSize = size; + return true; +} + +static bool utf8ToClipboardText(const void * source, size_t sourceSize, + uint8_t ** output, size_t * outputSize) +{ + if ((sourceSize && !source) || sourceSize > INT_MAX || + (sourceSize && memchr(source, 0, sourceSize))) + return false; + + int sourceLength = 0; + if (sourceSize) + { + sourceLength = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + source, (int)sourceSize, NULL, 0); + if (sourceLength <= 0) + return false; + } + + wchar_t * wide = malloc( + ((size_t)sourceLength + 1) * sizeof(*wide)); + if (!wide) + return false; + if (sourceLength && MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + source, (int)sourceSize, wide, sourceLength) != sourceLength) + { + free(wide); + return false; + } + + size_t lineBreaks = 0; + for (int i = 0; i < sourceLength; ++i) + if (wide[i] == L'\n' && (!i || wide[i - 1] != L'\r')) + ++lineBreaks; + if ((size_t)sourceLength > + (SIZE_MAX / sizeof(wchar_t)) - lineBreaks - 1) + { + free(wide); + return false; + } + + const size_t count = (size_t)sourceLength + lineBreaks + 1; + wchar_t * text = malloc(count * sizeof(*text)); + if (!text) + { + free(wide); + return false; + } + + size_t at = 0; + for (int i = 0; i < sourceLength; ++i) + { + if (wide[i] == L'\n' && (!i || wide[i - 1] != L'\r')) + text[at++] = L'\r'; + text[at++] = wide[i]; + } + text[at++] = L'\0'; + free(wide); + + *output = (uint8_t *)text; + *outputSize = at * sizeof(*text); + return true; +} + +static bool clipboardTextToUtf8(const void * source, size_t sourceSize, + uint8_t ** output, size_t * outputSize) +{ + if (!source || sourceSize < sizeof(wchar_t) || + sourceSize % sizeof(wchar_t)) + return false; + + const wchar_t * text = source; + const size_t capacity = sourceSize / sizeof(*text); + size_t length = 0; + while (length < capacity && text[length]) + ++length; + if (length == capacity || length > INT_MAX) + return false; + + wchar_t * normalized = malloc((length + 1) * sizeof(*normalized)); + if (!normalized) + return false; + + size_t normalizedLength = 0; + for (size_t i = 0; i < length; ++i) + { + if (text[i] == L'\r' && i + 1 < length && text[i + 1] == L'\n') + { + ++i; + normalized[normalizedLength++] = L'\n'; + } + else + normalized[normalizedLength++] = text[i]; + } + + const int size = normalizedLength ? + WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + normalized, (int)normalizedLength, NULL, 0, NULL, NULL) : 0; + if (normalizedLength && size <= 0) + { + free(normalized); + return false; + } + + uint8_t * utf8 = size ? malloc((size_t)size) : NULL; + if (size && (!utf8 || WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + normalized, (int)normalizedLength, (char *)utf8, size, + NULL, NULL) != size)) + { + free(normalized); + free(utf8); + return false; + } + free(normalized); + + *output = utf8; + *outputSize = (size_t)size; + return true; +} + +static HGLOBAL makeGlobalData(const void * data, size_t size) +{ + if (size && !data) + return NULL; + + HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, size ? size : 1); + if (!handle) + return NULL; + + void * target = GlobalLock(handle); + if (!target) + { + GlobalFree(handle); + return NULL; + } + if (size) + memcpy(target, data, size); + GlobalUnlock(handle); + return handle; +} + +static bool copyGlobalData(HANDLE handle, uint8_t ** output, + size_t * outputSize) +{ + const SIZE_T size = GlobalSize(handle); + if (!size) + return false; + + const void * source = GlobalLock(handle); + if (!source) + return false; + + uint8_t * data = malloc(size); + if (data) + memcpy(data, source, size); + GlobalUnlock(handle); + if (!data) + return false; + + *output = data; + *outputSize = size; + return true; +} + +static bool localTypeAvailable(LG_ClipboardData type) +{ + switch (type) + { + case LG_CLIPBOARD_DATA_TEXT: + return IsClipboardFormatAvailable(CF_UNICODETEXT); + + case LG_CLIPBOARD_DATA_PNG: + return windowsCB.pngFormat && + IsClipboardFormatAvailable(windowsCB.pngFormat); + + case LG_CLIPBOARD_DATA_BMP: + return IsClipboardFormatAvailable(CF_DIBV5) || + IsClipboardFormatAvailable(CF_DIB); + + case LG_CLIPBOARD_DATA_TIFF: + return IsClipboardFormatAvailable(CF_TIFF); + + case LG_CLIPBOARD_DATA_JPEG: + return (windowsCB.jfifFormat && + IsClipboardFormatAvailable(windowsCB.jfifFormat)) || + (windowsCB.jpegFormat && + IsClipboardFormatAvailable(windowsCB.jpegFormat)); + + case LG_CLIPBOARD_DATA_FILES: + case LG_CLIPBOARD_DATA_NONE: + return false; + } + + return false; +} + +static UINT localClipboardFormat(LG_ClipboardData type) +{ + switch (type) + { + case LG_CLIPBOARD_DATA_TEXT: + return CF_UNICODETEXT; + case LG_CLIPBOARD_DATA_PNG: + return windowsCB.pngFormat; + case LG_CLIPBOARD_DATA_BMP: + if (IsClipboardFormatAvailable(CF_DIBV5)) + return CF_DIBV5; + return CF_DIB; + case LG_CLIPBOARD_DATA_TIFF: + return CF_TIFF; + case LG_CLIPBOARD_DATA_JPEG: + if (windowsCB.jfifFormat && + IsClipboardFormatAvailable(windowsCB.jfifFormat)) + return windowsCB.jfifFormat; + return windowsCB.jpegFormat; + case LG_CLIPBOARD_DATA_FILES: + case LG_CLIPBOARD_DATA_NONE: + return 0; + } + + return 0; +} + +static bool readLocalClipboard(LG_ClipboardData type, + uint8_t ** output, size_t * outputSize) +{ + const UINT format = localClipboardFormat(type); + if (!format) + return false; + + HANDLE handle = GetClipboardData(format); + if (!handle && type == LG_CLIPBOARD_DATA_BMP && format == CF_DIBV5 && + IsClipboardFormatAvailable(CF_DIB)) + handle = GetClipboardData(CF_DIB); + else if (!handle && type == LG_CLIPBOARD_DATA_JPEG) + { + const UINT alternate = format == windowsCB.jfifFormat ? + windowsCB.jpegFormat : windowsCB.jfifFormat; + if (alternate && IsClipboardFormatAvailable(alternate)) + handle = GetClipboardData(alternate); + } + if (!handle) + return false; + + uint8_t * data = NULL; + size_t size = 0; + if (type == LG_CLIPBOARD_DATA_TEXT) + { + const SIZE_T globalSize = GlobalSize(handle); + const void * source = globalSize ? GlobalLock(handle) : NULL; + const bool result = source && clipboardTextToUtf8( + source, globalSize, &data, &size); + if (source) + GlobalUnlock(handle); + if (!result) + return false; + } + else if (type == LG_CLIPBOARD_DATA_BMP) + { + const SIZE_T sourceSize = GlobalSize(handle); + if (!sourceSize || + sourceSize > UINT32_MAX - sizeof(BITMAPFILEHEADER)) + return false; + + uint8_t * dib = NULL; + size_t dibSize = 0; + if (!copyGlobalData(handle, &dib, &dibSize)) + return false; + const bool result = dibToBMP(dib, dibSize, &data, &size); + free(dib); + if (!result) + return false; + } + else if (!copyGlobalData(handle, &data, &size)) + return false; + + *output = data; + *outputSize = size; + return true; +} + +static void updateLocalClipboard(void) +{ + const HWND owner = GetClipboardOwner(); + const DWORD sequence = GetClipboardSequenceNumber(); + if (owner == windowsCB.window) + return; + if (owner == windowsCB.localOwner && + sequence == windowsCB.localSequence) + return; + + LG_ClipboardData types[LG_CLIPBOARD_DATA_NONE]; + size_t count = 0; + uint32_t mask = 0; + for (LG_ClipboardData type = LG_CLIPBOARD_DATA_TEXT; + type < LG_CLIPBOARD_DATA_FILES; ++type) + { + if (!localTypeAvailable(type)) + continue; + + types[count++] = type; + mask |= UINT32_C(1) << type; + } + + windowsCB.localOwner = owner; + windowsCB.localSequence = sequence; + windowsCB.localTypes = mask; + atomic_fetch_add_explicit( + &windowsCBGeneration, 1, memory_order_acq_rel); + if (count) + clipboard_notifyTypes(types, count); + else + clipboard_release(); +} + +static void handleLocalRequest( + const struct WindowsClipboardOperation * operation) +{ + const LG_ClipboardData type = operation->dataType; + const uint32_t typeMask = type < LG_CLIPBOARD_DATA_NONE ? + UINT32_C(1) << type : 0; + if (requestCanceled(operation->request)) + return; + if (!typeMask || !(windowsCB.localTypes & typeMask) || + windowsCB.localSequence != GetClipboardSequenceNumber() || + windowsCB.localOwner != GetClipboardOwner() || !openClipboard()) + { + clipboard_abort(operation->request); + return; + } + + const bool current = windowsCB.localSequence == + GetClipboardSequenceNumber() && + windowsCB.localOwner == GetClipboardOwner(); + uint8_t * data = NULL; + size_t size = 0; + const bool read = current && readLocalClipboard(type, + &data, &size); + const HWND owner = GetClipboardOwner(); + const DWORD sequence = GetClipboardSequenceNumber(); + const bool sequenceChanged = sequence != windowsCB.localSequence; + CloseClipboard(); + const bool unchanged = owner == GetClipboardOwner() && + sequence == GetClipboardSequenceNumber(); + if (requestCanceled(operation->request)) + { + free(data); + return; + } + if (!read || (sequenceChanged && !owner) || !unchanged || + owner != windowsCB.localOwner) + { + free(data); + clipboard_abort(operation->request); + return; + } + + windowsCB.localSequence = sequence; + clipboard_data(operation->request, type, data, size); + free(data); +} + +static bool prepareRemoteData(LG_ClipboardData type, + const void * source, size_t sourceSize, UINT formats[2], + HGLOBAL handles[2], size_t * count) +{ + if (sourceSize && !source) + return false; + + uint8_t * converted = NULL; + size_t convertedSize = 0; + const void * data = source; + size_t size = sourceSize; + + switch (type) + { + case LG_CLIPBOARD_DATA_TEXT: + if (!utf8ToClipboardText( + source, sourceSize, &converted, &convertedSize)) + return false; + data = converted; + size = convertedSize; + formats[0] = CF_UNICODETEXT; + *count = 1; + break; + + case LG_CLIPBOARD_DATA_PNG: + if (!windowsCB.pngFormat) + return false; + formats[0] = windowsCB.pngFormat; + *count = 1; + break; + + case LG_CLIPBOARD_DATA_BMP: + if (!bmpToDIB(source, sourceSize, &converted, &convertedSize)) + return false; + data = converted; + size = convertedSize; + formats[0] = CF_DIB; + *count = 1; + break; + + case LG_CLIPBOARD_DATA_TIFF: + formats[0] = CF_TIFF; + *count = 1; + break; + + case LG_CLIPBOARD_DATA_JPEG: + if (!windowsCB.jfifFormat && !windowsCB.jpegFormat) + return false; + if (windowsCB.jfifFormat) + formats[(*count)++] = windowsCB.jfifFormat; + if (windowsCB.jpegFormat) + formats[(*count)++] = windowsCB.jpegFormat; + break; + + case LG_CLIPBOARD_DATA_FILES: + case LG_CLIPBOARD_DATA_NONE: + return false; + } + + bool success = true; + for (size_t i = 0; i < *count; ++i) + { + handles[i] = makeGlobalData(data, size); + if (!handles[i]) + { + success = false; + break; + } + } + free(converted); + if (success) + return true; + + for (size_t i = 0; i < *count; ++i) + if (handles[i]) + GlobalFree(handles[i]); + return false; +} + +static void handleRemoteData( + const struct WindowsClipboardOperation * operation) +{ + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire)) + return; + + UINT formats[2] = { 0 }; + HGLOBAL handles[2] = { NULL }; + size_t count = 0; + if (!prepareRemoteData(operation->dataType, + operation->data, operation->size, formats, handles, &count)) + { + DEBUG_WARN("Ignoring invalid guest clipboard data"); + return; + } + + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire) || !openClipboard()) + goto fail; + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire) || !EmptyClipboard()) + { + CloseClipboard(); + goto fail; + } + + bool published = false; + for (size_t i = 0; i < count; ++i) + { + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire)) + break; + + if (SetClipboardData(formats[i], handles[i])) + { + handles[i] = NULL; + published = true; + } + else + DEBUG_WINERROR("Failed to publish Windows clipboard data", + GetLastError()); + } + CloseClipboard(); + windowsCB.localOwner = GetClipboardOwner(); + windowsCB.localSequence = GetClipboardSequenceNumber(); + windowsCB.localTypes = 0; + if (!published) + DEBUG_WARN("Failed to publish any Windows clipboard format"); + +fail: + for (size_t i = 0; i < count; ++i) + if (handles[i]) + GlobalFree(handles[i]); +} + +static void handleRemoteRelease( + const struct WindowsClipboardOperation * operation) +{ + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire) || + GetClipboardOwner() != windowsCB.window || !openClipboard()) + return; + + bool released = false; + if (operation->generation == atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire) && + GetClipboardOwner() == windowsCB.window) + released = EmptyClipboard(); + CloseClipboard(); + if (released) + { + windowsCB.localOwner = GetClipboardOwner(); + windowsCB.localSequence = GetClipboardSequenceNumber(); + windowsCB.localTypes = 0; + } +} + +static bool growTransfer(struct WindowsClipboardTransfer * transfer, + size_t wanted) +{ + if (wanted <= transfer->capacity) + return true; + + size_t capacity = transfer->capacity ? transfer->capacity : 4096; + while (capacity < wanted) + { + if (capacity > SIZE_MAX / 2) + { + capacity = wanted; + break; + } + capacity *= 2; + } + + uint8_t * data = realloc(transfer->data, capacity); + if (!data) + return false; + transfer->data = data; + transfer->capacity = capacity; + return true; +} + +static void freeTransfer(struct WindowsClipboardTransfer * transfer) +{ + if (!transfer) + return; + + free(transfer->data); + free(transfer); +} + +static LG_ClipboardResult remoteDataBegin(void * opaque, + LG_ClipboardData type, uint64_t sizeHint) +{ + struct WindowsClipboardTransfer * transfer = opaque; + size_t size = 0; + if (transfer->started || type != transfer->type || + (sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && + !uint64ToSize(sizeHint, &size))) + return LG_CLIPBOARD_RESULT_FAILED; + + transfer->started = true; + if (sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && size && + !growTransfer(transfer, size)) + return LG_CLIPBOARD_RESULT_FAILED; + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult remoteDataChunk(void * opaque, uint64_t offset, + const void * data, size_t size) +{ + struct WindowsClipboardTransfer * transfer = opaque; + if (!transfer->started || offset != transfer->size || !data || !size || + size > SIZE_MAX - transfer->size || + !growTransfer(transfer, transfer->size + size)) + return LG_CLIPBOARD_RESULT_FAILED; + + memcpy(transfer->data + transfer->size, data, size); + transfer->size += size; + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult remoteDataEnd(void * opaque, uint64_t finalSize) +{ + struct WindowsClipboardTransfer * transfer = opaque; + if (!transfer->started || finalSize != transfer->size) + return LG_CLIPBOARD_RESULT_FAILED; + + struct WindowsClipboardOperation * operation = + newOperation(WINDOWS_CB_OPERATION_DATA); + if (!operation) + return LG_CLIPBOARD_RESULT_FAILED; + + operation->generation = transfer->generation; + operation->dataType = transfer->type; + operation->data = transfer->data; + operation->size = transfer->size; + transfer->data = NULL; + if (!queueOperation(operation)) + return LG_CLIPBOARD_RESULT_FAILED; + freeTransfer(transfer); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static void remoteDataCancel(void * opaque, + LG_ClipboardCancelReason reason) +{ + (void)reason; + freeTransfer(opaque); +} + +static const LG_ClipboardStreamOps remoteStream = +{ + .begin = remoteDataBegin, + .chunk = remoteDataChunk, + .end = remoteDataEnd, + .cancel = remoteDataCancel, +}; + +static void handleRemoteNotice( + const struct WindowsClipboardOperation * operation) +{ + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire)) + return; + if (operation->dataType == LG_CLIPBOARD_DATA_FILES) + { + handleRemoteRelease(operation); + return; + } + if (operation->dataType >= LG_CLIPBOARD_DATA_NONE) + return; + + struct WindowsClipboardTransfer * transfer = + calloc(1, sizeof(*transfer)); + if (!transfer) + { + DEBUG_ERROR("Out of memory while requesting guest clipboard data"); + return; + } + + LG_ClipboardRequest request; + transfer->generation = operation->generation; + transfer->type = operation->dataType; + if (!clipboard_requestStream( + transfer->type, &remoteStream, transfer, &request)) + freeTransfer(transfer); +} + +static void dispatchOperations(void) +{ + struct WindowsClipboardOperation * operation; + while ((operation = takeOperation())) + { + switch (operation->type) + { + case WINDOWS_CB_OPERATION_NOTICE: + handleRemoteNotice(operation); + break; + + case WINDOWS_CB_OPERATION_RELEASE: + handleRemoteRelease(operation); + break; + + case WINDOWS_CB_OPERATION_REQUEST: + handleLocalRequest(operation); + break; + + case WINDOWS_CB_OPERATION_DATA: + handleRemoteData(operation); + break; + } + + freeOperation(operation); + } +} + +static bool eventInit(HWND window) +{ + if (!window) + return false; + + windowsCB.pngFormat = RegisterClipboardFormatW(L"PNG"); + windowsCB.jfifFormat = RegisterClipboardFormatW(L"JFIF"); + windowsCB.jpegFormat = RegisterClipboardFormatW(L"JPEG"); + if (!windowsCB.pngFormat || !windowsCB.jfifFormat || + !windowsCB.jpegFormat) + { + DEBUG_WINERROR("Failed to register Windows clipboard formats", + GetLastError()); + return false; + } + + if (!AddClipboardFormatListener(window)) + { + DEBUG_WINERROR("Failed to register the Windows clipboard listener", + GetLastError()); + return false; + } + + AcquireSRWLockExclusive(&windowsCBQueueLock); + windowsCB.window = window; + windowsCB.listener = true; + windowsCB.accepting = true; + ReleaseSRWLockExclusive(&windowsCBQueueLock); + atomic_store_explicit(&windowsCBGeneration, 0, memory_order_release); + updateLocalClipboard(); + return true; +} + +bool windowsCBHandleMessage(HWND window, UINT message, + WPARAM wParam, LPARAM lParam, LRESULT * result) +{ + (void)wParam; + (void)lParam; + + switch (message) + { + case WM_LG_CLIPBOARD_INIT: + *result = eventInit(window); + return true; + + case WM_LG_CLIPBOARD_DISPATCH: + dispatchOperations(); + *result = 0; + return true; + + case WM_CLIPBOARDUPDATE: + if (windowsCB.accepting) + updateLocalClipboard(); + *result = 0; + return true; + } + + return false; +} + +void windowsCBEventShutdown(void) +{ + HWND window; + bool listener; + struct WindowsClipboardOperation * operations; + + AcquireSRWLockExclusive(&windowsCBQueueLock); + window = windowsCB.window; + listener = windowsCB.listener; + operations = windowsCB.queueHead; + windowsCB.accepting = false; + windowsCB.listener = false; + windowsCB.window = NULL; + windowsCB.queueHead = NULL; + windowsCB.queueTail = NULL; + ReleaseSRWLockExclusive(&windowsCBQueueLock); + + atomic_fetch_add_explicit( + &windowsCBGeneration, 1, memory_order_acq_rel); + if (listener && window && !RemoveClipboardFormatListener(window)) + DEBUG_WINERROR("Failed to remove the Windows clipboard listener", + GetLastError()); + freeOperations(operations); +} + +bool windowsCBInit(void) +{ + const HWND window = windowsGetWindow(); + return window && SendMessageW( + window, WM_LG_CLIPBOARD_INIT, 0, 0) != 0; +} + +void windowsCBNotice(LG_ClipboardData type) +{ + const uint64_t generation = atomic_fetch_add_explicit( + &windowsCBGeneration, 1, memory_order_acq_rel) + 1; + struct WindowsClipboardOperation * operation = + newOperation(WINDOWS_CB_OPERATION_NOTICE); + if (!operation) + return; + + operation->generation = generation; + operation->dataType = type; + queueOperation(operation); +} + +void windowsCBRelease(void) +{ + const uint64_t generation = atomic_fetch_add_explicit( + &windowsCBGeneration, 1, memory_order_acq_rel) + 1; + struct WindowsClipboardOperation * operation = + newOperation(WINDOWS_CB_OPERATION_RELEASE); + if (!operation) + return; + + operation->generation = generation; + queueOperation(operation); +} + +void windowsCBRequest(LG_ClipboardRequest request, LG_ClipboardData type) +{ + struct WindowsClipboardOperation * operation = + newOperation(WINDOWS_CB_OPERATION_REQUEST); + if (!operation) + { + clipboard_abort(request); + return; + } + + operation->request = request; + operation->dataType = type; + if (!queueOperation(operation)) + clipboard_abort(request); +} + +void windowsCBRequestReady(LG_ClipboardRequest request) +{ + (void)request; +} + +void windowsCBRequestCancel(LG_ClipboardRequest request, + LG_ClipboardCancelReason reason) +{ + (void)reason; + + AcquireSRWLockExclusive(&windowsCBQueueLock); + windowsCB.canceledRequest = request; + ReleaseSRWLockExclusive(&windowsCBQueueLock); +} diff --git a/client/displayservers/Windows/clipboard.h b/client/displayservers/Windows/clipboard.h new file mode 100644 index 00000000..3342e82c --- /dev/null +++ b/client/displayservers/Windows/clipboard.h @@ -0,0 +1,42 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _H_WINDOWSDS_CLIPBOARD_ +#define _H_WINDOWSDS_CLIPBOARD_ + +#include + +#include + +#include "interface/displayserver.h" + +bool windowsCBHandleMessage(HWND window, UINT message, + WPARAM wParam, LPARAM lParam, LRESULT * result); +void windowsCBEventShutdown(void); + +bool windowsCBInit(void); +void windowsCBNotice(LG_ClipboardData type); +void windowsCBRelease(void); +void windowsCBRequest(LG_ClipboardRequest request, LG_ClipboardData type); +void windowsCBRequestReady(LG_ClipboardRequest request); +void windowsCBRequestCancel(LG_ClipboardRequest request, + LG_ClipboardCancelReason reason); + +#endif diff --git a/client/displayservers/Windows/windows.c b/client/displayservers/Windows/windows.c index 4ce064f5..79612cc3 100644 --- a/client/displayservers/Windows/windows.c +++ b/client/displayservers/Windows/windows.c @@ -54,6 +54,8 @@ #include #include +#include "clipboard.h" + #define LG_WINDOW_CLASS L"LookingGlassClientWindow" enum WindowsMessage @@ -849,6 +851,7 @@ static LRESULT handleCustomMessage(UINT message, WPARAM wParam, switch (message) { case WM_LG_SHUTDOWN: + windowsCBEventShutdown(); eventSourceFree(); setKeyboardGrab(false); setPointerGrab(false, false); @@ -930,6 +933,11 @@ static LRESULT handleCustomMessage(UINT message, WPARAM wParam, static LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam) { + LRESULT clipboardResult; + if (windowsCBHandleMessage( + window, message, wParam, lParam, &clipboardResult)) + return clipboardResult; + if (message >= WM_LG_SHUTDOWN && message <= WM_LG_EVENT_SOURCE) return handleCustomMessage(message, wParam, lParam); @@ -1887,4 +1895,10 @@ struct LG_DisplayServerOps LGDS_Windows = .getFullscreen = windowsGetFullscreen, .setFullscreen = windowsSetFullscreen, .minimize = windowsMinimize, + .cbInit = windowsCBInit, + .cbNotice = windowsCBNotice, + .cbRelease = windowsCBRelease, + .cbRequest = windowsCBRequest, + .cbRequestReady = windowsCBRequestReady, + .cbRequestCancel = windowsCBRequestCancel, };