diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj
index 6963fd1e..c1aca813 100644
--- a/idd/LGIdd/LGIdd.vcxproj
+++ b/idd/LGIdd/LGIdd.vcxproj
@@ -78,6 +78,7 @@
+
@@ -154,6 +155,7 @@
+
diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters
index 1e6cf0bc..e547c041 100644
--- a/idd/LGIdd/LGIdd.vcxproj.filters
+++ b/idd/LGIdd/LGIdd.vcxproj.filters
@@ -250,6 +250,9 @@
Transport\LGMP
+
+ Transport\LGMP
+
Transport\LGMP
@@ -414,6 +417,9 @@
Transport\LGMP
+
+ Transport\LGMP
+
Transport\LGMP
diff --git a/idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.cpp b/idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.cpp
new file mode 100644
index 00000000..d721b5e8
--- /dev/null
+++ b/idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.cpp
@@ -0,0 +1,431 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#include "transport/lgmp/CLGMPClipboardFiles.h"
+
+bool CLGMPClipboardFiles::FromHelper(Direction direction)
+{
+ return direction == Direction::HELPER_TO_CLIENT;
+}
+
+bool CLGMPClipboardFiles::RequesterHelper(uint64_t transfer)
+{
+ return kvmfrClipboardTransferFromHelper(transfer) != 0;
+}
+
+bool CLGMPClipboardFiles::SameRequester(
+ Direction direction, uint64_t transfer)
+{
+ return FromHelper(direction) == RequesterHelper(transfer);
+}
+
+CLGMPClipboardFiles::Acquisition *
+CLGMPClipboardFiles::FindAcquisition(uint64_t transfer)
+{
+ for (Acquisition& acquisition : m_acquisitions)
+ if (acquisition.transfer == transfer)
+ return &acquisition;
+ return nullptr;
+}
+
+const CLGMPClipboardFiles::Acquisition *
+CLGMPClipboardFiles::FindAcquisition(uint64_t transfer) const
+{
+ for (const Acquisition& acquisition : m_acquisitions)
+ if (acquisition.transfer == transfer)
+ return &acquisition;
+ return nullptr;
+}
+
+const CLGMPClipboardFiles::Acquisition *
+CLGMPClipboardFiles::FindAcquired(
+ uint64_t dataset, bool requesterHelper) const
+{
+ for (const Acquisition& acquisition : m_acquisitions)
+ if (acquisition.dataset == dataset && acquisition.requesterHelper ==
+ requesterHelper && acquisition.acquired)
+ return &acquisition;
+ return nullptr;
+}
+
+bool CLGMPClipboardFiles::HasAcquisition(
+ uint64_t dataset, bool requesterHelper) const
+{
+ for (const Acquisition& acquisition : m_acquisitions)
+ if (acquisition.dataset == dataset &&
+ acquisition.requesterHelper == requesterHelper)
+ return true;
+ return false;
+}
+
+CLGMPClipboardFiles::Acquisition * CLGMPClipboardFiles::FreeAcquisition()
+{
+ for (Acquisition& acquisition : m_acquisitions)
+ if (!acquisition.Active())
+ return &acquisition;
+ return nullptr;
+}
+
+bool CLGMPClipboardFiles::HasFreeAcquisition() const
+{
+ for (const Acquisition& acquisition : m_acquisitions)
+ if (!acquisition.Active())
+ return true;
+ return false;
+}
+
+CLGMPClipboardFiles::Request * CLGMPClipboardFiles::FindRequest(
+ uint64_t transfer)
+{
+ for (Request& request : m_requests)
+ if (request.transfer == transfer)
+ return &request;
+ return nullptr;
+}
+
+const CLGMPClipboardFiles::Request * CLGMPClipboardFiles::FindRequest(
+ uint64_t transfer) const
+{
+ for (const Request& request : m_requests)
+ if (request.transfer == transfer)
+ return &request;
+ return nullptr;
+}
+
+CLGMPClipboardFiles::Request * CLGMPClipboardFiles::FreeRequest()
+{
+ for (Request& request : m_requests)
+ if (!request.Active())
+ return &request;
+ return nullptr;
+}
+
+bool CLGMPClipboardFiles::HasFreeRequest() const
+{
+ for (const Request& request : m_requests)
+ if (!request.Active())
+ return true;
+ return false;
+}
+
+bool CLGMPClipboardFiles::TransferInUse(uint64_t transfer) const
+{
+ return FindAcquisition(transfer) || FindRequest(transfer);
+}
+
+bool CLGMPClipboardFiles::ValidateData(const Request& request,
+ const KVMFRClipboardMessage& message) const
+{
+ if (message.clipboardGeneration != request.dataset ||
+ message.token != request.operation ||
+ message.offset != request.expectedOffset ||
+ message.sequence != request.expectedSequence)
+ return false;
+
+ const uint64_t end = message.offset + message.length;
+ if (!request.began)
+ {
+ if (message.offset || message.sequence ||
+ !(message.flags & KVMFR_CLIPBOARD_FLAG_BEGIN))
+ return false;
+ }
+ else if (message.flags & KVMFR_CLIPBOARD_FLAG_BEGIN)
+ return false;
+
+ const uint64_t hint = request.began ? request.sizeHint : message.size;
+ if (hint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && end > hint)
+ return false;
+ if (request.operation == KVMFR_CLIPBOARD_FILE_OP_READ &&
+ end > request.requestedBytes)
+ return false;
+ if (message.flags & KVMFR_CLIPBOARD_FLAG_END)
+ return message.size == end &&
+ (hint == KVMFR_CLIPBOARD_SIZE_UNKNOWN || hint == end);
+ return true;
+}
+
+void CLGMPClipboardFiles::RemoveAcquisition(Acquisition& acquisition)
+{
+ const uint64_t dataset = acquisition.dataset;
+ const bool requesterHelper = acquisition.requesterHelper;
+ acquisition.Clear();
+ for (Request& request : m_requests)
+ if (request.dataset == dataset &&
+ request.requesterHelper == requesterHelper)
+ request.Clear();
+}
+
+bool CLGMPClipboardFiles::IsRecord(const KVMFRClipboardMessage& message)
+{
+ return kvmfrClipboardFileMessageType(message.type) != 0;
+}
+
+bool CLGMPClipboardFiles::IsData(const KVMFRClipboardMessage& message)
+{
+ return message.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA;
+}
+
+bool CLGMPClipboardFiles::DirectionValid(
+ const KVMFRClipboardMessage& message, Direction direction)
+{
+ if (!kvmfrClipboardFileMessageValid(&message))
+ return false;
+
+ switch (message.type)
+ {
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE:
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE:
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST:
+ return SameRequester(direction, message.transfer);
+
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED:
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA:
+ return !SameRequester(direction, message.transfer);
+
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL:
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+bool CLGMPClipboardFiles::TransferActive(uint64_t transfer) const
+{
+ return TransferInUse(transfer);
+}
+
+bool CLGMPClipboardFiles::IsStaleTerminal(
+ const KVMFRClipboardMessage& message, Direction direction) const
+{
+ if (!DirectionValid(message, direction) ||
+ TransferInUse(message.transfer))
+ return false;
+
+ switch (message.type)
+ {
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED:
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA:
+ return !SameRequester(direction, message.transfer);
+
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE:
+ return SameRequester(direction, message.transfer);
+
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL:
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+bool CLGMPClipboardFiles::Validate(const KVMFRClipboardMessage& message,
+ Direction direction, uint64_t offeredDataset, bool filesOffered) const
+{
+ if (!DirectionValid(message, direction))
+ return false;
+
+ switch (message.type)
+ {
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE:
+ return SameRequester(direction, message.transfer) && filesOffered &&
+ message.clipboardGeneration == offeredDataset &&
+ !TransferInUse(message.transfer) &&
+ !HasAcquisition(message.clipboardGeneration,
+ FromHelper(direction)) && HasFreeAcquisition();
+
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED:
+ {
+ const Acquisition * acquisition =
+ FindAcquisition(message.transfer);
+ return acquisition && !acquisition->acquired &&
+ acquisition->dataset == message.clipboardGeneration &&
+ FromHelper(direction) != acquisition->requesterHelper;
+ }
+
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE:
+ {
+ const Acquisition * acquisition =
+ FindAcquisition(message.transfer);
+ return acquisition && acquisition->acquired &&
+ acquisition->dataset == message.clipboardGeneration &&
+ FromHelper(direction) == acquisition->requesterHelper;
+ }
+
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST:
+ return SameRequester(direction, message.transfer) &&
+ !TransferInUse(message.transfer) && HasFreeRequest() &&
+ FindAcquired(message.clipboardGeneration,
+ FromHelper(direction));
+
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA:
+ {
+ const Request * request = FindRequest(message.transfer);
+ return request &&
+ FromHelper(direction) != request->requesterHelper &&
+ ValidateData(*request, message);
+ }
+
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL:
+ {
+ const Acquisition * acquisition =
+ FindAcquisition(message.transfer);
+ if (acquisition)
+ return acquisition->dataset == message.clipboardGeneration;
+ const Request * request = FindRequest(message.transfer);
+ return request && request->dataset == message.clipboardGeneration;
+ }
+
+ default:
+ return false;
+ }
+}
+
+void CLGMPClipboardFiles::Apply(
+ const KVMFRClipboardMessage& message, Direction direction)
+{
+ switch (message.type)
+ {
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE:
+ {
+ Acquisition * acquisition = FreeAcquisition();
+ if (!acquisition)
+ return;
+ acquisition->dataset = message.clipboardGeneration;
+ acquisition->transfer = message.transfer;
+ acquisition->requesterHelper = FromHelper(direction);
+ acquisition->acquired = false;
+ break;
+ }
+
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED:
+ {
+ Acquisition * acquisition = FindAcquisition(message.transfer);
+ if (!acquisition)
+ return;
+ if (message.token == KVMFR_CLIPBOARD_FILE_ERROR_NONE)
+ acquisition->acquired = true;
+ else
+ RemoveAcquisition(*acquisition);
+ break;
+ }
+
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE:
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL:
+ {
+ Acquisition * acquisition = FindAcquisition(message.transfer);
+ if (acquisition)
+ {
+ RemoveAcquisition(*acquisition);
+ break;
+ }
+ Request * request = FindRequest(message.transfer);
+ if (request)
+ request->Clear();
+ break;
+ }
+
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST:
+ {
+ Request * request = FreeRequest();
+ if (!request)
+ return;
+ request->dataset = message.clipboardGeneration;
+ request->transfer = message.transfer;
+ request->operation = message.token;
+ request->requestedBytes = message.token ==
+ KVMFR_CLIPBOARD_FILE_OP_READ ? message.flags : 0;
+ request->requesterHelper = FromHelper(direction);
+ request->sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN;
+ break;
+ }
+
+ case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA:
+ {
+ Request * request = FindRequest(message.transfer);
+ if (!request)
+ return;
+ if (!request->began)
+ {
+ request->began = true;
+ request->sizeHint = message.size;
+ }
+ request->expectedOffset += message.length;
+ ++request->expectedSequence;
+ if (message.flags & KVMFR_CLIPBOARD_FLAG_END)
+ request->Clear();
+ break;
+ }
+ }
+}
+
+void CLGMPClipboardFiles::Reset()
+{
+ for (Acquisition& acquisition : m_acquisitions)
+ acquisition.Clear();
+ for (Request& request : m_requests)
+ request.Clear();
+}
+
+unsigned CLGMPClipboardFiles::Disconnect(KVMFRClipboardMessage * records,
+ unsigned capacity, uint32_t endpointGeneration)
+{
+ unsigned count = 0;
+ for (const Request& request : m_requests)
+ {
+ if (!request.Active() || count == capacity)
+ continue;
+ KVMFRClipboardMessage& record = records[count++];
+ record = {};
+ record.version = KVMFR_CLIPBOARD_VERSION;
+ record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL;
+ record.generation = endpointGeneration;
+ record.clipboardGeneration = request.dataset;
+ record.transfer = request.transfer;
+ record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
+ record.token = request.requesterHelper ?
+ KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED :
+ KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED;
+ }
+
+ for (const Acquisition& acquisition : m_acquisitions)
+ {
+ if (!acquisition.Active() || count == capacity)
+ continue;
+ KVMFRClipboardMessage& record = records[count++];
+ record = {};
+ record.version = KVMFR_CLIPBOARD_VERSION;
+ record.type = acquisition.requesterHelper ?
+ (acquisition.acquired ? KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL :
+ KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED) :
+ (acquisition.acquired ? KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE :
+ KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL);
+ record.generation = endpointGeneration;
+ record.clipboardGeneration = acquisition.dataset;
+ record.transfer = acquisition.transfer;
+ record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
+ if (acquisition.requesterHelper)
+ record.token = KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED;
+ else if (!acquisition.acquired)
+ record.token = KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED;
+ }
+ Reset();
+ return count;
+}
diff --git a/idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.h b/idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.h
new file mode 100644
index 00000000..41f4bb51
--- /dev/null
+++ b/idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.h
@@ -0,0 +1,114 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#pragma once
+
+#include "common/KVMFRClipboard.h"
+
+#include
+
+class CLGMPClipboardFiles
+{
+public:
+ enum class Direction
+ {
+ CLIENT_TO_HELPER,
+ HELPER_TO_CLIENT,
+ };
+
+ static constexpr unsigned MAX_ACQUISITIONS =
+ KVMFR_CLIPBOARD_FILE_MAX_ACQUISITIONS;
+ static constexpr unsigned MAX_REQUESTS =
+ KVMFR_CLIPBOARD_FILE_MAX_REQUESTS;
+
+private:
+ struct Acquisition
+ {
+ uint64_t dataset = 0;
+ uint64_t transfer = 0;
+ bool requesterHelper = false;
+ bool acquired = false;
+
+ bool Active() const { return transfer != 0; }
+ void Clear() { *this = {}; }
+ };
+
+ struct Request
+ {
+ uint64_t dataset = 0;
+ uint64_t transfer = 0;
+ uint64_t expectedOffset = 0;
+ uint64_t sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN;
+ uint32_t expectedSequence = 0;
+ uint32_t requestedBytes = 0;
+ KVMFRClipboardFileOperation operation = 0;
+ bool requesterHelper = false;
+ bool began = false;
+
+ bool Active() const { return transfer != 0; }
+ void Clear() { *this = {}; }
+ };
+
+ Acquisition m_acquisitions[MAX_ACQUISITIONS] = {};
+ Request m_requests[MAX_REQUESTS] = {};
+
+ static bool FromHelper(Direction direction);
+ static bool RequesterHelper(uint64_t transfer);
+ static bool SameRequester(Direction direction, uint64_t transfer);
+
+ Acquisition * FindAcquisition(uint64_t transfer);
+ const Acquisition * FindAcquisition(uint64_t transfer) const;
+ const Acquisition * FindAcquired(
+ uint64_t dataset, bool requesterHelper) const;
+ bool HasAcquisition(uint64_t dataset, bool requesterHelper) const;
+ Acquisition * FreeAcquisition();
+ bool HasFreeAcquisition() const;
+
+ Request * FindRequest(uint64_t transfer);
+ const Request * FindRequest(uint64_t transfer) const;
+ Request * FreeRequest();
+ bool HasFreeRequest() const;
+
+ bool TransferInUse(uint64_t transfer) const;
+ bool ValidateData(const Request& request,
+ const KVMFRClipboardMessage& message) const;
+ void RemoveAcquisition(Acquisition& acquisition);
+
+public:
+ static bool IsRecord(const KVMFRClipboardMessage& message);
+ static bool IsData(const KVMFRClipboardMessage& message);
+ static bool DirectionValid(const KVMFRClipboardMessage& message,
+ Direction direction);
+ bool TransferActive(uint64_t transfer) const;
+ bool IsStaleTerminal(const KVMFRClipboardMessage& message,
+ Direction direction) const;
+
+ bool Validate(const KVMFRClipboardMessage& message,
+ Direction direction, uint64_t offeredDataset,
+ bool filesOffered) const;
+ void Apply(const KVMFRClipboardMessage& message, Direction direction);
+ void Reset();
+
+ // Builds the records which release Helper-owned resources or fail Helper
+ // requests when an LGMP clipboard owner disappears. The state is reset even
+ // when capacity is smaller than the number of generated records.
+ unsigned Disconnect(KVMFRClipboardMessage * records, unsigned capacity,
+ uint32_t endpointGeneration);
+};
diff --git a/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.cpp
index 50462e4a..694f1382 100644
--- a/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.cpp
+++ b/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.cpp
@@ -75,7 +75,7 @@ namespace
kvmfrClipboardTransferFromHelper(message.transfer) :
kvmfrClipboardTransferFromClient(message.transfer);
return message.clipboardGeneration && validTransfer &&
- kvmfrClipboardFormatValid(message.format) &&
+ kvmfrClipboardRepresentationFormatValid(message.format) &&
!message.offset && !message.size && !message.flags &&
!message.token && !message.length && !message.sequence;
}
@@ -83,7 +83,8 @@ namespace
bool ValidCancel(const KVMFRClipboardMessage& message)
{
return message.transfer && !message.offset && !message.size &&
- (!message.format || kvmfrClipboardFormatValid(message.format)) &&
+ (!message.format ||
+ kvmfrClipboardRepresentationFormatValid(message.format)) &&
!message.flags && !message.length && !message.sequence;
}
@@ -91,7 +92,8 @@ namespace
{
return message.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST ||
message.type == KVMFR_CLIPBOARD_MESSAGE_DATA ||
- message.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL;
+ message.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL ||
+ CLGMPClipboardFiles::IsRecord(message);
}
}
@@ -367,6 +369,39 @@ void CLGMPClipboardTransport::QueueStaleRequestCancel(
QueueInternalTarget(cancel);
}
+void CLGMPClipboardTransport::QueueStaleFileRecord(
+ const KVMFRClipboardMessage& record)
+{
+ if (!m_target || !m_available || !m_endpointGeneration)
+ return;
+
+ KVMFRClipboardMessage response = {};
+ response.version = KVMFR_CLIPBOARD_VERSION;
+ response.generation = m_endpointGeneration;
+ response.clipboardGeneration = record.clipboardGeneration;
+ response.transfer = record.transfer;
+ response.format = KVMFR_CLIPBOARD_FORMAT_FILES;
+ response.token = KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED;
+ if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE)
+ response.type = KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED;
+ else if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST)
+ response.type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL;
+ else
+ return;
+ QueueInternalTarget(response);
+}
+
+void CLGMPClipboardTransport::QueueFileDisconnect()
+{
+ KVMFRClipboardMessage records[
+ CLGMPClipboardFiles::MAX_ACQUISITIONS +
+ CLGMPClipboardFiles::MAX_REQUESTS] = {};
+ const unsigned count = m_files.Disconnect(records, _countof(records),
+ m_endpointGeneration);
+ for (unsigned i = 0; i < count; ++i)
+ QueueInternalTarget(records[i]);
+}
+
bool CLGMPClipboardTransport::QueueInternalTarget(
const KVMFRClipboardMessage& record)
{
@@ -423,12 +458,14 @@ void CLGMPClipboardTransport::ReleaseOwner(
clientID, generation, reason);
if (clearHelper)
{
+ QueueFileDisconnect();
QueueTransferCancel(clientToHelper);
QueueTransferCancel(helperToClient);
QueueHelperClear();
}
else
{
+ m_files.Reset();
m_clientClipboardGeneration = 0;
m_clientFormats = 0;
}
@@ -441,6 +478,7 @@ void CLGMPClipboardTransport::ResetProtocol(bool keepClipboard)
ReleaseOwner("endpoint reset", false);
m_clientToHelper.Clear();
m_helperToClient.Clear();
+ m_files.Reset();
m_clientClipboardGeneration = 0;
m_clientFormats = 0;
m_discardHelperToClient = 0;
@@ -661,10 +699,18 @@ bool CLGMPClipboardTransport::ValidateOwnedControl(
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
return ValidateInboundRecord(message);
+ 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_CANCEL:
+ return ValidateInboundRecord(message);
+
case KVMFR_CLIPBOARD_MESSAGE_COMMIT:
return message.token >= 1 && message.token <= MEMORY_COUNT &&
message.clipboardGeneration && message.transfer &&
- kvmfrClipboardFormatValid(message.format) &&
+ (kvmfrClipboardRepresentationFormatValid(message.format) ||
+ message.format == KVMFR_CLIPBOARD_FORMAT_FILES) &&
message.length <= KVMFR_CLIPBOARD_DATA_BYTES &&
!(message.flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END)) &&
@@ -678,6 +724,21 @@ bool CLGMPClipboardTransport::ValidateOwnedControl(
bool CLGMPClipboardTransport::ValidateInboundRecord(
const KVMFRClipboardMessage& message) const
{
+ if (CLGMPClipboardFiles::IsRecord(message))
+ {
+ if (m_files.IsStaleTerminal(message,
+ CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER))
+ return true;
+ const bool filesOffered = m_cachedValid &&
+ m_cachedClipboard.type == KVMFR_CLIPBOARD_MESSAGE_OFFER &&
+ (m_helperFormats & KVMFR_CLIPBOARD_FORMAT_MASK_FILES);
+ const uint64_t offeredDataset = filesOffered ?
+ m_cachedClipboard.clipboardGeneration : 0;
+ return m_files.Validate(message,
+ CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER,
+ offeredDataset, filesOffered);
+ }
+
switch (message.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
@@ -704,6 +765,15 @@ bool CLGMPClipboardTransport::ValidateOutboundRecord(
message.generation != m_endpointGeneration)
return false;
+ if (CLGMPClipboardFiles::IsRecord(message))
+ {
+ const bool filesOffered = m_clientClipboardGeneration &&
+ (m_clientFormats & KVMFR_CLIPBOARD_FORMAT_MASK_FILES);
+ return m_files.Validate(message,
+ CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT,
+ m_clientClipboardGeneration, filesOffered);
+ }
+
switch (message.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
@@ -718,7 +788,7 @@ bool CLGMPClipboardTransport::ValidateOutboundRecord(
case KVMFR_CLIPBOARD_MESSAGE_DATA:
return message.clipboardGeneration &&
kvmfrClipboardTransferFromClient(message.transfer) &&
- kvmfrClipboardFormatValid(message.format) &&
+ kvmfrClipboardRepresentationFormatValid(message.format) &&
message.length <= KVMFR_CLIPBOARD_DATA_BYTES &&
!(message.flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END)) && !message.token &&
@@ -786,6 +856,13 @@ void CLGMPClipboardTransport::AdvanceChunk(
void CLGMPClipboardTransport::ApplyInbound(
const KVMFRClipboardMessage& message)
{
+ if (CLGMPClipboardFiles::IsRecord(message))
+ {
+ m_files.Apply(message,
+ CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER);
+ return;
+ }
+
switch (message.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
@@ -824,6 +901,13 @@ void CLGMPClipboardTransport::ApplyInbound(
void CLGMPClipboardTransport::ApplyOutbound(
const KVMFRClipboardMessage& message)
{
+ if (CLGMPClipboardFiles::IsRecord(message))
+ {
+ m_files.Apply(message,
+ CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT);
+ return;
+ }
+
switch (message.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
@@ -977,6 +1061,22 @@ bool CLGMPClipboardTransport::ProcessMessage(
if (!IsOwner(clientID, message.generation))
return true;
+
+ if (CLGMPClipboardFiles::IsRecord(message) &&
+ CLGMPClipboardFiles::DirectionValid(message,
+ CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER) &&
+ !m_files.TransferActive(message.transfer) &&
+ (message.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE ||
+ message.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST) &&
+ !ValidateInboundRecord(message))
+ {
+ // A cancellation or replacement can cross a request which was generated
+ // from the preceding file offer. The already-posted lifecycle record will
+ // cancel the client-side work; consuming this stale request avoids
+ // treating normal cross-queue ordering as a protocol failure.
+ RenewLease();
+ return true;
+ }
if (!ValidateOwnedControl(message))
{
ReleaseOwner("invalid clipboard message", true);
@@ -1009,9 +1109,12 @@ bool CLGMPClipboardTransport::ProcessMessage(
lgmpHostMemPtr(m_grantMemory[grantIndex]));
KVMFRClipboardMessage dataMessage = {};
memcpy(&dataMessage, slot, sizeof(dataMessage));
+ const bool fileData = dataMessage.type ==
+ KVMFR_CLIPBOARD_MESSAGE_FILE_DATA;
const bool matchesCommit =
dataMessage.version == KVMFR_CLIPBOARD_VERSION &&
- dataMessage.type == KVMFR_CLIPBOARD_MESSAGE_DATA &&
+ (dataMessage.type == KVMFR_CLIPBOARD_MESSAGE_DATA ||
+ fileData) &&
dataMessage.generation == m_ownerGeneration &&
dataMessage.clipboardGeneration == message.clipboardGeneration &&
dataMessage.transfer == message.transfer &&
@@ -1020,16 +1123,28 @@ bool CLGMPClipboardTransport::ProcessMessage(
dataMessage.format == message.format &&
dataMessage.flags == message.flags &&
dataMessage.length == message.length &&
- dataMessage.sequence == message.sequence &&
- !dataMessage.token;
- if (!matchesCommit ||
- !kvmfrClipboardTransferFromHelper(dataMessage.transfer) ||
- !ValidateChunk(m_clientToHelper, dataMessage))
+ dataMessage.sequence == message.sequence;
+ const bool staleFileData = fileData && m_files.IsStaleTerminal(
+ dataMessage, CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER);
+ const bool validData = fileData ?
+ (staleFileData || m_files.Validate(dataMessage,
+ CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER, 0, false)) :
+ (!dataMessage.token &&
+ kvmfrClipboardTransferFromHelper(dataMessage.transfer) &&
+ ValidateChunk(m_clientToHelper, dataMessage));
+ if (!matchesCommit || !validData)
{
ReleaseOwner("invalid clipboard commit", true);
return true;
}
+ if (staleFileData)
+ {
+ grant = {};
+ RenewLease();
+ return true;
+ }
+
grant.committed = true;
dataMessage.generation = m_endpointGeneration;
RenewLease();
@@ -1039,6 +1154,13 @@ bool CLGMPClipboardTransport::ProcessMessage(
return false;
}
+ if (m_files.IsStaleTerminal(message,
+ CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER))
+ {
+ RenewLease();
+ return true;
+ }
+
if (message.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST &&
m_helperToClient.Active())
{
@@ -1144,7 +1266,11 @@ ClipboardChannelResult CLGMPClipboardTransport::SendControl(
ClipboardChannelResult CLGMPClipboardTransport::SendData(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
- if (!ValidateChunk(m_helperToClient, record))
+ const bool valid = CLGMPClipboardFiles::IsData(record) ?
+ m_files.Validate(record,
+ CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT, 0, false) :
+ ValidateChunk(m_helperToClient, record);
+ if (!valid)
return ClipboardChannelResult::FAILED;
PLGMPMemory memory = FindAvailable(m_dataMemory);
@@ -1192,6 +1318,7 @@ ClipboardChannelResult CLGMPClipboardTransport::SendClipboard(
if (BlockedOwnerLost(record))
{
QueueStaleRequestCancel(record);
+ QueueStaleFileRecord(record);
ClearOutboundBlock();
Wake();
return ClipboardChannelResult::ACCEPTED;
@@ -1201,8 +1328,31 @@ ClipboardChannelResult CLGMPClipboardTransport::SendClipboard(
record.generation != m_endpointGeneration)
return ClipboardChannelResult::FAILED;
- if (!ValidateOutboundRecord(record) ||
- (record.length && !data))
+ if (m_files.IsStaleTerminal(record,
+ CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT))
+ {
+ ClearOutboundBlock();
+ return ClipboardChannelResult::ACCEPTED;
+ }
+
+ const bool validOutbound = ValidateOutboundRecord(record);
+ if (!validOutbound && CLGMPClipboardFiles::IsRecord(record) &&
+ CLGMPClipboardFiles::DirectionValid(record,
+ CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT) &&
+ !m_files.TransferActive(record.transfer) &&
+ (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE ||
+ record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST))
+ {
+ // The LGMP owner may disappear or replace its dataset before Helper
+ // observes the matching lifecycle record. Fail new work locally without
+ // tearing down the shared Helper clipboard channel.
+ QueueStaleFileRecord(record);
+ ClearOutboundBlock();
+ Wake();
+ return ClipboardChannelResult::ACCEPTED;
+ }
+
+ if (!validOutbound || (record.length && !data))
return ClipboardChannelResult::FAILED;
if (record.transfer &&
@@ -1248,6 +1398,7 @@ ClipboardChannelResult CLGMPClipboardTransport::SendClipboard(
if (OwnerScopedLifecycle(record))
{
QueueStaleRequestCancel(record);
+ QueueStaleFileRecord(record);
ClearOutboundBlock();
Wake();
return ClipboardChannelResult::ACCEPTED;
@@ -1265,7 +1416,9 @@ ClipboardChannelResult CLGMPClipboardTransport::SendClipboard(
}
ClipboardChannelResult result;
- if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA)
+ if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA)
+ result = SendData(record, data);
+ else if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA)
{
if (!m_helperToClient.Active() ||
record.transfer != m_helperToClient.transfer)
@@ -1316,6 +1469,7 @@ void CLGMPClipboardTransport::ClipboardState(
ReleaseOwner("Helper endpoint changed", false);
m_clientToHelper.Clear();
m_helperToClient.Clear();
+ m_files.Reset();
m_clientClipboardGeneration = 0;
m_clientFormats = 0;
m_discardHelperToClient = 0;
@@ -1344,6 +1498,7 @@ void CLGMPClipboardTransport::ClipboardReset(
ReleaseOwner("Helper reset", false);
m_clientToHelper.Clear();
m_helperToClient.Clear();
+ m_files.Reset();
m_clientClipboardGeneration = 0;
m_clientFormats = 0;
m_discardHelperToClient = 0;
@@ -1385,8 +1540,11 @@ void CLGMPClipboardTransport::Thread()
else if (m_ownerClientID && GetTickCount64() >= m_ownerDeadline)
ReleaseOwner("lease expired", true);
+ // A restarted client can reuse dataset and transfer IDs. Deliver every
+ // old-owner cleanup record before admitting messages from a new owner.
if (!RetryTarget() || !PumpInternalTarget() ||
- !DrainMessage() || !PublishStatus() ||
+ (m_internalTargetCount == 0 && !DrainMessage()) ||
+ !PublishStatus() ||
!ReplayClipboard() || !PostGrants())
{
notifyFailed = true;
@@ -1398,7 +1556,7 @@ void CLGMPClipboardTransport::Thread()
notifyReady = m_target != nullptr;
readyTarget = m_target;
}
- if (m_pendingTarget.valid || m_ownerClientID)
+ if (m_pendingTarget.valid || m_internalTargetCount || m_ownerClientID)
timeout = ACTIVE_POLL_MS;
}
diff --git a/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.h b/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.h
index c7ea59f5..0c8f8d92 100644
--- a/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.h
+++ b/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.h
@@ -22,6 +22,7 @@
#include "CSRWLock.h"
#include "transport/IClipboardSource.h"
+#include "transport/lgmp/CLGMPClipboardFiles.h"
#include "common/KVMFRClipboard.h"
#include "common/LGMPConfig.h"
@@ -39,8 +40,10 @@ class CLGMPHost;
class CLGMPClipboardTransport final : public IClipboardSource
{
private:
- static constexpr unsigned MEMORY_COUNT = 2;
- static constexpr unsigned INTERNAL_TARGET_COUNT = 3;
+ static constexpr unsigned MEMORY_COUNT = 8;
+ static constexpr unsigned INTERNAL_TARGET_COUNT =
+ CLGMPClipboardFiles::MAX_ACQUISITIONS +
+ CLGMPClipboardFiles::MAX_REQUESTS + 3;
static constexpr ULONGLONG OWNER_LEASE_MS = 1000;
static constexpr DWORD ACTIVE_POLL_MS = 5;
static constexpr DWORD IDLE_POLL_MS = 50;
@@ -135,6 +138,7 @@ private:
PendingTarget m_pendingTarget;
KVMFRClipboardMessage m_internalTarget[INTERNAL_TARGET_COUNT] = {};
unsigned m_internalTargetCount = 0;
+ CLGMPClipboardFiles m_files;
bool Initialize();
void DeInit();
@@ -156,6 +160,8 @@ private:
void QueueTransferCancel(const Transfer& transfer);
void QueueStaleRequestCancel(
const KVMFRClipboardMessage& request);
+ void QueueStaleFileRecord(const KVMFRClipboardMessage& record);
+ void QueueFileDisconnect();
bool QueueInternalTarget(const KVMFRClipboardMessage& record);
bool PumpInternalTarget();