From a8d531b79793a8b2bfe01d82eb9a052c33e6311b Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Fri, 14 Aug 2026 04:15:31 +1000 Subject: [PATCH] [client] clipboard: introduce streamed transfers --- client/include/interface/clipboard.h | 76 +- client/include/interface/displayserver.h | 3 + client/src/clipboard.c | 1121 ++++++++++++++++++++-- client/src/clipboard.h | 10 + client/tests/CMakeLists.txt | 32 + client/tests/clipboard_test.c | 373 +++++++ client/tests/spice_clipboard_test.c | 238 +++++ 7 files changed, 1771 insertions(+), 82 deletions(-) create mode 100644 client/tests/spice_clipboard_test.c diff --git a/client/include/interface/clipboard.h b/client/include/interface/clipboard.h index 4c286571..bea00bf6 100644 --- a/client/include/interface/clipboard.h +++ b/client/include/interface/clipboard.h @@ -43,6 +43,42 @@ typedef void (*LG_ClipboardReplyFn)(void * opaque, typedef uint64_t LG_ClipboardRequest; #define LG_CLIPBOARD_REQUEST_INVALID UINT64_C(0) +#define LG_CLIPBOARD_SIZE_UNKNOWN UINT64_MAX + +typedef enum LG_ClipboardResult +{ + /* The operation is terminal and consumed no data. */ + LG_CLIPBOARD_RESULT_FAILED = 0, + /* The operation, including the complete chunk, was accepted. */ + LG_CLIPBOARD_RESULT_ACCEPTED = 1, + /* Nothing was consumed. Retry only after the matching ready callback. */ + LG_CLIPBOARD_RESULT_BLOCKED = 2, +} +LG_ClipboardResult; + +typedef enum LG_ClipboardCancelReason +{ + LG_CLIPBOARD_CANCEL_ABORTED = 0, + LG_CLIPBOARD_CANCEL_REPLACED = 1, + LG_CLIPBOARD_CANCEL_UNAVAILABLE = 2, + LG_CLIPBOARD_CANCEL_INVALID = 3, +} +LG_ClipboardCancelReason; + +/* A consumer of a provider-to-client stream. Callbacks are serialized and + * buffers are borrowed only for the duration of chunk(). A callback which + * returns BLOCKED consumes nothing; the consumer must subsequently call + * its request-ready entry point before the same operation can be retried. */ +typedef struct LG_ClipboardStreamOps +{ + LG_ClipboardResult (*begin)(void * opaque, LG_ClipboardData type, + uint64_t sizeHint); + LG_ClipboardResult (*chunk)(void * opaque, uint64_t offset, + const void * data, size_t size); + LG_ClipboardResult (*end)(void * opaque, uint64_t finalSize); + void (*cancel)(void * opaque, LG_ClipboardCancelReason reason); +} +LG_ClipboardStreamOps; typedef struct LG_ClipboardStatus { @@ -61,6 +97,22 @@ typedef struct LG_ClipboardEventOps void (*notice)(void * opaque, const LG_ClipboardData types[], size_t count); void (*data)(void * opaque, LG_ClipboardRequest request, LG_ClipboardData type, const void * data, size_t size); + LG_ClipboardResult (*dataBegin)(void * opaque, + LG_ClipboardRequest request, LG_ClipboardData type, + uint64_t sizeHint); + LG_ClipboardResult (*dataChunk)(void * opaque, + LG_ClipboardRequest request, uint64_t offset, + const void * data, size_t size); + LG_ClipboardResult (*dataEnd)(void * opaque, + LG_ClipboardRequest request, uint64_t finalSize); + void (*dataCancel)(void * opaque, LG_ClipboardRequest request, + LG_ClipboardCancelReason reason); + /* Signals that the provider can accept a retry of the operation which most + * recently returned BLOCKED for its request. */ + void (*dataReady)(void * opaque, LG_ClipboardRequest request); + /* Cancels a request previously delivered through request(). */ + void (*requestCancel)(void * opaque, LG_ClipboardRequest request, + LG_ClipboardCancelReason reason); void (*release)(void * opaque); bool (*request)(void * opaque, LG_ClipboardRequest request, LG_ClipboardData type); @@ -93,10 +145,30 @@ typedef struct LG_ClipboardOps bool (*notifyTypes)(void * opaque, const LG_ClipboardData types[], size_t count); /* Data is a complete response to a remote request. NONE with no payload - * reports that the request could not be completed. The provider must finish - * any transport framing before returning. */ + * reports that the request could not be completed. This operation is the + * legacy whole-buffer alternative to the stream operation group below. */ bool (*data)(void * opaque, LG_ClipboardRequest request, LG_ClipboardData type, const void * data, size_t size); + /* Streaming providers implement this complete group instead of data(). + * BEGIN may use LG_CLIPBOARD_SIZE_UNKNOWN. CHUNK is all-or-nothing and its + * offset must be the next byte in the stream. END's size is authoritative. + * A BLOCKED operation consumes nothing and is retried only after dataReady + * is delivered through the attached event operations. Ready callbacks must + * be serialized with other events, delivered without backend locks held, + * and never delivered synchronously from one of these operations. */ + LG_ClipboardResult (*dataBegin)(void * opaque, + LG_ClipboardRequest request, LG_ClipboardData type, + uint64_t sizeHint); + LG_ClipboardResult (*dataChunk)(void * opaque, + LG_ClipboardRequest request, uint64_t offset, + const void * data, size_t size); + LG_ClipboardResult (*dataEnd)(void * opaque, + LG_ClipboardRequest request, uint64_t finalSize); + bool (*dataCancel)(void * opaque, LG_ClipboardRequest request, + LG_ClipboardCancelReason reason); + /* Signals that the client can accept a retry of a provider-to-client stream + * operation which returned BLOCKED. */ + bool (*dataReady)(void * opaque, LG_ClipboardRequest request); /* A successful request produces exactly one matching data event unless the * provider is detached, becomes unavailable, or publishes a newer notice * or release first. */ diff --git a/client/include/interface/displayserver.h b/client/include/interface/displayserver.h index 0d313014..9be6d71b 100644 --- a/client/include/interface/displayserver.h +++ b/client/include/interface/displayserver.h @@ -249,6 +249,9 @@ struct LG_DisplayServerOps void (*cbNotice)(LG_ClipboardData type); void (*cbRelease)(void); void (*cbRequest)(LG_ClipboardRequest request, LG_ClipboardData type); + void (*cbRequestReady)(LG_ClipboardRequest request); + void (*cbRequestCancel)(LG_ClipboardRequest request, + LG_ClipboardCancelReason reason); }; #ifdef ENABLE_EGL diff --git a/client/src/clipboard.c b/client/src/clipboard.c index 621ec4ed..60e05b53 100644 --- a/client/src/clipboard.c +++ b/client/src/clipboard.c @@ -29,6 +29,8 @@ #include #include +#define COMPAT_CHUNK_SIZE (64U * 1024U) + typedef struct ClipboardBinding { const LG_ClipboardOps * ops; @@ -45,10 +47,33 @@ typedef struct ClipboardRequest uint32_t remoteGeneration; LG_ClipboardData type; LG_ClipboardReplyFn replyFn; - void * opaque; + const LG_ClipboardStreamOps * stream; + void * opaque; + + bool started; + bool blocked; + uint64_t sizeHint; + uint64_t offset; + + uint8_t * buffer; + size_t bufferSize; + size_t bufferCapacity; + + bool compat; + unsigned int compatPhase; + LG_ClipboardData compatType; + uint8_t * compatData; + size_t compatSize; } ClipboardRequest; +enum +{ + COMPAT_BEGIN, + COMPAT_CHUNK, + COMPAT_END, +}; + static struct { LG_Lock registrationLock; @@ -76,6 +101,17 @@ static struct LG_ClipboardRequest remoteRequestId; LG_ClipboardRequest remoteTransferId; LG_ClipboardData remoteRequestType; + bool remoteStarted; + bool remoteBlocked; + uint64_t remoteSizeHint; + uint64_t remoteOffset; + + uint8_t * remoteBuffer; + size_t remoteBufferSize; + size_t remoteBufferCapacity; + + bool remoteCompat; + unsigned int remoteCompatPhase; LG_ClipboardRequest requestSerial; LG_ClipboardRequest transferSerial; @@ -83,6 +119,8 @@ static struct } clipboard; +static LG_ClipboardResult pumpRemoteCompat(void); + static bool validType(LG_ClipboardData type) { return type >= LG_CLIPBOARD_DATA_TEXT && @@ -132,13 +170,144 @@ static bool localHasTypeNL(LG_ClipboardData type) return false; } -static void clearRemoteRequestNL(void) +static bool validStream(const LG_ClipboardStreamOps * stream) { + return stream && stream->begin && stream->chunk && + stream->end && stream->cancel; +} + +static bool streamProvider(const LG_ClipboardOps * ops) +{ + return ops && ops->dataBegin && ops->dataChunk && ops->dataEnd && + ops->dataCancel && ops->dataReady; +} + +static bool growBuffer(uint8_t ** buffer, size_t * capacity, size_t size) +{ + if (size <= *capacity) + return true; + + size_t next = *capacity ? *capacity : 4096; + while (next < size) + { + if (next > SIZE_MAX / 2) + { + next = size; + break; + } + next *= 2; + } + + uint8_t * resized = realloc(*buffer, next); + if (!resized) + return false; + + *buffer = resized; + *capacity = next; + return true; +} + +static LG_ClipboardResult legacyBegin(void * opaque, + LG_ClipboardData type, uint64_t sizeHint) +{ + ClipboardRequest * request = opaque; + if (type != request->type || + (sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && sizeHint > UINT32_MAX)) + return LG_CLIPBOARD_RESULT_FAILED; + + free(request->buffer); + request->buffer = NULL; + request->bufferSize = 0; + request->bufferCapacity = 0; + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult legacyChunk(void * opaque, uint64_t offset, + const void * data, size_t size) +{ + ClipboardRequest * request = opaque; + if (offset != request->bufferSize || !size || !data || + offset > UINT32_MAX || size > UINT32_MAX - offset) + return LG_CLIPBOARD_RESULT_FAILED; + + const size_t required = request->bufferSize + size; + if (!growBuffer(&request->buffer, &request->bufferCapacity, required)) + return LG_CLIPBOARD_RESULT_FAILED; + + memcpy(request->buffer + request->bufferSize, data, size); + request->bufferSize = required; + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult legacyEnd(void * opaque, uint64_t finalSize) +{ + ClipboardRequest * request = opaque; + if (finalSize != request->bufferSize || finalSize > UINT32_MAX) + return LG_CLIPBOARD_RESULT_FAILED; + + request->replyFn(request->opaque, request->type, + request->bufferSize ? request->buffer : NULL, + (uint32_t)request->bufferSize); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static void legacyCancel(void * opaque, LG_ClipboardCancelReason reason) +{ + (void)reason; + ClipboardRequest * request = opaque; + request->replyFn( + request->opaque, LG_CLIPBOARD_DATA_NONE, NULL, 0); +} + +static const LG_ClipboardStreamOps legacyStream = +{ + .begin = legacyBegin, + .chunk = legacyChunk, + .end = legacyEnd, + .cancel = legacyCancel, +}; + +static LG_ClipboardRequest clearRemoteRequestNL(void) +{ + const LG_ClipboardRequest transfer = clipboard.remoteRequest ? + clipboard.remoteTransferId : LG_CLIPBOARD_REQUEST_INVALID; + free(clipboard.remoteBuffer); clipboard.remoteRequest = false; clipboard.remoteRequestBinding = (ClipboardBinding) { 0 }; clipboard.remoteRequestId = LG_CLIPBOARD_REQUEST_INVALID; clipboard.remoteTransferId = LG_CLIPBOARD_REQUEST_INVALID; clipboard.remoteRequestType = LG_CLIPBOARD_DATA_NONE; + clipboard.remoteStarted = false; + clipboard.remoteBlocked = false; + clipboard.remoteSizeHint = LG_CLIPBOARD_SIZE_UNKNOWN; + clipboard.remoteOffset = 0; + clipboard.remoteBuffer = NULL; + clipboard.remoteBufferSize = 0; + clipboard.remoteBufferCapacity = 0; + clipboard.remoteCompat = false; + clipboard.remoteCompatPhase = COMPAT_BEGIN; + return transfer; +} + +static void freeRequest(ClipboardRequest * request) +{ + if (!request) + return; + + free(request->compatData); + free(request->buffer); + free(request); +} + +static void * requestStreamOpaque(ClipboardRequest * request) +{ + return request->replyFn ? request : request->opaque; +} + +static void cancelRequest(ClipboardRequest * request, + LG_ClipboardCancelReason reason) +{ + request->stream->cancel(requestStreamOpaque(request), reason); } static ClipboardRequest * takeCancelableRequest( @@ -187,20 +356,20 @@ static ClipboardRequest * takeRequest(LG_ClipboardRequest id) /* callbackLock must be held while completing canceled requests. */ static void cancelRequestsNL(const ClipboardBinding * binding, - uint32_t generation, bool all) + uint32_t generation, bool all, LG_ClipboardCancelReason reason) { ClipboardRequest * request; while ((request = takeCancelableRequest(binding, generation, all))) { - request->replyFn( - request->opaque, LG_CLIPBOARD_DATA_NONE, NULL, 0); - free(request); + cancelRequest(request, reason); + freeRequest(request); } } static void resetRemote(void) { bool release; + LG_ClipboardRequest transfer; LG_LOCK(clipboard.requestLock); LG_LOCK(clipboard.stateLock); @@ -210,12 +379,16 @@ static void resetRemote(void) clipboard.remoteType = LG_CLIPBOARD_DATA_NONE; clipboard.remoteGeneration = nextGeneration( clipboard.remoteGeneration); - clearRemoteRequestNL(); + transfer = clearRemoteRequestNL(); LG_UNLOCK(clipboard.stateLock); LG_UNLOCK(clipboard.requestLock); LG_LOCK(clipboard.callbackLock); - cancelRequestsNL(NULL, 0, true); + cancelRequestsNL(NULL, 0, true, LG_CLIPBOARD_CANCEL_UNAVAILABLE); + if (transfer != LG_CLIPBOARD_REQUEST_INVALID && + g_state.ds->cbRequestCancel) + g_state.ds->cbRequestCancel( + transfer, LG_CLIPBOARD_CANCEL_UNAVAILABLE); LG_LOCK(clipboard.stateLock); release = release && clipboard.localAvailable && !clipboard.remoteNotice; @@ -228,7 +401,8 @@ static void resetRemote(void) static bool validOps(const LG_ClipboardOps * ops) { return ops && ops->name && ops->attach && ops->detach && - ops->release && ops->notifyTypes && ops->data && ops->request; + ops->release && ops->notifyTypes && ops->request && + (ops->data || streamProvider(ops)); } static ClipboardBinding makeBinding( @@ -300,6 +474,7 @@ static void eventNotice(void * opaque, LG_UNLOCK_SHARED(clipboard.activeLock); bool notice; uint32_t generation; + LG_ClipboardRequest transfer; LG_LOCK(clipboard.requestLock); LG_LOCK(clipboard.stateLock); @@ -308,13 +483,17 @@ static void eventNotice(void * opaque, clipboard.remoteGeneration = nextGeneration( clipboard.remoteGeneration); generation = clipboard.remoteGeneration; - clearRemoteRequestNL(); + transfer = clearRemoteRequestNL(); notice = clipboard.localAvailable && g_params.clipboardToLocal; LG_UNLOCK(clipboard.stateLock); LG_UNLOCK(clipboard.requestLock); LG_LOCK(clipboard.callbackLock); - cancelRequestsNL(¤t, generation, false); + cancelRequestsNL(¤t, generation, false, + LG_CLIPBOARD_CANCEL_REPLACED); + if (transfer != LG_CLIPBOARD_REQUEST_INVALID && + g_state.ds->cbRequestCancel) + g_state.ds->cbRequestCancel(transfer, LG_CLIPBOARD_CANCEL_REPLACED); LG_LOCK(clipboard.stateLock); notice = notice && clipboard.localAvailable && clipboard.remoteNotice && @@ -328,31 +507,36 @@ static void eventNotice(void * opaque, LG_UNLOCK(clipboard.callbackLock); } -static void eventData(void * opaque, LG_ClipboardRequest id, - LG_ClipboardData type, const void * data, size_t size) +static bool returnRequest(ClipboardRequest * request) +{ + if (ll_push(clipboard.requests, request)) + return true; + + cancelRequest(request, LG_CLIPBOARD_CANCEL_UNAVAILABLE); + freeRequest(request); + return false; +} + +/* callbackLock must be held. The returned request has been removed from the + * request list and must either be returned to it or freed. */ +static ClipboardRequest * takeIncoming(ClipboardBinding * binding, + LG_ClipboardRequest id, ClipboardBinding * current) { - ClipboardBinding * binding = opaque; if (id == LG_CLIPBOARD_REQUEST_INVALID) - return; + return NULL; LG_LOCK_SHARED(clipboard.activeLock); if (!bindingActiveNL(binding)) { LG_UNLOCK_SHARED(clipboard.activeLock); - return; + return NULL; } - - const ClipboardBinding current = *binding; + *current = *binding; LG_UNLOCK_SHARED(clipboard.activeLock); - LG_LOCK(clipboard.callbackLock); + ClipboardRequest * request = takeRequest(id); if (!request) - { - LG_UNLOCK(clipboard.callbackLock); - DEBUG_WARN("Ignoring stale clipboard data from %s", - current.ops->name); - return; - } + return NULL; LG_LOCK(clipboard.stateLock); const bool deliver = clipboard.localAvailable && @@ -361,22 +545,333 @@ static void eventData(void * opaque, LG_ClipboardRequest id, LG_UNLOCK(clipboard.stateLock); LG_LOCK_SHARED(clipboard.activeLock); - const bool active = bindingActiveNL(¤t); + const bool active = bindingActiveNL(current); LG_UNLOCK_SHARED(clipboard.activeLock); - if (!deliver || !active || !bindingEqual(&request->binding, ¤t) || - type != request->type || !validType(type) || - size > UINT32_MAX || (size && !data)) + if (deliver && active && bindingEqual(&request->binding, current)) + return request; + + cancelRequest(request, LG_CLIPBOARD_CANCEL_UNAVAILABLE); + freeRequest(request); + return NULL; +} + +static LG_ClipboardResult incomingBegin(ClipboardRequest * request, + LG_ClipboardData type, uint64_t sizeHint, bool * terminal) +{ + if (request->started || request->blocked || type != request->type || + !validType(type)) + goto invalid; + + const LG_ClipboardResult result = request->stream->begin( + requestStreamOpaque(request), type, sizeHint); + switch (result) { - DEBUG_ERROR("Invalid clipboard response from %s", current.ops->name); - request->replyFn( - request->opaque, LG_CLIPBOARD_DATA_NONE, NULL, 0); + case LG_CLIPBOARD_RESULT_ACCEPTED: + request->started = true; + request->sizeHint = sizeHint; + request->offset = 0; + return result; + + case LG_CLIPBOARD_RESULT_BLOCKED: + request->blocked = true; + return result; + + case LG_CLIPBOARD_RESULT_FAILED: + break; + } + +invalid: + cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID); + *terminal = true; + return LG_CLIPBOARD_RESULT_FAILED; +} + +static LG_ClipboardResult incomingChunk(ClipboardRequest * request, + uint64_t offset, const void * data, size_t size, bool * terminal) +{ + if (!request->started || request->blocked || offset != request->offset || + !data || !size || size > UINT64_MAX - offset) + goto invalid; + + const LG_ClipboardResult result = request->stream->chunk( + requestStreamOpaque(request), offset, data, size); + switch (result) + { + case LG_CLIPBOARD_RESULT_ACCEPTED: + request->offset += size; + return result; + + case LG_CLIPBOARD_RESULT_BLOCKED: + request->blocked = true; + return result; + + case LG_CLIPBOARD_RESULT_FAILED: + break; + } + +invalid: + cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID); + *terminal = true; + return LG_CLIPBOARD_RESULT_FAILED; +} + +static LG_ClipboardResult incomingEnd(ClipboardRequest * request, + uint64_t finalSize, bool * terminal) +{ + if (!request->started || request->blocked || finalSize != request->offset) + goto invalid; + + const LG_ClipboardResult result = request->stream->end( + requestStreamOpaque(request), finalSize); + switch (result) + { + case LG_CLIPBOARD_RESULT_ACCEPTED: + *terminal = true; + return result; + + case LG_CLIPBOARD_RESULT_BLOCKED: + request->blocked = true; + return result; + + case LG_CLIPBOARD_RESULT_FAILED: + break; + } + +invalid: + cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID); + *terminal = true; + return LG_CLIPBOARD_RESULT_FAILED; +} + +static LG_ClipboardResult pumpIncomingCompat(ClipboardRequest * request, + bool * terminal) +{ + for (;;) + { + LG_ClipboardResult result; + switch (request->compatPhase) + { + case COMPAT_BEGIN: + result = incomingBegin(request, request->compatType, + request->compatSize, terminal); + if (result != LG_CLIPBOARD_RESULT_ACCEPTED) + return result; + request->compatPhase = COMPAT_CHUNK; + break; + + case COMPAT_CHUNK: + if (request->offset < request->compatSize) + { + const size_t remaining = request->compatSize - request->offset; + const size_t size = remaining < COMPAT_CHUNK_SIZE ? + remaining : COMPAT_CHUNK_SIZE; + result = incomingChunk(request, request->offset, + request->compatData + request->offset, size, terminal); + if (result != LG_CLIPBOARD_RESULT_ACCEPTED) + return result; + break; + } + request->compatPhase = COMPAT_END; + break; + + case COMPAT_END: + return incomingEnd(request, request->compatSize, terminal); + + default: + cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID); + *terminal = true; + return LG_CLIPBOARD_RESULT_FAILED; + } + } +} + +static LG_ClipboardResult eventDataBegin(void * opaque, + LG_ClipboardRequest id, LG_ClipboardData type, uint64_t sizeHint) +{ + ClipboardBinding * binding = opaque; + LG_LOCK(clipboard.callbackLock); + ClipboardBinding current; + ClipboardRequest * request = takeIncoming(binding, id, ¤t); + if (!request) + { + LG_UNLOCK(clipboard.callbackLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + bool terminal = false; + LG_ClipboardResult result; + if (request->compat) + { + cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID); + terminal = true; + result = LG_CLIPBOARD_RESULT_FAILED; } else - request->replyFn( - request->opaque, type, data, (uint32_t)size); - + result = incomingBegin(request, type, sizeHint, &terminal); + if (!terminal && !returnRequest(request)) + { + LG_UNLOCK(clipboard.callbackLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (terminal) + freeRequest(request); LG_UNLOCK(clipboard.callbackLock); - free(request); + return result; +} + +static LG_ClipboardResult eventDataChunk(void * opaque, + LG_ClipboardRequest id, uint64_t offset, const void * data, size_t size) +{ + ClipboardBinding * binding = opaque; + LG_LOCK(clipboard.callbackLock); + ClipboardBinding current; + ClipboardRequest * request = takeIncoming(binding, id, ¤t); + if (!request) + { + LG_UNLOCK(clipboard.callbackLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + bool terminal = false; + LG_ClipboardResult result; + if (request->compat) + { + cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID); + terminal = true; + result = LG_CLIPBOARD_RESULT_FAILED; + } + else + result = incomingChunk(request, offset, data, size, &terminal); + if (!terminal && !returnRequest(request)) + { + LG_UNLOCK(clipboard.callbackLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (terminal) + freeRequest(request); + LG_UNLOCK(clipboard.callbackLock); + return result; +} + +static LG_ClipboardResult eventDataEnd(void * opaque, + LG_ClipboardRequest id, uint64_t finalSize) +{ + ClipboardBinding * binding = opaque; + LG_LOCK(clipboard.callbackLock); + ClipboardBinding current; + ClipboardRequest * request = takeIncoming(binding, id, ¤t); + if (!request) + { + LG_UNLOCK(clipboard.callbackLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + bool terminal = false; + LG_ClipboardResult result; + if (request->compat) + { + cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID); + terminal = true; + result = LG_CLIPBOARD_RESULT_FAILED; + } + else + result = incomingEnd(request, finalSize, &terminal); + if (!terminal && !returnRequest(request)) + { + LG_UNLOCK(clipboard.callbackLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (terminal) + freeRequest(request); + LG_UNLOCK(clipboard.callbackLock); + return result; +} + +static void eventDataCancel(void * opaque, LG_ClipboardRequest id, + LG_ClipboardCancelReason reason) +{ + ClipboardBinding * binding = opaque; + LG_LOCK(clipboard.callbackLock); + ClipboardBinding current; + ClipboardRequest * request = takeIncoming(binding, id, ¤t); + if (request) + { + cancelRequest(request, reason); + freeRequest(request); + } + LG_UNLOCK(clipboard.callbackLock); +} + +static void eventData(void * opaque, LG_ClipboardRequest id, + LG_ClipboardData type, const void * data, size_t size) +{ + ClipboardBinding * binding = opaque; + if (type == LG_CLIPBOARD_DATA_NONE) + { + eventDataCancel(opaque, id, LG_CLIPBOARD_CANCEL_ABORTED); + return; + } + + LG_LOCK(clipboard.callbackLock); + ClipboardBinding current; + ClipboardRequest * request = takeIncoming(binding, id, ¤t); + if (!request) + { + LG_UNLOCK(clipboard.callbackLock); + DEBUG_WARN("Ignoring stale clipboard data"); + return; + } + + bool terminal = false; + LG_ClipboardResult result = LG_CLIPBOARD_RESULT_FAILED; + if (type != request->type || !validType(type) || (size && !data) || + request->started || request->compat) + { + cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID); + terminal = true; + } + else if (request->replyFn && size <= UINT32_MAX) + { + request->replyFn(request->opaque, type, data, (uint32_t)size); + terminal = true; + result = LG_CLIPBOARD_RESULT_ACCEPTED; + } + else if (!request->replyFn) + { + if (size) + { + request->compatData = malloc(size); + if (request->compatData) + memcpy(request->compatData, data, size); + } + if (!size || request->compatData) + { + request->compat = true; + request->compatPhase = COMPAT_BEGIN; + request->compatType = type; + request->compatSize = size; + result = pumpIncomingCompat(request, &terminal); + } + else + { + cancelRequest(request, LG_CLIPBOARD_CANCEL_UNAVAILABLE); + terminal = true; + } + } + else + { + cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID); + terminal = true; + } + + if (!terminal && !returnRequest(request)) + result = LG_CLIPBOARD_RESULT_FAILED; + if (terminal) + freeRequest(request); + LG_UNLOCK(clipboard.callbackLock); + + if (result == LG_CLIPBOARD_RESULT_FAILED) + DEBUG_ERROR("Invalid clipboard response from %s", current.ops->name); } static void eventRelease(void * opaque) @@ -392,6 +887,7 @@ static void eventRelease(void * opaque) LG_UNLOCK_SHARED(clipboard.activeLock); bool release; + LG_ClipboardRequest transfer; LG_LOCK(clipboard.requestLock); LG_LOCK(clipboard.stateLock); release = clipboard.remoteNotice && clipboard.localAvailable && @@ -400,11 +896,15 @@ static void eventRelease(void * opaque) clipboard.remoteType = LG_CLIPBOARD_DATA_NONE; clipboard.remoteGeneration = nextGeneration( clipboard.remoteGeneration); + transfer = clearRemoteRequestNL(); LG_UNLOCK(clipboard.stateLock); LG_UNLOCK(clipboard.requestLock); LG_LOCK(clipboard.callbackLock); - cancelRequestsNL(NULL, 0, true); + cancelRequestsNL(NULL, 0, true, LG_CLIPBOARD_CANCEL_REPLACED); + if (transfer != LG_CLIPBOARD_REQUEST_INVALID && + g_state.ds->cbRequestCancel) + g_state.ds->cbRequestCancel(transfer, LG_CLIPBOARD_CANCEL_REPLACED); LG_LOCK(clipboard.stateLock); release = release && clipboard.localAvailable && !clipboard.remoteNotice; @@ -474,12 +974,68 @@ static bool eventRequest(void * opaque, LG_ClipboardRequest id, return deliver && active; } +static void eventDataReady(void * opaque, LG_ClipboardRequest id) +{ + ClipboardBinding * binding = opaque; + LG_LOCK(clipboard.callbackLock); + LG_LOCK(clipboard.writeLock); + LG_LOCK_SHARED(clipboard.activeLock); + LG_LOCK(clipboard.stateLock); + const bool valid = bindingActiveNL(binding) && clipboard.remoteRequest && + clipboard.remoteRequestId == id && clipboard.remoteBlocked && + bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active); + const bool compat = valid && clipboard.remoteCompat; + const LG_ClipboardRequest transfer = valid ? + clipboard.remoteTransferId : LG_CLIPBOARD_REQUEST_INVALID; + if (valid) + clipboard.remoteBlocked = false; + LG_UNLOCK(clipboard.stateLock); + + if (compat) + pumpRemoteCompat(); + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.writeLock); + if (!compat && valid && g_state.ds->cbRequestReady) + g_state.ds->cbRequestReady(transfer); + LG_UNLOCK(clipboard.callbackLock); +} + +static void eventRequestCancel(void * opaque, LG_ClipboardRequest id, + LG_ClipboardCancelReason reason) +{ + ClipboardBinding * binding = opaque; + LG_LOCK(clipboard.callbackLock); + LG_LOCK(clipboard.writeLock); + LG_LOCK_SHARED(clipboard.activeLock); + LG_LOCK(clipboard.stateLock); + const bool valid = bindingActiveNL(binding) && clipboard.remoteRequest && + clipboard.remoteRequestId == id && + bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active); + const LG_ClipboardRequest transfer = valid ? + clipboard.remoteTransferId : LG_CLIPBOARD_REQUEST_INVALID; + if (valid) + clearRemoteRequestNL(); + LG_UNLOCK(clipboard.stateLock); + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.writeLock); + + if (valid && g_state.ds->cbRequestCancel) + g_state.ds->cbRequestCancel(transfer, reason); + LG_UNLOCK(clipboard.callbackLock); +} + static const LG_ClipboardEventOps eventOps = { - .notice = eventNotice, - .data = eventData, - .release = eventRelease, - .request = eventRequest, + .notice = eventNotice, + .data = eventData, + .dataBegin = eventDataBegin, + .dataChunk = eventDataChunk, + .dataEnd = eventDataEnd, + .dataCancel = eventDataCancel, + .dataReady = eventDataReady, + .requestCancel = eventRequestCancel, + .release = eventRelease, + .request = eventRequest, }; /* providerLock must be held. dropActive suppresses all calls into an endpoint @@ -633,7 +1189,7 @@ void lgClipboard_free(void) lgClipboard_setTransport(NULL, NULL); lgClipboard_setFallback(NULL, NULL); LG_LOCK(clipboard.callbackLock); - cancelRequestsNL(NULL, 0, true); + cancelRequestsNL(NULL, 0, true, LG_CLIPBOARD_CANCEL_UNAVAILABLE); LG_UNLOCK(clipboard.callbackLock); if (clipboard.requests) { @@ -655,6 +1211,7 @@ void lgClipboard_setLocalAvailable(bool available) bool notice; bool release; LG_ClipboardData type; + LG_ClipboardRequest transfer = LG_CLIPBOARD_REQUEST_INVALID; LG_LOCK(clipboard.callbackLock); LG_LOCK(clipboard.requestLock); @@ -666,7 +1223,7 @@ void lgClipboard_setLocalAvailable(bool available) g_params.clipboardToLocal; type = clipboard.remoteType; if (!available) - clearRemoteRequestNL(); + transfer = clearRemoteRequestNL(); LG_UNLOCK(clipboard.stateLock); LG_UNLOCK(clipboard.requestLock); @@ -674,7 +1231,11 @@ void lgClipboard_setLocalAvailable(bool available) g_state.ds->cbNotice(type); if (!available) { - cancelRequestsNL(NULL, 0, true); + cancelRequestsNL(NULL, 0, true, LG_CLIPBOARD_CANCEL_UNAVAILABLE); + if (transfer != LG_CLIPBOARD_REQUEST_INVALID && + g_state.ds->cbRequestCancel) + g_state.ds->cbRequestCancel( + transfer, LG_CLIPBOARD_CANCEL_UNAVAILABLE); if (release) g_state.ds->cbRelease(); } @@ -768,6 +1329,302 @@ void lgClipboard_notifyTypes( LG_UNLOCK(clipboard.writeLock); } +/* writeLock and activeLock must be held. */ +static bool cancelRemoteNL(LG_ClipboardCancelReason reason) +{ + const bool result = streamProvider(clipboard.active.ops) ? + clipboard.active.ops->dataCancel(clipboard.active.opaque, + clipboard.remoteRequestId, reason) : + clipboard.active.ops->data(clipboard.active.opaque, + clipboard.remoteRequestId, LG_CLIPBOARD_DATA_NONE, NULL, 0); + clearRemoteRequestNL(); + return result; +} + +/* writeLock and activeLock must be held. */ +static LG_ClipboardResult sendBegin(LG_ClipboardRequest transfer, + LG_ClipboardData type, uint64_t sizeHint) +{ + LG_LOCK(clipboard.stateLock); + const bool matches = clipboard.remoteRequest && + clipboard.remoteTransferId == transfer && clipboard.active.ops && + bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active); + if (!matches) + { + LG_UNLOCK(clipboard.stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (clipboard.remoteBlocked) + { + LG_UNLOCK(clipboard.stateLock); + return LG_CLIPBOARD_RESULT_BLOCKED; + } + if (clipboard.remoteStarted || clipboard.remoteRequestType != type || + !validType(type)) + { + cancelRemoteNL(LG_CLIPBOARD_CANCEL_INVALID); + LG_UNLOCK(clipboard.stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + LG_ClipboardResult result; + if (streamProvider(clipboard.active.ops)) + result = clipboard.active.ops->dataBegin(clipboard.active.opaque, + clipboard.remoteRequestId, type, sizeHint); + else if (sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && sizeHint > SIZE_MAX) + result = LG_CLIPBOARD_RESULT_FAILED; + else + { + free(clipboard.remoteBuffer); + clipboard.remoteBuffer = NULL; + clipboard.remoteBufferSize = 0; + clipboard.remoteBufferCapacity = 0; + result = LG_CLIPBOARD_RESULT_ACCEPTED; + } + + if (result == LG_CLIPBOARD_RESULT_ACCEPTED) + { + clipboard.remoteStarted = true; + clipboard.remoteSizeHint = sizeHint; + clipboard.remoteOffset = 0; + } + else if (result == LG_CLIPBOARD_RESULT_BLOCKED) + clipboard.remoteBlocked = true; + else + clearRemoteRequestNL(); + LG_UNLOCK(clipboard.stateLock); + return result; +} + +/* writeLock and activeLock must be held. */ +static LG_ClipboardResult sendChunk(LG_ClipboardRequest transfer, + uint64_t offset, const void * data, size_t size) +{ + LG_LOCK(clipboard.stateLock); + const bool matches = clipboard.remoteRequest && + clipboard.remoteTransferId == transfer && clipboard.active.ops && + bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active); + if (!matches) + { + LG_UNLOCK(clipboard.stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (clipboard.remoteBlocked) + { + LG_UNLOCK(clipboard.stateLock); + return LG_CLIPBOARD_RESULT_BLOCKED; + } + if (!clipboard.remoteStarted || clipboard.remoteOffset != offset || + !data || !size || size > UINT64_MAX - offset) + { + cancelRemoteNL(LG_CLIPBOARD_CANCEL_INVALID); + LG_UNLOCK(clipboard.stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + LG_ClipboardResult result; + if (streamProvider(clipboard.active.ops)) + result = clipboard.active.ops->dataChunk(clipboard.active.opaque, + clipboard.remoteRequestId, offset, data, size); + else if (offset > SIZE_MAX || size > SIZE_MAX - (size_t)offset || + !growBuffer(&clipboard.remoteBuffer, + &clipboard.remoteBufferCapacity, (size_t)offset + size)) + result = LG_CLIPBOARD_RESULT_FAILED; + else + { + memcpy(clipboard.remoteBuffer + offset, data, size); + clipboard.remoteBufferSize = (size_t)offset + size; + result = LG_CLIPBOARD_RESULT_ACCEPTED; + } + + if (result == LG_CLIPBOARD_RESULT_ACCEPTED) + clipboard.remoteOffset += size; + else if (result == LG_CLIPBOARD_RESULT_BLOCKED) + clipboard.remoteBlocked = true; + else + clearRemoteRequestNL(); + LG_UNLOCK(clipboard.stateLock); + return result; +} + +/* writeLock and activeLock must be held. */ +static LG_ClipboardResult sendEnd(LG_ClipboardRequest transfer, + uint64_t finalSize) +{ + LG_LOCK(clipboard.stateLock); + const bool matches = clipboard.remoteRequest && + clipboard.remoteTransferId == transfer && clipboard.active.ops && + bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active); + if (!matches) + { + LG_UNLOCK(clipboard.stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (clipboard.remoteBlocked) + { + LG_UNLOCK(clipboard.stateLock); + return LG_CLIPBOARD_RESULT_BLOCKED; + } + if (!clipboard.remoteStarted || clipboard.remoteOffset != finalSize) + { + cancelRemoteNL(LG_CLIPBOARD_CANCEL_INVALID); + LG_UNLOCK(clipboard.stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + LG_ClipboardResult result; + if (streamProvider(clipboard.active.ops)) + result = clipboard.active.ops->dataEnd(clipboard.active.opaque, + clipboard.remoteRequestId, finalSize); + else + result = clipboard.active.ops->data(clipboard.active.opaque, + clipboard.remoteRequestId, clipboard.remoteRequestType, + clipboard.remoteBufferSize ? clipboard.remoteBuffer : NULL, + clipboard.remoteBufferSize) ? + LG_CLIPBOARD_RESULT_ACCEPTED : LG_CLIPBOARD_RESULT_FAILED; + + if (result == LG_CLIPBOARD_RESULT_BLOCKED) + clipboard.remoteBlocked = true; + else + clearRemoteRequestNL(); + LG_UNLOCK(clipboard.stateLock); + return result; +} + +/* writeLock and activeLock must be held. */ +static bool sendCancel(LG_ClipboardRequest transfer, + LG_ClipboardCancelReason reason) +{ + LG_LOCK(clipboard.stateLock); + const bool valid = clipboard.remoteRequest && + clipboard.remoteTransferId == transfer && clipboard.active.ops && + bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active); + if (!valid) + { + LG_UNLOCK(clipboard.stateLock); + return false; + } + + const bool result = cancelRemoteNL(reason); + LG_UNLOCK(clipboard.stateLock); + return result; +} + +static LG_ClipboardResult pumpRemoteCompat(void) +{ + for (;;) + { + LG_LOCK(clipboard.stateLock); + if (!clipboard.remoteRequest || !clipboard.remoteCompat) + { + LG_UNLOCK(clipboard.stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + const LG_ClipboardRequest transfer = clipboard.remoteTransferId; + const LG_ClipboardData type = clipboard.remoteRequestType; + const unsigned int phase = clipboard.remoteCompatPhase; + const uint64_t offset = clipboard.remoteOffset; + const size_t total = clipboard.remoteBufferSize; + const uint8_t * data = clipboard.remoteBuffer; + LG_UNLOCK(clipboard.stateLock); + + LG_ClipboardResult result; + switch (phase) + { + case COMPAT_BEGIN: + result = sendBegin(transfer, type, total); + if (result != LG_CLIPBOARD_RESULT_ACCEPTED) + return result; + LG_LOCK(clipboard.stateLock); + if (clipboard.remoteRequest) + clipboard.remoteCompatPhase = COMPAT_CHUNK; + LG_UNLOCK(clipboard.stateLock); + break; + + case COMPAT_CHUNK: + if (offset < total) + { + const size_t remaining = total - offset; + const size_t size = remaining < COMPAT_CHUNK_SIZE ? + remaining : COMPAT_CHUNK_SIZE; + result = sendChunk(transfer, offset, data + offset, size); + if (result != LG_CLIPBOARD_RESULT_ACCEPTED) + return result; + break; + } + LG_LOCK(clipboard.stateLock); + if (clipboard.remoteRequest) + clipboard.remoteCompatPhase = COMPAT_END; + LG_UNLOCK(clipboard.stateLock); + break; + + case COMPAT_END: + return sendEnd(transfer, total); + + default: + sendCancel(transfer, LG_CLIPBOARD_CANCEL_INVALID); + return LG_CLIPBOARD_RESULT_FAILED; + } + } +} + +LG_ClipboardResult lgClipboard_dataBegin(LG_ClipboardRequest transfer, + LG_ClipboardData type, uint64_t sizeHint) +{ + if (!g_params.clipboardToVM || + transfer == LG_CLIPBOARD_REQUEST_INVALID) + return LG_CLIPBOARD_RESULT_FAILED; + + LG_LOCK(clipboard.writeLock); + LG_LOCK_SHARED(clipboard.activeLock); + LG_LOCK(clipboard.stateLock); + const bool compat = clipboard.remoteCompat; + LG_UNLOCK(clipboard.stateLock); + const LG_ClipboardResult result = compat ? + LG_CLIPBOARD_RESULT_FAILED : sendBegin(transfer, type, sizeHint); + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.writeLock); + return result; +} + +LG_ClipboardResult lgClipboard_dataChunk(LG_ClipboardRequest transfer, + uint64_t offset, const void * data, size_t size) +{ + if (!g_params.clipboardToVM || + transfer == LG_CLIPBOARD_REQUEST_INVALID) + return LG_CLIPBOARD_RESULT_FAILED; + + LG_LOCK(clipboard.writeLock); + LG_LOCK_SHARED(clipboard.activeLock); + LG_LOCK(clipboard.stateLock); + const bool compat = clipboard.remoteCompat; + LG_UNLOCK(clipboard.stateLock); + const LG_ClipboardResult result = compat ? + LG_CLIPBOARD_RESULT_FAILED : sendChunk(transfer, offset, data, size); + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.writeLock); + return result; +} + +LG_ClipboardResult lgClipboard_dataEnd(LG_ClipboardRequest transfer, + uint64_t finalSize) +{ + if (!g_params.clipboardToVM || + transfer == LG_CLIPBOARD_REQUEST_INVALID) + return LG_CLIPBOARD_RESULT_FAILED; + + LG_LOCK(clipboard.writeLock); + LG_LOCK_SHARED(clipboard.activeLock); + LG_LOCK(clipboard.stateLock); + const bool compat = clipboard.remoteCompat; + LG_UNLOCK(clipboard.stateLock); + const LG_ClipboardResult result = compat ? + LG_CLIPBOARD_RESULT_FAILED : sendEnd(transfer, finalSize); + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.writeLock); + return result; +} + void lgClipboard_data(LG_ClipboardRequest transfer, LG_ClipboardData type, const void * data, size_t size) { @@ -778,35 +1635,62 @@ void lgClipboard_data(LG_ClipboardRequest transfer, LG_LOCK(clipboard.writeLock); LG_LOCK_SHARED(clipboard.activeLock); LG_LOCK(clipboard.stateLock); - const bool matches = clipboard.remoteRequest && clipboard.remoteTransferId == transfer; - if (!matches) + const bool provider = matches && clipboard.active.ops && + bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active); + const bool valid = provider && clipboard.remoteRequestType == type && + validType(type) && (!size || data) && !clipboard.remoteStarted && + !clipboard.remoteBlocked && !clipboard.remoteCompat; + + if (!valid) { LG_UNLOCK(clipboard.stateLock); + if (provider) + sendCancel(transfer, LG_CLIPBOARD_CANCEL_INVALID); LG_UNLOCK_SHARED(clipboard.activeLock); LG_UNLOCK(clipboard.writeLock); + DEBUG_WARN("Ignoring unexpected local clipboard data"); return; } - const LG_ClipboardRequest request = clipboard.remoteRequestId; - const bool provider = clipboard.active.ops && - bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active); - const bool valid = provider && clipboard.remoteRequestType == type && - validType(type) && (!size || data); - clearRemoteRequestNL(); + if (!streamProvider(clipboard.active.ops)) + { + const LG_ClipboardRequest request = clipboard.remoteRequestId; + clearRemoteRequestNL(); + LG_UNLOCK(clipboard.stateLock); + const bool result = clipboard.active.ops->data(clipboard.active.opaque, + request, type, data, size); + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.writeLock); + if (!result) + DEBUG_WARN("Failed to send remote clipboard data"); + return; + } + + uint8_t * copy = size ? malloc(size) : NULL; + if (size && !copy) + { + LG_UNLOCK(clipboard.stateLock); + sendCancel(transfer, LG_CLIPBOARD_CANCEL_UNAVAILABLE); + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.writeLock); + DEBUG_ERROR("Out of memory"); + return; + } + if (size) + memcpy(copy, data, size); + clipboard.remoteBuffer = copy; + clipboard.remoteBufferSize = size; + clipboard.remoteBufferCapacity = size; + clipboard.remoteCompat = true; + clipboard.remoteCompatPhase = COMPAT_BEGIN; LG_UNLOCK(clipboard.stateLock); - const bool result = provider && clipboard.active.ops->data( - clipboard.active.opaque, request, - valid ? type : LG_CLIPBOARD_DATA_NONE, - valid ? data : NULL, valid ? size : 0); + const LG_ClipboardResult result = pumpRemoteCompat(); LG_UNLOCK_SHARED(clipboard.activeLock); LG_UNLOCK(clipboard.writeLock); - - if (!valid) - DEBUG_WARN("Ignoring unexpected local clipboard data"); - else if (!result) + if (result == LG_CLIPBOARD_RESULT_FAILED) DEBUG_WARN("Failed to send remote clipboard data"); } @@ -818,19 +1702,8 @@ void lgClipboard_abort(LG_ClipboardRequest transfer) LG_LOCK(clipboard.writeLock); LG_LOCK_SHARED(clipboard.activeLock); - LG_LOCK(clipboard.stateLock); - const bool matches = clipboard.remoteRequest && - clipboard.remoteTransferId == transfer; - const bool valid = matches && clipboard.active.ops && - bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active); - const LG_ClipboardRequest request = clipboard.remoteRequestId; - if (matches) - clearRemoteRequestNL(); - LG_UNLOCK(clipboard.stateLock); - - const bool result = !valid || clipboard.active.ops->data( - clipboard.active.opaque, request, - LG_CLIPBOARD_DATA_NONE, NULL, 0); + const bool result = sendCancel( + transfer, LG_CLIPBOARD_CANCEL_ABORTED); LG_UNLOCK_SHARED(clipboard.activeLock); LG_UNLOCK(clipboard.writeLock); @@ -838,14 +1711,17 @@ void lgClipboard_abort(LG_ClipboardRequest transfer) DEBUG_WARN("Failed to abort remote clipboard data"); } -bool lgClipboard_request(LG_ClipboardData type, - LG_ClipboardReplyFn replyFn, void * opaque) +static bool requestClipboard(LG_ClipboardData type, + LG_ClipboardReplyFn replyFn, const LG_ClipboardStreamOps * stream, + void * opaque, LG_ClipboardRequest * resultId) { + if (resultId) + *resultId = LG_CLIPBOARD_REQUEST_INVALID; if (!g_params.clipboardToLocal || !validType(type) || - !replyFn || !clipboard.requests) + (!replyFn && !validStream(stream)) || !clipboard.requests) return false; - ClipboardRequest * request = malloc(sizeof(*request)); + ClipboardRequest * request = calloc(1, sizeof(*request)); if (!request) { DEBUG_ERROR("Out of memory"); @@ -856,7 +1732,7 @@ bool lgClipboard_request(LG_ClipboardData type, if (!clipboard.active.ops) { LG_UNLOCK_SHARED(clipboard.activeLock); - free(request); + freeRequest(request); return false; } @@ -876,7 +1752,9 @@ bool lgClipboard_request(LG_ClipboardData type, .remoteGeneration = generation, .type = type, .replyFn = replyFn, + .stream = replyFn ? &legacyStream : stream, .opaque = opaque, + .sizeHint = LG_CLIPBOARD_SIZE_UNKNOWN, }; } LG_UNLOCK(clipboard.stateLock); @@ -891,16 +1769,99 @@ bool lgClipboard_request(LG_ClipboardData type, LG_UNLOCK_SHARED(clipboard.activeLock); if (result) + { + if (resultId) + *resultId = id; return true; + } if (queued) { ClipboardRequest * failed = takeRequest(id); if (!failed) + { + if (resultId) + *resultId = id; return true; - free(failed); + } + freeRequest(failed); return false; } - free(request); + freeRequest(request); return false; } + +bool lgClipboard_requestStream(LG_ClipboardData type, + const LG_ClipboardStreamOps * stream, void * opaque, + LG_ClipboardRequest * request) +{ + if (!request) + return false; + return requestClipboard(type, NULL, stream, opaque, request); +} + +bool lgClipboard_requestReady(LG_ClipboardRequest id) +{ + if (id == LG_CLIPBOARD_REQUEST_INVALID || !clipboard.requests) + return false; + + LG_LOCK(clipboard.callbackLock); + ClipboardRequest * request = takeRequest(id); + if (!request || !request->blocked) + { + if (request) + returnRequest(request); + LG_UNLOCK(clipboard.callbackLock); + return false; + } + + request->blocked = false; + if (request->compat) + { + bool terminal = false; + const LG_ClipboardResult result = + pumpIncomingCompat(request, &terminal); + if (!terminal && !returnRequest(request)) + { + LG_UNLOCK(clipboard.callbackLock); + return false; + } + if (terminal) + freeRequest(request); + LG_UNLOCK(clipboard.callbackLock); + return result != LG_CLIPBOARD_RESULT_FAILED; + } + + const ClipboardBinding binding = request->binding; + if (!returnRequest(request)) + { + LG_UNLOCK(clipboard.callbackLock); + return false; + } + + LG_LOCK_SHARED(clipboard.activeLock); + const bool active = bindingActiveNL(&binding) && + streamProvider(binding.ops); + const bool result = active && binding.ops->dataReady( + binding.opaque, id); + LG_UNLOCK_SHARED(clipboard.activeLock); + if (!result) + { + ClipboardRequest * failed = takeRequest(id); + if (failed) + { + cancelRequest(failed, LG_CLIPBOARD_CANCEL_UNAVAILABLE); + freeRequest(failed); + } + } + LG_UNLOCK(clipboard.callbackLock); + return result; +} + +bool lgClipboard_request(LG_ClipboardData type, + LG_ClipboardReplyFn replyFn, void * opaque) +{ + if (!replyFn) + return false; + return requestClipboard(type, replyFn, NULL, opaque, NULL); +} diff --git a/client/src/clipboard.h b/client/src/clipboard.h index a671a780..5fcc1aa7 100644 --- a/client/src/clipboard.h +++ b/client/src/clipboard.h @@ -36,9 +36,19 @@ void lgClipboard_dropTransport(void); void lgClipboard_release(void); void lgClipboard_notifyTypes( const LG_ClipboardData types[], size_t count); +LG_ClipboardResult lgClipboard_dataBegin(LG_ClipboardRequest request, + LG_ClipboardData type, uint64_t sizeHint); +LG_ClipboardResult lgClipboard_dataChunk(LG_ClipboardRequest request, + uint64_t offset, const void * data, size_t size); +LG_ClipboardResult lgClipboard_dataEnd(LG_ClipboardRequest request, + uint64_t finalSize); void lgClipboard_data(LG_ClipboardRequest request, LG_ClipboardData type, const void * data, size_t size); void lgClipboard_abort(LG_ClipboardRequest request); +bool lgClipboard_requestStream(LG_ClipboardData type, + const LG_ClipboardStreamOps * stream, void * opaque, + LG_ClipboardRequest * request); +bool lgClipboard_requestReady(LG_ClipboardRequest request); bool lgClipboard_request(LG_ClipboardData type, LG_ClipboardReplyFn replyFn, void * opaque); diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 3c80bc4b..7e50d25a 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -428,6 +428,10 @@ set(CLIPBOARD_CASES generation local reentrant + stream-local + stream-blocked + stream-remote + stream-legacy ) foreach(name IN LISTS CLIPBOARD_CASES) add_test(NAME clipboard-${name} @@ -438,6 +442,34 @@ foreach(name IN LISTS CLIPBOARD_CASES) ) endforeach() +add_executable(spice-clipboard-tests + spice_clipboard_test.c + ../src/clipboard.c + ../transports/SPICE/clipboard.c +) +target_compile_definitions(spice-clipboard-tests PRIVATE + CIMGUI_DEFINE_ENUMS_AND_STRUCTS=1 +) +target_include_directories(spice-clipboard-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../src" + "${CMAKE_CURRENT_SOURCE_DIR}/../transports/SPICE" + "${PROJECT_TOP}/repos/PureSpice/include" + "${PROJECT_TOP}/repos/gui/cimgui" + "${PROJECT_TOP}/repos/gui/cimgui/imgui" +) +target_link_libraries(spice-clipboard-tests + ${EXE_FLAGS} + lg_common +) +foreach(name IN ITEMS outbound inbound) + add_test(NAME spice-clipboard-${name} + COMMAND spice-clipboard-tests ${name} + ) + set_tests_properties(spice-clipboard-${name} PROPERTIES + TIMEOUT 10 + ) +endforeach() + add_executable(transport-fallback-tests transport_fallback_test.c ../src/transport_fallback.c diff --git a/client/tests/clipboard_test.c b/client/tests/clipboard_test.c index 8ff687b0..284f3c31 100644 --- a/client/tests/clipboard_test.c +++ b/client/tests/clipboard_test.c @@ -54,6 +54,11 @@ struct Provider unsigned int release; unsigned int notice; unsigned int data; + unsigned int begin; + unsigned int chunk; + unsigned int end; + unsigned int cancel; + unsigned int ready; unsigned int request; LG_ClipboardData noticeTypes[LG_CLIPBOARD_DATA_NONE]; size_t noticeCount; @@ -63,6 +68,17 @@ struct Provider LG_ClipboardData dataType[MAX_CALL]; size_t dataSize[MAX_CALL]; uint8_t dataBuf[MAX_CALL][MAX_DATA]; + LG_ClipboardResult beginResult; + LG_ClipboardResult chunkResult; + LG_ClipboardResult endResult; + LG_ClipboardRequest streamId; + LG_ClipboardData streamType; + uint64_t sizeHint; + uint64_t chunkOffset[MAX_CALL]; + size_t chunkSize[MAX_CALL]; + uint8_t chunkBuf[MAX_CALL][MAX_DATA]; + uint64_t finalSize; + LG_ClipboardCancelReason cancelReason; }; struct Display @@ -70,6 +86,8 @@ struct Display unsigned int notice; unsigned int release; unsigned int request; + unsigned int ready; + unsigned int cancel; LG_ClipboardData noticeType[MAX_CALL]; LG_ClipboardRequest reqId[MAX_CALL]; LG_ClipboardData reqType[MAX_CALL]; @@ -77,6 +95,9 @@ struct Display LG_ClipboardData autoType; const void * autoBuf; size_t autoSize; + bool autoBegin; + LG_ClipboardResult readyResult; + LG_ClipboardCancelReason cancelReason; }; struct Reply @@ -89,6 +110,24 @@ struct Reply bool nested; }; +struct StreamSink +{ + unsigned int begin; + unsigned int chunk; + unsigned int end; + unsigned int cancel; + LG_ClipboardResult beginResult; + LG_ClipboardResult chunkResult; + LG_ClipboardResult endResult; + LG_ClipboardData type; + uint64_t sizeHint; + uint64_t offset; + uint64_t finalSize; + LG_ClipboardCancelReason reason; + size_t size; + uint8_t data[MAX_DATA]; +}; + static struct Provider p; static struct Provider q; static struct Provider r; @@ -97,6 +136,61 @@ static struct Display d; struct AppState g_state; struct AppParams g_params; +static LG_ClipboardResult sinkBegin(void * opaque, + LG_ClipboardData type, uint64_t sizeHint) +{ + struct StreamSink * sink = opaque; + ++sink->begin; + sink->type = type; + sink->sizeHint = sizeHint; + return sink->beginResult; +} + +static LG_ClipboardResult sinkChunk(void * opaque, uint64_t offset, + const void * data, size_t size) +{ + struct StreamSink * sink = opaque; + ++sink->chunk; + sink->offset = offset; + if (sink->chunkResult == LG_CLIPBOARD_RESULT_ACCEPTED) + { + CHECK(offset + size <= MAX_DATA); + memcpy(sink->data + offset, data, size); + sink->size = offset + size; + } + return sink->chunkResult; +} + +static LG_ClipboardResult sinkEnd(void * opaque, uint64_t finalSize) +{ + struct StreamSink * sink = opaque; + ++sink->end; + sink->finalSize = finalSize; + return sink->endResult; +} + +static void sinkCancel(void * opaque, LG_ClipboardCancelReason reason) +{ + struct StreamSink * sink = opaque; + ++sink->cancel; + sink->reason = reason; +} + +static const LG_ClipboardStreamOps sinkOps = +{ + .begin = sinkBegin, + .chunk = sinkChunk, + .end = sinkEnd, + .cancel = sinkCancel, +}; + +static void initSink(struct StreamSink * sink) +{ + sink->beginResult = LG_CLIPBOARD_RESULT_ACCEPTED; + sink->chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; + sink->endResult = LG_CLIPBOARD_RESULT_ACCEPTED; +} + static void setStat(void * opaque, LG_ClipboardStatusFn callback, void * callbackOpaque) { @@ -175,6 +269,59 @@ static bool data(void * opaque, LG_ClipboardRequest id, return provider->callOK; } +static LG_ClipboardResult dataBegin(void * opaque, + LG_ClipboardRequest id, LG_ClipboardData type, uint64_t sizeHint) +{ + struct Provider * provider = opaque; + ++provider->begin; + provider->streamId = id; + provider->streamType = type; + provider->sizeHint = sizeHint; + return provider->beginResult; +} + +static LG_ClipboardResult dataChunk(void * opaque, + LG_ClipboardRequest id, uint64_t offset, const void * buf, size_t size) +{ + struct Provider * provider = opaque; + CHECK(provider->chunk < MAX_CALL); + CHECK(id == provider->streamId); + const unsigned int no = provider->chunk++; + provider->chunkOffset[no] = offset; + provider->chunkSize[no] = size; + if (size <= MAX_DATA) + memcpy(provider->chunkBuf[no], buf, size); + return provider->chunkResult; +} + +static LG_ClipboardResult dataEnd(void * opaque, + LG_ClipboardRequest id, uint64_t finalSize) +{ + struct Provider * provider = opaque; + CHECK(id == provider->streamId); + ++provider->end; + provider->finalSize = finalSize; + return provider->endResult; +} + +static bool dataCancel(void * opaque, LG_ClipboardRequest id, + LG_ClipboardCancelReason reason) +{ + struct Provider * provider = opaque; + ++provider->cancel; + provider->streamId = id; + provider->cancelReason = reason; + return provider->callOK; +} + +static bool dataReady(void * opaque, LG_ClipboardRequest id) +{ + struct Provider * provider = opaque; + ++provider->ready; + provider->streamId = id; + return provider->callOK; +} + static bool request(void * opaque, LG_ClipboardRequest id, LG_ClipboardData type) { @@ -209,6 +356,21 @@ static const LG_ClipboardOps statOps = .request = request, }; +static const LG_ClipboardOps streamOps = +{ + .name = "stream", + .attach = attach, + .detach = detach, + .release = release, + .notifyTypes = notify, + .dataBegin = dataBegin, + .dataChunk = dataChunk, + .dataEnd = dataEnd, + .dataCancel = dataCancel, + .dataReady = dataReady, + .request = request, +}; + static void dsNotice(LG_ClipboardData type) { CHECK(d.notice < MAX_CALL); @@ -230,11 +392,29 @@ static void dsRequest(LG_ClipboardRequest id, LG_ClipboardData type) lgClipboard_data(id, d.autoType, d.autoBuf, d.autoSize); } +static void dsRequestReady(LG_ClipboardRequest id) +{ + ++d.ready; + if (d.autoBegin) + d.readyResult = lgClipboard_dataBegin( + id, LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN); +} + +static void dsRequestCancel(LG_ClipboardRequest id, + LG_ClipboardCancelReason reason) +{ + (void)id; + ++d.cancel; + d.cancelReason = reason; +} + static struct LG_DisplayServerOps dsOps = { .cbNotice = dsNotice, .cbRelease = dsRelease, .cbRequest = dsRequest, + .cbRequestReady = dsRequestReady, + .cbRequestCancel = dsRequestCancel, }; static void initProvider(struct Provider * provider) @@ -244,6 +424,9 @@ static void initProvider(struct Provider * provider) provider->callOK = true; provider->reqOK = true; provider->gen = 1; + provider->beginResult = LG_CLIPBOARD_RESULT_ACCEPTED; + provider->chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; + provider->endResult = LG_CLIPBOARD_RESULT_ACCEPTED; } static void init(void) @@ -634,6 +817,192 @@ static void testReentrant(void) lgClipboard_free(); } +static void testStreamLocal(void) +{ + init(); + lgClipboard_setFallback(&streamOps, &p); + const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; + lgClipboard_notifyTypes(types, 1); + + CHECK(p.ev->request(p.evCtx, 40, LG_CLIPBOARD_DATA_TEXT)); + const LG_ClipboardRequest transfer = d.reqId[0]; + CHECK(lgClipboard_dataBegin(transfer, LG_CLIPBOARD_DATA_TEXT, + LG_CLIPBOARD_SIZE_UNKNOWN) == LG_CLIPBOARD_RESULT_ACCEPTED); + const uint8_t first[] = { 1, 2 }; + const uint8_t second[] = { 3, 4, 5 }; + CHECK(lgClipboard_dataChunk(transfer, 0, first, sizeof(first)) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(lgClipboard_dataChunk(transfer, sizeof(first), + second, sizeof(second)) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(lgClipboard_dataEnd(transfer, + sizeof(first) + sizeof(second)) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(p.begin == 1); + CHECK(p.streamId == 40); + CHECK(p.streamType == LG_CLIPBOARD_DATA_TEXT); + CHECK(p.sizeHint == LG_CLIPBOARD_SIZE_UNKNOWN); + CHECK(p.chunk == 2); + CHECK(p.chunkOffset[0] == 0); + CHECK(p.chunkOffset[1] == sizeof(first)); + CHECK(p.finalSize == sizeof(first) + sizeof(second)); + + CHECK(p.ev->request(p.evCtx, 41, LG_CLIPBOARD_DATA_TEXT)); + const LG_ClipboardRequest invalid = d.reqId[1]; + CHECK(lgClipboard_dataBegin(invalid, LG_CLIPBOARD_DATA_TEXT, 1) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(lgClipboard_dataChunk(invalid, 1, first, 1) == + LG_CLIPBOARD_RESULT_FAILED); + CHECK(p.cancel == 1); + CHECK(p.streamId == 41); + CHECK(p.cancelReason == LG_CLIPBOARD_CANCEL_INVALID); + + CHECK(p.ev->request(p.evCtx, 42, LG_CLIPBOARD_DATA_TEXT)); + p.ev->requestCancel(p.evCtx, 42, LG_CLIPBOARD_CANCEL_REPLACED); + CHECK(d.cancel == 1); + CHECK(d.cancelReason == LG_CLIPBOARD_CANCEL_REPLACED); + + lgClipboard_free(); +} + +static void testStreamBlocked(void) +{ + init(); + lgClipboard_setFallback(&streamOps, &p); + const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; + lgClipboard_notifyTypes(types, 1); + CHECK(p.ev->request(p.evCtx, 50, LG_CLIPBOARD_DATA_TEXT)); + + p.beginResult = LG_CLIPBOARD_RESULT_BLOCKED; + uint8_t data[] = { 7, 8, 9 }; + lgClipboard_data(d.reqId[0], LG_CLIPBOARD_DATA_TEXT, + data, sizeof(data)); + CHECK(p.begin == 1); + CHECK(p.chunk == 0); + CHECK(p.end == 0); + data[0] = 0; + + p.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED; + p.ev->dataReady(p.evCtx, 50); + CHECK(p.begin == 2); + CHECK(p.chunk == 1); + CHECK(p.end == 1); + CHECK(p.chunkSize[0] == 3); + CHECK(p.chunkBuf[0][0] == 7); + CHECK(p.chunkBuf[0][1] == 8); + CHECK(p.chunkBuf[0][2] == 9); + CHECK(p.finalSize == 3); + + CHECK(p.ev->request(p.evCtx, 51, LG_CLIPBOARD_DATA_TEXT)); + const LG_ClipboardRequest transfer = d.reqId[1]; + p.beginResult = LG_CLIPBOARD_RESULT_BLOCKED; + CHECK(lgClipboard_dataBegin(transfer, LG_CLIPBOARD_DATA_TEXT, + LG_CLIPBOARD_SIZE_UNKNOWN) == LG_CLIPBOARD_RESULT_BLOCKED); + p.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED; + d.autoBegin = true; + p.ev->dataReady(p.evCtx, 51); + CHECK(d.ready == 1); + CHECK(d.readyResult == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(lgClipboard_dataEnd(transfer, 0) == + LG_CLIPBOARD_RESULT_ACCEPTED); + + lgClipboard_free(); +} + +static void testStreamRemote(void) +{ + init(); + lgClipboard_setFallback(&streamOps, &p); + const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; + notice(&p, types, 1); + + struct StreamSink sink = { 0 }; + initSink(&sink); + LG_ClipboardRequest id; + CHECK(lgClipboard_requestStream( + LG_CLIPBOARD_DATA_TEXT, &sinkOps, &sink, &id)); + CHECK(id == p.reqId[0]); + CHECK(p.ev->dataBegin(p.evCtx, id, LG_CLIPBOARD_DATA_TEXT, + LG_CLIPBOARD_SIZE_UNKNOWN) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(sink.begin == 1); + CHECK(sink.sizeHint == LG_CLIPBOARD_SIZE_UNKNOWN); + + const uint8_t data[] = { 1, 3, 5, 7 }; + sink.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED; + CHECK(p.ev->dataChunk(p.evCtx, id, 0, data, sizeof(data)) == + LG_CLIPBOARD_RESULT_BLOCKED); + CHECK(sink.chunk == 1); + sink.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; + CHECK(lgClipboard_requestReady(id)); + CHECK(p.ready == 1); + CHECK(p.streamId == id); + CHECK(p.ev->dataChunk(p.evCtx, id, 0, data, sizeof(data)) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(p.ev->dataEnd(p.evCtx, id, sizeof(data)) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(sink.chunk == 2); + CHECK(sink.end == 1); + CHECK(sink.finalSize == sizeof(data)); + CHECK(memcmp(sink.data, data, sizeof(data)) == 0); + + struct StreamSink cancel = { 0 }; + initSink(&cancel); + CHECK(lgClipboard_requestStream( + LG_CLIPBOARD_DATA_TEXT, &sinkOps, &cancel, &id)); + p.ev->dataCancel(p.evCtx, id, LG_CLIPBOARD_CANCEL_ABORTED); + CHECK(cancel.cancel == 1); + CHECK(cancel.reason == LG_CLIPBOARD_CANCEL_ABORTED); + + lgClipboard_free(); +} + +static void testStreamLegacy(void) +{ + init(); + bind(&p); + const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; + lgClipboard_notifyTypes(types, 1); + + CHECK(p.ev->request(p.evCtx, 60, LG_CLIPBOARD_DATA_TEXT)); + const LG_ClipboardRequest transfer = d.reqId[0]; + const uint8_t first[] = { 2, 4 }; + const uint8_t second[] = { 6, 8 }; + CHECK(lgClipboard_dataBegin(transfer, LG_CLIPBOARD_DATA_TEXT, + LG_CLIPBOARD_SIZE_UNKNOWN) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(lgClipboard_dataChunk(transfer, 0, first, sizeof(first)) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(lgClipboard_dataChunk(transfer, sizeof(first), second, + sizeof(second)) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(lgClipboard_dataEnd(transfer, 4) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(p.data == 1); + CHECK(p.dataId[0] == 60); + CHECK(p.dataType[0] == LG_CLIPBOARD_DATA_TEXT); + CHECK(p.dataSize[0] == 4); + CHECK(memcmp(p.dataBuf[0], "\x02\x04\x06\x08", 4) == 0); + + notice(&p, types, 1); + struct StreamSink sink = { 0 }; + initSink(&sink); + sink.beginResult = LG_CLIPBOARD_RESULT_BLOCKED; + LG_ClipboardRequest id; + CHECK(lgClipboard_requestStream( + LG_CLIPBOARD_DATA_TEXT, &sinkOps, &sink, &id)); + const uint8_t remote[] = { 9, 8, 7 }; + remoteData(&p, id, LG_CLIPBOARD_DATA_TEXT, remote, sizeof(remote)); + CHECK(sink.begin == 1); + CHECK(sink.chunk == 0); + + sink.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED; + CHECK(lgClipboard_requestReady(id)); + CHECK(p.ready == 0); + CHECK(sink.begin == 2); + CHECK(sink.chunk == 1); + CHECK(sink.end == 1); + CHECK(sink.size == sizeof(remote)); + CHECK(memcmp(sink.data, remote, sizeof(remote)) == 0); + + lgClipboard_free(); +} + struct Test { const char * name; @@ -649,6 +1018,10 @@ static const struct Test tests[] = { "generation", testGeneration }, { "local" , testLocal }, { "reentrant" , testReentrant }, + { "stream-local" , testStreamLocal }, + { "stream-blocked", testStreamBlocked }, + { "stream-remote" , testStreamRemote }, + { "stream-legacy" , testStreamLegacy }, }; int main(int argc, char ** argv) diff --git a/client/tests/spice_clipboard_test.c b/client/tests/spice_clipboard_test.c new file mode 100644 index 00000000..d93a3a6d --- /dev/null +++ b/client/tests/spice_clipboard_test.c @@ -0,0 +1,238 @@ +/** + * 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 "../src/clipboard.h" +#include "../src/main.h" +#include "../transports/SPICE/clipboard.h" +#include "test.h" + +#include "common/debug.h" + +#include +#include +#include +#include + +#define MAX_DATA 32U +#define TEST_ALARM_S 5U + +struct AppState g_state; +struct AppParams g_params; + +static struct +{ + unsigned int grab; + unsigned int request; + unsigned int dataStart; + unsigned int data; + PSDataType requestType; + PSDataType dataType; + size_t dataSize; + uint8_t dataBuf[MAX_DATA]; + + unsigned int notice; + LG_ClipboardRequest localRequest; + + unsigned int reply; + LG_ClipboardData replyType; + uint32_t replySize; + uint8_t replyBuf[MAX_DATA]; +} +test; + +bool purespice_clipboardRequest(PSDataType type) +{ + ++test.request; + test.requestType = type; + return true; +} + +bool purespice_clipboardGrab(PSDataType types[], int count) +{ + CHECK(types); + CHECK(count > 0); + ++test.grab; + return true; +} + +bool purespice_clipboardRelease(void) +{ + return true; +} + +bool purespice_clipboardDataStart(PSDataType type, size_t size) +{ + ++test.dataStart; + test.dataType = type; + test.dataSize = size; + return true; +} + +bool purespice_clipboardData( + PSDataType type, uint8_t * data, size_t size) +{ + CHECK(type == test.dataType); + CHECK(size == test.dataSize); + CHECK(size <= sizeof(test.dataBuf)); + ++test.data; + if (size) + memcpy(test.dataBuf, data, size); + return true; +} + +static void cbNotice(LG_ClipboardData type) +{ + CHECK(type == LG_CLIPBOARD_DATA_TEXT); + ++test.notice; +} + +static void cbRelease(void) +{ +} + +static void cbRequest( + LG_ClipboardRequest request, LG_ClipboardData type) +{ + CHECK(type == LG_CLIPBOARD_DATA_TEXT); + test.localRequest = request; +} + +static struct LG_DisplayServerOps displayOps = +{ + .cbNotice = cbNotice, + .cbRelease = cbRelease, + .cbRequest = cbRequest, +}; + +static void reply(void * opaque, LG_ClipboardData type, + const uint8_t * data, uint32_t size) +{ + (void)opaque; + ++test.reply; + test.replyType = type; + test.replySize = size; + CHECK(size <= sizeof(test.replyBuf)); + if (size) + memcpy(test.replyBuf, data, size); +} + +static SpiceClipboard * setup(void) +{ + SpiceClipboard * clipboard; + CHECK(spiceClipboard_init(&clipboard)); + spiceClipboard_setCallbackTarget(clipboard); + spiceClipboard_setAvailable(clipboard, true); + + g_state.ds = &displayOps; + g_params.clipboardToVM = true; + g_params.clipboardToLocal = true; + lgClipboard_init(); + lgClipboard_setLocalAvailable(true); + lgClipboard_setFallback(spiceClipboard_getOps(), clipboard); + return clipboard; +} + +static void teardown(SpiceClipboard ** clipboard) +{ + lgClipboard_free(); + spiceClipboard_setCallbackTarget(NULL); + spiceClipboard_free(clipboard); +} + +static void testOutbound(void) +{ + SpiceClipboard * clipboard = setup(); + const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; + lgClipboard_notifyTypes(types, 1); + CHECK(test.grab == 1); + + spiceClipboard_request(SPICE_DATA_TEXT); + CHECK(test.localRequest != LG_CLIPBOARD_REQUEST_INVALID); + const uint8_t first[] = { 1, 2 }; + const uint8_t second[] = { 3, 4 }; + CHECK(lgClipboard_dataBegin(test.localRequest, + LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(lgClipboard_dataChunk(test.localRequest, + 0, first, sizeof(first)) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(lgClipboard_dataChunk(test.localRequest, + sizeof(first), second, sizeof(second)) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(lgClipboard_dataEnd(test.localRequest, 4) == + LG_CLIPBOARD_RESULT_ACCEPTED); + + CHECK(test.dataStart == 1); + CHECK(test.data == 1); + CHECK(test.dataType == SPICE_DATA_TEXT); + CHECK(test.dataSize == 4); + CHECK(memcmp(test.dataBuf, "\x01\x02\x03\x04", 4) == 0); + teardown(&clipboard); +} + +static void testInbound(void) +{ + SpiceClipboard * clipboard = setup(); + spiceClipboard_notice(SPICE_DATA_TEXT); + CHECK(test.notice == 1); + CHECK(lgClipboard_request(LG_CLIPBOARD_DATA_TEXT, reply, NULL)); + CHECK(test.request == 1); + CHECK(test.requestType == SPICE_DATA_TEXT); + + uint8_t data[] = "one\r\ntwo"; + spiceClipboard_data(SPICE_DATA_TEXT, data, sizeof(data) - 1); + CHECK(test.reply == 1); + CHECK(test.replyType == LG_CLIPBOARD_DATA_TEXT); + CHECK(test.replySize == 7); + CHECK(memcmp(test.replyBuf, "one\ntwo", 7) == 0); + teardown(&clipboard); +} + +struct Test +{ + const char * name; + void (*run)(void); +}; + +static const struct Test tests[] = +{ + { "outbound", testOutbound }, + { "inbound" , testInbound }, +}; + +int main(int argc, char ** argv) +{ + if (argc != 2) + { + fprintf(stderr, "usage: %s \n", argv[0]); + return EXIT_FAILURE; + } + + alarm(TEST_ALARM_S); + debug_init(); + for (unsigned int i = 0; i < sizeof(tests) / sizeof(tests[0]); ++i) + if (strcmp(argv[1], tests[i].name) == 0) + { + tests[i].run(); + return EXIT_SUCCESS; + } + + fprintf(stderr, "unknown test: %s\n", argv[1]); + return EXIT_FAILURE; +}