FreeRDP/client/common/client_cliprdr_file.c
Pascal Nowack d7d3055b5f X11/cliprdr: Rework server to client clipboard handling
The purpose of clipboard data locking is to make the other peer
retaining the current file list until a pending paste operation is done,
even though the clipboard selection changed.
As it may be difficult to determine, when a lock is needed, imitate the
same behaviour as mstsc:
When the server side supports clipboard data locking, always attempt to
lock the file list on the server regardless of what is advertised in a
FormatList PDU.
The Lock Clipboard Data PDU can even be already sent, before the
Format List Response PDU is sent.
This is also what mstsc, does: First, lock the new (potential) file
list, then unlock the file list, when the pending paste operation is
done.
So, rework the current clipboard implementation in that direction.

Since the implementation for timeouts for old file lists is a bit hard,
for now always force unlock pending locks, when the selection changes.
However, timeouts for old file lists can still be added in the future.

The reworked clipboard handling is done with the help of three hash
tables:

1. The inode table: This hash table manages all inodes for each file.
   The keys in this table are the inodes themselves, while the values
   the files and directories and their attributes (file size, last write
   time, etc.).
2. The clipdata table: This table manages the locks for each file list.
   The keys in this table represent the clip data id and the values the
   clip data entries, which have a reference to the clip data dir, a
   directory containing the whole selection, and some helper attributes,
   like the clip data id itself.
3. The request table: Every file size or file range request is managed
   here. When a FileContentsRequest is made, its stream id with the
   respective details are added to this table. When a response is
   received, these details can then be easily looked up here.
2023-07-20 11:36:11 +02:00

2526 lines
70 KiB
C

