From f2eadca4ab94cc6fcced4512c089078e9a92e920 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Sat, 3 Oct 2026 13:27:48 +1000 Subject: [PATCH] [client] windows: implement file clipboard support --- client/displayservers/Windows/CMakeLists.txt | 1 + client/displayservers/Windows/clipboard.c | 445 ++- client/include/core/clipboard.h | 2 +- client/include/core/clipboard_files.h | 8 + .../platform/windows/clipboard_files.h | 43 + .../platform/windows/clipboard_files_win32.h | 88 + client/platform/Windows/CMakeLists.txt | 2 + client/platform/Windows/clipboard_files.c | 1703 ++++++++++- .../platform/Windows/clipboard_files_win32.c | 2570 +++++++++++++++++ client/tests/CMakeLists.txt | 11 - client/tests/clipboard_files_windows_test.c | 103 - 11 files changed, 4798 insertions(+), 178 deletions(-) create mode 100644 client/include/platform/windows/clipboard_files.h create mode 100644 client/include/platform/windows/clipboard_files_win32.h create mode 100644 client/platform/Windows/clipboard_files_win32.c delete mode 100644 client/tests/clipboard_files_windows_test.c diff --git a/client/displayservers/Windows/CMakeLists.txt b/client/displayservers/Windows/CMakeLists.txt index 16a66942..3cf111d5 100644 --- a/client/displayservers/Windows/CMakeLists.txt +++ b/client/displayservers/Windows/CMakeLists.txt @@ -27,6 +27,7 @@ target_link_libraries(displayserver_Windows lg_common user32 gdi32 + ole32 ) if(ENABLE_EGL) diff --git a/client/displayservers/Windows/clipboard.c b/client/displayservers/Windows/clipboard.c index 0ccf514d..eee8a826 100644 --- a/client/displayservers/Windows/clipboard.c +++ b/client/displayservers/Windows/clipboard.c @@ -29,9 +29,14 @@ #include "common/debug.h" #include "common/windebug.h" #include "core/clipboard.h" +#include "core/clipboard_files.h" #include "platform.h" +#include "platform/windows/clipboard_files.h" -#define WINDOWS_CB_OPEN_ATTEMPTS 8 +#define WINDOWS_CB_OPEN_ATTEMPTS 8 +#define WINDOWS_CB_RETRY_INTERVAL_MS 50 +#define WINDOWS_CB_RETRY_TIMEOUT_MS 5000 +#define WINDOWS_CB_RETRY_TIMER_ID ((UINT_PTR)0x4C47) enum WindowsClipboardMessage { @@ -47,6 +52,13 @@ enum WindowsClipboardOperationType WINDOWS_CB_OPERATION_DATA, }; +enum WindowsClipboardCaptureResult +{ + WINDOWS_CB_CAPTURE_RETRY, + WINDOWS_CB_CAPTURE_TYPES, + WINDOWS_CB_CAPTURE_FILES, +}; + struct WindowsClipboardOperation { struct WindowsClipboardOperation * next; @@ -79,15 +91,23 @@ struct WindowsClipboardState UINT pngFormat; UINT jfifFormat; UINT jpegFormat; + IDataObject * remoteFiles; LG_ClipboardRequest canceledRequest; + ULONGLONG retryDeadline; + DWORD retrySequence; bool listener; bool accepting; + bool oleInitialized; + bool applyingRemote; + bool pendingUpdate; }; static SRWLOCK windowsCBQueueLock = SRWLOCK_INIT; static struct WindowsClipboardState windowsCB; static _Atomic(uint64_t) windowsCBGeneration; +static void updateLocalClipboard(void); + static uint16_t readLE16(const uint8_t * data) { return (uint16_t)data[0] | @@ -557,6 +577,8 @@ static bool localTypeAvailable(LG_ClipboardData type) IsClipboardFormatAvailable(windowsCB.jpegFormat)); case LG_CLIPBOARD_DATA_FILES: + return IsClipboardFormatAvailable(CF_HDROP); + case LG_CLIPBOARD_DATA_NONE: return false; } @@ -649,36 +671,294 @@ static bool readLocalClipboard(LG_ClipboardData type, return true; } +static void releaseRemoteFilesReference(void) +{ + if (!windowsCB.remoteFiles) + return; + + windowsCB.remoteFiles->lpVtbl->Release(windowsCB.remoteFiles); + windowsCB.remoteFiles = NULL; +} + +static bool remoteFilesAreCurrent(void) +{ + return windowsCB.oleInitialized && windowsCB.remoteFiles && + OleIsCurrentClipboard(windowsCB.remoteFiles) == S_OK; +} + +static HRESULT setOleClipboard(IDataObject * object) +{ + HRESULT status = E_FAIL; + DWORD delay = 1; + // OleSetClipboard can synchronously dispatch a clipboard notification. + windowsCB.applyingRemote = true; + for (unsigned int attempt = 0; + attempt < WINDOWS_CB_OPEN_ATTEMPTS; ++attempt) + { + status = OleSetClipboard(object); + if (SUCCEEDED(status)) + break; + if (attempt + 1U < WINDOWS_CB_OPEN_ATTEMPTS) + { + Sleep(delay); + delay *= 2U; + } + } + windowsCB.applyingRemote = false; + if (windowsCB.pendingUpdate && windowsCB.accepting && windowsCB.window) + { + windowsCB.pendingUpdate = false; + if (!PostMessageW(windowsCB.window, WM_CLIPBOARDUPDATE, 0, 0)) + DEBUG_WINERROR("Failed to replay a Windows clipboard update", + GetLastError()); + } + return status; +} + +static bool clearRemoteFilesClipboard(void) +{ + if (!windowsCB.remoteFiles) + return true; + + if (remoteFilesAreCurrent()) + { + const HRESULT status = setOleClipboard(NULL); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to clear the Windows file clipboard", status); + return false; + } + } + + releaseRemoteFilesReference(); + return true; +} + +struct WindowsClipboardCapture +{ + LG_ClipboardData types[LG_CLIPBOARD_DATA_NONE]; + size_t count; + uint32_t mask; + uint8_t * fileData; + size_t fileSize; + HWND owner; + DWORD sequence; +}; + +static enum WindowsClipboardCaptureResult captureLocalClipboard( + HWND owner, DWORD sequence, struct WindowsClipboardCapture * capture) +{ + memset(capture, 0, sizeof(*capture)); + // Explorer can still own the clipboard when WM_CLIPBOARDUPDATE arrives. + // Acquire it before probing formats so that generation is retried instead + // of being recorded as an empty clipboard. + if (!openClipboard()) + return WINDOWS_CB_CAPTURE_RETRY; + + if (owner != GetClipboardOwner() || + sequence != GetClipboardSequenceNumber()) + { + CloseClipboard(); + return WINDOWS_CB_CAPTURE_RETRY; + } + + const bool files = localTypeAvailable(LG_CLIPBOARD_DATA_FILES); + bool copied = false; + if (files) + { + HANDLE handle = GetClipboardData(CF_HDROP); + copied = handle && copyGlobalData(handle, + &capture->fileData, &capture->fileSize); + } + else + { + for (LG_ClipboardData type = LG_CLIPBOARD_DATA_TEXT; + type < LG_CLIPBOARD_DATA_FILES; ++type) + { + if (!localTypeAvailable(type)) + continue; + + capture->types[capture->count++] = type; + capture->mask |= UINT32_C(1) << type; + } + } + + capture->owner = GetClipboardOwner(); + capture->sequence = GetClipboardSequenceNumber(); + const bool sequenceChanged = capture->sequence != sequence; + const BOOL closed = CloseClipboard(); + const DWORD closeError = closed ? ERROR_SUCCESS : GetLastError(); + const HWND currentOwner = GetClipboardOwner(); + const DWORD currentSequence = GetClipboardSequenceNumber(); + const bool stable = closed && capture->owner == currentOwner && + capture->sequence == currentSequence && capture->owner == owner && + (!sequenceChanged || (files && owner)); + if (!stable || (files && !copied)) + { + if (!closed) + DEBUG_WINERROR("Failed to close the Windows clipboard", closeError); + free(capture->fileData); + capture->fileData = NULL; + return WINDOWS_CB_CAPTURE_RETRY; + } + + return files ? WINDOWS_CB_CAPTURE_FILES : WINDOWS_CB_CAPTURE_TYPES; +} + +static void clearLocalRetry(void) +{ + if (windowsCB.window && windowsCB.retryDeadline) + KillTimer(windowsCB.window, WINDOWS_CB_RETRY_TIMER_ID); + windowsCB.retryDeadline = 0; + windowsCB.retrySequence = 0; +} + +static void expireLocalRetry(DWORD sequence) +{ + clearLocalRetry(); + if (!windowsCB.accepting || remoteFilesAreCurrent()) + return; + if (sequence != GetClipboardSequenceNumber()) + { + PostMessageW(windowsCB.window, WM_CLIPBOARDUPDATE, 0, 0); + return; + } + + windowsCB.localOwner = GetClipboardOwner(); + windowsCB.localSequence = sequence; + windowsCB.localTypes = 0; + clipboardFiles_clearLocal(); + clipboard_release(); +} + +static void deferLocalClipboard(DWORD sequence) +{ + const ULONGLONG now = GetTickCount64(); + if (!windowsCB.retryDeadline || windowsCB.retrySequence != sequence) + { + clearLocalRetry(); + windowsCB.retrySequence = sequence; + windowsCB.retryDeadline = now + WINDOWS_CB_RETRY_TIMEOUT_MS; + } + + if (now < windowsCB.retryDeadline && + SetTimer(windowsCB.window, WINDOWS_CB_RETRY_TIMER_ID, + WINDOWS_CB_RETRY_INTERVAL_MS, NULL)) + return; + + if (now >= windowsCB.retryDeadline) + DEBUG_WARN("Timed out waiting for the Windows clipboard"); + else + DEBUG_WINERROR("Failed to schedule a Windows clipboard retry", + GetLastError()); + expireLocalRetry(sequence); +} + +static void retryLocalClipboard(void) +{ + if (windowsCB.window) + KillTimer(windowsCB.window, WINDOWS_CB_RETRY_TIMER_ID); + if (!windowsCB.retryDeadline) + return; + if (!windowsCB.accepting || remoteFilesAreCurrent()) + { + clearLocalRetry(); + return; + } + + const DWORD sequence = GetClipboardSequenceNumber(); + if (sequence != windowsCB.retrySequence) + { + clearLocalRetry(); + PostMessageW(windowsCB.window, WM_CLIPBOARDUPDATE, 0, 0); + return; + } + if (GetTickCount64() >= windowsCB.retryDeadline) + { + DEBUG_WARN("Timed out waiting for the Windows clipboard"); + expireLocalRetry(sequence); + return; + } + + updateLocalClipboard(); +} + static void updateLocalClipboard(void) { + if (windowsCB.applyingRemote) + { + windowsCB.pendingUpdate = true; + return; + } + const HWND owner = GetClipboardOwner(); const DWORD sequence = GetClipboardSequenceNumber(); + if (remoteFilesAreCurrent()) + { + clearLocalRetry(); + return; + } + releaseRemoteFilesReference(); 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; + if (!windowsCB.retryDeadline || windowsCB.retrySequence != sequence) + atomic_fetch_add_explicit( + &windowsCBGeneration, 1, memory_order_acq_rel); - types[count++] = type; - mask |= UINT32_C(1) << type; + struct WindowsClipboardCapture capture; + const enum WindowsClipboardCaptureResult result = + captureLocalClipboard(owner, sequence, &capture); + if (result == WINDOWS_CB_CAPTURE_RETRY) + { + if (owner != GetClipboardOwner() || + sequence != GetClipboardSequenceNumber()) + { + clearLocalRetry(); + PostMessageW(windowsCB.window, WM_CLIPBOARDUPDATE, 0, 0); + } + else + deferLocalClipboard(sequence); + return; } - 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); + if (result == WINDOWS_CB_CAPTURE_FILES) + { + HGLOBAL duplicate = makeGlobalData(capture.fileData, capture.fileSize); + free(capture.fileData); + if (!duplicate) + { + deferLocalClipboard(capture.sequence); + return; + } + + const bool published = windowsClipboardFiles_setLocalDrop( + (HDROP)duplicate); + GlobalFree(duplicate); + if (!published) + { + DEBUG_WARN("Failed to capture the Windows file clipboard"); + deferLocalClipboard(capture.sequence); + return; + } + + clearLocalRetry(); + windowsCB.localOwner = capture.owner; + windowsCB.localSequence = capture.sequence; + windowsCB.localTypes = UINT32_C(1) << LG_CLIPBOARD_DATA_FILES; + return; + } + + clearLocalRetry(); + windowsCB.localOwner = capture.owner; + windowsCB.localSequence = capture.sequence; + windowsCB.localTypes = capture.mask; + clipboardFiles_clearLocal(); + if (capture.count) + clipboard_notifyTypes(capture.types, capture.count); else clipboard_release(); } @@ -827,7 +1107,8 @@ static void handleRemoteData( } if (operation->generation != atomic_load_explicit( - &windowsCBGeneration, memory_order_acquire) || !openClipboard()) + &windowsCBGeneration, memory_order_acquire) || + !clearRemoteFilesClipboard() || !openClipboard()) goto fail; if (operation->generation != atomic_load_explicit( &windowsCBGeneration, memory_order_acquire) || !EmptyClipboard()) @@ -835,7 +1116,6 @@ static void handleRemoteData( CloseClipboard(); goto fail; } - bool published = false; for (size_t i = 0; i < count; ++i) { @@ -853,6 +1133,7 @@ static void handleRemoteData( GetLastError()); } CloseClipboard(); + clipboardFiles_clearLocal(); windowsCB.localOwner = GetClipboardOwner(); windowsCB.localSequence = GetClipboardSequenceNumber(); windowsCB.localTypes = 0; @@ -865,12 +1146,96 @@ fail: GlobalFree(handles[i]); } +static void handleRemoteFiles( + const struct WindowsClipboardOperation * operation) +{ + if (!windowsCB.oleInitialized) + { + DEBUG_WARN("OLE is unavailable; cannot publish guest clipboard files"); + return; + } + + const uint64_t presentation = + clipboardFiles_remotePresentationAcquire(); + if (!presentation) + { + DEBUG_WARN("Failed to acquire the guest clipboard file offer"); + return; + } + + IDataObject * object = NULL; + if (!windowsClipboardFiles_createRemoteDataObject( + presentation, &object) || !object) + { + clipboardFiles_remotePresentationRelease(presentation); + DEBUG_WARN("Failed to create the guest clipboard file object"); + return; + } + + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire)) + { + object->lpVtbl->Release(object); + return; + } + + const HRESULT status = setOleClipboard(object); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to publish guest clipboard files", status); + object->lpVtbl->Release(object); + return; + } + + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire)) + { + const HRESULT clearStatus = OleIsCurrentClipboard(object) == S_OK ? + setOleClipboard(NULL) : S_OK; + releaseRemoteFilesReference(); + if (FAILED(clearStatus) && OleIsCurrentClipboard(object) == S_OK) + { + DEBUG_WINERROR("Failed to withdraw stale guest clipboard files", + clearStatus); + windowsCB.remoteFiles = object; + } + else + object->lpVtbl->Release(object); + return; + } + + releaseRemoteFilesReference(); + windowsCB.remoteFiles = object; + windowsCB.localOwner = GetClipboardOwner(); + windowsCB.localSequence = GetClipboardSequenceNumber(); + windowsCB.localTypes = 0; + clipboardFiles_clearLocal(); + clipboardFiles_remotePresentationDelivered(presentation); +} + static void handleRemoteRelease( const struct WindowsClipboardOperation * operation) { if (operation->generation != atomic_load_explicit( - &windowsCBGeneration, memory_order_acquire) || - GetClipboardOwner() != windowsCB.window || !openClipboard()) + &windowsCBGeneration, memory_order_acquire)) + return; + + if (windowsCB.remoteFiles) + { + const bool current = remoteFilesAreCurrent(); + if (!clearRemoteFilesClipboard()) + return; + if (current) + { + windowsCB.localOwner = GetClipboardOwner(); + windowsCB.localSequence = GetClipboardSequenceNumber(); + windowsCB.localTypes = 0; + clipboardFiles_clearLocal(); + } + return; + } + + if (GetClipboardOwner() != windowsCB.window || !openClipboard()) return; bool released = false; @@ -884,6 +1249,7 @@ static void handleRemoteRelease( windowsCB.localOwner = GetClipboardOwner(); windowsCB.localSequence = GetClipboardSequenceNumber(); windowsCB.localTypes = 0; + clipboardFiles_clearLocal(); } } @@ -997,7 +1363,7 @@ static void handleRemoteNotice( return; if (operation->dataType == LG_CLIPBOARD_DATA_FILES) { - handleRemoteRelease(operation); + handleRemoteFiles(operation); return; } if (operation->dataType >= LG_CLIPBOARD_DATA_NONE) @@ -1052,6 +1418,13 @@ static bool eventInit(HWND window) if (!window) return false; + const HRESULT oleStatus = OleInitialize(NULL); + if (SUCCEEDED(oleStatus)) + windowsCB.oleInitialized = true; + else + DEBUG_WINERROR("Failed to initialize OLE for clipboard files", + oleStatus); + windowsCB.pngFormat = RegisterClipboardFormatW(L"PNG"); windowsCB.jfifFormat = RegisterClipboardFormatW(L"JFIF"); windowsCB.jpegFormat = RegisterClipboardFormatW(L"JPEG"); @@ -1060,6 +1433,11 @@ static bool eventInit(HWND window) { DEBUG_WINERROR("Failed to register Windows clipboard formats", GetLastError()); + if (windowsCB.oleInitialized) + { + OleUninitialize(); + windowsCB.oleInitialized = false; + } return false; } @@ -1067,6 +1445,11 @@ static bool eventInit(HWND window) { DEBUG_WINERROR("Failed to register the Windows clipboard listener", GetLastError()); + if (windowsCB.oleInitialized) + { + OleUninitialize(); + windowsCB.oleInitialized = false; + } return false; } @@ -1083,7 +1466,6 @@ static bool eventInit(HWND window) bool windowsCBHandleMessage(HWND window, UINT message, WPARAM wParam, LPARAM lParam, LRESULT * result) { - (void)wParam; (void)lParam; switch (message) @@ -1102,6 +1484,13 @@ bool windowsCBHandleMessage(HWND window, UINT message, updateLocalClipboard(); *result = 0; return true; + + case WM_TIMER: + if ((UINT_PTR)wParam != WINDOWS_CB_RETRY_TIMER_ID) + return false; + retryLocalClipboard(); + *result = 0; + return true; } return false; @@ -1113,6 +1502,7 @@ void windowsCBEventShutdown(void) bool listener; struct WindowsClipboardOperation * operations; + clearLocalRetry(); AcquireSRWLockExclusive(&windowsCBQueueLock); window = windowsCB.window; listener = windowsCB.listener; @@ -1129,6 +1519,13 @@ void windowsCBEventShutdown(void) if (listener && window && !RemoveClipboardFormatListener(window)) DEBUG_WINERROR("Failed to remove the Windows clipboard listener", GetLastError()); + if (!clearRemoteFilesClipboard()) + releaseRemoteFilesReference(); + if (windowsCB.oleInitialized) + { + OleUninitialize(); + windowsCB.oleInitialized = false; + } freeOperations(operations); } diff --git a/client/include/core/clipboard.h b/client/include/core/clipboard.h index 80ae17dd..e6bb0254 100644 --- a/client/include/core/clipboard.h +++ b/client/include/core/clipboard.h @@ -61,7 +61,7 @@ bool clipboard_requestCancel(LG_ClipboardRequest request, bool clipboard_request(LG_ClipboardData type, LG_ClipboardReplyFn replyFn, void * opaque); -/* File-dataset transport entry points used by clipboard_files.c. */ +/* File-dataset transport entry points used by platform clipboard backends. */ bool clipboard_fileAcquire(uint64_t dataset, uint64_t acquisition); bool clipboard_fileAcquired(uint64_t dataset, uint64_t acquisition, LG_ClipboardFileError error); diff --git a/client/include/core/clipboard_files.h b/client/include/core/clipboard_files.h index 75a4cce0..41b3956b 100644 --- a/client/include/core/clipboard_files.h +++ b/client/include/core/clipboard_files.h @@ -23,6 +23,10 @@ #include "interface/clipboard.h" +#ifdef __cplusplus +extern "C" { +#endif + bool clipboardFiles_init(void); void clipboardFiles_free(void); @@ -83,4 +87,8 @@ bool clipboardFiles_testLocalRequest( const LG_ClipboardFileRequest * request); #endif +#ifdef __cplusplus +} +#endif + #endif diff --git a/client/include/platform/windows/clipboard_files.h b/client/include/platform/windows/clipboard_files.h new file mode 100644 index 00000000..535e0a4b --- /dev/null +++ b/client/include/platform/windows/clipboard_files.h @@ -0,0 +1,43 @@ +/** + * 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_LG_PLATFORM_WINDOWS_CLIPBOARD_FILES_ +#define _H_LG_PLATFORM_WINDOWS_CLIPBOARD_FILES_ + +#include +#include + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +bool windowsClipboardFiles_setLocalDrop(HDROP drop); +bool windowsClipboardFiles_createRemoteDataObject(uint64_t presentation, + IDataObject ** dataObject); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/client/include/platform/windows/clipboard_files_win32.h b/client/include/platform/windows/clipboard_files_win32.h new file mode 100644 index 00000000..7ed3d25f --- /dev/null +++ b/client/include/platform/windows/clipboard_files_win32.h @@ -0,0 +1,88 @@ +/** + * 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_LG_PLATFORM_WINDOWS_CLIPBOARD_FILES_WIN32_ +#define _H_LG_PLATFORM_WINDOWS_CLIPBOARD_FILES_WIN32_ + +#include "interface/clipboard.h" + +#include +#include +#include + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct WindowsClipboardLocalFiles WindowsClipboardLocalFiles; +typedef struct WindowsClipboardRemoteManifest WindowsClipboardRemoteManifest; + +typedef struct WindowsClipboardRemoteProviderOps +{ + HRESULT (*read )(void * opaque, uint64_t dataset, uint64_t acquisition, + uint64_t node, uint64_t offset, void * data, ULONG length, + ULONG * read); + void (*release)(void * opaque, uint64_t dataset, uint64_t acquisition); +} +WindowsClipboardRemoteProviderOps; + +/* Capture copies the selection metadata. List and read return malloc-owned + * buffers which the caller releases with free(). */ +WindowsClipboardLocalFiles * windowsClipboardLocalFilesCapture(HDROP drop, + LG_ClipboardFileError * error); +void windowsClipboardLocalFilesRetain(WindowsClipboardLocalFiles * files); +void windowsClipboardLocalFilesRelease(WindowsClipboardLocalFiles * files); +bool windowsClipboardLocalFilesList(WindowsClipboardLocalFiles * files, + uint64_t node, void ** data, size_t * size, + LG_ClipboardFileError * error); +bool windowsClipboardLocalFilesRead(WindowsClipboardLocalFiles * files, + uint64_t node, uint64_t offset, uint32_t length, void ** data, + size_t * size, LG_ClipboardFileError * error); + +WindowsClipboardRemoteManifest * windowsClipboardRemoteManifestCreate(void); +void windowsClipboardRemoteManifestFree( + WindowsClipboardRemoteManifest * manifest); +/* Call NextDirectory before each LIST request, then append the complete + * response for the returned node. The first returned node is the wire root. */ +bool windowsClipboardRemoteManifestAppendList( + WindowsClipboardRemoteManifest * manifest, uint64_t parent, + const void * data, size_t size, LG_ClipboardFileError * error); +bool windowsClipboardRemoteManifestNextDirectory( + WindowsClipboardRemoteManifest * manifest, uint64_t * node); +bool windowsClipboardRemoteManifestHasEntries( + const WindowsClipboardRemoteManifest * manifest); + +/* The data object owns one provider lease after a successful return. The + * release callback runs once after both the object and all of its streams are + * released. A failed call does not invoke it. */ +HRESULT windowsClipboardRemoteCreateDataObject( + WindowsClipboardRemoteManifest * manifest, uint64_t dataset, + uint64_t acquisition, const WindowsClipboardRemoteProviderOps * ops, + void * opaque, IDataObject ** object); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/client/platform/Windows/CMakeLists.txt b/client/platform/Windows/CMakeLists.txt index cceb8962..97df2a91 100644 --- a/client/platform/Windows/CMakeLists.txt +++ b/client/platform/Windows/CMakeLists.txt @@ -3,6 +3,7 @@ project(client_platform_Windows LANGUAGES C CXX) add_library(client_platform_Windows STATIC clipboard_files.c + clipboard_files_win32.c config.c font.cpp input.c @@ -16,6 +17,7 @@ target_link_libraries(client_platform_Windows ole32 shell32 shlwapi + user32 uuid ) diff --git a/client/platform/Windows/clipboard_files.c b/client/platform/Windows/clipboard_files.c index c2aa8975..b9724627 100644 --- a/client/platform/Windows/clipboard_files.c +++ b/client/platform/Windows/clipboard_files.c @@ -20,14 +20,1115 @@ #include "core/clipboard_files.h" #include "core/clipboard.h" +#include "platform/windows/clipboard_files.h" +#include "platform/windows/clipboard_files_win32.h" + +#include "common/debug.h" +#include "common/event.h" +#include "common/locking.h" +#include "common/random.h" +#include "common/thread.h" + +#include + +#include +#include +#include +#include +#include + +#define LOCAL_WORKERS 2U +#define MAX_LIST_BYTES (64U * 1024U * 1024U) +#define REMOTE_TIMEOUT_MS 30000U + +enum RemoteActionType +{ + REMOTE_ACTION_NONE, + REMOTE_ACTION_CANCEL, + REMOTE_ACTION_RELEASE, +}; + +struct LocalDataset +{ + struct LocalDataset * next; + WindowsClipboardLocalFiles * files; + uint64_t dataset; + unsigned int references; + bool current; +}; + +struct LocalAcquisition +{ + struct LocalAcquisition * next; + struct LocalDataset * dataset; + uint64_t wireDataset; + uint64_t acquisition; +}; + +struct LocalJob +{ + struct LocalJob * next; + struct LocalDataset * dataset; + LG_ClipboardFileRequest request; + LGEvent * ready; + atomic_bool cancelled; +}; + +struct RemoteDataset +{ + struct RemoteDataset * next; + WindowsClipboardRemoteManifest * manifestData; + uint64_t dataset; + uint64_t acquisition; + uint64_t currentRequest; + unsigned int presentationPins; + bool current; + bool acquiring; + bool acquired; + bool manifest; + bool ready; + bool stale; +}; + +struct RemoteRequest +{ + struct RemoteRequest * next; + struct RemoteDataset * dataset; + LG_ClipboardFileRequest request; + LGEvent * completeEvent; + uint8_t * data; + size_t dataSize; + size_t dataCapacity; + uint8_t * output; + size_t outputCapacity; + uint64_t sizeHint; + LG_ClipboardFileError error; + bool began; + bool complete; + bool manifest; +}; + +struct RemoteAction +{ + enum RemoteActionType type; + struct RemoteDataset * object; + uint64_t dataset; + uint64_t acquisition; +}; + +struct RemoteProviderContext +{ + LG_Lock lock; + LGEvent * idle; + unsigned int references; + unsigned int calls; + bool accepting; +}; + +static struct +{ + LG_Lock lock; + bool lockInitialized; + bool initialized; + bool stopping; + uint64_t transferSerial; + uint64_t datasetSerial; + struct LocalDataset * localDatasets; + struct LocalDataset * localCurrent; + struct LocalAcquisition * localAcquisitions; + struct LocalJob * jobsHead; + struct LocalJob ** jobsTail; + struct LocalJob * activeJobs; + struct RemoteDataset * remoteDatasets; + struct RemoteDataset * remoteCurrent; + struct RemoteRequest * remoteRequests; + LGEvent * jobEvent; + struct RemoteProviderContext * provider; + LGThread * workers[LOCAL_WORKERS]; +} +files; + +static HRESULT fileErrorHRESULT(LG_ClipboardFileError error) +{ + switch (error) + { + case LG_CLIPBOARD_FILE_ERROR_NONE: + return S_OK; + case LG_CLIPBOARD_FILE_ERROR_ACCESS: + return STG_E_ACCESSDENIED; + case LG_CLIPBOARD_FILE_ERROR_NO_MEMORY: + return E_OUTOFMEMORY; + case LG_CLIPBOARD_FILE_ERROR_INVALID: + return STG_E_INVALIDPARAMETER; + default: + return STG_E_READFAULT; + } +} + +static uint64_t nextTransferNL(void) +{ + do + ++files.transferSerial; + while (!files.transferSerial || + kvmfrClipboardTransferFromHelper(files.transferSerial)); + return files.transferSerial; +} + +static uint64_t nextDatasetNL(void) +{ + do + ++files.datasetSerial; + while (!files.datasetSerial || + kvmfrClipboardTransferFromHelper(files.datasetSerial)); + return files.datasetSerial; +} + +static bool growBuffer(uint8_t ** buffer, size_t * capacity, size_t wanted) +{ + if (wanted <= *capacity) + return !wanted || *buffer; + + size_t next = *capacity ? *capacity : 4096U; + while (next < wanted) + { + if (next > SIZE_MAX / 2U) + { + next = wanted; + break; + } + next *= 2U; + } + + void * resized = realloc(*buffer, next); + if (!resized) + return false; + *buffer = resized; + *capacity = next; + return true; +} + +static struct LocalDataset * localDatasetByIdNL(uint64_t dataset) +{ + for (struct LocalDataset * item = files.localDatasets; + item; item = item->next) + if (item->dataset == dataset) + return item; + return NULL; +} + +static struct LocalDataset * acquiredLocalDatasetNL(uint64_t dataset) +{ + for (struct LocalAcquisition * item = files.localAcquisitions; + item; item = item->next) + if (item->wireDataset == dataset) + return item->dataset; + return NULL; +} + +static bool localAcquisitionIdExistsNL(uint64_t acquisition) +{ + for (struct LocalAcquisition * item = files.localAcquisitions; + item; item = item->next) + if (item->acquisition == acquisition) + return true; + return false; +} + +static bool localJobIdExistsNL(uint64_t request) +{ + for (struct LocalJob * job = files.jobsHead; job; job = job->next) + if (job->request.request == request) + return true; + for (struct LocalJob * job = files.activeJobs; job; job = job->next) + if (job->request.request == request) + return true; + return false; +} + +static size_t localAcquisitionCountNL(void) +{ + size_t count = 0; + for (struct LocalAcquisition * item = files.localAcquisitions; + item; item = item->next) + ++count; + return count; +} + +static size_t localJobCountNL(void) +{ + size_t count = 0; + for (struct LocalJob * job = files.jobsHead; job; job = job->next) + ++count; + for (struct LocalJob * job = files.activeJobs; job; job = job->next) + ++count; + return count; +} + +static size_t remoteDatasetCountNL(void) +{ + size_t count = 0; + for (struct RemoteDataset * item = files.remoteDatasets; + item; item = item->next) + ++count; + return count; +} + +static size_t remoteRequestCountNL(void) +{ + size_t count = 0; + for (struct RemoteRequest * item = files.remoteRequests; + item; item = item->next) + ++count; + return count; +} + +static struct LocalDataset * takeUnusedLocalDatasetNL( + struct LocalDataset * dataset) +{ + if (!dataset || dataset->current || dataset->references) + return NULL; + + struct LocalDataset ** link = &files.localDatasets; + while (*link && *link != dataset) + link = &(*link)->next; + if (!*link) + return NULL; + *link = dataset->next; + dataset->next = NULL; + return dataset; +} + +static struct LocalDataset * takeAllUnusedLocalDatasetsNL(void) +{ + struct LocalDataset * retired = NULL; + struct LocalDataset ** link = &files.localDatasets; + while (*link) + { + struct LocalDataset * dataset = *link; + if (dataset->current || dataset->references) + { + link = &dataset->next; + continue; + } + *link = dataset->next; + dataset->next = retired; + retired = dataset; + } + return retired; +} + +static void freeLocalDatasets(struct LocalDataset * datasets) +{ + while (datasets) + { + struct LocalDataset * next = datasets->next; + windowsClipboardLocalFilesRelease(datasets->files); + free(datasets); + datasets = next; + } +} + +static struct RemoteDataset * remoteDatasetByIdNL(uint64_t dataset) +{ + for (struct RemoteDataset * item = files.remoteDatasets; + item; item = item->next) + if (item->dataset == dataset) + return item; + return NULL; +} + +static struct RemoteDataset * remoteDatasetByAcquisitionNL( + uint64_t acquisition) +{ + for (struct RemoteDataset * item = files.remoteDatasets; + item; item = item->next) + if (item->acquisition == acquisition) + return item; + return NULL; +} + +static struct RemoteRequest * remoteRequestByIdNL(uint64_t request) +{ + for (struct RemoteRequest * item = files.remoteRequests; + item; item = item->next) + if (item->request.request == request) + return item; + return NULL; +} + +static struct RemoteRequest * takeRemoteRequestNL(uint64_t request) +{ + struct RemoteRequest ** link = &files.remoteRequests; + while (*link && (*link)->request.request != request) + link = &(*link)->next; + struct RemoteRequest * result = *link; + if (result) + { + *link = result->next; + result->next = NULL; + } + return result; +} + +static void freeRemoteRequest(struct RemoteRequest * request) +{ + if (!request) + return; + if (request->completeEvent) + lgFreeEvent(request->completeEvent); + free(request->data); + free(request); +} + +static void cancelRemoteManifest(struct RemoteRequest * request, + LG_ClipboardFileError reason) +{ + if (!request) + return; + + if (!clipboard_fileCancel(request->request.dataset, + request->request.request, reason)) + DEBUG_WARN("Failed to cancel a clipboard file manifest request"); + freeRemoteRequest(request); +} + +/* files.lock must be held. Read requests remain owned by their waiting + * providers; only manifest requests are returned to the caller. */ +static struct RemoteRequest * disconnectRemoteRequestsNL(void) +{ + struct RemoteRequest * manifests = NULL; + struct RemoteRequest * request = files.remoteRequests; + files.remoteRequests = NULL; + + while (request) + { + struct RemoteRequest * next = request->next; + request->next = NULL; + request->error = LG_CLIPBOARD_FILE_ERROR_DISCONNECTED; + request->complete = true; + if (request->manifest) + { + request->next = manifests; + manifests = request; + } + else if (request->completeEvent) + lgSignalEvent(request->completeEvent); + request = next; + } + + return manifests; +} + +static struct RemoteAction retireRemoteDatasetNL( + struct RemoteDataset * dataset) +{ + struct RemoteAction action = { 0 }; + if (!dataset || dataset->current || dataset->manifest || + dataset->presentationPins) + return action; + + struct RemoteDataset ** link = &files.remoteDatasets; + while (*link && *link != dataset) + link = &(*link)->next; + if (!*link) + return action; + *link = dataset->next; + + dataset->next = NULL; + dataset->ready = false; + dataset->stale = true; + action.object = dataset; + action.dataset = dataset->dataset; + action.acquisition = dataset->acquisition; + if (dataset->acquired) + action.type = REMOTE_ACTION_RELEASE; + else if (dataset->acquiring) + action.type = REMOTE_ACTION_CANCEL; + dataset->acquired = false; + dataset->acquiring = false; + return action; +} + +static void performRemoteAction(struct RemoteAction action) +{ + switch (action.type) + { + case REMOTE_ACTION_CANCEL: + if (!clipboard_fileCancel(action.dataset, action.acquisition, + LG_CLIPBOARD_FILE_ERROR_CANCELLED)) + DEBUG_WARN("Failed to cancel a clipboard file acquisition"); + break; + + case REMOTE_ACTION_RELEASE: + if (!clipboard_fileRelease(action.dataset, action.acquisition)) + DEBUG_WARN("Failed to release a clipboard file acquisition"); + break; + + case REMOTE_ACTION_NONE: + break; + } + + if (action.object) + { + windowsClipboardRemoteManifestFree(action.object->manifestData); + free(action.object); + } +} + +static bool sameRemoteRequest(const struct RemoteRequest * request, + const LG_ClipboardFileRequest * descriptor) +{ + return request->request.dataset == descriptor->dataset && + request->request.request == descriptor->request && + request->request.node == descriptor->node && + request->request.offset == descriptor->offset && + request->request.length == descriptor->length && + request->request.operation == descriptor->operation; +} + +static void failRemoteDataset(struct RemoteDataset * dataset, + LG_ClipboardFileError error) +{ + if (!dataset) + return; + + struct RemoteRequest * request = NULL; + struct RemoteAction action = { 0 }; + uint64_t failed = 0; + LG_LOCK(files.lock); + if (!dataset->stale && remoteDatasetByIdNL(dataset->dataset) == dataset) + { + failed = dataset->dataset; + if (dataset->currentRequest) + request = takeRemoteRequestNL(dataset->currentRequest); + dataset->currentRequest = 0; + dataset->manifest = false; + dataset->current = false; + dataset->ready = false; + if (files.remoteCurrent == dataset) + files.remoteCurrent = NULL; + action = retireRemoteDatasetNL(dataset); + } + LG_UNLOCK(files.lock); + + if (request) + cancelRemoteManifest(request, error); + performRemoteAction(action); + if (failed) + clipboard_fileRemoteFailed(failed); +} + +static bool startRemoteList(struct RemoteDataset * dataset, uint64_t parent) +{ + struct RemoteRequest * request = calloc(1, sizeof(*request)); + if (!request) + { + failRemoteDataset(dataset, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + return false; + } + + LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_NO_SPACE; + LG_LOCK(files.lock); + if (!files.stopping && dataset && !dataset->stale && + dataset->current && dataset->manifest && + !dataset->currentRequest && + remoteRequestCountNL() + localJobCountNL() < + KVMFR_CLIPBOARD_FILE_MAX_REQUESTS) + { + request->dataset = dataset; + request->request.dataset = dataset->dataset; + request->request.request = nextTransferNL(); + request->request.node = parent; + request->request.operation = LG_CLIPBOARD_FILE_LIST; + request->sizeHint = LG_CLIPBOARD_SIZE_UNKNOWN; + request->manifest = true; + request->next = files.remoteRequests; + files.remoteRequests = request; + dataset->currentRequest = request->request.request; + error = LG_CLIPBOARD_FILE_ERROR_NONE; + } + LG_UNLOCK(files.lock); + + if (error) + { + freeRemoteRequest(request); + failRemoteDataset(dataset, error); + return false; + } + if (clipboard_fileRequest(&request->request)) + return true; + + LG_LOCK(files.lock); + struct RemoteRequest * removed = takeRemoteRequestNL( + request->request.request); + if (dataset->currentRequest == request->request.request) + dataset->currentRequest = 0; + LG_UNLOCK(files.lock); + freeRemoteRequest(removed); + failRemoteDataset(dataset, LG_CLIPBOARD_FILE_ERROR_DISCONNECTED); + return false; +} + +static void continueRemoteManifest(struct RemoteRequest * request) +{ + if (!request || !request->dataset) + { + freeRemoteRequest(request); + return; + } + + struct RemoteDataset * dataset = request->dataset; + LG_ClipboardFileError error = request->error; + uint64_t next = 0; + bool ready = false; + + LG_LOCK(files.lock); + const bool current = !dataset->stale && dataset->current && + dataset->manifest && + dataset->currentRequest == request->request.request; + if (current && !error && !windowsClipboardRemoteManifestAppendList( + dataset->manifestData, request->request.node, request->data, + request->dataSize, &error)) + error = error ? error : LG_CLIPBOARD_FILE_ERROR_INVALID; + if (current && !error) + { + dataset->currentRequest = 0; + const bool hasNext = windowsClipboardRemoteManifestNextDirectory( + dataset->manifestData, &next); + if (!hasNext) + { + if (!windowsClipboardRemoteManifestHasEntries(dataset->manifestData)) + error = LG_CLIPBOARD_FILE_ERROR_INVALID; + else + { + dataset->manifest = false; + dataset->ready = true; + ready = true; + } + } + } + LG_UNLOCK(files.lock); + freeRemoteRequest(request); + + if (!current) + return; + if (error) + { + failRemoteDataset(dataset, error); + return; + } + if (next) + { + startRemoteList(dataset, next); + return; + } + if (ready) + clipboard_fileRemoteReady(dataset->dataset); +} + +static struct RemoteProviderContext * providerCreate(void) +{ + struct RemoteProviderContext * context = calloc(1, sizeof(*context)); + if (!context) + return NULL; + + LG_LOCK_INIT(context->lock); + context->idle = lgCreateEvent(false, 0); + if (!context->idle) + { + LG_LOCK_FREE(context->lock); + free(context); + return NULL; + } + context->references = 1; + context->accepting = true; + lgSignalEvent(context->idle); + return context; +} + +static void providerRetain(struct RemoteProviderContext * context) +{ + if (!context) + return; + + LG_LOCK(context->lock); + ++context->references; + LG_UNLOCK(context->lock); +} + +static void providerRelease(struct RemoteProviderContext * context) +{ + if (!context) + return; + + LG_LOCK(context->lock); + const bool destroy = context->references && !--context->references; + LG_UNLOCK(context->lock); + if (!destroy) + return; + + lgFreeEvent(context->idle); + LG_LOCK_FREE(context->lock); + free(context); +} + +static bool providerEnter(struct RemoteProviderContext * context) +{ + if (!context) + return false; + + LG_LOCK(context->lock); + if (!context->accepting) + { + LG_UNLOCK(context->lock); + return false; + } + if (!context->calls) + lgResetEvent(context->idle); + ++context->calls; + LG_UNLOCK(context->lock); + return true; +} + +static void providerLeave(struct RemoteProviderContext * context) +{ + LG_LOCK(context->lock); + if (context->calls && !--context->calls) + lgSignalEvent(context->idle); + LG_UNLOCK(context->lock); +} + +static void providerDisable(struct RemoteProviderContext * context) +{ + if (!context) + return; + + LG_LOCK(context->lock); + context->accepting = false; + LG_UNLOCK(context->lock); +} + +static void providerWait(struct RemoteProviderContext * context) +{ + if (!context) + return; + + LG_LOCK(context->lock); + const bool idle = !context->calls; + LG_UNLOCK(context->lock); + if (!idle) + lgWaitEvent(context->idle, TIMEOUT_INFINITE); +} + +static HRESULT remoteProviderRead(void * opaque, uint64_t datasetId, + uint64_t acquisition, uint64_t node, uint64_t offset, void * output, + ULONG length, ULONG * read) +{ + struct RemoteProviderContext * context = opaque; + if (!read || (!output && length)) + return STG_E_INVALIDPOINTER; + *read = 0; + if (!providerEnter(context)) + return STG_E_READFAULT; + + HRESULT result = S_OK; + uint8_t * destination = output; + while (length) + { + const uint32_t wanted = + length > KVMFR_CLIPBOARD_FILE_READ_BYTES ? + KVMFR_CLIPBOARD_FILE_READ_BYTES : (uint32_t)length; + struct RemoteRequest * request = calloc(1, sizeof(*request)); + if (!request) + { + result = E_OUTOFMEMORY; + break; + } + request->completeEvent = lgCreateEvent(true, 0); + if (!request->completeEvent) + { + freeRemoteRequest(request); + result = E_OUTOFMEMORY; + break; + } + + LG_LOCK(files.lock); + struct RemoteDataset * dataset = remoteDatasetByIdNL(datasetId); + const bool valid = !files.stopping && dataset && + dataset->acquisition == acquisition && + dataset->acquired && !dataset->stale && dataset->presentationPins && + remoteRequestCountNL() + localJobCountNL() < + KVMFR_CLIPBOARD_FILE_MAX_REQUESTS; + if (valid) + { + request->dataset = dataset; + request->request.dataset = datasetId; + request->request.request = nextTransferNL(); + request->request.node = node; + request->request.offset = offset; + request->request.length = wanted; + request->request.operation = LG_CLIPBOARD_FILE_READ; + request->output = destination; + request->outputCapacity = wanted; + request->sizeHint = LG_CLIPBOARD_SIZE_UNKNOWN; + request->next = files.remoteRequests; + files.remoteRequests = request; + } + LG_UNLOCK(files.lock); + if (!valid) + { + freeRemoteRequest(request); + result = STG_E_READFAULT; + break; + } + + if (!clipboard_fileRequest(&request->request)) + { + LG_LOCK(files.lock); + takeRemoteRequestNL(request->request.request); + LG_UNLOCK(files.lock); + freeRemoteRequest(request); + result = STG_E_READFAULT; + break; + } + + const bool signalled = lgWaitEvent(request->completeEvent, + REMOTE_TIMEOUT_MS); + bool removed; + LG_LOCK(files.lock); + removed = takeRemoteRequestNL(request->request.request) == request; + if (!request->complete) + { + request->error = LG_CLIPBOARD_FILE_ERROR_IO; + request->complete = true; + } + const LG_ClipboardFileError error = request->error; + const size_t actual = request->dataSize; + LG_UNLOCK(files.lock); + if (!signalled && removed) + clipboard_fileCancel(datasetId, request->request.request, + LG_CLIPBOARD_FILE_ERROR_IO); + + if (error) + { + result = fileErrorHRESULT(error); + freeRemoteRequest(request); + break; + } + if (actual != wanted) + { + result = STG_E_READFAULT; + freeRemoteRequest(request); + break; + } + + destination += actual; + offset += actual; + length -= (ULONG)actual; + *read += (ULONG)actual; + freeRemoteRequest(request); + } + + providerLeave(context); + return result; +} + +static void remoteProviderRelease(void * opaque, uint64_t datasetId, + uint64_t acquisition) +{ + struct RemoteProviderContext * context = opaque; + if (!providerEnter(context)) + { + providerRelease(context); + return; + } + + struct RemoteAction action = { 0 }; + LG_LOCK(files.lock); + struct RemoteDataset * dataset = datasetId ? + remoteDatasetByIdNL(datasetId) : + remoteDatasetByAcquisitionNL(acquisition); + if (dataset && dataset->acquisition == acquisition && + dataset->presentationPins) + { + --dataset->presentationPins; + action = retireRemoteDatasetNL(dataset); + } + LG_UNLOCK(files.lock); + performRemoteAction(action); + providerLeave(context); + providerRelease(context); +} + +static const WindowsClipboardRemoteProviderOps remoteProviderOps = +{ + .read = remoteProviderRead, + .release = remoteProviderRelease, +}; + +static bool sendLocalCall(struct LocalJob * job, int operation, + uint64_t offset, const void * data, size_t size, bool end) +{ + for (;;) + { + if (atomic_load_explicit(&job->cancelled, memory_order_acquire)) + return false; + + LG_ClipboardResult result; + switch (operation) + { + case 0: + result = clipboard_fileDataBegin(&job->request, offset); + break; + case 1: + result = clipboard_fileDataChunk(&job->request, offset, + data, size, end); + break; + default: + result = clipboard_fileDataEnd(&job->request, offset); + break; + } + if (result == LG_CLIPBOARD_RESULT_ACCEPTED) + return true; + if (result == LG_CLIPBOARD_RESULT_FAILED) + return false; + if (!lgWaitEvent(job->ready, TIMEOUT_INFINITE)) + return false; + } +} + +static bool sendLocalBuffer(struct LocalJob * job, + const uint8_t * data, size_t size) +{ + if (!sendLocalCall(job, 0, size, NULL, 0, false)) + return false; + size_t offset = 0; + while (offset < size) + { + size_t chunk = size - offset; + if (chunk > KVMFR_CLIPBOARD_DATA_BYTES) + chunk = KVMFR_CLIPBOARD_DATA_BYTES; + const bool end = chunk == size - offset; + if (!sendLocalCall(job, 1, offset, data + offset, chunk, end)) + return false; + offset += chunk; + } + return sendLocalCall(job, 2, size, NULL, 0, true); +} + +static void processLocalJob(struct LocalJob * job) +{ + LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_CANCELLED; + void * data = NULL; + size_t size = 0; + bool success = false; + if (!atomic_load_explicit(&job->cancelled, memory_order_acquire)) + { + if (job->request.operation == LG_CLIPBOARD_FILE_LIST) + success = windowsClipboardLocalFilesList(job->dataset->files, + job->request.node, &data, &size, &error); + else + success = windowsClipboardLocalFilesRead(job->dataset->files, + job->request.node, job->request.offset, job->request.length, + &data, &size, &error); + } + + if (success && !atomic_load_explicit( + &job->cancelled, memory_order_acquire)) + { + success = sendLocalBuffer(job, data, size); + if (!success) + error = LG_CLIPBOARD_FILE_ERROR_DISCONNECTED; + } + free(data); + if (!success && !atomic_load_explicit( + &job->cancelled, memory_order_acquire) && error) + clipboard_fileCancel(job->request.dataset, job->request.request, error); +} + +static int localWorker(void * opaque) +{ + (void)opaque; + for (;;) + { + LG_LOCK(files.lock); + struct LocalJob * job = files.jobsHead; + if (job) + { + files.jobsHead = job->next; + if (!files.jobsHead) + files.jobsTail = &files.jobsHead; + job->next = files.activeJobs; + files.activeJobs = job; + } + const bool stopping = files.stopping; + if (!job && !stopping) + lgResetEvent(files.jobEvent); + LG_UNLOCK(files.lock); + + if (!job) + { + if (stopping) + return 0; + lgWaitEvent(files.jobEvent, TIMEOUT_INFINITE); + continue; + } + + processLocalJob(job); + LG_LOCK(files.lock); + struct LocalJob ** link = &files.activeJobs; + while (*link && *link != job) + link = &(*link)->next; + if (*link) + *link = job->next; + if (job->dataset->references) + --job->dataset->references; + struct LocalDataset * retired = takeUnusedLocalDatasetNL(job->dataset); + LG_UNLOCK(files.lock); + freeLocalDatasets(retired); + lgFreeEvent(job->ready); + free(job); + } +} + +static void cancelLocalState(bool stop) +{ + struct LocalDataset * retired; + LG_LOCK(files.lock); + if (stop) + files.stopping = true; + while (files.localAcquisitions) + { + struct LocalAcquisition * item = files.localAcquisitions; + files.localAcquisitions = item->next; + if (item->dataset->references) + --item->dataset->references; + free(item); + } + for (struct LocalJob * job = files.jobsHead; job; job = job->next) + atomic_store_explicit(&job->cancelled, true, memory_order_release); + for (struct LocalJob * job = files.activeJobs; job; job = job->next) + { + atomic_store_explicit(&job->cancelled, true, memory_order_release); + lgSignalEvent(job->ready); + } + retired = takeAllUnusedLocalDatasetsNL(); + LG_UNLOCK(files.lock); + freeLocalDatasets(retired); + if (files.jobEvent) + lgSignalEvent(files.jobEvent); +} bool clipboardFiles_init(void) { + if (files.lockInitialized) + return false; + memset(&files, 0, sizeof(files)); + LG_LOCK_INIT(files.lock); + files.lockInitialized = true; + + uint64_t nonce = 0; + if (!lgRandomBytes(&nonce, sizeof(nonce))) + { + DEBUG_ERROR("Failed to seed clipboard file identifiers"); + goto fail; + } + nonce &= ~KVMFR_CLIPBOARD_TRANSFER_HELPER; + if (!nonce) + nonce = 1; + files.transferSerial = nonce; + files.datasetSerial = (nonce ^ UINT64_C(0x4c474346494c4553)) & + ~KVMFR_CLIPBOARD_TRANSFER_HELPER; + if (!files.datasetSerial) + files.datasetSerial = 1; + + files.jobsTail = &files.jobsHead; + files.jobEvent = lgCreateEvent(false, 0); + files.provider = providerCreate(); + if (!files.jobEvent || !files.provider) + goto fail; + for (unsigned int index = 0; index < LOCAL_WORKERS; ++index) + if (!lgCreateThread("clipboard-files", localWorker, NULL, + &files.workers[index])) + goto fail; + files.initialized = true; return true; + +fail: + cancelLocalState(true); + for (unsigned int index = 0; index < LOCAL_WORKERS; ++index) + if (files.workers[index]) + lgJoinThread(files.workers[index], NULL); + if (files.jobEvent) + lgFreeEvent(files.jobEvent); + providerDisable(files.provider); + providerWait(files.provider); + providerRelease(files.provider); + files.jobEvent = NULL; + files.provider = NULL; + files.lockInitialized = false; + LG_LOCK_FREE(files.lock); + return false; } void clipboardFiles_free(void) { + if (!files.initialized) + return; + + cancelLocalState(true); + LG_LOCK(files.lock); + struct RemoteProviderContext * provider = files.provider; + files.provider = NULL; + LG_UNLOCK(files.lock); + providerDisable(provider); + + LG_LOCK(files.lock); + struct RemoteRequest * requests = disconnectRemoteRequestsNL(); + LG_UNLOCK(files.lock); + + while (requests) + { + struct RemoteRequest * next = requests->next; + freeRemoteRequest(requests); + requests = next; + } + + providerWait(provider); + + for (unsigned int index = 0; index < LOCAL_WORKERS; ++index) + if (files.workers[index]) + lgJoinThread(files.workers[index], NULL); + + LG_LOCK(files.lock); + struct RemoteDataset * datasets = files.remoteDatasets; + struct LocalDataset * local = files.localDatasets; + files.remoteDatasets = NULL; + files.remoteCurrent = NULL; + files.localDatasets = NULL; + files.localCurrent = NULL; + files.initialized = false; + LG_UNLOCK(files.lock); + + while (datasets) + { + struct RemoteDataset * next = datasets->next; + if (datasets->acquired) + clipboard_fileRelease(datasets->dataset, datasets->acquisition); + else if (datasets->acquiring) + clipboard_fileCancel(datasets->dataset, datasets->acquisition, + LG_CLIPBOARD_FILE_ERROR_CANCELLED); + windowsClipboardRemoteManifestFree(datasets->manifestData); + free(datasets); + datasets = next; + } + freeLocalDatasets(local); + + lgFreeEvent(files.jobEvent); + files.jobEvent = NULL; + files.lockInitialized = false; + LG_LOCK_FREE(files.lock); + providerRelease(provider); } bool clipboardFiles_setLocal(const char * mime, @@ -39,8 +1140,59 @@ bool clipboardFiles_setLocal(const char * mime, return false; } +bool windowsClipboardFiles_setLocalDrop(HDROP drop) +{ + LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_NONE; + WindowsClipboardLocalFiles * captured = + windowsClipboardLocalFilesCapture(drop, &error); + if (!captured) + { + DEBUG_WARN("Failed to capture Windows clipboard files: error=%u", + (unsigned int)error); + return false; + } + + struct LocalDataset * dataset = calloc(1, sizeof(*dataset)); + if (!dataset) + { + windowsClipboardLocalFilesRelease(captured); + return false; + } + dataset->files = captured; + dataset->current = true; + + LG_LOCK(files.lock); + if (!files.initialized || files.stopping) + { + LG_UNLOCK(files.lock); + freeLocalDatasets(dataset); + return false; + } + dataset->dataset = nextDatasetNL(); + dataset->next = files.localDatasets; + files.localDatasets = dataset; + if (files.localCurrent) + files.localCurrent->current = false; + struct LocalDataset * retired = takeUnusedLocalDatasetNL( + files.localCurrent); + files.localCurrent = dataset; + LG_UNLOCK(files.lock); + freeLocalDatasets(retired); + + clipboard_notifyFiles(dataset->dataset); + return true; +} + void clipboardFiles_clearLocal(void) { + LG_LOCK(files.lock); + if (files.localCurrent) + files.localCurrent->current = false; + struct LocalDataset * retired = takeUnusedLocalDatasetNL( + files.localCurrent); + files.localCurrent = NULL; + LG_UNLOCK(files.lock); + freeLocalDatasets(retired); } bool clipboardFiles_getRemote(const char * mime, @@ -56,7 +1208,46 @@ bool clipboardFiles_getRemote(const char * mime, uint64_t clipboardFiles_remotePresentationAcquire(void) { - return 0; + LG_LOCK(files.lock); + struct RemoteDataset * dataset = files.remoteCurrent; + const uint64_t presentation = dataset && dataset->ready && + dataset->acquired && !dataset->stale ? dataset->acquisition : 0; + if (presentation) + ++dataset->presentationPins; + LG_UNLOCK(files.lock); + return presentation; +} + +bool windowsClipboardFiles_createRemoteDataObject(uint64_t presentation, + IDataObject ** object) +{ + if (!presentation || !object) + return false; + *object = NULL; + + LG_LOCK(files.lock); + struct RemoteDataset * dataset = + remoteDatasetByAcquisitionNL(presentation); + const bool valid = dataset && dataset->presentationPins && + dataset->ready && dataset->acquired && !dataset->stale && files.provider; + HRESULT result = E_FAIL; + if (valid) + { + providerRetain(files.provider); + result = windowsClipboardRemoteCreateDataObject( + dataset->manifestData, dataset->dataset, dataset->acquisition, + &remoteProviderOps, files.provider, object); + if (FAILED(result)) + providerRelease(files.provider); + } + LG_UNLOCK(files.lock); + if (FAILED(result)) + { + DEBUG_WARN("Failed to create the Windows clipboard file object: " + "0x%08lx", (unsigned long)result); + return false; + } + return true; } bool clipboardFiles_getRemotePresentation(uint64_t presentation, @@ -78,105 +1269,539 @@ void clipboardFiles_remotePresentationDelivered(uint64_t presentation) void clipboardFiles_remotePresentationRelease(uint64_t presentation) { - (void)presentation; + struct RemoteAction action = { 0 }; + LG_LOCK(files.lock); + struct RemoteDataset * dataset = remoteDatasetByAcquisitionNL(presentation); + if (dataset && dataset->presentationPins) + { + --dataset->presentationPins; + action = retireRemoteDatasetNL(dataset); + } + LG_UNLOCK(files.lock); + performRemoteAction(action); } bool clipboardFiles_remoteReady(uint64_t dataset) { - (void)dataset; - return false; + LG_LOCK(files.lock); + struct RemoteDataset * found = remoteDatasetByIdNL(dataset); + const bool ready = found && found->current && found->ready && + !found->stale; + LG_UNLOCK(files.lock); + return ready; } -bool clipboardFiles_remoteOffer(uint64_t dataset) +bool clipboardFiles_remoteOffer(uint64_t datasetId) { - (void)dataset; + if (!datasetId) + return false; + + struct RemoteDataset * candidate = calloc(1, sizeof(*candidate)); + if (!candidate) + return false; + candidate->manifestData = windowsClipboardRemoteManifestCreate(); + if (!candidate->manifestData) + { + free(candidate); + return false; + } + + struct RemoteRequest * cancelled = NULL; + struct RemoteAction previousAction = { 0 }; + LG_LOCK(files.lock); + if (!files.initialized || files.stopping) + { + LG_UNLOCK(files.lock); + windowsClipboardRemoteManifestFree(candidate->manifestData); + free(candidate); + return false; + } + if (files.remoteCurrent && files.remoteCurrent->dataset == datasetId && + !files.remoteCurrent->stale) + { + LG_UNLOCK(files.lock); + windowsClipboardRemoteManifestFree(candidate->manifestData); + free(candidate); + return true; + } + + struct RemoteDataset * dataset = remoteDatasetByIdNL(datasetId); + if (!dataset && remoteDatasetCountNL() + localAcquisitionCountNL() >= + KVMFR_CLIPBOARD_FILE_MAX_ACQUISITIONS) + { + LG_UNLOCK(files.lock); + windowsClipboardRemoteManifestFree(candidate->manifestData); + free(candidate); + return false; + } + if (!dataset) + { + dataset = candidate; + candidate = NULL; + dataset->dataset = datasetId; + dataset->acquisition = nextTransferNL(); + dataset->acquiring = true; + dataset->next = files.remoteDatasets; + files.remoteDatasets = dataset; + } + + if (files.remoteCurrent) + { + struct RemoteDataset * previous = files.remoteCurrent; + previous->current = false; + if (previous->currentRequest) + { + cancelled = takeRemoteRequestNL(previous->currentRequest); + previous->currentRequest = 0; + previous->manifest = false; + } + previousAction = retireRemoteDatasetNL(previous); + } + dataset->current = true; + files.remoteCurrent = dataset; + LG_UNLOCK(files.lock); + + if (candidate) + { + windowsClipboardRemoteManifestFree(candidate->manifestData); + free(candidate); + } + cancelRemoteManifest(cancelled, LG_CLIPBOARD_FILE_ERROR_CANCELLED); + performRemoteAction(previousAction); + if (!dataset->acquiring) + { + if (dataset->ready) + clipboard_fileRemoteReady(datasetId); + return true; + } + if (clipboard_fileAcquire(datasetId, dataset->acquisition)) + return true; + failRemoteDataset(dataset, LG_CLIPBOARD_FILE_ERROR_DISCONNECTED); return false; } void clipboardFiles_remoteClear(void) { + struct RemoteRequest * cancelled = NULL; + struct RemoteAction action = { 0 }; + LG_LOCK(files.lock); + struct RemoteDataset * dataset = files.remoteCurrent; + if (dataset) + { + dataset->current = false; + if (dataset->currentRequest) + { + cancelled = takeRemoteRequestNL(dataset->currentRequest); + dataset->currentRequest = 0; + dataset->manifest = false; + } + files.remoteCurrent = NULL; + action = retireRemoteDatasetNL(dataset); + } + LG_UNLOCK(files.lock); + cancelRemoteManifest(cancelled, LG_CLIPBOARD_FILE_ERROR_CANCELLED); + performRemoteAction(action); } void clipboardFiles_providerUnavailable(void) { + if (!files.initialized) + return; + + cancelLocalState(false); + + LG_LOCK(files.lock); + struct RemoteRequest * request = disconnectRemoteRequestsNL(); + struct RemoteDataset * dataset = files.remoteDatasets; + files.remoteDatasets = NULL; + files.remoteCurrent = NULL; + LG_UNLOCK(files.lock); + + while (request) + { + struct RemoteRequest * next = request->next; + freeRemoteRequest(request); + request = next; + } + while (dataset) + { + struct RemoteDataset * next = dataset->next; + windowsClipboardRemoteManifestFree(dataset->manifestData); + free(dataset); + dataset = next; + } } -void clipboardFiles_remoteAcquired(uint64_t dataset, +void clipboardFiles_remoteAcquired(uint64_t datasetId, uint64_t acquisition, LG_ClipboardFileError error) { - (void)dataset; - (void)acquisition; - (void)error; + LG_LOCK(files.lock); + struct RemoteDataset * dataset = remoteDatasetByIdNL(datasetId); + const bool valid = dataset && + dataset->acquisition == acquisition && + dataset->acquiring && !dataset->stale; + if (valid) + { + dataset->acquiring = false; + dataset->acquired = !error; + dataset->manifest = !error; + } + LG_UNLOCK(files.lock); + if (!valid) + return; + if (error) + { + failRemoteDataset(dataset, error); + return; + } + startRemoteList(dataset, KVMFR_CLIPBOARD_FILE_ROOT_NODE); } LG_ClipboardResult clipboardFiles_remoteDataBegin( - const LG_ClipboardFileRequest * request, uint64_t sizeHint) + const LG_ClipboardFileRequest * descriptor, uint64_t sizeHint) { - (void)request; - (void)sizeHint; + if (!descriptor) + return LG_CLIPBOARD_RESULT_FAILED; + + LG_LOCK(files.lock); + struct RemoteRequest * request = + remoteRequestByIdNL(descriptor->request); + struct RemoteDataset * failedDataset = request && request->manifest ? + request->dataset : NULL; + const bool valid = request && !request->began && + !request->complete && + sameRemoteRequest(request, descriptor) && + (descriptor->operation != LG_CLIPBOARD_FILE_READ || + sizeHint == descriptor->length) && + (descriptor->operation != LG_CLIPBOARD_FILE_LIST || + sizeHint == LG_CLIPBOARD_SIZE_UNKNOWN || sizeHint <= MAX_LIST_BYTES); + if (valid) + { + request->began = true; + request->sizeHint = sizeHint; + } + else if (request) + { + takeRemoteRequestNL(descriptor->request); + if (!request->manifest) + { + request->error = LG_CLIPBOARD_FILE_ERROR_INVALID; + request->complete = true; + lgSignalEvent(request->completeEvent); + } + } + LG_UNLOCK(files.lock); + + if (valid) + return LG_CLIPBOARD_RESULT_ACCEPTED; + if (failedDataset) + { + cancelRemoteManifest(request, LG_CLIPBOARD_FILE_ERROR_INVALID); + failRemoteDataset(failedDataset, LG_CLIPBOARD_FILE_ERROR_INVALID); + } return LG_CLIPBOARD_RESULT_FAILED; } LG_ClipboardResult clipboardFiles_remoteDataChunk( - const LG_ClipboardFileRequest * request, uint64_t responseOffset, + const LG_ClipboardFileRequest * descriptor, uint64_t responseOffset, const void * data, size_t size) { - (void)request; - (void)responseOffset; - (void)data; - (void)size; + if (!descriptor) + return LG_CLIPBOARD_RESULT_FAILED; + + const size_t responseStart = (size_t)responseOffset; + const bool envelopeValid = data && size && + (uint64_t)responseStart == responseOffset && + size <= SIZE_MAX - responseStart; + LG_LOCK(files.lock); + struct RemoteRequest * request = + remoteRequestByIdNL(descriptor->request); + struct RemoteDataset * failedDataset = request && request->manifest ? + request->dataset : NULL; + const size_t end = envelopeValid ? + responseStart + size : 0; + bool valid = envelopeValid && request && + request->began && !request->complete && + sameRemoteRequest(request, descriptor) && + responseOffset == request->dataSize && + (request->sizeHint == LG_CLIPBOARD_SIZE_UNKNOWN || + end <= request->sizeHint); + if (valid && descriptor->operation == LG_CLIPBOARD_FILE_READ) + { + valid = end <= descriptor->length && end <= request->outputCapacity && + request->output; + if (valid) + memcpy(request->output + request->dataSize, data, size); + } + else if (valid) + { + valid = end <= MAX_LIST_BYTES && growBuffer(&request->data, + &request->dataCapacity, end); + if (valid) + memcpy(request->data + request->dataSize, data, size); + } + if (valid) + request->dataSize = end; + else if (request) + { + takeRemoteRequestNL(descriptor->request); + if (!request->manifest) + { + request->error = LG_CLIPBOARD_FILE_ERROR_INVALID; + request->complete = true; + lgSignalEvent(request->completeEvent); + } + } + LG_UNLOCK(files.lock); + + if (valid) + return LG_CLIPBOARD_RESULT_ACCEPTED; + if (failedDataset) + { + cancelRemoteManifest(request, LG_CLIPBOARD_FILE_ERROR_INVALID); + failRemoteDataset(failedDataset, LG_CLIPBOARD_FILE_ERROR_INVALID); + } return LG_CLIPBOARD_RESULT_FAILED; } LG_ClipboardResult clipboardFiles_remoteDataEnd( - const LG_ClipboardFileRequest * request, uint64_t finalSize) + const LG_ClipboardFileRequest * descriptor, uint64_t finalSize) { - (void)request; - (void)finalSize; - return LG_CLIPBOARD_RESULT_FAILED; + if (!descriptor) + return LG_CLIPBOARD_RESULT_FAILED; + + LG_LOCK(files.lock); + struct RemoteRequest * request = takeRemoteRequestNL(descriptor->request); + const bool manifest = request && request->manifest; + struct RemoteDataset * dataset = manifest ? request->dataset : NULL; + const bool valid = request && request->began && + !request->complete && + sameRemoteRequest(request, descriptor) && + request->dataSize == finalSize && + (request->sizeHint == LG_CLIPBOARD_SIZE_UNKNOWN || + request->sizeHint == finalSize) && + (descriptor->operation != LG_CLIPBOARD_FILE_READ || + finalSize == descriptor->length); + if (request) + { + request->error = valid ? LG_CLIPBOARD_FILE_ERROR_NONE : + LG_CLIPBOARD_FILE_ERROR_INVALID; + request->complete = true; + if (!request->manifest) + lgSignalEvent(request->completeEvent); + } + LG_UNLOCK(files.lock); + + if (!request) + return LG_CLIPBOARD_RESULT_FAILED; + if (manifest) + { + if (valid) + continueRemoteManifest(request); + else + { + freeRemoteRequest(request); + failRemoteDataset(dataset, LG_CLIPBOARD_FILE_ERROR_INVALID); + } + } + return valid ? LG_CLIPBOARD_RESULT_ACCEPTED : + LG_CLIPBOARD_RESULT_FAILED; } -void clipboardFiles_remoteCancel(uint64_t dataset, - uint64_t request, LG_ClipboardFileError reason) +void clipboardFiles_remoteCancel(uint64_t datasetId, + uint64_t requestId, LG_ClipboardFileError reason) { - (void)dataset; - (void)request; - (void)reason; + LG_LOCK(files.lock); + struct RemoteRequest * request = takeRemoteRequestNL(requestId); + const bool manifest = request && request->manifest; + struct RemoteDataset * failedDataset = manifest ? request->dataset : NULL; + struct RemoteDataset * acquisition = remoteDatasetByIdNL(datasetId); + if (!acquisition || acquisition->acquisition != requestId || + !acquisition->acquiring) + acquisition = NULL; + else + acquisition->acquiring = false; + if (request) + { + request->error = reason; + request->complete = true; + if (!request->manifest) + lgSignalEvent(request->completeEvent); + } + LG_UNLOCK(files.lock); + + if (manifest) + { + freeRemoteRequest(request); + failRemoteDataset(failedDataset, reason); + } + else if (acquisition) + failRemoteDataset(acquisition, reason); } -void clipboardFiles_localAcquire(uint64_t dataset, +void clipboardFiles_localAcquire(uint64_t wireDataset, uint64_t acquisition) { - clipboard_fileAcquired(dataset, acquisition, - LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED); + LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_STALE; + struct LocalAcquisition * item = calloc(1, sizeof(*item)); + LG_LOCK(files.lock); + struct LocalDataset * dataset = localDatasetByIdNL(wireDataset); + struct LocalAcquisition * existing = files.localAcquisitions; + while (existing && existing->acquisition != acquisition) + existing = existing->next; + if (!acquisition || files.stopping) + error = LG_CLIPBOARD_FILE_ERROR_INVALID; + else if (existing) + error = existing->wireDataset == wireDataset ? + LG_CLIPBOARD_FILE_ERROR_NONE : LG_CLIPBOARD_FILE_ERROR_INVALID; + else if (!item) + error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + else if (dataset && localAcquisitionCountNL() + remoteDatasetCountNL() < + KVMFR_CLIPBOARD_FILE_MAX_ACQUISITIONS) + { + item->dataset = dataset; + item->wireDataset = wireDataset; + item->acquisition = acquisition; + item->next = files.localAcquisitions; + files.localAcquisitions = item; + ++dataset->references; + item = NULL; + error = LG_CLIPBOARD_FILE_ERROR_NONE; + } + else if (dataset) + error = LG_CLIPBOARD_FILE_ERROR_NO_SPACE; + LG_UNLOCK(files.lock); + free(item); + clipboard_fileAcquired(wireDataset, acquisition, error); } -void clipboardFiles_localRelease(uint64_t dataset, +void clipboardFiles_localRelease(uint64_t wireDataset, uint64_t acquisition) { - (void)dataset; - (void)acquisition; + struct LocalAcquisition * item = NULL; + struct LocalDataset * retired = NULL; + LG_LOCK(files.lock); + struct LocalAcquisition ** link = &files.localAcquisitions; + while (*link && ((*link)->wireDataset != wireDataset || + (*link)->acquisition != acquisition)) + link = &(*link)->next; + if (*link) + { + item = *link; + *link = item->next; + if (item->dataset->references) + --item->dataset->references; + retired = takeUnusedLocalDatasetNL(item->dataset); + } + LG_UNLOCK(files.lock); + free(item); + freeLocalDatasets(retired); } void clipboardFiles_localRequest( const LG_ClipboardFileRequest * request) { - if (!request) + if (!request || !request->dataset || !request->request || + (request->operation != LG_CLIPBOARD_FILE_LIST && + request->operation != LG_CLIPBOARD_FILE_READ) || + (request->operation == LG_CLIPBOARD_FILE_LIST && + (request->offset || request->length)) || + (request->operation == LG_CLIPBOARD_FILE_READ && + (!request->length || + request->length > KVMFR_CLIPBOARD_FILE_READ_BYTES || + request->offset > UINT64_MAX - request->length))) + { + if (request) + clipboard_fileCancel(request->dataset, request->request, + LG_CLIPBOARD_FILE_ERROR_INVALID); return; + } - clipboard_fileCancel(request->dataset, request->request, - LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED); + struct LocalJob * job = calloc(1, sizeof(*job)); + if (!job) + { + clipboard_fileCancel(request->dataset, request->request, + LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + return; + } + job->ready = lgCreateEvent(true, 0); + if (!job->ready) + { + free(job); + clipboard_fileCancel(request->dataset, request->request, + LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + return; + } + atomic_init(&job->cancelled, false); + job->request = *request; + + LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_STALE; + LG_LOCK(files.lock); + job->dataset = acquiredLocalDatasetNL(request->dataset); + if (localAcquisitionIdExistsNL(request->request) || + localJobIdExistsNL(request->request)) + error = LG_CLIPBOARD_FILE_ERROR_INVALID; + else if (job->dataset && !files.stopping && + localJobCountNL() + remoteRequestCountNL() < + KVMFR_CLIPBOARD_FILE_MAX_REQUESTS) + { + ++job->dataset->references; + *files.jobsTail = job; + files.jobsTail = &job->next; + error = LG_CLIPBOARD_FILE_ERROR_NONE; + } + else if (job->dataset) + error = LG_CLIPBOARD_FILE_ERROR_NO_SPACE; + LG_UNLOCK(files.lock); + + if (error) + { + lgFreeEvent(job->ready); + free(job); + clipboard_fileCancel(request->dataset, request->request, error); + return; + } + lgSignalEvent(files.jobEvent); } void clipboardFiles_localCancel(uint64_t dataset, uint64_t request, LG_ClipboardFileError reason) { - (void)dataset; - (void)request; (void)reason; + struct LocalAcquisition * acquisition = NULL; + struct LocalDataset * retired = NULL; + LG_LOCK(files.lock); + struct LocalAcquisition ** link = &files.localAcquisitions; + while (*link && ((*link)->wireDataset != dataset || + (*link)->acquisition != request)) + link = &(*link)->next; + if (*link) + { + acquisition = *link; + *link = acquisition->next; + if (acquisition->dataset->references) + --acquisition->dataset->references; + retired = takeUnusedLocalDatasetNL(acquisition->dataset); + } + for (struct LocalJob * job = files.jobsHead; job; job = job->next) + if (job->request.dataset == dataset && job->request.request == request) + atomic_store_explicit(&job->cancelled, true, memory_order_release); + for (struct LocalJob * job = files.activeJobs; job; job = job->next) + if (job->request.dataset == dataset && job->request.request == request) + { + atomic_store_explicit(&job->cancelled, true, memory_order_release); + lgSignalEvent(job->ready); + } + LG_UNLOCK(files.lock); + free(acquisition); + freeLocalDatasets(retired); } void clipboardFiles_localReady(uint64_t request) { - (void)request; + LG_LOCK(files.lock); + for (struct LocalJob * job = files.activeJobs; job; job = job->next) + if (job->request.request == request) + lgSignalEvent(job->ready); + LG_UNLOCK(files.lock); } diff --git a/client/platform/Windows/clipboard_files_win32.c b/client/platform/Windows/clipboard_files_win32.c new file mode 100644 index 00000000..f13ce001 --- /dev/null +++ b/client/platform/Windows/clipboard_files_win32.c @@ -0,0 +1,2570 @@ +/** + * 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 + */ + +#define CINTERFACE +#define COBJMACROS +#define CONST_VTABLE + +#include "platform/windows/clipboard_files_win32.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include + +#define WINDOWS_EPOCH_TICKS UINT64_C(116444736000000000) + +typedef struct WindowsClipboardFileIdentity +{ + uint64_t size; + uint64_t createdNs; + uint64_t modifiedNs; + DWORD volumeSerial; + DWORD fileIndexHigh; + DWORD fileIndexLow; + DWORD attributes; + KVMFRClipboardFileType type; +} +WindowsClipboardFileIdentity; + +typedef struct WindowsClipboardLocalNode +{ + uint64_t id; + wchar_t * name; + wchar_t * path; + WindowsClipboardFileIdentity identity; + uint64_t * children; + size_t childCount; + size_t childCapacity; + bool childrenLoaded; +} +WindowsClipboardLocalNode; + +struct WindowsClipboardLocalFiles +{ + volatile LONG refs; + CRITICAL_SECTION lock; + WindowsClipboardLocalNode * nodes; + size_t nodeCount; + size_t nodeCapacity; + uint64_t * roots; + size_t rootCount; + size_t rootCapacity; + bool published; +}; + +typedef struct WindowsClipboardRemoteEntry +{ + uint64_t node; + uint64_t parent; + uint64_t size; + uint64_t createdNs; + uint64_t modifiedNs; + KVMFRClipboardFileType type; + wchar_t * name; + uint8_t listState; +} +WindowsClipboardRemoteEntry; + +struct WindowsClipboardRemoteManifest +{ + volatile LONG refs; + CRITICAL_SECTION lock; + WindowsClipboardRemoteEntry * entries; + size_t entryCount; + size_t entryCapacity; + uint8_t rootState; + bool frozen; +}; + +static void setError(LG_ClipboardFileError * error, + LG_ClipboardFileError value) +{ + if (error) + *error = value; +} + +static LG_ClipboardFileError fileError(DWORD error) +{ + switch (error) + { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_HANDLE: + return LG_CLIPBOARD_FILE_ERROR_NOT_FOUND; + + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: + return LG_CLIPBOARD_FILE_ERROR_ACCESS; + + case ERROR_DIRECTORY: + return LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY; + + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: + return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + + case ERROR_DISK_FULL: + case ERROR_HANDLE_DISK_FULL: + return LG_CLIPBOARD_FILE_ERROR_NO_SPACE; + + case ERROR_DEVICE_NOT_CONNECTED: + case ERROR_BROKEN_PIPE: + return LG_CLIPBOARD_FILE_ERROR_DISCONNECTED; + + case ERROR_OPERATION_ABORTED: + case ERROR_CANCELLED: + return LG_CLIPBOARD_FILE_ERROR_CANCELLED; + + case ERROR_NOT_SUPPORTED: + return LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED; + + case ERROR_INVALID_DATA: + case ERROR_INVALID_NAME: + case ERROR_INVALID_PARAMETER: + return LG_CLIPBOARD_FILE_ERROR_INVALID; + + default: + return LG_CLIPBOARD_FILE_ERROR_IO; + } +} + +static uint64_t fileTimeToUnixNs(FILETIME time) +{ + ULARGE_INTEGER value; + + value.LowPart = time.dwLowDateTime; + value.HighPart = time.dwHighDateTime; + if (value.QuadPart <= WINDOWS_EPOCH_TICKS) + return 0; + + const uint64_t ticks = value.QuadPart - WINDOWS_EPOCH_TICKS; + return ticks <= UINT64_MAX / UINT64_C(100) ? + ticks * UINT64_C(100) : UINT64_MAX; +} + +static FILETIME unixNsToFileTime(uint64_t ns) +{ + ULARGE_INTEGER value; + FILETIME result; + const uint64_t ticks = ns / UINT64_C(100); + + value.QuadPart = ticks <= UINT64_MAX - WINDOWS_EPOCH_TICKS ? + ticks + WINDOWS_EPOCH_TICKS : UINT64_MAX; + result.dwLowDateTime = value.LowPart; + result.dwHighDateTime = value.HighPart; + return result; +} + +static wchar_t * duplicateWide(const wchar_t * value) +{ + if (!value) + return NULL; + + const size_t length = wcslen(value); + if (length > (SIZE_MAX / sizeof(*value)) - 1U) + return NULL; + + wchar_t * result = malloc((length + 1U) * sizeof(*result)); + if (!result) + return NULL; + + memcpy(result, value, (length + 1U) * sizeof(*result)); + return result; +} + +static wchar_t * joinPath(const wchar_t * parent, const wchar_t * child) +{ + const size_t parentLength = wcslen(parent); + const size_t childLength = wcslen(child); + const bool separator = parentLength && parent[parentLength - 1U] != L'\\' && + parent[parentLength - 1U] != L'/'; + + if (parentLength > SIZE_MAX - childLength - (separator ? 2U : 1U) || + parentLength + childLength + (separator ? 2U : 1U) > + SIZE_MAX / sizeof(wchar_t)) + return NULL; + + const size_t length = parentLength + childLength + (separator ? 1U : 0U); + wchar_t * result = malloc((length + 1U) * sizeof(*result)); + if (!result) + return NULL; + + memcpy(result, parent, parentLength * sizeof(*result)); + size_t offset = parentLength; + if (separator) + result[offset++] = L'\\'; + memcpy(result + offset, child, (childLength + 1U) * sizeof(*result)); + return result; +} + +static bool hasDotComponent(const wchar_t * path, size_t offset) +{ + const size_t length = wcslen(path); + while (offset < length) + { + size_t end = offset; + while (end < length && path[end] != L'\\') + ++end; + + const size_t component = end - offset; + if ((component == 1U && path[offset] == L'.') || + (component == 2U && path[offset] == L'.' && + path[offset + 1U] == L'.')) + return true; + + offset = end + 1U; + } + + return false; +} + +static wchar_t * normalizeSourcePath(const wchar_t * input) +{ + if (!input || !*input) + { + SetLastError(ERROR_INVALID_NAME); + return NULL; + } + + wchar_t * path = duplicateWide(input); + if (!path) + { + SetLastError(ERROR_OUTOFMEMORY); + return NULL; + } + + for (wchar_t * value = path; *value; ++value) + if (*value == L'/') + *value = L'\\'; + + const size_t length = wcslen(path); + const bool drive = length >= 3U && + ((path[0] >= L'A' && path[0] <= L'Z') || + (path[0] >= L'a' && path[0] <= L'z')) && + path[1] == L':' && path[2] == L'\\'; + const bool unc = length >= 3U && path[0] == L'\\' && + path[1] == L'\\' && path[2] != L'?' && path[2] != L'.'; + const bool extendedDrive = length >= 7U && + wcsncmp(path, L"\\\\?\\", 4U) == 0 && + ((path[4] >= L'A' && path[4] <= L'Z') || + (path[4] >= L'a' && path[4] <= L'z')) && + path[5] == L':' && path[6] == L'\\'; + const bool extendedUNC = length >= 8U && + wcsncmp(path, L"\\\\?\\", 4U) == 0 && + (path[4] == L'U' || path[4] == L'u') && + (path[5] == L'N' || path[5] == L'n') && + (path[6] == L'C' || path[6] == L'c') && path[7] == L'\\'; + const size_t componentOffset = drive ? 3U : unc ? 2U : + extendedDrive ? 7U : extendedUNC ? 8U : 0U; + + if (!componentOffset || hasDotComponent(path, componentOffset)) + { + free(path); + SetLastError(ERROR_INVALID_NAME); + return NULL; + } + + if (extendedDrive || extendedUNC) + { + if (extendedUNC) + { + path[4] = L'U'; + path[5] = L'N'; + path[6] = L'C'; + } + return path; + } + + const size_t prefixLength = unc ? 8U : 4U; + const size_t skip = unc ? 2U : 0U; + if (length - skip > (SIZE_MAX / sizeof(wchar_t)) - prefixLength - 1U) + { + free(path); + SetLastError(ERROR_OUTOFMEMORY); + return NULL; + } + + wchar_t * result = malloc((prefixLength + length - skip + 1U) * + sizeof(*result)); + if (!result) + { + free(path); + SetLastError(ERROR_OUTOFMEMORY); + return NULL; + } + + const wchar_t * prefix = unc ? L"\\\\?\\UNC\\" : L"\\\\?\\"; + memcpy(result, prefix, prefixLength * sizeof(*result)); + memcpy(result + prefixLength, path + skip, + (length - skip + 1U) * sizeof(*result)); + free(path); + return result; +} + +static wchar_t * baseName(const wchar_t * path) +{ + size_t end = wcslen(path); + while (end > 1U && (path[end - 1U] == L'\\' || path[end - 1U] == L'/')) + { + if (end == 3U && path[1] == L':') + break; + --end; + } + + size_t start = end; + while (start && path[start - 1U] != L'\\' && path[start - 1U] != L'/') + --start; + + size_t length = end - start; + if (length == 2U && path[start + 1U] == L':') + length = 1U; + if (length > (SIZE_MAX / sizeof(wchar_t)) - 1U) + return NULL; + + wchar_t * result = malloc((length + 1U) * sizeof(*result)); + if (!result) + return NULL; + + memcpy(result, path + start, length * sizeof(*result)); + result[length] = L'\0'; + return result; +} + +static bool validComponent(const wchar_t * name) +{ + if (!name) + return false; + + const size_t length = wcslen(name); + if (!length || length > 255U || !wcscmp(name, L".") || + !wcscmp(name, L"..") || name[length - 1U] == L'.' || + name[length - 1U] == L' ') + return false; + + for (const wchar_t * value = name; *value; ++value) + if (*value < 32 || *value == L'\\' || *value == L'/' || + *value == L'<' || *value == L'>' || *value == L':' || + *value == L'"' || *value == L'|' || *value == L'?' || + *value == L'*') + return false; + + const size_t baseLength = wcscspn(name, L"."); + wchar_t base[5] = { 0 }; + size_t count = 0; + while (count < baseLength && count < ARRAYSIZE(base) - 1U) + { + wchar_t value = name[count]; + if (value >= L'a' && value <= L'z') + value = (wchar_t)(value - (L'a' - L'A')); + base[count++] = value; + } + + if (baseLength == 3U && (!wcscmp(base, L"CON") || + !wcscmp(base, L"PRN") || + !wcscmp(base, L"AUX") || !wcscmp(base, L"NUL"))) + return false; + + if (baseLength == 4U && (!wcsncmp(base, L"COM", 3U) || + !wcsncmp(base, L"LPT", 3U))) + { + const wchar_t suffix = base[3]; + if ((suffix >= L'1' && suffix <= L'9') || suffix == L'\u00b9' || + suffix == L'\u00b2' || suffix == L'\u00b3') + return false; + } + + return true; +} + +static bool sameComponent(const wchar_t * first, const wchar_t * second) +{ + const int result = CompareStringOrdinal(first, -1, second, -1, TRUE); + return result == CSTR_EQUAL || (!result && !_wcsicmp(first, second)); +} + +static bool captureIdentity(const wchar_t * path, + WindowsClipboardFileIdentity * identity, LG_ClipboardFileError * error) +{ + const DWORD flags = FILE_FLAG_OPEN_REPARSE_POINT | + FILE_FLAG_BACKUP_SEMANTICS; + HANDLE file = CreateFileW(path, FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, + OPEN_EXISTING, flags, NULL); + if (file == INVALID_HANDLE_VALUE) + { + setError(error, fileError(GetLastError())); + return false; + } + + BY_HANDLE_FILE_INFORMATION information; + FILE_ATTRIBUTE_TAG_INFO tag; + memset(&information, 0, sizeof(information)); + memset(&tag, 0, sizeof(tag)); + const bool gotInformation = GetFileInformationByHandle(file, + &information) != FALSE; + const bool gotTag = gotInformation && + GetFileInformationByHandleEx(file, + FileAttributeTagInfo, &tag, sizeof(tag)) != FALSE; + const DWORD winError = gotTag ? ERROR_SUCCESS : GetLastError(); + CloseHandle(file); + + if (!gotTag) + { + setError(error, fileError(winError)); + return false; + } + + const DWORD attributes = information.dwFileAttributes | tag.FileAttributes; + if ((attributes & FILE_ATTRIBUTE_DEVICE) || + (attributes & FILE_ATTRIBUTE_REPARSE_POINT)) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED); + return false; + } + + const bool directory = + (information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + identity->size = directory ? 0 : + ((uint64_t)information.nFileSizeHigh << 32U) | + information.nFileSizeLow; + identity->createdNs = fileTimeToUnixNs(information.ftCreationTime); + identity->modifiedNs = fileTimeToUnixNs(information.ftLastWriteTime); + identity->volumeSerial = information.dwVolumeSerialNumber; + identity->fileIndexHigh = information.nFileIndexHigh; + identity->fileIndexLow = information.nFileIndexLow; + identity->attributes = information.dwFileAttributes; + identity->type = directory ? + KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY : + KVMFR_CLIPBOARD_FILE_TYPE_REGULAR; + return true; +} + +static bool sameIdentity(const WindowsClipboardFileIdentity * expected, + const BY_HANDLE_FILE_INFORMATION * information) +{ + const bool directory = + (information->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + const uint64_t size = directory ? 0 : + ((uint64_t)information->nFileSizeHigh << 32U) | + information->nFileSizeLow; + + return !(information->dwFileAttributes & + (FILE_ATTRIBUTE_DEVICE | FILE_ATTRIBUTE_REPARSE_POINT)) && + directory == + (expected->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY) && + size == expected->size && + fileTimeToUnixNs(information->ftCreationTime) == expected->createdNs && + fileTimeToUnixNs(information->ftLastWriteTime) == expected->modifiedNs && + information->dwVolumeSerialNumber == expected->volumeSerial && + information->nFileIndexHigh == expected->fileIndexHigh && + information->nFileIndexLow == expected->fileIndexLow; +} + +static bool checkIdentity(const wchar_t * path, + const WindowsClipboardFileIdentity * expected, + LG_ClipboardFileError * error) +{ + WindowsClipboardFileIdentity current; + if (!captureIdentity(path, ¤t, error)) + return false; + + if (current.size != expected->size || + current.createdNs != expected->createdNs || + current.modifiedNs != expected->modifiedNs || + current.volumeSerial != expected->volumeSerial || + current.fileIndexHigh != expected->fileIndexHigh || + current.fileIndexLow != expected->fileIndexLow || + current.attributes != expected->attributes || + current.type != expected->type) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_STALE); + return false; + } + + return true; +} + +static void freeLocalNode(WindowsClipboardLocalNode * node) +{ + free(node->name); + free(node->path); + free(node->children); + memset(node, 0, sizeof(*node)); +} + +static bool reserveLocalNodes(WindowsClipboardLocalFiles * files, + size_t count) +{ + if (count <= files->nodeCapacity) + return true; + if (count > SIZE_MAX / sizeof(*files->nodes)) + return false; + + size_t capacity = files->nodeCapacity ? files->nodeCapacity : 16U; + while (capacity < count) + { + if (capacity > SIZE_MAX / 2U) + { + capacity = count; + break; + } + capacity *= 2U; + } + if (capacity > SIZE_MAX / sizeof(*files->nodes)) + capacity = count; + + void * nodes = realloc(files->nodes, capacity * sizeof(*files->nodes)); + if (!nodes) + return false; + + files->nodes = nodes; + files->nodeCapacity = capacity; + return true; +} + +static bool reserveIDs(uint64_t ** values, size_t * capacity, size_t count) +{ + if (count <= *capacity) + return true; + if (count > SIZE_MAX / sizeof(**values)) + return false; + + size_t next = *capacity ? *capacity : 16U; + while (next < count) + { + if (next > SIZE_MAX / 2U) + { + next = count; + break; + } + next *= 2U; + } + if (next > SIZE_MAX / sizeof(**values)) + next = count; + + void * result = realloc(*values, next * sizeof(**values)); + if (!result) + return false; + + *values = result; + *capacity = next; + return true; +} + +static bool addLocalRoot(WindowsClipboardLocalFiles * files, + wchar_t * path, LG_ClipboardFileError * error) +{ + WindowsClipboardLocalNode node; + memset(&node, 0, sizeof(node)); + node.path = path; + node.name = baseName(path); + if (!node.name) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + return false; + } + if (!validComponent(node.name)) + { + free(node.name); + setError(error, LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED); + return false; + } + if (!captureIdentity(node.path, &node.identity, error)) + { + free(node.name); + return false; + } + + for (size_t index = 0; index < files->rootCount; ++index) + { + const uint64_t root = files->roots[index]; + if (sameComponent(files->nodes[root - 1U].name, node.name)) + { + free(node.name); + setError(error, LG_CLIPBOARD_FILE_ERROR_INVALID); + return false; + } + } + + if (!reserveLocalNodes(files, files->nodeCount + 1U) || + !reserveIDs(&files->roots, &files->rootCapacity, + files->rootCount + 1U)) + { + free(node.name); + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + return false; + } + + node.id = files->nodeCount + 1U; + files->nodes[files->nodeCount++] = node; + files->roots[files->rootCount++] = node.id; + return true; +} + +static bool refreshLocalRoots(WindowsClipboardLocalFiles * files, + LG_ClipboardFileError * error) +{ + WindowsClipboardFileIdentity * identities = calloc(files->rootCount, + sizeof(*identities)); + if (!identities && files->rootCount) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + return false; + } + + for (size_t index = 0; index < files->rootCount; ++index) + { + const uint64_t id = files->roots[index]; + if (!captureIdentity(files->nodes[id - 1U].path, &identities[index], + error)) + { + free(identities); + return false; + } + } + + for (size_t index = 0; index < files->rootCount; ++index) + { + const uint64_t id = files->roots[index]; + files->nodes[id - 1U].identity = identities[index]; + } + + free(identities); + return true; +} + +static void freeLocalNodeArray(WindowsClipboardLocalNode * nodes, + size_t count) +{ + if (!nodes) + return; + + for (size_t index = 0; index < count; ++index) + freeLocalNode(&nodes[index]); + free(nodes); +} + +static bool loadLocalChildren(WindowsClipboardLocalFiles * files, + size_t parentIndex, LG_ClipboardFileError * error) +{ + if (parentIndex >= files->nodeCount) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NOT_FOUND); + return false; + } + + WindowsClipboardLocalNode * parent = &files->nodes[parentIndex]; + if (parent->identity.type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY); + return false; + } + if (parent->childrenLoaded) + return true; + if (!checkIdentity(parent->path, &parent->identity, error)) + return false; + + wchar_t * parentPath = duplicateWide(parent->path); + wchar_t * pattern = parentPath ? joinPath(parentPath, L"*") : NULL; + if (!parentPath || !pattern) + { + free(parentPath); + free(pattern); + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + return false; + } + + WIN32_FIND_DATAW found; + memset(&found, 0, sizeof(found)); + HANDLE search = FindFirstFileExW(pattern, FindExInfoBasic, &found, + FindExSearchNameMatch, NULL, 0); + free(pattern); + if (search == INVALID_HANDLE_VALUE) + { + const DWORD winError = GetLastError(); + if (winError == ERROR_FILE_NOT_FOUND && + checkIdentity(parentPath, &parent->identity, error)) + { + parent->childrenLoaded = true; + free(parentPath); + return true; + } + + free(parentPath); + if (winError != ERROR_FILE_NOT_FOUND) + setError(error, fileError(winError)); + return false; + } + + WindowsClipboardLocalNode * children = NULL; + size_t count = 0; + size_t capacity = 0; + bool success = true; + DWORD winError = ERROR_SUCCESS; + + do + { + if (!wcscmp(found.cFileName, L".") || + !wcscmp(found.cFileName, L"..")) + continue; + if (!validComponent(found.cFileName)) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED); + success = false; + break; + } + + for (size_t index = 0; index < count; ++index) + if (sameComponent(children[index].name, found.cFileName)) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_INVALID); + success = false; + break; + } + if (!success) + break; + + if (count == capacity) + { + size_t next = capacity ? capacity * 2U : 16U; + if (next < capacity || next > SIZE_MAX / sizeof(*children)) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + success = false; + break; + } + void * result = realloc(children, next * sizeof(*children)); + if (!result) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + success = false; + break; + } + children = result; + memset(children + capacity, 0, + (next - capacity) * sizeof(*children)); + capacity = next; + } + + WindowsClipboardLocalNode * child = &children[count]; + child->name = duplicateWide(found.cFileName); + child->path = child->name ? joinPath(parentPath, child->name) : NULL; + if (!child->name || !child->path) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + success = false; + break; + } + if (!captureIdentity(child->path, &child->identity, error)) + { + success = false; + break; + } + ++count; + } + while (FindNextFileW(search, &found)); + + if (success) + { + winError = GetLastError(); + if (winError != ERROR_NO_MORE_FILES) + { + setError(error, fileError(winError)); + success = false; + } + } + FindClose(search); + + if (success && !checkIdentity(parentPath, + &files->nodes[parentIndex].identity, error)) + success = false; + free(parentPath); + + if (success && (!reserveLocalNodes(files, files->nodeCount + count) || + !reserveIDs(&files->nodes[parentIndex].children, + &files->nodes[parentIndex].childCapacity, count))) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + success = false; + } + + if (!success) + { + freeLocalNodeArray(children, count + (count < capacity ? 1U : 0U)); + return false; + } + + parent = &files->nodes[parentIndex]; + for (size_t index = 0; index < count; ++index) + { + WindowsClipboardLocalNode * child = &children[index]; + child->id = files->nodeCount + 1U; + parent->children[parent->childCount++] = child->id; + files->nodes[files->nodeCount++] = *child; + memset(child, 0, sizeof(*child)); + } + parent->childrenLoaded = true; + freeLocalNodeArray(children, count); + return true; +} + +static bool wideToUTF8(const wchar_t * text, uint8_t ** data, + size_t * size) +{ + *data = NULL; + *size = 0; + if (!text || !*text) + { + SetLastError(ERROR_INVALID_DATA); + return false; + } + + const size_t length = wcslen(text); + if (length > INT_MAX) + { + SetLastError(ERROR_INVALID_DATA); + return false; + } + + const int count = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + text, (int)length, NULL, 0, NULL, NULL); + if (count <= 0) + return false; + + uint8_t * output = malloc((size_t)count); + if (!output) + { + SetLastError(ERROR_OUTOFMEMORY); + return false; + } + + if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, text, (int)length, + (char *)output, count, NULL, NULL) != count) + { + free(output); + return false; + } + + *data = output; + *size = (size_t)count; + return true; +} + +WindowsClipboardLocalFiles * windowsClipboardLocalFilesCapture(HDROP drop, + LG_ClipboardFileError * error) +{ + setError(error, LG_CLIPBOARD_FILE_ERROR_INVALID); + if (!drop) + return NULL; + + WindowsClipboardLocalFiles * files = calloc(1, sizeof(*files)); + if (!files) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + return NULL; + } + files->refs = 1; + if (!InitializeCriticalSectionEx(&files->lock, 0, 0)) + { + const DWORD winError = GetLastError(); + free(files); + setError(error, fileError(winError ? winError : ERROR_OUTOFMEMORY)); + return NULL; + } + + const UINT count = DragQueryFileW(drop, UINT_MAX, NULL, 0); + if (!count) + { + windowsClipboardLocalFilesRelease(files); + return NULL; + } + + for (UINT index = 0; index < count; ++index) + { + const UINT length = DragQueryFileW(drop, index, NULL, 0); + if (!length) + { + windowsClipboardLocalFilesRelease(files); + return NULL; + } + + wchar_t * input = malloc(((size_t)length + 1U) * sizeof(*input)); + if (!input) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + windowsClipboardLocalFilesRelease(files); + return NULL; + } + if (DragQueryFileW(drop, index, input, length + 1U) != length) + { + free(input); + windowsClipboardLocalFilesRelease(files); + return NULL; + } + + wchar_t * path = normalizeSourcePath(input); + free(input); + if (!path) + { + setError(error, fileError(GetLastError())); + windowsClipboardLocalFilesRelease(files); + return NULL; + } + if (!addLocalRoot(files, path, error)) + { + free(path); + windowsClipboardLocalFilesRelease(files); + return NULL; + } + } + + setError(error, LG_CLIPBOARD_FILE_ERROR_NONE); + return files; +} + +void windowsClipboardLocalFilesRetain(WindowsClipboardLocalFiles * files) +{ + if (files) + InterlockedIncrement(&files->refs); +} + +void windowsClipboardLocalFilesRelease(WindowsClipboardLocalFiles * files) +{ + if (!files || InterlockedDecrement(&files->refs)) + return; + + for (size_t index = 0; index < files->nodeCount; ++index) + freeLocalNode(&files->nodes[index]); + free(files->nodes); + free(files->roots); + DeleteCriticalSection(&files->lock); + free(files); +} + +bool windowsClipboardLocalFilesList(WindowsClipboardLocalFiles * files, + uint64_t node, void ** data, size_t * size, + LG_ClipboardFileError * error) +{ + if (!data || !size) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_INVALID); + return false; + } + *data = NULL; + *size = 0; + setError(error, LG_CLIPBOARD_FILE_ERROR_NONE); + if (!files) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_INVALID); + return false; + } + + EnterCriticalSection(&files->lock); + const uint64_t * children = NULL; + size_t childCount = 0; + bool success = true; + if (node == KVMFR_CLIPBOARD_FILE_ROOT_NODE) + { + if (!files->published && !refreshLocalRoots(files, error)) + success = false; + if (success) + { + files->published = true; + children = files->roots; + childCount = files->rootCount; + } + } + else if (!files->published || node > files->nodeCount) + { + setError(error, !files->published ? LG_CLIPBOARD_FILE_ERROR_INVALID : + LG_CLIPBOARD_FILE_ERROR_NOT_FOUND); + success = false; + } + else + { + const size_t index = (size_t)(node - 1U); + if (!loadLocalChildren(files, index, error)) + success = false; + else + { + children = files->nodes[index].children; + childCount = files->nodes[index].childCount; + } + } + + uint8_t * output = NULL; + size_t used = 0; + size_t capacity = 0; + for (size_t index = 0; success && index < childCount; ++index) + { + const uint64_t id = children[index]; + if (!id || id > files->nodeCount) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_INVALID); + success = false; + break; + } + + const WindowsClipboardLocalNode * child = &files->nodes[id - 1U]; + uint8_t * name = NULL; + size_t nameSize = 0; + if (!wideToUTF8(child->name, &name, &nameSize)) + { + setError(error, fileError(GetLastError())); + success = false; + break; + } + + const size_t recordSize = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(nameSize); + if (recordSize < sizeof(KVMFRClipboardFileEntry) || + used > SIZE_MAX - recordSize) + { + free(name); + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + success = false; + break; + } + if (used + recordSize > capacity) + { + size_t next = capacity ? capacity : 4096U; + while (next < used + recordSize) + { + if (next > SIZE_MAX / 2U) + { + next = used + recordSize; + break; + } + next *= 2U; + } + uint8_t * result = realloc(output, next); + if (!result) + { + free(name); + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + success = false; + break; + } + output = result; + capacity = next; + } + + KVMFRClipboardFileEntry entry; + memset(&entry, 0, sizeof(entry)); + entry.node = child->id; + entry.size = child->identity.size; + entry.createdNs = child->identity.createdNs; + entry.modifiedNs = child->identity.modifiedNs; + entry.type = child->identity.type; + entry.nameLength = (uint32_t)nameSize; + memset(output + used, 0, recordSize); + memcpy(output + used, &entry, sizeof(entry)); + memcpy(output + used + sizeof(entry), name, nameSize); + used += recordSize; + free(name); + } + LeaveCriticalSection(&files->lock); + + if (!success) + { + free(output); + return false; + } + + *data = output; + *size = used; + return true; +} + +bool windowsClipboardLocalFilesRead(WindowsClipboardLocalFiles * files, + uint64_t node, uint64_t offset, uint32_t length, void ** data, + size_t * size, LG_ClipboardFileError * error) +{ + if (!data || !size) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_INVALID); + return false; + } + *data = NULL; + *size = 0; + setError(error, LG_CLIPBOARD_FILE_ERROR_NONE); + if (!files || length > KVMFR_CLIPBOARD_FILE_READ_BYTES || + offset > INT64_MAX || offset > UINT64_MAX - length) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_INVALID); + return false; + } + + wchar_t * path = NULL; + WindowsClipboardFileIdentity identity; + memset(&identity, 0, sizeof(identity)); + EnterCriticalSection(&files->lock); + if (!files->published || !node || node > files->nodeCount) + setError(error, LG_CLIPBOARD_FILE_ERROR_NOT_FOUND); + else + { + const WindowsClipboardLocalNode * source = &files->nodes[node - 1U]; + if (source->identity.type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR) + setError(error, LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY); + else + { + path = duplicateWide(source->path); + identity = source->identity; + if (!path) + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + } + } + LeaveCriticalSection(&files->lock); + if (!path) + return false; + + HANDLE file = CreateFileW(path, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_RANDOM_ACCESS | + FILE_FLAG_OPEN_REPARSE_POINT, + NULL); + free(path); + if (file == INVALID_HANDLE_VALUE) + { + setError(error, fileError(GetLastError())); + return false; + } + + FILE_STANDARD_INFO standard; + FILE_ATTRIBUTE_TAG_INFO tag; + BY_HANDLE_FILE_INFORMATION before; + memset(&standard, 0, sizeof(standard)); + memset(&tag, 0, sizeof(tag)); + memset(&before, 0, sizeof(before)); + bool success = GetFileInformationByHandleEx(file, FileStandardInfo, + &standard, sizeof(standard)) && + GetFileInformationByHandleEx(file, FileAttributeTagInfo, + &tag, sizeof(tag)) && + GetFileInformationByHandle(file, &before); + DWORD winError = success ? ERROR_SUCCESS : GetLastError(); + if (success && (standard.Directory || + (tag.FileAttributes & + (FILE_ATTRIBUTE_DEVICE | FILE_ATTRIBUTE_REPARSE_POINT)))) + { + setError(error, standard.Directory ? + LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY : + LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED); + success = false; + } + if (success && (!sameIdentity(&identity, &before) || + standard.EndOfFile.QuadPart < 0 || + (uint64_t)standard.EndOfFile.QuadPart != identity.size)) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_STALE); + success = false; + } + if (success && (offset > identity.size || + length > identity.size - offset)) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_INVALID); + success = false; + } + + const uint32_t wanted = success ? length : 0; + uint8_t * output = wanted ? malloc(wanted) : NULL; + if (success && wanted && !output) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + success = false; + } + + LARGE_INTEGER position; + DWORD read = 0; + position.QuadPart = (LONGLONG)offset; + if (success && (!SetFilePointerEx(file, position, NULL, FILE_BEGIN) || + (wanted && !ReadFile(file, output, wanted, &read, NULL)))) + { + winError = GetLastError(); + success = false; + } + + BY_HANDLE_FILE_INFORMATION after; + memset(&after, 0, sizeof(after)); + if (success && !GetFileInformationByHandle(file, &after)) + { + winError = GetLastError(); + success = false; + } + if (success && !sameIdentity(&identity, &after)) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_STALE); + success = false; + } + CloseHandle(file); + + if (!success || read != wanted) + { + free(output); + if (error && *error == LG_CLIPBOARD_FILE_ERROR_NONE) + *error = fileError(winError ? winError : ERROR_READ_FAULT); + return false; + } + + *data = output; + *size = wanted; + return true; +} + +static wchar_t * utf8ToWide(const uint8_t * data, size_t size) +{ + if (!data || !size || size > INT_MAX) + { + SetLastError(ERROR_INVALID_DATA); + return NULL; + } + + for (size_t index = 0; index < size; ++index) + if (!data[index]) + { + SetLastError(ERROR_INVALID_DATA); + return NULL; + } + + const int count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + (const char *)data, (int)size, NULL, 0); + if (count <= 0) + return NULL; + wchar_t * output = malloc(((size_t)count + 1U) * sizeof(*output)); + if (!output) + { + SetLastError(ERROR_OUTOFMEMORY); + return NULL; + } + + if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + (const char *)data, (int)size, output, count) != count) + { + free(output); + return NULL; + } + output[count] = L'\0'; + return output; +} + +static void freeRemoteEntry(WindowsClipboardRemoteEntry * entry) +{ + free(entry->name); + memset(entry, 0, sizeof(*entry)); +} + +static void remoteManifestRetain(WindowsClipboardRemoteManifest * manifest) +{ + InterlockedIncrement(&manifest->refs); +} + +static void remoteManifestRelease(WindowsClipboardRemoteManifest * manifest) +{ + if (InterlockedDecrement(&manifest->refs)) + return; + + for (size_t index = 0; index < manifest->entryCount; ++index) + freeRemoteEntry(&manifest->entries[index]); + free(manifest->entries); + DeleteCriticalSection(&manifest->lock); + free(manifest); +} + +static bool reserveRemoteEntries(WindowsClipboardRemoteManifest * manifest, + size_t count) +{ + if (count <= manifest->entryCapacity) + return true; + if (count > SIZE_MAX / sizeof(*manifest->entries)) + return false; + + size_t capacity = manifest->entryCapacity ? manifest->entryCapacity : 16U; + while (capacity < count) + { + if (capacity > SIZE_MAX / 2U) + { + capacity = count; + break; + } + capacity *= 2U; + } + if (capacity > SIZE_MAX / sizeof(*manifest->entries)) + capacity = count; + + void * entries = realloc(manifest->entries, + capacity * sizeof(*manifest->entries)); + if (!entries) + return false; + + manifest->entries = entries; + manifest->entryCapacity = capacity; + return true; +} + +static ptrdiff_t findRemoteEntry( + const WindowsClipboardRemoteManifest * manifest, uint64_t node) +{ + for (size_t index = 0; index < manifest->entryCount; ++index) + if (manifest->entries[index].node == node) + return (ptrdiff_t)index; + return -1; +} + +WindowsClipboardRemoteManifest * windowsClipboardRemoteManifestCreate(void) +{ + WindowsClipboardRemoteManifest * manifest = calloc(1, sizeof(*manifest)); + if (!manifest) + return NULL; + + manifest->refs = 1; + if (!InitializeCriticalSectionEx(&manifest->lock, 0, 0)) + { + free(manifest); + return NULL; + } + return manifest; +} + +void windowsClipboardRemoteManifestFree( + WindowsClipboardRemoteManifest * manifest) +{ + if (manifest) + remoteManifestRelease(manifest); +} + +bool windowsClipboardRemoteManifestAppendList( + WindowsClipboardRemoteManifest * manifest, uint64_t parent, + const void * data, size_t size, LG_ClipboardFileError * error) +{ + setError(error, LG_CLIPBOARD_FILE_ERROR_INVALID); + if (!manifest || (!data && size)) + return false; + + EnterCriticalSection(&manifest->lock); + if (manifest->frozen) + { + LeaveCriticalSection(&manifest->lock); + return false; + } + + ptrdiff_t parentIndex = -1; + uint8_t * parentState = &manifest->rootState; + if (parent) + { + parentIndex = findRemoteEntry(manifest, parent); + if (parentIndex < 0 || + manifest->entries[parentIndex].type != + KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY) + { + LeaveCriticalSection(&manifest->lock); + return false; + } + parentState = &manifest->entries[parentIndex].listState; + } + if (*parentState != 1U) + { + LeaveCriticalSection(&manifest->lock); + return false; + } + + WindowsClipboardRemoteEntry * pending = NULL; + size_t count = 0; + size_t capacity = 0; + size_t offset = 0; + bool success = true; + while (offset < size) + { + if (size - offset < sizeof(KVMFRClipboardFileEntry)) + { + success = false; + break; + } + + KVMFRClipboardFileEntry wire; + memcpy(&wire, (const uint8_t *)data + offset, sizeof(wire)); + const size_t bytes = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(wire.nameLength); + if (!wire.node || !wire.nameLength || + (wire.type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR && + wire.type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY) || + bytes < sizeof(wire) || bytes > size - offset || + wire.nameLength > bytes - sizeof(wire)) + { + success = false; + break; + } + + wchar_t * name = utf8ToWide((const uint8_t *)data + offset + + sizeof(wire), wire.nameLength); + if (!name || !validComponent(name)) + { + if (!name && GetLastError() == ERROR_OUTOFMEMORY) + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + free(name); + success = false; + break; + } + + for (size_t pad = sizeof(wire) + wire.nameLength; pad < bytes; ++pad) + if (((const uint8_t *)data)[offset + pad]) + { + success = false; + break; + } + if (!success) + { + free(name); + break; + } + + if (findRemoteEntry(manifest, wire.node) >= 0) + success = false; + for (size_t index = 0; success && index < count; ++index) + if (pending[index].node == wire.node || + sameComponent(pending[index].name, name)) + success = false; + for (size_t index = 0; success && index < manifest->entryCount; ++index) + if (manifest->entries[index].parent == parent && + sameComponent(manifest->entries[index].name, name)) + success = false; + if (!success) + { + free(name); + break; + } + + if (count == capacity) + { + size_t next = capacity ? capacity * 2U : 16U; + if (next < capacity || next > SIZE_MAX / sizeof(*pending)) + { + free(name); + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + success = false; + break; + } + void * result = realloc(pending, next * sizeof(*pending)); + if (!result) + { + free(name); + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + success = false; + break; + } + pending = result; + memset(pending + capacity, 0, + (next - capacity) * sizeof(*pending)); + capacity = next; + } + + WindowsClipboardRemoteEntry * entry = &pending[count++]; + entry->node = wire.node; + entry->parent = parent; + entry->size = wire.type == KVMFR_CLIPBOARD_FILE_TYPE_REGULAR ? + wire.size : 0; + entry->createdNs = wire.createdNs; + entry->modifiedNs = wire.modifiedNs; + entry->type = wire.type; + entry->name = name; + offset += bytes; + } + + if (success && offset != size) + success = false; + if (success && (manifest->entryCount > SIZE_MAX - count || + !reserveRemoteEntries(manifest, manifest->entryCount + count))) + { + setError(error, LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + success = false; + } + + if (success) + { + for (size_t index = 0; index < count; ++index) + { + manifest->entries[manifest->entryCount++] = pending[index]; + memset(&pending[index], 0, sizeof(pending[index])); + } + if (parentIndex < 0) + manifest->rootState = 2U; + else + manifest->entries[parentIndex].listState = 2U; + setError(error, LG_CLIPBOARD_FILE_ERROR_NONE); + } + else if (!error || *error == LG_CLIPBOARD_FILE_ERROR_INVALID) + setError(error, LG_CLIPBOARD_FILE_ERROR_INVALID); + + for (size_t index = 0; index < count; ++index) + freeRemoteEntry(&pending[index]); + free(pending); + LeaveCriticalSection(&manifest->lock); + return success; +} + +bool windowsClipboardRemoteManifestNextDirectory( + WindowsClipboardRemoteManifest * manifest, uint64_t * node) +{ + if (!manifest || !node) + return false; + + EnterCriticalSection(&manifest->lock); + bool found = false; + if (!manifest->frozen && manifest->rootState == 0U) + { + manifest->rootState = 1U; + *node = KVMFR_CLIPBOARD_FILE_ROOT_NODE; + found = true; + } + else if (!manifest->frozen) + for (size_t index = 0; index < manifest->entryCount; ++index) + { + WindowsClipboardRemoteEntry * entry = &manifest->entries[index]; + if (entry->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY && + entry->listState == 0U) + { + entry->listState = 1U; + *node = entry->node; + found = true; + break; + } + } + LeaveCriticalSection(&manifest->lock); + return found; +} + +bool windowsClipboardRemoteManifestHasEntries( + const WindowsClipboardRemoteManifest * manifest) +{ + if (!manifest) + return false; + + WindowsClipboardRemoteManifest * mutableManifest = + (WindowsClipboardRemoteManifest *)manifest; + EnterCriticalSection(&mutableManifest->lock); + const bool result = mutableManifest->entryCount != 0; + LeaveCriticalSection(&mutableManifest->lock); + return result; +} + +typedef struct WindowsClipboardFileDataObject +{ + IDataObject iface; + volatile LONG refs; + WindowsClipboardRemoteManifest * manifest; + uint64_t dataset; + uint64_t acquisition; + WindowsClipboardRemoteProviderOps ops; + void * opaque; + FILEDESCRIPTORW * descriptors; + size_t descriptorCount; + UINT fileDescriptorFormat; + UINT fileContentsFormat; + UINT preferredDropEffectFormat; +} +WindowsClipboardFileDataObject; + +typedef struct WindowsClipboardFileStream +{ + IStream iface; + volatile LONG refs; + CRITICAL_SECTION lock; + WindowsClipboardFileDataObject * owner; + size_t entryIndex; + uint64_t offset; +} +WindowsClipboardFileStream; + +typedef struct WindowsClipboardFormatEnumerator +{ + IEnumFORMATETC iface; + volatile LONG refs; + FORMATETC formats[3]; + ULONG count; + ULONG index; +} +WindowsClipboardFormatEnumerator; + +static WindowsClipboardFileDataObject * dataObjectFromInterface( + IDataObject * iface) +{ + return CONTAINING_RECORD(iface, WindowsClipboardFileDataObject, iface); +} + +static WindowsClipboardFileStream * streamFromInterface(IStream * iface) +{ + return CONTAINING_RECORD(iface, WindowsClipboardFileStream, iface); +} + +static WindowsClipboardFormatEnumerator * formatEnumeratorFromInterface( + IEnumFORMATETC * iface) +{ + return CONTAINING_RECORD(iface, WindowsClipboardFormatEnumerator, iface); +} + +static HRESULT STDMETHODCALLTYPE dataObjectQueryInterface(IDataObject * self, + REFIID iid, void ** object); +static ULONG STDMETHODCALLTYPE dataObjectAddRef(IDataObject * self); +static ULONG STDMETHODCALLTYPE dataObjectRelease(IDataObject * self); +static HRESULT STDMETHODCALLTYPE dataObjectGetData(IDataObject * self, + FORMATETC * format, STGMEDIUM * medium); +static HRESULT STDMETHODCALLTYPE dataObjectGetDataHere(IDataObject * self, + FORMATETC * format, STGMEDIUM * medium); +static HRESULT STDMETHODCALLTYPE dataObjectQueryGetData(IDataObject * self, + FORMATETC * format); +static HRESULT STDMETHODCALLTYPE dataObjectGetCanonicalFormatEtc( + IDataObject * self, FORMATETC * input, FORMATETC * output); +static HRESULT STDMETHODCALLTYPE dataObjectSetData(IDataObject * self, + FORMATETC * format, STGMEDIUM * medium, BOOL release); +static HRESULT STDMETHODCALLTYPE dataObjectEnumFormatEtc(IDataObject * self, + DWORD direction, IEnumFORMATETC ** enumerator); +static HRESULT STDMETHODCALLTYPE dataObjectDAdvise(IDataObject * self, + FORMATETC * format, DWORD flags, IAdviseSink * sink, + DWORD * connection); +static HRESULT STDMETHODCALLTYPE dataObjectDUnadvise(IDataObject * self, + DWORD connection); +static HRESULT STDMETHODCALLTYPE dataObjectEnumDAdvise(IDataObject * self, + IEnumSTATDATA ** enumerator); + +static const IDataObjectVtbl dataObjectVTable = +{ + .QueryInterface = dataObjectQueryInterface, + .AddRef = dataObjectAddRef, + .Release = dataObjectRelease, + .GetData = dataObjectGetData, + .GetDataHere = dataObjectGetDataHere, + .QueryGetData = dataObjectQueryGetData, + .GetCanonicalFormatEtc = dataObjectGetCanonicalFormatEtc, + .SetData = dataObjectSetData, + .EnumFormatEtc = dataObjectEnumFormatEtc, + .DAdvise = dataObjectDAdvise, + .DUnadvise = dataObjectDUnadvise, + .EnumDAdvise = dataObjectEnumDAdvise, +}; + +static HRESULT STDMETHODCALLTYPE streamQueryInterface(IStream * self, + REFIID iid, void ** object); +static ULONG STDMETHODCALLTYPE streamAddRef(IStream * self); +static ULONG STDMETHODCALLTYPE streamRelease(IStream * self); +static HRESULT STDMETHODCALLTYPE streamRead(IStream * self, void * data, + ULONG length, ULONG * read); +static HRESULT STDMETHODCALLTYPE streamWrite(IStream * self, + const void * data, ULONG length, ULONG * written); +static HRESULT STDMETHODCALLTYPE streamSeek(IStream * self, + LARGE_INTEGER move, DWORD origin, ULARGE_INTEGER * position); +static HRESULT STDMETHODCALLTYPE streamSetSize(IStream * self, + ULARGE_INTEGER size); +static HRESULT STDMETHODCALLTYPE streamCopyTo(IStream * self, + IStream * target, ULARGE_INTEGER count, ULARGE_INTEGER * read, + ULARGE_INTEGER * written); +static HRESULT STDMETHODCALLTYPE streamCommit(IStream * self, DWORD flags); +static HRESULT STDMETHODCALLTYPE streamRevert(IStream * self); +static HRESULT STDMETHODCALLTYPE streamLockRegion(IStream * self, + ULARGE_INTEGER offset, ULARGE_INTEGER count, DWORD type); +static HRESULT STDMETHODCALLTYPE streamUnlockRegion(IStream * self, + ULARGE_INTEGER offset, ULARGE_INTEGER count, DWORD type); +static HRESULT STDMETHODCALLTYPE streamStat(IStream * self, STATSTG * stat, + DWORD flags); +static HRESULT STDMETHODCALLTYPE streamClone(IStream * self, + IStream ** clone); + +static const IStreamVtbl streamVTable = +{ + .QueryInterface = streamQueryInterface, + .AddRef = streamAddRef, + .Release = streamRelease, + .Read = streamRead, + .Write = streamWrite, + .Seek = streamSeek, + .SetSize = streamSetSize, + .CopyTo = streamCopyTo, + .Commit = streamCommit, + .Revert = streamRevert, + .LockRegion = streamLockRegion, + .UnlockRegion = streamUnlockRegion, + .Stat = streamStat, + .Clone = streamClone, +}; + +static HRESULT STDMETHODCALLTYPE formatEnumeratorQueryInterface( + IEnumFORMATETC * self, REFIID iid, void ** object); +static ULONG STDMETHODCALLTYPE formatEnumeratorAddRef(IEnumFORMATETC * self); +static ULONG STDMETHODCALLTYPE formatEnumeratorRelease(IEnumFORMATETC * self); +static HRESULT STDMETHODCALLTYPE formatEnumeratorNext(IEnumFORMATETC * self, + ULONG count, FORMATETC * formats, ULONG * fetched); +static HRESULT STDMETHODCALLTYPE formatEnumeratorSkip(IEnumFORMATETC * self, + ULONG count); +static HRESULT STDMETHODCALLTYPE formatEnumeratorReset(IEnumFORMATETC * self); +static HRESULT STDMETHODCALLTYPE formatEnumeratorClone(IEnumFORMATETC * self, + IEnumFORMATETC ** clone); + +static const IEnumFORMATETCVtbl formatEnumeratorVTable = +{ + .QueryInterface = formatEnumeratorQueryInterface, + .AddRef = formatEnumeratorAddRef, + .Release = formatEnumeratorRelease, + .Next = formatEnumeratorNext, + .Skip = formatEnumeratorSkip, + .Reset = formatEnumeratorReset, + .Clone = formatEnumeratorClone, +}; + +static HRESULT STDMETHODCALLTYPE dataObjectQueryInterface(IDataObject * self, + REFIID iid, void ** object) +{ + if (!object) + return E_POINTER; + *object = NULL; + if (IsEqualIID(iid, &IID_IUnknown) || IsEqualIID(iid, &IID_IDataObject)) + { + *object = self; + IDataObject_AddRef(self); + return S_OK; + } + return E_NOINTERFACE; +} + +static ULONG STDMETHODCALLTYPE dataObjectAddRef(IDataObject * self) +{ + WindowsClipboardFileDataObject * object = dataObjectFromInterface(self); + return (ULONG)InterlockedIncrement(&object->refs); +} + +static ULONG STDMETHODCALLTYPE dataObjectRelease(IDataObject * self) +{ + WindowsClipboardFileDataObject * object = dataObjectFromInterface(self); + const LONG refs = + InterlockedDecrement(&object->refs); + if (refs) + return (ULONG)refs; + + object->ops.release(object->opaque, object->dataset, object->acquisition); + remoteManifestRelease(object->manifest); + free(object->descriptors); + free(object); + return 0; +} + +static bool dataObjectFormatMatches(const FORMATETC * format, + UINT clipboardFormat, DWORD medium) +{ + return format && format->cfFormat == clipboardFormat && + format->dwAspect == DVASPECT_CONTENT && format->lindex >= -1 && + (format->tymed & medium) != 0; +} + +static HRESULT createFileStream(WindowsClipboardFileDataObject * owner, + size_t entryIndex, uint64_t offset, IStream ** result) +{ + *result = NULL; + WindowsClipboardFileStream * stream = calloc(1, sizeof(*stream)); + if (!stream) + return E_OUTOFMEMORY; + if (!InitializeCriticalSectionEx(&stream->lock, 0, 0)) + { + const DWORD winError = GetLastError(); + free(stream); + return HRESULT_FROM_WIN32(winError ? winError : ERROR_OUTOFMEMORY); + } + + stream->iface.lpVtbl = &streamVTable; + stream->refs = 1; + stream->owner = owner; + stream->entryIndex = entryIndex; + stream->offset = offset; + IDataObject_AddRef(&owner->iface); + *result = &stream->iface; + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE dataObjectGetData(IDataObject * self, + FORMATETC * format, STGMEDIUM * medium) +{ + if (!format || !medium) + return E_POINTER; + memset(medium, 0, sizeof(*medium)); + + WindowsClipboardFileDataObject * object = dataObjectFromInterface(self); + if (dataObjectFormatMatches(format, object->fileDescriptorFormat, + TYMED_HGLOBAL) && format->lindex == -1) + { + if (object->descriptorCount > UINT_MAX || + object->descriptorCount > + (SIZE_MAX - offsetof(FILEGROUPDESCRIPTORW, fgd)) / + sizeof(*object->descriptors)) + return E_OUTOFMEMORY; + + const SIZE_T bytes = offsetof(FILEGROUPDESCRIPTORW, fgd) + + object->descriptorCount * sizeof(*object->descriptors); + HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, bytes); + if (!memory) + return E_OUTOFMEMORY; + + FILEGROUPDESCRIPTORW * group = GlobalLock(memory); + if (!group) + { + GlobalFree(memory); + return E_OUTOFMEMORY; + } + group->cItems = (UINT)object->descriptorCount; + memcpy(group->fgd, object->descriptors, + object->descriptorCount * sizeof(*object->descriptors)); + GlobalUnlock(memory); + medium->tymed = TYMED_HGLOBAL; + medium->hGlobal = memory; + return S_OK; + } + + if (dataObjectFormatMatches(format, object->fileContentsFormat, + TYMED_ISTREAM)) + { + if (format->lindex < 0 || + (size_t)format->lindex >= object->manifest->entryCount) + return DV_E_LINDEX; + const WindowsClipboardRemoteEntry * entry = + &object->manifest->entries[(size_t)format->lindex]; + if (entry->type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR) + return DV_E_LINDEX; + + IStream * stream = NULL; + const HRESULT result = createFileStream(object, + (size_t)format->lindex, 0, &stream); + if (FAILED(result)) + return result; + medium->tymed = TYMED_ISTREAM; + medium->pstm = stream; + return S_OK; + } + + if (dataObjectFormatMatches(format, object->preferredDropEffectFormat, + TYMED_HGLOBAL) && format->lindex == -1) + { + HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD)); + if (!memory) + return E_OUTOFMEMORY; + DWORD * effect = GlobalLock(memory); + if (!effect) + { + GlobalFree(memory); + return E_OUTOFMEMORY; + } + *effect = DROPEFFECT_COPY; + GlobalUnlock(memory); + medium->tymed = TYMED_HGLOBAL; + medium->hGlobal = memory; + return S_OK; + } + + return DV_E_FORMATETC; +} + +static HRESULT STDMETHODCALLTYPE dataObjectGetDataHere(IDataObject * self, + FORMATETC * format, STGMEDIUM * medium) +{ + (void)self; + (void)format; + (void)medium; + return DATA_E_FORMATETC; +} + +static HRESULT STDMETHODCALLTYPE dataObjectQueryGetData(IDataObject * self, + FORMATETC * format) +{ + if (!format) + return E_POINTER; + WindowsClipboardFileDataObject * object = dataObjectFromInterface(self); + if (dataObjectFormatMatches(format, object->fileDescriptorFormat, + TYMED_HGLOBAL) && format->lindex == -1) + return S_OK; + if (dataObjectFormatMatches(format, object->preferredDropEffectFormat, + TYMED_HGLOBAL) && format->lindex == -1) + return S_OK; + if (dataObjectFormatMatches(format, object->fileContentsFormat, + TYMED_ISTREAM)) + { + if (format->lindex == -1) + for (size_t index = 0; index < object->manifest->entryCount; ++index) + if (object->manifest->entries[index].type == + KVMFR_CLIPBOARD_FILE_TYPE_REGULAR) + return S_OK; + if (format->lindex >= 0 && + (size_t)format->lindex < object->manifest->entryCount && + object->manifest->entries[(size_t)format->lindex].type == + KVMFR_CLIPBOARD_FILE_TYPE_REGULAR) + return S_OK; + } + return DV_E_FORMATETC; +} + +static HRESULT STDMETHODCALLTYPE dataObjectGetCanonicalFormatEtc( + IDataObject * self, FORMATETC * input, FORMATETC * output) +{ + (void)self; + if (!input || !output) + return E_POINTER; + *output = *input; + output->ptd = NULL; + return DATA_S_SAMEFORMATETC; +} + +static HRESULT STDMETHODCALLTYPE dataObjectSetData(IDataObject * self, + FORMATETC * format, STGMEDIUM * medium, BOOL release) +{ + (void)self; + (void)format; + (void)medium; + (void)release; + return E_NOTIMPL; +} + +static HRESULT createFormatEnumerator( + const WindowsClipboardFileDataObject * object, ULONG index, + IEnumFORMATETC ** result) +{ + *result = NULL; + WindowsClipboardFormatEnumerator * enumerator = + calloc(1, sizeof(*enumerator)); + if (!enumerator) + return E_OUTOFMEMORY; + + enumerator->iface.lpVtbl = &formatEnumeratorVTable; + enumerator->refs = 1; + enumerator->formats[0].cfFormat = + (CLIPFORMAT)object->fileDescriptorFormat; + enumerator->formats[0].dwAspect = DVASPECT_CONTENT; + enumerator->formats[0].lindex = -1; + enumerator->formats[0].tymed = TYMED_HGLOBAL; + + bool hasRegular = false; + for (size_t entry = 0; entry < object->manifest->entryCount; ++entry) + if (object->manifest->entries[entry].type == + KVMFR_CLIPBOARD_FILE_TYPE_REGULAR) + { + hasRegular = true; + break; + } + + ULONG format = 1; + if (hasRegular) + { + enumerator->formats[format].cfFormat = + (CLIPFORMAT)object->fileContentsFormat; + enumerator->formats[format].dwAspect = DVASPECT_CONTENT; + enumerator->formats[format].lindex = -1; + enumerator->formats[format].tymed = TYMED_ISTREAM; + ++format; + } + enumerator->formats[format].cfFormat = + (CLIPFORMAT)object->preferredDropEffectFormat; + enumerator->formats[format].dwAspect = DVASPECT_CONTENT; + enumerator->formats[format].lindex = -1; + enumerator->formats[format].tymed = TYMED_HGLOBAL; + + enumerator->count = format + 1U; + enumerator->index = index > enumerator->count ? + enumerator->count : index; + *result = &enumerator->iface; + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE dataObjectEnumFormatEtc(IDataObject * self, + DWORD direction, IEnumFORMATETC ** enumerator) +{ + if (!enumerator) + return E_POINTER; + *enumerator = NULL; + if (direction != DATADIR_GET) + return E_NOTIMPL; + return createFormatEnumerator(dataObjectFromInterface(self), 0, + enumerator); +} + +static HRESULT STDMETHODCALLTYPE dataObjectDAdvise(IDataObject * self, + FORMATETC * format, DWORD flags, IAdviseSink * sink, + DWORD * connection) +{ + (void)self; + (void)format; + (void)flags; + (void)sink; + (void)connection; + return OLE_E_ADVISENOTSUPPORTED; +} + +static HRESULT STDMETHODCALLTYPE streamQueryInterface(IStream * self, + REFIID iid, void ** object) +{ + if (!object) + return E_POINTER; + *object = NULL; + if (IsEqualIID(iid, &IID_IUnknown) || + IsEqualIID(iid, &IID_ISequentialStream) || + IsEqualIID(iid, &IID_IStream)) + { + *object = self; + IStream_AddRef(self); + return S_OK; + } + return E_NOINTERFACE; +} + +static ULONG STDMETHODCALLTYPE streamAddRef(IStream * self) +{ + WindowsClipboardFileStream * stream = streamFromInterface(self); + return (ULONG)InterlockedIncrement(&stream->refs); +} + +static ULONG STDMETHODCALLTYPE streamRelease(IStream * self) +{ + WindowsClipboardFileStream * stream = streamFromInterface(self); + const LONG refs = InterlockedDecrement(&stream->refs); + if (refs) + return (ULONG)refs; + + IDataObject_Release(&stream->owner->iface); + DeleteCriticalSection(&stream->lock); + free(stream); + return 0; +} + +static HRESULT STDMETHODCALLTYPE streamRead(IStream * self, void * data, + ULONG length, ULONG * read) +{ + if (read) + *read = 0; + if (!data && length) + return STG_E_INVALIDPOINTER; + if (!length) + return S_OK; + + WindowsClipboardFileStream * stream = streamFromInterface(self); + WindowsClipboardFileDataObject * owner = stream->owner; + EnterCriticalSection(&stream->lock); + const WindowsClipboardRemoteEntry * entry = + &owner->manifest->entries[stream->entryIndex]; + if (stream->offset >= entry->size) + { + LeaveCriticalSection(&stream->lock); + return S_FALSE; + } + + const ULONG wanted = (ULONG)((entry->size - stream->offset < length) ? + entry->size - stream->offset : length); + ULONG completed = 0; + HRESULT result = S_OK; + while (completed < wanted) + { + const ULONG chunk = + wanted - completed > KVMFR_CLIPBOARD_FILE_READ_BYTES ? + KVMFR_CLIPBOARD_FILE_READ_BYTES : wanted - completed; + ULONG actual = 0; + result = owner->ops.read(owner->opaque, owner->dataset, + owner->acquisition, entry->node, stream->offset, + (uint8_t *)data + completed, chunk, &actual); + if (FAILED(result)) + break; + if (actual != chunk) + { + result = STG_E_READFAULT; + break; + } + + stream->offset += actual; + completed += actual; + } + LeaveCriticalSection(&stream->lock); + + if (read) + *read = completed; + if (FAILED(result)) + return result; + return completed == length ? S_OK : S_FALSE; +} + +static HRESULT STDMETHODCALLTYPE streamWrite(IStream * self, + const void * data, ULONG length, ULONG * written) +{ + (void)self; + (void)data; + (void)length; + if (written) + *written = 0; + return STG_E_ACCESSDENIED; +} + +static HRESULT STDMETHODCALLTYPE streamSeek(IStream * self, + LARGE_INTEGER move, DWORD origin, ULARGE_INTEGER * position) +{ + WindowsClipboardFileStream * stream = streamFromInterface(self); + EnterCriticalSection(&stream->lock); + const WindowsClipboardRemoteEntry * entry = + &stream->owner->manifest->entries[stream->entryIndex]; + + uint64_t base; + switch (origin) + { + case STREAM_SEEK_SET: + base = 0; + break; + + case STREAM_SEEK_CUR: + base = stream->offset; + break; + + case STREAM_SEEK_END: + base = entry->size; + break; + + default: + LeaveCriticalSection(&stream->lock); + return STG_E_INVALIDFUNCTION; + } + + uint64_t next; + if (move.QuadPart < 0) + { + const uint64_t amount = (uint64_t)(-(move.QuadPart + 1)) + 1U; + if (amount > base) + { + LeaveCriticalSection(&stream->lock); + return STG_E_INVALIDFUNCTION; + } + next = base - amount; + } + else + { + const uint64_t amount = (uint64_t)move.QuadPart; + if (amount > UINT64_MAX - base) + { + LeaveCriticalSection(&stream->lock); + return STG_E_INVALIDFUNCTION; + } + next = base + amount; + } + + stream->offset = next; + if (position) + position->QuadPart = next; + LeaveCriticalSection(&stream->lock); + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE streamSetSize(IStream * self, + ULARGE_INTEGER size) +{ + (void)self; + (void)size; + return STG_E_ACCESSDENIED; +} + +static HRESULT STDMETHODCALLTYPE streamCopyTo(IStream * self, + IStream * target, ULARGE_INTEGER count, ULARGE_INTEGER * read, + ULARGE_INTEGER * written) +{ + if (!target) + return STG_E_INVALIDPOINTER; + if (read) + read->QuadPart = 0; + if (written) + written->QuadPart = 0; + + uint8_t * buffer = malloc(KVMFR_CLIPBOARD_FILE_READ_BYTES); + if (!buffer) + return E_OUTOFMEMORY; + + uint64_t remaining = count.QuadPart; + HRESULT result = S_OK; + while (remaining) + { + const ULONG wanted = + (ULONG)(remaining > KVMFR_CLIPBOARD_FILE_READ_BYTES ? + KVMFR_CLIPBOARD_FILE_READ_BYTES : remaining); + ULONG got = 0; + result = IStream_Read(self, buffer, wanted, &got); + if (FAILED(result) || !got) + { + if (SUCCEEDED(result)) + result = S_FALSE; + break; + } + + ULONG sent = 0; + result = IStream_Write(target, buffer, got, &sent); + if (read) + read->QuadPart += got; + if (written) + written->QuadPart += sent; + if (FAILED(result) || sent != got) + { + if (SUCCEEDED(result)) + result = STG_E_WRITEFAULT; + break; + } + remaining -= got; + } + + free(buffer); + return result; +} + +static HRESULT STDMETHODCALLTYPE streamCommit(IStream * self, DWORD flags) +{ + (void)self; + (void)flags; + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE streamRevert(IStream * self) +{ + (void)self; + return STG_E_REVERTED; +} + +static HRESULT STDMETHODCALLTYPE streamLockRegion(IStream * self, + ULARGE_INTEGER offset, ULARGE_INTEGER count, DWORD type) +{ + (void)self; + (void)offset; + (void)count; + (void)type; + return STG_E_INVALIDFUNCTION; +} + +static HRESULT STDMETHODCALLTYPE streamUnlockRegion(IStream * self, + ULARGE_INTEGER offset, ULARGE_INTEGER count, DWORD type) +{ + (void)self; + (void)offset; + (void)count; + (void)type; + return STG_E_INVALIDFUNCTION; +} + +static HRESULT STDMETHODCALLTYPE streamStat(IStream * self, STATSTG * stat, + DWORD flags) +{ + if (!stat) + return STG_E_INVALIDPOINTER; + memset(stat, 0, sizeof(*stat)); + + WindowsClipboardFileStream * stream = streamFromInterface(self); + const WindowsClipboardRemoteEntry * entry = + &stream->owner->manifest->entries[stream->entryIndex]; + stat->type = STGTY_STREAM; + stat->cbSize.QuadPart = entry->size; + stat->grfMode = STGM_READ; + stat->ctime = unixNsToFileTime(entry->createdNs); + stat->mtime = unixNsToFileTime(entry->modifiedNs); + if (!(flags & STATFLAG_NONAME)) + { + const size_t length = wcslen(entry->name); + if (length > (SIZE_MAX / sizeof(wchar_t)) - 1U) + return E_OUTOFMEMORY; + stat->pwcsName = CoTaskMemAlloc((length + 1U) * sizeof(wchar_t)); + if (!stat->pwcsName) + return E_OUTOFMEMORY; + memcpy(stat->pwcsName, entry->name, + (length + 1U) * sizeof(wchar_t)); + } + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE streamClone(IStream * self, + IStream ** clone) +{ + if (!clone) + return E_POINTER; + *clone = NULL; + + WindowsClipboardFileStream * stream = streamFromInterface(self); + EnterCriticalSection(&stream->lock); + const HRESULT result = createFileStream(stream->owner, + stream->entryIndex, stream->offset, clone); + LeaveCriticalSection(&stream->lock); + return result; +} + +static HRESULT STDMETHODCALLTYPE formatEnumeratorQueryInterface( + IEnumFORMATETC * self, REFIID iid, void ** object) +{ + if (!object) + return E_POINTER; + *object = NULL; + if (IsEqualIID(iid, &IID_IUnknown) || + IsEqualIID(iid, &IID_IEnumFORMATETC)) + { + *object = self; + IEnumFORMATETC_AddRef(self); + return S_OK; + } + return E_NOINTERFACE; +} + +static ULONG STDMETHODCALLTYPE formatEnumeratorAddRef(IEnumFORMATETC * self) +{ + WindowsClipboardFormatEnumerator * enumerator = + formatEnumeratorFromInterface(self); + return (ULONG)InterlockedIncrement(&enumerator->refs); +} + +static ULONG STDMETHODCALLTYPE formatEnumeratorRelease(IEnumFORMATETC * self) +{ + WindowsClipboardFormatEnumerator * enumerator = + formatEnumeratorFromInterface(self); + const LONG refs = + InterlockedDecrement(&enumerator->refs); + if (!refs) + free(enumerator); + return refs ? (ULONG)refs : 0; +} + +static HRESULT STDMETHODCALLTYPE formatEnumeratorNext(IEnumFORMATETC * self, + ULONG count, FORMATETC * formats, ULONG * fetched) +{ + if (fetched) + *fetched = 0; + if (!formats || (count != 1U && !fetched)) + return E_INVALIDARG; + + WindowsClipboardFormatEnumerator * enumerator = + formatEnumeratorFromInterface(self); + ULONG copied = 0; + while (copied < count && enumerator->index < enumerator->count) + formats[copied++] = enumerator->formats[enumerator->index++]; + if (fetched) + *fetched = copied; + return copied == count ? S_OK : S_FALSE; +} + +static HRESULT STDMETHODCALLTYPE formatEnumeratorSkip(IEnumFORMATETC * self, + ULONG count) +{ + WindowsClipboardFormatEnumerator * enumerator = + formatEnumeratorFromInterface(self); + const ULONG available = + enumerator->count - enumerator->index; + const ULONG skipped = + count < available ? count : available; + enumerator->index += skipped; + return skipped == count ? S_OK : S_FALSE; +} + +static HRESULT STDMETHODCALLTYPE formatEnumeratorReset(IEnumFORMATETC * self) +{ + WindowsClipboardFormatEnumerator * enumerator = + formatEnumeratorFromInterface(self); + enumerator->index = 0; + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE formatEnumeratorClone(IEnumFORMATETC * self, + IEnumFORMATETC ** clone) +{ + if (!clone) + return E_POINTER; + *clone = NULL; + + WindowsClipboardFormatEnumerator * source = + formatEnumeratorFromInterface(self); + WindowsClipboardFormatEnumerator * result = calloc(1, sizeof(*result)); + if (!result) + return E_OUTOFMEMORY; + + result->iface.lpVtbl = &formatEnumeratorVTable; + result->refs = 1; + memcpy(result->formats, source->formats, sizeof(result->formats)); + result->count = source->count; + result->index = source->index; + *clone = &result->iface; + return S_OK; +} + +HRESULT windowsClipboardRemoteCreateDataObject( + WindowsClipboardRemoteManifest * manifest, uint64_t dataset, + uint64_t acquisition, const WindowsClipboardRemoteProviderOps * ops, + void * opaque, IDataObject ** object) +{ + if (!object) + return E_POINTER; + *object = NULL; + if (!manifest || !dataset || !acquisition || !ops || !ops->read || + !ops->release) + return E_INVALIDARG; + + EnterCriticalSection(&manifest->lock); + if (manifest->rootState != 2U || + !manifest->entryCount || manifest->entryCount > LONG_MAX) + { + LeaveCriticalSection(&manifest->lock); + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + } + + FILEDESCRIPTORW * descriptors = calloc(manifest->entryCount, + sizeof(*descriptors)); + wchar_t ** paths = calloc(manifest->entryCount, + sizeof(*paths)); + if (!descriptors || !paths) + { + free(descriptors); + free(paths); + LeaveCriticalSection(&manifest->lock); + return E_OUTOFMEMORY; + } + + HRESULT result = S_OK; + for (size_t index = 0; index < manifest->entryCount; ++index) + { + const WindowsClipboardRemoteEntry * entry = &manifest->entries[index]; + if (!entry->node || !validComponent(entry->name) || + (entry->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY && + entry->listState != 2U)) + { + result = HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + break; + } + + if (!entry->parent) + paths[index] = duplicateWide(entry->name); + else + { + const ptrdiff_t parent = findRemoteEntry(manifest, entry->parent); + if (parent < 0 || (size_t)parent >= index || + manifest->entries[parent].type != + KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY || !paths[parent]) + { + result = HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + break; + } + paths[index] = joinPath(paths[parent], entry->name); + } + if (!paths[index]) + { + result = E_OUTOFMEMORY; + break; + } + + const size_t pathLength = wcslen(paths[index]); + if (pathLength >= ARRAYSIZE(descriptors[index].cFileName)) + { + result = HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + break; + } + + FILEDESCRIPTORW * descriptor = &descriptors[index]; + descriptor->dwFlags = FD_ATTRIBUTES | FD_UNICODE | FD_PROGRESSUI; + descriptor->dwFileAttributes = entry->type == + KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? FILE_ATTRIBUTE_DIRECTORY : + FILE_ATTRIBUTE_NORMAL; + if (entry->createdNs) + { + descriptor->dwFlags |= FD_CREATETIME; + descriptor->ftCreationTime = unixNsToFileTime(entry->createdNs); + } + if (entry->modifiedNs) + { + descriptor->dwFlags |= FD_WRITESTIME; + descriptor->ftLastWriteTime = unixNsToFileTime(entry->modifiedNs); + } + if (entry->type == KVMFR_CLIPBOARD_FILE_TYPE_REGULAR) + { + descriptor->dwFlags |= FD_FILESIZE; + descriptor->nFileSizeHigh = (DWORD)(entry->size >> 32U); + descriptor->nFileSizeLow = (DWORD)entry->size; + } + memcpy(descriptor->cFileName, paths[index], + (pathLength + 1U) * sizeof(wchar_t)); + } + + for (size_t index = 0; index < manifest->entryCount; ++index) + free(paths[index]); + free(paths); + if (FAILED(result)) + { + free(descriptors); + LeaveCriticalSection(&manifest->lock); + return result; + } + + const UINT fileDescriptorFormat = + RegisterClipboardFormatW(L"FileGroupDescriptorW"); + const UINT fileContentsFormat = + RegisterClipboardFormatW(L"FileContents"); + const UINT preferredDropEffectFormat = + RegisterClipboardFormatW(L"Preferred DropEffect"); + if (!fileDescriptorFormat || !fileContentsFormat || + !preferredDropEffectFormat) + { + const DWORD winError = GetLastError(); + free(descriptors); + LeaveCriticalSection(&manifest->lock); + return HRESULT_FROM_WIN32(winError ? winError : ERROR_INVALID_DATA); + } + + WindowsClipboardFileDataObject * dataObject = + calloc(1, sizeof(*dataObject)); + if (!dataObject) + { + free(descriptors); + LeaveCriticalSection(&manifest->lock); + return E_OUTOFMEMORY; + } + + dataObject->iface.lpVtbl = &dataObjectVTable; + dataObject->refs = 1; + dataObject->manifest = manifest; + dataObject->dataset = dataset; + dataObject->acquisition = acquisition; + dataObject->ops = *ops; + dataObject->opaque = opaque; + dataObject->descriptors = descriptors; + dataObject->descriptorCount = manifest->entryCount; + dataObject->fileDescriptorFormat = fileDescriptorFormat; + dataObject->fileContentsFormat = fileContentsFormat; + dataObject->preferredDropEffectFormat = preferredDropEffectFormat; + + manifest->frozen = true; + remoteManifestRetain(manifest); + LeaveCriticalSection(&manifest->lock); + + *object = &dataObject->iface; + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE dataObjectDUnadvise(IDataObject * self, + DWORD connection) +{ + (void)self; + (void)connection; + return OLE_E_ADVISENOTSUPPORTED; +} + +static HRESULT STDMETHODCALLTYPE dataObjectEnumDAdvise(IDataObject * self, + IEnumSTATDATA ** enumerator) +{ + (void)self; + if (enumerator) + *enumerator = NULL; + return OLE_E_ADVISENOTSUPPORTED; +} diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index dd26bfee..31d0e9f3 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -40,17 +40,6 @@ set_tests_properties(event-source-tests PROPERTIES TIMEOUT 10 ) -add_executable(windows-clipboard-files-tests - clipboard_files_windows_test.c - ../platform/Windows/clipboard_files.c -) -add_test(NAME windows-clipboard-files-tests - COMMAND windows-clipboard-files-tests -) -set_tests_properties(windows-clipboard-files-tests PROPERTIES - TIMEOUT 10 -) - if(LG_CLIENT_PLATFORM_LINUX) add_executable(config-platform-tests config_platform_test.c diff --git a/client/tests/clipboard_files_windows_test.c b/client/tests/clipboard_files_windows_test.c deleted file mode 100644 index 24a9f958..00000000 --- a/client/tests/clipboard_files_windows_test.c +++ /dev/null @@ -1,103 +0,0 @@ -/** - * 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 "core/clipboard_files.h" -#include "test.h" - -static struct -{ - uint64_t dataset; - uint64_t transfer; - LG_ClipboardFileError error; - unsigned int acquired; - unsigned int cancelled; -} -trace; - -bool clipboard_fileAcquired(uint64_t dataset, uint64_t acquisition, - LG_ClipboardFileError error) -{ - trace.dataset = dataset; - trace.transfer = acquisition; - trace.error = error; - ++trace.acquired; - return true; -} - -bool clipboard_fileCancel(uint64_t dataset, uint64_t request, - LG_ClipboardFileError reason) -{ - trace.dataset = dataset; - trace.transfer = request; - trace.error = reason; - ++trace.cancelled; - return true; -} - -int main(void) -{ - const LG_ClipboardFileRequest request = - { - .dataset = UINT64_C(0x1234), - .request = UINT64_C(0x5678), - .node = 1, - .operation = LG_CLIPBOARD_FILE_READ, - }; - - CHECK(clipboardFiles_init()); - CHECK(!clipboardFiles_remoteOffer(request.dataset)); - CHECK(!clipboardFiles_remoteReady(request.dataset)); - - char * data = (char *)(uintptr_t)1; - size_t size = 1; - CHECK(!clipboardFiles_getRemote("text/uri-list", &data, &size)); - CHECK(!data); - CHECK(size == 0); - - data = (char *)(uintptr_t)1; - size = 1; - CHECK(!clipboardFiles_getRemotePresentation( - 1, "text/uri-list", &data, &size)); - CHECK(!data); - CHECK(size == 0); - - clipboardFiles_localAcquire(request.dataset, request.request); - CHECK(trace.acquired == 1); - CHECK(trace.dataset == request.dataset); - CHECK(trace.transfer == request.request); - CHECK(trace.error == LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED); - - clipboardFiles_localRequest(&request); - CHECK(trace.cancelled == 1); - CHECK(trace.dataset == request.dataset); - CHECK(trace.transfer == request.request); - CHECK(trace.error == LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED); - - clipboardFiles_localRequest(NULL); - CHECK(trace.cancelled == 1); - CHECK(clipboardFiles_remoteDataBegin(&request, 0) == - LG_CLIPBOARD_RESULT_FAILED); - CHECK(clipboardFiles_remoteDataChunk(&request, 0, NULL, 0) == - LG_CLIPBOARD_RESULT_FAILED); - CHECK(clipboardFiles_remoteDataEnd(&request, 0) == - LG_CLIPBOARD_RESULT_FAILED); - clipboardFiles_free(); - return 0; -}