Compare commits

..

2 Commits

Author SHA1 Message Date
rustdesk
33f59beebb clipboard: fail a size request for an entry that has no size
The descriptor now carries FD_FILESIZE only when the size was actually read,
which leaves both size fields at zero for an entry where it was not. The
non-stream FILECONTENTS_SIZE handler passed those fields straight back, so a
receiver asking about such an entry would be told it is empty and would write a
zero-length file without any error. The previous code returned whatever
GetFileSize() had left behind, which for a failure is INVALID_FILE_SIZE -- also
wrong, but wrong loudly.

Answer only when the descriptor has a size, and fail the request otherwise. The
receiver then fails the paste instead of completing it with an empty file, which
is the same class of outcome the old code produced for that entry.

The IStream_Stat path is unchanged; it already answers from the stream.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-16 14:18:13 +08:00
rustdesk
f4853a1e41 clipboard: declare file sizes in the Windows file list
Pasting files from a Windows peer got slower with the number of files and
then stopped working, while the total size made no difference: one 32 MB
file pasted at once, 45 small files took seconds, and 50 small files
totalling 8.3 MB left the shell spinning and copied nothing.

The sender never set FD_FILESIZE, although it had already read the size and
filled nFileSizeLow/nFileSizeHigh. Without that flag the receiver cannot
trust those fields, so CliprdrStream_New() asks for the size of each file
with its own FILECONTENTS_SIZE request and blocks on the reply for up to
CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS. Those streams are all built up front,
in the loop that answers the shell's request for the file group descriptor,
so the round trips run one after another inside IDataObject::GetData() and
the shell waits for every one of them before the paste can begin. The cost
is therefore per file, not per byte, which is what the reports describe.

The size is now declared, and only when it is known: GetFileSizeEx() replaces
GetFileSize(), whose INVALID_FILE_SIZE return cannot be told from a genuine
4GB-1 file without GetLastError(), and directories are skipped because the
handle opened with FILE_FLAG_BACKUP_SEMANTICS above is not a file handle.
A regular file whose size cannot be read is rejected instead of publishing an
ambiguous zero size. This is required because the Unix receiver currently
consumes the descriptor size fields regardless of FD_FILESIZE for
compatibility with older Windows senders.

Upstream FreeRDP, which this file comes from, sets FD_FILESIZE here. It has
been commented out in our copy since the file was first added in 6672087f7,
with no recorded reason; the "for compatibility" note above it was written
later, in 55005f812, about code that already looked this way. The Unix
receiver carries the other half of the same workaround in filetype.rs, where
the size is trusted whether or not the flag is set, explicitly "for
compatibility with Windows".

This is a sender-side fix: the paste gets faster once the machine the files
come from runs it, whichever version does the pasting.

Fixes #16238

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-16 14:18:13 +08:00

View File

@@ -2443,9 +2443,6 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t
return NULL;
}
// to-do: use `fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI`.
// We keep `fd->dwFlags = FD_ATTRIBUTES | FD_WRITESTIME | FD_PROGRESSUI` for compatibility.
// fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI;
fd->dwFlags = FD_ATTRIBUTES | FD_WRITESTIME | FD_PROGRESSUI;
fd->dwFileAttributes = GetFileAttributesW(file_name);
if (fd->dwFileAttributes == INVALID_FILE_ATTRIBUTES)
@@ -2458,7 +2455,34 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t
fd->dwFlags &= ~FD_WRITESTIME;
}
fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh);
// Announce the size in the file list. Without FD_FILESIZE the receiving side cannot
// trust the size fields, so CliprdrStream_New() asks for each file's size with its own
// FILECONTENTS_SIZE request and blocks on the reply. Those requests are made for every
// entry up front, while the shell is inside IDataObject::GetData(), so the cost grows
// with the number of files and not with their size.
//
// GetFileSize() reports failure as INVALID_FILE_SIZE, which cannot be told apart from a
// genuine 4GB-1 file without GetLastError(), and it fails outright on the directory
// handles FILE_FLAG_BACKUP_SEMANTICS lets us open above. A directory gets no size. A
// file whose size cannot be read is rejected rather than sent with the flag off: the
// Unix receiver reads the size fields whether or not the flag is set, for compatibility
// with older Windows senders, and would take the zero for an empty file.
if ((fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0)
{
LARGE_INTEGER file_size = {0};
if (!GetFileSizeEx(hFile, &file_size) || file_size.QuadPart < 0)
{
CloseHandle(hFile);
free(fd);
return NULL;
}
fd->nFileSizeLow = file_size.LowPart;
fd->nFileSizeHigh = (DWORD)file_size.HighPart;
fd->dwFlags |= FD_FILESIZE;
}
if ((wcslen(file_name + pathLen) + 1) > sizeof(fd->cFileName) / sizeof(fd->cFileName[0]))
{
// The file name is too long, which is not a normal case.
@@ -3515,15 +3539,24 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
{
if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE)
{
FILEDESCRIPTORW *fd;
if (clipboard->nFiles <= fileContentsRequest->listIndex)
{
rc = ERROR_INTERNAL_ERROR;
goto exit;
}
*((UINT32 *)&pData[0]) =
clipboard->fileDescriptor[fileContentsRequest->listIndex]->nFileSizeLow;
*((UINT32 *)&pData[4]) =
clipboard->fileDescriptor[fileContentsRequest->listIndex]->nFileSizeHigh;
fd = clipboard->fileDescriptor[fileContentsRequest->listIndex];
// The size fields only mean anything when FD_FILESIZE says so. Answering with
// them regardless would describe an entry whose size could not be read as empty.
if ((fd->dwFlags & FD_FILESIZE) == 0)
{
rc = ERROR_INTERNAL_ERROR;
goto exit;
}
*((UINT32 *)&pData[0]) = fd->nFileSizeLow;
*((UINT32 *)&pData[4]) = fd->nFileSizeHigh;
uSize = cbRequested;
}
else if (fileContentsRequest->dwFlags == FILECONTENTS_RANGE)