/**
* FreeRDP: A Remote Desktop Protocol Implementation
* X11 Clipboard Redirection
*
* Copyright 2010-2011 Vic Lee
* Copyright 2015 Thincast Technologies GmbH
* Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
* Copyright 2023 Armin Novak <armin.novak@thincast.com>
* Copyright 2023 Pascal Nowack <Pascal.Nowack@gmx.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <freerdp/config.h>
#include <stdlib.h>
#include <errno.h>
#ifdef WITH_FUSE3
#define FUSE_USE_VERSION 30
#include <fuse_lowlevel.h>
#elif WITH_FUSE2
#define FUSE_USE_VERSION 26
#include <fuse_lowlevel.h>
#endif
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
#include <sys/mount.h>
#include <sys/stat.h>
#include <errno.h>
#include <time.h>
#endif
#include <winpr/crt.h>
#include <winpr/assert.h>
#include <winpr/image.h>
#include <winpr/stream.h>
#include <winpr/clipboard.h>
#include <winpr/path.h>
#include <freerdp/utils/signal.h>
#include <freerdp/log.h>
#include <freerdp/client/cliprdr.h>
#include <freerdp/channels/channels.h>
#include <freerdp/channels/cliprdr.h>
#include <freerdp/client/client_cliprdr_file.h>
#define MAX_CLIP_DATA_DIR_LEN 10
#define MAX_CLIPBOARD_FORMATS 255
#define NO_CLIP_DATA_ID (UINT64_C(1) << 32)
#define WIN32_FILETIME_TO_UNIX_EPOCH UINT64_C(11644473600)
#ifdef WITH_DEBUG_CLIPRDR
#define DEBUG_CLIPRDR(log, ...) WLog_Print(log, WLOG_DEBUG, __VA_ARGS__)
#else
#define DEBUG_CLIPRDR(log, ...) \
do \
{ \
} while (0)
#endif
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
typedef enum _FuseLowlevelOperationType
{
FUSE_LL_OPERATION_NONE,
FUSE_LL_OPERATION_LOOKUP,
FUSE_LL_OPERATION_GETATTR,
FUSE_LL_OPERATION_READ,
} FuseLowlevelOperationType;
typedef struct _CliprdrFuseFile CliprdrFuseFile;
struct _CliprdrFuseFile
{
CliprdrFuseFile* parent;
wArrayList* children;
char* filename;
char* filename_with_root;
UINT32 list_idx;
fuse_ino_t ino;
BOOL is_directory;
BOOL is_readonly;
BOOL has_size;
UINT64 size;
BOOL has_last_write_time;
UINT64 last_write_time_unix;
BOOL has_clip_data_id;
UINT32 clip_data_id;
};
typedef struct
{
CliprdrFileContext* file_context;
CliprdrFuseFile* clip_data_dir;
BOOL has_clip_data_id;
UINT32 clip_data_id;
} CliprdrFuseClipDataEntry;
typedef struct
{
CliprdrFileContext* file_context;
wArrayList* fuse_files;
BOOL all_files;
BOOL has_clip_data_id;
UINT32 clip_data_id;
} FuseFileClearContext;
typedef struct
{
FuseLowlevelOperationType operation_type;
CliprdrFuseFile* fuse_file;
fuse_req_t fuse_req;
UINT32 stream_id;
} CliprdrFuseRequest;
typedef struct
{
CliprdrFuseFile* parent;
char* parent_path;
} CliprdrFuseFindParentContext;
#endif
typedef struct
{
char* name;
FILE* fp;
INT64 size;
CliprdrFileContext* context;
} CliprdrLocalFile;
typedef struct
{
UINT32 lockId;
BOOL locked;
size_t count;
CliprdrLocalFile* files;
CliprdrFileContext* context;
} CliprdrLocalStream;
struct cliprdr_file_context
{
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
/* FUSE related**/
HANDLE fuse_thread;
struct fuse_session* fuse_sess;
wHashTable* inode_table;
wHashTable* clip_data_table;
wHashTable* request_table;
CliprdrFuseFile* root_dir;
CliprdrFuseClipDataEntry* clip_data_entry_without_id;
UINT32 current_clip_data_id;
fuse_ino_t next_ino;
UINT32 next_clip_data_id;
UINT32 next_stream_id;
#endif
/* File clipping */
BOOL file_formats_registered;
UINT32 file_capability_flags;
UINT32 local_lock_id;
wHashTable* local_streams;
wLog* log;
void* clipboard;
CliprdrClientContext* context;
char* path;
char* exposed_path;
BYTE server_data_hash[WINPR_SHA256_DIGEST_LENGTH];
BYTE client_data_hash[WINPR_SHA256_DIGEST_LENGTH];
};
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
static void fuse_file_free(void* data)
{
CliprdrFuseFile* fuse_file = data;
if (!fuse_file)
return;
ArrayList_Free(fuse_file->children);
free(fuse_file->filename_with_root);
free(fuse_file);
}
static CliprdrFuseFile* fuse_file_new(void)
{
CliprdrFuseFile* fuse_file;
fuse_file = calloc(1, sizeof(CliprdrFuseFile));
if (!fuse_file)
return NULL;
fuse_file->children = ArrayList_New(FALSE);
if (!fuse_file->children)
{
free(fuse_file);
return NULL;
}
return fuse_file;
}
static void clip_data_entry_free(void* data)
{
CliprdrFuseClipDataEntry* clip_data_entry = data;
if (!clip_data_entry)
return;
if (clip_data_entry->has_clip_data_id)
{
CliprdrFileContext* file_context = clip_data_entry->file_context;
CLIPRDR_UNLOCK_CLIPBOARD_DATA unlock_clipboard_data = { 0 };
WINPR_ASSERT(file_context);
unlock_clipboard_data.common.msgType = CB_UNLOCK_CLIPDATA;
unlock_clipboard_data.clipDataId = clip_data_entry->clip_data_id;
file_context->context->ClientUnlockClipboardData(file_context->context,
&unlock_clipboard_data);
clip_data_entry->has_clip_data_id = FALSE;
WLog_Print(file_context->log, WLOG_DEBUG, "Destroyed ClipDataEntry with id %u",
clip_data_entry->clip_data_id);
}
free(clip_data_entry);
}
static BOOL does_server_support_clipdata_locking(CliprdrFileContext* file_context)
{
WINPR_ASSERT(file_context);
if (cliprdr_file_context_remote_get_flags(file_context) & CB_CAN_LOCK_CLIPDATA)
return TRUE;
return FALSE;
}
static UINT32 get_next_free_clip_data_id(CliprdrFileContext* file_context)
{
UINT32 clip_data_id;
WINPR_ASSERT(file_context);
HashTable_Lock(file_context->inode_table);
clip_data_id = file_context->next_clip_data_id;
while (clip_data_id == 0 ||
HashTable_GetItemValue(file_context->clip_data_table, (void*)(UINT_PTR)clip_data_id))
++clip_data_id;
file_context->next_clip_data_id = clip_data_id + 1;
HashTable_Unlock(file_context->inode_table);
return clip_data_id;
}
static CliprdrFuseClipDataEntry* clip_data_entry_new(CliprdrFileContext* file_context,
BOOL needs_clip_data_id)
{
CliprdrFuseClipDataEntry* clip_data_entry;
CLIPRDR_LOCK_CLIPBOARD_DATA lock_clipboard_data = { 0 };
WINPR_ASSERT(file_context);
clip_data_entry = calloc(1, sizeof(CliprdrFuseClipDataEntry));
if (!clip_data_entry)
return NULL;
clip_data_entry->file_context = file_context;
clip_data_entry->clip_data_id = get_next_free_clip_data_id(file_context);
if (!needs_clip_data_id)
return clip_data_entry;
lock_clipboard_data.common.msgType = CB_LOCK_CLIPDATA;
lock_clipboard_data.clipDataId = clip_data_entry->clip_data_id;
if (file_context->context->ClientLockClipboardData(file_context->context, &lock_clipboard_data))
{
HashTable_Lock(file_context->inode_table);
clip_data_entry_free(clip_data_entry);
HashTable_Unlock(file_context->inode_table);
return NULL;
}
clip_data_entry->has_clip_data_id = TRUE;
WLog_Print(file_context->log, WLOG_DEBUG, "Created ClipDataEntry with id %u",
clip_data_entry->clip_data_id);
return clip_data_entry;
}
static BOOL should_remove_fuse_file(CliprdrFuseFile* fuse_file, BOOL all_files,
BOOL has_clip_data_id, BOOL clip_data_id)
{
if (all_files)
return TRUE;
if (fuse_file->ino == FUSE_ROOT_ID)
return FALSE;
if (!fuse_file->has_clip_data_id && !has_clip_data_id)
return TRUE;
if (fuse_file->has_clip_data_id && has_clip_data_id && fuse_file->clip_data_id == clip_data_id)
return TRUE;
return FALSE;
}
static BOOL maybe_clear_fuse_request(const void* key, void* value, void* arg)
{
CliprdrFuseRequest* fuse_request = value;
FuseFileClearContext* clear_context = arg;
CliprdrFileContext* file_context = clear_context->file_context;
CliprdrFuseFile* fuse_file = fuse_request->fuse_file;
WINPR_ASSERT(file_context);
if (!should_remove_fuse_file(fuse_file, clear_context->all_files,
clear_context->has_clip_data_id, clear_context->clip_data_id))
return TRUE;
WLog_Print(file_context->log, WLOG_DEBUG, "Clearing FileContentsRequest for file \"%s\"",
fuse_file->filename_with_root);
fuse_reply_err(fuse_request->fuse_req, EIO);
HashTable_Remove(file_context->request_table, key);
free(fuse_request);
return TRUE;
}
static BOOL maybe_steal_inode(const void* key, void* value, void* arg)
{
CliprdrFuseFile* fuse_file = value;
FuseFileClearContext* clear_context = arg;
CliprdrFileContext* file_context = clear_context->file_context;
WINPR_ASSERT(file_context);
if (should_remove_fuse_file(fuse_file, clear_context->all_files,
clear_context->has_clip_data_id, clear_context->clip_data_id))
{
if (!ArrayList_Append(clear_context->fuse_files, fuse_file))
WLog_Print(file_context->log, WLOG_ERROR,
"Failed to append FUSE file to list for deletion");
HashTable_Remove(file_context->inode_table, key);
}
return TRUE;
}
static BOOL notify_delete_child(void* data, size_t index, va_list ap)
{
CliprdrFuseFile* child = data;
WINPR_ASSERT(child);
CliprdrFileContext* file_context = va_arg(ap, CliprdrFileContext*);
CliprdrFuseFile* parent = va_arg(ap, CliprdrFuseFile*);
WINPR_ASSERT(file_context);
WINPR_ASSERT(parent);
fuse_lowlevel_notify_delete(file_context->fuse_sess, parent->ino, child->ino, child->filename,
strlen(child->filename));
return TRUE;
}
static BOOL invalidate_inode(void* data, size_t index, va_list ap)
{
CliprdrFuseFile* fuse_file = data;
WINPR_ASSERT(fuse_file);
CliprdrFileContext* file_context = va_arg(ap, CliprdrFileContext*);
WINPR_ASSERT(file_context);
ArrayList_ForEach(fuse_file->children, notify_delete_child, file_context, fuse_file);
WLog_Print(file_context->log, WLOG_DEBUG, "Invalidating inode %lu for file \"%s\"",
fuse_file->ino, fuse_file->filename);
fuse_lowlevel_notify_inval_inode(file_context->fuse_sess, fuse_file->ino, 0, 0);
WLog_Print(file_context->log, WLOG_DEBUG, "Inode %lu invalidated", fuse_file->ino);
return TRUE;
}
static void clear_selection(CliprdrFileContext* file_context, BOOL all_selections,
CliprdrFuseClipDataEntry* clip_data_entry)
{
FuseFileClearContext clear_context = { 0 };
CliprdrFuseFile* root_dir;
CliprdrFuseFile* clip_data_dir = NULL;
WINPR_ASSERT(file_context);
root_dir = file_context->root_dir;
WINPR_ASSERT(root_dir);
clear_context.file_context = file_context;
clear_context.fuse_files = ArrayList_New(FALSE);
WINPR_ASSERT(clear_context.fuse_files);
wObject* aobj = ArrayList_Object(clear_context.fuse_files);
WINPR_ASSERT(aobj);
aobj->fnObjectFree = fuse_file_free;
if (clip_data_entry)
{
clip_data_dir = clip_data_entry->clip_data_dir;
clip_data_entry->clip_data_dir = NULL;
WINPR_ASSERT(clip_data_dir);
ArrayList_Remove(root_dir->children, clip_data_dir);
clear_context.has_clip_data_id = clip_data_dir->has_clip_data_id;
clear_context.clip_data_id = clip_data_dir->clip_data_id;
}
clear_context.all_files = all_selections;
if (clip_data_entry && clip_data_entry->has_clip_data_id)
WLog_Print(file_context->log, WLOG_DEBUG, "Clearing selection for clipDataId %u",
clip_data_entry->clip_data_id);
else
WLog_Print(file_context->log, WLOG_DEBUG, "Clearing selection%s",
all_selections ? "s" : "");
HashTable_Foreach(file_context->request_table, maybe_clear_fuse_request, &clear_context);
HashTable_Foreach(file_context->inode_table, maybe_steal_inode, &clear_context);
HashTable_Unlock(file_context->inode_table);
/*
* fuse_lowlevel_notify_inval_inode() is a blocking operation. If we receive a
* FUSE request (e.g. read()), then FUSE would block in read(), since the
* mutex of the inode_table would still be locked, if we wouldn't unlock it
* here.
* So, to avoid a deadlock here, unlock the mutex and reply all incoming
* operations with -ENOENT until the invalidation process is complete.
*/
ArrayList_ForEach(clear_context.fuse_files, invalidate_inode, file_context);
if (clip_data_dir)
fuse_lowlevel_notify_delete(file_context->fuse_sess, file_context->root_dir->ino,
clip_data_dir->ino, clip_data_dir->filename,
strlen(clip_data_dir->filename));
ArrayList_Free(clear_context.fuse_files);
HashTable_Lock(file_context->inode_table);
if (clip_data_entry && clip_data_entry->has_clip_data_id)
WLog_Print(file_context->log, WLOG_DEBUG, "Selection cleared for clipDataId %u",
clip_data_entry->clip_data_id);
else
WLog_Print(file_context->log, WLOG_DEBUG, "Selection%s cleared", all_selections ? "s" : "");
}
static void clear_entry_selection(CliprdrFuseClipDataEntry* clip_data_entry)
{
WINPR_ASSERT(clip_data_entry);
if (!clip_data_entry->clip_data_dir)
return;
clear_selection(clip_data_entry->file_context, FALSE, clip_data_entry);
}
static void clear_no_cdi_entry(CliprdrFileContext* file_context)
{
WINPR_ASSERT(file_context);
if (!file_context->clip_data_entry_without_id)
return;
WINPR_ASSERT(file_context->inode_table);
HashTable_Lock(file_context->inode_table);
clear_entry_selection(file_context->clip_data_entry_without_id);
clip_data_entry_free(file_context->clip_data_entry_without_id);
file_context->clip_data_entry_without_id = NULL;
HashTable_Unlock(file_context->inode_table);
}
static BOOL clear_clip_data_entries(const void* key, void* value, void* arg)
{
clear_entry_selection(value);
return TRUE;
}
static void clear_cdi_entries(CliprdrFileContext* file_context)
{
WINPR_ASSERT(file_context);
HashTable_Lock(file_context->inode_table);
HashTable_Foreach(file_context->clip_data_table, clear_clip_data_entries, NULL);
HashTable_Clear(file_context->clip_data_table);
HashTable_Unlock(file_context->inode_table);
}
UINT prepare_clip_data_entry_with_id(CliprdrFileContext* file_context)
{
CliprdrFuseClipDataEntry* clip_data_entry;
WINPR_ASSERT(file_context);
clip_data_entry = clip_data_entry_new(file_context, TRUE);
if (!clip_data_entry)
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to create clipDataEntry");
return ERROR_INTERNAL_ERROR;
}
HashTable_Lock(file_context->inode_table);
if (!HashTable_Insert(file_context->clip_data_table,
(void*)(UINT_PTR)clip_data_entry->clip_data_id, clip_data_entry))
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to insert clipDataEntry");
clip_data_entry_free(clip_data_entry);
return ERROR_INTERNAL_ERROR;
}
HashTable_Unlock(file_context->inode_table);
file_context->current_clip_data_id = clip_data_entry->clip_data_id;
return CHANNEL_RC_OK;
}
UINT prepare_clip_data_entry_without_id(CliprdrFileContext* file_context)
{
WINPR_ASSERT(file_context);
WINPR_ASSERT(!file_context->clip_data_entry_without_id);
file_context->clip_data_entry_without_id = clip_data_entry_new(file_context, FALSE);
if (!file_context->clip_data_entry_without_id)
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to create clipDataEntry");
return ERROR_INTERNAL_ERROR;
}
return CHANNEL_RC_OK;
}
#endif
UINT cliprdr_file_context_notify_new_server_format_list(CliprdrFileContext* file_context)
{
WINPR_ASSERT(file_context);
WINPR_ASSERT(file_context->context);
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
clear_no_cdi_entry(file_context);
/* TODO: assign timeouts to old locks instead */
clear_cdi_entries(file_context);
if (does_server_support_clipdata_locking(file_context))
return prepare_clip_data_entry_with_id(file_context);
else
return prepare_clip_data_entry_without_id(file_context);
#else
return CHANNEL_RC_OK;
#endif
}
UINT cliprdr_file_context_notify_new_client_format_list(CliprdrFileContext* file_context)
{
WINPR_ASSERT(file_context);
WINPR_ASSERT(file_context->context);
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
clear_no_cdi_entry(file_context);
/* TODO: assign timeouts to old locks instead */
clear_cdi_entries(file_context);
#endif
return CHANNEL_RC_OK;
}
static CliprdrLocalStream* cliprdr_local_stream_new(CliprdrFileContext* context, UINT32 streamID,
const char* data, size_t size);
static void cliprdr_file_session_terminate(CliprdrFileContext* file);
static BOOL local_stream_discard(const void* key, void* value, void* arg);
static void writelog(wLog* log, DWORD level, const char* fname, const char* fkt, size_t line, ...)
{
if (!WLog_IsLevelActive(log, level))
return;
va_list ap;
va_start(ap, line);
WLog_PrintMessageVA(log, WLOG_MESSAGE_TEXT, level, line, fname, fkt, ap);
va_end(ap);
}
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
static void cliprdr_file_fuse_lookup(fuse_req_t req, fuse_ino_t parent, const char* name);
static void cliprdr_file_fuse_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi);
static void cliprdr_file_fuse_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
struct fuse_file_info* fi);
static void cliprdr_file_fuse_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
struct fuse_file_info* fi);
static void cliprdr_file_fuse_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi);
static void cliprdr_file_fuse_opendir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi);
static const struct fuse_lowlevel_ops cliprdr_file_fuse_oper = {
.lookup = cliprdr_file_fuse_lookup,
.getattr = cliprdr_file_fuse_getattr,
.readdir = cliprdr_file_fuse_readdir,
.open = cliprdr_file_fuse_open,
.read = cliprdr_file_fuse_read,
.opendir = cliprdr_file_fuse_opendir,
};
static CliprdrFuseFile* get_fuse_file_by_ino(CliprdrFileContext* file_context, fuse_ino_t fuse_ino)
{
WINPR_ASSERT(file_context);
return HashTable_GetItemValue(file_context->inode_table, (void*)(UINT_PTR)fuse_ino);
}
static CliprdrFuseFile* get_fuse_file_by_name_from_parent(CliprdrFileContext* file_context,
CliprdrFuseFile* parent, const char* name)
{
WINPR_ASSERT(file_context);
WINPR_ASSERT(parent);
for (size_t i = 0; i < ArrayList_Count(parent->children); ++i)
{
CliprdrFuseFile* child = ArrayList_GetItem(parent->children, i);
WINPR_ASSERT(child);
if (strcmp(name, child->filename) == 0)
return child;
}
WLog_Print(file_context->log, WLOG_DEBUG,
"Requested file \"%s\" in directory \"%s\" does not exist", name, parent->filename);
return NULL;
}
static CliprdrFuseRequest* cliprdr_fuse_request_new(CliprdrFileContext* file_context,
CliprdrFuseFile* fuse_file, fuse_req_t fuse_req,
FuseLowlevelOperationType operation_type)
{
CliprdrFuseRequest* fuse_request;
UINT32 stream_id = file_context->next_stream_id;
WINPR_ASSERT(file_context);
WINPR_ASSERT(fuse_file);
fuse_request = calloc(1, sizeof(CliprdrFuseRequest));
if (!fuse_request)
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to allocate FUSE request for file \"%s\"",
fuse_file->filename_with_root);
return NULL;
}
fuse_request->fuse_file = fuse_file;
fuse_request->fuse_req = fuse_req;
fuse_request->operation_type = operation_type;
while (stream_id == 0 ||
HashTable_GetItemValue(file_context->request_table, (void*)(UINT_PTR)stream_id))
++stream_id;
fuse_request->stream_id = stream_id;
file_context->next_stream_id = stream_id + 1;
if (!HashTable_Insert(file_context->request_table, (void*)(UINT_PTR)fuse_request->stream_id,
fuse_request))
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to track FUSE request for file \"%s\"",
fuse_file->filename_with_root);
free(fuse_request);
return NULL;
}
return fuse_request;
}
static BOOL request_file_size_async(CliprdrFileContext* file_context, CliprdrFuseFile* fuse_file,
fuse_req_t fuse_req, FuseLowlevelOperationType operation_type)
{
CliprdrFuseRequest* fuse_request;
CLIPRDR_FILE_CONTENTS_REQUEST file_contents_request = { 0 };
WINPR_ASSERT(file_context);
WINPR_ASSERT(fuse_file);
fuse_request = cliprdr_fuse_request_new(file_context, fuse_file, fuse_req, operation_type);
if (!fuse_request)
return FALSE;
file_contents_request.common.msgType = CB_FILECONTENTS_REQUEST;
file_contents_request.streamId = fuse_request->stream_id;
file_contents_request.listIndex = fuse_file->list_idx;
file_contents_request.dwFlags = FILECONTENTS_SIZE;
file_contents_request.cbRequested = 0x8;
file_contents_request.haveClipDataId = fuse_file->has_clip_data_id;
file_contents_request.clipDataId = fuse_file->clip_data_id;
if (file_context->context->ClientFileContentsRequest(file_context->context,
&file_contents_request))
{
WLog_Print(file_context->log, WLOG_ERROR,
"Failed to send FileContentsRequest for file \"%s\"",
fuse_file->filename_with_root);
HashTable_Remove(file_context->request_table, (void*)(UINT_PTR)fuse_request->stream_id);
free(fuse_request);
return FALSE;
}
WLog_Print(file_context->log, WLOG_DEBUG,
"Requested file size for file \"%s\" with stream id %u", fuse_file->filename,
fuse_request->stream_id);
return TRUE;
}
static void write_file_attributes(CliprdrFuseFile* fuse_file, struct stat* attr)
{
memset(attr, 0, sizeof(struct stat));
if (!fuse_file)
return;
attr->st_ino = fuse_file->ino;
if (fuse_file->is_directory)
{
attr->st_mode = S_IFDIR | (fuse_file->is_readonly ? 0555 : 0755);
attr->st_nlink = 2;
}
else
{
attr->st_mode = S_IFREG | (fuse_file->is_readonly ? 0444 : 0644);
attr->st_nlink = 1;
attr->st_size = fuse_file->size;
}
attr->st_uid = getuid();
attr->st_gid = getgid();
attr->st_atime = attr->st_mtime = attr->st_ctime =
(fuse_file->has_last_write_time ? fuse_file->last_write_time_unix : time(NULL));
}
static void cliprdr_file_fuse_lookup(fuse_req_t fuse_req, fuse_ino_t parent_ino, const char* name)
{
CliprdrFileContext* file_context = fuse_req_userdata(fuse_req);
CliprdrFuseFile *parent, *fuse_file;
struct fuse_entry_param entry = { 0 };
WINPR_ASSERT(file_context);
HashTable_Lock(file_context->inode_table);
if (!(parent = get_fuse_file_by_ino(file_context, parent_ino)) || !parent->is_directory)
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_req, ENOENT);
return;
}
if (!(fuse_file = get_fuse_file_by_name_from_parent(file_context, parent, name)))
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_req, ENOENT);
return;
}
WLog_Print(file_context->log, WLOG_DEBUG, "lookup() has been called for \"%s\"", name);
WLog_Print(file_context->log, WLOG_DEBUG, "Parent is \"%s\", child is \"%s\"",
parent->filename_with_root, fuse_file->filename_with_root);
if (!fuse_file->is_directory && !fuse_file->has_size)
{
BOOL result;
result =
request_file_size_async(file_context, fuse_file, fuse_req, FUSE_LL_OPERATION_LOOKUP);
HashTable_Unlock(file_context->inode_table);
if (!result)
fuse_reply_err(fuse_req, EIO);
return;
}
entry.ino = fuse_file->ino;
write_file_attributes(fuse_file, &entry.attr);
entry.attr_timeout = 1.0;
entry.entry_timeout = 1.0;
HashTable_Unlock(file_context->inode_table);
fuse_reply_entry(fuse_req, &entry);
}
static void cliprdr_file_fuse_getattr(fuse_req_t fuse_req, fuse_ino_t fuse_ino,
struct fuse_file_info* file_info)
{
CliprdrFileContext* file_context = fuse_req_userdata(fuse_req);
CliprdrFuseFile* fuse_file;
struct stat attr = { 0 };
WINPR_ASSERT(file_context);
HashTable_Lock(file_context->inode_table);
if (!(fuse_file = get_fuse_file_by_ino(file_context, fuse_ino)))
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_req, ENOENT);
return;
}
WLog_Print(file_context->log, WLOG_DEBUG, "getattr() has been called for file \"%s\"",
fuse_file->filename_with_root);
if (!fuse_file->is_directory && !fuse_file->has_size)
{
BOOL result;
result =
request_file_size_async(file_context, fuse_file, fuse_req, FUSE_LL_OPERATION_GETATTR);
HashTable_Unlock(file_context->inode_table);
if (!result)
fuse_reply_err(fuse_req, EIO);
return;
}
write_file_attributes(fuse_file, &attr);
HashTable_Unlock(file_context->inode_table);
fuse_reply_attr(fuse_req, &attr, 1.0);
}
static void cliprdr_file_fuse_open(fuse_req_t fuse_req, fuse_ino_t fuse_ino,
struct fuse_file_info* file_info)
{
CliprdrFileContext* file_context = fuse_req_userdata(fuse_req);
CliprdrFuseFile* fuse_file;
WINPR_ASSERT(file_context);
HashTable_Lock(file_context->inode_table);
if (!(fuse_file = get_fuse_file_by_ino(file_context, fuse_ino)))
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_req, ENOENT);
return;
}
if (fuse_file->is_directory)
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_req, EISDIR);
return;
}
HashTable_Unlock(file_context->inode_table);
if ((file_info->flags & O_ACCMODE) != O_RDONLY)
{
fuse_reply_err(fuse_req, EACCES);
return;
}
/* Important for KDE to get file correctly */
file_info->direct_io = 1;
fuse_reply_open(fuse_req, file_info);
}
static BOOL request_file_range_async(CliprdrFileContext* file_context, CliprdrFuseFile* fuse_file,
fuse_req_t fuse_req, off_t offset, size_t requested_size)
{
CliprdrFuseRequest* fuse_request;
CLIPRDR_FILE_CONTENTS_REQUEST file_contents_request = { 0 };
WINPR_ASSERT(file_context);
WINPR_ASSERT(fuse_file);
fuse_request =
cliprdr_fuse_request_new(file_context, fuse_file, fuse_req, FUSE_LL_OPERATION_READ);
if (!fuse_request)
return FALSE;
file_contents_request.common.msgType = CB_FILECONTENTS_REQUEST;
file_contents_request.streamId = fuse_request->stream_id;
file_contents_request.listIndex = fuse_file->list_idx;
file_contents_request.dwFlags = FILECONTENTS_RANGE;
file_contents_request.nPositionLow = offset & 0xFFFFFFFF;
file_contents_request.nPositionHigh = offset >> 32 & 0xFFFFFFFF;
file_contents_request.cbRequested = requested_size;
file_contents_request.haveClipDataId = fuse_file->has_clip_data_id;
file_contents_request.clipDataId = fuse_file->clip_data_id;
if (file_context->context->ClientFileContentsRequest(file_context->context,
&file_contents_request))
{
WLog_Print(file_context->log, WLOG_ERROR,
"Failed to send FileContentsRequest for file \"%s\"",
fuse_file->filename_with_root);
HashTable_Remove(file_context->request_table, (void*)(UINT_PTR)fuse_request->stream_id);
free(fuse_request);
return FALSE;
}
WLog_Print(file_context->log, WLOG_DEBUG,
"Requested file range (%zu Bytes at offset %lu) for file \"%s\" with stream id %u",
requested_size, offset, fuse_file->filename, fuse_request->stream_id);
return TRUE;
}
static void cliprdr_file_fuse_read(fuse_req_t fuse_req, fuse_ino_t fuse_ino, size_t size,
off_t offset, struct fuse_file_info* file_info)
{
CliprdrFileContext* file_context = fuse_req_userdata(fuse_req);
CliprdrFuseFile* fuse_file;
BOOL result;
WINPR_ASSERT(file_context);
HashTable_Lock(file_context->inode_table);
if (!(fuse_file = get_fuse_file_by_ino(file_context, fuse_ino)))
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_req, ENOENT);
return;
}
if (fuse_file->is_directory)
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_req, EISDIR);
return;
}
if (!fuse_file->has_size || offset > fuse_file->size)
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_req, EINVAL);
return;
}
size = MIN(size, 8 * 1024 * 1024);
result = request_file_range_async(file_context, fuse_file, fuse_req, offset, size);
HashTable_Unlock(file_context->inode_table);
if (!result)
fuse_reply_err(fuse_req, EIO);
}
static void cliprdr_file_fuse_opendir(fuse_req_t fuse_req, fuse_ino_t fuse_ino,
struct fuse_file_info* file_info)
{
CliprdrFileContext* file_context = fuse_req_userdata(fuse_req);
CliprdrFuseFile* fuse_file;
WINPR_ASSERT(file_context);
HashTable_Lock(file_context->inode_table);
if (!(fuse_file = get_fuse_file_by_ino(file_context, fuse_ino)))
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_req, ENOENT);
return;
}
if (!fuse_file->is_directory)
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_req, ENOTDIR);
return;
}
HashTable_Unlock(file_context->inode_table);
if ((file_info->flags & O_ACCMODE) != O_RDONLY)
{
fuse_reply_err(fuse_req, EACCES);
return;
}
fuse_reply_open(fuse_req, file_info);
}
static void cliprdr_file_fuse_readdir(fuse_req_t fuse_req, fuse_ino_t fuse_ino, size_t max_size,
off_t offset, struct fuse_file_info* file_info)
{
CliprdrFileContext* file_context = fuse_req_userdata(fuse_req);
CliprdrFuseFile *fuse_file, *child;
struct stat attr = { 0 };
size_t written_size, entry_size;
char* filename;
char* buf;
off_t i;
size_t j;
WINPR_ASSERT(file_context);
HashTable_Lock(file_context->inode_table);
if (!(fuse_file = get_fuse_file_by_ino(file_context, fuse_ino)))
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_req, ENOENT);
return;
}
if (!fuse_file->is_directory)
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_req, ENOTDIR);
return;
}
WLog_Print(file_context->log, WLOG_DEBUG, "Reading directory \"%s\" at offset %lu",
fuse_file->filename_with_root, offset);
if (offset >= ArrayList_Count(fuse_file->children))
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_buf(fuse_req, NULL, 0);
return;
}
buf = calloc(max_size, sizeof(char));
if (!buf)
{
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_req, ENOMEM);
return;
}
written_size = 0;
for (i = offset; i < 2; ++i)
{
if (i == 0)
{
write_file_attributes(fuse_file, &attr);
filename = ".";
}
else if (i == 1)
{
write_file_attributes(fuse_file->parent, &attr);
attr.st_ino = fuse_file->parent ? attr.st_ino : FUSE_ROOT_ID;
attr.st_mode = fuse_file->parent ? attr.st_mode : 0555;
filename = "..";
}
else
{
WINPR_ASSERT(FALSE);
}
/**
* buf needs to be large enough to hold the entry. If it's not, then the
* entry is not filled in but the size of the entry is still returned.
*/
entry_size = fuse_add_direntry(fuse_req, buf + written_size, max_size - written_size,
filename, &attr, i + 1);
if (entry_size > max_size - written_size)
break;
written_size += entry_size;
}
for (j = 0, i = 2; j < ArrayList_Count(fuse_file->children); ++j, ++i)
{
if (i < offset)
continue;
child = ArrayList_GetItem(fuse_file->children, j);
write_file_attributes(child, &attr);
entry_size = fuse_add_direntry(fuse_req, buf + written_size, max_size - written_size,
child->filename, &attr, i + 1);
if (entry_size > max_size - written_size)
break;
written_size += entry_size;
}
HashTable_Unlock(file_context->inode_table);
fuse_reply_buf(fuse_req, buf, written_size);
free(buf);
}
static void fuse_abort(int sig, const char* signame, void* context)
{
CliprdrFileContext* file = (CliprdrFileContext*)context;
if (file)
{
WLog_Print(file->log, WLOG_INFO, "signal %s [%d] aborting session", signame, sig);
cliprdr_file_session_terminate(file);
}
}
static DWORD WINAPI cliprdr_file_fuse_thread(LPVOID arg)
{
CliprdrFileContext* file = (CliprdrFileContext*)arg;
WINPR_ASSERT(file);
DEBUG_CLIPRDR(file->log, "Starting fuse with mountpoint '%s'", file->path);
struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
#if FUSE_USE_VERSION >= 30
fuse_opt_add_arg(&args, file->path);
if ((file->fuse_sess = fuse_session_new(&args, &cliprdr_file_fuse_oper,
sizeof(cliprdr_file_fuse_oper), (void*)file)) != NULL)
{
freerdp_add_signal_cleanup_handler(file, fuse_abort);
if (0 == fuse_session_mount(file->fuse_sess, file->path))
{
fuse_session_loop(file->fuse_sess);
fuse_session_unmount(file->fuse_sess);
}
freerdp_del_signal_cleanup_handler(file, fuse_abort);
fuse_session_destroy(file->fuse_sess);
}
#else
struct fuse_chan* ch = fuse_mount(file->path, &args);
if (ch != NULL)
{
file->fuse_sess = fuse_lowlevel_new(&args, &cliprdr_file_fuse_oper,
sizeof(cliprdr_file_fuse_oper), (void*)file);
if (file->fuse_sess != NULL)
{
freerdp_add_signal_cleanup_handler(file, fuse_abort);
fuse_session_add_chan(file->fuse_sess, ch);
const int err = fuse_session_loop(file->fuse_sess);
if (err != 0)
WLog_Print(file->log, WLOG_WARN, "fuse_session_loop failed with %d", err);
fuse_session_remove_chan(ch);
freerdp_del_signal_cleanup_handler(file, fuse_abort);
fuse_session_destroy(file->fuse_sess);
}
fuse_unmount(file->path, ch);
}
#endif
fuse_opt_free_args(&args);
DEBUG_CLIPRDR(file->log, "Quitting fuse with mountpoint '%s'", file->path);
ExitThread(0);
return 0;
}
static UINT cliprdr_file_context_server_file_contents_response(
CliprdrClientContext* cliprdr_context,
const CLIPRDR_FILE_CONTENTS_RESPONSE* file_contents_response)
{
CliprdrFileContext* file_context;
CliprdrFuseRequest* fuse_request;
struct fuse_entry_param entry = { 0 };
WINPR_ASSERT(cliprdr_context);
WINPR_ASSERT(file_contents_response);
file_context = cliprdr_context->custom;
WINPR_ASSERT(file_context);
HashTable_Lock(file_context->inode_table);
fuse_request = HashTable_GetItemValue(file_context->request_table,
(void*)(UINT_PTR)file_contents_response->streamId);
if (!fuse_request)
{
HashTable_Unlock(file_context->inode_table);
return CHANNEL_RC_OK;
}
HashTable_Remove(file_context->request_table,
(void*)(UINT_PTR)file_contents_response->streamId);
if (!(file_contents_response->common.msgFlags & CB_RESPONSE_OK))
{
WLog_Print(file_context->log, WLOG_WARN,
"FileContentsRequests for file \"%s\" was unsuccessful",
fuse_request->fuse_file->filename);
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_request->fuse_req, EIO);
free(fuse_request);
return CHANNEL_RC_OK;
}
if ((fuse_request->operation_type == FUSE_LL_OPERATION_LOOKUP ||
fuse_request->operation_type == FUSE_LL_OPERATION_GETATTR) &&
file_contents_response->cbRequested < sizeof(UINT64))
{
WLog_Print(file_context->log, WLOG_WARN,
"Received invalid file size for file \"%s\" from the client",
fuse_request->fuse_file->filename);
HashTable_Unlock(file_context->inode_table);
fuse_reply_err(fuse_request->fuse_req, EIO);
free(fuse_request);
return CHANNEL_RC_OK;
}
if (fuse_request->operation_type == FUSE_LL_OPERATION_LOOKUP ||
fuse_request->operation_type == FUSE_LL_OPERATION_GETATTR)
{
WLog_Print(file_context->log, WLOG_DEBUG,
"Received file size for file \"%s\" with stream id %u",
fuse_request->fuse_file->filename, file_contents_response->streamId);
fuse_request->fuse_file->size = *((UINT64*)file_contents_response->requestedData);
fuse_request->fuse_file->has_size = TRUE;
entry.ino = fuse_request->fuse_file->ino;
write_file_attributes(fuse_request->fuse_file, &entry.attr);
entry.attr_timeout = 1.0;
entry.entry_timeout = 1.0;
}
else if (fuse_request->operation_type == FUSE_LL_OPERATION_READ)
{
WLog_Print(file_context->log, WLOG_DEBUG,
"Received file range for file \"%s\" with stream id %u",
fuse_request->fuse_file->filename, file_contents_response->streamId);
}
HashTable_Unlock(file_context->inode_table);
switch (fuse_request->operation_type)
{
case FUSE_LL_OPERATION_NONE:
break;
case FUSE_LL_OPERATION_LOOKUP:
fuse_reply_entry(fuse_request->fuse_req, &entry);
break;
case FUSE_LL_OPERATION_GETATTR:
fuse_reply_attr(fuse_request->fuse_req, &entry.attr, entry.attr_timeout);
break;
case FUSE_LL_OPERATION_READ:
fuse_reply_buf(fuse_request->fuse_req,
(const char*)file_contents_response->requestedData,
file_contents_response->cbRequested);
break;
}
free(fuse_request);
return CHANNEL_RC_OK;
}
#endif
static UINT cliprdr_file_context_send_file_contents_failure(
CliprdrFileContext* file, const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest)
{
CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 };
WINPR_ASSERT(file);
WINPR_ASSERT(fileContentsRequest);
const UINT64 offset = (((UINT64)fileContentsRequest->nPositionHigh) << 32) |
((UINT64)fileContentsRequest->nPositionLow);
writelog(file->log, WLOG_WARN, __FILE__, __FUNCTION__, __LINE__,
"server file contents request [lockID %" PRIu32 ", streamID %" PRIu32
", index %" PRIu32 "] offset %" PRIu64 ", size %" PRIu32 " failed",
fileContentsRequest->clipDataId, fileContentsRequest->streamId,
fileContentsRequest->listIndex, offset, fileContentsRequest->cbRequested);
response.common.msgFlags = CB_RESPONSE_FAIL;
response.streamId = fileContentsRequest->streamId;
WINPR_ASSERT(file->context);
WINPR_ASSERT(file->context->ClientFileContentsResponse);
return file->context->ClientFileContentsResponse(file->context, &response);
}
static UINT
cliprdr_file_context_send_contents_response(CliprdrFileContext* file,
const CLIPRDR_FILE_CONTENTS_REQUEST* request,
const void* data, size_t size)
{
CLIPRDR_FILE_CONTENTS_RESPONSE response = { .streamId = request->streamId,
.requestedData = data,
.cbRequested = size,
.common.msgFlags = CB_RESPONSE_OK };
WINPR_ASSERT(request);
WINPR_ASSERT(file);
WLog_Print(file->log, WLOG_DEBUG, "send contents response streamID=%" PRIu32 ", size=%" PRIu32,
response.streamId, response.cbRequested);
WINPR_ASSERT(file->context);
WINPR_ASSERT(file->context->ClientFileContentsResponse);
return file->context->ClientFileContentsResponse(file->context, &response);
}
static BOOL dump_streams(const void* key, void* value, void* arg)
{
const UINT32* ukey = key;
CliprdrLocalStream* cur = value;
writelog(cur->context->log, WLOG_WARN, __FILE__, __FUNCTION__, __LINE__,
"[key %" PRIu32 "] lockID %" PRIu32 ", count %" PRIuz ", locked %d", *ukey,
cur->lockId, cur->count, cur->locked);
for (size_t x = 0; x < cur->count; x++)
{
const CliprdrLocalFile* file = &cur->files[x];
writelog(cur->context->log, WLOG_WARN, __FILE__, __FUNCTION__, __LINE__,
"file [%" PRIuz "] ", x, file->name, file->size);
}
return TRUE;
}
static CliprdrLocalFile* file_info_for_request(CliprdrFileContext* file, UINT32 lockId,
UINT32 listIndex)
{
WINPR_ASSERT(file);
CliprdrLocalStream* cur = HashTable_GetItemValue(file->local_streams, &lockId);
if (cur)
{
if (listIndex < cur->count)
{
CliprdrLocalFile* f = &cur->files[listIndex];
return f;
}
else
{
writelog(file->log, WLOG_WARN, __FILE__, __FUNCTION__, __LINE__,
"invalid entry index for lockID %" PRIu32 ", index %" PRIu32 " [count %" PRIu32
"] [locked %d]",
lockId, listIndex, cur->count, cur->locked);
}
}
else
{
writelog(file->log, WLOG_WARN, __FILE__, __FUNCTION__, __LINE__,
"missing entry for lockID %" PRIu32 ", index %" PRIu32, lockId, listIndex);
HashTable_Foreach(file->local_streams, dump_streams, file);
}
return NULL;
}
static CliprdrLocalFile* file_for_request(CliprdrFileContext* file, UINT32 lockId, UINT32 listIndex)
{
CliprdrLocalFile* f = file_info_for_request(file, lockId, listIndex);
if (f)
{
if (!f->fp)
{
const char* name = f->name;
f->fp = winpr_fopen(name, "rb");
}
if (!f->fp)
{
writelog(file->log, WLOG_WARN, __FILE__, __FUNCTION__, __LINE__,
"[lockID %" PRIu32 ", index %" PRIu32
"] failed to open file '%s' [size %" PRId64 "] %s [%d]",
lockId, listIndex, f->name, f->size, strerror(errno), errno);
return NULL;
}
}
return f;
}
static void cliprdr_local_file_try_close(CliprdrLocalFile* file, UINT res, UINT64 offset,
UINT64 size)
{
WINPR_ASSERT(file);
if (res != 0)
{
WINPR_ASSERT(file->context);
WLog_Print(file->context->log, WLOG_DEBUG, "closing file %s after error %" PRIu32,
file->name, res);
}
else if (((file->size > 0) && (offset + size >= file->size)))
{
WINPR_ASSERT(file->context);
WLog_Print(file->context->log, WLOG_DEBUG, "closing file %s after read", file->name);
}
else
{
// TODO: we need to keep track of open files to avoid running out of file descriptors
// TODO: for the time being just close again.
}
if (file->fp)
fclose(file->fp);
file->fp = NULL;
}
static UINT cliprdr_file_context_server_file_size_request(
CliprdrFileContext* file, const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest)
{
WINPR_ASSERT(fileContentsRequest);
if (fileContentsRequest->cbRequested != sizeof(UINT64))
{
WLog_Print(file->log, WLOG_WARN, "unexpected FILECONTENTS_SIZE request: %" PRIu32 " bytes",
fileContentsRequest->cbRequested);
}
HashTable_Lock(file->local_streams);
UINT res = CHANNEL_RC_OK;
CliprdrLocalFile* rfile =
file_for_request(file, fileContentsRequest->clipDataId, fileContentsRequest->listIndex);
if (!rfile)
res = cliprdr_file_context_send_file_contents_failure(file, fileContentsRequest);
else
{
if (_fseeki64(rfile->fp, 0, SEEK_END) < 0)
res = cliprdr_file_context_send_file_contents_failure(file, fileContentsRequest);
else
{
const INT64 size = _ftelli64(rfile->fp);
rfile->size = size;
cliprdr_local_file_try_close(rfile, res, 0, 0);
res = cliprdr_file_context_send_contents_response(file, fileContentsRequest, &size,
sizeof(size));
}
}
HashTable_Unlock(file->local_streams);
return res;
}
static UINT cliprdr_file_context_server_file_range_request(
CliprdrFileContext* file, const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest)
{
BYTE* data = NULL;
WINPR_ASSERT(fileContentsRequest);
HashTable_Lock(file->local_streams);
const UINT64 offset = (((UINT64)fileContentsRequest->nPositionHigh) << 32) |
((UINT64)fileContentsRequest->nPositionLow);
CliprdrLocalFile* rfile =
file_for_request(file, fileContentsRequest->clipDataId, fileContentsRequest->listIndex);
if (!rfile)
goto fail;
if (_fseeki64(rfile->fp, offset, SEEK_SET) < 0)
goto fail;
data = malloc(fileContentsRequest->cbRequested);
if (!data)
goto fail;
const size_t r = fread(data, 1, fileContentsRequest->cbRequested, rfile->fp);
const UINT rc = cliprdr_file_context_send_contents_response(file, fileContentsRequest, data, r);
free(data);
cliprdr_local_file_try_close(rfile, rc, offset, fileContentsRequest->cbRequested);
HashTable_Unlock(file->local_streams);
return rc;
fail:
if (rfile)
cliprdr_local_file_try_close(rfile, ERROR_INTERNAL_ERROR, offset,
fileContentsRequest->cbRequested);
free(data);
HashTable_Unlock(file->local_streams);
return cliprdr_file_context_send_file_contents_failure(file, fileContentsRequest);
}
static UINT change_lock(CliprdrFileContext* file, UINT32 lockId, BOOL lock)
{
WINPR_ASSERT(file);
HashTable_Lock(file->local_streams);
CliprdrLocalStream* stream = HashTable_GetItemValue(file->local_streams, &lockId);
if (lock && !stream)
{
stream = cliprdr_local_stream_new(file, lockId, NULL, 0);
HashTable_Insert(file->local_streams, &lockId, stream);
file->local_lock_id = lockId;
}
if (stream)
{
stream->locked = lock;
stream->lockId = lockId;
}
if (!lock)
HashTable_Foreach(file->local_streams, local_stream_discard, file);
HashTable_Unlock(file->local_streams);
return CHANNEL_RC_OK;
}
static UINT cliprdr_file_context_lock(CliprdrClientContext* context,
const CLIPRDR_LOCK_CLIPBOARD_DATA* lockClipboardData)
{
WINPR_ASSERT(context);
WINPR_ASSERT(lockClipboardData);
CliprdrFileContext* file = (context->custom);
return change_lock(file, lockClipboardData->clipDataId, TRUE);
}
static UINT cliprdr_file_context_unlock(CliprdrClientContext* context,
const CLIPRDR_UNLOCK_CLIPBOARD_DATA* unlockClipboardData)
{
WINPR_ASSERT(context);
WINPR_ASSERT(unlockClipboardData);
CliprdrFileContext* file = (context->custom);
return change_lock(file, unlockClipboardData->clipDataId, FALSE);
}
static UINT cliprdr_file_context_server_file_contents_request(
CliprdrClientContext* context, const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest)
{
UINT error = NO_ERROR;
WINPR_ASSERT(context);
WINPR_ASSERT(fileContentsRequest);
CliprdrFileContext* file = (context->custom);
WINPR_ASSERT(file);
/*
* MS-RDPECLIP 2.2.5.3 File Contents Request PDU (CLIPRDR_FILECONTENTS_REQUEST):
* The FILECONTENTS_SIZE and FILECONTENTS_RANGE flags MUST NOT be set at the same time.
*/
if ((fileContentsRequest->dwFlags & (FILECONTENTS_SIZE | FILECONTENTS_RANGE)) ==
(FILECONTENTS_SIZE | FILECONTENTS_RANGE))
{
WLog_Print(file->log, WLOG_ERROR, "invalid CLIPRDR_FILECONTENTS_REQUEST.dwFlags");
return cliprdr_file_context_send_file_contents_failure(file, fileContentsRequest);
}
if (fileContentsRequest->dwFlags & FILECONTENTS_SIZE)
error = cliprdr_file_context_server_file_size_request(file, fileContentsRequest);
if (fileContentsRequest->dwFlags & FILECONTENTS_RANGE)
error = cliprdr_file_context_server_file_range_request(file, fileContentsRequest);
if (error)
{
WLog_Print(file->log, WLOG_ERROR, "failed to handle CLIPRDR_FILECONTENTS_REQUEST: 0x%08X",
error);
return cliprdr_file_context_send_file_contents_failure(file, fileContentsRequest);
}
return CHANNEL_RC_OK;
}
static BOOL xf_cliprdr_clipboard_is_valid_unix_filename(LPCWSTR filename)
{
LPCWSTR c;
if (!filename)
return FALSE;
if (filename[0] == L'\0')
return FALSE;
/* Reserved characters */
for (c = filename; *c; ++c)
{
if (*c == L'/')
return FALSE;
}
return TRUE;
}
BOOL cliprdr_file_context_init(CliprdrFileContext* file, CliprdrClientContext* cliprdr)
{
WINPR_ASSERT(file);
WINPR_ASSERT(cliprdr);
cliprdr->custom = file;
file->context = cliprdr;
cliprdr->ServerLockClipboardData = cliprdr_file_context_lock;
cliprdr->ServerUnlockClipboardData = cliprdr_file_context_unlock;
cliprdr->ServerFileContentsRequest = cliprdr_file_context_server_file_contents_request;
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
cliprdr->ServerFileContentsResponse = cliprdr_file_context_server_file_contents_response;
#endif
return TRUE;
}
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
static void clear_all_selections(CliprdrFileContext* file_context)
{
WINPR_ASSERT(file_context);
WINPR_ASSERT(file_context->inode_table);
HashTable_Lock(file_context->inode_table);
clear_selection(file_context, TRUE, NULL);
HashTable_Clear(file_context->clip_data_table);
HashTable_Unlock(file_context->inode_table);
}
#endif
BOOL cliprdr_file_context_uninit(CliprdrFileContext* file, CliprdrClientContext* cliprdr)
{
WINPR_ASSERT(file);
WINPR_ASSERT(cliprdr);
// Clear all data before the channel is closed
// the cleanup handlers are dependent on a working channel.
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
if (file->inode_table)
{
clear_no_cdi_entry(file);
clear_all_selections(file);
}
#endif
HashTable_Clear(file->local_streams);
file->context = NULL;
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
cliprdr->ServerFileContentsResponse = NULL;
#endif
return TRUE;
}
static BOOL cliprdr_file_content_changed_and_update(void* ihash, size_t hsize, const void* data,
size_t size)
{
BYTE hash[WINPR_SHA256_DIGEST_LENGTH] = { 0 };
if (hsize < sizeof(hash))
return FALSE;
if (!winpr_Digest(WINPR_MD_SHA256, data, size, hash, sizeof(hash)))
return FALSE;
const BOOL changed = memcmp(hash, ihash, sizeof(hash)) != 0;
if (changed)
memcpy(ihash, hash, sizeof(hash));
return changed;
}
static BOOL cliprdr_file_server_content_changed_and_update(CliprdrFileContext* file,
const void* data, size_t size)
{
WINPR_ASSERT(file);
return cliprdr_file_content_changed_and_update(file->server_data_hash,
sizeof(file->server_data_hash), data, size);
}
static BOOL cliprdr_file_client_content_changed_and_update(CliprdrFileContext* file,
const void* data, size_t size)
{
WINPR_ASSERT(file);
return cliprdr_file_content_changed_and_update(file->client_data_hash,
sizeof(file->client_data_hash), data, size);
}
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
static fuse_ino_t get_next_free_inode(CliprdrFileContext* file_context)
{
fuse_ino_t ino;
WINPR_ASSERT(file_context);
ino = file_context->next_ino;
while (ino == 0 || ino == FUSE_ROOT_ID ||
HashTable_GetItemValue(file_context->inode_table, (void*)(UINT_PTR)ino))
++ino;
file_context->next_ino = ino + 1;
return ino;
}
static CliprdrFuseFile* clip_data_dir_new(CliprdrFileContext* file_context, BOOL has_clip_data_id,
UINT32 clip_data_id)
{
CliprdrFuseFile* root_dir;
CliprdrFuseFile* clip_data_dir;
size_t path_length;
WINPR_ASSERT(file_context);
clip_data_dir = fuse_file_new();
if (!clip_data_dir)
return NULL;
path_length = 1 + MAX_CLIP_DATA_DIR_LEN + 1;
clip_data_dir->filename_with_root = calloc(path_length, sizeof(char));
if (!clip_data_dir->filename_with_root)
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to allocate filename");
fuse_file_free(clip_data_dir);
return NULL;
}
if (has_clip_data_id)
_snprintf(clip_data_dir->filename_with_root, path_length, "/%u", clip_data_id);
else
_snprintf(clip_data_dir->filename_with_root, path_length, "/%lu", NO_CLIP_DATA_ID);
clip_data_dir->filename = strrchr(clip_data_dir->filename_with_root, '/') + 1;
clip_data_dir->ino = get_next_free_inode(file_context);
clip_data_dir->is_directory = TRUE;
clip_data_dir->is_readonly = TRUE;
clip_data_dir->has_clip_data_id = has_clip_data_id;
clip_data_dir->clip_data_id = clip_data_id;
root_dir = file_context->root_dir;
if (!ArrayList_Append(root_dir->children, clip_data_dir))
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to append FUSE file");
fuse_file_free(clip_data_dir);
return NULL;
}
clip_data_dir->parent = root_dir;
if (!HashTable_Insert(file_context->inode_table, (void*)(UINT_PTR)clip_data_dir->ino,
clip_data_dir))
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to insert inode into inode table");
ArrayList_Remove(root_dir->children, clip_data_dir);
fuse_file_free(clip_data_dir);
return NULL;
}
return clip_data_dir;
}
static char* get_parent_path(const char* filepath)
{
char* base;
size_t parent_path_length;
char* parent_path;
base = strrchr(filepath, '/');
WINPR_ASSERT(base);
while (base > filepath && *base == '/')
--base;
parent_path_length = 1 + base - filepath;
parent_path = calloc(parent_path_length + 1, sizeof(char));
if (!parent_path)
return NULL;
memcpy(parent_path, filepath, parent_path_length);
return parent_path;
}
static BOOL is_fuse_file_not_parent(const void* key, void* value, void* arg)
{
CliprdrFuseFile* fuse_file = value;
CliprdrFuseFindParentContext* find_context = arg;
if (!fuse_file->is_directory)
return TRUE;
if (strcmp(find_context->parent_path, fuse_file->filename_with_root) == 0)
{
find_context->parent = fuse_file;
return FALSE;
}
return TRUE;
}
static CliprdrFuseFile* get_parent_directory(CliprdrFileContext* file_context, const char* path)
{
CliprdrFuseFindParentContext find_context = { 0 };
WINPR_ASSERT(file_context);
WINPR_ASSERT(path);
find_context.parent_path = get_parent_path(path);
if (!find_context.parent_path)
return NULL;
WINPR_ASSERT(!find_context.parent);
if (HashTable_Foreach(file_context->inode_table, is_fuse_file_not_parent, &find_context))
{
free(find_context.parent_path);
return NULL;
}
WINPR_ASSERT(find_context.parent);
free(find_context.parent_path);
return find_context.parent;
}
static BOOL set_selection_for_clip_data_entry(CliprdrFileContext* file_context,
CliprdrFuseClipDataEntry* clip_data_entry,
FILEDESCRIPTORW* files, UINT32 n_files)
{
CliprdrFuseFile* clip_data_dir;
WINPR_ASSERT(file_context);
WINPR_ASSERT(clip_data_entry);
WINPR_ASSERT(files);
clip_data_dir = clip_data_entry->clip_data_dir;
WINPR_ASSERT(clip_data_dir);
if (clip_data_entry->has_clip_data_id)
WLog_Print(file_context->log, WLOG_DEBUG, "Setting selection for clipDataId %u",
clip_data_entry->clip_data_id);
else
WLog_Print(file_context->log, WLOG_DEBUG, "Setting selection");
for (UINT32 i = 0; i < n_files; ++i)
{
FILEDESCRIPTORW* file = &files[i];
CliprdrFuseFile* fuse_file;
char* filename;
size_t path_length;
fuse_file = fuse_file_new();
if (!fuse_file)
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to create FUSE file");
clear_entry_selection(clip_data_entry);
return FALSE;
}
filename = ConvertWCharToUtf8Alloc(file->cFileName, NULL);
if (!filename)
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to convert filename");
fuse_file_free(fuse_file);
clear_entry_selection(clip_data_entry);
return FALSE;
}
for (size_t j = 0; filename[j]; ++j)
{
if (filename[j] == '\\')
filename[j] = '/';
}
path_length = strlen(clip_data_dir->filename_with_root) + 1 + strlen(filename) + 1;
fuse_file->filename_with_root = calloc(path_length, sizeof(char));
if (!fuse_file->filename_with_root)
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to allocate filename");
free(filename);
fuse_file_free(fuse_file);
clear_entry_selection(clip_data_entry);
return FALSE;
}
_snprintf(fuse_file->filename_with_root, path_length, "%s/%s",
clip_data_dir->filename_with_root, filename);
free(filename);
fuse_file->filename = strrchr(fuse_file->filename_with_root, '/') + 1;
fuse_file->parent = get_parent_directory(file_context, fuse_file->filename_with_root);
if (!fuse_file->parent)
{
WLog_Print(file_context->log, WLOG_ERROR, "Found no parent for FUSE file");
fuse_file_free(fuse_file);
clear_entry_selection(clip_data_entry);
return FALSE;
}
if (!ArrayList_Append(fuse_file->parent->children, fuse_file))
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to append FUSE file");
fuse_file_free(fuse_file);
clear_entry_selection(clip_data_entry);
return FALSE;
}
fuse_file->list_idx = i;
fuse_file->ino = get_next_free_inode(file_context);
fuse_file->has_clip_data_id = clip_data_entry->has_clip_data_id;
fuse_file->clip_data_id = clip_data_entry->clip_data_id;
if (file->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
fuse_file->is_directory = TRUE;
if (file->dwFileAttributes & FILE_ATTRIBUTE_READONLY)
fuse_file->is_readonly = TRUE;
if (file->dwFlags & FD_FILESIZE)
{
fuse_file->size = ((UINT64)file->nFileSizeHigh << 32) + file->nFileSizeLow;
fuse_file->has_size = TRUE;
}
if (file->dwFlags & FD_WRITESTIME)
{
UINT64 filetime;
filetime = file->ftLastWriteTime.dwHighDateTime;
filetime <<= 32;
filetime += file->ftLastWriteTime.dwLowDateTime;
fuse_file->last_write_time_unix =
filetime / (10 * 1000 * 1000) - WIN32_FILETIME_TO_UNIX_EPOCH;
fuse_file->has_last_write_time = TRUE;
}
if (!HashTable_Insert(file_context->inode_table, (void*)(UINT_PTR)fuse_file->ino,
fuse_file))
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to insert inode into inode table");
fuse_file_free(fuse_file);
clear_entry_selection(clip_data_entry);
return FALSE;
}
}
if (clip_data_entry->has_clip_data_id)
WLog_Print(file_context->log, WLOG_DEBUG, "Selection set for clipDataId %u",
clip_data_entry->clip_data_id);
else
WLog_Print(file_context->log, WLOG_DEBUG, "Selection set");
return TRUE;
}
static BOOL update_exposed_path(CliprdrFileContext* file_context, wClipboard* clip,
CliprdrFuseClipDataEntry* clip_data_entry)
{
wClipboardDelegate* delegate;
CliprdrFuseFile* clip_data_dir;
WINPR_ASSERT(file_context);
WINPR_ASSERT(clip);
WINPR_ASSERT(clip_data_entry);
delegate = ClipboardGetDelegate(clip);
WINPR_ASSERT(delegate);
clip_data_dir = clip_data_entry->clip_data_dir;
WINPR_ASSERT(clip_data_dir);
free(file_context->exposed_path);
file_context->exposed_path = GetCombinedPath(file_context->path, clip_data_dir->filename);
if (file_context->exposed_path)
WLog_Print(file_context->log, WLOG_DEBUG, "Updated exposed path to \"%s\"",
file_context->exposed_path);
delegate->basePath = file_context->exposed_path;
return delegate->basePath != NULL;
}
#endif
BOOL cliprdr_file_context_update_server_data(CliprdrFileContext* file_context, wClipboard* clip,
const void* data, size_t size)
{
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
CliprdrFuseClipDataEntry* clip_data_entry;
FILEDESCRIPTORW* files = NULL;
UINT32 n_files = 0;
WINPR_ASSERT(file_context);
WINPR_ASSERT(clip);
if (cliprdr_parse_file_list(data, size, &files, &n_files))
{
WLog_Print(file_context->log, WLOG_ERROR, "Failed to parse file list");
return FALSE;
}
HashTable_Lock(file_context->inode_table);
if (does_server_support_clipdata_locking(file_context))
clip_data_entry = HashTable_GetItemValue(
file_context->clip_data_table, (void*)(UINT_PTR)file_context->current_clip_data_id);
else
clip_data_entry = file_context->clip_data_entry_without_id;
WINPR_ASSERT(clip_data_entry);
clear_entry_selection(clip_data_entry);
WINPR_ASSERT(!clip_data_entry->clip_data_dir);
clip_data_entry->clip_data_dir =
clip_data_dir_new(file_context, does_server_support_clipdata_locking(file_context),
file_context->current_clip_data_id);
if (!clip_data_entry->clip_data_dir)
{
HashTable_Unlock(file_context->inode_table);
free(files);
return FALSE;
}
if (!update_exposed_path(file_context, clip, clip_data_entry))
{
HashTable_Unlock(file_context->inode_table);
free(files);
return FALSE;
}
if (!set_selection_for_clip_data_entry(file_context, clip_data_entry, files, n_files))
{
HashTable_Unlock(file_context->inode_table);
free(files);
return FALSE;
}
HashTable_Unlock(file_context->inode_table);
return TRUE;
#else
return FALSE;
#endif
}
void* cliprdr_file_context_get_context(CliprdrFileContext* file)
{
WINPR_ASSERT(file);
return file->clipboard;
}
void cliprdr_file_session_terminate(CliprdrFileContext* file)
{
if (!file)
return;
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
if (file->fuse_sess)
fuse_session_exit(file->fuse_sess);
#endif
/* not elegant but works for umounting FUSE
fuse_chan must receive an oper buf to unblock fuse_session_receive_buf function.
*/
winpr_PathFileExists(file->path);
}
void cliprdr_file_context_free(CliprdrFileContext* file)
{
if (!file)
return;
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
if (file->inode_table)
{
clear_no_cdi_entry(file);
clear_all_selections(file);
}
if (file->fuse_thread)
{
cliprdr_file_session_terminate(file);
WaitForSingleObject(file->fuse_thread, INFINITE);
CloseHandle(file->fuse_thread);
}
HashTable_Free(file->request_table);
HashTable_Free(file->clip_data_table);
HashTable_Free(file->inode_table);
#endif
HashTable_Free(file->local_streams);
winpr_RemoveDirectory(file->path);
free(file->path);
free(file->exposed_path);
free(file);
}
static BOOL create_base_path(CliprdrFileContext* file)
{
WINPR_ASSERT(file);
char base[64] = { 0 };
_snprintf(base, sizeof(base), "com.freerdp.client.cliprdr.%" PRIu32, GetCurrentProcessId());
file->path = GetKnownSubPath(KNOWN_PATH_TEMP, base);
if (!file->path)
return FALSE;
if (!winpr_PathFileExists(file->path) && !winpr_PathMakePath(file->path, 0))
{
WLog_Print(file->log, WLOG_ERROR, "Failed to create directory '%s'", file->path);
return FALSE;
}
return TRUE;
}
static void cliprdr_local_file_free(CliprdrLocalFile* file)
{
const CliprdrLocalFile empty = { 0 };
if (!file)
return;
if (file->fp)
{
WLog_Print(file->context->log, WLOG_DEBUG, "closing file %s, discarding entry", file->name);
fclose(file->fp);
}
free(file->name);
*file = empty;
}
static BOOL cliprdr_local_file_new(CliprdrFileContext* context, CliprdrLocalFile* f,
const char* path)
{
const CliprdrLocalFile empty = { 0 };
WINPR_ASSERT(f);
WINPR_ASSERT(context);
WINPR_ASSERT(path);
*f = empty;
f->context = context;
f->name = winpr_str_url_decode(path, strlen(path));
if (!f->name)
goto fail;
return TRUE;
fail:
cliprdr_local_file_free(f);
return FALSE;
}
static void cliprdr_local_files_free(CliprdrLocalStream* stream)
{
WINPR_ASSERT(stream);
for (size_t x = 0; x < stream->count; x++)
cliprdr_local_file_free(&stream->files[x]);
free(stream->files);
stream->files = NULL;
stream->count = 0;
}
static void cliprdr_local_stream_free(void* obj)
{
CliprdrLocalStream* stream = (CliprdrLocalStream*)obj;
if (stream)
cliprdr_local_files_free(stream);
free(stream);
}
static BOOL append_entry(CliprdrLocalStream* stream, const char* path)
{
CliprdrLocalFile* tmp = realloc(stream->files, sizeof(CliprdrLocalFile) * (stream->count + 1));
if (!tmp)
return FALSE;
stream->files = tmp;
CliprdrLocalFile* f = &stream->files[stream->count++];
return cliprdr_local_file_new(stream->context, f, path);
}
static BOOL is_directory(const char* path)
{
WCHAR* wpath = ConvertUtf8ToWCharAlloc(path, NULL);
if (!wpath)
return FALSE;
HANDLE hFile =
CreateFileW(wpath, 0, FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
free(wpath);
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
BY_HANDLE_FILE_INFORMATION fileInformation = { 0 };
const BOOL status = GetFileInformationByHandle(hFile, &fileInformation);
CloseHandle(hFile);
if (!status)
return FALSE;
return (fileInformation.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? TRUE : FALSE;
}
static BOOL add_directory(CliprdrLocalStream* stream, const char* path)
{
char* wildcardpath = GetCombinedPath(path, "*");
if (!wildcardpath)
return FALSE;
WCHAR* wpath = ConvertUtf8ToWCharAlloc(wildcardpath, NULL);
free(wildcardpath);
if (!wpath)
return FALSE;
WIN32_FIND_DATAW FindFileData = { 0 };
HANDLE hFind = FindFirstFileW(wpath, &FindFileData);
free(wpath);
if (hFind == INVALID_HANDLE_VALUE)
return FALSE;
BOOL rc = FALSE;
char* next = NULL;
do
{
const WCHAR dot[] = { '.', '\0' };
const WCHAR dotdot[] = { '.', '.', '\0' };
if (_wcscmp(FindFileData.cFileName, dot) == 0)
continue;
if (_wcscmp(FindFileData.cFileName, dotdot) == 0)
continue;
char cFileName[MAX_PATH] = { 0 };
ConvertWCharNToUtf8(FindFileData.cFileName, ARRAYSIZE(FindFileData.cFileName), cFileName,
ARRAYSIZE(cFileName));
free(next);
next = GetCombinedPath(path, cFileName);
if (!next)
goto fail;
if (!append_entry(stream, next))
goto fail;
if (is_directory(next))
{
if (!add_directory(stream, next))
goto fail;
}
} while (FindNextFileW(hFind, &FindFileData));
rc = TRUE;
fail:
free(next);
FindClose(hFind);
return rc;
}
static BOOL cliprdr_local_stream_update(CliprdrLocalStream* stream, const char* data, size_t size)
{
BOOL rc = FALSE;
WINPR_ASSERT(stream);
if (size == 0)
return TRUE;
cliprdr_local_files_free(stream);
stream->files = calloc(size, sizeof(CliprdrLocalFile));
if (!stream->files)
return FALSE;
char* copy = strndup(data, size);
if (!copy)
return FALSE;
char* ptr = strtok(copy, "\r\n");
while (ptr)
{
const char* name = ptr;
if (strncmp("file:///", ptr, 8) == 0)
name = &ptr[7];
else if (strncmp("file:/", ptr, 6) == 0)
name = &ptr[5];
if (!append_entry(stream, name))
goto fail;
if (is_directory(name))
{
const BOOL res = add_directory(stream, name);
if (!res)
goto fail;
}
ptr = strtok(NULL, "\r\n");
}
rc = TRUE;
fail:
free(copy);
return rc;
}
CliprdrLocalStream* cliprdr_local_stream_new(CliprdrFileContext* context, UINT32 lockId,
const char* data, size_t size)
{
WINPR_ASSERT(context);
CliprdrLocalStream* stream = calloc(1, sizeof(CliprdrLocalStream));
if (!stream)
return NULL;
stream->context = context;
if (!cliprdr_local_stream_update(stream, data, size))
goto fail;
stream->lockId = lockId;
return stream;
fail:
cliprdr_local_stream_free(stream);
return NULL;
}
static UINT32 UINTPointerHash(const void* id)
{
WINPR_ASSERT(id);
return *((const UINT32*)id);
}
static BOOL UINTPointerCompare(const void* pointer1, const void* pointer2)
{
if (!pointer1 || !pointer2)
return pointer1 == pointer2;
const UINT32* a = pointer1;
const UINT32* b = pointer2;
return *a == *b;
}
static void* UINTPointerClone(const void* other)
{
const UINT32* src = other;
if (!src)
return NULL;
UINT32* copy = calloc(1, sizeof(UINT32));
if (!copy)
return NULL;
*copy = *src;
return copy;
}
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
static CliprdrFuseFile* fuse_file_new_root(CliprdrFileContext* file_context)
{
CliprdrFuseFile* root_dir;
root_dir = fuse_file_new();
if (!root_dir)
return NULL;
root_dir->filename_with_root = calloc(2, sizeof(char));
if (!root_dir->filename_with_root)
{
fuse_file_free(root_dir);
return NULL;
}
_snprintf(root_dir->filename_with_root, 2, "/");
root_dir->filename = root_dir->filename_with_root;
root_dir->ino = FUSE_ROOT_ID;
root_dir->is_directory = TRUE;
root_dir->is_readonly = TRUE;
if (!HashTable_Insert(file_context->inode_table, (void*)(UINT_PTR)root_dir->ino, root_dir))
{
fuse_file_free(root_dir);
return NULL;
}
return root_dir;
}
#endif
CliprdrFileContext* cliprdr_file_context_new(void* context)
{
CliprdrFileContext* file = calloc(1, sizeof(CliprdrFileContext));
if (!file)
return NULL;
file->log = WLog_Get(CLIENT_TAG("common.cliprdr.file"));
file->clipboard = context;
file->local_streams = HashTable_New(FALSE);
if (!file->local_streams)
goto fail;
if (!HashTable_SetHashFunction(file->local_streams, UINTPointerHash))
goto fail;
wObject* hkobj = HashTable_KeyObject(file->local_streams);
WINPR_ASSERT(hkobj);
hkobj->fnObjectEquals = UINTPointerCompare;
hkobj->fnObjectFree = free;
hkobj->fnObjectNew = UINTPointerClone;
wObject* hobj = HashTable_ValueObject(file->local_streams);
WINPR_ASSERT(hobj);
hobj->fnObjectFree = cliprdr_local_stream_free;
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
file->inode_table = HashTable_New(FALSE);
file->clip_data_table = HashTable_New(FALSE);
file->request_table = HashTable_New(FALSE);
if (!file->inode_table || !file->clip_data_table || !file->request_table)
goto fail;
wObject* ctobj = HashTable_ValueObject(file->clip_data_table);
WINPR_ASSERT(ctobj);
ctobj->fnObjectFree = clip_data_entry_free;
file->root_dir = fuse_file_new_root(file);
if (!file->root_dir)
goto fail;
#endif
if (!create_base_path(file))
goto fail;
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
if (!(file->fuse_thread = CreateThread(NULL, 0, cliprdr_file_fuse_thread, file, 0, NULL)))
goto fail;
#endif
return file;
fail:
cliprdr_file_context_free(file);
return NULL;
}
BOOL local_stream_discard(const void* key, void* value, void* arg)
{
CliprdrFileContext* file = arg;
CliprdrLocalStream* stream = value;
WINPR_ASSERT(file);
WINPR_ASSERT(stream);
if (!stream->locked)
HashTable_Remove(file->local_streams, key);
return TRUE;
}
BOOL cliprdr_file_context_clear(CliprdrFileContext* file)
{
WINPR_ASSERT(file);
WLog_Print(file->log, WLOG_DEBUG, "clear file clipboard...");
HashTable_Lock(file->local_streams);
HashTable_Foreach(file->local_streams, local_stream_discard, file);
HashTable_Unlock(file->local_streams);
memset(file->server_data_hash, 0, sizeof(file->server_data_hash));
memset(file->client_data_hash, 0, sizeof(file->client_data_hash));
return TRUE;
}
BOOL cliprdr_file_context_update_client_data(CliprdrFileContext* file, const char* data,
size_t size)
{
BOOL rc = FALSE;
WINPR_ASSERT(file);
if (!cliprdr_file_client_content_changed_and_update(file, data, size))
return TRUE;
if (!cliprdr_file_context_clear(file))
return FALSE;
UINT32 lockId = file->local_lock_id;
HashTable_Lock(file->local_streams);
CliprdrLocalStream* stream = HashTable_GetItemValue(file->local_streams, &lockId);
WLog_Print(file->log, WLOG_DEBUG, "update client file list (stream=%p)...", stream);
if (stream)
rc = cliprdr_local_stream_update(stream, data, size);
else
{
stream = cliprdr_local_stream_new(file, lockId, data, size);
rc = HashTable_Insert(file->local_streams, &stream->lockId, stream);
}
HashTable_Unlock(file->local_streams);
return rc;
}
UINT32 cliprdr_file_context_current_flags(CliprdrFileContext* file)
{
WINPR_ASSERT(file);
if ((file->file_capability_flags & CB_STREAM_FILECLIP_ENABLED) == 0)
return 0;
if (!file->file_formats_registered)
return 0;
return CB_STREAM_FILECLIP_ENABLED | CB_FILECLIP_NO_FILE_PATHS |
CB_HUGE_FILE_SUPPORT_ENABLED; // | CB_CAN_LOCK_CLIPDATA;
}
BOOL cliprdr_file_context_set_locally_available(CliprdrFileContext* file, BOOL available)
{
WINPR_ASSERT(file);
file->file_formats_registered = available;
return TRUE;
}
BOOL cliprdr_file_context_remote_set_flags(CliprdrFileContext* file, UINT32 flags)
{
WINPR_ASSERT(file);
file->file_capability_flags = flags;
return TRUE;
}
UINT32 cliprdr_file_context_remote_get_flags(CliprdrFileContext* file)
{
WINPR_ASSERT(file);
return file->file_capability_flags;
}
BOOL cliprdr_file_context_has_local_support(CliprdrFileContext* file)
{
WINPR_UNUSED(file);
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
return TRUE;
#else
return FALSE;
#endif
}