From 6672087f7c24bd204c4cdeff6df6ac2dd75fa9b4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 14 Feb 2022 17:34:09 +0800 Subject: [PATCH] windows clipboard Signed-off-by: fufesou --- .gitignore | 1 + Cargo.lock | 11 + Cargo.toml | 3 +- libs/clipboard/Cargo.toml | 15 + libs/clipboard/README.md | 16 + libs/clipboard/build.rs | 43 + libs/clipboard/docs/assets/scene3.png | Bin 0 -> 7346 bytes libs/clipboard/docs/assets/win_A_B.png | Bin 0 -> 53354 bytes libs/clipboard/docs/assets/win_B_A.png | Bin 0 -> 54430 bytes libs/clipboard/src/OSX/Clipboard.m | 11 + libs/clipboard/src/X11/xf_cliprdr.c | 11 + libs/clipboard/src/cliprdr.h | 231 ++ libs/clipboard/src/cliprdr.rs | 566 +++++ libs/clipboard/src/lib.rs | 548 +++++ libs/clipboard/src/windows/wf_cliprdr.c | 2789 +++++++++++++++++++++++ libs/hbb_common/protos/message.proto | 68 + src/common.rs | 1 + src/server.rs | 4 + src/server/cliprdr_service.rs | 104 + src/server/connection.rs | 17 + src/server/service.rs | 6 + src/ui/remote.rs | 62 + 22 files changed, 4506 insertions(+), 1 deletion(-) create mode 100644 libs/clipboard/Cargo.toml create mode 100644 libs/clipboard/README.md create mode 100644 libs/clipboard/build.rs create mode 100644 libs/clipboard/docs/assets/scene3.png create mode 100644 libs/clipboard/docs/assets/win_A_B.png create mode 100644 libs/clipboard/docs/assets/win_B_A.png create mode 100644 libs/clipboard/src/OSX/Clipboard.m create mode 100644 libs/clipboard/src/X11/xf_cliprdr.c create mode 100644 libs/clipboard/src/cliprdr.h create mode 100644 libs/clipboard/src/cliprdr.rs create mode 100644 libs/clipboard/src/lib.rs create mode 100644 libs/clipboard/src/windows/wf_cliprdr.c create mode 100644 src/server/cliprdr_service.rs diff --git a/.gitignore b/.gitignore index cce3b90a1..56f4c29c6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ __pycache__ src/version.rs *dmg sciter.dll +**pdb diff --git a/Cargo.lock b/Cargo.lock index ebc50fcb6..32e50572e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -388,6 +388,16 @@ dependencies = [ "textwrap 0.14.2", ] +[[package]] +name = "clipboard" +version = "0.1.0" +dependencies = [ + "cc", + "hbb_common", + "lazy_static", + "thiserror", +] + [[package]] name = "clipboard-master" version = "3.1.3" @@ -3365,6 +3375,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "clap 3.0.10", + "clipboard", "clipboard-master", "cocoa 0.24.0", "core-foundation 0.9.2", diff --git a/Cargo.toml b/Cargo.toml index a6218e983..7cab6e7a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ whoami = "1.2" scrap = { path = "libs/scrap" } hbb_common = { path = "libs/hbb_common" } enigo = { path = "libs/enigo" } +clipboard = { path = "libs/clipboard" } sys-locale = "0.1" serde_derive = "1.0" serde = "1.0" @@ -82,7 +83,7 @@ rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" } android_logger = "0.10" [workspace] -members = ["libs/scrap", "libs/hbb_common", "libs/enigo"] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard"] [package.metadata.winres] LegalCopyright = "Copyright © 2020" diff --git a/libs/clipboard/Cargo.toml b/libs/clipboard/Cargo.toml new file mode 100644 index 000000000..3d2b5ef77 --- /dev/null +++ b/libs/clipboard/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "clipboard" +version = "0.1.0" +edition = "2021" +build= "build.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +cc = "1.0" + +[dependencies] +thiserror = "1.0.30" +lazy_static = "1.4" +hbb_common = { path = "../hbb_common" } diff --git a/libs/clipboard/README.md b/libs/clipboard/README.md new file mode 100644 index 000000000..ea2b85785 --- /dev/null +++ b/libs/clipboard/README.md @@ -0,0 +1,16 @@ +# clipboard + +Copy files and text through network. +Main lowlevel logic from [FreeRDP](https://github.com/FreeRDP/FreeRDP). + +TODO: Move this lib to a separate project. + +## impl + +### windows + +![scene](./docs/assets/scene3.png) + +![A1->B1](./docs/assets/win_A_B.png) + +![B1->A1](./docs/assets/win_B_A.png) diff --git a/libs/clipboard/build.rs b/libs/clipboard/build.rs new file mode 100644 index 000000000..b5c547637 --- /dev/null +++ b/libs/clipboard/build.rs @@ -0,0 +1,43 @@ +use cc; + +fn build_c_impl() { + let mut build = cc::Build::new(); + + #[cfg(target_os = "windows")] + build.file("src/windows/wf_cliprdr.c"); + #[cfg(target_os = "linux")] + build.file("src/X11/xf_cliprdr.c"); + #[cfg(target_os = "macos")] + build.file("src/OSX/Clipboard.m"); + + build.flag_if_supported("-Wno-c++0x-extensions"); + build.flag_if_supported("-Wno-return-type-c-linkage"); + build.flag_if_supported("-Wno-invalid-offsetof"); + build.flag_if_supported("-Wno-unused-parameter"); + + if build.get_compiler().is_like_msvc() { + build.define("WIN32", ""); + // build.define("_AMD64_", ""); + build.flag("-Zi"); + build.flag("-GR-"); + // build.flag("-std:c++11"); + } else { + build.flag("-fPIC"); + // build.flag("-std=c++11"); + // build.flag("-include"); + // build.flag(&confdefs_path.to_string_lossy()); + } + + build.compile("mycliprdr"); + + #[cfg(target_os = "windows")] + println!("cargo:rerun-if-changed=src/windows/wf_cliprdr.c"); + #[cfg(target_os = "linux")] + println!("cargo:rerun-if-changed=src/X11/xf_cliprdr.c"); + #[cfg(target_os = "macos")] + println!("cargo:rerun-if-changed=src/OSX/Clipboard.m"); +} + +fn main() { + build_c_impl(); +} diff --git a/libs/clipboard/docs/assets/scene3.png b/libs/clipboard/docs/assets/scene3.png new file mode 100644 index 0000000000000000000000000000000000000000..6392364604de5eeb899a7c5e98e705a4ba7ae0eb GIT binary patch literal 7346 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 literal 0 HcmV?d00001 diff --git a/libs/clipboard/docs/assets/win_B_A.png b/libs/clipboard/docs/assets/win_B_A.png new file mode 100644 index 0000000000000000000000000000000000000000..377fa801f973be79c975c4337fe0817fca3d2da3 GIT binary patch literal 54430 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 z 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 R<}L6~T0&mDL{!iBe*rIU#v}j$ literal 0 HcmV?d00001 diff --git a/libs/clipboard/src/OSX/Clipboard.m b/libs/clipboard/src/OSX/Clipboard.m new file mode 100644 index 000000000..658579ce0 --- /dev/null +++ b/libs/clipboard/src/OSX/Clipboard.m @@ -0,0 +1,11 @@ +#include "../cliprdr.h" + +void mac_cliprdr_init(CliprdrClientContext *cliprdr) +{ + (void)cliprdr; +} + +void mac_cliprdr_uninit(CliprdrClientContext *cliprdr) +{ + (void)cliprdr; +} diff --git a/libs/clipboard/src/X11/xf_cliprdr.c b/libs/clipboard/src/X11/xf_cliprdr.c new file mode 100644 index 000000000..55d34a088 --- /dev/null +++ b/libs/clipboard/src/X11/xf_cliprdr.c @@ -0,0 +1,11 @@ +#include "../cliprdr.h" + +void xf_cliprdr_init(CliprdrClientContext* cliprdr) +{ + (void)cliprdr; +} + +void xf_cliprdr_uninit( CliprdrClientContext* cliprdr) +{ + (void)cliprdr; +} diff --git a/libs/clipboard/src/cliprdr.h b/libs/clipboard/src/cliprdr.h new file mode 100644 index 000000000..0e581cc2f --- /dev/null +++ b/libs/clipboard/src/cliprdr.h @@ -0,0 +1,231 @@ +#ifndef WF_CLIPRDR_H__ +#define WF_CLIPRDR_H__ + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef signed char INT8, *PINT8; + typedef signed short INT16, *PINT16; + typedef signed int INT32, *PINT32; + typedef unsigned char UINT8, *PUINT8; + typedef unsigned short UINT16, *PUINT16; + typedef unsigned int UINT32, *PUINT32; + typedef unsigned int UINT; + typedef int BOOL; + typedef unsigned char BYTE; + +/* Clipboard Messages */ +#define DEFINE_CLIPRDR_HEADER_COMMON() \ + UINT32 serverConnID; \ + UINT32 remoteConnID; \ + UINT16 msgType; \ + UINT16 msgFlags; \ + UINT32 dataLen + + struct _CLIPRDR_HEADER + { + DEFINE_CLIPRDR_HEADER_COMMON(); + }; + typedef struct _CLIPRDR_HEADER CLIPRDR_HEADER; + + struct _CLIPRDR_CAPABILITY_SET + { + UINT16 capabilitySetType; + UINT16 capabilitySetLength; + }; + typedef struct _CLIPRDR_CAPABILITY_SET CLIPRDR_CAPABILITY_SET; + + struct _CLIPRDR_GENERAL_CAPABILITY_SET + { + UINT16 capabilitySetType; + UINT16 capabilitySetLength; + + UINT32 version; + UINT32 generalFlags; + }; + typedef struct _CLIPRDR_GENERAL_CAPABILITY_SET CLIPRDR_GENERAL_CAPABILITY_SET; + + struct _CLIPRDR_CAPABILITIES + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 cCapabilitiesSets; + CLIPRDR_CAPABILITY_SET *capabilitySets; + }; + typedef struct _CLIPRDR_CAPABILITIES CLIPRDR_CAPABILITIES; + + struct _CLIPRDR_MONITOR_READY + { + DEFINE_CLIPRDR_HEADER_COMMON(); + }; + typedef struct _CLIPRDR_MONITOR_READY CLIPRDR_MONITOR_READY; + + struct _CLIPRDR_TEMP_DIRECTORY + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + char szTempDir[520]; + }; + typedef struct _CLIPRDR_TEMP_DIRECTORY CLIPRDR_TEMP_DIRECTORY; + + struct _CLIPRDR_FORMAT + { + UINT32 formatId; + char *formatName; + }; + typedef struct _CLIPRDR_FORMAT CLIPRDR_FORMAT; + + struct _CLIPRDR_FORMAT_LIST + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 numFormats; + CLIPRDR_FORMAT *formats; + }; + typedef struct _CLIPRDR_FORMAT_LIST CLIPRDR_FORMAT_LIST; + + struct _CLIPRDR_FORMAT_LIST_RESPONSE + { + DEFINE_CLIPRDR_HEADER_COMMON(); + }; + typedef struct _CLIPRDR_FORMAT_LIST_RESPONSE CLIPRDR_FORMAT_LIST_RESPONSE; + + struct _CLIPRDR_LOCK_CLIPBOARD_DATA + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 clipDataId; + }; + typedef struct _CLIPRDR_LOCK_CLIPBOARD_DATA CLIPRDR_LOCK_CLIPBOARD_DATA; + + struct _CLIPRDR_UNLOCK_CLIPBOARD_DATA + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 clipDataId; + }; + typedef struct _CLIPRDR_UNLOCK_CLIPBOARD_DATA CLIPRDR_UNLOCK_CLIPBOARD_DATA; + + struct _CLIPRDR_FORMAT_DATA_REQUEST + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 requestedFormatId; + }; + typedef struct _CLIPRDR_FORMAT_DATA_REQUEST CLIPRDR_FORMAT_DATA_REQUEST; + + struct _CLIPRDR_FORMAT_DATA_RESPONSE + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + const BYTE *requestedFormatData; + }; + typedef struct _CLIPRDR_FORMAT_DATA_RESPONSE CLIPRDR_FORMAT_DATA_RESPONSE; + + struct _CLIPRDR_FILE_CONTENTS_REQUEST + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 streamId; + UINT32 listIndex; + UINT32 dwFlags; + UINT32 nPositionLow; + UINT32 nPositionHigh; + UINT32 cbRequested; + BOOL haveClipDataId; + UINT32 clipDataId; + }; + typedef struct _CLIPRDR_FILE_CONTENTS_REQUEST CLIPRDR_FILE_CONTENTS_REQUEST; + + struct _CLIPRDR_FILE_CONTENTS_RESPONSE + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 streamId; + UINT32 cbRequested; + const BYTE *requestedData; + }; + typedef struct _CLIPRDR_FILE_CONTENTS_RESPONSE CLIPRDR_FILE_CONTENTS_RESPONSE; + + typedef struct _cliprdr_client_context CliprdrClientContext; + + typedef UINT (*pcCliprdrServerCapabilities)(CliprdrClientContext *context, + const CLIPRDR_CAPABILITIES *capabilities); + typedef UINT (*pcCliprdrClientCapabilities)(CliprdrClientContext *context, + const CLIPRDR_CAPABILITIES *capabilities); + typedef UINT (*pcCliprdrMonitorReady)(CliprdrClientContext *context, + const CLIPRDR_MONITOR_READY *monitorReady); + typedef UINT (*pcCliprdrTempDirectory)(CliprdrClientContext *context, + const CLIPRDR_TEMP_DIRECTORY *tempDirectory); + typedef UINT (*pcCliprdrClientFormatList)(CliprdrClientContext *context, + const CLIPRDR_FORMAT_LIST *formatList); + typedef UINT (*pcCliprdrServerFormatList)(CliprdrClientContext *context, + const CLIPRDR_FORMAT_LIST *formatList); + typedef UINT (*pcCliprdrClientFormatListResponse)( + CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse); + typedef UINT (*pcCliprdrServerFormatListResponse)( + CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse); + typedef UINT (*pcCliprdrClientLockClipboardData)( + CliprdrClientContext *context, const CLIPRDR_LOCK_CLIPBOARD_DATA *lockClipboardData); + typedef UINT (*pcCliprdrServerLockClipboardData)( + CliprdrClientContext *context, const CLIPRDR_LOCK_CLIPBOARD_DATA *lockClipboardData); + typedef UINT (*pcCliprdrClientUnlockClipboardData)( + CliprdrClientContext *context, const CLIPRDR_UNLOCK_CLIPBOARD_DATA *unlockClipboardData); + typedef UINT (*pcCliprdrServerUnlockClipboardData)( + CliprdrClientContext *context, const CLIPRDR_UNLOCK_CLIPBOARD_DATA *unlockClipboardData); + typedef UINT (*pcCliprdrClientFormatDataRequest)( + CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest); + typedef UINT (*pcCliprdrServerFormatDataRequest)( + CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest); + typedef UINT (*pcCliprdrClientFormatDataResponse)( + CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse); + typedef UINT (*pcCliprdrServerFormatDataResponse)( + CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse); + typedef UINT (*pcCliprdrClientFileContentsRequest)( + CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest); + typedef UINT (*pcCliprdrServerFileContentsRequest)( + CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest); + typedef UINT (*pcCliprdrClientFileContentsResponse)( + CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse); + typedef UINT (*pcCliprdrServerFileContentsResponse)( + CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse); + + // TODO: hide more members of clipboard context + struct _cliprdr_client_context + { + void *custom; + BOOL enableFiles; + BOOL enableOthers; + + pcCliprdrServerCapabilities ServerCapabilities; + pcCliprdrClientCapabilities ClientCapabilities; + pcCliprdrMonitorReady MonitorReady; + pcCliprdrTempDirectory TempDirectory; + pcCliprdrClientFormatList ClientFormatList; + pcCliprdrServerFormatList ServerFormatList; + pcCliprdrClientFormatListResponse ClientFormatListResponse; + pcCliprdrServerFormatListResponse ServerFormatListResponse; + pcCliprdrClientLockClipboardData ClientLockClipboardData; + pcCliprdrServerLockClipboardData ServerLockClipboardData; + pcCliprdrClientUnlockClipboardData ClientUnlockClipboardData; + pcCliprdrServerUnlockClipboardData ServerUnlockClipboardData; + pcCliprdrClientFormatDataRequest ClientFormatDataRequest; + pcCliprdrServerFormatDataRequest ServerFormatDataRequest; + pcCliprdrClientFormatDataResponse ClientFormatDataResponse; + pcCliprdrServerFormatDataResponse ServerFormatDataResponse; + pcCliprdrClientFileContentsRequest ClientFileContentsRequest; + pcCliprdrServerFileContentsRequest ServerFileContentsRequest; + pcCliprdrClientFileContentsResponse ClientFileContentsResponse; + pcCliprdrServerFileContentsResponse ServerFileContentsResponse; + + UINT32 lastRequestedFormatId; + }; + +#ifdef __cplusplus +} +#endif + +#endif // WF_CLIPRDR_H__ + diff --git a/libs/clipboard/src/cliprdr.rs b/libs/clipboard/src/cliprdr.rs new file mode 100644 index 000000000..b88d9c40d --- /dev/null +++ b/libs/clipboard/src/cliprdr.rs @@ -0,0 +1,566 @@ +#![allow(dead_code)] +#![allow(non_camel_case_types)] +#![allow(unused_variables)] +#![allow(non_snake_case)] +#![allow(deref_nullptr)] + +use std::{boxed::Box, result::Result}; +use thiserror::Error; + +pub type size_t = ::std::os::raw::c_ulonglong; +pub type __vcrt_bool = bool; +pub type wchar_t = ::std::os::raw::c_ushort; + +pub type POINTER_64_INT = ::std::os::raw::c_ulonglong; +pub type INT8 = ::std::os::raw::c_schar; +pub type PINT8 = *mut ::std::os::raw::c_schar; +pub type INT16 = ::std::os::raw::c_short; +pub type PINT16 = *mut ::std::os::raw::c_short; +pub type INT32 = ::std::os::raw::c_int; +pub type PINT32 = *mut ::std::os::raw::c_int; +pub type INT64 = ::std::os::raw::c_longlong; +pub type PINT64 = *mut ::std::os::raw::c_longlong; +pub type UINT8 = ::std::os::raw::c_uchar; +pub type PUINT8 = *mut ::std::os::raw::c_uchar; +pub type UINT16 = ::std::os::raw::c_ushort; +pub type PUINT16 = *mut ::std::os::raw::c_ushort; +pub type UINT32 = ::std::os::raw::c_uint; +pub type PUINT32 = *mut ::std::os::raw::c_uint; +pub type UINT64 = ::std::os::raw::c_ulonglong; +pub type PUINT64 = *mut ::std::os::raw::c_ulonglong; +pub type LONG32 = ::std::os::raw::c_int; +pub type PLONG32 = *mut ::std::os::raw::c_int; +pub type ULONG32 = ::std::os::raw::c_uint; +pub type PULONG32 = *mut ::std::os::raw::c_uint; +pub type DWORD32 = ::std::os::raw::c_uint; +pub type PDWORD32 = *mut ::std::os::raw::c_uint; +pub type INT_PTR = ::std::os::raw::c_longlong; +pub type PINT_PTR = *mut ::std::os::raw::c_longlong; +pub type UINT_PTR = ::std::os::raw::c_ulonglong; +pub type PUINT_PTR = *mut ::std::os::raw::c_ulonglong; +pub type LONG_PTR = ::std::os::raw::c_longlong; +pub type PLONG_PTR = *mut ::std::os::raw::c_longlong; +pub type ULONG_PTR = ::std::os::raw::c_ulonglong; +pub type PULONG_PTR = *mut ::std::os::raw::c_ulonglong; +pub type SHANDLE_PTR = ::std::os::raw::c_longlong; +pub type HANDLE_PTR = ::std::os::raw::c_ulonglong; +pub type UHALF_PTR = ::std::os::raw::c_uint; +pub type PUHALF_PTR = *mut ::std::os::raw::c_uint; +pub type HALF_PTR = ::std::os::raw::c_int; +pub type PHALF_PTR = *mut ::std::os::raw::c_int; +pub type SIZE_T = ULONG_PTR; +pub type PSIZE_T = *mut ULONG_PTR; +pub type SSIZE_T = LONG_PTR; +pub type PSSIZE_T = *mut LONG_PTR; +pub type DWORD_PTR = ULONG_PTR; +pub type PDWORD_PTR = *mut ULONG_PTR; +pub type LONG64 = ::std::os::raw::c_longlong; +pub type PLONG64 = *mut ::std::os::raw::c_longlong; +pub type ULONG64 = ::std::os::raw::c_ulonglong; +pub type PULONG64 = *mut ::std::os::raw::c_ulonglong; +pub type DWORD64 = ::std::os::raw::c_ulonglong; +pub type PDWORD64 = *mut ::std::os::raw::c_ulonglong; +pub type KAFFINITY = ULONG_PTR; +pub type PKAFFINITY = *mut KAFFINITY; +pub type PVOID = *mut ::std::os::raw::c_void; +pub type CHAR = ::std::os::raw::c_char; +pub type SHORT = ::std::os::raw::c_short; +pub type LONG = ::std::os::raw::c_long; +pub type WCHAR = wchar_t; +pub type PWCHAR = *mut WCHAR; +pub type LPWCH = *mut WCHAR; +pub type PWCH = *mut WCHAR; +pub type LPCWCH = *const WCHAR; +pub type PCWCH = *const WCHAR; +pub type NWPSTR = *mut WCHAR; +pub type LPWSTR = *mut WCHAR; +pub type PWSTR = *mut WCHAR; +pub type PZPWSTR = *mut PWSTR; +pub type PCZPWSTR = *const PWSTR; +pub type LPUWSTR = *mut WCHAR; +pub type PUWSTR = *mut WCHAR; +pub type LPCWSTR = *const WCHAR; +pub type PCWSTR = *const WCHAR; +pub type PZPCWSTR = *mut PCWSTR; +pub type PCZPCWSTR = *const PCWSTR; +pub type LPCUWSTR = *const WCHAR; +pub type PCUWSTR = *const WCHAR; +pub type PZZWSTR = *mut WCHAR; +pub type PCZZWSTR = *const WCHAR; +pub type PUZZWSTR = *mut WCHAR; +pub type PCUZZWSTR = *const WCHAR; +pub type PNZWCH = *mut WCHAR; +pub type PCNZWCH = *const WCHAR; +pub type PUNZWCH = *mut WCHAR; +pub type PCUNZWCH = *const WCHAR; +pub type PCHAR = *mut CHAR; +pub type LPCH = *mut CHAR; +pub type PCH = *mut CHAR; +pub type LPCCH = *const CHAR; +pub type PCCH = *const CHAR; +pub type NPSTR = *mut CHAR; +pub type LPSTR = *mut CHAR; +pub type PSTR = *mut CHAR; +pub type PZPSTR = *mut PSTR; +pub type PCZPSTR = *const PSTR; +pub type LPCSTR = *const CHAR; +pub type PCSTR = *const CHAR; +pub type PZPCSTR = *mut PCSTR; +pub type PCZPCSTR = *const PCSTR; +pub type PZZSTR = *mut CHAR; +pub type PCZZSTR = *const CHAR; +pub type PNZCH = *mut CHAR; +pub type PCNZCH = *const CHAR; +pub type TCHAR = ::std::os::raw::c_char; +pub type PTCHAR = *mut ::std::os::raw::c_char; +pub type TBYTE = ::std::os::raw::c_uchar; +pub type PTBYTE = *mut ::std::os::raw::c_uchar; +pub type LPTCH = LPCH; +pub type PTCH = LPCH; +pub type LPCTCH = LPCCH; +pub type PCTCH = LPCCH; +pub type PTSTR = LPSTR; +pub type LPTSTR = LPSTR; +pub type PUTSTR = LPSTR; +pub type LPUTSTR = LPSTR; +pub type PCTSTR = LPCSTR; +pub type LPCTSTR = LPCSTR; +pub type PCUTSTR = LPCSTR; +pub type LPCUTSTR = LPCSTR; +pub type PZZTSTR = PZZSTR; +pub type PUZZTSTR = PZZSTR; +pub type PCZZTSTR = PCZZSTR; +pub type PCUZZTSTR = PCZZSTR; +pub type PZPTSTR = PZPSTR; +pub type PNZTCH = PNZCH; +pub type PUNZTCH = PNZCH; +pub type PCNZTCH = PCNZCH; +pub type PCUNZTCH = PCNZCH; +pub type PSHORT = *mut SHORT; +pub type PLONG = *mut LONG; +pub type ULONG = ::std::os::raw::c_ulong; +pub type PULONG = *mut ULONG; +pub type USHORT = ::std::os::raw::c_ushort; +pub type PUSHORT = *mut USHORT; +pub type UCHAR = ::std::os::raw::c_uchar; +pub type PUCHAR = *mut UCHAR; +pub type PSZ = *mut ::std::os::raw::c_char; +pub type DWORD = ::std::os::raw::c_ulong; +pub type BOOL = ::std::os::raw::c_int; +pub type BYTE = ::std::os::raw::c_uchar; +pub type WORD = ::std::os::raw::c_ushort; +pub type FLOAT = f32; +pub type PFLOAT = *mut FLOAT; +pub type PBOOL = *mut BOOL; +pub type LPBOOL = *mut BOOL; +pub type PBYTE = *mut BYTE; +pub type LPBYTE = *mut BYTE; +pub type PINT = *mut ::std::os::raw::c_int; +pub type LPINT = *mut ::std::os::raw::c_int; +pub type PWORD = *mut WORD; +pub type LPWORD = *mut WORD; +pub type LPLONG = *mut ::std::os::raw::c_long; +pub type PDWORD = *mut DWORD; +pub type LPDWORD = *mut DWORD; +pub type LPVOID = *mut ::std::os::raw::c_void; +pub type LPCVOID = *const ::std::os::raw::c_void; +pub type INT = ::std::os::raw::c_int; +pub type UINT = ::std::os::raw::c_uint; +pub type PUINT = *mut ::std::os::raw::c_uint; +pub type va_list = *mut ::std::os::raw::c_char; + +pub const TRUE: ::std::os::raw::c_int = 1; +pub const FALSE: ::std::os::raw::c_int = 0; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_HEADER { + pub serverConnID: UINT32, + pub remoteConnID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, +} +pub type CLIPRDR_HEADER = _CLIPRDR_HEADER; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_CAPABILITY_SET { + pub capabilitySetType: UINT16, + pub capabilitySetLength: UINT16, +} +pub type CLIPRDR_CAPABILITY_SET = _CLIPRDR_CAPABILITY_SET; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_GENERAL_CAPABILITY_SET { + pub capabilitySetType: UINT16, + pub capabilitySetLength: UINT16, + pub version: UINT32, + pub generalFlags: UINT32, +} +pub type CLIPRDR_GENERAL_CAPABILITY_SET = _CLIPRDR_GENERAL_CAPABILITY_SET; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_CAPABILITIES { + pub serverConnID: UINT32, + pub remoteConnID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub cCapabilitiesSets: UINT32, + pub capabilitySets: *mut CLIPRDR_CAPABILITY_SET, +} +pub type CLIPRDR_CAPABILITIES = _CLIPRDR_CAPABILITIES; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_MONITOR_READY { + pub serverConnID: UINT32, + pub remoteConnID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, +} +pub type CLIPRDR_MONITOR_READY = _CLIPRDR_MONITOR_READY; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_TEMP_DIRECTORY { + pub serverConnID: UINT32, + pub remoteConnID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub szTempDir: [::std::os::raw::c_char; 520usize], +} +pub type CLIPRDR_TEMP_DIRECTORY = _CLIPRDR_TEMP_DIRECTORY; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FORMAT { + pub formatId: UINT32, + pub formatName: *mut ::std::os::raw::c_char, +} +pub type CLIPRDR_FORMAT = _CLIPRDR_FORMAT; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FORMAT_LIST { + pub serverConnID: UINT32, + pub remoteConnID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub numFormats: UINT32, + pub formats: *mut CLIPRDR_FORMAT, +} +pub type CLIPRDR_FORMAT_LIST = _CLIPRDR_FORMAT_LIST; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FORMAT_LIST_RESPONSE { + pub serverConnID: UINT32, + pub remoteConnID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, +} +pub type CLIPRDR_FORMAT_LIST_RESPONSE = _CLIPRDR_FORMAT_LIST_RESPONSE; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_LOCK_CLIPBOARD_DATA { + pub serverConnID: UINT32, + pub remoteConnID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub clipDataId: UINT32, +} +pub type CLIPRDR_LOCK_CLIPBOARD_DATA = _CLIPRDR_LOCK_CLIPBOARD_DATA; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_UNLOCK_CLIPBOARD_DATA { + pub serverConnID: UINT32, + pub remoteConnID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub clipDataId: UINT32, +} +pub type CLIPRDR_UNLOCK_CLIPBOARD_DATA = _CLIPRDR_UNLOCK_CLIPBOARD_DATA; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FORMAT_DATA_REQUEST { + pub serverConnID: UINT32, + pub remoteConnID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub requestedFormatId: UINT32, +} +pub type CLIPRDR_FORMAT_DATA_REQUEST = _CLIPRDR_FORMAT_DATA_REQUEST; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FORMAT_DATA_RESPONSE { + pub serverConnID: UINT32, + pub remoteConnID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub requestedFormatData: *const BYTE, +} +pub type CLIPRDR_FORMAT_DATA_RESPONSE = _CLIPRDR_FORMAT_DATA_RESPONSE; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FILE_CONTENTS_REQUEST { + pub serverConnID: UINT32, + pub remoteConnID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub streamId: UINT32, + pub listIndex: UINT32, + pub dwFlags: UINT32, + pub nPositionLow: UINT32, + pub nPositionHigh: UINT32, + pub cbRequested: UINT32, + pub haveClipDataId: BOOL, + pub clipDataId: UINT32, +} +pub type CLIPRDR_FILE_CONTENTS_REQUEST = _CLIPRDR_FILE_CONTENTS_REQUEST; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FILE_CONTENTS_RESPONSE { + pub serverConnID: UINT32, + pub remoteConnID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub streamId: UINT32, + pub cbRequested: UINT32, + pub requestedData: *const BYTE, +} +pub type CLIPRDR_FILE_CONTENTS_RESPONSE = _CLIPRDR_FILE_CONTENTS_RESPONSE; +pub type CliprdrClientContext = _cliprdr_client_context; +pub type pcCliprdrServerCapabilities = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + capabilities: *const CLIPRDR_CAPABILITIES, + ) -> UINT, +>; +pub type pcCliprdrClientCapabilities = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + capabilities: *const CLIPRDR_CAPABILITIES, + ) -> UINT, +>; +pub type pcCliprdrMonitorReady = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + monitorReady: *const CLIPRDR_MONITOR_READY, + ) -> UINT, +>; +pub type pcCliprdrTempDirectory = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + tempDirectory: *const CLIPRDR_TEMP_DIRECTORY, + ) -> UINT, +>; +pub type pcCliprdrClientFormatList = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatList: *const CLIPRDR_FORMAT_LIST, + ) -> UINT, +>; +pub type pcCliprdrServerFormatList = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatList: *const CLIPRDR_FORMAT_LIST, + ) -> UINT, +>; +pub type pcCliprdrClientFormatListResponse = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatListResponse: *const CLIPRDR_FORMAT_LIST_RESPONSE, + ) -> UINT, +>; +pub type pcCliprdrServerFormatListResponse = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatListResponse: *const CLIPRDR_FORMAT_LIST_RESPONSE, + ) -> UINT, +>; +pub type pcCliprdrClientLockClipboardData = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + lockClipboardData: *const CLIPRDR_LOCK_CLIPBOARD_DATA, + ) -> UINT, +>; +pub type pcCliprdrServerLockClipboardData = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + lockClipboardData: *const CLIPRDR_LOCK_CLIPBOARD_DATA, + ) -> UINT, +>; +pub type pcCliprdrClientUnlockClipboardData = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + unlockClipboardData: *const CLIPRDR_UNLOCK_CLIPBOARD_DATA, + ) -> UINT, +>; +pub type pcCliprdrServerUnlockClipboardData = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + unlockClipboardData: *const CLIPRDR_UNLOCK_CLIPBOARD_DATA, + ) -> UINT, +>; +pub type pcCliprdrClientFormatDataRequest = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatDataRequest: *const CLIPRDR_FORMAT_DATA_REQUEST, + ) -> UINT, +>; +pub type pcCliprdrServerFormatDataRequest = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatDataRequest: *const CLIPRDR_FORMAT_DATA_REQUEST, + ) -> UINT, +>; +pub type pcCliprdrClientFormatDataResponse = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatDataResponse: *const CLIPRDR_FORMAT_DATA_RESPONSE, + ) -> UINT, +>; +pub type pcCliprdrServerFormatDataResponse = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatDataResponse: *const CLIPRDR_FORMAT_DATA_RESPONSE, + ) -> UINT, +>; +pub type pcCliprdrClientFileContentsRequest = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + fileContentsRequest: *const CLIPRDR_FILE_CONTENTS_REQUEST, + ) -> UINT, +>; +pub type pcCliprdrServerFileContentsRequest = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + fileContentsRequest: *const CLIPRDR_FILE_CONTENTS_REQUEST, + ) -> UINT, +>; +pub type pcCliprdrClientFileContentsResponse = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + fileContentsResponse: *const CLIPRDR_FILE_CONTENTS_RESPONSE, + ) -> UINT, +>; +pub type pcCliprdrServerFileContentsResponse = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + fileContentsResponse: *const CLIPRDR_FILE_CONTENTS_RESPONSE, + ) -> UINT, +>; + +// TODO: hide more members of clipboard context +#[repr(C)] +#[derive(Debug, Clone)] +pub struct _cliprdr_client_context { + pub custom: *mut ::std::os::raw::c_void, + pub enableFiles: BOOL, + pub enableOthers: BOOL, + pub ServerCapabilities: pcCliprdrServerCapabilities, + pub ClientCapabilities: pcCliprdrClientCapabilities, + pub MonitorReady: pcCliprdrMonitorReady, + pub TempDirectory: pcCliprdrTempDirectory, + pub ClientFormatList: pcCliprdrClientFormatList, + pub ServerFormatList: pcCliprdrServerFormatList, + pub ClientFormatListResponse: pcCliprdrClientFormatListResponse, + pub ServerFormatListResponse: pcCliprdrServerFormatListResponse, + pub ClientLockClipboardData: pcCliprdrClientLockClipboardData, + pub ServerLockClipboardData: pcCliprdrServerLockClipboardData, + pub ClientUnlockClipboardData: pcCliprdrClientUnlockClipboardData, + pub ServerUnlockClipboardData: pcCliprdrServerUnlockClipboardData, + pub ClientFormatDataRequest: pcCliprdrClientFormatDataRequest, + pub ServerFormatDataRequest: pcCliprdrServerFormatDataRequest, + pub ClientFormatDataResponse: pcCliprdrClientFormatDataResponse, + pub ServerFormatDataResponse: pcCliprdrServerFormatDataResponse, + pub ClientFileContentsRequest: pcCliprdrClientFileContentsRequest, + pub ServerFileContentsRequest: pcCliprdrServerFileContentsRequest, + pub ClientFileContentsResponse: pcCliprdrClientFileContentsResponse, + pub ServerFileContentsResponse: pcCliprdrServerFileContentsResponse, + pub lastRequestedFormatId: UINT32, +} + +// #[link(name = "user32")] +// #[link(name = "ole32")] +extern "C" { + pub(crate) fn init_cliprdr(context: *mut CliprdrClientContext) -> BOOL; + pub(crate) fn uninit_cliprdr(context: *mut CliprdrClientContext) -> BOOL; +} + +#[derive(Error, Debug)] +pub enum CliprdrError { + #[error("invalid cliprdr name")] + CliprdrName, + #[error("failed to init cliprdr")] + CliprdrInit, + #[error("unknown cliprdr error")] + Unknown, +} + +impl CliprdrClientContext { + pub fn create( + enable_files: bool, + enable_others: bool, + client_format_list: pcCliprdrClientFormatList, + client_format_list_response: pcCliprdrClientFormatListResponse, + client_format_data_request: pcCliprdrClientFormatDataRequest, + client_format_data_response: pcCliprdrClientFormatDataResponse, + client_file_contents_request: pcCliprdrClientFileContentsRequest, + client_file_contents_response: pcCliprdrClientFileContentsResponse, + ) -> Result, CliprdrError> { + let context = CliprdrClientContext { + custom: 0 as *mut _, + enableFiles: if enable_files { TRUE } else { FALSE }, + enableOthers: if enable_others { TRUE } else { FALSE }, + ServerCapabilities: None, + ClientCapabilities: None, + MonitorReady: None, + TempDirectory: None, + ClientFormatList: client_format_list, + ServerFormatList: None, + ClientFormatListResponse: client_format_list_response, + ServerFormatListResponse: None, + ClientLockClipboardData: None, + ServerLockClipboardData: None, + ClientUnlockClipboardData: None, + ServerUnlockClipboardData: None, + ClientFormatDataRequest: client_format_data_request, + ServerFormatDataRequest: None, + ClientFormatDataResponse: client_format_data_response, + ServerFormatDataResponse: None, + ClientFileContentsRequest: client_file_contents_request, + ServerFileContentsRequest: None, + ClientFileContentsResponse: client_file_contents_response, + ServerFileContentsResponse: None, + lastRequestedFormatId: 0, + }; + let mut context = Box::new(context); + unsafe { + if FALSE == init_cliprdr(&mut (*context)) { + println!("Failed to init cliprdr"); + Err(CliprdrError::CliprdrInit) + } else { + Ok(context) + } + } + } +} + +impl Drop for CliprdrClientContext { + fn drop(&mut self) { + unsafe { + if FALSE == uninit_cliprdr(&mut *self) { + println!("Failed to uninit cliprdr"); + } else { + println!("Succeeded to uninit cliprdr"); + } + } + } +} diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs new file mode 100644 index 000000000..21ca6bfb8 --- /dev/null +++ b/libs/clipboard/src/lib.rs @@ -0,0 +1,548 @@ +use cliprdr::*; +use hbb_common::{ + log, + message_proto::cliprdr as msg_cliprdr, + message_proto::*, + tokio::sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Mutex as TokioMutex, + }, + ResultType, +}; +use std::{ + boxed::Box, + ffi::{CStr, CString}, +}; + +pub mod cliprdr; + +#[derive(Debug)] +pub struct ConnID { + pub server_conn_id: u32, + pub remote_conn_id: u32, +} + +lazy_static::lazy_static! { + static ref MSG_CHANNEL_CLIENT: (UnboundedSender<(ConnID, Message)>, TokioMutex>) = { + let (tx, rx) = unbounded_channel(); + (tx, TokioMutex::new(rx)) + }; +} + +#[inline(always)] +pub fn get_rx_client_msg<'a>() -> &'a TokioMutex> { + &MSG_CHANNEL_CLIENT.1 +} + +pub fn server_msg(context: &mut Box, conn_id: ConnID, msg: Cliprdr) -> u32 { + match msg.union { + Some(msg_cliprdr::Union::ready(_)) => { + // proc ready + 0 + } + Some(msg_cliprdr::Union::format_list(req)) => { + log::debug!("server_format_list called"); + let ret = server_format_list(context, conn_id, req); + log::debug!("server_format_list called, return {}", ret); + ret + } + Some(msg_cliprdr::Union::format_list_response(req)) => { + log::debug!("format_list_response called"); + let ret = server_format_list_response(context, conn_id, req); + log::debug!("server_format_list_response called, return {}", ret); + ret + } + Some(msg_cliprdr::Union::format_data_request(req)) => { + log::debug!("format_data_request called"); + let ret = server_format_data_request(context, conn_id, req); + log::debug!("server_format_data_request called, return {}", ret); + ret + } + Some(msg_cliprdr::Union::format_data_response(req)) => { + log::debug!("format_data_response called"); + let ret = server_format_data_response(context, conn_id, req); + log::debug!("server_format_data_response called, return {}", ret); + ret + } + Some(msg_cliprdr::Union::file_contents_request(req)) => { + log::debug!("file_contents_request called"); + let ret = server_file_contents_request(context, conn_id, req); + log::debug!("server_file_contents_request called, return {}", ret); + ret + } + Some(msg_cliprdr::Union::file_contents_response(req)) => { + log::debug!("file_contents_response called"); + let ret = server_file_contents_response(context, conn_id, req); + log::debug!("server_file_contents_response called, return {}", ret); + ret + } + None => { + unreachable!() + } + } +} + +fn server_format_list( + context: &mut Box, + conn_id: ConnID, + data: CliprdrServerFormatList, +) -> u32 { + // do not check msgFlags for now + unsafe { + let num_formats = data.formats.len() as UINT32; + let mut formats = data + .formats + .into_iter() + .map(|format| { + if format.format.is_empty() { + CLIPRDR_FORMAT { + formatId: format.id as UINT32, + formatName: 0 as *mut _, + } + } else { + let n = match CString::new(format.format) { + Ok(n) => n, + Err(_) => CString::new("").unwrap(), + }; + CLIPRDR_FORMAT { + formatId: format.id as UINT32, + formatName: n.into_raw(), + } + } + }) + .collect::>(); + + let server_conn_id = if conn_id.server_conn_id != 0 { + conn_id.server_conn_id as UINT32 + } else { + data.server_conn_id as UINT32 + }; + let remote_conn_id = if conn_id.remote_conn_id != 0 { + conn_id.remote_conn_id as UINT32 + } else { + data.remote_conn_id as UINT32 + }; + + let format_list = CLIPRDR_FORMAT_LIST { + serverConnID: server_conn_id, + remoteConnID: remote_conn_id, + msgType: 0 as UINT16, + msgFlags: 0 as UINT16, + dataLen: 0 as UINT32, + numFormats: num_formats, + formats: formats.as_mut_ptr(), + }; + + let ret = ((**context).ServerFormatList.unwrap())(&mut (**context), &format_list); + + for f in formats { + if !f.formatName.is_null() { + // retake pointer to free memory + let _ = CString::from_raw(f.formatName); + } + } + + ret as u32 + } +} +fn server_format_list_response( + context: &mut Box, + conn_id: ConnID, + data: CliprdrServerFormatListResponse, +) -> u32 { + unsafe { + let server_conn_id = if conn_id.server_conn_id != 0 { + conn_id.server_conn_id as UINT32 + } else { + data.server_conn_id as UINT32 + }; + let remote_conn_id = if conn_id.remote_conn_id != 0 { + conn_id.remote_conn_id as UINT32 + } else { + data.remote_conn_id as UINT32 + }; + + let format_list_response = CLIPRDR_FORMAT_LIST_RESPONSE { + serverConnID: server_conn_id, + remoteConnID: remote_conn_id, + msgType: 0 as UINT16, + msgFlags: data.msg_flags as UINT16, + dataLen: 0 as UINT32, + }; + + let ret = + (**context).ServerFormatListResponse.unwrap()(&mut (**context), &format_list_response); + + ret as u32 + } +} +fn server_format_data_request( + context: &mut Box, + conn_id: ConnID, + data: CliprdrServerFormatDataRequest, +) -> u32 { + unsafe { + let server_conn_id = if conn_id.server_conn_id != 0 { + conn_id.server_conn_id as UINT32 + } else { + data.server_conn_id as UINT32 + }; + let remote_conn_id = if conn_id.remote_conn_id != 0 { + conn_id.remote_conn_id as UINT32 + } else { + data.remote_conn_id as UINT32 + }; + + let format_data_request = CLIPRDR_FORMAT_DATA_REQUEST { + serverConnID: server_conn_id, + remoteConnID: remote_conn_id, + msgType: 0 as UINT16, + msgFlags: 0 as UINT16, + dataLen: 0 as UINT32, + requestedFormatId: data.requested_format_id as UINT32, + }; + let ret = + ((**context).ServerFormatDataRequest.unwrap())(&mut (**context), &format_data_request); + ret as u32 + } +} +fn server_format_data_response( + context: &mut Box, + conn_id: ConnID, + mut data: CliprdrServerFormatDataResponse, +) -> u32 { + unsafe { + let server_conn_id = if conn_id.server_conn_id != 0 { + conn_id.server_conn_id as UINT32 + } else { + data.server_conn_id as UINT32 + }; + let remote_conn_id = if conn_id.remote_conn_id != 0 { + conn_id.remote_conn_id as UINT32 + } else { + data.remote_conn_id as UINT32 + }; + let format_data_response = CLIPRDR_FORMAT_DATA_RESPONSE { + serverConnID: server_conn_id, + remoteConnID: remote_conn_id, + msgType: 0 as UINT16, + msgFlags: data.msg_flags as UINT16, + dataLen: data.format_data.len() as UINT32, + requestedFormatData: data.format_data.as_mut_ptr(), + }; + let ret = ((**context).ServerFormatDataResponse.unwrap())( + &mut (**context), + &format_data_response, + ); + ret as u32 + } +} +fn server_file_contents_request( + context: &mut Box, + conn_id: ConnID, + data: CliprdrFileContentsRequest, +) -> u32 { + unsafe { + let server_conn_id = if conn_id.server_conn_id != 0 { + conn_id.server_conn_id as UINT32 + } else { + data.server_conn_id as UINT32 + }; + let remote_conn_id = if conn_id.remote_conn_id != 0 { + conn_id.remote_conn_id as UINT32 + } else { + data.remote_conn_id as UINT32 + }; + let file_contents_request = CLIPRDR_FILE_CONTENTS_REQUEST { + serverConnID: server_conn_id, + remoteConnID: remote_conn_id, + msgType: 0 as UINT16, + msgFlags: 0 as UINT16, + dataLen: 0 as UINT32, + streamId: data.stream_id as UINT32, + listIndex: data.list_index as UINT32, + dwFlags: data.dw_flags as UINT32, + nPositionLow: data.n_position_low as UINT32, + nPositionHigh: data.n_position_high as UINT32, + cbRequested: data.cb_requested as UINT32, + haveClipDataId: if data.have_clip_data_id { TRUE } else { FALSE }, + clipDataId: data.clip_data_id as UINT32, + }; + let ret = ((**context).ServerFileContentsRequest.unwrap())( + &mut (**context), + &file_contents_request, + ); + ret as u32 + } +} +fn server_file_contents_response( + context: &mut Box, + conn_id: ConnID, + mut data: CliprdrFileContentsResponse, +) -> u32 { + unsafe { + let server_conn_id = if conn_id.server_conn_id != 0 { + conn_id.server_conn_id as UINT32 + } else { + data.server_conn_id as UINT32 + }; + let remote_conn_id = if conn_id.remote_conn_id != 0 { + conn_id.remote_conn_id as UINT32 + } else { + data.remote_conn_id as UINT32 + }; + let file_contents_response = CLIPRDR_FILE_CONTENTS_RESPONSE { + serverConnID: server_conn_id, + remoteConnID: remote_conn_id, + msgType: 0 as UINT16, + msgFlags: data.msg_flags as UINT16, + dataLen: 4 + data.requested_data.len() as UINT32, + streamId: data.stream_id as UINT32, + cbRequested: data.requested_data.len() as UINT32, + requestedData: data.requested_data.as_mut_ptr(), + }; + let ret = ((**context).ServerFileContentsResponse.unwrap())( + &mut (**context), + &file_contents_response, + ); + ret as u32 + } +} + +pub fn create_cliprdr_context(enable_files: bool, enable_others: bool) -> ResultType> { + Ok(CliprdrClientContext::create( + enable_files, + enable_others, + Some(client_format_list), + Some(client_format_list_response), + Some(client_format_data_request), + Some(client_format_data_response), + Some(client_file_contents_request), + Some(client_file_contents_response), + )?) +} + +extern "C" fn client_format_list( + _context: *mut CliprdrClientContext, + format_list: *const CLIPRDR_FORMAT_LIST, +) -> UINT { + log::debug!("client_format_list called"); + + let mut data = CliprdrServerFormatList::default(); + unsafe { + let mut i = 0u32; + while i < (*format_list).numFormats { + let format_data = &(*(*format_list).formats.offset(i as isize)); + if format_data.formatName.is_null() { + data.formats.push(CliprdrFormat { + id: format_data.formatId as i32, + format: "".to_owned(), + ..Default::default() + }); + } else { + let format_name = CStr::from_ptr(format_data.formatName).to_str(); + let format_name = match format_name { + Ok(n) => n.to_owned(), + Err(_) => { + log::warn!("failed to get format name"); + "".to_owned() + } + }; + data.formats.push(CliprdrFormat { + id: format_data.formatId as i32, + format: format_name, + ..Default::default() + }); + } + i += 1; + } + + data.server_conn_id = (*format_list).serverConnID as i32; + data.remote_conn_id = (*format_list).remoteConnID as i32; + } + let conn_id = ConnID { + server_conn_id: data.server_conn_id as u32, + remote_conn_id: data.remote_conn_id as u32, + }; + + let mut msg = Message::new(); + let mut cliprdr = Cliprdr::new(); + cliprdr.set_format_list(data); + msg.set_cliprdr(cliprdr); + + // no need to handle result here + MSG_CHANNEL_CLIENT.0.send((conn_id, msg)).unwrap(); + + 0 +} + +extern "C" fn client_format_list_response( + _context: *mut CliprdrClientContext, + format_list_response: *const CLIPRDR_FORMAT_LIST_RESPONSE, +) -> UINT { + log::debug!("client_format_list_response called"); + + let mut data = CliprdrServerFormatListResponse::default(); + unsafe { + data.server_conn_id = (*format_list_response).serverConnID as i32; + data.remote_conn_id = (*format_list_response).remoteConnID as i32; + data.msg_flags = (*format_list_response).msgFlags as i32; + } + let conn_id = ConnID { + server_conn_id: data.server_conn_id as u32, + remote_conn_id: data.remote_conn_id as u32, + }; + + let mut msg = Message::new(); + let mut cliprdr = Cliprdr::new(); + cliprdr.set_format_list_response(data); + msg.set_cliprdr(cliprdr); + // no need to handle result here + MSG_CHANNEL_CLIENT.0.send((conn_id, msg)).unwrap(); + + 0 +} + +extern "C" fn client_format_data_request( + _context: *mut CliprdrClientContext, + format_data_request: *const CLIPRDR_FORMAT_DATA_REQUEST, +) -> UINT { + log::debug!("client_format_data_request called"); + + let mut data = CliprdrServerFormatDataRequest::default(); + unsafe { + data.server_conn_id = (*format_data_request).serverConnID as i32; + data.remote_conn_id = (*format_data_request).remoteConnID as i32; + data.requested_format_id = (*format_data_request).requestedFormatId as i32; + } + let conn_id = ConnID { + server_conn_id: data.server_conn_id as u32, + remote_conn_id: data.remote_conn_id as u32, + }; + + let mut msg = Message::new(); + let mut cliprdr = Cliprdr::new(); + cliprdr.set_format_data_request(data); + msg.set_cliprdr(cliprdr); + // no need to handle result here + MSG_CHANNEL_CLIENT.0.send((conn_id, msg)).unwrap(); + + 0 +} + +extern "C" fn client_format_data_response( + _context: *mut CliprdrClientContext, + format_data_response: *const CLIPRDR_FORMAT_DATA_RESPONSE, +) -> UINT { + log::debug!("client_format_data_response called"); + + let mut data = CliprdrServerFormatDataResponse::default(); + unsafe { + data.server_conn_id = (*format_data_response).serverConnID as i32; + data.remote_conn_id = (*format_data_response).remoteConnID as i32; + data.msg_flags = (*format_data_response).msgFlags as i32; + data.format_data = std::slice::from_raw_parts( + (*format_data_response).requestedFormatData, + (*format_data_response).dataLen as usize, + ) + .to_vec(); + } + let conn_id = ConnID { + server_conn_id: data.server_conn_id as u32, + remote_conn_id: data.remote_conn_id as u32, + }; + + let mut msg = Message::new(); + let mut cliprdr = Cliprdr::new(); + cliprdr.set_format_data_response(data); + msg.set_cliprdr(cliprdr); + // no need to handle result here + MSG_CHANNEL_CLIENT.0.send((conn_id, msg)).unwrap(); + + 0 +} + +extern "C" fn client_file_contents_request( + _context: *mut CliprdrClientContext, + file_contents_request: *const CLIPRDR_FILE_CONTENTS_REQUEST, +) -> UINT { + log::debug!("client_file_contents_request called"); + + // TODO: support huge file? + // if (!cliprdr->hasHugeFileSupport) + // { + // if (((UINT64)fileContentsRequest->cbRequested + fileContentsRequest->nPositionLow) > + // UINT32_MAX) + // return ERROR_INVALID_PARAMETER; + // if (fileContentsRequest->nPositionHigh != 0) + // return ERROR_INVALID_PARAMETER; + // } + + let mut data = CliprdrFileContentsRequest::default(); + unsafe { + data.server_conn_id = (*file_contents_request).serverConnID as i32; + data.remote_conn_id = (*file_contents_request).remoteConnID as i32; + data.stream_id = (*file_contents_request).streamId as i32; + data.list_index = (*file_contents_request).listIndex as i32; + data.dw_flags = (*file_contents_request).dwFlags as i32; + data.n_position_low = (*file_contents_request).nPositionLow as i32; + data.n_position_high = (*file_contents_request).nPositionHigh as i32; + data.cb_requested = (*file_contents_request).cbRequested as i32; + data.have_clip_data_id = (*file_contents_request).haveClipDataId == TRUE; + data.clip_data_id = (*file_contents_request).clipDataId as i32; + } + let conn_id = ConnID { + server_conn_id: data.server_conn_id as u32, + remote_conn_id: data.remote_conn_id as u32, + }; + + let mut msg = Message::new(); + let mut cliprdr = Cliprdr::new(); + cliprdr.set_file_contents_request(data); + msg.set_cliprdr(cliprdr); + // no need to handle result here + MSG_CHANNEL_CLIENT.0.send((conn_id, msg)).unwrap(); + + 0 +} + +extern "C" fn client_file_contents_response( + _context: *mut CliprdrClientContext, + file_contents_response: *const CLIPRDR_FILE_CONTENTS_RESPONSE, +) -> UINT { + log::debug!("client_file_contents_response called"); + + let mut data = CliprdrFileContentsResponse::default(); + unsafe { + data.server_conn_id = (*file_contents_response).serverConnID as i32; + data.remote_conn_id = (*file_contents_response).remoteConnID as i32; + data.msg_flags = (*file_contents_response).msgFlags as i32; + data.stream_id = (*file_contents_response).streamId as i32; + data.requested_data = std::slice::from_raw_parts( + (*file_contents_response).requestedData, + (*file_contents_response).cbRequested as usize, + ) + .to_vec(); + } + let conn_id = ConnID { + server_conn_id: data.server_conn_id as u32, + remote_conn_id: data.remote_conn_id as u32, + }; + + let mut msg = Message::new(); + let mut cliprdr = Cliprdr::new(); + cliprdr.set_file_contents_response(data); + msg.set_cliprdr(cliprdr); + // no need to handle result here + MSG_CHANNEL_CLIENT.0.send((conn_id, msg)).unwrap(); + + 0 +} + +#[cfg(test)] +mod tests { + // #[test] + // fn test_cliprdr_run() { + // super::cliprdr_run(); + // } +} diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c new file mode 100644 index 000000000..3c51724c0 --- /dev/null +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -0,0 +1,2789 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows Clipboard Redirection + * + * Copyright 2012 Jason Champion + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * 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. + */ + +#define CINTERFACE +#define COBJMACROS + +#include +#include +#include +#include +#include + +#include + +#include + +#include "../cliprdr.h" + +#define CLIPRDR_SVC_CHANNEL_NAME "cliprdr" + +/** + * Clipboard Formats + */ +#define CB_FORMAT_HTML 0xD010 +#define CB_FORMAT_PNG 0xD011 +#define CB_FORMAT_JPEG 0xD012 +#define CB_FORMAT_GIF 0xD013 +#define CB_FORMAT_TEXTURILIST 0xD014 +#define CB_FORMAT_GNOMECOPIEDFILES 0xD015 +#define CB_FORMAT_MATECOPIEDFILES 0xD016 + +/* CLIPRDR_HEADER.msgType */ +#define CB_MONITOR_READY 0x0001 +#define CB_FORMAT_LIST 0x0002 +#define CB_FORMAT_LIST_RESPONSE 0x0003 +#define CB_FORMAT_DATA_REQUEST 0x0004 +#define CB_FORMAT_DATA_RESPONSE 0x0005 +#define CB_TEMP_DIRECTORY 0x0006 +#define CB_CLIP_CAPS 0x0007 +#define CB_FILECONTENTS_REQUEST 0x0008 +#define CB_FILECONTENTS_RESPONSE 0x0009 +#define CB_LOCK_CLIPDATA 0x000A +#define CB_UNLOCK_CLIPDATA 0x000B + +/* CLIPRDR_HEADER.msgFlags */ +#define CB_RESPONSE_OK 0x0001 +#define CB_RESPONSE_FAIL 0x0002 +#define CB_ASCII_NAMES 0x0004 + +/* CLIPRDR_CAPS_SET.capabilitySetType */ +#define CB_CAPSTYPE_GENERAL 0x0001 + +/* CLIPRDR_GENERAL_CAPABILITY.lengthCapability */ +#define CB_CAPSTYPE_GENERAL_LEN 12 + +/* CLIPRDR_GENERAL_CAPABILITY.version */ +#define CB_CAPS_VERSION_1 0x00000001 +#define CB_CAPS_VERSION_2 0x00000002 + +/* CLIPRDR_GENERAL_CAPABILITY.generalFlags */ +#define CB_USE_LONG_FORMAT_NAMES 0x00000002 +#define CB_STREAM_FILECLIP_ENABLED 0x00000004 +#define CB_FILECLIP_NO_FILE_PATHS 0x00000008 +#define CB_CAN_LOCK_CLIPDATA 0x00000010 +#define CB_HUGE_FILE_SUPPORT_ENABLED 0x00000020 + +/* File Contents Request Flags */ +#define FILECONTENTS_SIZE 0x00000001 +#define FILECONTENTS_RANGE 0x00000002 + +/* Special Clipboard Response Formats */ + +struct _CLIPRDR_MFPICT +{ + UINT32 mappingMode; + UINT32 xExt; + UINT32 yExt; + UINT32 metaFileSize; + BYTE *metaFileData; +}; +typedef struct _CLIPRDR_MFPICT CLIPRDR_MFPICT; + +struct _CONN_ID +{ + UINT32 serverConnID; + UINT32 remoteConnID; +}; +typedef struct _CONN_ID CONN_ID; + +struct _FORMAT_IDS +{ + UINT32 serverConnID; + UINT32 remoteConnID; + UINT32 size; + UINT32 *formats; +}; +typedef struct _FORMAT_IDS FORMAT_IDS; + +/* File Contents Request Flags */ +#define FILECONTENTS_SIZE 0x00000001 +#define FILECONTENTS_RANGE 0x00000002 + +#define CHANNEL_RC_OK 0 +#define CHANNEL_RC_ALREADY_INITIALIZED 1 +#define CHANNEL_RC_NOT_INITIALIZED 2 +#define CHANNEL_RC_ALREADY_CONNECTED 3 +#define CHANNEL_RC_NOT_CONNECTED 4 +#define CHANNEL_RC_TOO_MANY_CHANNELS 5 +#define CHANNEL_RC_BAD_CHANNEL 6 +#define CHANNEL_RC_BAD_CHANNEL_HANDLE 7 +#define CHANNEL_RC_NO_BUFFER 8 +#define CHANNEL_RC_BAD_INIT_HANDLE 9 +#define CHANNEL_RC_NOT_OPEN 10 +#define CHANNEL_RC_BAD_PROC 11 +#define CHANNEL_RC_NO_MEMORY 12 +#define CHANNEL_RC_UNKNOWN_CHANNEL_NAME 13 +#define CHANNEL_RC_ALREADY_OPEN 14 +#define CHANNEL_RC_NOT_IN_VIRTUALCHANNELENTRY 15 +#define CHANNEL_RC_NULL_DATA 16 +#define CHANNEL_RC_ZERO_LENGTH 17 +#define CHANNEL_RC_INVALID_INSTANCE 18 +#define CHANNEL_RC_UNSUPPORTED_VERSION 19 +#define CHANNEL_RC_INITIALIZATION_ERROR 20 + +#define TAG "windows" + +#ifdef WITH_DEBUG_CLIPRDR +#define DEBUG_CLIPRDR(...) printf(TAG, __VA_ARGS__) +#else +#define DEBUG_CLIPRDR(...) \ + do \ + { \ + } while (0) +#endif + +typedef BOOL(WINAPI *fnAddClipboardFormatListener)(HWND hwnd); +typedef BOOL(WINAPI *fnRemoveClipboardFormatListener)(HWND hwnd); +typedef BOOL(WINAPI *fnGetUpdatedClipboardFormats)(PUINT lpuiFormats, UINT cFormats, + PUINT pcFormatsOut); + +struct format_mapping +{ + UINT32 remote_format_id; + UINT32 local_format_id; + WCHAR *name; +}; +typedef struct format_mapping formatMapping; + +struct _CliprdrEnumFORMATETC +{ + IEnumFORMATETC iEnumFORMATETC; + + LONG m_lRefCount; + LONG m_nIndex; + LONG m_nNumFormats; + FORMATETC *m_pFormatEtc; +}; +typedef struct _CliprdrEnumFORMATETC CliprdrEnumFORMATETC; + +struct _CliprdrStream +{ + IStream iStream; + + LONG m_lRefCount; + ULONG m_lIndex; + ULARGE_INTEGER m_lSize; + ULARGE_INTEGER m_lOffset; + FILEDESCRIPTORW m_Dsc; + void *m_pData; + UINT32 m_serverConnID; + UINT32 m_remoteConnID; +}; +typedef struct _CliprdrStream CliprdrStream; + +struct _CliprdrDataObject +{ + IDataObject iDataObject; + + LONG m_lRefCount; + FORMATETC *m_pFormatEtc; + STGMEDIUM *m_pStgMedium; + ULONG m_nNumFormats; + ULONG m_nStreams; + IStream **m_pStream; + void *m_pData; + UINT32 m_serverConnID; + UINT32 m_remoteConnID; +}; +typedef struct _CliprdrDataObject CliprdrDataObject; + +struct wf_clipboard +{ + // wfContext* wfc; + // rdpChannels* channels; + CliprdrClientContext *context; + + BOOL sync; + UINT32 capabilities; + + size_t map_size; + size_t map_capacity; + formatMapping *format_mappings; + + UINT32 requestedFormatId; + + HWND hwnd; + HANDLE hmem; + HANDLE thread; + HANDLE response_data_event; + + LPDATAOBJECT data_obj; + ULONG req_fsize; + char *req_fdata; + HANDLE req_fevent; + + size_t nFiles; + size_t file_array_size; + WCHAR **file_names; + FILEDESCRIPTORW **fileDescriptor; + + BOOL legacyApi; + HMODULE hUser32; + HWND hWndNextViewer; + fnAddClipboardFormatListener AddClipboardFormatListener; + fnRemoveClipboardFormatListener RemoveClipboardFormatListener; + fnGetUpdatedClipboardFormats GetUpdatedClipboardFormats; +}; +typedef struct wf_clipboard wfClipboard; + +#define WM_CLIPRDR_MESSAGE (WM_USER + 156) +#define OLE_SETCLIPBOARD 1 +#define DELAY_RENDERING 2 + +BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr); +BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr); + +static BOOL wf_create_file_obj(UINT32 serverConnID, UINT32 remoteConnID, wfClipboard *cliprdrrdr, IDataObject **ppDataObject); +static void wf_destroy_file_obj(IDataObject *instance); +static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format); +static UINT cliprdr_send_data_request(UINT32 serverConnID, UINT32 remoteConnID, wfClipboard *clipboard, UINT32 format); +static UINT cliprdr_send_lock(wfClipboard *clipboard); +static UINT cliprdr_send_unlock(wfClipboard *clipboard); +static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 serverConnID, UINT32 remoteConnID, const void *streamid, + ULONG index, UINT32 flag, DWORD positionhigh, + DWORD positionlow, ULONG request); + +static void CliprdrDataObject_Delete(CliprdrDataObject *instance); + +static CliprdrEnumFORMATETC *CliprdrEnumFORMATETC_New(ULONG nFormats, FORMATETC *pFormatEtc); +static void CliprdrEnumFORMATETC_Delete(CliprdrEnumFORMATETC *instance); + +static void CliprdrStream_Delete(CliprdrStream *instance); + +static BOOL try_open_clipboard(HWND hwnd) +{ + size_t x; + for (x = 0; x < 10; x++) + { + if (OpenClipboard(hwnd)) + return TRUE; + Sleep(10); + } + return FALSE; +} + +/** + * IStream + */ + +static HRESULT STDMETHODCALLTYPE CliprdrStream_QueryInterface(IStream *This, REFIID riid, + void **ppvObject) +{ + if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown)) + { + IStream_AddRef(This); + *ppvObject = This; + return S_OK; + } + else + { + *ppvObject = 0; + return E_NOINTERFACE; + } +} + +static ULONG STDMETHODCALLTYPE CliprdrStream_AddRef(IStream *This) +{ + CliprdrStream *instance = (CliprdrStream *)This; + + if (!instance) + return 0; + + return InterlockedIncrement(&instance->m_lRefCount); +} + +static ULONG STDMETHODCALLTYPE CliprdrStream_Release(IStream *This) +{ + LONG count; + CliprdrStream *instance = (CliprdrStream *)This; + + if (!instance) + return 0; + + count = InterlockedDecrement(&instance->m_lRefCount); + + if (count == 0) + { + CliprdrStream_Delete(instance); + return 0; + } + else + { + return count; + } +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULONG cb, + ULONG *pcbRead) +{ + int ret; + CliprdrStream *instance = (CliprdrStream *)This; + wfClipboard *clipboard; + + if (!pv || !pcbRead || !instance) + return E_INVALIDARG; + + clipboard = (wfClipboard *)instance->m_pData; + *pcbRead = 0; + + if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart) + return S_FALSE; + + ret = cliprdr_send_request_filecontents(clipboard, instance->m_serverConnID, instance->m_remoteConnID, (void *)This, instance->m_lIndex, + FILECONTENTS_RANGE, instance->m_lOffset.HighPart, + instance->m_lOffset.LowPart, cb); + + if (ret < 0) + return E_FAIL; + + if (clipboard->req_fdata) + { + CopyMemory(pv, clipboard->req_fdata, clipboard->req_fsize); + free(clipboard->req_fdata); + } + + *pcbRead = clipboard->req_fsize; + instance->m_lOffset.QuadPart += clipboard->req_fsize; + + if (clipboard->req_fsize < cb) + return S_FALSE; + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Write(IStream *This, const void *pv, ULONG cb, + ULONG *pcbWritten) +{ + (void)This; + (void)pv; + (void)cb; + (void)pcbWritten; + return STG_E_ACCESSDENIED; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Seek(IStream *This, LARGE_INTEGER dlibMove, + DWORD dwOrigin, ULARGE_INTEGER *plibNewPosition) +{ + ULONGLONG newoffset; + CliprdrStream *instance = (CliprdrStream *)This; + + if (!instance) + return E_INVALIDARG; + + newoffset = instance->m_lOffset.QuadPart; + + switch (dwOrigin) + { + case STREAM_SEEK_SET: + newoffset = dlibMove.QuadPart; + break; + + case STREAM_SEEK_CUR: + newoffset += dlibMove.QuadPart; + break; + + case STREAM_SEEK_END: + newoffset = instance->m_lSize.QuadPart + dlibMove.QuadPart; + break; + + default: + return E_INVALIDARG; + } + + if (newoffset < 0 || newoffset >= instance->m_lSize.QuadPart) + return E_FAIL; + + instance->m_lOffset.QuadPart = newoffset; + + if (plibNewPosition) + plibNewPosition->QuadPart = instance->m_lOffset.QuadPart; + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_SetSize(IStream *This, ULARGE_INTEGER libNewSize) +{ + (void)This; + (void)libNewSize; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_CopyTo(IStream *This, IStream *pstm, + ULARGE_INTEGER cb, ULARGE_INTEGER *pcbRead, + ULARGE_INTEGER *pcbWritten) +{ + (void)This; + (void)pstm; + (void)cb; + (void)pcbRead; + (void)pcbWritten; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Commit(IStream *This, DWORD grfCommitFlags) +{ + (void)This; + (void)grfCommitFlags; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Revert(IStream *This) +{ + (void)This; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_LockRegion(IStream *This, ULARGE_INTEGER libOffset, + ULARGE_INTEGER cb, DWORD dwLockType) +{ + (void)This; + (void)libOffset; + (void)cb; + (void)dwLockType; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_UnlockRegion(IStream *This, ULARGE_INTEGER libOffset, + ULARGE_INTEGER cb, DWORD dwLockType) +{ + (void)This; + (void)libOffset; + (void)cb; + (void)dwLockType; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Stat(IStream *This, STATSTG *pstatstg, + DWORD grfStatFlag) +{ + CliprdrStream *instance = (CliprdrStream *)This; + + if (!instance) + return E_INVALIDARG; + + if (pstatstg == NULL) + return STG_E_INVALIDPOINTER; + + ZeroMemory(pstatstg, sizeof(STATSTG)); + + switch (grfStatFlag) + { + case STATFLAG_DEFAULT: + return STG_E_INSUFFICIENTMEMORY; + + case STATFLAG_NONAME: + pstatstg->cbSize.QuadPart = instance->m_lSize.QuadPart; + pstatstg->grfLocksSupported = LOCK_EXCLUSIVE; + pstatstg->grfMode = GENERIC_READ; + pstatstg->grfStateBits = 0; + pstatstg->type = STGTY_STREAM; + break; + + case STATFLAG_NOOPEN: + return STG_E_INVALIDFLAG; + + default: + return STG_E_INVALIDFLAG; + } + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Clone(IStream *This, IStream **ppstm) +{ + (void)This; + (void)ppstm; + return E_NOTIMPL; +} + +static CliprdrStream *CliprdrStream_New(UINT32 serverConnID, UINT32 remoteConnID, ULONG index, void *pData, const FILEDESCRIPTORW *dsc) +{ + IStream *iStream; + BOOL success = FALSE; + BOOL isDir = FALSE; + CliprdrStream *instance; + wfClipboard *clipboard = (wfClipboard *)pData; + instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream)); + + if (instance) + { + instance->m_Dsc = *dsc; + iStream = &instance->iStream; + iStream->lpVtbl = (IStreamVtbl *)calloc(1, sizeof(IStreamVtbl)); + + if (iStream->lpVtbl) + { + iStream->lpVtbl->QueryInterface = CliprdrStream_QueryInterface; + iStream->lpVtbl->AddRef = CliprdrStream_AddRef; + iStream->lpVtbl->Release = CliprdrStream_Release; + iStream->lpVtbl->Read = CliprdrStream_Read; + iStream->lpVtbl->Write = CliprdrStream_Write; + iStream->lpVtbl->Seek = CliprdrStream_Seek; + iStream->lpVtbl->SetSize = CliprdrStream_SetSize; + iStream->lpVtbl->CopyTo = CliprdrStream_CopyTo; + iStream->lpVtbl->Commit = CliprdrStream_Commit; + iStream->lpVtbl->Revert = CliprdrStream_Revert; + iStream->lpVtbl->LockRegion = CliprdrStream_LockRegion; + iStream->lpVtbl->UnlockRegion = CliprdrStream_UnlockRegion; + iStream->lpVtbl->Stat = CliprdrStream_Stat; + iStream->lpVtbl->Clone = CliprdrStream_Clone; + instance->m_lRefCount = 1; + instance->m_lIndex = index; + instance->m_pData = pData; + instance->m_lOffset.QuadPart = 0; + instance->m_serverConnID = serverConnID; + instance->m_remoteConnID = remoteConnID; + + if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES) + { + if (instance->m_Dsc.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + isDir = TRUE; + } + + if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir) + { + /* get content size of this stream */ + if (cliprdr_send_request_filecontents(clipboard, instance->m_serverConnID, instance->m_remoteConnID, (void *)instance, + instance->m_lIndex, FILECONTENTS_SIZE, 0, 0, + 8) == CHANNEL_RC_OK) + { + success = TRUE; + } + + instance->m_lSize.QuadPart = *((LONGLONG *)clipboard->req_fdata); + free(clipboard->req_fdata); + } + else + success = TRUE; + } + } + + if (!success) + { + CliprdrStream_Delete(instance); + instance = NULL; + } + + return instance; +} + +void CliprdrStream_Delete(CliprdrStream *instance) +{ + if (instance) + { + free(instance->iStream.lpVtbl); + free(instance); + } +} + +/** + * IDataObject + */ + +static LONG cliprdr_lookup_format(CliprdrDataObject *instance, FORMATETC *pFormatEtc) +{ + ULONG i; + + if (!instance || !pFormatEtc) + return -1; + + for (i = 0; i < instance->m_nNumFormats; i++) + { + if ((pFormatEtc->tymed & instance->m_pFormatEtc[i].tymed) && + pFormatEtc->cfFormat == instance->m_pFormatEtc[i].cfFormat && + pFormatEtc->dwAspect & instance->m_pFormatEtc[i].dwAspect) + { + return (LONG)i; + } + } + + return -1; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_QueryInterface(IDataObject *This, REFIID riid, + void **ppvObject) +{ + (void)This; + + if (!ppvObject) + return E_INVALIDARG; + + if (IsEqualIID(riid, &IID_IDataObject) || IsEqualIID(riid, &IID_IUnknown)) + { + IDataObject_AddRef(This); + *ppvObject = This; + return S_OK; + } + else + { + *ppvObject = 0; + return E_NOINTERFACE; + } +} + +static ULONG STDMETHODCALLTYPE CliprdrDataObject_AddRef(IDataObject *This) +{ + CliprdrDataObject *instance = (CliprdrDataObject *)This; + + if (!instance) + return E_INVALIDARG; + + return InterlockedIncrement(&instance->m_lRefCount); +} + +static ULONG STDMETHODCALLTYPE CliprdrDataObject_Release(IDataObject *This) +{ + LONG count; + CliprdrDataObject *instance = (CliprdrDataObject *)This; + + if (!instance) + return E_INVALIDARG; + + count = InterlockedDecrement(&instance->m_lRefCount); + + if (count == 0) + { + CliprdrDataObject_Delete(instance); + return 0; + } + else + return count; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetData(IDataObject *This, FORMATETC *pFormatEtc, + STGMEDIUM *pMedium) +{ + ULONG i; + LONG idx; + CliprdrDataObject *instance = (CliprdrDataObject *)This; + wfClipboard *clipboard; + + if (!pFormatEtc || !pMedium || !instance) + return E_INVALIDARG; + + clipboard = (wfClipboard *)instance->m_pData; + + if (!clipboard) + return E_INVALIDARG; + + if ((idx = cliprdr_lookup_format(instance, pFormatEtc)) == -1) + return DV_E_FORMATETC; + + pMedium->tymed = instance->m_pFormatEtc[idx].tymed; + pMedium->pUnkForRelease = 0; + + if (instance->m_pFormatEtc[idx].cfFormat == RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW)) + { + // FILEGROUPDESCRIPTOR *dsc; + FILEGROUPDESCRIPTORW *dsc; + // DWORD remote_format_id = get_remote_format_id(clipboard, instance->m_pFormatEtc[idx].cfFormat); + // FIXME: origin code may be failed here??? + if (cliprdr_send_data_request(instance->m_serverConnID, instance->m_remoteConnID, clipboard, instance->m_pFormatEtc[idx].cfFormat) != 0) + return E_UNEXPECTED; + + pMedium->hGlobal = clipboard->hmem; /* points to a FILEGROUPDESCRIPTOR structure */ + /* GlobalLock returns a pointer to the first byte of the memory block, + * in which is a FILEGROUPDESCRIPTOR structure, whose first UINT member + * is the number of FILEDESCRIPTOR's */ + // dsc = (FILEGROUPDESCRIPTOR *)GlobalLock(clipboard->hmem); + dsc = (FILEGROUPDESCRIPTORW *)GlobalLock(clipboard->hmem); + instance->m_nStreams = dsc->cItems; + GlobalUnlock(clipboard->hmem); + + if (instance->m_nStreams > 0) + { + if (!instance->m_pStream) + { + instance->m_pStream = (LPSTREAM *)calloc(instance->m_nStreams, sizeof(LPSTREAM)); + + if (instance->m_pStream) + { + for (i = 0; i < instance->m_nStreams; i++) + { + instance->m_pStream[i] = + (IStream *)CliprdrStream_New(instance->m_serverConnID, instance->m_remoteConnID, i, clipboard, &dsc->fgd[i]); + + if (!instance->m_pStream[i]) + return E_OUTOFMEMORY; + } + } + } + } + + if (!instance->m_pStream) + { + if (clipboard->hmem) + { + GlobalFree(clipboard->hmem); + clipboard->hmem = NULL; + } + + pMedium->hGlobal = NULL; + return E_OUTOFMEMORY; + } + } + else if (instance->m_pFormatEtc[idx].cfFormat == RegisterClipboardFormat(CFSTR_FILECONTENTS)) + { + if ((pFormatEtc->lindex >= 0) && ((ULONG)pFormatEtc->lindex < instance->m_nStreams)) + { + pMedium->pstm = instance->m_pStream[pFormatEtc->lindex]; + IDataObject_AddRef(instance->m_pStream[pFormatEtc->lindex]); + } + else + return E_INVALIDARG; + } + else + return E_UNEXPECTED; + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetDataHere(IDataObject *This, + FORMATETC *pformatetc, + STGMEDIUM *pmedium) +{ + (void)This; + (void)pformatetc; + (void)pmedium; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_QueryGetData(IDataObject *This, + FORMATETC *pformatetc) +{ + CliprdrDataObject *instance = (CliprdrDataObject *)This; + + if (!pformatetc) + return E_INVALIDARG; + + if (cliprdr_lookup_format(instance, pformatetc) == -1) + return DV_E_FORMATETC; + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetCanonicalFormatEtc(IDataObject *This, + FORMATETC *pformatectIn, + FORMATETC *pformatetcOut) +{ + (void)This; + (void)pformatectIn; + + if (!pformatetcOut) + return E_INVALIDARG; + + pformatetcOut->ptd = NULL; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_SetData(IDataObject *This, FORMATETC *pformatetc, + STGMEDIUM *pmedium, BOOL fRelease) +{ + (void)This; + (void)pformatetc; + (void)pmedium; + (void)fRelease; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_EnumFormatEtc(IDataObject *This, + DWORD dwDirection, + IEnumFORMATETC **ppenumFormatEtc) +{ + CliprdrDataObject *instance = (CliprdrDataObject *)This; + + if (!instance || !ppenumFormatEtc) + return E_INVALIDARG; + + if (dwDirection == DATADIR_GET) + { + *ppenumFormatEtc = (IEnumFORMATETC *)CliprdrEnumFORMATETC_New(instance->m_nNumFormats, + instance->m_pFormatEtc); + return (*ppenumFormatEtc) ? S_OK : E_OUTOFMEMORY; + } + else + { + return E_NOTIMPL; + } +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_DAdvise(IDataObject *This, FORMATETC *pformatetc, + DWORD advf, IAdviseSink *pAdvSink, + DWORD *pdwConnection) +{ + (void)This; + (void)pformatetc; + (void)advf; + (void)pAdvSink; + (void)pdwConnection; + return OLE_E_ADVISENOTSUPPORTED; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_DUnadvise(IDataObject *This, DWORD dwConnection) +{ + (void)This; + (void)dwConnection; + return OLE_E_ADVISENOTSUPPORTED; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_EnumDAdvise(IDataObject *This, + IEnumSTATDATA **ppenumAdvise) +{ + (void)This; + (void)ppenumAdvise; + return OLE_E_ADVISENOTSUPPORTED; +} + +static CliprdrDataObject *CliprdrDataObject_New(UINT32 serverConnID, UINT32 remoteConnID, FORMATETC *fmtetc, STGMEDIUM *stgmed, ULONG count, + void *data) +{ + CliprdrDataObject *instance; + IDataObject *iDataObject; + instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject)); + + if (!instance) + goto error; + + iDataObject = &instance->iDataObject; + iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl)); + + if (!iDataObject->lpVtbl) + goto error; + + iDataObject->lpVtbl->QueryInterface = CliprdrDataObject_QueryInterface; + iDataObject->lpVtbl->AddRef = CliprdrDataObject_AddRef; + iDataObject->lpVtbl->Release = CliprdrDataObject_Release; + iDataObject->lpVtbl->GetData = CliprdrDataObject_GetData; + iDataObject->lpVtbl->GetDataHere = CliprdrDataObject_GetDataHere; + iDataObject->lpVtbl->QueryGetData = CliprdrDataObject_QueryGetData; + iDataObject->lpVtbl->GetCanonicalFormatEtc = CliprdrDataObject_GetCanonicalFormatEtc; + iDataObject->lpVtbl->SetData = CliprdrDataObject_SetData; + iDataObject->lpVtbl->EnumFormatEtc = CliprdrDataObject_EnumFormatEtc; + iDataObject->lpVtbl->DAdvise = CliprdrDataObject_DAdvise; + iDataObject->lpVtbl->DUnadvise = CliprdrDataObject_DUnadvise; + iDataObject->lpVtbl->EnumDAdvise = CliprdrDataObject_EnumDAdvise; + instance->m_lRefCount = 1; + instance->m_nNumFormats = count; + instance->m_pData = data; + instance->m_nStreams = 0; + instance->m_pStream = NULL; + instance->m_serverConnID = serverConnID; + instance->m_remoteConnID = remoteConnID; + + if (count > 0) + { + ULONG i; + instance->m_pFormatEtc = (FORMATETC *)calloc(count, sizeof(FORMATETC)); + + if (!instance->m_pFormatEtc) + goto error; + + instance->m_pStgMedium = (STGMEDIUM *)calloc(count, sizeof(STGMEDIUM)); + + if (!instance->m_pStgMedium) + goto error; + + for (i = 0; i < count; i++) + { + instance->m_pFormatEtc[i] = fmtetc[i]; + instance->m_pStgMedium[i] = stgmed[i]; + } + } + + return instance; +error: + CliprdrDataObject_Delete(instance); + return NULL; +} + +void CliprdrDataObject_Delete(CliprdrDataObject *instance) +{ + if (instance) + { + free(instance->iDataObject.lpVtbl); + free(instance->m_pFormatEtc); + free(instance->m_pStgMedium); + + if (instance->m_pStream) + { + ULONG i; + + for (i = 0; i < instance->m_nStreams; i++) + CliprdrStream_Release(instance->m_pStream[i]); + + free(instance->m_pStream); + } + + free(instance); + } +} + +static BOOL wf_create_file_obj(CONN_ID *conn_id, wfClipboard *clipboard, IDataObject **ppDataObject) +{ + FORMATETC fmtetc[2]; + STGMEDIUM stgmeds[2]; + + if (!ppDataObject) + return FALSE; + + fmtetc[0].cfFormat = RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + fmtetc[0].dwAspect = DVASPECT_CONTENT; + fmtetc[0].lindex = 0; + fmtetc[0].ptd = NULL; + fmtetc[0].tymed = TYMED_HGLOBAL; + stgmeds[0].tymed = TYMED_HGLOBAL; + stgmeds[0].hGlobal = NULL; + stgmeds[0].pUnkForRelease = NULL; + fmtetc[1].cfFormat = RegisterClipboardFormat(CFSTR_FILECONTENTS); + fmtetc[1].dwAspect = DVASPECT_CONTENT; + fmtetc[1].lindex = 0; + fmtetc[1].ptd = NULL; + fmtetc[1].tymed = TYMED_ISTREAM; + stgmeds[1].tymed = TYMED_ISTREAM; + stgmeds[1].pstm = NULL; + stgmeds[1].pUnkForRelease = NULL; + *ppDataObject = (IDataObject *)CliprdrDataObject_New(conn_id->serverConnID, conn_id->remoteConnID, fmtetc, stgmeds, 2, clipboard); + return (*ppDataObject) ? TRUE : FALSE; +} + +static void wf_destroy_file_obj(IDataObject *instance) +{ + if (instance) + IDataObject_Release(instance); +} + +/** + * IEnumFORMATETC + */ + +static void cliprdr_format_deep_copy(FORMATETC *dest, FORMATETC *source) +{ + *dest = *source; + + if (source->ptd) + { + dest->ptd = (DVTARGETDEVICE *)CoTaskMemAlloc(sizeof(DVTARGETDEVICE)); + + if (dest->ptd) + *(dest->ptd) = *(source->ptd); + } +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_QueryInterface(IEnumFORMATETC *This, + REFIID riid, void **ppvObject) +{ + (void)This; + + if (IsEqualIID(riid, &IID_IEnumFORMATETC) || IsEqualIID(riid, &IID_IUnknown)) + { + IEnumFORMATETC_AddRef(This); + *ppvObject = This; + return S_OK; + } + else + { + *ppvObject = 0; + return E_NOINTERFACE; + } +} + +static ULONG STDMETHODCALLTYPE CliprdrEnumFORMATETC_AddRef(IEnumFORMATETC *This) +{ + CliprdrEnumFORMATETC *instance = (CliprdrEnumFORMATETC *)This; + + if (!instance) + return 0; + + return InterlockedIncrement(&instance->m_lRefCount); +} + +static ULONG STDMETHODCALLTYPE CliprdrEnumFORMATETC_Release(IEnumFORMATETC *This) +{ + LONG count; + CliprdrEnumFORMATETC *instance = (CliprdrEnumFORMATETC *)This; + + if (!instance) + return 0; + + count = InterlockedDecrement(&instance->m_lRefCount); + + if (count == 0) + { + CliprdrEnumFORMATETC_Delete(instance); + return 0; + } + else + { + return count; + } +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_Next(IEnumFORMATETC *This, ULONG celt, + FORMATETC *rgelt, ULONG *pceltFetched) +{ + ULONG copied = 0; + CliprdrEnumFORMATETC *instance = (CliprdrEnumFORMATETC *)This; + + if (!instance || !celt || !rgelt) + return E_INVALIDARG; + + while ((instance->m_nIndex < instance->m_nNumFormats) && (copied < celt)) + { + cliprdr_format_deep_copy(&rgelt[copied++], &instance->m_pFormatEtc[instance->m_nIndex++]); + } + + if (pceltFetched != 0) + *pceltFetched = copied; + + return (copied == celt) ? S_OK : E_FAIL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_Skip(IEnumFORMATETC *This, ULONG celt) +{ + CliprdrEnumFORMATETC *instance = (CliprdrEnumFORMATETC *)This; + + if (!instance) + return E_INVALIDARG; + + if (instance->m_nIndex + (LONG)celt > instance->m_nNumFormats) + return E_FAIL; + + instance->m_nIndex += celt; + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_Reset(IEnumFORMATETC *This) +{ + CliprdrEnumFORMATETC *instance = (CliprdrEnumFORMATETC *)This; + + if (!instance) + return E_INVALIDARG; + + instance->m_nIndex = 0; + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_Clone(IEnumFORMATETC *This, + IEnumFORMATETC **ppEnum) +{ + CliprdrEnumFORMATETC *instance = (CliprdrEnumFORMATETC *)This; + + if (!instance || !ppEnum) + return E_INVALIDARG; + + *ppEnum = + (IEnumFORMATETC *)CliprdrEnumFORMATETC_New(instance->m_nNumFormats, instance->m_pFormatEtc); + + if (!*ppEnum) + return E_OUTOFMEMORY; + + ((CliprdrEnumFORMATETC *)*ppEnum)->m_nIndex = instance->m_nIndex; + return S_OK; +} + +CliprdrEnumFORMATETC *CliprdrEnumFORMATETC_New(ULONG nFormats, FORMATETC *pFormatEtc) +{ + ULONG i; + CliprdrEnumFORMATETC *instance; + IEnumFORMATETC *iEnumFORMATETC; + + if ((nFormats != 0) && !pFormatEtc) + return NULL; + + instance = (CliprdrEnumFORMATETC *)calloc(1, sizeof(CliprdrEnumFORMATETC)); + + if (!instance) + goto error; + + iEnumFORMATETC = &instance->iEnumFORMATETC; + iEnumFORMATETC->lpVtbl = (IEnumFORMATETCVtbl *)calloc(1, sizeof(IEnumFORMATETCVtbl)); + + if (!iEnumFORMATETC->lpVtbl) + goto error; + + iEnumFORMATETC->lpVtbl->QueryInterface = CliprdrEnumFORMATETC_QueryInterface; + iEnumFORMATETC->lpVtbl->AddRef = CliprdrEnumFORMATETC_AddRef; + iEnumFORMATETC->lpVtbl->Release = CliprdrEnumFORMATETC_Release; + iEnumFORMATETC->lpVtbl->Next = CliprdrEnumFORMATETC_Next; + iEnumFORMATETC->lpVtbl->Skip = CliprdrEnumFORMATETC_Skip; + iEnumFORMATETC->lpVtbl->Reset = CliprdrEnumFORMATETC_Reset; + iEnumFORMATETC->lpVtbl->Clone = CliprdrEnumFORMATETC_Clone; + instance->m_lRefCount = 1; + instance->m_nIndex = 0; + instance->m_nNumFormats = nFormats; + + if (nFormats > 0) + { + instance->m_pFormatEtc = (FORMATETC *)calloc(nFormats, sizeof(FORMATETC)); + + if (!instance->m_pFormatEtc) + goto error; + + for (i = 0; i < nFormats; i++) + cliprdr_format_deep_copy(&instance->m_pFormatEtc[i], &pFormatEtc[i]); + } + + return instance; +error: + CliprdrEnumFORMATETC_Delete(instance); + return NULL; +} + +void CliprdrEnumFORMATETC_Delete(CliprdrEnumFORMATETC *instance) +{ + LONG i; + + if (instance) + { + free(instance->iEnumFORMATETC.lpVtbl); + + if (instance->m_pFormatEtc) + { + for (i = 0; i < instance->m_nNumFormats; i++) + { + if (instance->m_pFormatEtc[i].ptd) + CoTaskMemFree(instance->m_pFormatEtc[i].ptd); + } + + free(instance->m_pFormatEtc); + } + + free(instance); + } +} + +/***********************************************************************************/ + +static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *format_name) +{ + size_t i; + formatMapping *map; + WCHAR *unicode_name; +#if !defined(UNICODE) + size_t size; +#endif + + if (!clipboard || !format_name) + return 0; + +#if defined(UNICODE) + unicode_name = _wcsdup(format_name); +#else + size = _tcslen(format_name); + unicode_name = calloc(size + 1, sizeof(WCHAR)); + + if (!unicode_name) + return 0; + + MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size); +#endif + + if (!unicode_name) + return 0; + + for (i = 0; i < clipboard->map_size; i++) + { + map = &clipboard->format_mappings[i]; + + if (map->name) + { + if (wcscmp(map->name, unicode_name) == 0) + { + free(unicode_name); + return map->local_format_id; + } + } + } + + free(unicode_name); + return 0; +} + +static BOOL file_transferring(wfClipboard *clipboard) +{ + return get_local_format_id_by_name(clipboard, CFSTR_FILEDESCRIPTORW) ? TRUE : FALSE; +} + +static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format) +{ + UINT32 i; + formatMapping *map; + + if (!clipboard) + return 0; + + for (i = 0; i < clipboard->map_size; i++) + { + map = &clipboard->format_mappings[i]; + + if (map->local_format_id == local_format) + return map->remote_format_id; + } + + return local_format; +} + +static void map_ensure_capacity(wfClipboard *clipboard) +{ + if (!clipboard) + return; + + if (clipboard->map_size >= clipboard->map_capacity) + { + size_t new_size; + formatMapping *new_map; + new_size = clipboard->map_capacity * 2; + new_map = + (formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * new_size); + + if (!new_map) + return; + + clipboard->format_mappings = new_map; + clipboard->map_capacity = new_size; + } +} + +static BOOL clear_format_map(wfClipboard *clipboard) +{ + size_t i; + formatMapping *map; + + if (!clipboard) + return FALSE; + + if (clipboard->format_mappings) + { + for (i = 0; i < clipboard->map_capacity; i++) + { + map = &clipboard->format_mappings[i]; + map->remote_format_id = 0; + map->local_format_id = 0; + free(map->name); + map->name = NULL; + } + } + + clipboard->map_size = 0; + return TRUE; +} + +static UINT cliprdr_send_tempdir(wfClipboard *clipboard) +{ + CLIPRDR_TEMP_DIRECTORY tempDirectory; + + if (!clipboard) + return -1; + + if (GetEnvironmentVariableA("TEMP", tempDirectory.szTempDir, sizeof(tempDirectory.szTempDir)) == + 0) + return -1; + + return clipboard->context->TempDirectory(clipboard->context, &tempDirectory); +} + +static BOOL cliprdr_GetUpdatedClipboardFormats(wfClipboard *clipboard, PUINT lpuiFormats, + UINT cFormats, PUINT pcFormatsOut) +{ + UINT index = 0; + UINT format = 0; + BOOL clipboardOpen = FALSE; + + if (!clipboard->legacyApi) + return clipboard->GetUpdatedClipboardFormats(lpuiFormats, cFormats, pcFormatsOut); + + clipboardOpen = try_open_clipboard(clipboard->hwnd); + + if (!clipboardOpen) + { + *pcFormatsOut = 0; + return TRUE; /* Other app holding clipboard */ + } + + while (index < cFormats) + { + format = EnumClipboardFormats(format); + + if (!format) + break; + + lpuiFormats[index] = format; + index++; + } + + *pcFormatsOut = index; + CloseClipboard(); + return TRUE; +} + +static UINT cliprdr_send_format_list(wfClipboard *clipboard) +{ + UINT rc; + int count = 0; + UINT32 index; + UINT32 numFormats = 0; + UINT32 formatId = 0; + char formatName[1024]; + CLIPRDR_FORMAT *formats = NULL; + CLIPRDR_FORMAT_LIST formatList = {0}; + + if (!clipboard) + return ERROR_INTERNAL_ERROR; + + ZeroMemory(&formatList, sizeof(CLIPRDR_FORMAT_LIST)); + + /* Ignore if other app is holding clipboard */ + if (try_open_clipboard(clipboard->hwnd)) + { + count = CountClipboardFormats(); + numFormats = (UINT32)count; + formats = (CLIPRDR_FORMAT *)calloc(numFormats, sizeof(CLIPRDR_FORMAT)); + + if (!formats) + { + CloseClipboard(); + return CHANNEL_RC_NO_MEMORY; + } + + index = 0; + + if (IsClipboardFormatAvailable(CF_HDROP)) + { + UINT fsid = RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + UINT fcid = RegisterClipboardFormat(CFSTR_FILECONTENTS); + + formats[index++].formatId = fsid; + formats[index++].formatId = fcid; + } + else + { + while (formatId = EnumClipboardFormats(formatId)) + formats[index++].formatId = formatId; + } + + numFormats = index; + + if (!CloseClipboard()) + { + free(formats); + return ERROR_INTERNAL_ERROR; + } + + for (index = 0; index < numFormats; index++) + { + if (GetClipboardFormatNameA(formats[index].formatId, formatName, sizeof(formatName))) + { + formats[index].formatName = _strdup(formatName); + } + } + } + + formatList.serverConnID = 0; + formatList.remoteConnID = 0; + formatList.numFormats = numFormats; + formatList.formats = formats; + formatList.msgType = CB_FORMAT_LIST; + + // send + rc = clipboard->context->ClientFormatList(clipboard->context, &formatList); + + for (index = 0; index < numFormats; index++) + free(formats[index].formatName); + + free(formats); + return rc; + // return 0; +} + +static UINT cliprdr_send_data_request(UINT32 serverConnID, UINT32 remoteConnID, wfClipboard *clipboard, UINT32 formatId) +{ + UINT rc; + UINT32 remoteFormatId; + CLIPRDR_FORMAT_DATA_REQUEST formatDataRequest; + + if (!clipboard || !clipboard->context || !clipboard->context->ClientFormatDataRequest) + return ERROR_INTERNAL_ERROR; + + remoteFormatId = get_remote_format_id(clipboard, formatId); + + formatDataRequest.serverConnID = serverConnID; + formatDataRequest.remoteConnID = remoteConnID; + formatDataRequest.requestedFormatId = remoteFormatId; + clipboard->requestedFormatId = formatId; + rc = clipboard->context->ClientFormatDataRequest(clipboard->context, &formatDataRequest); + + if (WaitForSingleObject(clipboard->response_data_event, INFINITE) != WAIT_OBJECT_0) + rc = ERROR_INTERNAL_ERROR; + else if (!ResetEvent(clipboard->response_data_event)) + rc = ERROR_INTERNAL_ERROR; + + return rc; +} + +UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 serverConnID, UINT32 remoteConnID, const void *streamid, ULONG index, + UINT32 flag, DWORD positionhigh, DWORD positionlow, + ULONG nreq) +{ + UINT rc; + CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest; + + if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest) + return ERROR_INTERNAL_ERROR; + + fileContentsRequest.serverConnID = serverConnID; + fileContentsRequest.remoteConnID = remoteConnID; + fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid; + fileContentsRequest.listIndex = index; + fileContentsRequest.dwFlags = flag; + fileContentsRequest.nPositionLow = positionlow; + fileContentsRequest.nPositionHigh = positionhigh; + fileContentsRequest.cbRequested = nreq; + fileContentsRequest.clipDataId = 0; + fileContentsRequest.msgFlags = 0; + rc = clipboard->context->ClientFileContentsRequest(clipboard->context, &fileContentsRequest); + + if (WaitForSingleObject(clipboard->req_fevent, INFINITE) != WAIT_OBJECT_0) + rc = ERROR_INTERNAL_ERROR; + else if (!ResetEvent(clipboard->req_fevent)) + rc = ERROR_INTERNAL_ERROR; + + return rc; +} + +static UINT cliprdr_send_response_filecontents(wfClipboard *clipboard, UINT32 serverConnID, UINT32 remoteConnID, UINT32 streamId, UINT32 size, + BYTE *data) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE fileContentsResponse; + + if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsResponse) + return ERROR_INTERNAL_ERROR; + + fileContentsResponse.serverConnID = serverConnID; + fileContentsResponse.remoteConnID = remoteConnID; + fileContentsResponse.streamId = streamId; + fileContentsResponse.cbRequested = size; + fileContentsResponse.requestedData = data; + fileContentsResponse.msgFlags = CB_RESPONSE_OK; + return clipboard->context->ClientFileContentsResponse(clipboard->context, + &fileContentsResponse); +} + +static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) +{ + static wfClipboard *clipboard = NULL; + + switch (Msg) + { + case WM_CREATE: + DEBUG_CLIPRDR("info: WM_CREATE"); + clipboard = (wfClipboard *)((CREATESTRUCT *)lParam)->lpCreateParams; + clipboard->hwnd = hWnd; + + if (!clipboard->legacyApi) + clipboard->AddClipboardFormatListener(hWnd); + else + clipboard->hWndNextViewer = SetClipboardViewer(hWnd); + + break; + + case WM_CLOSE: + DEBUG_CLIPRDR("info: WM_CLOSE"); + + if (!clipboard->legacyApi) + clipboard->RemoveClipboardFormatListener(hWnd); + + break; + + case WM_DESTROY: + if (clipboard->legacyApi) + ChangeClipboardChain(hWnd, clipboard->hWndNextViewer); + + break; + + case WM_CLIPBOARDUPDATE: + DEBUG_CLIPRDR("info: WM_CLIPBOARDUPDATE"); + + // if (clipboard->sync) + { + if ((GetClipboardOwner() != clipboard->hwnd) && + (S_FALSE == OleIsCurrentClipboard(clipboard->data_obj))) + { + if (clipboard->hmem) + { + GlobalFree(clipboard->hmem); + clipboard->hmem = NULL; + } + + cliprdr_send_format_list(clipboard); + } + } + + break; + + case WM_RENDERALLFORMATS: + DEBUG_CLIPRDR("info: WM_RENDERALLFORMATS"); + + /* discard all contexts in clipboard */ + if (!try_open_clipboard(clipboard->hwnd)) + { + DEBUG_CLIPRDR("OpenClipboard failed with 0x%x", GetLastError()); + break; + } + + EmptyClipboard(); + CloseClipboard(); + break; + + case WM_RENDERFORMAT: + DEBUG_CLIPRDR("info: WM_RENDERFORMAT"); + + // https://docs.microsoft.com/en-us/windows/win32/dataxchg/wm-renderformat?redirectedfrom=MSDN + if (cliprdr_send_data_request(0, 0, clipboard, (UINT32)wParam) != 0) + { + DEBUG_CLIPRDR("error: cliprdr_send_data_request failed."); + break; + } + + if (!SetClipboardData((UINT)wParam, clipboard->hmem)) + { + DEBUG_CLIPRDR("SetClipboardData failed with 0x%x", GetLastError()); + + if (clipboard->hmem) + { + GlobalFree(clipboard->hmem); + clipboard->hmem = NULL; + } + } + + /* Note: GlobalFree() is not needed when success */ + break; + + case WM_DRAWCLIPBOARD: + if (clipboard->legacyApi) + { + if ((GetClipboardOwner() != clipboard->hwnd) && + (S_FALSE == OleIsCurrentClipboard(clipboard->data_obj))) + { + cliprdr_send_format_list(clipboard); + } + + SendMessage(clipboard->hWndNextViewer, Msg, wParam, lParam); + } + + break; + + case WM_CHANGECBCHAIN: + if (clipboard->legacyApi) + { + HWND hWndCurrViewer = (HWND)wParam; + HWND hWndNextViewer = (HWND)lParam; + + if (hWndCurrViewer == clipboard->hWndNextViewer) + clipboard->hWndNextViewer = hWndNextViewer; + else if (clipboard->hWndNextViewer) + SendMessage(clipboard->hWndNextViewer, Msg, wParam, lParam); + } + + break; + + case WM_CLIPRDR_MESSAGE: + DEBUG_CLIPRDR("info: WM_CLIPRDR_MESSAGE"); + + switch (wParam) + { + case OLE_SETCLIPBOARD: + DEBUG_CLIPRDR("info: OLE_SETCLIPBOARD"); + + if (clipboard->data_obj != NULL) + { + wf_destroy_file_obj(clipboard->data_obj); + clipboard->data_obj = NULL; + } + if (wf_create_file_obj((CONN_ID *)lParam, clipboard, &clipboard->data_obj)) + { + HRESULT res = OleSetClipboard(clipboard->data_obj); + if (res != S_OK) + { + wf_destroy_file_obj(clipboard->data_obj); + clipboard->data_obj = NULL; + } + } + free((void *)lParam); + + break; + + case DELAY_RENDERING: + if (!try_open_clipboard(clipboard->hwnd)) + { + // failed to open clipboard + break; + } + + FORMAT_IDS *format_ids = (FORMAT_IDS *)lParam; + for (UINT32 i = 0; i < format_ids->size; ++i) + { + if (cliprdr_send_data_request(format_ids->serverConnID, format_ids->remoteConnID, clipboard, format_ids->formats[i]) != 0) + { + DEBUG_CLIPRDR("error: cliprdr_send_data_request failed."); + continue; + } + + if (!SetClipboardData(format_ids->formats[i], clipboard->hmem)) + { + printf("SetClipboardData failed with 0x%x\n", GetLastError()); + DEBUG_CLIPRDR("SetClipboardData failed with 0x%x", GetLastError()); + + if (clipboard->hmem) + { + GlobalFree(clipboard->hmem); + clipboard->hmem = NULL; + } + } + } + + if (!CloseClipboard() && GetLastError()) + { + // failed to close clipboard? + } + + free(format_ids->formats); + free(format_ids); + + break; + + default: + break; + } + + break; + + case WM_DESTROYCLIPBOARD: + case WM_ASKCBFORMATNAME: + case WM_HSCROLLCLIPBOARD: + case WM_PAINTCLIPBOARD: + case WM_SIZECLIPBOARD: + case WM_VSCROLLCLIPBOARD: + default: + return DefWindowProc(hWnd, Msg, wParam, lParam); + } + + return 0; +} + +static int create_cliprdr_window(wfClipboard *clipboard) +{ + WNDCLASSEX wnd_cls; + ZeroMemory(&wnd_cls, sizeof(WNDCLASSEX)); + wnd_cls.cbSize = sizeof(WNDCLASSEX); + wnd_cls.style = CS_OWNDC; + wnd_cls.lpfnWndProc = cliprdr_proc; + wnd_cls.cbClsExtra = 0; + wnd_cls.cbWndExtra = 0; + wnd_cls.hIcon = NULL; + wnd_cls.hCursor = NULL; + wnd_cls.hbrBackground = NULL; + wnd_cls.lpszMenuName = NULL; + wnd_cls.lpszClassName = _T("ClipboardHiddenMessageProcessor"); + wnd_cls.hInstance = GetModuleHandle(NULL); + wnd_cls.hIconSm = NULL; + RegisterClassEx(&wnd_cls); + clipboard->hwnd = + CreateWindowEx(WS_EX_LEFT, _T("ClipboardHiddenMessageProcessor"), _T("rdpclip"), 0, 0, 0, 0, + 0, HWND_MESSAGE, NULL, GetModuleHandle(NULL), clipboard); + + if (!clipboard->hwnd) + { + DEBUG_CLIPRDR("error: CreateWindowEx failed with %x.", GetLastError()); + return -1; + } + + return 0; +} + +static DWORD WINAPI cliprdr_thread_func(LPVOID arg) +{ + int ret; + MSG msg; + BOOL mcode; + wfClipboard *clipboard = (wfClipboard *)arg; + OleInitialize(0); + + if ((ret = create_cliprdr_window(clipboard)) != 0) + { + OleUninitialize(); + DEBUG_CLIPRDR("error: create clipboard window failed."); + return 0; + } + + while ((mcode = GetMessage(&msg, 0, 0, 0)) != 0) + { + if (mcode == -1) + { + DEBUG_CLIPRDR("error: clipboard thread GetMessage failed."); + break; + } + else + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + OleUninitialize(); + return 0; +} + +static void clear_file_array(wfClipboard *clipboard) +{ + size_t i; + + if (!clipboard) + return; + + /* clear file_names array */ + if (clipboard->file_names) + { + for (i = 0; i < clipboard->nFiles; i++) + { + free(clipboard->file_names[i]); + clipboard->file_names[i] = NULL; + } + + free(clipboard->file_names); + clipboard->file_names = NULL; + } + + /* clear fileDescriptor array */ + if (clipboard->fileDescriptor) + { + for (i = 0; i < clipboard->nFiles; i++) + { + free(clipboard->fileDescriptor[i]); + clipboard->fileDescriptor[i] = NULL; + } + + free(clipboard->fileDescriptor); + clipboard->fileDescriptor = NULL; + } + + clipboard->file_array_size = 0; + clipboard->nFiles = 0; +} + +static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG positionLow, + LONG positionHigh, DWORD nRequested, DWORD *puSize) +{ + BOOL res = FALSE; + HANDLE hFile; + DWORD nGet, rc; + + if (!file_name || !buffer || !puSize) + { + printf("get file contents Invalid Arguments.\n"); + return FALSE; + } + + hFile = CreateFileW(file_name, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, NULL); + + if (hFile == INVALID_HANDLE_VALUE) + return FALSE; + + rc = SetFilePointer(hFile, positionLow, &positionHigh, FILE_BEGIN); + + if (rc == INVALID_SET_FILE_POINTER) + goto error; + + if (!ReadFile(hFile, buffer, nRequested, &nGet, NULL)) + { + DEBUG_CLIPRDR("ReadFile failed with 0x%08lX.", GetLastError()); + goto error; + } + + res = TRUE; +error: + + if (!CloseHandle(hFile)) + res = FALSE; + + if (res) + *puSize = nGet; + + return res; +} + +/* path_name has a '\' at the end. e.g. c:\newfolder\, file_name is c:\newfolder\new.txt */ +static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t pathLen) +{ + HANDLE hFile; + FILEDESCRIPTORW *fd; + fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW)); + + if (!fd) + return NULL; + + hFile = CreateFileW(file_name, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, NULL); + + if (hFile == INVALID_HANDLE_VALUE) + { + free(fd); + return NULL; + } + + // fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI; + fd->dwFlags = FD_ATTRIBUTES | FD_WRITESTIME | FD_PROGRESSUI; + fd->dwFileAttributes = GetFileAttributesW(file_name); + if (fd->dwFileAttributes == INVALID_FILE_ATTRIBUTES) + { + // TODO: debug handle some errors + } + + if (!GetFileTime(hFile, NULL, NULL, &fd->ftLastWriteTime)) + { + fd->dwFlags &= ~FD_WRITESTIME; + } + + fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh); + wcscpy_s(fd->cFileName, sizeof(fd->cFileName) / 2, file_name + pathLen); + CloseHandle(hFile); + + return fd; +} + +static BOOL wf_cliprdr_array_ensure_capacity(wfClipboard *clipboard) +{ + if (!clipboard) + return FALSE; + + if (clipboard->nFiles == clipboard->file_array_size) + { + size_t new_size; + FILEDESCRIPTORW **new_fd; + WCHAR **new_name; + new_size = (clipboard->file_array_size + 1) * 2; + new_fd = (FILEDESCRIPTORW **)realloc(clipboard->fileDescriptor, + new_size * sizeof(FILEDESCRIPTORW *)); + + if (new_fd) + clipboard->fileDescriptor = new_fd; + + new_name = (WCHAR **)realloc(clipboard->file_names, new_size * sizeof(WCHAR *)); + + if (new_name) + clipboard->file_names = new_name; + + if (!new_fd || !new_name) + return FALSE; + + clipboard->file_array_size = new_size; + } + + return TRUE; +} + +static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_file_name, + size_t pathLen) +{ + if (!wf_cliprdr_array_ensure_capacity(clipboard)) + return FALSE; + + /* add to name array */ + clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc(MAX_PATH * 2); + + if (!clipboard->file_names[clipboard->nFiles]) + return FALSE; + + wcscpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name); + /* add to descriptor array */ + clipboard->fileDescriptor[clipboard->nFiles] = + wf_cliprdr_get_file_descriptor(full_file_name, pathLen); + + if (!clipboard->fileDescriptor[clipboard->nFiles]) + { + free(clipboard->file_names[clipboard->nFiles]); + return FALSE; + } + + clipboard->nFiles++; + return TRUE; +} + +static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, size_t pathLen) +{ + HANDLE hFind; + WCHAR DirSpec[MAX_PATH]; + WIN32_FIND_DATA FindFileData; + + if (!clipboard || !Dir) + return FALSE; + + // StringCchCopy(DirSpec, MAX_PATH, Dir); + // StringCchCat(DirSpec, MAX_PATH, TEXT("\\*")); + StringCchCopyW(DirSpec, MAX_PATH, Dir); + StringCchCatW(DirSpec, MAX_PATH, L"\\*"); + + // hFind = FindFirstFile(DirSpec, &FindFileData); + hFind = FindFirstFileW(DirSpec, &FindFileData); + + if (hFind == INVALID_HANDLE_VALUE) + { + // printf("FindFirstFile failed with 0x%x.\n", GetLastError()); + DEBUG_CLIPRDR("FindFirstFile failed with 0x%x.", GetLastError()); + return FALSE; + } + + while (FindNextFileW(hFind, &FindFileData)) + { + // if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && + // wcscmp(FindFileData.cFileName, _T(".")) == 0 || + // wcscmp(FindFileData.cFileName, _T("..")) == 0) + if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && + wcscmp(FindFileData.cFileName, L".") == 0 || + wcscmp(FindFileData.cFileName, L"..") == 0) + { + continue; + } + + if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) + { + WCHAR DirAdd[MAX_PATH]; + // StringCchCopy(DirAdd, MAX_PATH, Dir); + // StringCchCat(DirAdd, MAX_PATH, _T("\\")); + // StringCchCat(DirAdd, MAX_PATH, FindFileData.cFileName); + StringCchCopyW(DirAdd, MAX_PATH, Dir); + StringCchCatW(DirAdd, MAX_PATH, L"\\"); + StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName); + + if (!wf_cliprdr_add_to_file_arrays(clipboard, DirAdd, pathLen)) + return FALSE; + + if (!wf_cliprdr_traverse_directory(clipboard, DirAdd, pathLen)) + return FALSE; + } + else + { + WCHAR fileName[MAX_PATH]; + // StringCchCopy(fileName, MAX_PATH, Dir); + // StringCchCat(fileName, MAX_PATH, _T("\\")); + // StringCchCat(fileName, MAX_PATH, FindFileData.cFileName); + + StringCchCopyW(fileName, MAX_PATH, Dir); + StringCchCatW(fileName, MAX_PATH, L"\\"); + StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName); + + if (!wf_cliprdr_add_to_file_arrays(clipboard, fileName, pathLen)) + return FALSE; + } + } + + FindClose(hFind); + return TRUE; +} + +static UINT wf_cliprdr_send_client_capabilities(wfClipboard *clipboard) +{ + CLIPRDR_CAPABILITIES capabilities; + CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet; + + if (!clipboard || !clipboard->context || !clipboard->context->ClientCapabilities) + return ERROR_INTERNAL_ERROR; + + capabilities.serverConnID = 0; + capabilities.remoteConnID = 0; + capabilities.cCapabilitiesSets = 1; + capabilities.capabilitySets = (CLIPRDR_CAPABILITY_SET *)&(generalCapabilitySet); + generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL; + generalCapabilitySet.capabilitySetLength = 12; + generalCapabilitySet.version = CB_CAPS_VERSION_2; + generalCapabilitySet.generalFlags = + CB_USE_LONG_FORMAT_NAMES | CB_STREAM_FILECLIP_ENABLED | CB_FILECLIP_NO_FILE_PATHS; + return clipboard->context->ClientCapabilities(clipboard->context, &capabilities); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_cliprdr_monitor_ready(CliprdrClientContext *context, + const CLIPRDR_MONITOR_READY *monitorReady) +{ + UINT rc; + wfClipboard *clipboard = (wfClipboard *)context->custom; + + if (!context || !monitorReady) + return ERROR_INTERNAL_ERROR; + + clipboard->sync = TRUE; + rc = wf_cliprdr_send_client_capabilities(clipboard); + + if (rc != CHANNEL_RC_OK) + return rc; + + return cliprdr_send_format_list(clipboard); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_cliprdr_server_capabilities(CliprdrClientContext *context, + const CLIPRDR_CAPABILITIES *capabilities) +{ + UINT32 index; + CLIPRDR_CAPABILITY_SET *capabilitySet; + wfClipboard *clipboard = (wfClipboard *)context->custom; + + if (!context || !capabilities) + return ERROR_INTERNAL_ERROR; + + for (index = 0; index < capabilities->cCapabilitiesSets; index++) + { + capabilitySet = &(capabilities->capabilitySets[index]); + + if ((capabilitySet->capabilitySetType == CB_CAPSTYPE_GENERAL) && + (capabilitySet->capabilitySetLength >= CB_CAPSTYPE_GENERAL_LEN)) + { + CLIPRDR_GENERAL_CAPABILITY_SET *generalCapabilitySet = + (CLIPRDR_GENERAL_CAPABILITY_SET *)capabilitySet; + clipboard->capabilities = generalCapabilitySet->generalFlags; + break; + } + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context, + const CLIPRDR_FORMAT_LIST *formatList) +{ + UINT rc = ERROR_INTERNAL_ERROR; + UINT32 i; + formatMapping *mapping; + CLIPRDR_FORMAT *format; + wfClipboard *clipboard = (wfClipboard *)context->custom; + + if (!clear_format_map(clipboard)) + return ERROR_INTERNAL_ERROR; + + for (i = 0; i < formatList->numFormats; i++) + { + format = &(formatList->formats[i]); + mapping = &(clipboard->format_mappings[i]); + mapping->remote_format_id = format->formatId; + + if (format->formatName) + { + int size = MultiByteToWideChar(CP_UTF8, 0, format->formatName, + strlen(format->formatName), NULL, 0); + mapping->name = calloc(size + 1, sizeof(WCHAR)); + + if (mapping->name) + { + MultiByteToWideChar(CP_UTF8, 0, format->formatName, strlen(format->formatName), + mapping->name, size); + mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name); + } + } + else + { + mapping->name = NULL; + mapping->local_format_id = mapping->remote_format_id; + } + + clipboard->map_size++; + map_ensure_capacity(clipboard); + } + + if (file_transferring(clipboard)) + { + if (context->enableFiles) + { + CONN_ID *p_conn_id = (CONN_ID *)calloc(1, sizeof(CONN_ID)); + p_conn_id->serverConnID = formatList->serverConnID; + p_conn_id->remoteConnID = formatList->remoteConnID; + if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id)) + rc = CHANNEL_RC_OK; + } + else + { + rc = CHANNEL_RC_OK; + } + } + else + { + if (context->enableOthers) + { + if (!try_open_clipboard(clipboard->hwnd)) + return CHANNEL_RC_OK; /* Ignore, other app holding clipboard */ + + if (EmptyClipboard()) + { + // Modified: do not apply delayed rendering + // for (i = 0; i < (UINT32)clipboard->map_size; i++) + // SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL); + + FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS)); + format_ids->serverConnID = formatList->serverConnID; + format_ids->remoteConnID = formatList->remoteConnID; + format_ids->size = (UINT32)clipboard->map_size; + format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32)); + for (i = 0; i < format_ids->size; ++i) + { + format_ids->formats[i] = clipboard->format_mappings[i].local_format_id; + } + if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAY_RENDERING, format_ids)) + { + rc = CHANNEL_RC_OK; + } + else + { + rc = ERROR_INTERNAL_ERROR; + } + } + + if (!CloseClipboard() && GetLastError()) + return ERROR_INTERNAL_ERROR; + } + else + { + rc = CHANNEL_RC_OK; + } + } + + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_format_list_response(CliprdrClientContext *context, + const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse) +{ + (void)context; + (void)formatListResponse; + + if (formatListResponse->msgFlags != CB_RESPONSE_OK) + return E_FAIL; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_lock_clipboard_data(CliprdrClientContext *context, + const CLIPRDR_LOCK_CLIPBOARD_DATA *lockClipboardData) +{ + (void)context; + (void)lockClipboardData; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_unlock_clipboard_data(CliprdrClientContext *context, + const CLIPRDR_UNLOCK_CLIPBOARD_DATA *unlockClipboardData) +{ + (void)context; + (void)unlockClipboardData; + return CHANNEL_RC_OK; +} + +static BOOL wf_cliprdr_process_filename(wfClipboard *clipboard, WCHAR *wFileName, size_t str_len) +{ + size_t pathLen; + size_t offset = str_len; + + if (!clipboard || !wFileName) + return FALSE; + + /* find the last '\' in full file name */ + while (offset > 0) + { + if (wFileName[offset] == L'\\') + break; + else + offset--; + } + + pathLen = offset + 1; + + if (!wf_cliprdr_add_to_file_arrays(clipboard, wFileName, pathLen)) + return FALSE; + + if ((clipboard->fileDescriptor[clipboard->nFiles - 1]->dwFileAttributes & + FILE_ATTRIBUTE_DIRECTORY) != 0) + { + /* this is a directory */ + if (!wf_cliprdr_traverse_directory(clipboard, wFileName, pathLen)) + return FALSE; + } + + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_format_data_request(CliprdrClientContext *context, + const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest) +{ + UINT rc; + size_t size = 0; + void *buff = NULL; + char *globlemem = NULL; + HANDLE hClipdata = NULL; + UINT32 requestedFormatId; + CLIPRDR_FORMAT_DATA_RESPONSE response; + wfClipboard *clipboard; + + if (!context || !formatDataRequest) + return ERROR_INTERNAL_ERROR; + + clipboard = (wfClipboard *)context->custom; + + if (!clipboard) + return ERROR_INTERNAL_ERROR; + + requestedFormatId = formatDataRequest->requestedFormatId; + + if (requestedFormatId == RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW)) + { + size_t len; + size_t i; + WCHAR *wFileName; + HRESULT result; + LPDATAOBJECT dataObj; + FORMATETC format_etc; + STGMEDIUM stg_medium; + DROPFILES *dropFiles; + FILEGROUPDESCRIPTORW *groupDsc; + result = OleGetClipboard(&dataObj); + + if (FAILED(result)) + return ERROR_INTERNAL_ERROR; + + ZeroMemory(&format_etc, sizeof(FORMATETC)); + ZeroMemory(&stg_medium, sizeof(STGMEDIUM)); + /* get DROPFILES struct from OLE */ + format_etc.cfFormat = CF_HDROP; + format_etc.tymed = TYMED_HGLOBAL; + format_etc.dwAspect = 1; + format_etc.lindex = -1; + result = IDataObject_GetData(dataObj, &format_etc, &stg_medium); + + if (FAILED(result)) + { + DEBUG_CLIPRDR("dataObj->GetData failed."); + goto exit; + } + + dropFiles = (DROPFILES *)GlobalLock(stg_medium.hGlobal); + + if (!dropFiles) + { + GlobalUnlock(stg_medium.hGlobal); + ReleaseStgMedium(&stg_medium); + clipboard->nFiles = 0; + goto exit; + } + + clear_file_array(clipboard); + + if (dropFiles->fWide) + { + /* dropFiles contains file names */ + for (wFileName = (WCHAR *)((char *)dropFiles + dropFiles->pFiles); + (len = wcslen(wFileName)) > 0; wFileName += len + 1) + { + wf_cliprdr_process_filename(clipboard, wFileName, wcslen(wFileName)); + } + } + else + { + char *p; + + for (p = (char *)((char *)dropFiles + dropFiles->pFiles); (len = strlen(p)) > 0; + p += len + 1, clipboard->nFiles++) + { + int cchWideChar; + WCHAR *wFileName; + cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0); + wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR)); + MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar); + wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar); + } + } + + GlobalUnlock(stg_medium.hGlobal); + ReleaseStgMedium(&stg_medium); + exit: + size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW); + groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size); + + if (groupDsc) + { + groupDsc->cItems = clipboard->nFiles; + + for (i = 0; i < clipboard->nFiles; i++) + { + if (clipboard->fileDescriptor[i]) + groupDsc->fgd[i] = *clipboard->fileDescriptor[i]; + } + + buff = groupDsc; + } + + IDataObject_Release(dataObj); + } + else + { + /* Ignore if other app is holding the clipboard */ + if (try_open_clipboard(clipboard->hwnd)) + { + hClipdata = GetClipboardData(requestedFormatId); + + if (!hClipdata) + { + CloseClipboard(); + return ERROR_INTERNAL_ERROR; + } + + globlemem = (char *)GlobalLock(hClipdata); + size = (int)GlobalSize(hClipdata); + buff = malloc(size); + CopyMemory(buff, globlemem, size); + GlobalUnlock(hClipdata); + CloseClipboard(); + } + } + + response.serverConnID = formatDataRequest->serverConnID; + response.remoteConnID = formatDataRequest->remoteConnID; + response.msgFlags = CB_RESPONSE_OK; + response.dataLen = size; + response.requestedFormatData = (BYTE *)buff; + rc = clipboard->context->ClientFormatDataResponse(clipboard->context, &response); + + free(buff); + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_format_data_response(CliprdrClientContext *context, + const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse) +{ + BYTE *data; + HANDLE hMem; + wfClipboard *clipboard; + + if (!context || !formatDataResponse) + return ERROR_INTERNAL_ERROR; + + if (formatDataResponse->msgFlags != CB_RESPONSE_OK) + return E_FAIL; + + clipboard = (wfClipboard *)context->custom; + + if (!clipboard) + return ERROR_INTERNAL_ERROR; + + hMem = GlobalAlloc(GMEM_MOVEABLE, formatDataResponse->dataLen); + + if (!hMem) + return ERROR_INTERNAL_ERROR; + + data = (BYTE *)GlobalLock(hMem); + + if (!data) + { + GlobalFree(hMem); + return ERROR_INTERNAL_ERROR; + } + + CopyMemory(data, formatDataResponse->requestedFormatData, formatDataResponse->dataLen); + + if (!GlobalUnlock(hMem) && GetLastError()) + { + GlobalFree(hMem); + return ERROR_INTERNAL_ERROR; + } + + clipboard->hmem = hMem; + + if (!SetEvent(clipboard->response_data_event)) + return ERROR_INTERNAL_ERROR; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_file_contents_request(CliprdrClientContext *context, + const CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest) +{ + DWORD uSize = 0; + BYTE *pData = NULL; + HRESULT hRet = S_OK; + FORMATETC vFormatEtc; + LPDATAOBJECT pDataObj = NULL; + STGMEDIUM vStgMedium; + BOOL bIsStreamFile = TRUE; + static LPSTREAM pStreamStc = NULL; + static UINT32 uStreamIdStc = 0; + wfClipboard *clipboard; + UINT rc = ERROR_INTERNAL_ERROR; + UINT sRc; + UINT32 cbRequested; + + if (!context || !fileContentsRequest) + return ERROR_INTERNAL_ERROR; + + clipboard = (wfClipboard *)context->custom; + + if (!clipboard) + return ERROR_INTERNAL_ERROR; + + cbRequested = fileContentsRequest->cbRequested; + if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE) + cbRequested = sizeof(UINT64); + + pData = (BYTE *)calloc(1, cbRequested); + + if (!pData) + goto error; + + hRet = OleGetClipboard(&pDataObj); + + if (FAILED(hRet)) + { + printf("filecontents: get ole clipboard failed.\n"); + goto error; + } + + ZeroMemory(&vFormatEtc, sizeof(FORMATETC)); + ZeroMemory(&vStgMedium, sizeof(STGMEDIUM)); + vFormatEtc.cfFormat = RegisterClipboardFormat(CFSTR_FILECONTENTS); + vFormatEtc.tymed = TYMED_ISTREAM; + vFormatEtc.dwAspect = 1; + vFormatEtc.lindex = fileContentsRequest->listIndex; + vFormatEtc.ptd = NULL; + + if ((uStreamIdStc != fileContentsRequest->streamId) || !pStreamStc) + { + LPENUMFORMATETC pEnumFormatEtc; + ULONG CeltFetched; + FORMATETC vFormatEtc2; + + if (pStreamStc) + { + IStream_Release(pStreamStc); + pStreamStc = NULL; + } + + bIsStreamFile = FALSE; + hRet = IDataObject_EnumFormatEtc(pDataObj, DATADIR_GET, &pEnumFormatEtc); + + if (hRet == S_OK) + { + do + { + hRet = IEnumFORMATETC_Next(pEnumFormatEtc, 1, &vFormatEtc2, &CeltFetched); + + if (hRet == S_OK) + { + if (vFormatEtc2.cfFormat == RegisterClipboardFormat(CFSTR_FILECONTENTS)) + { + hRet = IDataObject_GetData(pDataObj, &vFormatEtc, &vStgMedium); + + if (hRet == S_OK) + { + pStreamStc = vStgMedium.pstm; + uStreamIdStc = fileContentsRequest->streamId; + bIsStreamFile = TRUE; + } + + break; + } + } + } while (hRet == S_OK); + } + } + + if (bIsStreamFile == TRUE) + { + if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE) + { + STATSTG vStatStg; + ZeroMemory(&vStatStg, sizeof(STATSTG)); + hRet = IStream_Stat(pStreamStc, &vStatStg, STATFLAG_NONAME); + + if (hRet == S_OK) + { + *((UINT32 *)&pData[0]) = vStatStg.cbSize.LowPart; + *((UINT32 *)&pData[4]) = vStatStg.cbSize.HighPart; + uSize = cbRequested; + } + } + else if (fileContentsRequest->dwFlags == FILECONTENTS_RANGE) + { + LARGE_INTEGER dlibMove; + ULARGE_INTEGER dlibNewPosition; + dlibMove.HighPart = fileContentsRequest->nPositionHigh; + dlibMove.LowPart = fileContentsRequest->nPositionLow; + hRet = IStream_Seek(pStreamStc, dlibMove, STREAM_SEEK_SET, &dlibNewPosition); + + if (SUCCEEDED(hRet)) + hRet = IStream_Read(pStreamStc, pData, cbRequested, (PULONG)&uSize); + } + } + else + { + if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE) + { + if (clipboard->nFiles <= fileContentsRequest->listIndex) + goto error; + *((UINT32 *)&pData[0]) = + clipboard->fileDescriptor[fileContentsRequest->listIndex]->nFileSizeLow; + *((UINT32 *)&pData[4]) = + clipboard->fileDescriptor[fileContentsRequest->listIndex]->nFileSizeHigh; + uSize = cbRequested; + } + else if (fileContentsRequest->dwFlags == FILECONTENTS_RANGE) + { + BOOL bRet; + if (clipboard->nFiles <= fileContentsRequest->listIndex) + goto error; + bRet = wf_cliprdr_get_file_contents( + clipboard->file_names[fileContentsRequest->listIndex], pData, + fileContentsRequest->nPositionLow, fileContentsRequest->nPositionHigh, cbRequested, + &uSize); + + if (bRet == FALSE) + { + printf("get file contents failed.\n"); + uSize = 0; + goto error; + } + } + } + + rc = CHANNEL_RC_OK; +error: + + if (pDataObj) + IDataObject_Release(pDataObj); + + if (uSize == 0) + { + free(pData); + pData = NULL; + } + + sRc = + cliprdr_send_response_filecontents( + clipboard, + fileContentsRequest->serverConnID, + fileContentsRequest->remoteConnID, + fileContentsRequest->streamId, + uSize, + pData); + free(pData); + + if (sRc != CHANNEL_RC_OK) + return sRc; + + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_file_contents_response(CliprdrClientContext *context, + const CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse) +{ + wfClipboard *clipboard; + + if (!context || !fileContentsResponse) + return ERROR_INTERNAL_ERROR; + + if (fileContentsResponse->msgFlags != CB_RESPONSE_OK) + return E_FAIL; + + clipboard = (wfClipboard *)context->custom; + + if (!clipboard) + return ERROR_INTERNAL_ERROR; + + clipboard->req_fsize = fileContentsResponse->cbRequested; + clipboard->req_fdata = (char *)malloc(fileContentsResponse->cbRequested); + + if (!clipboard->req_fdata) + return ERROR_INTERNAL_ERROR; + + CopyMemory(clipboard->req_fdata, fileContentsResponse->requestedData, + fileContentsResponse->cbRequested); + + if (!SetEvent(clipboard->req_fevent)) + { + free(clipboard->req_fdata); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr) +{ + if (!clipboard || !cliprdr) + return FALSE; + + clipboard->context = cliprdr; + clipboard->sync = FALSE; + clipboard->map_capacity = 32; + clipboard->map_size = 0; + clipboard->hUser32 = LoadLibraryA("user32.dll"); + clipboard->data_obj = NULL; + + if (clipboard->hUser32) + { + clipboard->AddClipboardFormatListener = (fnAddClipboardFormatListener)GetProcAddress( + clipboard->hUser32, "AddClipboardFormatListener"); + clipboard->RemoveClipboardFormatListener = (fnRemoveClipboardFormatListener)GetProcAddress( + clipboard->hUser32, "RemoveClipboardFormatListener"); + clipboard->GetUpdatedClipboardFormats = (fnGetUpdatedClipboardFormats)GetProcAddress( + clipboard->hUser32, "GetUpdatedClipboardFormats"); + } + + if (!(clipboard->hUser32 && clipboard->AddClipboardFormatListener && + clipboard->RemoveClipboardFormatListener && clipboard->GetUpdatedClipboardFormats)) + clipboard->legacyApi = TRUE; + + if (!(clipboard->format_mappings = + (formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping)))) + goto error; + + if (!(clipboard->response_data_event = CreateEvent(NULL, TRUE, FALSE, NULL))) + goto error; + + if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL))) + goto error; + + if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL))) + goto error; + + cliprdr->MonitorReady = wf_cliprdr_monitor_ready; + cliprdr->ServerCapabilities = wf_cliprdr_server_capabilities; + cliprdr->ServerFormatList = wf_cliprdr_server_format_list; + cliprdr->ServerFormatListResponse = wf_cliprdr_server_format_list_response; + cliprdr->ServerLockClipboardData = wf_cliprdr_server_lock_clipboard_data; + cliprdr->ServerUnlockClipboardData = wf_cliprdr_server_unlock_clipboard_data; + cliprdr->ServerFormatDataRequest = wf_cliprdr_server_format_data_request; + cliprdr->ServerFormatDataResponse = wf_cliprdr_server_format_data_response; + cliprdr->ServerFileContentsRequest = wf_cliprdr_server_file_contents_request; + cliprdr->ServerFileContentsResponse = wf_cliprdr_server_file_contents_response; + cliprdr->custom = (void *)clipboard; + return TRUE; +error: + wf_cliprdr_uninit(clipboard, cliprdr); + return FALSE; +} + +BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr) +{ + if (!clipboard || !cliprdr) + return FALSE; + + cliprdr->custom = NULL; + + if (clipboard->hwnd) + PostMessage(clipboard->hwnd, WM_QUIT, 0, 0); + + if (clipboard->thread) + { + WaitForSingleObject(clipboard->thread, INFINITE); + CloseHandle(clipboard->thread); + } + + if (clipboard->data_obj) + { + wf_destroy_file_obj(clipboard->data_obj); + clipboard->data_obj = NULL; + } + + if (clipboard->response_data_event) + CloseHandle(clipboard->response_data_event); + + if (clipboard->req_fevent) + CloseHandle(clipboard->req_fevent); + + clear_file_array(clipboard); + clear_format_map(clipboard); + free(clipboard->format_mappings); + return TRUE; +} + +wfClipboard clipboard; + +BOOL init_cliprdr(CliprdrClientContext *context) +{ + return wf_cliprdr_init(&clipboard, context); +} + +BOOL uninit_cliprdr(CliprdrClientContext *context) +{ + return wf_cliprdr_uninit(&clipboard, context); +} diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 16470a753..a20f5c5ae 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -306,6 +306,73 @@ message FileDirCreate { string path = 2; } +// main logic from freeRDP +message CliprdrMonitorReady { + int32 server_conn_id = 1; + int32 remote_conn_id = 2; +} + +message CliprdrFormat { + int32 server_conn_id = 1; + int32 remote_conn_id = 2; + int32 id = 3; + string format = 4; +} +message CliprdrServerFormatList { + int32 server_conn_id = 1; + int32 remote_conn_id = 2; + repeated CliprdrFormat formats = 3; +} +message CliprdrServerFormatListResponse { + int32 server_conn_id = 1; + int32 remote_conn_id = 2; + int32 msg_flags = 3; +} + +message CliprdrServerFormatDataRequest { + int32 server_conn_id = 1; + int32 remote_conn_id = 2; + int32 requested_format_id = 3; +} +message CliprdrServerFormatDataResponse { + int32 server_conn_id = 1; + int32 remote_conn_id = 2; + int32 msg_flags = 3; + bytes format_data = 4; +} + +message CliprdrFileContentsRequest { + int32 server_conn_id = 1; + int32 remote_conn_id = 2; + int32 stream_id = 3; + int32 list_index = 4; + int32 dw_flags = 5; + int32 n_position_low = 6; + int32 n_position_high = 7; + int32 cb_requested = 8; + bool have_clip_data_id = 9; + int32 clip_data_id = 10; +} +message CliprdrFileContentsResponse { + int32 server_conn_id = 1; + int32 remote_conn_id = 2; + int32 msg_flags = 3; + int32 stream_id = 4; + bytes requested_data = 5; +} + +message Cliprdr { + oneof union { + CliprdrMonitorReady ready = 1; + CliprdrServerFormatList format_list = 2; + CliprdrServerFormatListResponse format_list_response = 3; + CliprdrServerFormatDataRequest format_data_request = 4; + CliprdrServerFormatDataResponse format_data_response = 5; + CliprdrFileContentsRequest file_contents_request = 6; + CliprdrFileContentsResponse file_contents_response = 7; + } +} + message SwitchDisplay { int32 display = 1; sint32 x = 2; @@ -405,5 +472,6 @@ message Message { FileAction file_action = 17; FileResponse file_response = 18; Misc misc = 19; + Cliprdr cliprdr = 20; } } diff --git a/src/common.rs b/src/common.rs index 16da93b7c..e8e59dea7 100644 --- a/src/common.rs +++ b/src/common.rs @@ -16,6 +16,7 @@ use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; use std::sync::{Arc, Mutex}; pub const CLIPBOARD_NAME: &'static str = "clipboard"; +pub const CLIPRDR_NAME: &'static str = "cliprdr"; pub const CLIPBOARD_INTERVAL: u64 = 333; lazy_static::lazy_static! { diff --git a/src/server.rs b/src/server.rs index 2951c8982..ed7315200 100644 --- a/src/server.rs +++ b/src/server.rs @@ -29,6 +29,8 @@ use std::{ mod audio_service; mod clipboard_service; +#[cfg(windows)] +pub mod cliprdr_service; mod connection; pub mod input_service; mod service; @@ -61,6 +63,8 @@ pub fn new() -> ServerPtr { server.add_service(Box::new(audio_service::new())); server.add_service(Box::new(video_service::new())); server.add_service(Box::new(clipboard_service::new())); + #[cfg(windows)] + server.add_service(Box::new(cliprdr_service::new())); server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_pos())); Arc::new(RwLock::new(server)) diff --git a/src/server/cliprdr_service.rs b/src/server/cliprdr_service.rs new file mode 100644 index 000000000..53ff10dcf --- /dev/null +++ b/src/server/cliprdr_service.rs @@ -0,0 +1,104 @@ +use super::*; +use clipboard::{create_cliprdr_context, get_rx_client_msg, server_msg, ConnID}; +use hbb_common::{ + log, + tokio::sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Mutex as TokioMutex, + }, + tokio::time::{self, Duration, Instant}, + ResultType, +}; +use std::sync::atomic::{AtomicBool, Ordering}; + +pub use crate::common::CLIPRDR_NAME as NAME; + +lazy_static::lazy_static! { + static ref MSG_CHANNEL_SERVER: (UnboundedSender<(ConnID, Cliprdr)>, TokioMutex>) = { + let (tx, rx) = unbounded_channel(); + (tx, TokioMutex::new(rx)) + }; +} + +static RUNNING: AtomicBool = AtomicBool::new(false); + +pub fn new() -> GenericService { + let sp = GenericService::new(NAME, true); + sp.run::<_>(listen::run); + sp +} + +pub fn handle_serve_cliprdr_msg(id: i32, msg: Cliprdr) { + if RUNNING.load(Ordering::SeqCst) { + log::debug!("handle handle_serve_cliprdr_msg"); + MSG_CHANNEL_SERVER + .0 + .send(( + ConnID { + server_conn_id: id as u32, + remote_conn_id: 0, + }, + msg, + )) + .unwrap(); + } else { + // should not reach this branch + } +} + +mod listen { + use super::*; + + static WAIT: Duration = Duration::from_millis(1500); + + #[tokio::main] + pub async fn run(sp: GenericService) -> ResultType<()> { + let mut cliprdr_context = create_cliprdr_context(true, false)?; + + RUNNING.store(false, Ordering::SeqCst); + + let mut timer = time::interval_at(Instant::now() + WAIT, WAIT); + let mut client_rx = get_rx_client_msg().lock().await; + let mut server_rx = MSG_CHANNEL_SERVER.1.lock().await; + while sp.ok() { + RUNNING.store(true, Ordering::SeqCst); + + tokio::select! { + msg = client_rx.recv() => { + match msg { + Some((conn_id, msg)) => { + if conn_id.server_conn_id == 0 { + sp.send(msg) + } else { + sp.send_to(msg, conn_id.server_conn_id as i32) + } + } + None => { + unreachable!() + } + } + } + msg = server_rx.recv() => { + match msg { + Some((conn_id, msg)) => { + let res = server_msg(&mut cliprdr_context, conn_id, msg); + if res != 0 { + // log::warn!("failed to process message for {}", id); + } + } + None => { + unreachable!() + } + } + } + _ = timer.tick() => {}, + } + sp.snapshot(|_| Ok(()))?; + } + + RUNNING.store(false, Ordering::SeqCst); + log::info!("Clipboard listener stopped!"); + + Ok(()) + } +} diff --git a/src/server/connection.rs b/src/server/connection.rs index 9b8b43f25..e2461e88c 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -224,6 +224,10 @@ impl Connection { s.write().unwrap().subscribe( super::clipboard_service::NAME, conn.inner.clone(), conn.clipboard_enabled() && conn.keyboard); + #[cfg(windows)] + s.write().unwrap().subscribe( + super::cliprdr_service::NAME, + conn.inner.clone(), conn.clipboard_enabled() && conn.keyboard); } } else if &name == "audio" { conn.audio = enabled; @@ -600,6 +604,8 @@ impl Connection { } if !self.clipboard_enabled() || !self.keyboard { noperms.push(super::clipboard_service::NAME); + #[cfg(windows)] + noperms.push(super::cliprdr_service::NAME); } if !self.audio_enabled() { noperms.push(super::audio_service::NAME); @@ -824,6 +830,11 @@ impl Connection { update_clipboard(cb, None); } } + #[cfg(windows)] + Some(message::Union::cliprdr(clip)) => { + log::debug!("received cliprdr msg"); + cliprdr_service::handle_serve_cliprdr_msg(self.inner.id, clip) + } Some(message::Union::file_action(fa)) => { if self.file_transfer.is_some() { match fa.union { @@ -993,6 +1004,12 @@ impl Connection { self.inner.clone(), self.clipboard_enabled() && self.keyboard, ); + #[cfg(windows)] + s.write().unwrap().subscribe( + super::cliprdr_service::NAME, + self.inner.clone(), + self.clipboard_enabled() && self.keyboard, + ); } } } diff --git a/src/server/service.rs b/src/server/service.rs index 9c146b047..176d9fdf5 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -137,6 +137,12 @@ impl> ServiceTmpl { self.send_shared(Arc::new(msg)); } + pub fn send_to(&self, msg: Message, id: i32) { + if let Some(s) = self.0.write().unwrap().subscribes.get_mut(&id) { + s.send(Arc::new(msg)); + } + } + pub fn send_shared(&self, msg: Arc) { let mut lock = self.0.write().unwrap(); for s in lock.subscribes.values_mut() { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index b40e32a33..be26b1f57 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -2,6 +2,10 @@ use crate::client::*; use crate::common::{ self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL, }; +#[cfg(windows)] +use clipboard::{ + cliprdr::CliprdrClientContext, create_cliprdr_context, get_rx_client_msg, server_msg, ConnID, +}; use enigo::{self, Enigo, KeyboardControllable}; use hbb_common::{ allow_err, @@ -1159,6 +1163,16 @@ async fn io_loop(handler: Handler) { .as_mut() .map(|v| v.render_frame(data).ok()); }); + + #[cfg(windows)] + let cliprdr_context = match create_cliprdr_context(true, false) { + Ok(context) => Some(context), + Err(err) => { + handler.msgbox("error", "Create clipboard error", &err.to_string()); + None + } + }; + let mut remote = Remote { handler, video_sender, @@ -1172,6 +1186,10 @@ async fn io_loop(handler: Handler) { timer: time::interval(SEC30), last_update_jobs_status: (Instant::now(), Default::default()), first_frame: false, + #[cfg(windows)] + cliprdr_context, + #[cfg(windows)] + pid: std::process::id(), }; remote.io_loop().await; } @@ -1211,6 +1229,10 @@ struct Remote { timer: Interval, last_update_jobs_status: (Instant, HashMap), first_frame: bool, + #[cfg(windows)] + cliprdr_context: Option>, + #[cfg(windows)] + pid: u32, } impl Remote { @@ -1230,6 +1252,13 @@ impl Remote { } self.handler .call("setConnectionType", &make_args!(peer.is_secured(), direct)); + + // just build for now + #[cfg(not(windows))] + let (_client_tx, mut client_rx) = mpsc::unbounded_channel::(); + #[cfg(windows)] + let mut client_rx = get_rx_client_msg().lock().await; + loop { tokio::select! { res = peer.next() => { @@ -1260,6 +1289,21 @@ impl Remote { } } } + msg = client_rx.recv() => { + #[cfg(not(windows))] + println!("{:?}", msg); + #[cfg(windows)] + match msg { + Some((conn_id, msg)) => { + if conn_id.remote_conn_id == 0 || conn_id.remote_conn_id == self.pid { + allow_err!(peer.send(&msg).await); + } + } + None => { + unreachable!() + } + } + } _ = self.timer.tick() => { if last_recv_time.elapsed() >= SEC30 { self.handler.msgbox("error", "Connection Error", "Timeout"); @@ -1631,6 +1675,24 @@ impl Remote { update_clipboard(cb, Some(&self.old_clipboard)); } } + #[allow(unused_variables)] + Some(message::Union::cliprdr(clip)) => { + log::debug!("received cliprdr msg"); + #[cfg(windows)] + if !self.handler.lc.read().unwrap().disable_clipboard { + if let Some(context) = &mut self.cliprdr_context { + let res = server_msg( + context, + ConnID { + server_conn_id: 0, + remote_conn_id: self.pid, + }, + clip, + ); + log::debug!("server msg returns {}", res); + } + } + } Some(message::Union::file_response(fr)) => match fr.union { Some(file_response::Union::dir(fd)) => { let entries = fd.entries.to_vec();