/** * 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 "CClipboardManager.h" #include #include #include #include #include #include #include namespace { static constexpr size_t MEMORY_SPOOL_LIMIT = 1024U * 1024U; static constexpr size_t TEXT_CONVERSION_CHUNK = 16384; static constexpr uint64_t MAX_SPOOL_BYTES = UINT64_C(512) * 1024U * 1024U; static constexpr DWORD CHANNEL_RETRY_MS = 10; static constexpr DWORD REMOTE_RETRY_MS = 250; static constexpr DWORD REMOTE_RETRY_TIMEOUT_MS = 5000; static constexpr UINT_PTR REMOTE_RETRY_TIMER = 0x4c47; static constexpr uint32_t ORIGIN_MAGIC = 0x4c47434fU; // BI_ALPHABITFIELDS is a serialized DIB value, but desktop SDKs omit it. static constexpr DWORD DIB_ALPHA_BITFIELDS = 6U; struct ClipboardOrigin { uint32_t magic; uint32_t reserved; uint64_t epoch; uint64_t generation; }; bool WriteAll(HANDLE file, const void * data, size_t size) { const uint8_t * current = static_cast(data); while (size) { const DWORD chunk = static_cast((std::min)( size, (std::numeric_limits::max)())); DWORD written = 0; if (!WriteFile(file, current, chunk, &written, nullptr)) return false; if (written != chunk) { SetLastError(ERROR_WRITE_FAULT); return false; } current += written; size -= written; } return true; } bool ReadAll(HANDLE file, void * data, size_t size) { uint8_t * current = static_cast(data); while (size) { const DWORD chunk = static_cast((std::min)( size, (std::numeric_limits::max)())); DWORD read = 0; if (!ReadFile(file, current, chunk, &read, nullptr)) return false; if (read != chunk) { SetLastError(ERROR_HANDLE_EOF); return false; } current += read; size -= read; } return true; } void AppendUTF8(std::vector& output, uint32_t codepoint) { if (codepoint <= 0x7f) output.push_back(static_cast(codepoint)); else if (codepoint <= 0x7ff) { output.push_back(static_cast(0xc0 | (codepoint >> 6))); output.push_back(static_cast(0x80 | (codepoint & 0x3f))); } else if (codepoint <= 0xffff) { output.push_back(static_cast(0xe0 | (codepoint >> 12))); output.push_back(static_cast(0x80 | ((codepoint >> 6) & 0x3f))); output.push_back(static_cast(0x80 | (codepoint & 0x3f))); } else { output.push_back(static_cast(0xf0 | (codepoint >> 18))); output.push_back(static_cast(0x80 | ((codepoint >> 12) & 0x3f))); output.push_back(static_cast(0x80 | ((codepoint >> 6) & 0x3f))); output.push_back(static_cast(0x80 | (codepoint & 0x3f))); } } uint32_t DIBPixelOffset(const uint8_t * data, size_t size) { if (size < sizeof(DWORD)) return 0; const DWORD headerSize = *reinterpret_cast(data); if (headerSize < sizeof(BITMAPCOREHEADER) || headerSize > size) return 0; uint64_t offset = headerSize; if (headerSize == sizeof(BITMAPCOREHEADER)) { const BITMAPCOREHEADER * header = reinterpret_cast(data); if (header->bcBitCount <= 8) offset += (1ULL << header->bcBitCount) * sizeof(RGBTRIPLE); } else if (headerSize >= sizeof(BITMAPINFOHEADER)) { const BITMAPINFOHEADER * header = reinterpret_cast(data); if (headerSize == sizeof(BITMAPINFOHEADER) && (header->biCompression == BI_BITFIELDS || header->biCompression == DIB_ALPHA_BITFIELDS)) offset += header->biCompression == DIB_ALPHA_BITFIELDS ? 16U : 12U; const uint32_t colors = header->biClrUsed ? header->biClrUsed : (header->biBitCount <= 8 ? 1U << header->biBitCount : 0U); offset += static_cast(colors) * sizeof(RGBQUAD); } else return 0; return offset <= size && offset <= (std::numeric_limits::max)() ? static_cast(offset) : 0; } } class CClipboardSpool { private: std::vector m_memory; HANDLE m_file = INVALID_HANDLE_VALUE; uint64_t m_size = 0; bool Spill() { WCHAR path[MAX_PATH]; const DWORD length = GetTempPathW(ARRAYSIZE(path), path); if (!length) return false; if (length >= ARRAYSIZE(path)) { SetLastError(ERROR_INSUFFICIENT_BUFFER); return false; } for (unsigned int attempt = 0; attempt != 32 && m_file == INVALID_HANDLE_VALUE; ++attempt) { WCHAR name[MAX_PATH]; const uint64_t nonce = GetTickCount64() ^ (static_cast(GetCurrentProcessId()) << 32) ^ attempt; if (FAILED(StringCchPrintfW(name, ARRAYSIZE(name), L"%sLookingGlassClipboard-%08x-%016llx.tmp", path, GetCurrentProcessId(), nonce))) { SetLastError(ERROR_INSUFFICIENT_BUFFER); return false; } m_file = CreateFileW(name, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE | FILE_FLAG_SEQUENTIAL_SCAN, nullptr); } if (m_file == INVALID_HANDLE_VALUE) return false; if (!m_memory.empty() && !WriteAll(m_file, m_memory.data(), m_memory.size())) { const DWORD error = GetLastError(); CloseHandle(m_file); m_file = INVALID_HANDLE_VALUE; SetLastError(error ? error : ERROR_WRITE_FAULT); return false; } std::vector().swap(m_memory); return true; } public: ~CClipboardSpool() { if (m_file != INVALID_HANDLE_VALUE) CloseHandle(m_file); } bool Append(const void * data, size_t size) { if (!size) return true; if (size > MAX_SPOOL_BYTES || m_size > MAX_SPOOL_BYTES - size) { SetLastError(ERROR_FILE_TOO_LARGE); return false; } const uint64_t nextSize = m_size + size; if (m_file == INVALID_HANDLE_VALUE && nextSize <= MEMORY_SPOOL_LIMIT) { try { const uint8_t * bytes = static_cast(data); m_memory.insert(m_memory.end(), bytes, bytes + size); } catch (const std::bad_alloc&) { SetLastError(ERROR_OUTOFMEMORY); return false; } m_size = nextSize; return true; } if (m_file == INVALID_HANDLE_VALUE && !Spill()) return false; LARGE_INTEGER position = {}; position.QuadPart = m_size; if (!SetFilePointerEx(m_file, position, nullptr, FILE_BEGIN) || !WriteAll(m_file, data, size)) return false; m_size = nextSize; return true; } bool Read(uint64_t offset, void * data, size_t size) { if (offset > m_size || size > m_size - offset) { SetLastError(ERROR_INVALID_DATA); return false; } if (!size) return true; if (m_file == INVALID_HANDLE_VALUE) { memcpy(data, m_memory.data() + static_cast(offset), size); return true; } LARGE_INTEGER position = {}; position.QuadPart = offset; return SetFilePointerEx(m_file, position, nullptr, FILE_BEGIN) && ReadAll(m_file, data, size); } uint64_t Size() const { return m_size; } }; namespace { template bool ForEachUTF8(CClipboardSpool& spool, Callback callback) { uint32_t codepoint = 0; uint32_t minimum = 0; unsigned int remaining = 0; std::vector buffer; try { buffer.resize(KVMFR_CLIPBOARD_DATA_BYTES); } catch (const std::bad_alloc&) { SetLastError(ERROR_OUTOFMEMORY); return false; } for (uint64_t offset = 0; offset < spool.Size();) { const size_t length = static_cast((std::min)( buffer.size(), spool.Size() - offset)); if (!spool.Read(offset, buffer.data(), length)) return false; offset += length; for (size_t index = 0; index < length; ++index) { uint8_t value = buffer[index]; if (!remaining) { if (value < 0x80) { if (!callback(value)) return false; } else if (value >= 0xc2 && value <= 0xdf) { codepoint = value & 0x1f; minimum = 0x80; remaining = 1; } else if (value >= 0xe0 && value <= 0xef) { codepoint = value & 0x0f; minimum = 0x800; remaining = 2; } else if (value >= 0xf0 && value <= 0xf4) { codepoint = value & 0x07; minimum = 0x10000; remaining = 3; } else if (!callback(0xfffd)) return false; continue; } if ((value & 0xc0) != 0x80) { if (!callback(0xfffd)) return false; codepoint = 0; minimum = 0; remaining = 0; --index; continue; } codepoint = (codepoint << 6) | (value & 0x3f); if (--remaining) continue; if (codepoint < minimum || codepoint > 0x10ffff || (codepoint >= 0xd800 && codepoint <= 0xdfff)) codepoint = 0xfffd; if (!callback(codepoint)) return false; codepoint = 0; minimum = 0; } } return !remaining || callback(0xfffd); } HGLOBAL UnicodeFromUTF8(CClipboardSpool& spool) { uint64_t units = 0; uint32_t previous = 0; SetLastError(ERROR_SUCCESS); if (!ForEachUTF8(spool, [&units, &previous](uint32_t codepoint) { if (codepoint == '\n' && previous != '\r') ++units; units += codepoint > 0xffff ? 2U : 1U; previous = codepoint; return units < (std::numeric_limits::max)() / sizeof(wchar_t); })) { if (!GetLastError()) SetLastError(ERROR_FILE_TOO_LARGE); return nullptr; } ++units; if (units > (std::numeric_limits::max)() / sizeof(wchar_t)) { SetLastError(ERROR_FILE_TOO_LARGE); return nullptr; } HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, static_cast(units * sizeof(wchar_t))); if (!memory) return nullptr; wchar_t * output = static_cast(GlobalLock(memory)); if (!output) { GlobalFree(memory); return nullptr; } uint64_t index = 0; previous = 0; const bool valid = ForEachUTF8(spool, [&output, &index, &previous](uint32_t codepoint) { const uint32_t original = codepoint; if (codepoint == '\n' && previous != '\r') output[index++] = L'\r'; if (codepoint > 0xffff) { codepoint -= 0x10000; output[index++] = static_cast(0xd800 + (codepoint >> 10)); output[index++] = static_cast(0xdc00 + (codepoint & 0x3ff)); } else output[index++] = static_cast(codepoint); previous = original; return true; }); if (valid) output[index] = L'\0'; GlobalUnlock(memory); if (!valid) { const DWORD error = GetLastError(); GlobalFree(memory); SetLastError(error ? error : ERROR_INVALID_DATA); return nullptr; } return memory; } HGLOBAL CopySpoolToGlobal(CClipboardSpool& spool, uint64_t offset) { if (offset > spool.Size()) { SetLastError(ERROR_INVALID_DATA); return nullptr; } const uint64_t length = spool.Size() - offset; if (length > (std::numeric_limits::max)()) { SetLastError(ERROR_FILE_TOO_LARGE); return nullptr; } HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, static_cast(length)); if (!memory) return nullptr; void * output = GlobalLock(memory); if (!output || !spool.Read(offset, output, static_cast(length))) { const DWORD error = GetLastError(); if (output) GlobalUnlock(memory); GlobalFree(memory); SetLastError(error ? error : ERROR_READ_FAULT); return nullptr; } GlobalUnlock(memory); return memory; } } CClipboardManager::IncomingTransfer::~IncomingTransfer() { if (event) CloseHandle(event); } CClipboardManager::CClipboardManager(HWND hwnd, CClipboardChannel& channel) : m_hwnd(hwnd), m_channel(channel) { } CClipboardManager::~CClipboardManager() { Shutdown(); } bool CClipboardManager::Initialize() { m_formatPNG = RegisterClipboardFormatW(L"PNG"); m_formatJPEG = RegisterClipboardFormatW(L"JFIF"); m_formatOrigin = RegisterClipboardFormatW(L"LookingGlassClipboardOrigin"); if (!m_formatPNG || !m_formatJPEG || !m_formatOrigin) { DEBUG_ERROR_HR(GetLastError(), "Failed to register clipboard formats"); return false; } Atomic::Store(m_shutdown, false); m_stop = CreateEventW(nullptr, TRUE, FALSE, nullptr); m_wake = CreateEventW(nullptr, FALSE, FALSE, nullptr); if (!m_stop || !m_wake) { DEBUG_ERROR_HR(GetLastError(), "Failed to create clipboard events"); Shutdown(); return false; } m_thread = CreateThread(nullptr, 0, ThreadProc, this, 0, nullptr); if (!m_thread) { DEBUG_ERROR_HR(GetLastError(), "Failed to create clipboard worker"); Shutdown(); return false; } if (!AddClipboardFormatListener(m_hwnd)) { DEBUG_ERROR_HR(GetLastError(), "Failed to register the clipboard listener"); Shutdown(); return false; } m_listener = true; m_channel.SetHandler(this); return true; } void CClipboardManager::Shutdown() { if (Atomic::Swap(m_shutdown, true)) return; m_channel.ClearHandler(this); if (m_listener && m_hwnd) RemoveClipboardFormatListener(m_hwnd); m_listener = false; if (m_hwnd) KillTimer(m_hwnd, REMOTE_RETRY_TIMER); CancelIncoming(ERROR_OPERATION_ABORTED); if (m_stop) SetEvent(m_stop); if (m_thread) WaitForSingleObject(m_thread, INFINITE); if (m_thread) CloseHandle(m_thread); if (m_wake) CloseHandle(m_wake); if (m_stop) CloseHandle(m_stop); { std::lock_guard lock(m_workLock); for (size_t i = 0; i < m_recordWorkCount; ++i) m_recordWork[i].reset(); for (size_t i = 0; i < m_sendWorkCount; ++i) m_sendWork[i].reset(); m_recordWorkCount = 0; m_sendWorkCount = 0; } m_thread = nullptr; m_wake = nullptr; m_stop = nullptr; } DWORD WINAPI CClipboardManager::ThreadProc(void * context) { static_cast(context)->Thread(); return 0; } void CClipboardManager::Thread() { HANDLE events[] = { m_stop, m_wake }; for (;;) { const DWORD result = WaitForMultipleObjects( ARRAYSIZE(events), events, FALSE, INFINITE); if (result == WAIT_OBJECT_0) return; if (result != WAIT_OBJECT_0 + 1) return; for (;;) { Work work; bool haveWork = false; { std::lock_guard lock(m_workLock); if (m_pendingControlCount) { work.type = m_pendingControl[0].type; work.available = m_pendingControl[0].available; work.epoch = m_pendingControl[0].epoch; work.reason = m_pendingControl[0].reason; for (size_t i = 1; i < m_pendingControlCount; ++i) m_pendingControl[i - 1] = m_pendingControl[i]; --m_pendingControlCount; haveWork = true; } if (!haveWork && m_recordWorkCount) { work = std::move(*m_recordWork[0]); for (size_t i = 1; i < m_recordWorkCount; ++i) m_recordWork[i - 1] = std::move(m_recordWork[i]); m_recordWork[m_recordWorkCount - 1].reset(); --m_recordWorkCount; haveWork = true; } if (!haveWork) for (PendingCancel& pending : m_pendingCancel) if (pending.valid) { work.type = WorkType::SEND; work.record = pending.record; work.deadline = pending.deadline; pending.valid = false; haveWork = true; break; } if (!haveWork && m_sendWorkCount) { work = std::move(*m_sendWork[0]); for (size_t i = 1; i < m_sendWorkCount; ++i) m_sendWork[i - 1] = std::move(m_sendWork[i]); m_sendWork[m_sendWorkCount - 1].reset(); --m_sendWorkCount; haveWork = true; } } if (!haveWork) break; ProcessWork(std::move(work)); if (WaitForSingleObject(m_stop, 0) == WAIT_OBJECT_0) return; } } } bool CClipboardManager::QueueWork(Work&& work) { if (Atomic::Load(m_shutdown)) return false; const bool publication = work.type == WorkType::SEND && (work.record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER || work.record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR); { std::lock_guard lock(m_workLock); // A BUSY publication is retried through this same path. Validate it while // holding the queue lock so an older retry cannot erase a newer clipboard // publication which became live before it entered the queue. if (publication && work.record.clipboardGeneration != Atomic::Load( m_liveLocalGeneration, std::memory_order_acquire)) return false; std::array, MAX_WORK>& queue = work.type == WorkType::RECORD ? m_recordWork : m_sendWork; size_t& count = work.type == WorkType::RECORD ? m_recordWorkCount : m_sendWorkCount; auto erase = [&queue, &count](size_t index) { for (size_t i = index + 1; i < count; ++i) queue[i - 1] = std::move(queue[i]); queue[count - 1].reset(); --count; }; if (publication) { for (size_t i = 0; i < count;) { const Work& queued = *queue[i]; if (queued.type == WorkType::SEND && (queued.record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER || queued.record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR)) erase(i); else ++i; } } if (count == MAX_WORK) return false; queue[count++].emplace(std::move(work)); } SetEvent(m_wake); return true; } bool CClipboardManager::QueueCancel( const KVMFRClipboardMessage& record, uint32_t reason, uint64_t deadline) { KVMFRClipboardMessage cancel = record; cancel.version = KVMFR_CLIPBOARD_VERSION; cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL; cancel.token = reason; cancel.length = 0; cancel.flags = 0; cancel.offset = 0; cancel.size = 0; cancel.sequence = 0; if (!deadline) deadline = GetTickCount64() + SEND_TIMEOUT_MS; if (Atomic::Load(m_shutdown) || !cancel.transfer) return false; { std::lock_guard lock(m_workLock); for (size_t i = 0; i < m_sendWorkCount;) { const bool sameTransfer = m_sendWork[i]->record.transfer == cancel.transfer; if (sameTransfer && (m_sendWork[i]->type == WorkType::SEND_DATA || (m_sendWork[i]->type == WorkType::SEND && (m_sendWork[i]->record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST || m_sendWork[i]->record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL)))) { for (size_t j = i + 1; j < m_sendWorkCount; ++j) m_sendWork[j - 1] = std::move(m_sendWork[j]); m_sendWork[m_sendWorkCount - 1].reset(); --m_sendWorkCount; } else ++i; } PendingCancel * free = nullptr; for (PendingCancel& pending : m_pendingCancel) { if (pending.valid && pending.record.transfer == cancel.transfer) { pending.record = cancel; pending.deadline = deadline; SetEvent(m_wake); return true; } if (!pending.valid && !free) free = &pending; } if (!free) { // CANCELs are idempotent and bounded by their transfer ID. Under a // pathological peer stall retain the newest cancellation instead of // allocating or failing a producer-side error path. free = &m_pendingCancel[ m_pendingCancelCursor++ % MAX_PENDING_CANCEL]; } free->record = cancel; free->deadline = deadline; free->valid = true; } SetEvent(m_wake); return true; } void CClipboardManager::QueueControl(WorkType type, bool available, uint64_t epoch, uint32_t reason) { if (Atomic::Load(m_shutdown)) return; { std::lock_guard lock(m_workLock); if (m_pendingControlCount) { PendingControl& last = m_pendingControl[m_pendingControlCount - 1]; if (last.type == type && (type == WorkType::RESET || last.available == available)) { last.available = available; last.epoch = epoch; last.reason = reason; SetEvent(m_wake); return; } } if (m_pendingControlCount == MAX_PENDING_CONTROL) { // Collapse a pathologically noisy lifecycle into one reset. RESET's // worker path republishes the channel's current state after teardown. m_pendingControlCount = 1; m_pendingControl[0].type = WorkType::RESET; m_pendingControl[0].available = false; m_pendingControl[0].epoch = epoch; m_pendingControl[0].reason = reason ? reason : ERROR_OPERATION_ABORTED; } else { PendingControl& pending = m_pendingControl[m_pendingControlCount++]; pending.type = type; pending.available = available; pending.epoch = epoch; pending.reason = reason; } } SetEvent(m_wake); } bool CClipboardManager::QueueUI(UIWork&& work) { if (Atomic::Load(m_shutdown)) return false; std::array dropped = {}; size_t droppedCount = 0; { std::lock_guard lock(m_uiLock); auto erase = [this, &dropped, &droppedCount](size_t index) { if (m_uiWork[index].type == UIType::REQUEST) dropped[droppedCount++] = m_uiWork[index].record; for (size_t i = index + 1; i < m_uiWorkCount; ++i) m_uiWork[i - 1] = m_uiWork[i]; --m_uiWorkCount; m_uiWork[m_uiWorkCount] = UIWork {}; }; if (work.type == UIType::STATE && !work.available) while (m_uiWorkCount) erase(m_uiWorkCount - 1); else if (work.type == UIType::STATE) { for (size_t i = 0; i < m_uiWorkCount;) { if (m_uiWork[i].type == UIType::STATE && m_uiWork[i].available == work.available) erase(i); else ++i; } } else if (work.type == UIType::OFFER || work.type == UIType::CLEAR) { for (size_t i = 0; i < m_uiWorkCount;) { if (m_uiWork[i].type == UIType::OFFER || m_uiWork[i].type == UIType::CLEAR) erase(i); else ++i; } } if (m_uiWorkCount == MAX_UI_WORK) { if (work.type == UIType::REQUEST) return false; erase(0); } m_uiWork[m_uiWorkCount++] = work; } for (size_t i = 0; i < droppedCount; ++i) QueueCancel(dropped[i], ERROR_BUSY); if (PostMessageW(m_hwnd, WM_CLIPBOARD_WORK, 0, 0)) return true; bool removed = false; { std::lock_guard lock(m_uiLock); for (size_t i = m_uiWorkCount; i; --i) { UIWork& queued = m_uiWork[i - 1]; bool same = queued.type == work.type; if (same && work.type == UIType::STATE) same = queued.available == work.available && queued.epoch == work.epoch; else if (same && (work.type == UIType::OFFER || work.type == UIType::CLEAR)) same = queued.record.clipboardGeneration == work.record.clipboardGeneration; else if (same && work.type == UIType::REQUEST) same = queued.record.transfer == work.record.transfer; if (!same) continue; for (size_t j = i; j < m_uiWorkCount; ++j) m_uiWork[j - 1] = m_uiWork[j]; --m_uiWorkCount; m_uiWork[m_uiWorkCount] = UIWork {}; removed = true; break; } } // A previously posted drain may already have consumed the record. return !removed; } void CClipboardManager::ProcessWork(Work&& work) { switch (work.type) { case WorkType::STATE: { if (!work.available) { Atomic::Store(m_outgoingTransfer, 0, std::memory_order_release); CancelIncoming(ERROR_DEVICE_NOT_CONNECTED); } UIWork ui; ui.type = UIType::STATE; ui.available = work.available; ui.epoch = work.epoch; QueueUI(std::move(ui)); break; } case WorkType::RESET: { Atomic::Store(m_outgoingTransfer, 0, std::memory_order_release); CancelIncoming(work.reason ? work.reason : ERROR_OPERATION_ABORTED); UIWork ui; ui.type = UIType::STATE; ui.available = false; ui.epoch = work.epoch; QueueUI(std::move(ui)); if (m_channel.Available()) { UIWork resume; resume.type = UIType::STATE; resume.available = true; resume.epoch = m_channel.Epoch(); QueueUI(std::move(resume)); } break; } case WorkType::RECORD: ProcessRecord(work.record, work.data.empty() ? nullptr : work.data.data()); break; case WorkType::SEND: ProcessSend(std::move(work)); break; case WorkType::SEND_DATA: ProcessSendData(std::move(work)); break; } } void CClipboardManager::ProcessRecord( const KVMFRClipboardMessage& record, const uint8_t * data) { switch (record.type) { case KVMFR_CLIPBOARD_MESSAGE_OFFER: { CancelIncoming(ERROR_OPERATION_ABORTED); InvalidateOutgoing(ERROR_OPERATION_ABORTED); UIWork ui; ui.type = UIType::OFFER; ui.record = record; if (!QueueUI(std::move(ui))) DEBUG_WARN("Failed to queue remote clipboard offer"); break; } case KVMFR_CLIPBOARD_MESSAGE_CLEAR: { CancelIncoming(ERROR_OPERATION_ABORTED); InvalidateOutgoing(ERROR_OPERATION_ABORTED); UIWork ui; ui.type = UIType::CLEAR; ui.record = record; if (!QueueUI(std::move(ui))) DEBUG_WARN("Failed to queue remote clipboard clear"); break; } case KVMFR_CLIPBOARD_MESSAGE_REQUEST: { UIWork ui; ui.type = UIType::REQUEST; ui.record = record; if (!QueueUI(std::move(ui))) QueueCancel(record, ERROR_BUSY); break; } case KVMFR_CLIPBOARD_MESSAGE_DATA: ProcessData(record, data); break; case KVMFR_CLIPBOARD_MESSAGE_CANCEL: { ReleaseOutgoing(record.transfer); CancelIncoming(record.token ? record.token : ERROR_OPERATION_ABORTED, record.transfer); break; } } } void CClipboardManager::ProcessData( const KVMFRClipboardMessage& record, const uint8_t * data) { bool cancel = false; uint32_t cancelReason = ERROR_INVALID_DATA; { std::lock_guard lock(m_transferLock); const std::shared_ptr transfer = m_incoming; if (!transfer || transfer->complete || transfer->transfer != record.transfer || transfer->generation != record.clipboardGeneration || transfer->format != record.format) cancel = true; else { bool valid = record.offset == transfer->nextOffset && record.sequence == transfer->nextSequence; if (!transfer->began) { valid = valid && record.offset == 0 && (record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN); transfer->began = true; transfer->sizeHint = record.size; if (record.size != KVMFR_CLIPBOARD_SIZE_UNKNOWN && record.size > MAX_SPOOL_BYTES) { transfer->error = ERROR_FILE_TOO_LARGE; valid = false; } } else if (record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN) valid = false; if (valid && record.length && !transfer->spool->Append(data, record.length)) { const DWORD error = GetLastError(); transfer->error = error ? error : ERROR_DISK_FULL; valid = false; } if (valid) { transfer->nextOffset += record.length; ++transfer->nextSequence; if (record.flags & KVMFR_CLIPBOARD_FLAG_END) { valid = record.size == transfer->nextOffset && (transfer->sizeHint == KVMFR_CLIPBOARD_SIZE_UNKNOWN || transfer->sizeHint == transfer->nextOffset); if (valid) { transfer->complete = true; transfer->error = ERROR_SUCCESS; } } } if (!valid) { transfer->complete = true; if (transfer->error == ERROR_SUCCESS) transfer->error = ERROR_INVALID_DATA; cancel = true; cancelReason = transfer->error; } if (transfer->complete) SetEvent(transfer->event); } } if (cancel) QueueCancel(record, cancelReason); } void CClipboardManager::ProcessSend(Work&& work) { if (!work.deadline) work.deadline = GetTickCount64() + SEND_TIMEOUT_MS; const bool publication = work.record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER || work.record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR; ClipboardChannelResult result; if (publication) { // Serialize the final generation check with invalidation. Once a remote // offer has invalidated this generation, no queued retry may enter the // channel after that invalidation point. std::lock_guard lock(m_outgoingLock); if (work.record.clipboardGeneration != Atomic::Load( m_liveLocalGeneration, std::memory_order_acquire)) return; result = m_channel.Send(work.record, nullptr); } else if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST) { std::lock_guard lock(m_transferLock); if (!m_incoming || m_incoming->complete || m_incoming->transfer != work.record.transfer || m_incoming->generation != work.record.clipboardGeneration || m_incoming->format != work.record.format) return; if (GetTickCount64() >= work.deadline) { m_incoming->complete = true; m_incoming->error = ERROR_TIMEOUT; SetEvent(m_incoming->event); return; } result = m_channel.Send(work.record, nullptr); } else result = m_channel.Send(work.record, nullptr); if (result == ClipboardChannelResult::ACCEPTED) return; const KVMFRClipboardMessageType publicationType = work.record.type; const uint64_t publicationGeneration = work.record.clipboardGeneration; auto failLocalPublication = [this, publicationType, publicationGeneration]() { if (publicationType != KVMFR_CLIPBOARD_MESSAGE_OFFER && publicationType != KVMFR_CLIPBOARD_MESSAGE_CLEAR) return; uint64_t generation = publicationGeneration; if (Atomic::CAS(m_liveLocalGeneration, generation, UINT64_C(0), std::memory_order_acq_rel)) PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0); }; if (result == ClipboardChannelResult::BUSY && GetTickCount64() < work.deadline && WaitForSingleObject(m_stop, CHANNEL_RETRY_MS) != WAIT_OBJECT_0) { if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL) QueueCancel(work.record, work.record.token, work.deadline); else if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST) { const KVMFRClipboardMessage record = work.record; bool live = false; bool queued = false; { std::lock_guard lock(m_transferLock); live = m_incoming && !m_incoming->complete && m_incoming->transfer == record.transfer && m_incoming->generation == record.clipboardGeneration && m_incoming->format == record.format; if (live) queued = QueueWork(std::move(work)); } if (live && !queued) CancelIncoming(ERROR_BUSY, record.transfer); } else if (!QueueWork(std::move(work))) failLocalPublication(); return; } if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST) { const uint32_t reason = result == ClipboardChannelResult::BUSY ? ERROR_TIMEOUT : ERROR_DEVICE_NOT_CONNECTED; CancelIncoming(reason, work.record.transfer); } else failLocalPublication(); } void CClipboardManager::ProcessSendData(Work&& work) { if (!work.spool) { ReleaseOutgoing(work.record.transfer); QueueCancel(work.record, ERROR_INVALID_DATA); return; } if (Atomic::Load(m_outgoingTransfer, std::memory_order_acquire) != work.record.transfer) return; if (!work.deadline) work.deadline = GetTickCount64() + SEND_TIMEOUT_MS; if (GetTickCount64() >= work.deadline) { ReleaseOutgoing(work.record.transfer); QueueCancel(work.record, ERROR_TIMEOUT); return; } if (work.record.clipboardGeneration != Atomic::Load(m_liveLocalGeneration, std::memory_order_acquire)) { ReleaseOutgoing(work.record.transfer); QueueCancel(work.record, ERROR_OPERATION_ABORTED); return; } const uint64_t total = work.spool->Size(); if (work.record.offset > total) { ReleaseOutgoing(work.record.transfer); QueueCancel(work.record, ERROR_INVALID_DATA); return; } const size_t length = static_cast((std::min)( KVMFR_CLIPBOARD_DATA_BYTES, total - work.record.offset)); std::vector data; if (length) { try { data.resize(length); } catch (const std::bad_alloc&) { ReleaseOutgoing(work.record.transfer); QueueCancel(work.record, ERROR_OUTOFMEMORY); return; } if (!work.spool->Read(work.record.offset, data.data(), length)) { const DWORD error = GetLastError(); ReleaseOutgoing(work.record.transfer); QueueCancel(work.record, error ? error : ERROR_READ_FAULT); return; } } KVMFRClipboardMessage message = work.record; message.type = KVMFR_CLIPBOARD_MESSAGE_DATA; message.token = 0; message.length = static_cast(length); message.flags = 0; if (!message.offset) message.flags |= KVMFR_CLIPBOARD_FLAG_BEGIN; if (message.offset + length == total) message.flags |= KVMFR_CLIPBOARD_FLAG_END; message.size = message.flags & KVMFR_CLIPBOARD_FLAG_END ? total : (message.flags & KVMFR_CLIPBOARD_FLAG_BEGIN ? total : KVMFR_CLIPBOARD_SIZE_UNKNOWN); ClipboardChannelResult result = ClipboardChannelResult::FAILED; bool stale = false; bool timedOut = false; { std::lock_guard lock(m_outgoingLock); stale = Atomic::Load(m_outgoingTransfer, std::memory_order_acquire) != message.transfer || message.clipboardGeneration != Atomic::Load( m_liveLocalGeneration, std::memory_order_acquire); timedOut = GetTickCount64() >= work.deadline; if (!stale && !timedOut) result = m_channel.Send(message, data.empty() ? nullptr : data.data()); } if (stale) { ReleaseOutgoing(message.transfer); QueueCancel(message, ERROR_OPERATION_ABORTED); return; } if (timedOut) { ReleaseOutgoing(message.transfer); QueueCancel(message, ERROR_TIMEOUT); return; } if (result == ClipboardChannelResult::BUSY) { if (GetTickCount64() < work.deadline && WaitForSingleObject(m_stop, CHANNEL_RETRY_MS) != WAIT_OBJECT_0) { const KVMFRClipboardMessage record = work.record; if (!QueueWork(std::move(work))) { ReleaseOutgoing(record.transfer); QueueCancel(record, ERROR_BUSY); } } else { ReleaseOutgoing(work.record.transfer); QueueCancel(work.record, ERROR_TIMEOUT); } return; } if (result != ClipboardChannelResult::ACCEPTED) { ReleaseOutgoing(work.record.transfer); QueueCancel(work.record, ERROR_DEVICE_NOT_CONNECTED); return; } work.deadline = GetTickCount64() + SEND_TIMEOUT_MS; if (message.flags & KVMFR_CLIPBOARD_FLAG_END) { ReleaseOutgoing(work.record.transfer); return; } work.record.offset += length; ++work.record.sequence; const KVMFRClipboardMessage record = work.record; if (!QueueWork(std::move(work))) { ReleaseOutgoing(record.transfer); QueueCancel(record, ERROR_BUSY); } } void CClipboardManager::CancelIncoming(uint32_t reason, uint64_t transferID) { std::lock_guard lock(m_transferLock); const std::shared_ptr transfer = m_incoming; if (!transfer || (transferID && transferID != transfer->transfer)) return; transfer->complete = true; transfer->error = reason; SetEvent(transfer->event); } void CClipboardManager::ReleaseOutgoing(uint64_t transfer) { std::lock_guard lock(m_outgoingLock); Atomic::CAS(m_outgoingTransfer, transfer, UINT64_C(0), std::memory_order_acq_rel); } void CClipboardManager::ClipboardState(bool available, uint64_t epoch) { QueueControl(WorkType::STATE, available, epoch, 0); } ClipboardChannelResult CClipboardManager::ClipboardRecord( const KVMFRClipboardMessage& record, const uint8_t * data) { switch (record.type) { case KVMFR_CLIPBOARD_MESSAGE_OFFER: if (!record.clipboardGeneration || !record.token || (record.token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL) || record.format || record.transfer || record.length || record.flags || record.offset || record.size || record.sequence) return ClipboardChannelResult::FAILED; break; case KVMFR_CLIPBOARD_MESSAGE_CLEAR: if (!record.clipboardGeneration || record.format || record.transfer || record.length || record.flags || record.offset || record.size || record.sequence || record.token) return ClipboardChannelResult::FAILED; break; case KVMFR_CLIPBOARD_MESSAGE_REQUEST: if (!record.clipboardGeneration || !record.transfer || !kvmfrClipboardTransferFromClient(record.transfer) || !kvmfrClipboardFormatValid(record.format) || record.length || record.flags || record.offset || record.size || record.sequence || record.token) return ClipboardChannelResult::FAILED; break; case KVMFR_CLIPBOARD_MESSAGE_DATA: if (!record.clipboardGeneration || !record.transfer || !kvmfrClipboardTransferFromHelper(record.transfer) || !kvmfrClipboardFormatValid(record.format) || (record.length && !data)) return ClipboardChannelResult::FAILED; break; case KVMFR_CLIPBOARD_MESSAGE_CANCEL: if (!record.transfer || record.length || record.flags) return ClipboardChannelResult::FAILED; break; default: return ClipboardChannelResult::FAILED; } Work work; work.type = WorkType::RECORD; work.record = record; if (record.length) { try { work.data.assign(data, data + record.length); } catch (const std::bad_alloc&) { return ClipboardChannelResult::BUSY; } } if (QueueWork(std::move(work))) return ClipboardChannelResult::ACCEPTED; return Atomic::Load(m_shutdown) ? ClipboardChannelResult::FAILED : ClipboardChannelResult::BUSY; } void CClipboardManager::ClipboardReset(uint64_t epoch, uint32_t reason) { QueueControl(WorkType::RESET, false, epoch, reason); } bool CClipboardManager::HandleMessage(UINT message, WPARAM wParam, LPARAM, LRESULT& result) { switch (message) { case WM_CLIPBOARD_WORK: DrainUI(); result = 0; return true; case WM_CLIPBOARDUPDATE: HandleClipboardUpdate(); result = 0; return true; case WM_RENDERFORMAT: RenderFormat(static_cast(wParam)); result = 0; return true; case WM_RENDERALLFORMATS: RenderAllFormats(); result = 0; return true; case WM_TIMER: if (wParam == REMOTE_RETRY_TIMER) { RetryRemoteOffer(); result = 0; return true; } break; case WM_DESTROYCLIPBOARD: HandleDestroyClipboard(); result = 0; return true; } return false; } void CClipboardManager::DrainUI() { std::lock_guard dispatchLock(m_uiLock); for (;;) { UIWork work; if (!m_uiWorkCount) return; work = m_uiWork[0]; for (size_t i = 1; i < m_uiWorkCount; ++i) m_uiWork[i - 1] = m_uiWork[i]; --m_uiWorkCount; m_uiWork[m_uiWorkCount] = UIWork {}; switch (work.type) { case UIType::STATE: HandleState(work.available, work.epoch); break; case UIType::OFFER: HandleOffer(work.record); break; case UIType::CLEAR: HandleClear(work.record); break; case UIType::REQUEST: HandleRequest(work.record); break; } } } void CClipboardManager::HandleState(bool available, uint64_t epoch) { m_available = available; m_epoch = available ? epoch : 0; if (!available) { ClearRemoteRetry(); InvalidateLocalClipboard(ERROR_DEVICE_NOT_CONNECTED); ClearOwnedClipboard(); m_remoteControlGeneration = 0; return; } PublishLocalClipboard(); } void CClipboardManager::HandleOffer( const KVMFRClipboardMessage& record) { if (!m_available || !record.clipboardGeneration || !record.token || (record.token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL)) return; if (m_remoteControlGeneration && record.clipboardGeneration < m_remoteControlGeneration) return; const bool newOffer = m_pendingRemoteOffer.clipboardGeneration != record.clipboardGeneration; if (newOffer) { ClearRemoteRetry(); m_remoteControlGeneration = record.clipboardGeneration; m_pendingRemoteOffer = record; m_remoteRetryDeadline = GetTickCount64() + REMOTE_RETRY_TIMEOUT_MS; InvalidateLocalClipboard(ERROR_OPERATION_ABORTED); } if (ApplyRemoteOffer(record.token, record.clipboardGeneration)) { ClearRemoteRetry(); return; } if (GetTickCount64() < m_remoteRetryDeadline && SetTimer(m_hwnd, REMOTE_RETRY_TIMER, REMOTE_RETRY_MS, nullptr)) return; DEBUG_WARN("Failed to apply remote clipboard offer"); ClearRemoteRetry(); PublishLocalClipboard(); } void CClipboardManager::HandleClear( const KVMFRClipboardMessage& record) { if (record.clipboardGeneration && m_remoteControlGeneration && record.clipboardGeneration < m_remoteControlGeneration) return; ClearRemoteRetry(); m_remoteControlGeneration = record.clipboardGeneration; InvalidateLocalClipboard(ERROR_OPERATION_ABORTED); ClearOwnedClipboard(); } void CClipboardManager::HandleRequest( const KVMFRClipboardMessage& record) { if (!m_available || !record.transfer || !kvmfrClipboardFormatValid(record.format) || record.clipboardGeneration != m_localGeneration || record.clipboardGeneration != Atomic::Load( m_liveLocalGeneration, std::memory_order_acquire) || GetClipboardSequenceNumber() != m_localSequence) { QueueCancel(record, ERROR_NOT_FOUND); return; } uint64_t noTransfer = 0; if (!Atomic::CAS(m_outgoingTransfer, noTransfer, record.transfer, std::memory_order_acq_rel)) { QueueCancel(record, ERROR_BUSY); return; } std::shared_ptr spool = CaptureFormat(record.format, m_localSequence); if (!spool) { const DWORD error = GetLastError(); ReleaseOutgoing(record.transfer); QueueCancel(record, error ? error : ERROR_NOT_FOUND); return; } Work data; data.type = WorkType::SEND_DATA; data.record = record; data.record.version = KVMFR_CLIPBOARD_VERSION; data.record.type = KVMFR_CLIPBOARD_MESSAGE_DATA; data.record.sequence = 0; data.record.offset = 0; data.record.size = 0; data.record.token = 0; data.record.length = 0; data.record.flags = 0; data.spool = std::move(spool); if (!QueueWork(std::move(data))) { ReleaseOutgoing(record.transfer); QueueCancel(record, ERROR_BUSY); } } bool CClipboardManager::OpenClipboardRetry() const { for (unsigned int attempt = 0; attempt != 8; ++attempt) { if (OpenClipboard(m_hwnd)) return true; Sleep(5U << (std::min)(attempt, 5U)); } return false; } bool CClipboardManager::IsOurClipboard() { if (GetClipboardOwner() == m_hwnd) { m_ownedSequence = GetClipboardSequenceNumber(); return true; } if (!m_formatOrigin || !m_remoteGeneration || !IsClipboardFormatAvailable(m_formatOrigin) || !OpenClipboardRetry()) return false; bool ours = false; HANDLE data = GetClipboardData(m_formatOrigin); if (data && GlobalSize(data) >= sizeof(ClipboardOrigin)) { const ClipboardOrigin * origin = static_cast(GlobalLock(data)); if (origin) { ours = origin->magic == ORIGIN_MAGIC && origin->epoch == m_epoch && origin->generation == m_remoteGeneration; GlobalUnlock(data); } } CloseClipboard(); return ours; } uint32_t CClipboardManager::EnumerateFormats() const { uint32_t formats = 0; if (IsClipboardFormatAvailable(CF_UNICODETEXT)) formats |= KVMFR_CLIPBOARD_FORMAT_MASK_TEXT; if (m_formatPNG && IsClipboardFormatAvailable(m_formatPNG)) formats |= KVMFR_CLIPBOARD_FORMAT_MASK_PNG; if (IsClipboardFormatAvailable(CF_DIBV5) || IsClipboardFormatAvailable(CF_DIB)) formats |= KVMFR_CLIPBOARD_FORMAT_MASK_BMP; if (IsClipboardFormatAvailable(CF_TIFF)) formats |= KVMFR_CLIPBOARD_FORMAT_MASK_TIFF; if (m_formatJPEG && IsClipboardFormatAvailable(m_formatJPEG)) formats |= KVMFR_CLIPBOARD_FORMAT_MASK_JPEG; return formats; } void CClipboardManager::HandleClipboardUpdate() { if (m_applyingRemote || IsOurClipboard()) return; ClearRemoteRetry(); InvalidateLocalClipboard(ERROR_OPERATION_ABORTED); m_remoteGeneration = 0; m_remoteFormats = 0; m_ownedSequence = 0; CancelIncoming(ERROR_OPERATION_ABORTED); PublishLocalClipboard(); } void CClipboardManager::HandleDestroyClipboard() { if (m_applyingRemote) return; ClearRemoteRetry(); InvalidateLocalClipboard(ERROR_OPERATION_ABORTED); m_remoteGeneration = 0; m_remoteFormats = 0; m_ownedSequence = 0; CancelIncoming(ERROR_OPERATION_ABORTED); } void CClipboardManager::PublishLocalClipboard() { if (!m_available) { Atomic::Store(m_liveLocalGeneration, UINT64_C(0), std::memory_order_release); m_localSequence = GetClipboardSequenceNumber(); return; } const DWORD before = GetClipboardSequenceNumber(); const uint32_t formats = EnumerateFormats(); const DWORD after = GetClipboardSequenceNumber(); if (before != after) { PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0); return; } uint64_t generation = ++m_localGeneration; if (!generation) generation = ++m_localGeneration; m_localSequence = after; if (!formats) { PublishClear(generation); return; } Work work; work.type = WorkType::SEND; work.record.version = KVMFR_CLIPBOARD_VERSION; work.record.type = KVMFR_CLIPBOARD_MESSAGE_OFFER; work.record.clipboardGeneration = generation; work.record.token = formats; { std::lock_guard lock(m_outgoingLock); Atomic::Store(m_liveLocalGeneration, generation, std::memory_order_release); } if (!QueueWork(std::move(work))) { uint64_t live = generation; Atomic::CAS(m_liveLocalGeneration, live, UINT64_C(0), std::memory_order_acq_rel); PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0); } } void CClipboardManager::PublishClear(uint64_t generation) { Work work; work.type = WorkType::SEND; work.record.version = KVMFR_CLIPBOARD_VERSION; work.record.type = KVMFR_CLIPBOARD_MESSAGE_CLEAR; work.record.clipboardGeneration = generation; { std::lock_guard lock(m_outgoingLock); Atomic::Store(m_liveLocalGeneration, generation, std::memory_order_release); } if (!QueueWork(std::move(work))) { uint64_t live = generation; Atomic::CAS(m_liveLocalGeneration, live, UINT64_C(0), std::memory_order_acq_rel); PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0); } } void CClipboardManager::InvalidateOutgoing(uint32_t reason) { uint64_t generation; uint64_t transfer; { std::lock_guard lock(m_outgoingLock); generation = Atomic::Swap(m_liveLocalGeneration, UINT64_C(0), std::memory_order_acq_rel); transfer = Atomic::Swap( m_outgoingTransfer, UINT64_C(0), std::memory_order_acq_rel); } if (!transfer) return; KVMFRClipboardMessage cancel = {}; cancel.clipboardGeneration = generation; cancel.transfer = transfer; QueueCancel(cancel, reason); } void CClipboardManager::InvalidateLocalClipboard(uint32_t reason) { m_localSequence = 0; InvalidateOutgoing(reason); } void CClipboardManager::ClearRemoteRetry() { if (m_hwnd) KillTimer(m_hwnd, REMOTE_RETRY_TIMER); m_pendingRemoteOffer = {}; m_remoteRetryDeadline = 0; } void CClipboardManager::RetryRemoteOffer() { KillTimer(m_hwnd, REMOTE_RETRY_TIMER); if (!m_pendingRemoteOffer.clipboardGeneration || !m_available) return; const KVMFRClipboardMessage offer = m_pendingRemoteOffer; HandleOffer(offer); } bool CClipboardManager::SetOriginMarker(uint64_t generation) const { HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, sizeof(ClipboardOrigin)); if (!memory) return false; ClipboardOrigin * origin = static_cast(GlobalLock(memory)); if (!origin) { const DWORD error = GetLastError(); GlobalFree(memory); SetLastError(error ? error : ERROR_NOT_ENOUGH_MEMORY); return false; } *origin = { ORIGIN_MAGIC, 0, m_epoch, generation }; GlobalUnlock(memory); if (!SetClipboardData(m_formatOrigin, memory)) { const DWORD error = GetLastError(); GlobalFree(memory); SetLastError(error ? error : ERROR_INVALID_DATA); return false; } return true; } bool CClipboardManager::ApplyRemoteOffer(uint32_t formats, uint64_t generation) { if (!OpenClipboardRetry()) { DEBUG_WARN_HR(GetLastError(), "Failed to open the clipboard"); return false; } m_applyingRemote = true; const bool emptied = EmptyClipboard() != FALSE; DWORD error = emptied ? ERROR_SUCCESS : GetLastError(); if (emptied) { auto setDelayed = [&error](UINT format) { if (!format) { if (!error) error = ERROR_INVALID_DATA; return false; } SetLastError(ERROR_SUCCESS); const HANDLE result = SetClipboardData(format, nullptr); if (result || IsClipboardFormatAvailable(format)) return true; error = GetLastError(); if (!error) error = ERROR_INVALID_DATA; return false; }; bool complete = true; if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_TEXT) complete = setDelayed(CF_UNICODETEXT) && complete; if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_PNG) complete = setDelayed(m_formatPNG) && complete; if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_BMP) complete = setDelayed(CF_DIB) && complete; if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_TIFF) complete = setDelayed(CF_TIFF) && complete; if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_JPEG) complete = setDelayed(m_formatJPEG) && complete; if (!SetOriginMarker(generation)) { if (!error) error = GetLastError(); if (!error) error = ERROR_INVALID_DATA; complete = false; } if (complete) { m_remoteGeneration = generation; m_remoteFormats = formats; } else { EmptyClipboard(); m_remoteGeneration = 0; m_remoteFormats = 0; } } CloseClipboard(); m_applyingRemote = false; if (!emptied || error) { if (!error) error = ERROR_INVALID_DATA; SetLastError(error); DEBUG_WARN_HR(error, "Failed to replace the clipboard"); return false; } m_ownedSequence = GetClipboardSequenceNumber(); return true; } void CClipboardManager::ClearOwnedClipboard() { CancelIncoming(ERROR_OPERATION_ABORTED); if (m_hwnd && GetClipboardOwner() == m_hwnd && OpenClipboardRetry()) { m_applyingRemote = true; EmptyClipboard(); CloseClipboard(); m_applyingRemote = false; } m_remoteGeneration = 0; m_remoteFormats = 0; m_ownedSequence = 0; } KVMFRClipboardFormat CClipboardManager::ToWireFormat(UINT format) const { if (format == CF_UNICODETEXT) return KVMFR_CLIPBOARD_FORMAT_TEXT; if (format == m_formatPNG) return KVMFR_CLIPBOARD_FORMAT_PNG; if (format == CF_DIB || format == CF_DIBV5) return KVMFR_CLIPBOARD_FORMAT_BMP; if (format == CF_TIFF) return KVMFR_CLIPBOARD_FORMAT_TIFF; if (format == m_formatJPEG) return KVMFR_CLIPBOARD_FORMAT_JPEG; return KVMFR_CLIPBOARD_FORMAT_NONE; } UINT CClipboardManager::ToWindowsFormat(KVMFRClipboardFormat format) const { switch (format) { case KVMFR_CLIPBOARD_FORMAT_TEXT: return CF_UNICODETEXT; case KVMFR_CLIPBOARD_FORMAT_PNG: return m_formatPNG; case KVMFR_CLIPBOARD_FORMAT_BMP: return IsClipboardFormatAvailable(CF_DIBV5) ? CF_DIBV5 : CF_DIB; case KVMFR_CLIPBOARD_FORMAT_TIFF: return CF_TIFF; case KVMFR_CLIPBOARD_FORMAT_JPEG: return m_formatJPEG; default: return 0; } } std::shared_ptr CClipboardManager::CaptureFormat( KVMFRClipboardFormat format, DWORD sequence) { if (GetClipboardSequenceNumber() != sequence) { SetLastError(ERROR_RETRY); return nullptr; } if (!OpenClipboardRetry()) return nullptr; if (GetClipboardSequenceNumber() != sequence) { CloseClipboard(); SetLastError(ERROR_RETRY); return nullptr; } UINT windowsFormat = ToWindowsFormat(format); HANDLE handle = windowsFormat ? GetClipboardData(windowsFormat) : nullptr; if (!handle && format == KVMFR_CLIPBOARD_FORMAT_BMP && windowsFormat == CF_DIBV5) { windowsFormat = CF_DIB; handle = GetClipboardData(windowsFormat); } if (!handle) { const DWORD error = GetLastError(); CloseClipboard(); SetLastError(error ? error : ERROR_NOT_FOUND); return nullptr; } const SIZE_T sourceSize = GlobalSize(handle); if (sourceSize > MAX_SPOOL_BYTES) { CloseClipboard(); SetLastError(ERROR_FILE_TOO_LARGE); return nullptr; } const uint8_t * source = static_cast(GlobalLock(handle)); if (!source || !sourceSize) { const DWORD error = GetLastError(); if (source) GlobalUnlock(handle); CloseClipboard(); SetLastError(error ? error : ERROR_INVALID_DATA); return nullptr; } std::shared_ptr spool; try { spool = std::make_shared(); } catch (const std::bad_alloc&) { GlobalUnlock(handle); CloseClipboard(); SetLastError(ERROR_OUTOFMEMORY); return nullptr; } SetLastError(ERROR_SUCCESS); bool success = true; if (format == KVMFR_CLIPBOARD_FORMAT_TEXT) { if (sourceSize % sizeof(wchar_t)) { SetLastError(ERROR_INVALID_DATA); success = false; } else { const wchar_t * text = reinterpret_cast(source); const size_t capacity = sourceSize / sizeof(wchar_t); size_t length = 0; while (length < capacity && text[length]) ++length; std::vector output; try { output.reserve(TEXT_CONVERSION_CHUNK); for (size_t index = 0; success && index < length; ++index) { uint32_t codepoint = static_cast(text[index]); if (codepoint == '\r' && index + 1 < length && text[index + 1] == L'\n') continue; if (codepoint >= 0xd800 && codepoint <= 0xdbff) { if (index + 1 < length) { const uint32_t low = static_cast(text[index + 1]); if (low >= 0xdc00 && low <= 0xdfff) { codepoint = 0x10000 + ((codepoint - 0xd800) << 10) + (low - 0xdc00); ++index; } else codepoint = 0xfffd; } else codepoint = 0xfffd; } else if (codepoint >= 0xdc00 && codepoint <= 0xdfff) codepoint = 0xfffd; AppendUTF8(output, codepoint); if (output.size() >= TEXT_CONVERSION_CHUNK) { success = spool->Append(output.data(), output.size()); output.clear(); } } } catch (const std::bad_alloc&) { SetLastError(ERROR_OUTOFMEMORY); success = false; } if (success && !output.empty()) success = spool->Append(output.data(), output.size()); } } else if (format == KVMFR_CLIPBOARD_FORMAT_BMP) { const uint32_t dibOffset = DIBPixelOffset(source, sourceSize); if (!dibOffset || sourceSize > (std::numeric_limits::max)() - sizeof(BITMAPFILEHEADER)) { SetLastError(sourceSize > (std::numeric_limits::max)() - sizeof(BITMAPFILEHEADER) ? ERROR_FILE_TOO_LARGE : ERROR_INVALID_DATA); success = false; } else { BITMAPFILEHEADER header = {}; header.bfType = 0x4d42; header.bfSize = static_cast( sizeof(BITMAPFILEHEADER) + sourceSize); header.bfOffBits = sizeof(BITMAPFILEHEADER) + dibOffset; success = spool->Append(&header, sizeof(header)) && spool->Append(source, sourceSize); } } else success = spool->Append(source, sourceSize); DWORD error = success ? ERROR_SUCCESS : GetLastError(); if (!success && !error) error = ERROR_NOT_ENOUGH_MEMORY; GlobalUnlock(handle); if (GetClipboardSequenceNumber() != sequence) { CloseClipboard(); SetLastError(ERROR_RETRY); return nullptr; } CloseClipboard(); if (!success) { SetLastError(error); return nullptr; } return spool; } bool CClipboardManager::MaterializeFormat(KVMFRClipboardFormat format, CClipboardSpool& spool) { UINT windowsFormat = ToWindowsFormat(format); if (format == KVMFR_CLIPBOARD_FORMAT_BMP) windowsFormat = CF_DIB; if (!windowsFormat) { SetLastError(ERROR_INVALID_DATA); return false; } HGLOBAL memory = nullptr; if (format == KVMFR_CLIPBOARD_FORMAT_TEXT) memory = UnicodeFromUTF8(spool); else if (format == KVMFR_CLIPBOARD_FORMAT_BMP) { BITMAPFILEHEADER header = {}; const bool haveHeader = spool.Size() >= sizeof(header) && spool.Read(0, &header, sizeof(header)); if (!haveHeader || header.bfType != 0x4d42 || header.bfOffBits < sizeof(header) || header.bfOffBits > spool.Size()) { if (haveHeader && (header.bfType != 0x4d42 || header.bfOffBits < sizeof(header) || header.bfOffBits > spool.Size())) SetLastError(ERROR_INVALID_DATA); return false; } memory = CopySpoolToGlobal(spool, sizeof(header)); } else memory = CopySpoolToGlobal(spool, 0); if (!memory) return false; if (!SetClipboardData(windowsFormat, memory)) { const DWORD error = GetLastError(); GlobalFree(memory); SetLastError(error ? error : ERROR_INVALID_DATA); return false; } return true; } void CClipboardManager::RenderFormat(UINT windowsFormat, uint64_t deadline) { const KVMFRClipboardFormat format = ToWireFormat(windowsFormat); if (!m_available || !m_remoteGeneration || !kvmfrClipboardFormatValid(format) || !(m_remoteFormats & kvmfrClipboardFormatFlag(format))) return; if (deadline && GetTickCount64() >= deadline) return; std::shared_ptr transfer; try { transfer = std::make_shared(); transfer->spool = std::make_shared(); } catch (const std::bad_alloc&) { return; } transfer->event = CreateEventW(nullptr, TRUE, FALSE, nullptr); if (!transfer->event) return; if (!deadline) deadline = GetTickCount64() + RENDER_TIMEOUT_MS; transfer->generation = m_remoteGeneration; transfer->format = format; transfer->transfer = Atomic::FetchAdd(m_nextTransfer, UINT64_C(1), std::memory_order_relaxed); if (!kvmfrClipboardTransferFromHelper(transfer->transfer)) { Atomic::Store(m_nextTransfer, KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(2), std::memory_order_relaxed); transfer->transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(1); } { std::lock_guard lock(m_transferLock); if (m_incoming && !m_incoming->complete) return; m_incoming = transfer; } Work request; request.type = WorkType::SEND; request.record.version = KVMFR_CLIPBOARD_VERSION; request.record.type = KVMFR_CLIPBOARD_MESSAGE_REQUEST; request.record.clipboardGeneration = transfer->generation; request.record.transfer = transfer->transfer; request.record.format = transfer->format; request.deadline = deadline; if (!QueueWork(std::move(request))) CancelIncoming(ERROR_BUSY, transfer->transfer); const uint64_t now = GetTickCount64(); const DWORD waitMs = now >= deadline ? 0 : static_cast( (std::min)(deadline - now, MAXDWORD)); const DWORD wait = WaitForSingleObject(transfer->event, waitMs); const DWORD waitError = wait == WAIT_FAILED ? GetLastError() : ERROR_SUCCESS; bool requestComplete = false; uint32_t transferError = ERROR_SUCCESS; { std::lock_guard lock(m_transferLock); requestComplete = transfer->complete; transferError = transfer->error; } if (!requestComplete || transferError != ERROR_SUCCESS) { const uint32_t reason = requestComplete ? transferError : (wait == WAIT_TIMEOUT ? ERROR_TIMEOUT : (wait == WAIT_FAILED ? waitError : ERROR_INVALID_DATA)); CancelIncoming(reason ? reason : ERROR_OPERATION_ABORTED, transfer->transfer); KVMFRClipboardMessage cancel = {}; cancel.clipboardGeneration = transfer->generation; cancel.transfer = transfer->transfer; cancel.format = transfer->format; QueueCancel(cancel, reason ? reason : ERROR_OPERATION_ABORTED); } bool render = false; { std::lock_guard lock(m_transferLock); render = transfer->complete && transfer->error == ERROR_SUCCESS && transfer->generation == m_remoteGeneration && GetClipboardOwner() == m_hwnd; if (m_incoming == transfer) m_incoming.reset(); } if (render && !MaterializeFormat(format, *transfer->spool)) DEBUG_WARN_HR(GetLastError(), "Failed to render clipboard format %u", format); } void CClipboardManager::RenderAllFormats() { if (!OpenClipboardRetry()) return; if (GetClipboardOwner() != m_hwnd) { CloseClipboard(); return; } const uint32_t formats = m_remoteFormats; const uint64_t deadline = GetTickCount64() + RENDER_TIMEOUT_MS; if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_TEXT) RenderFormat(CF_UNICODETEXT, deadline); if (GetTickCount64() < deadline && (formats & KVMFR_CLIPBOARD_FORMAT_MASK_PNG)) RenderFormat(m_formatPNG, deadline); if (GetTickCount64() < deadline && (formats & KVMFR_CLIPBOARD_FORMAT_MASK_BMP)) RenderFormat(CF_DIB, deadline); if (GetTickCount64() < deadline && (formats & KVMFR_CLIPBOARD_FORMAT_MASK_TIFF)) RenderFormat(CF_TIFF, deadline); if (GetTickCount64() < deadline && (formats & KVMFR_CLIPBOARD_FORMAT_MASK_JPEG)) RenderFormat(m_formatJPEG, deadline); CloseClipboard(); }