From a841be3515eed952bba1bfc0b3cabd51e93e6638 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Fri, 14 Aug 2026 16:08:22 +1000 Subject: [PATCH] [client] lgmp: transport clipboard files --- client/tests/CMakeLists.txt | 6 + client/tests/lgmp_clipboard_test.c | 734 ++++++++++++++- client/transports/LGMP/clipboard.c | 1414 +++++++++++++++++++++++++--- 3 files changed, 2031 insertions(+), 123 deletions(-) diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 81e208cb..64274d5c 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -123,8 +123,14 @@ set(LGMP_CLIPBOARD_CASES blocked cancel restart + process-restart + file-stream + file-limits + file-owner-loss + file-malformed disconnect malformed + file-format ) foreach(name IN LISTS LGMP_CLIPBOARD_CASES) add_test(NAME lgmp-clipboard-${name} diff --git a/client/tests/lgmp_clipboard_test.c b/client/tests/lgmp_clipboard_test.c index 07b34813..59ac5d49 100644 --- a/client/tests/lgmp_clipboard_test.c +++ b/client/tests/lgmp_clipboard_test.c @@ -65,9 +65,15 @@ typedef struct EventTrace atomic_uint dataEnd; atomic_uint dataCancel; atomic_uint dataReady; + atomic_uint fileOffer; + atomic_uint fileAcquire; + atomic_uint fileAcquired; + atomic_uint fileRequest; + atomic_uint fileCancel; atomic_bool acceptRequest; atomic_bool blockChunkOnce; atomic_bool attached; + atomic_bool fileOfferBeforeNotice; LG_ClipboardData noticeTypes[LG_CLIPBOARD_DATA_NONE]; size_t noticeCount; LG_ClipboardRequest requestID; @@ -81,6 +87,10 @@ typedef struct EventTrace uint64_t finalSize; LG_ClipboardRequest cancelID; LG_ClipboardCancelReason cancelReason; + uint64_t fileDataset; + uint64_t fileAcquisition; + LG_ClipboardFileError fileError; + LG_ClipboardFileRequest fileRequestValue; } EventTrace; @@ -94,6 +104,7 @@ typedef struct TestState const LG_ClipboardOps * ops; uint32_t clientID; uint32_t hostSerial; + uint64_t localClipboardGeneration; PLGMPMemory memories[TEST_MEMORY_MAX]; unsigned memoryCount; PLGMPMemory grantMemory[KVMFR_CLIPBOARD_SLOT_COUNT]; @@ -119,6 +130,10 @@ static void notice(void * opaque, const LG_ClipboardData types[], size_t count) { EventTrace * trace = opaque; + if (count == 1 && types[0] == LG_CLIPBOARD_DATA_FILES) + atomic_store_explicit(&trace->fileOfferBeforeNotice, + atomic_load_explicit(&trace->fileOffer, memory_order_acquire) != 0, + memory_order_release); trace->noticeCount = count; if (count) memcpy(trace->noticeTypes, types, count * sizeof(*types)); @@ -202,7 +217,51 @@ static void requestCancelEvent(void * opaque, LG_ClipboardRequest id, trace->cancelID = id; trace->cancelReason = reason; atomic_fetch_add_explicit( - &trace->requestCancel, 1, memory_order_release); + &trace->requestCancel, 1, memory_order_release); +} + +static void fileOfferEvent(void * opaque, uint64_t dataset) +{ + EventTrace * trace = opaque; + trace->fileDataset = dataset; + atomic_fetch_add_explicit(&trace->fileOffer, 1, memory_order_release); +} + +static void fileAcquireEvent(void * opaque, uint64_t dataset, + uint64_t acquisition) +{ + EventTrace * trace = opaque; + trace->fileDataset = dataset; + trace->fileAcquisition = acquisition; + atomic_fetch_add_explicit(&trace->fileAcquire, 1, memory_order_release); +} + +static void fileAcquiredEvent(void * opaque, uint64_t dataset, + uint64_t acquisition, LG_ClipboardFileError error) +{ + EventTrace * trace = opaque; + trace->fileDataset = dataset; + trace->fileAcquisition = acquisition; + trace->fileError = error; + atomic_fetch_add_explicit(&trace->fileAcquired, 1, memory_order_release); +} + +static void fileRequestEvent(void * opaque, + const LG_ClipboardFileRequest * request) +{ + EventTrace * trace = opaque; + trace->fileRequestValue = *request; + atomic_fetch_add_explicit(&trace->fileRequest, 1, memory_order_release); +} + +static void fileCancelEvent(void * opaque, uint64_t dataset, + uint64_t request, LG_ClipboardFileError error) +{ + EventTrace * trace = opaque; + trace->fileDataset = dataset; + trace->fileAcquisition = request; + trace->fileError = error; + atomic_fetch_add_explicit(&trace->fileCancel, 1, memory_order_release); } static const LG_ClipboardEventOps EVENT_OPS = @@ -216,6 +275,11 @@ static const LG_ClipboardEventOps EVENT_OPS = .requestCancel = requestCancelEvent, .release = releaseNotice, .request = request, + .fileOffer = fileOfferEvent, + .fileAcquire = fileAcquireEvent, + .fileAcquired = fileAcquiredEvent, + .fileRequest = fileRequestEvent, + .fileCancel = fileCancelEvent, }; static void statusChanged(void * opaque, const LG_ClipboardStatus * status) @@ -402,6 +466,18 @@ static bool readType(TestState * state, KVMFRClipboardMessageType type, return false; } +static bool readNonKeepalive(TestState * state, + KVMFRClipboardMessage * record) +{ + for (unsigned i = 0; i < 32; ++i) + { + CHECK(readClient(state, record)); + if (record->type != KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE) + return true; + } + return false; +} + static bool noClientData(TestState * state) { for (unsigned i = 0; i < TEST_QUIET_MS; ++i) @@ -508,6 +584,7 @@ static bool testClaim(TestState * state) CHECK(state->ops->notifyTypes(state->clipboard, formats, 1)); KVMFRClipboardMessage offer; CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &offer)); + state->localClipboardGeneration = offer.clipboardGeneration; CHECK(offer.generation == claimRecord.generation); CHECK(offer.clipboardGeneration != 0); CHECK(offer.token == KVMFR_CLIPBOARD_FORMAT_MASK_TEXT); @@ -595,6 +672,7 @@ static bool prepareOutbound(TestState * state, uint32_t endpoint, CHECK(state->ops->notifyTypes(state->clipboard, formats, 1)); KVMFRClipboardMessage offer; CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &offer)); + state->localClipboardGeneration = offer.clipboardGeneration; KVMFRClipboardMessage requestRecord = hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST, @@ -634,6 +712,30 @@ static bool checkCommit(TestState * state, uint32_t token, return true; } +static bool checkFileCommit(TestState * state, uint32_t grantToken, + KVMFRClipboardMessage * commit, KVMFRClipboardMessage * slot, + const void ** data) +{ + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_COMMIT, commit)); + CHECK(commit->token == grantToken); + *slot = *(KVMFRClipboardMessage *) + lgmpHostMemPtr(state->grantMemory[grantToken - 1]); + CHECK(slot->type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA); + CHECK(slot->generation == commit->generation); + CHECK(slot->clipboardGeneration == commit->clipboardGeneration); + CHECK(slot->transfer == commit->transfer); + CHECK(slot->offset == commit->offset); + CHECK(slot->size == commit->size); + CHECK(slot->format == commit->format); + CHECK(slot->flags == commit->flags); + CHECK(slot->length == commit->length); + CHECK(slot->sequence == commit->sequence); + CHECK(slot->token == KVMFR_CLIPBOARD_FILE_OP_READ); + *data = (const uint8_t *)lgmpHostMemPtr( + state->grantMemory[grantToken - 1]) + sizeof(*slot); + return true; +} + static bool testOutboundGrant(TestState * state) { KVMFRClipboardMessage claimRecord; @@ -746,10 +848,10 @@ static bool testCancel(TestState * state) const LG_ClipboardRequest outbound = state->events.requestID; KVMFRClipboardMessage cancel = hostRecord(KVMFR_CLIPBOARD_MESSAGE_CANCEL, claimRecord.generation); - cancel.clipboardGeneration = 1; - cancel.transfer = outbound; - cancel.format = KVMFR_CLIPBOARD_FORMAT_TEXT; - cancel.token = LG_CLIPBOARD_CANCEL_ABORTED; + cancel.clipboardGeneration = state->localClipboardGeneration; + cancel.transfer = outbound; + cancel.format = KVMFR_CLIPBOARD_FORMAT_TEXT; + cancel.token = LG_CLIPBOARD_CANCEL_ABORTED; CHECK(postRecord(state, &cancel, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); CHECK(waitAtomic(state, &state->events.requestCancel, 1)); CHECK(state->events.cancelID == outbound); @@ -768,7 +870,7 @@ static bool testCancel(TestState * state) KVMFRClipboardMessage outboundRequest = hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST, claimRecord.generation); - outboundRequest.clipboardGeneration = 1; + outboundRequest.clipboardGeneration = state->localClipboardGeneration; outboundRequest.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | 5; outboundRequest.format = KVMFR_CLIPBOARD_FORMAT_TEXT; CHECK(postRecord(state, &outboundRequest, @@ -779,7 +881,8 @@ static bool testCancel(TestState * state) KVMFRClipboardMessage wireCancel; CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CANCEL, &wireCancel)); CHECK(wireCancel.transfer == outboundRequest.transfer); - CHECK(wireCancel.clipboardGeneration == 1); + CHECK(wireCancel.clipboardGeneration == + state->localClipboardGeneration); CHECK(wireCancel.format == KVMFR_CLIPBOARD_FORMAT_TEXT); CHECK(wireCancel.token == LG_CLIPBOARD_CANCEL_ABORTED); return true; @@ -814,6 +917,381 @@ static bool testRestart(TestState * state) return true; } +static bool testFileLimits(TestState * state) +{ + KVMFRClipboardMessage claimRecord; + CHECK(claim(state, 75, false, &claimRecord)); + CHECK(own(state, 75, claimRecord.generation)); + + const uint64_t localDataset = UINT64_C(0x1234000012340001); + const uint64_t remoteDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER | + UINT64_C(0x5678000056780001); + CHECK(state->ops->offerFiles(state->clipboard, localDataset)); + KVMFRClipboardMessage wire; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &wire)); + CHECK(wire.clipboardGeneration == localDataset); + + KVMFRClipboardMessage offer = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation); + offer.clipboardGeneration = remoteDataset; + offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES; + CHECK(postRecord(state, &offer, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.notice, 1)); + CHECK(atomic_load(&state->events.fileOffer) == 1); + CHECK(atomic_load(&state->events.fileOfferBeforeNotice)); + CHECK(state->events.fileDataset == remoteDataset); + + KVMFRClipboardMessage staleAcquire = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, + claimRecord.generation); + staleAcquire.clipboardGeneration = localDataset + 1U; + staleAcquire.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | + UINT64_C(0x98); + staleAcquire.format = KVMFR_CLIPBOARD_FORMAT_FILES; + CHECK(postRecord(state, &staleAcquire, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, &wire)); + CHECK(wire.clipboardGeneration == staleAcquire.clipboardGeneration); + CHECK(wire.transfer == staleAcquire.transfer); + CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_STALE); + CHECK(atomic_load(&state->events.fileAcquire) == 0); + + KVMFRClipboardMessage staleRequest = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST, + claimRecord.generation); + staleRequest.clipboardGeneration = localDataset; + staleRequest.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | + UINT64_C(0x99); + staleRequest.format = KVMFR_CLIPBOARD_FORMAT_FILES; + staleRequest.token = KVMFR_CLIPBOARD_FILE_OP_LIST; + CHECK(postRecord(state, &staleRequest, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL, &wire)); + CHECK(wire.clipboardGeneration == staleRequest.clipboardGeneration); + CHECK(wire.transfer == staleRequest.transfer); + CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_STALE); + CHECK(atomic_load(&state->events.fileRequest) == 0); + + for (uint64_t i = 0; i < 4; ++i) + { + const uint64_t acquisition = UINT64_C(0x100) + i; + CHECK(state->ops->fileAcquire( + state->clipboard, remoteDataset, acquisition)); + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &wire)); + CHECK(wire.clipboardGeneration == remoteDataset); + CHECK(wire.transfer == acquisition); + + KVMFRClipboardMessage acquired = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, + claimRecord.generation); + acquired.clipboardGeneration = remoteDataset; + acquired.transfer = acquisition; + acquired.format = KVMFR_CLIPBOARD_FORMAT_FILES; + CHECK(postRecord(state, &acquired, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.fileAcquired, (unsigned)i + 1)); + CHECK(state->events.fileError == LG_CLIPBOARD_FILE_ERROR_NONE); + } + + for (uint64_t i = 0; i < 4; ++i) + { + const uint64_t acquisition = KVMFR_CLIPBOARD_TRANSFER_HELPER | + (UINT64_C(0x200) + i); + KVMFRClipboardMessage acquire = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, + claimRecord.generation); + acquire.clipboardGeneration = localDataset; + acquire.transfer = acquisition; + acquire.format = KVMFR_CLIPBOARD_FORMAT_FILES; + CHECK(postRecord(state, &acquire, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.fileAcquire, (unsigned)i + 1)); + CHECK(state->events.fileDataset == localDataset); + CHECK(state->events.fileAcquisition == acquisition); + CHECK(state->ops->fileAcquired(state->clipboard, + localDataset, acquisition, LG_CLIPBOARD_FILE_ERROR_NONE)); + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, &wire)); + CHECK(wire.transfer == acquisition); + CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_NONE); + } + + CHECK(!state->ops->fileAcquire( + state->clipboard, remoteDataset, UINT64_C(0x999))); + KVMFRClipboardMessage overflowAcquire = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, + claimRecord.generation); + overflowAcquire.clipboardGeneration = localDataset; + overflowAcquire.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | + UINT64_C(0x999); + overflowAcquire.format = KVMFR_CLIPBOARD_FORMAT_FILES; + CHECK(postRecord(state, &overflowAcquire, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(readNonKeepalive(state, &wire)); + CHECK(wire.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED); + CHECK(wire.transfer == overflowAcquire.transfer); + CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE); + CHECK(atomic_load(&state->events.fileAcquire) == 4); + + for (uint64_t i = 0; i < 16; ++i) + { + const LG_ClipboardFileRequest request = + { + .dataset = remoteDataset, + .request = UINT64_C(0x1000) + i, + .operation = LG_CLIPBOARD_FILE_LIST, + }; + CHECK(state->ops->fileRequest(state->clipboard, &request)); + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST, &wire)); + CHECK(wire.clipboardGeneration == remoteDataset); + CHECK(wire.transfer == request.request); + CHECK(wire.token == KVMFR_CLIPBOARD_FILE_OP_LIST); + } + + for (uint64_t i = 0; i < 16; ++i) + { + KVMFRClipboardMessage request = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST, + claimRecord.generation); + request.clipboardGeneration = localDataset; + request.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | + (UINT64_C(0x2000) + i); + request.format = KVMFR_CLIPBOARD_FORMAT_FILES; + request.token = KVMFR_CLIPBOARD_FILE_OP_LIST; + CHECK(postRecord(state, &request, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.fileRequest, + (unsigned)i + 1)); + CHECK(state->events.fileRequestValue.dataset == localDataset); + CHECK(state->events.fileRequestValue.request == request.transfer); + } + + const LG_ClipboardFileRequest overflowRequest = + { + .dataset = remoteDataset, + .request = UINT64_C(0x9999), + .operation = LG_CLIPBOARD_FILE_LIST, + }; + CHECK(!state->ops->fileRequest(state->clipboard, &overflowRequest)); + KVMFRClipboardMessage inboundOverflow = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST, + claimRecord.generation); + inboundOverflow.clipboardGeneration = localDataset; + inboundOverflow.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | + UINT64_C(0x9999); + inboundOverflow.format = KVMFR_CLIPBOARD_FORMAT_FILES; + inboundOverflow.token = KVMFR_CLIPBOARD_FILE_OP_LIST; + CHECK(postRecord(state, &inboundOverflow, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(readNonKeepalive(state, &wire)); + CHECK(wire.type == KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL); + CHECK(wire.transfer == inboundOverflow.transfer); + CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE); + CHECK(atomic_load(&state->events.fileRequest) == 16); + return true; +} + +static bool testFileOwnerLoss(TestState * state) +{ + KVMFRClipboardMessage claimRecord; + CHECK(claim(state, 79, false, &claimRecord)); + CHECK(own(state, 79, claimRecord.generation)); + + const uint64_t dataset = KVMFR_CLIPBOARD_TRANSFER_HELPER | + UINT64_C(0x7900000179000001); + KVMFRClipboardMessage offer = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation); + offer.clipboardGeneration = dataset; + offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES; + CHECK(postRecord(state, &offer, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.fileOffer, 1)); + + const uint64_t acquisition = UINT64_C(0x7901); + CHECK(state->ops->fileAcquire( + state->clipboard, dataset, acquisition)); + KVMFRClipboardMessage wire; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &wire)); + + KVMFRClipboardMessage acquired = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, + claimRecord.generation); + acquired.clipboardGeneration = dataset; + acquired.transfer = acquisition; + acquired.format = KVMFR_CLIPBOARD_FORMAT_FILES; + CHECK(postRecord(state, &acquired, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.fileAcquired, 1)); + + const LG_ClipboardFileRequest request = + { + .dataset = dataset, + .request = UINT64_C(0x7902), + .operation = LG_CLIPBOARD_FILE_LIST, + }; + CHECK(state->ops->fileRequest(state->clipboard, &request)); + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST, &wire)); + + const unsigned cancellations = + atomic_load(&state->events.fileCancel); + CHECK(postStatus(state, 80, 0, 0, true, 0)); + CHECK(waitAtomic(state, &state->events.fileCancel, + cancellations + 2)); + CHECK(state->events.fileDataset == dataset); + CHECK(state->events.fileAcquisition == acquisition); + CHECK(state->events.fileError == LG_CLIPBOARD_FILE_ERROR_DISCONNECTED); + return true; +} + +static bool testMalformedFileData(TestState * state) +{ + KVMFRClipboardMessage claimRecord; + CHECK(claim(state, 81, false, &claimRecord)); + CHECK(own(state, 81, claimRecord.generation)); + + const uint64_t dataset = KVMFR_CLIPBOARD_TRANSFER_HELPER | + UINT64_C(0x8100000181000001); + KVMFRClipboardMessage offer = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation); + offer.clipboardGeneration = dataset; + offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES; + CHECK(postRecord(state, &offer, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.fileOffer, 1)); + + const uint64_t acquisition = UINT64_C(0x8101); + CHECK(state->ops->fileAcquire(state->clipboard, dataset, acquisition)); + KVMFRClipboardMessage wire; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &wire)); + + KVMFRClipboardMessage acquired = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, + claimRecord.generation); + acquired.clipboardGeneration = dataset; + acquired.transfer = acquisition; + acquired.format = KVMFR_CLIPBOARD_FORMAT_FILES; + CHECK(postRecord(state, &acquired, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.fileAcquired, 1)); + + const LG_ClipboardFileRequest request = + { + .dataset = dataset, + .request = UINT64_C(0x8102), + .node = 2, + .length = 4, + .operation = LG_CLIPBOARD_FILE_READ, + }; + CHECK(state->ops->fileRequest(state->clipboard, &request)); + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST, &wire)); + + KVMFRClipboardMessage malformed = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_DATA, + claimRecord.generation); + malformed.clipboardGeneration = dataset; + malformed.transfer = request.request; + malformed.offset = 1; + malformed.size = request.length; + malformed.format = KVMFR_CLIPBOARD_FORMAT_FILES; + malformed.flags = KVMFR_CLIPBOARD_FLAG_BEGIN; + malformed.length = 1; + malformed.token = KVMFR_CLIPBOARD_FILE_OP_READ; + const uint8_t byte = 0; + const unsigned cancellations = + atomic_load(&state->events.fileCancel); + CHECK(postRecord(state, &malformed, + KVMFR_CLIPBOARD_QUEUE_DATA, &byte)); + CHECK(waitAtomic(state, &state->events.fileCancel, cancellations + 1)); + CHECK(state->events.fileDataset == dataset); + CHECK(state->events.fileAcquisition == request.request); + CHECK(state->events.fileError == LG_CLIPBOARD_FILE_ERROR_INVALID); + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL, &wire)); + CHECK(wire.clipboardGeneration == dataset); + CHECK(wire.transfer == request.request); + CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_INVALID); + CHECK(!state->ops->fileCancel(state->clipboard, + dataset, request.request, LG_CLIPBOARD_FILE_ERROR_CANCELLED)); + + malformed.transfer = request.request + 1U; + CHECK(postRecord(state, &malformed, + KVMFR_CLIPBOARD_QUEUE_DATA, &byte)); + CHECK(waitMemory(state, state->memories[state->memoryCount - 1])); + CHECK(atomic_load(&state->events.fileCancel) == cancellations + 1); + return true; +} + +static bool testFileStream(TestState * state) +{ + KVMFRClipboardMessage claimRecord; + CHECK(claim(state, 78, false, &claimRecord)); + CHECK(own(state, 78, claimRecord.generation)); + const uint64_t dataset = UINT64_C(0x7800000178000001); + CHECK(state->ops->offerFiles(state->clipboard, dataset)); + KVMFRClipboardMessage wire; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &wire)); + + const uint64_t acquisition = KVMFR_CLIPBOARD_TRANSFER_HELPER | + UINT64_C(0x7801); + KVMFRClipboardMessage acquire = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, + claimRecord.generation); + acquire.clipboardGeneration = dataset; + acquire.transfer = acquisition; + acquire.format = KVMFR_CLIPBOARD_FORMAT_FILES; + CHECK(postRecord(state, &acquire, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.fileAcquire, 1)); + CHECK(state->ops->fileAcquired(state->clipboard, + dataset, acquisition, LG_CLIPBOARD_FILE_ERROR_NONE)); + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, &wire)); + + KVMFRClipboardMessage request = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST, + claimRecord.generation); + request.clipboardGeneration = dataset; + request.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | + UINT64_C(0x7802); + request.offset = 123; + request.size = 42; + request.format = KVMFR_CLIPBOARD_FORMAT_FILES; + request.flags = 3; + request.token = KVMFR_CLIPBOARD_FILE_OP_READ; + CHECK(postRecord(state, &request, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.fileRequest, 1)); + const LG_ClipboardFileRequest descriptor = + state->events.fileRequestValue; + CHECK(descriptor.dataset == dataset); + CHECK(descriptor.request == request.transfer); + CHECK(descriptor.node == 42); + CHECK(descriptor.offset == 123); + CHECK(descriptor.length == 3); + CHECK(descriptor.operation == LG_CLIPBOARD_FILE_READ); + CHECK(state->ops->fileDataBegin(state->clipboard, + &descriptor, 3) == LG_CLIPBOARD_RESULT_ACCEPTED); + + CHECK(postGrant(state, claimRecord.generation, 1)); + CHECK(waitMemory(state, state->grantMemory[0])); + static const uint8_t bytes[] = { 9, 8, 7 }; + CHECK(state->ops->fileDataChunk(state->clipboard, + &descriptor, 0, bytes, sizeof(bytes)) == + LG_CLIPBOARD_RESULT_ACCEPTED); + KVMFRClipboardMessage commit; + KVMFRClipboardMessage slot; + const void * data; + CHECK(checkFileCommit(state, 1, &commit, &slot, &data)); + CHECK(slot.flags == KVMFR_CLIPBOARD_FLAG_BEGIN); + CHECK(slot.offset == 0 && slot.sequence == 0 && slot.size == 3); + CHECK(memcmp(data, bytes, sizeof(bytes)) == 0); + + CHECK(postGrant(state, claimRecord.generation, 1)); + CHECK(waitMemory(state, state->grantMemory[0])); + CHECK(state->ops->fileDataEnd(state->clipboard, + &descriptor, 3) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(checkFileCommit(state, 1, &commit, &slot, &data)); + CHECK(slot.flags == KVMFR_CLIPBOARD_FLAG_END); + CHECK(slot.offset == 3 && slot.sequence == 1 && slot.size == 3); + CHECK(slot.length == 0); + return true; +} + static void * disconnectThread(void * opaque) { DisconnectTask * task = opaque; @@ -863,6 +1341,165 @@ static bool disconnectAndDrain(TestState * state, return valid; } +static bool recreateClipboard(TestState * state) +{ + state->ops->setStatusListener(state->clipboard, NULL, NULL); + if (atomic_load(&state->events.attached)) + { + state->ops->detach(state->clipboard); + atomic_store(&state->events.attached, false); + } + CHECK(disconnectAndDrain(state, NULL)); + lgmpClipboard_destroy(&state->clipboard); + + CHECK(lgmpClipboard_create(state->client, &state->clipboard)); + CHECK(lgmpClipboard_connect(state->clipboard, state->clientID)); + state->ops = lgmpClipboard_getOps(); + CHECK(state->ops); + state->ops->setStatusListener(state->clipboard, statusChanged, state); + for (unsigned i = 0; i < TEST_WAIT_MS; ++i) + { + CHECK(hostProcess(state)); + if (lgmpHostQueueHasSubs(state->queue)) + return true; + usleep(1000); + } + return false; +} + +static bool testProcessRestart(TestState * state) +{ + KVMFRClipboardMessage oldClaim; + CHECK(claim(state, 76, false, &oldClaim)); + CHECK(own(state, 76, oldClaim.generation)); + const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT }; + CHECK(state->ops->notifyTypes(state->clipboard, formats, 1)); + KVMFRClipboardMessage oldLocalOffer; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &oldLocalOffer)); + KVMFRClipboardMessage oldRequest; + CHECK(offerAndRequest(state, oldClaim.generation, + 760, 7600, &oldRequest)); + const uint64_t oldFileDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER | + UINT64_C(0x76000001); + const uint64_t oldFileAcquisition = UINT64_C(0x76010001); + KVMFRClipboardMessage fileOffer = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, oldClaim.generation); + fileOffer.clipboardGeneration = oldFileDataset; + fileOffer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES; + const unsigned oldFileOffers = atomic_load(&state->events.fileOffer); + CHECK(postRecord(state, &fileOffer, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.fileOffer, oldFileOffers + 1)); + CHECK(state->ops->fileAcquire(state->clipboard, + oldFileDataset, oldFileAcquisition)); + KVMFRClipboardMessage fileWire; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &fileWire)); + + CHECK(recreateClipboard(state)); + KVMFRClipboardMessage newClaim; + CHECK(claim(state, 77, false, &newClaim)); + CHECK(own(state, 77, newClaim.generation)); + CHECK(state->ops->notifyTypes(state->clipboard, formats, 1)); + KVMFRClipboardMessage newLocalOffer; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &newLocalOffer)); + CHECK(newLocalOffer.clipboardGeneration != + oldLocalOffer.clipboardGeneration); + KVMFRClipboardMessage newRequest; + CHECK(offerAndRequest(state, newClaim.generation, + 770, 7700, &newRequest)); + CHECK(newRequest.transfer != oldRequest.transfer); + + const unsigned cancelCount = atomic_load(&state->events.dataCancel); + KVMFRClipboardMessage staleCancel = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_CANCEL, newClaim.generation); + staleCancel.clipboardGeneration = 760; + staleCancel.transfer = oldRequest.transfer; + staleCancel.format = KVMFR_CLIPBOARD_FORMAT_TEXT; + staleCancel.token = LG_CLIPBOARD_CANCEL_ABORTED; + CHECK(postRecord(state, &staleCancel, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitMemory(state, state->memories[state->memoryCount - 1])); + CHECK(atomic_load(&state->events.dataCancel) == cancelCount); + + const unsigned beginCount = atomic_load(&state->events.dataBegin); + KVMFRClipboardMessage staleData = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, newClaim.generation); + staleData.clipboardGeneration = 760; + staleData.transfer = oldRequest.transfer; + staleData.format = KVMFR_CLIPBOARD_FORMAT_TEXT; + staleData.flags = KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END; + CHECK(postRecord(state, &staleData, KVMFR_CLIPBOARD_QUEUE_DATA, NULL)); + CHECK(waitMemory(state, state->memories[state->memoryCount - 1])); + CHECK(atomic_load(&state->events.dataBegin) == beginCount); + + KVMFRClipboardMessage data = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, newClaim.generation); + data.clipboardGeneration = 770; + data.transfer = newRequest.transfer; + data.format = KVMFR_CLIPBOARD_FORMAT_TEXT; + data.flags = KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END; + const unsigned endCount = atomic_load(&state->events.dataEnd); + CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, NULL)); + CHECK(waitAtomic(state, &state->events.dataEnd, endCount + 1)); + CHECK(atomic_load(&state->events.dataBegin) == beginCount + 1); + + const uint64_t newFileDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER | + UINT64_C(0x77000001); + const uint64_t newFileAcquisition = UINT64_C(0x77010001); + fileOffer = hostRecord( + KVMFR_CLIPBOARD_MESSAGE_OFFER, newClaim.generation); + fileOffer.clipboardGeneration = newFileDataset; + fileOffer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES; + const unsigned newFileOffers = atomic_load(&state->events.fileOffer); + CHECK(postRecord(state, &fileOffer, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.fileOffer, newFileOffers + 1)); + CHECK(state->ops->fileAcquire(state->clipboard, + newFileDataset, newFileAcquisition)); + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &fileWire)); + + const unsigned acquiredCount = + atomic_load(&state->events.fileAcquired); + KVMFRClipboardMessage staleAcquired = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, + newClaim.generation); + staleAcquired.clipboardGeneration = oldFileDataset; + staleAcquired.transfer = oldFileAcquisition; + staleAcquired.format = KVMFR_CLIPBOARD_FORMAT_FILES; + CHECK(postRecord(state, &staleAcquired, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitMemory(state, state->memories[state->memoryCount - 1])); + CHECK(atomic_load(&state->events.fileAcquired) == acquiredCount); + + KVMFRClipboardMessage staleFileCancel = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL, + newClaim.generation); + staleFileCancel.clipboardGeneration = oldFileDataset; + staleFileCancel.transfer = oldFileAcquisition; + staleFileCancel.format = KVMFR_CLIPBOARD_FORMAT_FILES; + staleFileCancel.token = KVMFR_CLIPBOARD_FILE_ERROR_STALE; + CHECK(postRecord(state, &staleFileCancel, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitMemory(state, state->memories[state->memoryCount - 1])); + CHECK(atomic_load(&state->events.fileCancel) == 0); + + KVMFRClipboardMessage acquired = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, + newClaim.generation); + acquired.clipboardGeneration = newFileDataset; + acquired.transfer = newFileAcquisition; + acquired.format = KVMFR_CLIPBOARD_FORMAT_FILES; + CHECK(postRecord(state, &acquired, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.fileAcquired, + acquiredCount + 1)); + CHECK(state->events.fileDataset == newFileDataset); + CHECK(state->events.fileAcquisition == newFileAcquisition); + return true; +} + static bool testDisconnect(TestState * state) { KVMFRClipboardMessage claimRecord; @@ -944,6 +1581,59 @@ static bool testMalformed(TestState * state) return true; } +static bool testFileFormatIsolation(TestState * state) +{ + KVMFRClipboardMessage claimRecord; + CHECK(claim(state, 91, false, &claimRecord)); + CHECK(own(state, 91, claimRecord.generation)); + + const LG_ClipboardData files[] = { LG_CLIPBOARD_DATA_FILES }; + CHECK(!state->ops->notifyTypes(state->clipboard, files, 1)); + CHECK(noClientData(state)); + + const uint64_t localDataset = UINT64_C(0x123456789abcdef); + CHECK(state->ops->offerFiles(state->clipboard, localDataset)); + KVMFRClipboardMessage localOffer; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &localOffer)); + CHECK(localOffer.clipboardGeneration == localDataset); + + KVMFRClipboardMessage request = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST, claimRecord.generation); + request.clipboardGeneration = localDataset; + request.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(1); + request.format = KVMFR_CLIPBOARD_FORMAT_FILES; + CHECK(postRecord(state, &request, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitMemory(state, state->memories[state->memoryCount - 1])); + CHECK(atomic_load(&state->events.request) == 0); + CHECK(state->ops->dataBegin(state->clipboard, request.transfer, + LG_CLIPBOARD_DATA_FILES, 0) == LG_CLIPBOARD_RESULT_FAILED); + + KVMFRClipboardMessage remoteOffer = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation); + remoteOffer.clipboardGeneration = UINT64_C(0x2222333344445555); + remoteOffer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES; + CHECK(postRecord(state, &remoteOffer, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.notice, 1)); + CHECK(state->events.noticeCount == 1); + CHECK(state->events.noticeTypes[0] == LG_CLIPBOARD_DATA_FILES); + CHECK(!state->ops->request(state->clipboard, + 100, LG_CLIPBOARD_DATA_FILES)); + CHECK(noClientData(state)); + + KVMFRClipboardMessage data = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation); + data.clipboardGeneration = remoteOffer.clipboardGeneration; + data.transfer = UINT64_C(0x2345); + data.format = KVMFR_CLIPBOARD_FORMAT_FILES; + data.flags = KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END; + CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, NULL)); + CHECK(waitMemory(state, state->memories[state->memoryCount - 1])); + CHECK(atomic_load(&state->events.dataBegin) == 0); + return true; +} + static bool stateInit(TestState * state) { memset(state, 0, sizeof(*state)); @@ -961,9 +1651,15 @@ static bool stateInit(TestState * state) atomic_init(&state->events.dataEnd, 0); atomic_init(&state->events.dataCancel, 0); atomic_init(&state->events.dataReady, 0); + atomic_init(&state->events.fileOffer, 0); + atomic_init(&state->events.fileAcquire, 0); + atomic_init(&state->events.fileAcquired, 0); + atomic_init(&state->events.fileRequest, 0); + atomic_init(&state->events.fileCancel, 0); atomic_init(&state->events.acceptRequest, true); atomic_init(&state->events.blockChunkOnce, false); atomic_init(&state->events.attached, false); + atomic_init(&state->events.fileOfferBeforeNotice, false); state->memory = mmap(NULL, TEST_SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); @@ -1037,15 +1733,21 @@ static const struct } TESTS[] = { - { "claim" , testClaim }, - { "offer-request" , testOfferRequest }, - { "inbound-stream", testInboundStream }, - { "outbound-grant", testOutboundGrant }, - { "blocked" , testBlocked }, - { "cancel" , testCancel }, - { "restart" , testRestart }, - { "disconnect" , testDisconnect }, - { "malformed" , testMalformed }, + { "claim" , testClaim }, + { "offer-request" , testOfferRequest }, + { "inbound-stream" , testInboundStream }, + { "outbound-grant" , testOutboundGrant }, + { "blocked" , testBlocked }, + { "cancel" , testCancel }, + { "restart" , testRestart }, + { "process-restart" , testProcessRestart }, + { "file-stream" , testFileStream }, + { "file-limits" , testFileLimits }, + { "file-owner-loss" , testFileOwnerLoss }, + { "file-malformed" , testMalformedFileData }, + { "disconnect" , testDisconnect }, + { "malformed" , testMalformed }, + { "file-format" , testFileFormatIsolation }, }; int main(int argc, char * argv[]) diff --git a/client/transports/LGMP/clipboard.c b/client/transports/LGMP/clipboard.c index 9bc19867..90fda515 100644 --- a/client/transports/LGMP/clipboard.c +++ b/client/transports/LGMP/clipboard.c @@ -28,12 +28,15 @@ #include "common/thread.h" #include "common/time.h" +#include #include #include #include #include +#include -#define CLIPBOARD_PENDING_MAX 32U +#define CLIPBOARD_PENDING_MAX 128U +#define CLIPBOARD_PENDING_NORMAL_MAX 64U #define CLIPBOARD_GRANTS KVMFR_CLIPBOARD_SLOT_COUNT #define CLIPBOARD_POLL_MS 10U #define CLIPBOARD_RETRY_MS 1U @@ -53,6 +56,26 @@ struct Grant bool available; }; +struct FileTransfer +{ + struct FileTransfer * next; + LG_ClipboardFileRequest request; + uint64_t sizeHint; + uint64_t responseOffset; + uint32_t sequence; + bool began; + bool blocked; +}; + +struct FileAcquisition +{ + struct FileAcquisition * next; + uint64_t dataset; + uint64_t acquisition; + bool fromHelper; + bool active; +}; + enum HeldPhase { HELD_PHASE_NONE, @@ -63,51 +86,54 @@ enum HeldPhase struct LGMPClipboard { - PLGMPClient client; - PLGMPClientQueue queue; - LG_Lock lock; - LG_Lock eventLock; - LG_Lock statusLock; - LGEvent * event; - LGThread * thread; - atomic_bool stop; + PLGMPClient client; + PLGMPClientQueue queue; + LG_Lock lock; + LG_Lock eventLock; + LG_Lock statusLock; + LGEvent * event; + LGThread * thread; + atomic_bool stop; - bool connected; - bool available; - bool statusValid; - bool claimed; - bool ownerConfirmed; - uint32_t clientID; - uint32_t endpointGeneration; - uint32_t providerGeneration; - uint32_t claimGeneration; - uint32_t publishedClaimGeneration; - uint32_t statusSerial; - uint64_t lastSend; + bool connected; + bool available; + bool statusValid; + bool claimed; + bool ownerConfirmed; + uint32_t clientID; + uint32_t endpointGeneration; + uint32_t providerGeneration; + uint32_t claimGeneration; + uint32_t publishedClaimGeneration; + uint32_t statusSerial; + uint64_t lastSend; - struct PendingRecord pending[CLIPBOARD_PENDING_MAX]; - unsigned pendingHead; - unsigned pendingCount; + struct PendingRecord pending[CLIPBOARD_PENDING_MAX]; + unsigned pendingHead; + unsigned pendingCount; - struct Grant grants[CLIPBOARD_GRANTS]; - bool writeBlocked; - LG_ClipboardRequest writeBlockedRequest; - LG_ClipboardRequest writeTransfer; - uint64_t writeClipboardGeneration; - uint64_t writeSizeHint; - KVMFRClipboardFormat writeFormat; - uint64_t writeOffset; - uint32_t writeSequence; - bool writeBegan; - bool writeWireBegan; + struct Grant grants[CLIPBOARD_GRANTS]; + bool writeBlocked; + LG_ClipboardRequest writeBlockedRequest; + LG_ClipboardRequest writeTransfer; + uint64_t writeClipboardGeneration; + uint64_t writeSizeHint; + KVMFRClipboardFormat writeFormat; + uint64_t writeOffset; + uint32_t writeSequence; + bool writeBegan; + bool writeWireBegan; - bool held; - bool heldReady; - enum HeldPhase heldPhase; - LGMPMessage heldMessage; - KVMFRClipboardMessage heldRecord; + bool held; + bool heldReady; + enum HeldPhase heldPhase; + LGMPMessage heldMessage; + KVMFRClipboardMessage heldRecord; + bool heldFile; + LG_ClipboardFileRequest heldFileRequest; uint64_t localClipboardGeneration; + uint64_t localGenerationSerial; KVMFRClipboardFormatFlags localFormats; uint64_t remoteClipboardGeneration; KVMFRClipboardFormatFlags remoteFormats; @@ -121,6 +147,13 @@ struct LGMPClipboard bool readBegan; uint64_t transferSerial; + struct FileTransfer * fileReads; + struct FileTransfer * fileWrites; + struct FileAcquisition * fileAcquisitions; + struct FileTransfer * retiredFileReads; + struct FileTransfer * retiredFileWrites; + struct FileAcquisition * retiredFileAcquisitions; + const LG_ClipboardEventOps * events; void * eventOpaque; LG_ClipboardStatusFn statusCallback; @@ -131,11 +164,12 @@ static LG_ClipboardData fromWireFormat(KVMFRClipboardFormat format) { switch (format) { - case KVMFR_CLIPBOARD_FORMAT_TEXT: return LG_CLIPBOARD_DATA_TEXT; - case KVMFR_CLIPBOARD_FORMAT_PNG : return LG_CLIPBOARD_DATA_PNG; - case KVMFR_CLIPBOARD_FORMAT_BMP : return LG_CLIPBOARD_DATA_BMP; - case KVMFR_CLIPBOARD_FORMAT_TIFF: return LG_CLIPBOARD_DATA_TIFF; - case KVMFR_CLIPBOARD_FORMAT_JPEG: return LG_CLIPBOARD_DATA_JPEG; + case KVMFR_CLIPBOARD_FORMAT_TEXT : return LG_CLIPBOARD_DATA_TEXT; + case KVMFR_CLIPBOARD_FORMAT_PNG : return LG_CLIPBOARD_DATA_PNG; + case KVMFR_CLIPBOARD_FORMAT_BMP : return LG_CLIPBOARD_DATA_BMP; + case KVMFR_CLIPBOARD_FORMAT_TIFF : return LG_CLIPBOARD_DATA_TIFF; + case KVMFR_CLIPBOARD_FORMAT_JPEG : return LG_CLIPBOARD_DATA_JPEG; + case KVMFR_CLIPBOARD_FORMAT_FILES: return LG_CLIPBOARD_DATA_FILES; default: return LG_CLIPBOARD_DATA_NONE; } } @@ -144,15 +178,28 @@ static KVMFRClipboardFormat toWireFormat(LG_ClipboardData format) { switch (format) { - case LG_CLIPBOARD_DATA_TEXT: return KVMFR_CLIPBOARD_FORMAT_TEXT; - case LG_CLIPBOARD_DATA_PNG : return KVMFR_CLIPBOARD_FORMAT_PNG; - case LG_CLIPBOARD_DATA_BMP : return KVMFR_CLIPBOARD_FORMAT_BMP; - case LG_CLIPBOARD_DATA_TIFF: return KVMFR_CLIPBOARD_FORMAT_TIFF; - case LG_CLIPBOARD_DATA_JPEG: return KVMFR_CLIPBOARD_FORMAT_JPEG; + case LG_CLIPBOARD_DATA_TEXT : return KVMFR_CLIPBOARD_FORMAT_TEXT; + case LG_CLIPBOARD_DATA_PNG : return KVMFR_CLIPBOARD_FORMAT_PNG; + case LG_CLIPBOARD_DATA_BMP : return KVMFR_CLIPBOARD_FORMAT_BMP; + case LG_CLIPBOARD_DATA_TIFF : return KVMFR_CLIPBOARD_FORMAT_TIFF; + case LG_CLIPBOARD_DATA_JPEG : return KVMFR_CLIPBOARD_FORMAT_JPEG; + case LG_CLIPBOARD_DATA_FILES: return KVMFR_CLIPBOARD_FORMAT_FILES; default: return KVMFR_CLIPBOARD_FORMAT_NONE; } } +static LG_ClipboardFileOperation fromWireFileOperation(uint32_t operation) +{ + return operation == KVMFR_CLIPBOARD_FILE_OP_READ ? + LG_CLIPBOARD_FILE_READ : LG_CLIPBOARD_FILE_LIST; +} + +static uint32_t toWireFileOperation(LG_ClipboardFileOperation operation) +{ + return operation == LG_CLIPBOARD_FILE_READ ? + KVMFR_CLIPBOARD_FILE_OP_READ : KVMFR_CLIPBOARD_FILE_OP_LIST; +} + static LG_ClipboardCancelReason fromWireCancel(uint32_t reason) { return reason <= LG_CLIPBOARD_CANCEL_INVALID ? @@ -173,7 +220,7 @@ static unsigned formatsFromMask(KVMFRClipboardFormatFlags mask, { unsigned count = 0; for (KVMFRClipboardFormat format = KVMFR_CLIPBOARD_FORMAT_TEXT; - format <= KVMFR_CLIPBOARD_FORMAT_JPEG; ++format) + format <= KVMFR_CLIPBOARD_FORMAT_FILES; ++format) if (mask & kvmfrClipboardFormatFlag(format)) formats[count++] = fromWireFormat(format); return count; @@ -194,6 +241,239 @@ static uint64_t nextClientTransfer(LGMPClipboard * clipboard) return clipboard->transferSerial; } +static uint64_t nextClientGeneration(LGMPClipboard * clipboard) +{ + if (++clipboard->localGenerationSerial == 0 || + kvmfrClipboardTransferFromHelper( + clipboard->localGenerationSerial)) + clipboard->localGenerationSerial = 1; + return clipboard->localGenerationSerial; +} + +static bool randomBytes(void * buffer, size_t size) +{ + uint8_t * output = buffer; + while (size) + { + const ssize_t received = getrandom(output, size, 0); + if (received < 0 && errno == EINTR) + continue; + if (received <= 0) + { + if (!received) + errno = EIO; + return false; + } + output += (size_t)received; + size -= (size_t)received; + } + return true; +} + +static LG_ClipboardFileError fromWireFileError(uint32_t error) +{ + switch (error) + { + case KVMFR_CLIPBOARD_FILE_ERROR_NONE: + return LG_CLIPBOARD_FILE_ERROR_NONE; + case KVMFR_CLIPBOARD_FILE_ERROR_NOT_FOUND: + return LG_CLIPBOARD_FILE_ERROR_NOT_FOUND; + case KVMFR_CLIPBOARD_FILE_ERROR_ACCESS: + return LG_CLIPBOARD_FILE_ERROR_ACCESS; + case KVMFR_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY: + return LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY; + case KVMFR_CLIPBOARD_FILE_ERROR_IS_DIRECTORY: + return LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY; + case KVMFR_CLIPBOARD_FILE_ERROR_IO: + return LG_CLIPBOARD_FILE_ERROR_IO; + case KVMFR_CLIPBOARD_FILE_ERROR_INVALID: + return LG_CLIPBOARD_FILE_ERROR_INVALID; + case KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY: + return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + case KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE: + return LG_CLIPBOARD_FILE_ERROR_NO_SPACE; + case KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED: + return LG_CLIPBOARD_FILE_ERROR_DISCONNECTED; + case KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED: + return LG_CLIPBOARD_FILE_ERROR_CANCELLED; + case KVMFR_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED: + return LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED; + case KVMFR_CLIPBOARD_FILE_ERROR_STALE: + return LG_CLIPBOARD_FILE_ERROR_STALE; + default: + return LG_CLIPBOARD_FILE_ERROR_INVALID; + } +} + +static KVMFRClipboardFileError toWireFileError( + LG_ClipboardFileError error) +{ + switch (error) + { + case LG_CLIPBOARD_FILE_ERROR_NONE: + return KVMFR_CLIPBOARD_FILE_ERROR_NONE; + case LG_CLIPBOARD_FILE_ERROR_NOT_FOUND: + return KVMFR_CLIPBOARD_FILE_ERROR_NOT_FOUND; + case LG_CLIPBOARD_FILE_ERROR_ACCESS: + return KVMFR_CLIPBOARD_FILE_ERROR_ACCESS; + case LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY: + return KVMFR_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY; + case LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY: + return KVMFR_CLIPBOARD_FILE_ERROR_IS_DIRECTORY; + case LG_CLIPBOARD_FILE_ERROR_IO: + return KVMFR_CLIPBOARD_FILE_ERROR_IO; + case LG_CLIPBOARD_FILE_ERROR_INVALID: + return KVMFR_CLIPBOARD_FILE_ERROR_INVALID; + case LG_CLIPBOARD_FILE_ERROR_NO_MEMORY: + return KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + case LG_CLIPBOARD_FILE_ERROR_NO_SPACE: + return KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE; + case LG_CLIPBOARD_FILE_ERROR_DISCONNECTED: + return KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED; + case LG_CLIPBOARD_FILE_ERROR_CANCELLED: + return KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED; + case LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED: + return KVMFR_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED; + case LG_CLIPBOARD_FILE_ERROR_STALE: + return KVMFR_CLIPBOARD_FILE_ERROR_STALE; + } + return KVMFR_CLIPBOARD_FILE_ERROR_INVALID; +} + +static bool validFileError(LG_ClipboardFileError error) +{ + return error >= LG_CLIPBOARD_FILE_ERROR_NONE && + error <= LG_CLIPBOARD_FILE_ERROR_STALE; +} + +static struct FileTransfer * fileTransferFindNL( + struct FileTransfer * transfers, uint64_t request) +{ + for (; transfers; transfers = transfers->next) + if (transfers->request.request == request) + return transfers; + return NULL; +} + +static struct FileTransfer * fileTransferTakeNL( + struct FileTransfer ** transfers, uint64_t request) +{ + while (*transfers && (*transfers)->request.request != request) + transfers = &(*transfers)->next; + if (!*transfers) + return NULL; + struct FileTransfer * result = *transfers; + *transfers = result->next; + result->next = NULL; + return result; +} + +static struct FileAcquisition * fileAcquisitionFindNL( + LGMPClipboard * clipboard, uint64_t dataset, uint64_t acquisition, + bool fromHelper) +{ + for (struct FileAcquisition * item = clipboard->fileAcquisitions; + item; item = item->next) + if (item->dataset == dataset && item->acquisition == acquisition && + item->fromHelper == fromHelper) + return item; + return NULL; +} + +static bool fileDatasetAcquiredNL(LGMPClipboard * clipboard, + uint64_t dataset, bool fromHelper) +{ + for (struct FileAcquisition * item = clipboard->fileAcquisitions; + item; item = item->next) + if (item->dataset == dataset && item->fromHelper == fromHelper && + item->active) + return true; + return false; +} + +static struct FileAcquisition * fileAcquisitionTakeNL( + LGMPClipboard * clipboard, uint64_t dataset, uint64_t acquisition, + bool fromHelper) +{ + struct FileAcquisition ** link = &clipboard->fileAcquisitions; + while (*link && ((*link)->dataset != dataset || + (*link)->acquisition != acquisition || + (*link)->fromHelper != fromHelper)) + link = &(*link)->next; + if (!*link) + return NULL; + struct FileAcquisition * result = *link; + *link = result->next; + result->next = NULL; + return result; +} + +static unsigned fileAcquisitionCountNL(const LGMPClipboard * clipboard) +{ + unsigned count = 0; + for (const struct FileAcquisition * item = clipboard->fileAcquisitions; + item; item = item->next) + ++count; + return count; +} + +static unsigned fileTransferCountNL(const LGMPClipboard * clipboard) +{ + unsigned count = 0; + for (const struct FileTransfer * item = clipboard->fileReads; + item; item = item->next) + ++count; + for (const struct FileTransfer * item = clipboard->fileWrites; + item; item = item->next) + ++count; + return count; +} + +static void clearFilesNL(LGMPClipboard * clipboard) +{ + struct FileTransfer ** readTail = &clipboard->fileReads; + while (*readTail) + readTail = &(*readTail)->next; + *readTail = clipboard->retiredFileReads; + clipboard->retiredFileReads = clipboard->fileReads; + clipboard->fileReads = NULL; + + struct FileTransfer ** writeTail = &clipboard->fileWrites; + while (*writeTail) + writeTail = &(*writeTail)->next; + *writeTail = clipboard->retiredFileWrites; + clipboard->retiredFileWrites = clipboard->fileWrites; + clipboard->fileWrites = NULL; + + struct FileAcquisition ** acquisitionTail = + &clipboard->fileAcquisitions; + while (*acquisitionTail) + acquisitionTail = &(*acquisitionTail)->next; + *acquisitionTail = clipboard->retiredFileAcquisitions; + clipboard->retiredFileAcquisitions = clipboard->fileAcquisitions; + clipboard->fileAcquisitions = NULL; +} + +static void freeFileTransfers(struct FileTransfer * transfers) +{ + while (transfers) + { + struct FileTransfer * next = transfers->next; + free(transfers); + transfers = next; + } +} + +static void freeFileAcquisitions(struct FileAcquisition * acquisitions) +{ + while (acquisitions) + { + struct FileAcquisition * next = acquisitions->next; + free(acquisitions); + acquisitions = next; + } +} + static struct PendingRecord * pendingAt( LGMPClipboard * clipboard, unsigned position) { @@ -207,11 +487,11 @@ static void signalWorker(LGMPClipboard * clipboard) lgSignalEvent(clipboard->event); } -static bool enqueueRecordNL(LGMPClipboard * clipboard, - KVMFRClipboardMessage record) +static bool enqueueRecordLimitNL(LGMPClipboard * clipboard, + KVMFRClipboardMessage record, unsigned limit) { if (!clipboard->connected || !clipboard->queue || - clipboard->pendingCount == CLIPBOARD_PENDING_MAX) + clipboard->pendingCount >= limit) return false; record.version = KVMFR_CLIPBOARD_VERSION; @@ -220,6 +500,33 @@ static bool enqueueRecordNL(LGMPClipboard * clipboard, return true; } +static bool enqueueRecordNL(LGMPClipboard * clipboard, + KVMFRClipboardMessage record) +{ + return enqueueRecordLimitNL( + clipboard, record, CLIPBOARD_PENDING_NORMAL_MAX); +} + +static bool enqueueUrgentRecordNL(LGMPClipboard * clipboard, + KVMFRClipboardMessage record) +{ + return enqueueRecordLimitNL(clipboard, record, CLIPBOARD_PENDING_MAX); +} + +static bool enqueueFileFailureNL(LGMPClipboard * clipboard, + const KVMFRClipboardMessage * request, LG_ClipboardFileError error) +{ + KVMFRClipboardMessage response = { 0 }; + response.type = request->type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE ? + KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED : + KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL; + response.clipboardGeneration = request->clipboardGeneration; + response.transfer = request->transfer; + response.format = KVMFR_CLIPBOARD_FORMAT_FILES; + response.token = toWireFileError(error); + return enqueueUrgentRecordNL(clipboard, response); +} + static bool enqueueTypeNL(LGMPClipboard * clipboard, KVMFRClipboardMessageType type) { @@ -240,7 +547,7 @@ static bool enqueueDataNL(LGMPClipboard * clipboard, KVMFRClipboardMessage record, const void * data) { if (!clipboard->connected || !clipboard->queue || - clipboard->pendingCount == CLIPBOARD_PENDING_MAX || + clipboard->pendingCount >= CLIPBOARD_PENDING_NORMAL_MAX || record.length > KVMFR_CLIPBOARD_DATA_BYTES || (record.length && !data)) return false; @@ -251,7 +558,6 @@ static bool enqueueDataNL(LGMPClipboard * clipboard, record.version = KVMFR_CLIPBOARD_VERSION; record.generation = clipboard->claimGeneration; - record.token = 0; memcpy(grant->header, &record, sizeof(record)); if (record.length) memcpy(grant->data, data, record.length); @@ -301,6 +607,7 @@ static void clearProtocolNL(LGMPClipboard * clipboard) clipboard->remoteFormats = 0; clearWriteNL(clipboard); clearReadNL(clipboard); + clearFilesNL(clipboard); memset(clipboard->grants, 0, sizeof(clipboard->grants)); } @@ -403,29 +710,23 @@ static bool validateRecord(const LGMPClipboard * clipboard, if (size < sizeof(*record) || record->version != KVMFR_CLIPBOARD_VERSION || record->type < KVMFR_CLIPBOARD_MESSAGE_OFFER || - record->type > KVMFR_CLIPBOARD_MESSAGE_ACK || + record->type > KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL || record->generation != clipboard->claimGeneration || record->length > KVMFR_CLIPBOARD_DATA_BYTES || record->offset > UINT64_MAX - record->length) return false; - if (record->type == KVMFR_CLIPBOARD_MESSAGE_DATA) + if (record->type == KVMFR_CLIPBOARD_MESSAGE_DATA || + record->type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA) { - const unsigned operations = - !!(record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN) + - !!record->length + !!(record->flags & KVMFR_CLIPBOARD_FLAG_END); - if (!record->transfer || - kvmfrClipboardTransferFromHelper(record->transfer) || - !kvmfrClipboardFormatValid(record->format) || - record->flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN | - KVMFR_CLIPBOARD_FLAG_END) || - !operations || - size != sizeof(*record) + KVMFR_CLIPBOARD_DATA_BYTES || + if (size != sizeof(*record) + KVMFR_CLIPBOARD_DATA_BYTES || queueType != KVMFR_CLIPBOARD_QUEUE_DATA) return false; } else if (queueType != KVMFR_CLIPBOARD_QUEUE_MESSAGE || - size != sizeof(*record) || record->length || record->flags) + size != sizeof(*record) || record->length || + (record->flags && + record->type != KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST)) return false; switch (record->type) @@ -446,14 +747,21 @@ static bool validateRecord(const LGMPClipboard * clipboard, case KVMFR_CLIPBOARD_MESSAGE_REQUEST: return record->clipboardGeneration && kvmfrClipboardTransferFromHelper(record->transfer) && - kvmfrClipboardFormatValid(record->format) && + kvmfrClipboardRepresentationFormatValid(record->format) && !record->offset && !record->size && !record->flags && !record->token && !record->length && !record->sequence; case KVMFR_CLIPBOARD_MESSAGE_DATA: { const uint64_t end = record->offset + record->length; + const unsigned operations = + !!(record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN) + + !!record->length + !!(record->flags & KVMFR_CLIPBOARD_FLAG_END); if (!record->clipboardGeneration || record->token || + kvmfrClipboardTransferFromHelper(record->transfer) || + !kvmfrClipboardRepresentationFormatValid(record->format) || + record->flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END) || !operations || ((record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN) && (record->offset || record->sequence)) || ((record->flags & KVMFR_CLIPBOARD_FLAG_END) && @@ -468,10 +776,19 @@ static bool validateRecord(const LGMPClipboard * clipboard, case KVMFR_CLIPBOARD_MESSAGE_CANCEL: return record->transfer && !record->offset && !record->size && - (!record->format || kvmfrClipboardFormatValid(record->format)) && + (!record->format || + kvmfrClipboardRepresentationFormatValid(record->format)) && !record->flags && !record->length && !record->sequence; + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE: + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED: + case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE: + case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST: + case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA: + case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL: + return kvmfrClipboardFileMessageValid(record); + default: return false; } @@ -530,8 +847,91 @@ static void advanceReadNL(LGMPClipboard * clipboard, clearReadNL(clipboard); } +static bool validateFileReadChunkNL(const LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record, + LG_ClipboardFileRequest * descriptor) +{ + struct FileTransfer * transfer = fileTransferFindNL( + clipboard->fileReads, record->transfer); + if (!transfer || record->clipboardGeneration != transfer->request.dataset || + record->format != KVMFR_CLIPBOARD_FORMAT_FILES || + record->token != toWireFileOperation(transfer->request.operation) || + record->offset != transfer->responseOffset || + record->sequence != transfer->sequence) + return false; + if (!transfer->began) + { + if (record->offset || record->sequence || + !(record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN)) + return false; + } + else if (record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN) + return false; + else if (!(record->flags & KVMFR_CLIPBOARD_FLAG_END) && + record->size != KVMFR_CLIPBOARD_SIZE_UNKNOWN) + return false; + + const uint64_t end = record->offset + record->length; + const uint64_t hint = transfer->began ? transfer->sizeHint : record->size; + if (hint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && end > hint) + return false; + if (transfer->request.operation == LG_CLIPBOARD_FILE_READ && + end > transfer->request.length) + return false; + if ((record->flags & KVMFR_CLIPBOARD_FLAG_END) && + (record->size != end || + (hint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && hint != end))) + return false; + *descriptor = transfer->request; + return true; +} + +static void advanceFileReadNL(LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record) +{ + struct FileTransfer * transfer = fileTransferFindNL( + clipboard->fileReads, record->transfer); + if (!transfer) + return; + if (!transfer->began) + { + transfer->began = true; + transfer->sizeHint = record->size; + } + transfer->responseOffset += record->length; + ++transfer->sequence; + if (record->flags & KVMFR_CLIPBOARD_FLAG_END) + free(fileTransferTakeNL(&clipboard->fileReads, record->transfer)); +} + +static bool rejectMalformedFileReadNL(LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record, + KVMFRClipboardMessage * cancellation, bool * queued) +{ + if (record->type != KVMFR_CLIPBOARD_MESSAGE_FILE_DATA) + return false; + + struct FileTransfer * transfer = fileTransferFindNL( + clipboard->fileReads, record->transfer); + if (!transfer || + transfer->request.dataset != record->clipboardGeneration) + return false; + + *cancellation = (KVMFRClipboardMessage) + { + .type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL, + .clipboardGeneration = transfer->request.dataset, + .transfer = transfer->request.request, + .format = KVMFR_CLIPBOARD_FORMAT_FILES, + .token = KVMFR_CLIPBOARD_FILE_ERROR_INVALID, + }; + free(fileTransferTakeNL(&clipboard->fileReads, record->transfer)); + *queued = enqueueUrgentRecordNL(clipboard, *cancellation); + return true; +} + static void dispatchOffer(LGMPClipboard * clipboard, - KVMFRClipboardFormatFlags mask) + uint64_t dataset, KVMFRClipboardFormatFlags mask) { LG_ClipboardData formats[LG_CLIPBOARD_DATA_NONE]; const unsigned count = formatsFromMask(mask, formats); @@ -541,11 +941,168 @@ static void dispatchOffer(LGMPClipboard * clipboard, const LG_ClipboardEventOps * events = clipboard->events; void * opaque = clipboard->eventOpaque; LG_UNLOCK(clipboard->lock); + if (events && events->fileOffer && + (mask & KVMFR_CLIPBOARD_FORMAT_MASK_FILES)) + events->fileOffer(opaque, dataset); if (events && events->notice) events->notice(opaque, formats, count); LG_UNLOCK(clipboard->eventLock); } +static void dispatchFileAcquire(LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + if (events && events->fileAcquire) + events->fileAcquire(opaque, + record->clipboardGeneration, record->transfer); + LG_UNLOCK(clipboard->eventLock); +} + +static void dispatchFileAcquired(LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + if (events && events->fileAcquired) + events->fileAcquired(opaque, record->clipboardGeneration, + record->transfer, fromWireFileError(record->token)); + LG_UNLOCK(clipboard->eventLock); +} + +static void dispatchFileRelease(LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + if (events && events->fileRelease) + events->fileRelease(opaque, + record->clipboardGeneration, record->transfer); + LG_UNLOCK(clipboard->eventLock); +} + +static void dispatchFileRequest(LGMPClipboard * clipboard, + const LG_ClipboardFileRequest * request) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + if (events && events->fileRequest) + events->fileRequest(opaque, request); + LG_UNLOCK(clipboard->eventLock); +} + +static LG_ClipboardResult dispatchFileData(LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record, + const LG_ClipboardFileRequest * request, const uint8_t * data, + enum HeldPhase phase) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + LG_ClipboardResult result = LG_CLIPBOARD_RESULT_FAILED; + if (events) + switch (phase) + { + case HELD_PHASE_BEGIN: + if (events->fileDataBegin) + result = events->fileDataBegin(opaque, request, record->size); + break; + case HELD_PHASE_CHUNK: + if (events->fileDataChunk) + result = events->fileDataChunk(opaque, request, + record->offset, data, record->length); + break; + case HELD_PHASE_END: + if (events->fileDataEnd) + result = events->fileDataEnd(opaque, request, record->size); + break; + case HELD_PHASE_NONE: + break; + } + LG_UNLOCK(clipboard->eventLock); + return result; +} + +static void dispatchFileCancel(LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + if (events && events->fileCancel) + events->fileCancel(opaque, record->clipboardGeneration, + record->transfer, fromWireFileError(record->token)); + LG_UNLOCK(clipboard->eventLock); +} + +static void dispatchRetiredFiles(LGMPClipboard * clipboard) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + struct FileTransfer * reads = clipboard->retiredFileReads; + struct FileTransfer * writes = clipboard->retiredFileWrites; + struct FileAcquisition * acquisitions = + clipboard->retiredFileAcquisitions; + clipboard->retiredFileReads = NULL; + clipboard->retiredFileWrites = NULL; + clipboard->retiredFileAcquisitions = NULL; + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + + if (events && events->fileCancel) + { + for (struct FileTransfer * transfer = reads; transfer; + transfer = transfer->next) + events->fileCancel(opaque, transfer->request.dataset, + transfer->request.request, + LG_CLIPBOARD_FILE_ERROR_DISCONNECTED); + for (struct FileTransfer * transfer = writes; transfer; + transfer = transfer->next) + events->fileCancel(opaque, transfer->request.dataset, + transfer->request.request, + LG_CLIPBOARD_FILE_ERROR_DISCONNECTED); + for (struct FileAcquisition * acquisition = acquisitions; + acquisition; acquisition = acquisition->next) + events->fileCancel(opaque, acquisition->dataset, + acquisition->acquisition, + LG_CLIPBOARD_FILE_ERROR_DISCONNECTED); + } + freeFileTransfers(reads); + freeFileTransfers(writes); + freeFileAcquisitions(acquisitions); + LG_UNLOCK(clipboard->eventLock); +} + +static void dispatchFileReady(LGMPClipboard * clipboard, uint64_t request) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + if (events && events->fileDataReady) + events->fileDataReady(opaque, request); + LG_UNLOCK(clipboard->eventLock); +} + static void dispatchRelease(LGMPClipboard * clipboard) { LG_LOCK(clipboard->eventLock); @@ -745,6 +1302,8 @@ static bool processHeld(LGMPClipboard * clipboard) KVMFRClipboardMessage record; const uint8_t * data; enum HeldPhase phase; + bool file; + LG_ClipboardFileRequest fileRequest; LG_LOCK(clipboard->lock); if (!clipboard->held || !clipboard->heldReady) { @@ -755,10 +1314,13 @@ static bool processHeld(LGMPClipboard * clipboard) data = record.length ? (const uint8_t *)clipboard->heldMessage.mem + sizeof(record) : NULL; phase = clipboard->heldPhase; + file = clipboard->heldFile; + fileRequest = clipboard->heldFileRequest; clipboard->heldReady = false; LG_UNLOCK(clipboard->lock); - const LG_ClipboardResult result = + const LG_ClipboardResult result = file ? + dispatchFileData(clipboard, &record, &fileRequest, data, phase) : dispatchData(clipboard, &record, data, phase); LG_LOCK(clipboard->lock); if (!clipboard->held) @@ -806,20 +1368,34 @@ static bool processHeld(LGMPClipboard * clipboard) continue; } - const bool matchingRead = + const bool matchingRead = !file && clipboard->readRequest == record.transfer && clipboard->readTransfer; - if (result == LG_CLIPBOARD_RESULT_FAILED && matchingRead) + struct FileTransfer * matchingFile = file ? fileTransferFindNL( + clipboard->fileReads, record.transfer) : NULL; + if (result == LG_CLIPBOARD_RESULT_FAILED && (matchingRead || matchingFile)) { KVMFRClipboardMessage cancel = { 0 }; - cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL; - cancel.clipboardGeneration = clipboard->readClipboardGeneration; - cancel.transfer = clipboard->readTransfer; - cancel.format = clipboard->readFormat; - cancel.token = LG_CLIPBOARD_CANCEL_INVALID; - enqueueRecordNL(clipboard, cancel); + cancel.type = file ? KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL : + KVMFR_CLIPBOARD_MESSAGE_CANCEL; + cancel.clipboardGeneration = file ? fileRequest.dataset : + clipboard->readClipboardGeneration; + cancel.transfer = file ? fileRequest.request : clipboard->readTransfer; + cancel.format = file ? KVMFR_CLIPBOARD_FORMAT_FILES : + clipboard->readFormat; + cancel.token = file ? + toWireFileError(LG_CLIPBOARD_FILE_ERROR_INVALID) : + LG_CLIPBOARD_CANCEL_INVALID; + if (file) + enqueueUrgentRecordNL(clipboard, cancel); + else + enqueueRecordNL(clipboard, cancel); } - if (matchingRead && result == LG_CLIPBOARD_RESULT_ACCEPTED) + if (matchingFile && result == LG_CLIPBOARD_RESULT_ACCEPTED) + advanceFileReadNL(clipboard, &record); + else if (matchingFile) + free(fileTransferTakeNL(&clipboard->fileReads, record.transfer)); + else if (matchingRead && result == LG_CLIPBOARD_RESULT_ACCEPTED) advanceReadNL(clipboard, &record); else if (matchingRead) clearReadNL(clipboard); @@ -828,6 +1404,9 @@ static bool processHeld(LGMPClipboard * clipboard) clipboard->held = false; memset(&clipboard->heldMessage, 0, sizeof(clipboard->heldMessage)); memset(&clipboard->heldRecord, 0, sizeof(clipboard->heldRecord)); + clipboard->heldFile = false; + memset(&clipboard->heldFileRequest, 0, + sizeof(clipboard->heldFileRequest)); if (done != LGMP_OK) { connectionFailed(clipboard, done); @@ -884,6 +1463,7 @@ static bool processMessage(LGMPClipboard * clipboard) if (status != LGMP_OK) connectionFailed(clipboard, status); LG_UNLOCK(clipboard->lock); + dispatchRetiredFiles(clipboard); if (changed) notifyStatus(clipboard); return status == LGMP_OK; @@ -891,6 +1471,8 @@ static bool processMessage(LGMPClipboard * clipboard) if (type == KVMFR_CLIPBOARD_QUEUE_GRANT) { + uint64_t fileReady[CLIPBOARD_PENDING_MAX]; + size_t fileReadyCount = 0; KVMFRClipboardSlotHeader * header = message.mem; const uint32_t token = KVMFR_CLIPBOARD_QUEUE_SERIAL(message.udata); const bool valid = message.size == @@ -922,6 +1504,15 @@ static bool processMessage(LGMPClipboard * clipboard) const LG_ClipboardRequest request = clipboard->writeBlockedRequest; if (ready) clipboard->writeBlocked = false; + if (stored) + for (struct FileTransfer * transfer = clipboard->fileWrites; + transfer && fileReadyCount < CLIPBOARD_PENDING_MAX; + transfer = transfer->next) + if (transfer->blocked) + { + transfer->blocked = false; + fileReady[fileReadyCount++] = transfer->request.request; + } if (status != LGMP_OK) connectionFailed(clipboard, status); LG_UNLOCK(clipboard->lock); @@ -929,6 +1520,8 @@ static bool processMessage(LGMPClipboard * clipboard) DEBUG_WARN("Ignoring invalid LGMP clipboard grant"); if (ready) dispatchReady(clipboard, request); + for (size_t i = 0; i < fileReadyCount; ++i) + dispatchFileReady(clipboard, fileReady[i]); return status == LGMP_OK; } @@ -950,31 +1543,56 @@ static bool processMessage(LGMPClipboard * clipboard) clipboard, &record, message.size, type); if (!valid) { + KVMFRClipboardMessage cancellation = { 0 }; + bool queued = false; + const bool cancelled = rejectMalformedFileReadNL( + clipboard, &record, &cancellation, &queued); status = lgmpClientMessageDone(clipboard->queue); if (status != LGMP_OK) connectionFailed(clipboard, status); LG_UNLOCK(clipboard->lock); + if (cancelled) + dispatchFileCancel(clipboard, &cancellation); + if (queued) + signalWorker(clipboard); DEBUG_WARN("Ignoring malformed LGMP clipboard record"); return status == LGMP_OK; } - if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA) + if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA || + record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA) { - if (!validateReadChunkNL(clipboard, &record)) + LG_ClipboardFileRequest fileRequest = { 0 }; + const bool file = record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA; + const bool chunkValid = file ? validateFileReadChunkNL( + clipboard, &record, &fileRequest) : + validateReadChunkNL(clipboard, &record); + if (!chunkValid) { + KVMFRClipboardMessage cancellation = { 0 }; + bool queued = false; + const bool cancelled = file && rejectMalformedFileReadNL( + clipboard, &record, &cancellation, &queued); status = lgmpClientMessageDone(clipboard->queue); if (status != LGMP_OK) connectionFailed(clipboard, status); LG_UNLOCK(clipboard->lock); + if (cancelled) + dispatchFileCancel(clipboard, &cancellation); + if (queued) + signalWorker(clipboard); DEBUG_WARN("Ignoring stale or malformed LGMP clipboard data"); return status == LGMP_OK; } - record.transfer = clipboard->readRequest; - clipboard->held = true; - clipboard->heldReady = true; - clipboard->heldMessage = message; - clipboard->heldRecord = record; - clipboard->heldPhase = + if (!file) + record.transfer = clipboard->readRequest; + clipboard->held = true; + clipboard->heldReady = true; + clipboard->heldMessage = message; + clipboard->heldRecord = record; + clipboard->heldFile = file; + clipboard->heldFileRequest = fileRequest; + clipboard->heldPhase = (record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN) ? HELD_PHASE_BEGIN : record.length ? HELD_PHASE_CHUNK : HELD_PHASE_END; LG_UNLOCK(clipboard->lock); @@ -989,7 +1607,9 @@ static bool processMessage(LGMPClipboard * clipboard) return false; } bool dispatch = true; + bool fileRejected = false; LG_ClipboardRequest cancelledRead = LG_CLIPBOARD_REQUEST_INVALID; + LG_ClipboardFileRequest fileRequest = { 0 }; switch (record.type) { case KVMFR_CLIPBOARD_MESSAGE_OFFER: @@ -1050,19 +1670,187 @@ static bool processMessage(LGMPClipboard * clipboard) } break; } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE: + { + if (!kvmfrClipboardTransferFromHelper(record.transfer)) + { + dispatch = false; + break; + } + if (fileAcquisitionFindNL(clipboard, record.clipboardGeneration, + record.transfer, true)) + { + fileRejected = enqueueFileFailureNL(clipboard, &record, + LG_CLIPBOARD_FILE_ERROR_INVALID); + dispatch = false; + break; + } + if (record.clipboardGeneration != + clipboard->localClipboardGeneration || + !(clipboard->localFormats & KVMFR_CLIPBOARD_FORMAT_MASK_FILES)) + { + fileRejected = enqueueFileFailureNL(clipboard, &record, + LG_CLIPBOARD_FILE_ERROR_STALE); + dispatch = false; + break; + } + if (fileAcquisitionCountNL(clipboard) >= + KVMFR_CLIPBOARD_FILE_MAX_ACQUISITIONS) + { + fileRejected = enqueueFileFailureNL(clipboard, &record, + LG_CLIPBOARD_FILE_ERROR_NO_SPACE); + dispatch = false; + break; + } + struct FileAcquisition * acquisition = calloc(1, sizeof(*acquisition)); + if (!acquisition) + { + fileRejected = enqueueFileFailureNL(clipboard, &record, + LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + dispatch = false; + break; + } + *acquisition = (struct FileAcquisition) + { + .dataset = record.clipboardGeneration, + .acquisition = record.transfer, + .fromHelper = true, + .next = clipboard->fileAcquisitions, + }; + clipboard->fileAcquisitions = acquisition; + break; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED: + { + if (kvmfrClipboardTransferFromHelper(record.transfer)) + { + dispatch = false; + break; + } + struct FileAcquisition * acquisition = fileAcquisitionFindNL( + clipboard, record.clipboardGeneration, record.transfer, false); + if (!acquisition || acquisition->active) + { + dispatch = false; + break; + } + if (record.token) + free(fileAcquisitionTakeNL(clipboard, + record.clipboardGeneration, record.transfer, false)); + else + acquisition->active = true; + break; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE: + { + struct FileAcquisition * acquisition = fileAcquisitionTakeNL( + clipboard, record.clipboardGeneration, record.transfer, true); + if (!acquisition) + { + dispatch = false; + break; + } + free(acquisition); + break; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST: + { + if (!kvmfrClipboardTransferFromHelper(record.transfer)) + { + dispatch = false; + break; + } + if (fileTransferFindNL(clipboard->fileWrites, record.transfer)) + { + fileRejected = enqueueFileFailureNL(clipboard, &record, + LG_CLIPBOARD_FILE_ERROR_INVALID); + dispatch = false; + break; + } + if (!fileDatasetAcquiredNL(clipboard, + record.clipboardGeneration, true)) + { + fileRejected = enqueueFileFailureNL(clipboard, &record, + LG_CLIPBOARD_FILE_ERROR_STALE); + dispatch = false; + break; + } + if (fileTransferCountNL(clipboard) >= + KVMFR_CLIPBOARD_FILE_MAX_REQUESTS) + { + fileRejected = enqueueFileFailureNL(clipboard, &record, + LG_CLIPBOARD_FILE_ERROR_NO_SPACE); + dispatch = false; + break; + } + struct FileTransfer * transfer = calloc(1, sizeof(*transfer)); + if (!transfer) + { + fileRejected = enqueueFileFailureNL(clipboard, &record, + LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + dispatch = false; + break; + } + fileRequest = (LG_ClipboardFileRequest) + { + .dataset = record.clipboardGeneration, + .request = record.transfer, + .node = record.size, + .offset = record.offset, + .length = record.token == KVMFR_CLIPBOARD_FILE_OP_READ ? + record.flags : 0, + .operation = fromWireFileOperation(record.token), + }; + transfer->request = fileRequest; + transfer->sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN; + transfer->next = clipboard->fileWrites; + clipboard->fileWrites = transfer; + break; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL: + { + const bool fromHelper = kvmfrClipboardTransferFromHelper( + record.transfer); + struct FileTransfer ** transfers = fromHelper ? + &clipboard->fileWrites : &clipboard->fileReads; + struct FileTransfer * found = fileTransferFindNL( + *transfers, record.transfer); + struct FileTransfer * transfer = found && + found->request.dataset == record.clipboardGeneration ? + fileTransferTakeNL(transfers, record.transfer) : NULL; + struct FileAcquisition * acquisition = fileAcquisitionTakeNL( + clipboard, record.clipboardGeneration, record.transfer, + fromHelper); + if (!transfer && !acquisition) + { + dispatch = false; + break; + } + free(transfer); + free(acquisition); + break; + } } LG_UNLOCK(clipboard->lock); if (!dispatch) { - DEBUG_WARN("Ignoring stale LGMP clipboard control record"); + if (fileRejected) + signalWorker(clipboard); + else + DEBUG_WARN("Ignoring stale LGMP clipboard control record"); return true; } switch (record.type) { case KVMFR_CLIPBOARD_MESSAGE_OFFER: - dispatchOffer(clipboard, record.token); + dispatchOffer(clipboard, record.clipboardGeneration, record.token); break; case KVMFR_CLIPBOARD_MESSAGE_CLEAR: dispatchRelease(clipboard); @@ -1089,6 +1877,21 @@ static bool processMessage(LGMPClipboard * clipboard) else dispatchCancel(clipboard, &record); break; + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE: + dispatchFileAcquire(clipboard, &record); + break; + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED: + dispatchFileAcquired(clipboard, &record); + break; + case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE: + dispatchFileRelease(clipboard, &record); + break; + case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST: + dispatchFileRequest(clipboard, &fileRequest); + break; + case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL: + dispatchFileCancel(clipboard, &record); + break; } return true; } @@ -1098,10 +1901,18 @@ static int clipboardThread(void * opaque) LGMPClipboard * clipboard = opaque; while (!atomic_load_explicit(&clipboard->stop, memory_order_acquire)) { - if (!processHeld(clipboard) || !processMessage(clipboard)) + const bool held = processHeld(clipboard); + dispatchRetiredFiles(clipboard); + if (!held) + break; + const bool message = processMessage(clipboard); + dispatchRetiredFiles(clipboard); + if (!message) break; LG_ClipboardRequest readyRequest = LG_CLIPBOARD_REQUEST_INVALID; + uint64_t fileReady[CLIPBOARD_PENDING_MAX]; + size_t fileReadyCount = 0; LG_LOCK(clipboard->lock); flushPendingNL(clipboard); const uint64_t now = microtime(); @@ -1110,23 +1921,36 @@ static int clipboardThread(void * opaque) now - clipboard->lastSend >= CLIPBOARD_KEEPALIVE_US) enqueueTypeNL(clipboard, KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE); if (clipboard->writeBlocked && - clipboard->pendingCount < CLIPBOARD_PENDING_MAX && + clipboard->pendingCount < CLIPBOARD_PENDING_NORMAL_MAX && availableGrantNL(clipboard)) { readyRequest = clipboard->writeBlockedRequest; clipboard->writeBlocked = false; } + if (clipboard->pendingCount < CLIPBOARD_PENDING_NORMAL_MAX && + availableGrantNL(clipboard)) + for (struct FileTransfer * transfer = clipboard->fileWrites; + transfer && fileReadyCount < CLIPBOARD_PENDING_MAX; + transfer = transfer->next) + if (transfer->blocked) + { + transfer->blocked = false; + fileReady[fileReadyCount++] = transfer->request.request; + } const bool retry = clipboard->pendingCount != 0; LG_UNLOCK(clipboard->lock); if (readyRequest != LG_CLIPBOARD_REQUEST_INVALID) dispatchReady(clipboard, readyRequest); + for (size_t i = 0; i < fileReadyCount; ++i) + dispatchFileReady(clipboard, fileReady[i]); if (atomic_load_explicit(&clipboard->stop, memory_order_acquire)) break; lgWaitEvent(clipboard->event, retry ? CLIPBOARD_RETRY_MS : CLIPBOARD_POLL_MS); } + dispatchRetiredFiles(clipboard); notifyStatus(clipboard); return 0; } @@ -1139,6 +1963,21 @@ bool lgmpClipboard_create(PLGMPClient client, LGMPClipboard ** result) if (!clipboard) return false; clipboard->client = client; + uint64_t nonce = 0; + if (!randomBytes(&nonce, sizeof(nonce))) + { + free(clipboard); + return false; + } + nonce &= ~KVMFR_CLIPBOARD_TRANSFER_HELPER; + if (!nonce) + nonce = 1; + clipboard->transferSerial = nonce; + clipboard->localGenerationSerial = + (nonce ^ UINT64_C(0x4c47434c49504244)) & + ~KVMFR_CLIPBOARD_TRANSFER_HELPER; + if (!clipboard->localGenerationSerial) + clipboard->localGenerationSerial = 1; clipboard->providerGeneration = 1; LG_LOCK_INIT(clipboard->lock); LG_LOCK_INIT(clipboard->eventLock); @@ -1320,6 +2159,7 @@ void lgmpClipboard_disconnect(LGMPClipboard * clipboard) clipboard->held = false; clearProtocolNL(clipboard); LG_UNLOCK(clipboard->lock); + dispatchRetiredFiles(clipboard); if (queue) lgmpClientUnsubscribe(&queue); if (event) @@ -1350,6 +2190,7 @@ static bool attach(void * opaque, const LG_ClipboardEventOps * events, { LGMPClipboard * clipboard = opaque; KVMFRClipboardFormatFlags formats; + uint64_t dataset; LG_ClipboardData replay[LG_CLIPBOARD_DATA_NONE]; bool available; LG_LOCK(clipboard->eventLock); @@ -1367,11 +2208,18 @@ static bool attach(void * opaque, const LG_ClipboardEventOps * events, } } formats = clipboard->remoteFormats; + dataset = clipboard->remoteClipboardGeneration; LG_UNLOCK(clipboard->lock); const unsigned replayCount = available ? formatsFromMask(formats, replay) : 0; - if (replayCount && events->notice) - events->notice(eventOpaque, replay, replayCount); + if (replayCount) + { + if ((formats & KVMFR_CLIPBOARD_FORMAT_MASK_FILES) && + events->fileOffer) + events->fileOffer(eventOpaque, dataset); + if (events->notice) + events->notice(eventOpaque, replay, replayCount); + } LG_UNLOCK(clipboard->eventLock); if (available) signalWorker(clipboard); @@ -1404,12 +2252,10 @@ static bool releaseClipboard(void * opaque) LG_LOCK(clipboard->lock); KVMFRClipboardMessage clear = { 0 }; clear.type = KVMFR_CLIPBOARD_MESSAGE_CLEAR; - clear.clipboardGeneration = clipboard->localClipboardGeneration + 1; - if (!clear.clipboardGeneration) - ++clear.clipboardGeneration; + clear.clipboardGeneration = nextClientGeneration(clipboard); const unsigned required = clipboard->claimed ? 1U : 2U; const bool result = - clipboard->pendingCount <= CLIPBOARD_PENDING_MAX - required && + clipboard->pendingCount <= CLIPBOARD_PENDING_NORMAL_MAX - required && ensureClaimNL(clipboard) && enqueueRecordNL(clipboard, clear); if (result) { @@ -1429,7 +2275,7 @@ static bool notifyTypes(void * opaque, const LG_ClipboardData types[], if (!types || !count || count > LG_CLIPBOARD_DATA_NONE) return false; for (size_t i = 0; i < count; ++i) - if (!kvmfrClipboardFormatValid(toWireFormat(types[i]))) + if (!kvmfrClipboardRepresentationFormatValid(toWireFormat(types[i]))) return false; const KVMFRClipboardFormatFlags formats = formatMask(types, count); @@ -1437,12 +2283,10 @@ static bool notifyTypes(void * opaque, const LG_ClipboardData types[], KVMFRClipboardMessage offer = { 0 }; offer.type = KVMFR_CLIPBOARD_MESSAGE_OFFER; offer.token = formats; - offer.clipboardGeneration = clipboard->localClipboardGeneration + 1; - if (!offer.clipboardGeneration) - ++offer.clipboardGeneration; + offer.clipboardGeneration = nextClientGeneration(clipboard); const unsigned required = clipboard->claimed ? 1U : 2U; const bool result = - clipboard->pendingCount <= CLIPBOARD_PENDING_MAX - required && + clipboard->pendingCount <= CLIPBOARD_PENDING_NORMAL_MAX - required && ensureClaimNL(clipboard) && enqueueRecordNL(clipboard, offer); if (result) @@ -1462,7 +2306,7 @@ static LG_ClipboardResult dataBegin(void * opaque, LGMPClipboard * clipboard = opaque; const KVMFRClipboardFormat format = toWireFormat(type); if (!kvmfrClipboardTransferFromHelper(request) || - !kvmfrClipboardFormatValid(format)) + !kvmfrClipboardRepresentationFormatValid(format)) return LG_CLIPBOARD_RESULT_FAILED; LG_LOCK(clipboard->lock); @@ -1623,7 +2467,7 @@ static bool requestData(void * opaque, LG_ClipboardRequest request, LGMPClipboard * clipboard = opaque; const KVMFRClipboardFormat format = toWireFormat(type); if (request == LG_CLIPBOARD_REQUEST_INVALID || - !kvmfrClipboardFormatValid(format)) + !kvmfrClipboardRepresentationFormatValid(format)) return false; LG_LOCK(clipboard->lock); if (clipboard->readRequest || !clipboard->remoteClipboardGeneration || @@ -1657,6 +2501,353 @@ static bool requestData(void * opaque, LG_ClipboardRequest request, return result; } +static bool offerFiles(void * opaque, uint64_t dataset) +{ + LGMPClipboard * clipboard = opaque; + if (!dataset || kvmfrClipboardTransferFromHelper(dataset)) + return false; + KVMFRClipboardMessage offer = { 0 }; + offer.type = KVMFR_CLIPBOARD_MESSAGE_OFFER; + offer.clipboardGeneration = dataset; + offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES; + LG_LOCK(clipboard->lock); + const unsigned required = clipboard->claimed ? 1U : 2U; + const bool result = clipboard->pendingCount <= + CLIPBOARD_PENDING_NORMAL_MAX - required && ensureClaimNL(clipboard) && + enqueueRecordNL(clipboard, offer); + if (result) + { + clipboard->localClipboardGeneration = dataset; + clipboard->localFormats = KVMFR_CLIPBOARD_FORMAT_MASK_FILES; + } + LG_UNLOCK(clipboard->lock); + if (result) + signalWorker(clipboard); + return result; +} + +static bool fileAcquire(void * opaque, uint64_t dataset, + uint64_t acquisitionId) +{ + LGMPClipboard * clipboard = opaque; + if (!dataset || !kvmfrClipboardTransferFromClient(acquisitionId)) + return false; + LG_LOCK(clipboard->lock); + if (fileAcquisitionCountNL(clipboard) >= + KVMFR_CLIPBOARD_FILE_MAX_ACQUISITIONS) + { + LG_UNLOCK(clipboard->lock); + return false; + } + struct FileAcquisition * acquisition = calloc(1, sizeof(*acquisition)); + if (!acquisition) + { + LG_UNLOCK(clipboard->lock); + return false; + } + KVMFRClipboardMessage record = { 0 }; + record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE; + record.clipboardGeneration = dataset; + record.transfer = acquisitionId; + record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + const bool result = dataset == clipboard->remoteClipboardGeneration && + (clipboard->remoteFormats & KVMFR_CLIPBOARD_FORMAT_MASK_FILES) && + !fileAcquisitionFindNL(clipboard, dataset, acquisitionId, false) && + ensureClaimNL(clipboard) && enqueueRecordNL(clipboard, record); + if (result) + { + *acquisition = (struct FileAcquisition) + { + .dataset = dataset, + .acquisition = acquisitionId, + .next = clipboard->fileAcquisitions, + }; + clipboard->fileAcquisitions = acquisition; + } + LG_UNLOCK(clipboard->lock); + if (!result) + free(acquisition); + else + signalWorker(clipboard); + return result; +} + +static bool fileAcquired(void * opaque, uint64_t dataset, + uint64_t acquisitionId, LG_ClipboardFileError error) +{ + LGMPClipboard * clipboard = opaque; + if (!dataset || !kvmfrClipboardTransferFromHelper(acquisitionId) || + !validFileError(error)) + return false; + LG_LOCK(clipboard->lock); + struct FileAcquisition * acquisition = fileAcquisitionFindNL( + clipboard, dataset, acquisitionId, true); + KVMFRClipboardMessage record = { 0 }; + record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED; + record.clipboardGeneration = dataset; + record.transfer = acquisitionId; + record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + record.token = toWireFileError(error); + const bool result = acquisition && !acquisition->active && + enqueueUrgentRecordNL(clipboard, record); + if (result && error == LG_CLIPBOARD_FILE_ERROR_NONE) + acquisition->active = true; + else if (result) + free(fileAcquisitionTakeNL( + clipboard, dataset, acquisitionId, true)); + LG_UNLOCK(clipboard->lock); + if (result) + signalWorker(clipboard); + return result; +} + +static bool fileRelease(void * opaque, uint64_t dataset, + uint64_t acquisitionId) +{ + LGMPClipboard * clipboard = opaque; + if (!dataset || !kvmfrClipboardTransferFromClient(acquisitionId)) + return false; + LG_LOCK(clipboard->lock); + struct FileAcquisition * acquisition = fileAcquisitionFindNL( + clipboard, dataset, acquisitionId, false); + KVMFRClipboardMessage record = { 0 }; + record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE; + record.clipboardGeneration = dataset; + record.transfer = acquisitionId; + record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + const bool result = acquisition && acquisition->active && + enqueueUrgentRecordNL(clipboard, record); + if (result) + free(fileAcquisitionTakeNL( + clipboard, dataset, acquisitionId, false)); + LG_UNLOCK(clipboard->lock); + if (result) + signalWorker(clipboard); + return result; +} + +static bool fileRequest(void * opaque, + const LG_ClipboardFileRequest * descriptor) +{ + LGMPClipboard * clipboard = opaque; + if (!descriptor || !descriptor->dataset || + !kvmfrClipboardTransferFromClient(descriptor->request) || + (descriptor->operation != LG_CLIPBOARD_FILE_LIST && + descriptor->operation != LG_CLIPBOARD_FILE_READ) || + (descriptor->operation == LG_CLIPBOARD_FILE_LIST && + (descriptor->offset || descriptor->length)) || + (descriptor->operation == LG_CLIPBOARD_FILE_READ && + (!descriptor->length || + descriptor->length > KVMFR_CLIPBOARD_FILE_READ_BYTES || + descriptor->offset > UINT64_MAX - descriptor->length))) + return false; + KVMFRClipboardMessage record = { 0 }; + record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST; + record.clipboardGeneration = descriptor->dataset; + record.transfer = descriptor->request; + record.offset = descriptor->offset; + record.size = descriptor->node; + record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + record.flags = descriptor->operation == + LG_CLIPBOARD_FILE_READ ? descriptor->length : 0; + record.token = toWireFileOperation(descriptor->operation); + LG_LOCK(clipboard->lock); + if (fileTransferCountNL(clipboard) >= + KVMFR_CLIPBOARD_FILE_MAX_REQUESTS) + { + LG_UNLOCK(clipboard->lock); + return false; + } + struct FileTransfer * transfer = calloc(1, sizeof(*transfer)); + if (!transfer) + { + LG_UNLOCK(clipboard->lock); + return false; + } + const bool result = fileDatasetAcquiredNL( + clipboard, descriptor->dataset, false) && + !fileTransferFindNL(clipboard->fileReads, descriptor->request) && + enqueueRecordNL(clipboard, record); + if (result) + { + transfer->request = *descriptor; + transfer->sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN; + transfer->next = clipboard->fileReads; + clipboard->fileReads = transfer; + } + LG_UNLOCK(clipboard->lock); + if (!result) + free(transfer); + else + signalWorker(clipboard); + return result; +} + +static bool sameFileRequest(const LG_ClipboardFileRequest * a, + const LG_ClipboardFileRequest * b) +{ + return a->dataset == b->dataset && a->request == b->request && + a->node == b->node && a->offset == b->offset && + a->length == b->length && a->operation == b->operation; +} + +static LG_ClipboardResult fileDataBegin(void * opaque, + const LG_ClipboardFileRequest * descriptor, uint64_t sizeHint) +{ + LGMPClipboard * clipboard = opaque; + if (!descriptor || !kvmfrClipboardTransferFromHelper(descriptor->request)) + return LG_CLIPBOARD_RESULT_FAILED; + LG_LOCK(clipboard->lock); + struct FileTransfer * transfer = fileTransferFindNL( + clipboard->fileWrites, descriptor->request); + const bool valid = transfer && !transfer->began && + sameFileRequest(&transfer->request, descriptor) && + (descriptor->operation != LG_CLIPBOARD_FILE_READ || + sizeHint <= descriptor->length); + if (valid) + { + transfer->began = true; + transfer->sizeHint = sizeHint; + transfer->responseOffset = 0; + transfer->sequence = 0; + transfer->blocked = false; + } + LG_UNLOCK(clipboard->lock); + return valid ? LG_CLIPBOARD_RESULT_ACCEPTED : + LG_CLIPBOARD_RESULT_FAILED; +} + +static LG_ClipboardResult fileDataChunk(void * opaque, + const LG_ClipboardFileRequest * descriptor, uint64_t responseOffset, + const void * data, size_t size) +{ + LGMPClipboard * clipboard = opaque; + if (!descriptor || !data || !size || + size > KVMFR_CLIPBOARD_DATA_BYTES || + responseOffset > UINT64_MAX - size) + return LG_CLIPBOARD_RESULT_FAILED; + LG_LOCK(clipboard->lock); + struct FileTransfer * transfer = fileTransferFindNL( + clipboard->fileWrites, descriptor->request); + if (!transfer || !transfer->began || + !sameFileRequest(&transfer->request, descriptor) || + transfer->responseOffset != responseOffset || + (transfer->sizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && + responseOffset + size > transfer->sizeHint) || + (descriptor->operation == LG_CLIPBOARD_FILE_READ && + responseOffset + size > descriptor->length)) + { + LG_UNLOCK(clipboard->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + KVMFRClipboardMessage record = { 0 }; + record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_DATA; + record.clipboardGeneration = descriptor->dataset; + record.transfer = descriptor->request; + record.offset = responseOffset; + record.sequence = transfer->sequence; + record.size = transfer->sequence ? + KVMFR_CLIPBOARD_SIZE_UNKNOWN : transfer->sizeHint; + record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + record.flags = transfer->sequence ? 0 : + KVMFR_CLIPBOARD_FLAG_BEGIN; + record.token = toWireFileOperation(descriptor->operation); + record.length = (uint32_t)size; + const bool result = enqueueDataNL(clipboard, record, data); + if (result) + { + transfer->responseOffset += size; + ++transfer->sequence; + } + else + transfer->blocked = true; + LG_UNLOCK(clipboard->lock); + if (result) + signalWorker(clipboard); + return result ? LG_CLIPBOARD_RESULT_ACCEPTED : + LG_CLIPBOARD_RESULT_BLOCKED; +} + +static LG_ClipboardResult fileDataEnd(void * opaque, + const LG_ClipboardFileRequest * descriptor, uint64_t finalSize) +{ + LGMPClipboard * clipboard = opaque; + if (!descriptor) + return LG_CLIPBOARD_RESULT_FAILED; + LG_LOCK(clipboard->lock); + struct FileTransfer * transfer = fileTransferFindNL( + clipboard->fileWrites, descriptor->request); + if (!transfer || !transfer->began || + !sameFileRequest(&transfer->request, descriptor) || + transfer->responseOffset != finalSize || + (transfer->sizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && + transfer->sizeHint != finalSize)) + { + LG_UNLOCK(clipboard->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + KVMFRClipboardMessage record = { 0 }; + record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_DATA; + record.clipboardGeneration = descriptor->dataset; + record.transfer = descriptor->request; + record.offset = finalSize; + record.sequence = transfer->sequence; + record.size = finalSize; + record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + record.flags = KVMFR_CLIPBOARD_FLAG_END | + (transfer->sequence ? 0 : KVMFR_CLIPBOARD_FLAG_BEGIN); + record.token = toWireFileOperation(descriptor->operation); + const bool result = enqueueDataNL(clipboard, record, NULL); + if (result) + free(fileTransferTakeNL( + &clipboard->fileWrites, descriptor->request)); + else + transfer->blocked = true; + LG_UNLOCK(clipboard->lock); + if (result) + signalWorker(clipboard); + return result ? LG_CLIPBOARD_RESULT_ACCEPTED : + LG_CLIPBOARD_RESULT_BLOCKED; +} + +static bool fileCancel(void * opaque, uint64_t dataset, + uint64_t request, LG_ClipboardFileError reason) +{ + LGMPClipboard * clipboard = opaque; + if (!dataset || !request || reason == LG_CLIPBOARD_FILE_ERROR_NONE || + !validFileError(reason)) + return false; + LG_LOCK(clipboard->lock); + struct FileTransfer * read = fileTransferFindNL( + clipboard->fileReads, request); + struct FileTransfer * write = fileTransferFindNL( + clipboard->fileWrites, request); + struct FileAcquisition * acquisition = fileAcquisitionFindNL( + clipboard, dataset, request, + kvmfrClipboardTransferFromHelper(request)); + KVMFRClipboardMessage record = { 0 }; + record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL; + record.clipboardGeneration = dataset; + record.transfer = request; + record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + record.token = toWireFileError(reason); + const bool matches = (read && read->request.dataset == dataset) || + (write && write->request.dataset == dataset) || acquisition; + const bool result = matches && enqueueUrgentRecordNL(clipboard, record); + if (result) + { + free(fileTransferTakeNL(&clipboard->fileReads, request)); + free(fileTransferTakeNL(&clipboard->fileWrites, request)); + if (acquisition) + free(fileAcquisitionTakeNL(clipboard, dataset, request, + kvmfrClipboardTransferFromHelper(request))); + } + LG_UNLOCK(clipboard->lock); + if (result) + signalWorker(clipboard); + return result; +} + static const LG_ClipboardOps CLIPBOARD_OPS = { .name = "LGMP", @@ -1665,12 +2856,21 @@ static const LG_ClipboardOps CLIPBOARD_OPS = .detach = detach, .release = releaseClipboard, .notifyTypes = notifyTypes, + .offerFiles = offerFiles, .dataBegin = dataBegin, .dataChunk = dataChunk, .dataEnd = dataEnd, .dataCancel = dataCancel, .dataReady = dataReady, .request = requestData, + .fileAcquire = fileAcquire, + .fileAcquired = fileAcquired, + .fileRelease = fileRelease, + .fileRequest = fileRequest, + .fileDataBegin = fileDataBegin, + .fileDataChunk = fileDataChunk, + .fileDataEnd = fileDataEnd, + .fileCancel = fileCancel, }; const LG_ClipboardOps * lgmpClipboard_getOps(void)