mirror of https://github.com/FreeRDP/FreeRDP
[client] move file clipboard to client common
This commit is contained in:
parent
d521c7fa74
commit
ba128f4661
|
@ -41,9 +41,7 @@ set(${MODULE_PREFIX}_SRCS
|
||||||
xf_channels.c
|
xf_channels.c
|
||||||
xf_channels.h
|
xf_channels.h
|
||||||
xf_cliprdr.c
|
xf_cliprdr.c
|
||||||
xf_cliprdr.h
|
xf_cliprdr.h
|
||||||
xf_cliprdr_file.c
|
|
||||||
xf_cliprdr_file.h
|
|
||||||
xf_monitor.c
|
xf_monitor.c
|
||||||
xf_monitor.h
|
xf_monitor.h
|
||||||
xf_disp.c
|
xf_disp.c
|
||||||
|
@ -226,36 +224,6 @@ if(WITH_XFIXES)
|
||||||
set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${XFIXES_LIBRARIES})
|
set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${XFIXES_LIBRARIES})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (NOT APPLE AND NOT WIN32 AND NOT ANDROID)
|
|
||||||
set(OPT_FUSE_DEFAULT ON)
|
|
||||||
else()
|
|
||||||
set(OPT_FUSE_DEFAULT OFF)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
option(WITH_FUSE "Build clipboard with FUSE file copy support" ${OPT_FUSE_DEFAULT})
|
|
||||||
if(WITH_FUSE)
|
|
||||||
include(FindPkgConfig)
|
|
||||||
find_package(PkgConfig REQUIRED)
|
|
||||||
|
|
||||||
pkg_check_modules(FUSE3 fuse3)
|
|
||||||
if (FUSE3_FOUND)
|
|
||||||
message("[FUSE3] using ${FUSE3_LIBRARIES}")
|
|
||||||
|
|
||||||
include_directories(${FUSE3_INCLUDE_DIRS})
|
|
||||||
add_definitions(-DWITH_FUSE3)
|
|
||||||
list(APPEND ${MODULE_PREFIX}_LIBS ${FUSE3_LIBRARIES})
|
|
||||||
else()
|
|
||||||
pkg_check_modules(FUSE2 REQUIRED fuse)
|
|
||||||
|
|
||||||
message("[FUSE2] using ${FUSE2_LIBRARIES}")
|
|
||||||
add_definitions(-DWITH_FUSE2)
|
|
||||||
include_directories(${FUSE2_INCLUDE_DIRS})
|
|
||||||
list(APPEND ${MODULE_PREFIX}_LIBS ${FUSE2_LIBRARIES})
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_definitions(-D_FILE_OFFSET_BITS=64)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
include_directories(${PROJECT_SOURCE_DIR}/resources)
|
include_directories(${PROJECT_SOURCE_DIR}/resources)
|
||||||
|
|
||||||
set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} freerdp-client freerdp m)
|
set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} freerdp-client freerdp m)
|
||||||
|
|
|
@ -44,8 +44,9 @@
|
||||||
#include <freerdp/channels/channels.h>
|
#include <freerdp/channels/channels.h>
|
||||||
#include <freerdp/channels/cliprdr.h>
|
#include <freerdp/channels/cliprdr.h>
|
||||||
|
|
||||||
|
#include <freerdp/client/client_cliprdr_file.h>
|
||||||
|
|
||||||
#include "xf_cliprdr.h"
|
#include "xf_cliprdr.h"
|
||||||
#include "xf_cliprdr_file.h"
|
|
||||||
#include "xf_event.h"
|
#include "xf_event.h"
|
||||||
|
|
||||||
#define TAG CLIENT_TAG("x11.cliprdr")
|
#define TAG CLIENT_TAG("x11.cliprdr")
|
||||||
|
@ -123,10 +124,6 @@ struct xf_clipboard
|
||||||
int xfixes_error_base;
|
int xfixes_error_base;
|
||||||
BOOL xfixes_supported;
|
BOOL xfixes_supported;
|
||||||
|
|
||||||
/* File clipping */
|
|
||||||
BOOL streams_supported;
|
|
||||||
BOOL file_formats_registered;
|
|
||||||
UINT32 file_capability_flags;
|
|
||||||
/* last sent data */
|
/* last sent data */
|
||||||
CLIPRDR_FORMAT* lastSentFormats;
|
CLIPRDR_FORMAT* lastSentFormats;
|
||||||
UINT32 lastSentNumFormats;
|
UINT32 lastSentNumFormats;
|
||||||
|
@ -773,6 +770,7 @@ static UINT xf_cliprdr_send_format_list(xfClipboard* clipboard, const CLIPRDR_FO
|
||||||
xf_clipboard_copy_formats(clipboard, formats, numFormats);
|
xf_clipboard_copy_formats(clipboard, formats, numFormats);
|
||||||
/* Ensure all pending requests are answered. */
|
/* Ensure all pending requests are answered. */
|
||||||
xf_cliprdr_send_data_response(clipboard, NULL, NULL, 0);
|
xf_cliprdr_send_data_response(clipboard, NULL, NULL, 0);
|
||||||
|
cliprdr_file_context_clear(clipboard->file);
|
||||||
|
|
||||||
WINPR_ASSERT(clipboard->context);
|
WINPR_ASSERT(clipboard->context);
|
||||||
WINPR_ASSERT(clipboard->context->ClientFormatList);
|
WINPR_ASSERT(clipboard->context->ClientFormatList);
|
||||||
|
@ -867,12 +865,19 @@ static void xf_cliprdr_process_requested_data(xfClipboard* clipboard, BOOL hasDa
|
||||||
UINT32 file_count = DstSize / sizeof(FILEDESCRIPTORW);
|
UINT32 file_count = DstSize / sizeof(FILEDESCRIPTORW);
|
||||||
pDstData = NULL;
|
pDstData = NULL;
|
||||||
DstSize = 0;
|
DstSize = 0;
|
||||||
error = cliprdr_serialize_file_list_ex(clipboard->file_capability_flags, file_array,
|
|
||||||
file_count, &pDstData, &DstSize);
|
const UINT32 flags = cliprdr_file_context_remote_get_flags(clipboard->file);
|
||||||
|
error = cliprdr_serialize_file_list_ex(flags, file_array, file_count, &pDstData, &DstSize);
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
WLog_ERR(TAG, "failed to serialize CLIPRDR_FILELIST: 0x%08X", error);
|
WLog_ERR(TAG, "failed to serialize CLIPRDR_FILELIST: 0x%08X", error);
|
||||||
|
|
||||||
|
UINT32 formatId = ClipboardGetFormatId(clipboard->system, mime_uri_list);
|
||||||
|
UINT32 url_size = 0;
|
||||||
|
char* url = ClipboardGetData(clipboard->system, formatId, &url_size);
|
||||||
|
cliprdr_file_context_update_client_data(clipboard->file, url, url_size);
|
||||||
|
free(url);
|
||||||
|
|
||||||
free(file_array);
|
free(file_array);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1453,11 +1458,7 @@ static UINT xf_cliprdr_send_client_capabilities(xfClipboard* clipboard)
|
||||||
generalCapabilitySet.generalFlags = CB_USE_LONG_FORMAT_NAMES;
|
generalCapabilitySet.generalFlags = CB_USE_LONG_FORMAT_NAMES;
|
||||||
|
|
||||||
WINPR_ASSERT(clipboard);
|
WINPR_ASSERT(clipboard);
|
||||||
if (clipboard->streams_supported && clipboard->file_formats_registered)
|
generalCapabilitySet.generalFlags |= cliprdr_file_context_current_flags(clipboard->file);
|
||||||
generalCapabilitySet.generalFlags |=
|
|
||||||
CB_STREAM_FILECLIP_ENABLED | CB_FILECLIP_NO_FILE_PATHS | CB_HUGE_FILE_SUPPORT_ENABLED;
|
|
||||||
|
|
||||||
clipboard->file_capability_flags = generalCapabilitySet.generalFlags;
|
|
||||||
|
|
||||||
WINPR_ASSERT(clipboard->context);
|
WINPR_ASSERT(clipboard->context);
|
||||||
WINPR_ASSERT(clipboard->context->ClientCapabilities);
|
WINPR_ASSERT(clipboard->context->ClientCapabilities);
|
||||||
|
@ -1584,7 +1585,7 @@ static UINT xf_cliprdr_server_capabilities(CliprdrClientContext* context,
|
||||||
capsPtr = (const BYTE*)capabilities->capabilitySets;
|
capsPtr = (const BYTE*)capabilities->capabilitySets;
|
||||||
WINPR_ASSERT(capsPtr);
|
WINPR_ASSERT(capsPtr);
|
||||||
|
|
||||||
clipboard->streams_supported = FALSE;
|
cliprdr_file_context_remote_set_flags(clipboard->file, 0);
|
||||||
|
|
||||||
for (i = 0; i < capabilities->cCapabilitiesSets; i++)
|
for (i = 0; i < capabilities->cCapabilitiesSets; i++)
|
||||||
{
|
{
|
||||||
|
@ -1594,10 +1595,7 @@ static UINT xf_cliprdr_server_capabilities(CliprdrClientContext* context,
|
||||||
{
|
{
|
||||||
generalCaps = (const CLIPRDR_GENERAL_CAPABILITY_SET*)caps;
|
generalCaps = (const CLIPRDR_GENERAL_CAPABILITY_SET*)caps;
|
||||||
|
|
||||||
if (generalCaps->generalFlags & CB_STREAM_FILECLIP_ENABLED)
|
cliprdr_file_context_remote_set_flags(clipboard->file, generalCaps->generalFlags);
|
||||||
{
|
|
||||||
clipboard->streams_supported = TRUE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
capsPtr += caps->capabilitySetLength;
|
capsPtr += caps->capabilitySetLength;
|
||||||
|
@ -1878,7 +1876,7 @@ xf_cliprdr_server_format_data_response(CliprdrClientContext* context,
|
||||||
|
|
||||||
if (strcmp(clipboard->requestedFormat->formatName, type_FileGroupDescriptorW) == 0)
|
if (strcmp(clipboard->requestedFormat->formatName, type_FileGroupDescriptorW) == 0)
|
||||||
{
|
{
|
||||||
if (!cliprdr_file_context_update_data(clipboard->file, data, size))
|
if (!cliprdr_file_context_update_server_data(clipboard->file, data, size))
|
||||||
WLog_WARN(TAG, "failed to update file descriptors");
|
WLog_WARN(TAG, "failed to update file descriptors");
|
||||||
|
|
||||||
srcFormatId = ClipboardGetFormatId(clipboard->system, type_FileGroupDescriptorW);
|
srcFormatId = ClipboardGetFormatId(clipboard->system, type_FileGroupDescriptorW);
|
||||||
|
@ -1997,82 +1995,6 @@ xf_cliprdr_server_format_data_response(CliprdrClientContext* context,
|
||||||
return CHANNEL_RC_OK;
|
return CHANNEL_RC_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static UINT
|
|
||||||
xf_cliprdr_send_file_contents_failure(CliprdrClientContext* context,
|
|
||||||
const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest)
|
|
||||||
{
|
|
||||||
CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 };
|
|
||||||
|
|
||||||
WINPR_ASSERT(fileContentsRequest);
|
|
||||||
|
|
||||||
response.common.msgFlags = CB_RESPONSE_FAIL;
|
|
||||||
response.streamId = fileContentsRequest->streamId;
|
|
||||||
|
|
||||||
WINPR_ASSERT(context);
|
|
||||||
WINPR_ASSERT(context->ClientFileContentsResponse);
|
|
||||||
return context->ClientFileContentsResponse(context, &response);
|
|
||||||
}
|
|
||||||
|
|
||||||
static UINT
|
|
||||||
xf_cliprdr_server_file_contents_request(CliprdrClientContext* context,
|
|
||||||
const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest)
|
|
||||||
{
|
|
||||||
UINT error = NO_ERROR;
|
|
||||||
xfClipboard* clipboard;
|
|
||||||
|
|
||||||
WINPR_ASSERT(context);
|
|
||||||
WINPR_ASSERT(fileContentsRequest);
|
|
||||||
|
|
||||||
clipboard = cliprdr_file_context_get_context(context->custom);
|
|
||||||
WINPR_ASSERT(clipboard);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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_ERR(TAG, "invalid CLIPRDR_FILECONTENTS_REQUEST.dwFlags");
|
|
||||||
return xf_cliprdr_send_file_contents_failure(context, fileContentsRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileContentsRequest->dwFlags & FILECONTENTS_SIZE)
|
|
||||||
error = xf_cliprdr_server_file_size_request(clipboard, fileContentsRequest);
|
|
||||||
|
|
||||||
if (fileContentsRequest->dwFlags & FILECONTENTS_RANGE)
|
|
||||||
error = xf_cliprdr_server_file_range_request(clipboard, fileContentsRequest);
|
|
||||||
|
|
||||||
if (error)
|
|
||||||
{
|
|
||||||
WLog_ERR(TAG, "failed to handle CLIPRDR_FILECONTENTS_REQUEST: 0x%08X", error);
|
|
||||||
return xf_cliprdr_send_file_contents_failure(context, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
xfClipboard* xf_clipboard_new(xfContext* xfc, BOOL relieveFilenameRestriction)
|
xfClipboard* xf_clipboard_new(xfContext* xfc, BOOL relieveFilenameRestriction)
|
||||||
{
|
{
|
||||||
int n = 0;
|
int n = 0;
|
||||||
|
@ -2090,6 +2012,10 @@ xfClipboard* xf_clipboard_new(xfContext* xfc, BOOL relieveFilenameRestriction)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clipboard->file = cliprdr_file_context_new(clipboard);
|
||||||
|
if (!clipboard->file)
|
||||||
|
goto fail;
|
||||||
|
|
||||||
xfc->clipboard = clipboard;
|
xfc->clipboard = clipboard;
|
||||||
clipboard->xfc = xfc;
|
clipboard->xfc = xfc;
|
||||||
channels = xfc->common.context.channels;
|
channels = xfc->common.context.channels;
|
||||||
|
@ -2199,7 +2125,7 @@ xfClipboard* xf_clipboard_new(xfContext* xfc, BOOL relieveFilenameRestriction)
|
||||||
const UINT32 uid = ClipboardGetFormatId(clipboard->system, mime_uri_list);
|
const UINT32 uid = ClipboardGetFormatId(clipboard->system, mime_uri_list);
|
||||||
if (uid)
|
if (uid)
|
||||||
{
|
{
|
||||||
clipboard->file_formats_registered = TRUE;
|
cliprdr_file_context_set_locally_available(clipboard->file, TRUE);
|
||||||
clientFormat->atom = XInternAtom(xfc->display, mime_uri_list, False);
|
clientFormat->atom = XInternAtom(xfc->display, mime_uri_list, False);
|
||||||
clientFormat->localFormat = uid;
|
clientFormat->localFormat = uid;
|
||||||
clientFormat->formatToRequest = fgid;
|
clientFormat->formatToRequest = fgid;
|
||||||
|
@ -2214,7 +2140,7 @@ xfClipboard* xf_clipboard_new(xfContext* xfc, BOOL relieveFilenameRestriction)
|
||||||
const UINT32 gid = ClipboardGetFormatId(clipboard->system, mime_gnome_copied_files);
|
const UINT32 gid = ClipboardGetFormatId(clipboard->system, mime_gnome_copied_files);
|
||||||
if (gid != 0)
|
if (gid != 0)
|
||||||
{
|
{
|
||||||
clipboard->file_formats_registered = TRUE;
|
cliprdr_file_context_set_locally_available(clipboard->file, TRUE);
|
||||||
clientFormat->atom = XInternAtom(xfc->display, mime_gnome_copied_files, False);
|
clientFormat->atom = XInternAtom(xfc->display, mime_gnome_copied_files, False);
|
||||||
clientFormat->localFormat = gid;
|
clientFormat->localFormat = gid;
|
||||||
clientFormat->formatToRequest = fgid;
|
clientFormat->formatToRequest = fgid;
|
||||||
|
@ -2229,7 +2155,7 @@ xfClipboard* xf_clipboard_new(xfContext* xfc, BOOL relieveFilenameRestriction)
|
||||||
const UINT32 mid = ClipboardGetFormatId(clipboard->system, mime_mate_copied_files);
|
const UINT32 mid = ClipboardGetFormatId(clipboard->system, mime_mate_copied_files);
|
||||||
if (mid != 0)
|
if (mid != 0)
|
||||||
{
|
{
|
||||||
clipboard->file_formats_registered = TRUE;
|
cliprdr_file_context_set_locally_available(clipboard->file, TRUE);
|
||||||
clientFormat->atom = XInternAtom(xfc->display, mime_mate_copied_files, False);
|
clientFormat->atom = XInternAtom(xfc->display, mime_mate_copied_files, False);
|
||||||
clientFormat->localFormat = mid;
|
clientFormat->localFormat = mid;
|
||||||
clientFormat->formatToRequest = fgid;
|
clientFormat->formatToRequest = fgid;
|
||||||
|
@ -2245,10 +2171,6 @@ xfClipboard* xf_clipboard_new(xfContext* xfc, BOOL relieveFilenameRestriction)
|
||||||
clipboard->numTargets = 2;
|
clipboard->numTargets = 2;
|
||||||
clipboard->incr_atom = XInternAtom(xfc->display, "INCR", FALSE);
|
clipboard->incr_atom = XInternAtom(xfc->display, "INCR", FALSE);
|
||||||
|
|
||||||
clipboard->file = cliprdr_file_context_new(clipboard);
|
|
||||||
if (!clipboard->file)
|
|
||||||
goto fail;
|
|
||||||
|
|
||||||
return clipboard;
|
return clipboard;
|
||||||
|
|
||||||
fail:
|
fail:
|
||||||
|
@ -2297,7 +2219,6 @@ void xf_cliprdr_init(xfContext* xfc, CliprdrClientContext* cliprdr)
|
||||||
cliprdr->ServerFormatListResponse = xf_cliprdr_server_format_list_response;
|
cliprdr->ServerFormatListResponse = xf_cliprdr_server_format_list_response;
|
||||||
cliprdr->ServerFormatDataRequest = xf_cliprdr_server_format_data_request;
|
cliprdr->ServerFormatDataRequest = xf_cliprdr_server_format_data_request;
|
||||||
cliprdr->ServerFormatDataResponse = xf_cliprdr_server_format_data_response;
|
cliprdr->ServerFormatDataResponse = xf_cliprdr_server_format_data_response;
|
||||||
cliprdr->ServerFileContentsRequest = xf_cliprdr_server_file_contents_request;
|
|
||||||
|
|
||||||
cliprdr_file_context_init(xfc->clipboard->file, cliprdr);
|
cliprdr_file_context_init(xfc->clipboard->file, cliprdr);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ set(SRCS
|
||||||
client_rails.c
|
client_rails.c
|
||||||
cmdline.c
|
cmdline.c
|
||||||
file.c
|
file.c
|
||||||
|
client_cliprdr_file.c
|
||||||
geometry.c
|
geometry.c
|
||||||
smartcard_cli.c)
|
smartcard_cli.c)
|
||||||
|
|
||||||
|
@ -39,6 +40,35 @@ foreach(FREERDP_CHANNELS_CLIENT_SRC ${FREERDP_CHANNELS_CLIENT_SRCS})
|
||||||
list(APPEND SRCS "${FREERDP_CHANNELS_CLIENT_SRC}")
|
list(APPEND SRCS "${FREERDP_CHANNELS_CLIENT_SRC}")
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
|
if (NOT APPLE AND NOT WIN32 AND NOT ANDROID)
|
||||||
|
set(OPT_FUSE_DEFAULT ON)
|
||||||
|
else()
|
||||||
|
set(OPT_FUSE_DEFAULT OFF)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
option(WITH_FUSE "Build clipboard with FUSE file copy support" ${OPT_FUSE_DEFAULT})
|
||||||
|
if(WITH_FUSE)
|
||||||
|
include(FindPkgConfig)
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
|
||||||
|
pkg_check_modules(FUSE3 fuse3)
|
||||||
|
if (FUSE3_FOUND)
|
||||||
|
message("[FUSE3] using ${FUSE3_LIBRARIES}")
|
||||||
|
|
||||||
|
include_directories(${FUSE3_INCLUDE_DIRS})
|
||||||
|
add_definitions(-DWITH_FUSE3)
|
||||||
|
list(APPEND ${MODULE_PREFIX}_LIBS ${FUSE3_LIBRARIES})
|
||||||
|
else()
|
||||||
|
pkg_check_modules(FUSE2 REQUIRED fuse)
|
||||||
|
|
||||||
|
message("[FUSE2] using ${FUSE2_LIBRARIES}")
|
||||||
|
add_definitions(-DWITH_FUSE2)
|
||||||
|
include_directories(${FUSE2_INCLUDE_DIRS})
|
||||||
|
list(APPEND ${MODULE_PREFIX}_LIBS ${FUSE2_LIBRARIES})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_definitions(-D_FILE_OFFSET_BITS=64)
|
||||||
|
endif()
|
||||||
|
|
||||||
# On windows create dll version information.
|
# On windows create dll version information.
|
||||||
# Vendor, product and year are already set in top level CMakeLists.txt
|
# Vendor, product and year are already set in top level CMakeLists.txt
|
||||||
|
|
|
@ -53,19 +53,17 @@
|
||||||
#include <freerdp/channels/channels.h>
|
#include <freerdp/channels/channels.h>
|
||||||
#include <freerdp/channels/cliprdr.h>
|
#include <freerdp/channels/cliprdr.h>
|
||||||
|
|
||||||
#include "xf_cliprdr_file.h"
|
#include <freerdp/client/client_cliprdr_file.h>
|
||||||
|
|
||||||
#define TAG CLIENT_TAG("x11.cliprdr.file")
|
|
||||||
|
|
||||||
#define MAX_CLIPBOARD_FORMATS 255
|
#define MAX_CLIPBOARD_FORMATS 255
|
||||||
#define WIN32_FILETIME_TO_UNIX_EPOCH_USEC UINT64_C(116444736000000000)
|
#define WIN32_FILETIME_TO_UNIX_EPOCH_USEC UINT64_C(116444736000000000)
|
||||||
|
|
||||||
#ifdef WITH_DEBUG_CLIPRDR
|
#ifdef WITH_DEBUG_CLIPRDR
|
||||||
#define DEBUG_CLIPRDR(...) WLog_DBG(TAG, __VA_ARGS__)
|
#define DEBUG_CLIPRDR(log, ...) WLog_Print(log, WLOG_DEBUG, __VA_ARGS__)
|
||||||
#else
|
#else
|
||||||
#define DEBUG_CLIPRDR(...) \
|
#define DEBUG_CLIPRDR(log, ...) \
|
||||||
do \
|
do \
|
||||||
{ \
|
{ \
|
||||||
} while (0)
|
} while (0)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -94,6 +92,21 @@ typedef struct
|
||||||
} CliprdrFuseInode;
|
} CliprdrFuseInode;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
char* name;
|
||||||
|
FILE* fp;
|
||||||
|
} CliprdrLocalFile;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
UINT32 streamID;
|
||||||
|
|
||||||
|
BOOL locked;
|
||||||
|
size_t count;
|
||||||
|
CliprdrLocalFile* files;
|
||||||
|
} CliprdrLocalStream;
|
||||||
|
|
||||||
struct cliprdr_file_context
|
struct cliprdr_file_context
|
||||||
{
|
{
|
||||||
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
|
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
|
||||||
|
@ -107,16 +120,27 @@ struct cliprdr_file_context
|
||||||
wArrayList* ino_list;
|
wArrayList* ino_list;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/* File clipping */
|
||||||
|
BOOL file_formats_registered;
|
||||||
|
UINT32 file_capability_flags;
|
||||||
|
|
||||||
|
UINT32 local_stream_id;
|
||||||
|
UINT32 remote_stream_id;
|
||||||
|
|
||||||
|
wHashTable* local_streams;
|
||||||
|
wLog* log;
|
||||||
void* clipboard;
|
void* clipboard;
|
||||||
CliprdrClientContext* context;
|
CliprdrClientContext* context;
|
||||||
char* path;
|
char* path;
|
||||||
BYTE hash[WINPR_SHA256_DIGEST_LENGTH];
|
BYTE hash[WINPR_SHA256_DIGEST_LENGTH];
|
||||||
};
|
};
|
||||||
|
|
||||||
void cliprdr_file_session_terminate(CliprdrFileContext* file);
|
static CliprdrLocalStream* cliprdr_local_stream_new(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);
|
||||||
|
|
||||||
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
|
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
|
||||||
static BOOL xf_fuse_repopulate(wArrayList* list);
|
static BOOL xf_fuse_repopulate(CliprdrFileContext* file, wArrayList* list);
|
||||||
static UINT xf_cliprdr_send_client_file_contents(CliprdrFileContext* file, UINT32 streamId,
|
static UINT xf_cliprdr_send_client_file_contents(CliprdrFileContext* file, UINT32 streamId,
|
||||||
UINT32 listIndex, UINT32 dwFlags,
|
UINT32 listIndex, UINT32 dwFlags,
|
||||||
UINT32 nPositionLow, UINT32 nPositionHigh,
|
UINT32 nPositionLow, UINT32 nPositionHigh,
|
||||||
|
@ -566,9 +590,11 @@ static void fuse_abort(int sig, const char* signame, void* context)
|
||||||
{
|
{
|
||||||
CliprdrFileContext* file = (CliprdrFileContext*)context;
|
CliprdrFileContext* file = (CliprdrFileContext*)context;
|
||||||
|
|
||||||
WLog_INFO(TAG, "signal %s [%d] aborting session", signame, sig);
|
|
||||||
if (file)
|
if (file)
|
||||||
|
{
|
||||||
|
WLog_Print(file->log, WLOG_INFO, "signal %s [%d] aborting session", signame, sig);
|
||||||
cliprdr_file_session_terminate(file);
|
cliprdr_file_session_terminate(file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static DWORD WINAPI cliprdr_file_fuse_thread(LPVOID arg)
|
static DWORD WINAPI cliprdr_file_fuse_thread(LPVOID arg)
|
||||||
|
@ -577,7 +603,7 @@ static DWORD WINAPI cliprdr_file_fuse_thread(LPVOID arg)
|
||||||
|
|
||||||
WINPR_ASSERT(file);
|
WINPR_ASSERT(file);
|
||||||
|
|
||||||
DEBUG_CLIPRDR("Starting fuse with mountpoint '%s'", file->path);
|
DEBUG_CLIPRDR(file->log, "Starting fuse with mountpoint '%s'", file->path);
|
||||||
|
|
||||||
struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
|
struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
|
||||||
#if FUSE_USE_VERSION >= 30
|
#if FUSE_USE_VERSION >= 30
|
||||||
|
@ -616,14 +642,16 @@ static DWORD WINAPI cliprdr_file_fuse_thread(LPVOID arg)
|
||||||
#endif
|
#endif
|
||||||
fuse_opt_free_args(&args);
|
fuse_opt_free_args(&args);
|
||||||
|
|
||||||
DEBUG_CLIPRDR("Quitting fuse with mountpoint '%s'", file->path);
|
DEBUG_CLIPRDR(file->log, "Quitting fuse with mountpoint '%s'", file->path);
|
||||||
|
|
||||||
ExitThread(0);
|
ExitThread(0);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static CliprdrFuseInode* cliprdr_file_fuse_create_root_node(void)
|
static CliprdrFuseInode* cliprdr_file_fuse_create_root_node(CliprdrFileContext* file)
|
||||||
{
|
{
|
||||||
|
WINPR_ASSERT(file);
|
||||||
|
|
||||||
CliprdrFuseInode* rootNode = (CliprdrFuseInode*)calloc(1, sizeof(CliprdrFuseInode));
|
CliprdrFuseInode* rootNode = (CliprdrFuseInode*)calloc(1, sizeof(CliprdrFuseInode));
|
||||||
if (!rootNode)
|
if (!rootNode)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
@ -640,20 +668,20 @@ static CliprdrFuseInode* cliprdr_file_fuse_create_root_node(void)
|
||||||
if (!rootNode->child_inos || !rootNode->name)
|
if (!rootNode->child_inos || !rootNode->name)
|
||||||
{
|
{
|
||||||
cliprdr_file_fuse_inode_free(rootNode);
|
cliprdr_file_fuse_inode_free(rootNode);
|
||||||
WLog_ERR(TAG, "fail to alloc rootNode's member");
|
WLog_Print(file->log, WLOG_ERROR, "fail to alloc rootNode's member");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
return rootNode;
|
return rootNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
static BOOL xf_fuse_repopulate(wArrayList* list)
|
static BOOL xf_fuse_repopulate(CliprdrFileContext* file, wArrayList* list)
|
||||||
{
|
{
|
||||||
if (!list)
|
if (!list)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
ArrayList_Lock(list);
|
ArrayList_Lock(list);
|
||||||
ArrayList_Clear(list);
|
ArrayList_Clear(list);
|
||||||
ArrayList_Append(list, cliprdr_file_fuse_create_root_node());
|
ArrayList_Append(list, cliprdr_file_fuse_create_root_node(file));
|
||||||
ArrayList_Unlock(list);
|
ArrayList_Unlock(list);
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
@ -705,9 +733,8 @@ static UINT xf_cliprdr_send_client_file_contents(CliprdrFileContext* file, UINT3
|
||||||
*
|
*
|
||||||
* @return 0 on success, otherwise a Win32 error code
|
* @return 0 on success, otherwise a Win32 error code
|
||||||
*/
|
*/
|
||||||
static UINT
|
static UINT cliprdr_file_context_server_file_contents_response(
|
||||||
xf_cliprdr_server_file_contents_response(CliprdrClientContext* context,
|
CliprdrClientContext* context, const CLIPRDR_FILE_CONTENTS_RESPONSE* fileContentsResponse)
|
||||||
const CLIPRDR_FILE_CONTENTS_RESPONSE* fileContentsResponse)
|
|
||||||
{
|
{
|
||||||
size_t count;
|
size_t count;
|
||||||
size_t index;
|
size_t index;
|
||||||
|
@ -819,16 +846,18 @@ static const char* cliprdr_file_fuse_split_basename(const char* name, size_t len
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static BOOL cliprdr_file_fuse_check_stream(wStream* s, size_t count)
|
static BOOL cliprdr_file_fuse_check_stream(CliprdrFileContext* file, wStream* s, size_t count)
|
||||||
{
|
{
|
||||||
UINT32 nrDescriptors;
|
UINT32 nrDescriptors;
|
||||||
if (!Stream_CheckAndLogRequiredLength(TAG, s, sizeof(UINT32)))
|
|
||||||
|
WINPR_ASSERT(file);
|
||||||
|
if (!Stream_CheckAndLogRequiredLengthWLog(file->log, s, sizeof(UINT32)))
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
Stream_Read_UINT32(s, nrDescriptors);
|
Stream_Read_UINT32(s, nrDescriptors);
|
||||||
if (count != nrDescriptors)
|
if (count != nrDescriptors)
|
||||||
{
|
{
|
||||||
WLog_WARN(TAG, "format data response mismatch");
|
WLog_Print(file->log, WLOG_WARN, "format data response mismatch");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
@ -850,7 +879,7 @@ static BOOL cliprdr_file_fuse_create_nodes(CliprdrFileContext* file, wStream* s,
|
||||||
mapDir = HashTable_New(TRUE);
|
mapDir = HashTable_New(TRUE);
|
||||||
if (!mapDir)
|
if (!mapDir)
|
||||||
{
|
{
|
||||||
WLog_ERR(TAG, "fail to alloc hashtable");
|
WLog_Print(file->log, WLOG_ERROR, "fail to alloc hashtable");
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
if (!HashTable_SetupForStringData(mapDir, FALSE))
|
if (!HashTable_SetupForStringData(mapDir, FALSE))
|
||||||
|
@ -859,7 +888,7 @@ static BOOL cliprdr_file_fuse_create_nodes(CliprdrFileContext* file, wStream* s,
|
||||||
FILEDESCRIPTORW* descriptor = (FILEDESCRIPTORW*)calloc(1, sizeof(FILEDESCRIPTORW));
|
FILEDESCRIPTORW* descriptor = (FILEDESCRIPTORW*)calloc(1, sizeof(FILEDESCRIPTORW));
|
||||||
if (!descriptor)
|
if (!descriptor)
|
||||||
{
|
{
|
||||||
WLog_ERR(TAG, "fail to alloc FILEDESCRIPTORW");
|
WLog_Print(file->log, WLOG_ERROR, "fail to alloc FILEDESCRIPTORW");
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
/* here we assume that parent folder always appears before its children */
|
/* here we assume that parent folder always appears before its children */
|
||||||
|
@ -869,7 +898,7 @@ static BOOL cliprdr_file_fuse_create_nodes(CliprdrFileContext* file, wStream* s,
|
||||||
inode = (CliprdrFuseInode*)calloc(1, sizeof(CliprdrFuseInode));
|
inode = (CliprdrFuseInode*)calloc(1, sizeof(CliprdrFuseInode));
|
||||||
if (!inode)
|
if (!inode)
|
||||||
{
|
{
|
||||||
WLog_ERR(TAG, "fail to alloc ino");
|
WLog_Print(file->log, WLOG_ERROR, "fail to alloc ino");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -972,7 +1001,7 @@ static BOOL cliprdr_file_fuse_create_nodes(CliprdrFileContext* file, wStream* s,
|
||||||
{
|
{
|
||||||
/* baseName is freed in cliprdr_file_fuse_inode_free*/
|
/* baseName is freed in cliprdr_file_fuse_inode_free*/
|
||||||
cliprdr_file_fuse_inode_free(inode);
|
cliprdr_file_fuse_inode_free(inode);
|
||||||
xf_fuse_repopulate(file->ino_list);
|
xf_fuse_repopulate(file, file->ino_list);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -1003,7 +1032,7 @@ static BOOL cliprdr_file_fuse_generate_list(CliprdrFileContext* file, const BYTE
|
||||||
|
|
||||||
if (size < 4)
|
if (size < 4)
|
||||||
{
|
{
|
||||||
WLog_ERR(TAG, "size of format data response invalid : %" PRIu32, size);
|
WLog_Print(file->log, WLOG_ERROR, "size of format data response invalid : %" PRIu32, size);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
size_t count = (size - 4) / sizeof(FILEDESCRIPTORW);
|
size_t count = (size - 4) / sizeof(FILEDESCRIPTORW);
|
||||||
|
@ -1011,9 +1040,9 @@ static BOOL cliprdr_file_fuse_generate_list(CliprdrFileContext* file, const BYTE
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
s = Stream_StaticConstInit(&sbuffer, data, size);
|
s = Stream_StaticConstInit(&sbuffer, data, size);
|
||||||
if (!s || !cliprdr_file_fuse_check_stream(s, count))
|
if (!s || !cliprdr_file_fuse_check_stream(file, s, count))
|
||||||
{
|
{
|
||||||
WLog_ERR(TAG, "Stream_New failed");
|
WLog_Print(file->log, WLOG_ERROR, "Stream_New failed");
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1024,7 +1053,7 @@ static BOOL cliprdr_file_fuse_generate_list(CliprdrFileContext* file, const BYTE
|
||||||
if (!rootNode)
|
if (!rootNode)
|
||||||
{
|
{
|
||||||
cliprdr_file_fuse_inode_free(rootNode);
|
cliprdr_file_fuse_inode_free(rootNode);
|
||||||
WLog_ERR(TAG, "fail to alloc rootNode to ino_list");
|
WLog_Print(file->log, WLOG_ERROR, "fail to alloc rootNode to ino_list");
|
||||||
goto error2;
|
goto error2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1037,6 +1066,232 @@ error:
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static UINT cliprdr_file_context_send_file_contents_failure(
|
||||||
|
CliprdrClientContext* context, const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest)
|
||||||
|
{
|
||||||
|
CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 };
|
||||||
|
|
||||||
|
WINPR_ASSERT(fileContentsRequest);
|
||||||
|
|
||||||
|
response.common.msgFlags = CB_RESPONSE_FAIL;
|
||||||
|
response.streamId = fileContentsRequest->streamId;
|
||||||
|
|
||||||
|
WINPR_ASSERT(context);
|
||||||
|
WINPR_ASSERT(context->ClientFileContentsResponse);
|
||||||
|
return context->ClientFileContentsResponse(context, &response);
|
||||||
|
}
|
||||||
|
|
||||||
|
static UINT cliprdr_file_context_send_contents(CliprdrClientContext* context,
|
||||||
|
const CLIPRDR_FILE_CONTENTS_REQUEST* request,
|
||||||
|
const void* data, size_t size)
|
||||||
|
{
|
||||||
|
CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 };
|
||||||
|
|
||||||
|
WINPR_ASSERT(request);
|
||||||
|
|
||||||
|
response.common.msgFlags = CB_RESPONSE_OK;
|
||||||
|
response.streamId = request->streamId;
|
||||||
|
response.requestedData = data;
|
||||||
|
response.cbRequested = size;
|
||||||
|
|
||||||
|
WINPR_ASSERT(context);
|
||||||
|
WINPR_ASSERT(context->ClientFileContentsResponse);
|
||||||
|
return context->ClientFileContentsResponse(context, &response);
|
||||||
|
}
|
||||||
|
|
||||||
|
static FILE* file_for_request(CliprdrFileContext* file, UINT32 streamId, UINT32 listIndex)
|
||||||
|
{
|
||||||
|
FILE* fp = NULL;
|
||||||
|
HashTable_Lock(file->local_streams);
|
||||||
|
|
||||||
|
CliprdrLocalStream* cur =
|
||||||
|
HashTable_GetItemValue(file->local_streams, (void*)(uintptr_t)streamId);
|
||||||
|
if (cur)
|
||||||
|
{
|
||||||
|
if (listIndex < cur->count)
|
||||||
|
{
|
||||||
|
CliprdrLocalFile* f = &cur->files[listIndex];
|
||||||
|
if (f->fp)
|
||||||
|
fp = f->fp;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (strncmp("file://", f->name, 7) == 0)
|
||||||
|
fp = fopen(&f->name[7], "rb");
|
||||||
|
else
|
||||||
|
fp = fopen(f->name, "rb");
|
||||||
|
f->fp = fp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HashTable_Unlock(file->local_streams);
|
||||||
|
|
||||||
|
return fp;
|
||||||
|
}
|
||||||
|
|
||||||
|
static UINT cliprdr_file_context_server_file_size_request(
|
||||||
|
CliprdrFileContext* file, const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest)
|
||||||
|
{
|
||||||
|
wClipboardFileSizeRequest request = { 0 };
|
||||||
|
|
||||||
|
WINPR_ASSERT(fileContentsRequest);
|
||||||
|
|
||||||
|
request.streamId = fileContentsRequest->streamId;
|
||||||
|
request.listIndex = fileContentsRequest->listIndex;
|
||||||
|
|
||||||
|
if (fileContentsRequest->cbRequested != sizeof(UINT64))
|
||||||
|
{
|
||||||
|
WLog_Print(file->log, WLOG_WARN, "unexpected FILECONTENTS_SIZE request: %" PRIu32 " bytes",
|
||||||
|
fileContentsRequest->cbRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE* fp =
|
||||||
|
file_for_request(file, fileContentsRequest->streamId, fileContentsRequest->listIndex);
|
||||||
|
if (!fp)
|
||||||
|
return cliprdr_file_context_send_file_contents_failure(file->context, fileContentsRequest);
|
||||||
|
|
||||||
|
if (_fseeki64(fp, 0, SEEK_END) < 0)
|
||||||
|
return cliprdr_file_context_send_file_contents_failure(file->context, fileContentsRequest);
|
||||||
|
|
||||||
|
const INT64 size = _ftelli64(fp);
|
||||||
|
return cliprdr_file_context_send_contents(file->context, fileContentsRequest, &size,
|
||||||
|
sizeof(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
static UINT cliprdr_file_context_server_file_range_request(
|
||||||
|
CliprdrFileContext* file, const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest)
|
||||||
|
{
|
||||||
|
BYTE* data = NULL;
|
||||||
|
wClipboardFileRangeRequest request = { 0 };
|
||||||
|
|
||||||
|
WINPR_ASSERT(fileContentsRequest);
|
||||||
|
|
||||||
|
request.streamId = fileContentsRequest->streamId;
|
||||||
|
request.listIndex = fileContentsRequest->listIndex;
|
||||||
|
request.nPositionLow = fileContentsRequest->nPositionLow;
|
||||||
|
request.nPositionHigh = fileContentsRequest->nPositionHigh;
|
||||||
|
request.cbRequested = fileContentsRequest->cbRequested;
|
||||||
|
FILE* fp =
|
||||||
|
file_for_request(file, fileContentsRequest->streamId, fileContentsRequest->listIndex);
|
||||||
|
if (!fp)
|
||||||
|
goto fail;
|
||||||
|
|
||||||
|
const UINT64 offset = (((UINT64)fileContentsRequest->nPositionHigh) << 32) |
|
||||||
|
((UINT64)fileContentsRequest->nPositionLow);
|
||||||
|
if (_fseeki64(fp, offset, SEEK_SET) < 0)
|
||||||
|
goto fail;
|
||||||
|
|
||||||
|
data = malloc(fileContentsRequest->cbRequested);
|
||||||
|
if (!data)
|
||||||
|
goto fail;
|
||||||
|
|
||||||
|
const size_t r = fread(data, 1, fileContentsRequest->cbRequested, fp);
|
||||||
|
const UINT rc = cliprdr_file_context_send_contents(file->context, fileContentsRequest, data, r);
|
||||||
|
free(data);
|
||||||
|
return rc;
|
||||||
|
fail:
|
||||||
|
free(data);
|
||||||
|
return cliprdr_file_context_send_file_contents_failure(file->context, fileContentsRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
static UINT change_lock(CliprdrFileContext* file, UINT32 streamID, BOOL lock)
|
||||||
|
{
|
||||||
|
WINPR_ASSERT(file);
|
||||||
|
|
||||||
|
HashTable_Lock(file->local_streams);
|
||||||
|
CliprdrLocalStream* stream =
|
||||||
|
HashTable_GetItemValue(file->local_streams, (void*)(uintptr_t)streamID);
|
||||||
|
if (lock && !stream)
|
||||||
|
{
|
||||||
|
stream = cliprdr_local_stream_new(streamID, NULL, 0);
|
||||||
|
HashTable_Insert(file->local_streams, (void*)(uintptr_t)streamID, stream);
|
||||||
|
file->local_stream_id = streamID;
|
||||||
|
}
|
||||||
|
if (stream)
|
||||||
|
stream->locked = lock;
|
||||||
|
|
||||||
|
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(context, 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(context, 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)
|
BOOL cliprdr_file_context_init(CliprdrFileContext* file, CliprdrClientContext* cliprdr)
|
||||||
{
|
{
|
||||||
WINPR_ASSERT(file);
|
WINPR_ASSERT(file);
|
||||||
|
@ -1044,8 +1299,12 @@ BOOL cliprdr_file_context_init(CliprdrFileContext* file, CliprdrClientContext* c
|
||||||
|
|
||||||
cliprdr->custom = file;
|
cliprdr->custom = file;
|
||||||
file->context = cliprdr;
|
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)
|
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
|
||||||
cliprdr->ServerFileContentsResponse = xf_cliprdr_server_file_contents_response;
|
cliprdr->ServerFileContentsResponse = cliprdr_file_context_server_file_contents_response;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
@ -1079,7 +1338,8 @@ static BOOL cliprdr_file_content_changed_and_update(CliprdrFileContext* file, co
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOL cliprdr_file_context_update_data(CliprdrFileContext* file, const void* data, size_t size)
|
BOOL cliprdr_file_context_update_server_data(CliprdrFileContext* file, const void* data,
|
||||||
|
size_t size)
|
||||||
{
|
{
|
||||||
WINPR_ASSERT(file);
|
WINPR_ASSERT(file);
|
||||||
|
|
||||||
|
@ -1109,127 +1369,269 @@ const char* cliprdr_file_context_base_path(CliprdrFileContext* file)
|
||||||
|
|
||||||
void cliprdr_file_session_terminate(CliprdrFileContext* file)
|
void cliprdr_file_session_terminate(CliprdrFileContext* file)
|
||||||
{
|
{
|
||||||
if (!file)
|
if (!file)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
|
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
|
||||||
if (file->fuse_sess)
|
if (file->fuse_sess)
|
||||||
fuse_session_exit(file->fuse_sess);
|
fuse_session_exit(file->fuse_sess);
|
||||||
#endif
|
#endif
|
||||||
/* not elegant but works for umounting FUSE
|
/* not elegant but works for umounting FUSE
|
||||||
fuse_chan must receieve a oper buf to unblock fuse_session_receive_buf function.
|
fuse_chan must receieve a oper buf to unblock fuse_session_receive_buf function.
|
||||||
*/
|
*/
|
||||||
winpr_PathFileExists(file->path);
|
winpr_PathFileExists(file->path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void cliprdr_file_context_free(CliprdrFileContext* file)
|
void cliprdr_file_context_free(CliprdrFileContext* file)
|
||||||
{
|
{
|
||||||
if (!file)
|
if (!file)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
|
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
|
||||||
if (file->fuse_thread)
|
if (file->fuse_thread)
|
||||||
{
|
{
|
||||||
cliprdr_file_session_terminate(file);
|
cliprdr_file_session_terminate(file);
|
||||||
WaitForSingleObject(file->fuse_thread, INFINITE);
|
WaitForSingleObject(file->fuse_thread, INFINITE);
|
||||||
CloseHandle(file->fuse_thread);
|
CloseHandle(file->fuse_thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
// fuse related
|
// fuse related
|
||||||
ArrayList_Free(file->stream_list);
|
ArrayList_Free(file->stream_list);
|
||||||
ArrayList_Free(file->ino_list);
|
ArrayList_Free(file->ino_list);
|
||||||
#endif
|
#endif
|
||||||
winpr_RemoveDirectory(file->path);
|
HashTable_Free(file->local_streams);
|
||||||
free(file->path);
|
winpr_RemoveDirectory(file->path);
|
||||||
free(file);
|
free(file->path);
|
||||||
|
free(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
static BOOL create_base_path(CliprdrFileContext* file)
|
static BOOL create_base_path(CliprdrFileContext* file)
|
||||||
{
|
{
|
||||||
WINPR_ASSERT(file);
|
WINPR_ASSERT(file);
|
||||||
|
|
||||||
char base[64] = { 0 };
|
char base[64] = { 0 };
|
||||||
_snprintf(base, sizeof(base), "/.xfreerdp.cliprdr.%" PRIu32, GetCurrentProcessId());
|
_snprintf(base, sizeof(base), "/.xfreerdp.cliprdr.%" PRIu32, GetCurrentProcessId());
|
||||||
|
|
||||||
file->path = GetKnownSubPath(KNOWN_PATH_TEMP, base);
|
file->path = GetKnownSubPath(KNOWN_PATH_TEMP, base);
|
||||||
if (!file->path)
|
if (!file->path)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
if (!winpr_PathFileExists(file->path) && !winpr_PathMakePath(file->path, 0))
|
if (!winpr_PathFileExists(file->path) && !winpr_PathMakePath(file->path, 0))
|
||||||
{
|
{
|
||||||
WLog_ERR(TAG, "Failed to create directory '%s'", file->path);
|
WLog_Print(file->log, WLOG_ERROR, "Failed to create directory '%s'", file->path);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cliprdr_local_file_free(CliprdrLocalFile* file)
|
||||||
|
{
|
||||||
|
if (!file)
|
||||||
|
return;
|
||||||
|
if (file->fp)
|
||||||
|
fclose(file->fp);
|
||||||
|
free(file->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cliprdr_local_stream_free(void* obj)
|
||||||
|
{
|
||||||
|
CliprdrLocalStream* stream = (CliprdrLocalStream*)obj;
|
||||||
|
if (stream)
|
||||||
|
{
|
||||||
|
for (size_t x = 0; x < stream->count; x++)
|
||||||
|
cliprdr_local_file_free(&stream->files[x]);
|
||||||
|
free(stream->files);
|
||||||
|
}
|
||||||
|
free(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL cliprdr_local_stream_update(CliprdrLocalStream* stream, const char* data, size_t size)
|
||||||
|
{
|
||||||
|
WINPR_ASSERT(stream);
|
||||||
|
if (size == 0)
|
||||||
|
return TRUE;
|
||||||
|
|
||||||
|
free(stream->files);
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
stream->files[stream->count++].name = _strdup(ptr);
|
||||||
|
ptr = strtok(NULL, "\r\n");
|
||||||
|
}
|
||||||
|
free(copy);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
CliprdrLocalStream* cliprdr_local_stream_new(UINT32 streamID, const char* data, size_t size)
|
||||||
|
{
|
||||||
|
CliprdrLocalStream* stream = calloc(1, sizeof(CliprdrLocalStream));
|
||||||
|
if (!stream)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
if (!cliprdr_local_stream_update(stream, data, size))
|
||||||
|
goto fail;
|
||||||
|
|
||||||
|
stream->streamID = streamID;
|
||||||
|
return stream;
|
||||||
|
|
||||||
|
fail:
|
||||||
|
cliprdr_local_stream_free(stream);
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
CliprdrFileContext* cliprdr_file_context_new(void* context)
|
CliprdrFileContext* cliprdr_file_context_new(void* context)
|
||||||
{
|
{
|
||||||
CliprdrFileContext* file = calloc(1, sizeof(CliprdrFileContext));
|
CliprdrFileContext* file = calloc(1, sizeof(CliprdrFileContext));
|
||||||
if (!file)
|
if (!file)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
file->clipboard = context;
|
file->log = WLog_Get(CLIENT_TAG("x11.cliprdr.file"));
|
||||||
if (!create_base_path(file))
|
file->clipboard = context;
|
||||||
goto fail;
|
if (!create_base_path(file))
|
||||||
|
goto fail;
|
||||||
|
|
||||||
|
file->local_streams = HashTable_New(FALSE);
|
||||||
|
if (!file->local_streams)
|
||||||
|
goto fail;
|
||||||
|
|
||||||
|
wObject* hobj = HashTable_ValueObject(file->local_streams);
|
||||||
|
WINPR_ASSERT(hobj);
|
||||||
|
hobj->fnObjectFree = cliprdr_local_stream_free;
|
||||||
|
|
||||||
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
|
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
|
||||||
file->current_stream_id = 0;
|
file->current_stream_id = 0;
|
||||||
file->stream_list = ArrayList_New(TRUE);
|
file->stream_list = ArrayList_New(TRUE);
|
||||||
if (!file->stream_list)
|
if (!file->stream_list)
|
||||||
{
|
{
|
||||||
WLog_ERR(TAG, "failed to allocate stream_list");
|
WLog_Print(file->log, WLOG_ERROR, "failed to allocate stream_list");
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
wObject* obj = ArrayList_Object(file->stream_list);
|
wObject* obj = ArrayList_Object(file->stream_list);
|
||||||
obj->fnObjectFree = free;
|
obj->fnObjectFree = free;
|
||||||
|
|
||||||
file->ino_list = ArrayList_New(TRUE);
|
file->ino_list = ArrayList_New(TRUE);
|
||||||
if (!file->ino_list)
|
if (!file->ino_list)
|
||||||
{
|
{
|
||||||
WLog_ERR(TAG, "failed to allocate stream_list");
|
WLog_Print(file->log, WLOG_ERROR, "failed to allocate stream_list");
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
obj = ArrayList_Object(file->ino_list);
|
obj = ArrayList_Object(file->ino_list);
|
||||||
obj->fnObjectFree = cliprdr_file_fuse_inode_free;
|
obj->fnObjectFree = cliprdr_file_fuse_inode_free;
|
||||||
|
|
||||||
if (!xf_fuse_repopulate(file->ino_list))
|
if (!xf_fuse_repopulate(file, file->ino_list))
|
||||||
goto fail;
|
goto fail;
|
||||||
|
|
||||||
if (!(file->fuse_thread = CreateThread(NULL, 0, cliprdr_file_fuse_thread, file, 0, NULL)))
|
if (!(file->fuse_thread = CreateThread(NULL, 0, cliprdr_file_fuse_thread, file, 0, NULL)))
|
||||||
{
|
{
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
return file;
|
return file;
|
||||||
|
|
||||||
fail:
|
fail:
|
||||||
cliprdr_file_context_free(file);
|
cliprdr_file_context_free(file);
|
||||||
return NULL;
|
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)
|
BOOL cliprdr_file_context_clear(CliprdrFileContext* file)
|
||||||
{
|
{
|
||||||
|
HashTable_Foreach(file->local_streams, local_stream_discard, file);
|
||||||
|
|
||||||
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
|
#if defined(WITH_FUSE2) || defined(WITH_FUSE3)
|
||||||
if (file->stream_list)
|
if (file->stream_list)
|
||||||
{
|
{
|
||||||
size_t index;
|
size_t index;
|
||||||
size_t count;
|
size_t count;
|
||||||
CliprdrFuseStream* stream;
|
CliprdrFuseStream* stream;
|
||||||
ArrayList_Lock(file->stream_list);
|
ArrayList_Lock(file->stream_list);
|
||||||
file->current_stream_id = 0;
|
file->current_stream_id = 0;
|
||||||
/* reply error to all req first don't care request type*/
|
/* reply error to all req first don't care request type*/
|
||||||
count = ArrayList_Count(file->stream_list);
|
count = ArrayList_Count(file->stream_list);
|
||||||
for (index = 0; index < count; index++)
|
for (index = 0; index < count; index++)
|
||||||
{
|
{
|
||||||
stream = (CliprdrFuseStream*)ArrayList_GetItem(file->stream_list, index);
|
stream = (CliprdrFuseStream*)ArrayList_GetItem(file->stream_list, index);
|
||||||
fuse_reply_err(stream->req, EIO);
|
fuse_reply_err(stream->req, EIO);
|
||||||
}
|
}
|
||||||
ArrayList_Unlock(file->stream_list);
|
ArrayList_Unlock(file->stream_list);
|
||||||
|
|
||||||
ArrayList_Clear(file->stream_list);
|
ArrayList_Clear(file->stream_list);
|
||||||
}
|
}
|
||||||
|
|
||||||
xf_fuse_repopulate(file->ino_list);
|
xf_fuse_repopulate(file, file->ino_list);
|
||||||
#endif
|
#endif
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL cliprdr_file_context_update_client_data(CliprdrFileContext* file, const char* data,
|
||||||
|
size_t size)
|
||||||
|
{
|
||||||
|
BOOL rc = FALSE;
|
||||||
|
UINT32 streamID = file->local_stream_id++;
|
||||||
|
|
||||||
|
HashTable_Lock(file->local_streams);
|
||||||
|
CliprdrLocalStream* stream =
|
||||||
|
HashTable_GetItemValue(file->local_streams, (void*)(uintptr_t)streamID);
|
||||||
|
|
||||||
|
if (stream)
|
||||||
|
rc = cliprdr_local_stream_update(stream, data, size);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stream = cliprdr_local_stream_new(streamID, data, size);
|
||||||
|
rc = HashTable_Insert(file->local_streams, (void*)(uintptr_t)streamID, 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;
|
||||||
}
|
}
|
|
@ -33,6 +33,13 @@ extern "C"
|
||||||
FREERDP_API CliprdrFileContext* cliprdr_file_context_new(void* context);
|
FREERDP_API CliprdrFileContext* cliprdr_file_context_new(void* context);
|
||||||
FREERDP_API void cliprdr_file_context_free(CliprdrFileContext* file);
|
FREERDP_API void cliprdr_file_context_free(CliprdrFileContext* file);
|
||||||
|
|
||||||
|
FREERDP_API BOOL cliprdr_file_context_set_locally_available(CliprdrFileContext* file,
|
||||||
|
BOOL available);
|
||||||
|
FREERDP_API BOOL cliprdr_file_context_remote_set_flags(CliprdrFileContext* file, UINT32 flags);
|
||||||
|
FREERDP_API UINT32 cliprdr_file_context_remote_get_flags(CliprdrFileContext* file);
|
||||||
|
|
||||||
|
FREERDP_API UINT32 cliprdr_file_context_current_flags(CliprdrFileContext* file);
|
||||||
|
|
||||||
FREERDP_API void* cliprdr_file_context_get_context(CliprdrFileContext* file);
|
FREERDP_API void* cliprdr_file_context_get_context(CliprdrFileContext* file);
|
||||||
FREERDP_API const char* cliprdr_file_context_base_path(CliprdrFileContext* file);
|
FREERDP_API const char* cliprdr_file_context_base_path(CliprdrFileContext* file);
|
||||||
|
|
||||||
|
@ -43,8 +50,11 @@ extern "C"
|
||||||
|
|
||||||
FREERDP_API BOOL cliprdr_file_context_clear(CliprdrFileContext* file);
|
FREERDP_API BOOL cliprdr_file_context_clear(CliprdrFileContext* file);
|
||||||
|
|
||||||
FREERDP_API BOOL cliprdr_file_context_update_data(CliprdrFileContext* file, const void* data,
|
FREERDP_API BOOL cliprdr_file_context_update_client_data(CliprdrFileContext* file,
|
||||||
size_t size);
|
const char* data, size_t count);
|
||||||
|
|
||||||
|
FREERDP_API BOOL cliprdr_file_context_update_server_data(CliprdrFileContext* file,
|
||||||
|
const void* data, size_t size);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
Loading…
Reference in New Issue