diff --git a/Cargo.lock b/Cargo.lock index 4e34f5bb2..f1265da90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,7 +243,7 @@ dependencies = [ "parking_lot", "thiserror", "winapi 0.3.9", - "x11rb", + "x11rb 0.10.1", ] [[package]] @@ -932,11 +932,21 @@ name = "clipboard" version = "0.1.0" dependencies = [ "cc", + "dashmap", + "fuser", "hbb_common", "lazy_static", + "libc", + "once_cell", + "parking_lot", + "percent-encoding", + "rand 0.8.5", "serde 1.0.163", "serde_derive", "thiserror", + "utf16string", + "x11-clipboard", + "x11rb 0.12.0", ] [[package]] @@ -1375,6 +1385,19 @@ dependencies = [ "cc", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if 1.0.0", + "hashbrown 0.14.0", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "dasp" version = "0.11.0" @@ -2221,6 +2244,21 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "fuser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21370f84640642c8ea36dfb2a6bfc4c55941f476fcf431f6fef25a5ddcf0169b" +dependencies = [ + "libc", + "log", + "memchr", + "page_size", + "pkg-config", + "smallvec", + "zerocopy", +] + [[package]] name = "futures" version = "0.3.28" @@ -2431,6 +2469,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "gethostname" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "getrandom" version = "0.2.9" @@ -2851,6 +2899,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "hbb_common" version = "0.1.0" @@ -3131,7 +3185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg 1.1.0", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -3385,9 +3439,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.144" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "libdbus-sys" @@ -3568,9 +3622,9 @@ checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg 1.1.0", "scopeguard", @@ -4150,7 +4204,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" dependencies = [ "dlv-list", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -4198,6 +4252,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "page_size" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b7663cbd190cfd818d08efa8497f6cd383076688c49a391ef7c0d03cd12b561" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "pam" version = "0.7.0" @@ -4288,15 +4352,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.48.0", ] [[package]] @@ -4342,9 +4406,9 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "phf" @@ -5193,9 +5257,11 @@ dependencies = [ "num_cpus", "objc", "objc_id", + "once_cell", "os-version", "pam", "parity-tokio-ipc", + "percent-encoding", "rdev", "repng", "reqwest", @@ -5230,6 +5296,8 @@ dependencies = [ "winreg 0.11.0", "winres", "wol-rs", + "x11-clipboard", + "x11rb 0.12.0", "zip", ] @@ -6221,7 +6289,7 @@ dependencies = [ "futures-io", "futures-sink", "futures-util", - "hashbrown", + "hashbrown 0.12.3", "pin-project-lite", "slab", "tokio", @@ -6510,6 +6578,15 @@ dependencies = [ "log", ] +[[package]] +name = "utf16string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b62a1e85e12d5d712bf47a85f426b73d303e2d00a90de5f3004df3596e9d216" +dependencies = [ + "byteorder", +] + [[package]] name = "utf8parse" version = "0.2.1" @@ -7258,6 +7335,14 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11-clipboard" +version = "0.8.1" +source = "git+https://github.com/clslaid/x11-clipboard?branch=feat/store-batch#5fc2e73bc01ada3681159b34cf3ea8f0d14cd904" +dependencies = [ + "x11rb 0.12.0", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -7275,11 +7360,24 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" dependencies = [ - "gethostname", + "gethostname 0.2.3", "nix 0.24.3", "winapi 0.3.9", "winapi-wsapoll", - "x11rb-protocol", + "x11rb-protocol 0.10.0", +] + +[[package]] +name = "x11rb" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" +dependencies = [ + "gethostname 0.3.0", + "nix 0.26.2", + "winapi 0.3.9", + "winapi-wsapoll", + "x11rb-protocol 0.12.0", ] [[package]] @@ -7291,6 +7389,15 @@ dependencies = [ "nix 0.24.3", ] +[[package]] +name = "x11rb-protocol" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" +dependencies = [ + "nix 0.26.2", +] + [[package]] name = "xdg-home" version = "1.0.0" @@ -7374,6 +7481,27 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b9c234616391070b0b173963ebc65a9195068e7ed3731c6edac2ec45ebe106" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f7f3a471f98d0a61c34322fbbfd10c384b07687f680d4119813713f72308d91" +dependencies = [ + "proc-macro2 1.0.63", + "quote 1.0.27", + "syn 2.0.15", +] + [[package]] name = "zip" version = "0.6.5" diff --git a/Cargo.toml b/Cargo.toml index f6c014483..dbeaf68ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,13 @@ linux_headless = ["pam" ] virtual_display_driver = ["virtual_display"] plugin_framework = [] linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"] +unix-file-copy-paste = [ + "dep:x11-clipboard", + "dep:x11rb", + "dep:percent-encoding", + "dep:once_cell", + "clipboard/unix-file-copy-paste", +] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -132,6 +139,10 @@ dbus = "0.9" dbus-crossroads = "0.5" pam = { git="https://github.com/fufesou/pam", optional = true } users = { version = "0.11" } +x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true} +x11rb = {version = "0.12", features = ["all-extensions"], optional = true} +percent-encoding = {version = "2.3", optional = true} +once_cell = {version = "1.18", optional = true} [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.13" diff --git a/build.py b/build.py index f65b3a4aa..8cff351e2 100755 --- a/build.py +++ b/build.py @@ -126,6 +126,11 @@ def make_parser(): action='store_true', help='Build windows portable' ) + parser.add_argument( + '--unix-file-copy-paste', + action='store_true', + help='Build with unix file copy paste feature' + ) parser.add_argument( '--flatpak', action='store_true', @@ -188,6 +193,7 @@ def download_extract_features(features, res_dir): import re proxy = '' + def req(url): if not proxy: return url @@ -275,6 +281,8 @@ def get_features(args): features.append('flatpak') if args.appimage: features.append('appimage') + if args.unix_file_copy_paste: + features.append('unix-file-copy-paste') print("features:", features) return features @@ -396,7 +404,8 @@ def build_deb_from_folder(version, binary_folder): def build_flutter_dmg(version, features): if not skip_cargo: # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project - system2(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') + system2( + f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') # copy dylib system2( "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") @@ -562,7 +571,8 @@ def main(): codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/* codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app '''.format(pa)) - system2('create-dmg "RustDesk %s.dmg" "target/release/bundle/osx/RustDesk.app"' % version) + system2( + 'create-dmg "RustDesk %s.dmg" "target/release/bundle/osx/RustDesk.app"' % version) os.rename('RustDesk %s.dmg' % version, 'rustdesk-%s.dmg' % version) if pa: diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 28b10785b..0bead716f 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -436,9 +436,9 @@ Future> toolbarDisplayToggle( child: Text(translate('Mute')))); } // file copy and paste - if (Platform.isWindows && - pi.platform == kPeerPlatformWindows && - perms['file'] != false) { + if (perms['file'] != false && + bind.mainHasFileClipboard() && + pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard)) { final option = 'enable-file-transfer'; final value = bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 073edbfec..460894c31 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -22,6 +22,7 @@ const String kPlatformAdditionsIsWayland = "is_wayland"; const String kPlatformAdditionsHeadless = "headless"; const String kPlatformAdditionsIsInstalled = "is_installed"; const String kPlatformAdditionsVirtualDisplays = "virtual_displays"; +const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard"; const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformLinux = "Linux"; diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 1ae62f341..a03a28e75 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -39,7 +39,7 @@ dependencies: package_info_plus: ^3.1.2 url_launcher: ^6.0.9 toggle_switch: ^2.1.0 - dash_chat_2: + dash_chat_2: git: url: https://github.com/rustdesk-org/Dash-Chat-2 draggable_float_widget: ^0.0.2 @@ -68,7 +68,7 @@ dependencies: get: ^4.6.5 visibility_detector: ^0.4.0+2 contextmenu: ^3.0.0 - desktop_drop: + desktop_drop: git: url: https://github.com/rustdesk-org/flutter-plugins path: ./packages/desktop_drop @@ -84,7 +84,7 @@ dependencies: git: url: https://github.com/rustdesk-org/flutter_improved_scrolling uni_links: ^0.5.1 - uni_links_desktop: + uni_links_desktop: git: url: https://github.com/rustdesk-org/uni_links_desktop path: ^1.8.1 diff --git a/libs/clipboard/Cargo.toml b/libs/clipboard/Cargo.toml index bc20ecfc5..182e73002 100644 --- a/libs/clipboard/Cargo.toml +++ b/libs/clipboard/Cargo.toml @@ -2,16 +2,42 @@ name = "clipboard" version = "0.1.0" edition = "2021" -build= "build.rs" +build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] cc = "1.0" +[features] +default = [] +unix-file-copy-paste = [ +"dep:x11rb", +"dep:x11-clipboard", +"dep:rand", +"dep:fuser", +"dep:libc", +"dep:dashmap", +"dep:percent-encoding", +"dep:utf16string", +"dep:once_cell" +] + [dependencies] thiserror = "1.0" lazy_static = "1.4" serde = "1.0" serde_derive = "1.0" hbb_common = { path = "../hbb_common" } +parking_lot = {version = "0.12"} + +[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] +once_cell = {version = "1.18", optional = true} +x11rb = {version = "0.12", features = ["all-extensions"], optional = true} +rand = {version = "0.8", optional = true} +fuser = {version = "0.13", optional = true} +libc = {version = "0.2", optional = true} +dashmap = {version ="5.5", optional = true} +percent-encoding = {version ="2.3", optional = true} +utf16string = {version = "0.2", optional = true} +x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true} diff --git a/libs/clipboard/build.rs b/libs/clipboard/build.rs index 7eb52c75b..d0da933f4 100644 --- a/libs/clipboard/build.rs +++ b/libs/clipboard/build.rs @@ -1,39 +1,37 @@ -use cc; - fn build_c_impl() { + #[cfg(not(target_os = "linux"))] 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"); + #[cfg(not(target_os = "linux"))] + { + 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("-Z7"); - 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()); + if build.get_compiler().is_like_msvc() { + build.define("WIN32", ""); + // build.define("_AMD64_", ""); + build.flag("-Z7"); + 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"); } - 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"); } diff --git a/libs/clipboard/src/X11/xf_cliprdr.c b/libs/clipboard/src/X11/xf_cliprdr.c deleted file mode 100644 index 55d34a088..000000000 --- a/libs/clipboard/src/X11/xf_cliprdr.c +++ /dev/null @@ -1,11 +0,0 @@ -#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.rs b/libs/clipboard/src/cliprdr.rs deleted file mode 100644 index c787d8ef6..000000000 --- a/libs/clipboard/src/cliprdr.rs +++ /dev/null @@ -1,573 +0,0 @@ -#![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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct _NOTIFICATION_MESSAGE { - pub r#type: UINT32, // 0 - info, 1 - warning, 2 - error - pub msg: *const BYTE, - pub details: *const BYTE, -} -pub type NOTIFICATION_MESSAGE = _NOTIFICATION_MESSAGE; -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 pcNotifyClipboardMsg = - ::std::option::Option 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 IsStopped: BOOL, - pub ResponseWaitTimeoutSecs: UINT32, - pub ServerCapabilities: pcCliprdrServerCapabilities, - pub ClientCapabilities: pcCliprdrClientCapabilities, - pub MonitorReady: pcCliprdrMonitorReady, - pub TempDirectory: pcCliprdrTempDirectory, - pub NotifyClipboardMsg: pcNotifyClipboardMsg, - 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; - pub(crate) fn empty_cliprdr(context: *mut CliprdrClientContext, connID: UINT32) -> 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, - response_wait_timeout_secs: u32, - notify_callback: pcNotifyClipboardMsg, - 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 }, - IsStopped: FALSE, - ResponseWaitTimeoutSecs: response_wait_timeout_secs, - ServerCapabilities: None, - ClientCapabilities: None, - MonitorReady: None, - TempDirectory: None, - NotifyClipboardMsg: notify_callback, - 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/context_send.rs b/libs/clipboard/src/context_send.rs index a2053d852..f3606509f 100644 --- a/libs/clipboard/src/context_send.rs +++ b/libs/clipboard/src/context_send.rs @@ -1,68 +1,72 @@ -use crate::cliprdr::*; -use hbb_common::log; +use hbb_common::{log, ResultType}; use std::sync::Mutex; +use crate::CliprdrServiceContext; + const CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS: u32 = 30; lazy_static::lazy_static! { - static ref CONTEXT_SEND: ContextSend = ContextSend{addr: Mutex::new(0)}; + static ref CONTEXT_SEND: ContextSend = ContextSend{addr: Mutex::new(None)}; } pub struct ContextSend { - addr: Mutex, + addr: Mutex>>, } impl ContextSend { #[inline] pub fn is_enabled() -> bool { - *CONTEXT_SEND.addr.lock().unwrap() != 0 + CONTEXT_SEND.addr.lock().unwrap().is_some() } pub fn set_is_stopped() { - let _res = Self::proc(|c| { - c.IsStopped = TRUE; - 0 - }); + let _res = Self::proc(|c| c.set_is_stopped().map_err(|e| e.into())); } pub fn enable(enabled: bool) { let mut lock = CONTEXT_SEND.addr.lock().unwrap(); if enabled { - if *lock == 0 { - match crate::create_cliprdr_context( - true, - false, - CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS, - ) { - Ok(context) => { - log::info!("clipboard context for file transfer created."); - *lock = Box::into_raw(context) as _; - } - Err(err) => { - log::error!( - "Create clipboard context for file transfer: {}", - err.to_string() - ); - } + if lock.is_some() { + return; + } + match crate::create_cliprdr_context(true, false, CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS) { + Ok(context) => { + log::info!("clipboard context for file transfer created."); + *lock = Some(context) + } + Err(err) => { + log::error!( + "create clipboard context for file transfer: {}", + err.to_string() + ); } } - } else { - if *lock != 0 { - unsafe { - let _ = Box::from_raw(*lock as *mut CliprdrClientContext); - } - log::info!("clipboard context for file transfer destroyed."); - *lock = 0; - } + } else if let Some(_clp) = lock.take() { + *lock = None; + log::info!("clipboard context for file transfer destroyed."); } } - pub fn proc u32>(f: F) -> u32 { - let lock = CONTEXT_SEND.addr.lock().unwrap(); - if *lock != 0 { - unsafe { f(&mut *(*lock as *mut CliprdrClientContext)) } - } else { - 0 + /// make sure the clipboard context is enabled. + pub fn make_sure_enabled() -> ResultType<()> { + let mut lock = CONTEXT_SEND.addr.lock().unwrap(); + if lock.is_some() { + return Ok(()); + } + + let ctx = crate::create_cliprdr_context(true, false, CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS)?; + *lock = Some(ctx); + log::info!("clipboard context for file transfer recreated."); + Ok(()) + } + + pub fn proc) -> ResultType<()>>( + f: F, + ) -> ResultType<()> { + let mut lock = CONTEXT_SEND.addr.lock().unwrap(); + match lock.as_mut() { + Some(context) => f(context), + None => Ok(()), } } } diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index 7cba12d1a..0e80cda2d 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -1,4 +1,9 @@ -use cliprdr::*; +#[allow(dead_code)] +use std::{ + path::PathBuf, + sync::{Arc, Mutex, RwLock}, +}; + use hbb_common::{ allow_err, lazy_static, log, tokio::sync::{ @@ -8,19 +13,59 @@ use hbb_common::{ ResultType, }; use serde_derive::{Deserialize, Serialize}; -use std::{ - boxed::Box, - ffi::{CStr, CString}, - sync::{Arc, Mutex, RwLock}, -}; +use thiserror::Error; -pub mod cliprdr; pub mod context_send; +pub mod platform; pub use context_send::*; +#[cfg(target_os = "windows")] const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001; +#[cfg(target_os = "windows")] const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002; +pub(crate) use platform::create_cliprdr_context; + +/// Ability to handle Clipboard File from remote rustdesk client +/// +/// # Note +/// There actually should be 2 parts to implement a useable clipboard file service, +/// but this only contains the RPC server part. +/// The local listener and transport part is too platform specific to wrap up in typeclasses. +pub trait CliprdrServiceContext: Send + Sync { + /// set to be stopped + fn set_is_stopped(&mut self) -> Result<(), CliprdrError>; + /// clear the content on clipboard + fn empty_clipboard(&mut self, conn_id: i32) -> Result; + + /// run as a server for clipboard RPC + fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError>; +} + +#[derive(Error, Debug)] +pub enum CliprdrError { + #[error("invalid cliprdr name")] + CliprdrName, + #[error("failed to init cliprdr")] + CliprdrInit, + #[error("cliprdr out of memory")] + CliprdrOutOfMemory, + #[error("cliprdr internal error")] + ClipboardInternalError, + #[error("cliprdr occupied")] + ClipboardOccupied, + #[error("conversion failure")] + ConversionFailure, + #[error("failure to read clipboard")] + OpenClipboard, + #[error("failure to read file metadata or content")] + FileError { path: PathBuf, err: std::io::Error }, + #[error("invalid request")] + InvalidRequest { description: String }, + #[error("unknown cliprdr error")] + Unknown(u32), +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] pub enum ClipboardFile { @@ -63,6 +108,7 @@ pub enum ClipboardFile { struct MsgChannel { peer_id: String, conn_id: i32, + #[allow(dead_code)] sender: UnboundedSender, receiver: Arc>>, } @@ -74,19 +120,19 @@ lazy_static::lazy_static! { impl ClipboardFile { pub fn is_stopping_allowed(&self) -> bool { - match self { + matches!( + self, ClipboardFile::MonitorReady - | ClipboardFile::FormatList { .. } - | ClipboardFile::FormatDataRequest { .. } => true, - _ => false, - } + | ClipboardFile::FormatList { .. } + | ClipboardFile::FormatDataRequest { .. } + ) } pub fn is_stopping_allowed_from_peer(&self) -> bool { - match self { - ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. } => true, - _ => false, - } + matches!( + self, + ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. } + ) } } @@ -148,8 +194,21 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc bool { - unsafe { TRUE == cliprdr::empty_cliprdr(context, conn_id as u32) } -} - -pub fn server_clip_file( - context: &mut CliprdrClientContext, - conn_id: i32, - msg: ClipboardFile, -) -> u32 { - let mut ret = 0; - match msg { - ClipboardFile::NotifyCallback { .. } => { - // unreachable - } - ClipboardFile::MonitorReady => { - log::debug!("server_monitor_ready called"); - ret = server_monitor_ready(context, conn_id); - log::debug!( - "server_monitor_ready called, conn_id {}, return {}", - conn_id, - ret - ); - } - ClipboardFile::FormatList { format_list } => { - log::debug!( - "server_format_list called, conn_id {}, format_list: {:?}", - conn_id, - &format_list - ); - ret = server_format_list(context, conn_id, format_list); - log::debug!( - "server_format_list called, conn_id {}, return {}", - conn_id, - ret - ); - } - ClipboardFile::FormatListResponse { msg_flags } => { - log::debug!("server_format_list_response called"); - ret = server_format_list_response(context, conn_id, msg_flags); - log::debug!( - "server_format_list_response called, conn_id {}, msg_flags {}, return {}", - conn_id, - msg_flags, - ret - ); - } - ClipboardFile::FormatDataRequest { - requested_format_id, - } => { - log::debug!("server_format_data_request called"); - ret = server_format_data_request(context, conn_id, requested_format_id); - log::debug!( - "server_format_data_request called, conn_id {}, requested_format_id {}, return {}", - conn_id, - requested_format_id, - ret - ); - } - ClipboardFile::FormatDataResponse { - msg_flags, - format_data, - } => { - log::debug!("server_format_data_response called"); - ret = server_format_data_response(context, conn_id, msg_flags, format_data); - log::debug!( - "server_format_data_response called, conn_id {}, msg_flags: {}, return {}", - conn_id, - msg_flags, - ret - ); - } - ClipboardFile::FileContentsRequest { - stream_id, - list_index, - dw_flags, - n_position_low, - n_position_high, - cb_requested, - have_clip_data_id, - clip_data_id, - } => { - log::debug!("server_file_contents_request called"); - ret = server_file_contents_request( - context, - conn_id, - stream_id, - list_index, - dw_flags, - n_position_low, - n_position_high, - cb_requested, - have_clip_data_id, - clip_data_id, - ); - log::debug!("server_file_contents_request called, conn_id {}, stream_id: {}, list_index {}, dw_flags {}, n_position_low {}, n_position_high {}, cb_requested {}, have_clip_data_id {}, clip_data_id {}, return {}", conn_id, - stream_id, - list_index, - dw_flags, - n_position_low, - n_position_high, - cb_requested, - have_clip_data_id, - clip_data_id, - ret - ); - } - ClipboardFile::FileContentsResponse { - msg_flags, - stream_id, - requested_data, - } => { - log::debug!("server_file_contents_response called"); - ret = server_file_contents_response( - context, - conn_id, - msg_flags, - stream_id, - requested_data, - ); - log::debug!("server_file_contents_response called, conn_id {}, msg_flags {}, stream_id {}, return {}", - conn_id, - msg_flags, - stream_id, - ret - ); - } - } - ret -} - -pub fn server_monitor_ready(context: &mut CliprdrClientContext, conn_id: i32) -> u32 { - unsafe { - let monitor_ready = CLIPRDR_MONITOR_READY { - connID: conn_id as UINT32, - msgType: 0 as UINT16, - msgFlags: 0 as UINT16, - dataLen: 0 as UINT32, - }; - if let Some(f) = context.MonitorReady { - let ret = f(context, &monitor_ready); - ret as u32 - } else { - ERR_CODE_SERVER_FUNCTION_NONE - } - } -} - -pub fn server_format_list( - context: &mut CliprdrClientContext, - conn_id: i32, - format_list: Vec<(i32, String)>, -) -> u32 { - unsafe { - let num_formats = format_list.len() as UINT32; - let mut formats = format_list - .into_iter() - .map(|format| { - if format.1.is_empty() { - CLIPRDR_FORMAT { - formatId: format.0 as UINT32, - formatName: 0 as *mut _, - } - } else { - let n = match CString::new(format.1) { - Ok(n) => n, - Err(_) => CString::new("").unwrap(), - }; - CLIPRDR_FORMAT { - formatId: format.0 as UINT32, - formatName: n.into_raw(), - } - } - }) - .collect::>(); - - let format_list = CLIPRDR_FORMAT_LIST { - connID: conn_id as UINT32, - msgType: 0 as UINT16, - msgFlags: 0 as UINT16, - dataLen: 0 as UINT32, - numFormats: num_formats, - formats: formats.as_mut_ptr(), - }; - - let ret = if let Some(f) = context.ServerFormatList { - f(context, &format_list) - } else { - ERR_CODE_SERVER_FUNCTION_NONE - }; - - for f in formats { - if !f.formatName.is_null() { - // retake pointer to free memory - let _ = CString::from_raw(f.formatName); - } - } - - ret as u32 - } -} - -pub fn server_format_list_response( - context: &mut CliprdrClientContext, - conn_id: i32, - msg_flags: i32, -) -> u32 { - unsafe { - let format_list_response = CLIPRDR_FORMAT_LIST_RESPONSE { - connID: conn_id as UINT32, - msgType: 0 as UINT16, - msgFlags: msg_flags as UINT16, - dataLen: 0 as UINT32, - }; - - if let Some(f) = context.ServerFormatListResponse { - f(context, &format_list_response) - } else { - ERR_CODE_SERVER_FUNCTION_NONE - } - } -} - -pub fn server_format_data_request( - context: &mut CliprdrClientContext, - conn_id: i32, - requested_format_id: i32, -) -> u32 { - unsafe { - let format_data_request = CLIPRDR_FORMAT_DATA_REQUEST { - connID: conn_id as UINT32, - msgType: 0 as UINT16, - msgFlags: 0 as UINT16, - dataLen: 0 as UINT32, - requestedFormatId: requested_format_id as UINT32, - }; - if let Some(f) = context.ServerFormatDataRequest { - f(context, &format_data_request) - } else { - ERR_CODE_SERVER_FUNCTION_NONE - } - } -} - -pub fn server_format_data_response( - context: &mut CliprdrClientContext, - conn_id: i32, - msg_flags: i32, - mut format_data: Vec, -) -> u32 { - unsafe { - let format_data_response = CLIPRDR_FORMAT_DATA_RESPONSE { - connID: conn_id as UINT32, - msgType: 0 as UINT16, - msgFlags: msg_flags as UINT16, - dataLen: format_data.len() as UINT32, - requestedFormatData: format_data.as_mut_ptr(), - }; - if let Some(f) = context.ServerFormatDataResponse { - f(context, &format_data_response) - } else { - ERR_CODE_SERVER_FUNCTION_NONE - } - } -} - -pub fn server_file_contents_request( - context: &mut CliprdrClientContext, - conn_id: i32, - stream_id: i32, - list_index: i32, - dw_flags: i32, - n_position_low: i32, - n_position_high: i32, - cb_requested: i32, - have_clip_data_id: bool, - clip_data_id: i32, -) -> u32 { - unsafe { - let file_contents_request = CLIPRDR_FILE_CONTENTS_REQUEST { - connID: conn_id as UINT32, - msgType: 0 as UINT16, - msgFlags: 0 as UINT16, - dataLen: 0 as UINT32, - streamId: stream_id as UINT32, - listIndex: list_index as UINT32, - dwFlags: dw_flags as UINT32, - nPositionLow: n_position_low as UINT32, - nPositionHigh: n_position_high as UINT32, - cbRequested: cb_requested as UINT32, - haveClipDataId: if have_clip_data_id { TRUE } else { FALSE }, - clipDataId: clip_data_id as UINT32, - }; - if let Some(f) = context.ServerFileContentsRequest { - f(context, &file_contents_request) - } else { - ERR_CODE_SERVER_FUNCTION_NONE - } - } -} - -pub fn server_file_contents_response( - context: &mut CliprdrClientContext, - conn_id: i32, - msg_flags: i32, - stream_id: i32, - mut requested_data: Vec, -) -> u32 { - unsafe { - let file_contents_response = CLIPRDR_FILE_CONTENTS_RESPONSE { - connID: conn_id as UINT32, - msgType: 0 as UINT16, - msgFlags: msg_flags as UINT16, - dataLen: 4 + requested_data.len() as UINT32, - streamId: stream_id as UINT32, - cbRequested: requested_data.len() as UINT32, - requestedData: requested_data.as_mut_ptr(), - }; - if let Some(f) = context.ServerFileContentsResponse { - f(context, &file_contents_response) - } else { - ERR_CODE_SERVER_FUNCTION_NONE - } - } -} - -pub fn create_cliprdr_context( - enable_files: bool, - enable_others: bool, - response_wait_timeout_secs: u32, -) -> ResultType> { - Ok(CliprdrClientContext::create( - enable_files, - enable_others, - response_wait_timeout_secs, - Some(notify_callback), - 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 notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE) -> UINT { - log::debug!("notify_callback called"); - let data = unsafe { - let msg = &*msg; - let details = if msg.details.is_null() { - Ok("") - } else { - CStr::from_ptr(msg.details as _).to_str() - }; - match (CStr::from_ptr(msg.msg as _).to_str(), details) { - (Ok(m), Ok(d)) => { - let msgtype = format!( - "custom-{}-nocancel-nook-hasclose", - if msg.r#type == 0 { - "info" - } else if msg.r#type == 1 { - "warn" - } else { - "error" - } - ); - let title = "Clipboard"; - let text = if d.is_empty() { - m.to_string() - } else { - format!("{} {}", m, d) - }; - ClipboardFile::NotifyCallback { - r#type: msgtype, - title: title.to_string(), - text, - } - } - _ => { - log::error!("notify_callback: failed to convert msg"); - return ERR_CODE_INVALID_PARAMETER; - } - } - }; +#[cfg(feature = "unix-file-copy-paste")] +#[inline] +fn send_data_to_all(data: ClipboardFile) { // no need to handle result here - send_data(conn_id as _, data); - - 0 -} - -extern "C" fn client_format_list( - _context: *mut CliprdrClientContext, - clip_format_list: *const CLIPRDR_FORMAT_LIST, -) -> UINT { - let conn_id; - let mut format_list: Vec<(i32, String)> = Vec::new(); - unsafe { - let mut i = 0u32; - while i < (*clip_format_list).numFormats { - let format_data = &(*(*clip_format_list).formats.offset(i as isize)); - if format_data.formatName.is_null() { - format_list.push((format_data.formatId as i32, "".to_owned())); - } 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() - } - }; - format_list.push((format_data.formatId as i32, format_name)); - } - // log::debug!("format list item {}: format id: {}, format name: {}", i, format_data.formatId, &format_name); - i += 1; - } - conn_id = (*clip_format_list).connID as i32; + for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() { + allow_err!(msg_channel.sender.send(data.clone())); } - log::debug!( - "client_format_list called, client id: {}, format_list: {:?}", - conn_id, - &format_list - ); - let data = ClipboardFile::FormatList { format_list }; - // no need to handle result here - if conn_id == 0 { - // msg_channel is used for debug, VEC_MSG_CHANNEL cannot be inspected by the debugger. - let msg_channel = VEC_MSG_CHANNEL.read().unwrap(); - msg_channel - .iter() - .for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone()))); - } else { - send_data(conn_id, data); - } - - 0 -} - -extern "C" fn client_format_list_response( - _context: *mut CliprdrClientContext, - format_list_response: *const CLIPRDR_FORMAT_LIST_RESPONSE, -) -> UINT { - let conn_id; - let msg_flags; - unsafe { - conn_id = (*format_list_response).connID as i32; - msg_flags = (*format_list_response).msgFlags as i32; - } - log::debug!( - "client_format_list_response called, client id: {}, msg_flags: {}", - conn_id, - msg_flags - ); - let data = ClipboardFile::FormatListResponse { msg_flags }; - send_data(conn_id, data); - - 0 -} - -extern "C" fn client_format_data_request( - _context: *mut CliprdrClientContext, - format_data_request: *const CLIPRDR_FORMAT_DATA_REQUEST, -) -> UINT { - let conn_id; - let requested_format_id; - unsafe { - conn_id = (*format_data_request).connID as i32; - requested_format_id = (*format_data_request).requestedFormatId as i32; - } - let data = ClipboardFile::FormatDataRequest { - requested_format_id, - }; - log::debug!( - "client_format_data_request called, conn_id: {}, requested_format_id: {}", - conn_id, - requested_format_id - ); - // no need to handle result here - send_data(conn_id, data); - - 0 -} - -extern "C" fn client_format_data_response( - _context: *mut CliprdrClientContext, - format_data_response: *const CLIPRDR_FORMAT_DATA_RESPONSE, -) -> UINT { - let conn_id; - let msg_flags; - let format_data; - unsafe { - conn_id = (*format_data_response).connID as i32; - msg_flags = (*format_data_response).msgFlags as i32; - if (*format_data_response).requestedFormatData.is_null() { - format_data = Vec::new(); - } else { - format_data = std::slice::from_raw_parts( - (*format_data_response).requestedFormatData, - (*format_data_response).dataLen as usize, - ) - .to_vec(); - } - } - log::debug!( - "client_format_data_response called, client id: {}, msg_flags: {}", - conn_id, - msg_flags - ); - let data = ClipboardFile::FormatDataResponse { - msg_flags, - format_data, - }; - send_data(conn_id, data); - - 0 -} - -extern "C" fn client_file_contents_request( - _context: *mut CliprdrClientContext, - file_contents_request: *const CLIPRDR_FILE_CONTENTS_REQUEST, -) -> UINT { - // 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 conn_id; - let stream_id; - let list_index; - let dw_flags; - let n_position_low; - let n_position_high; - let cb_requested; - let have_clip_data_id; - let clip_data_id; - unsafe { - conn_id = (*file_contents_request).connID as i32; - stream_id = (*file_contents_request).streamId as i32; - list_index = (*file_contents_request).listIndex as i32; - dw_flags = (*file_contents_request).dwFlags as i32; - n_position_low = (*file_contents_request).nPositionLow as i32; - n_position_high = (*file_contents_request).nPositionHigh as i32; - cb_requested = (*file_contents_request).cbRequested as i32; - have_clip_data_id = (*file_contents_request).haveClipDataId == TRUE; - clip_data_id = (*file_contents_request).clipDataId as i32; - } - let data = ClipboardFile::FileContentsRequest { - stream_id, - list_index, - dw_flags, - n_position_low, - n_position_high, - cb_requested, - have_clip_data_id, - clip_data_id, - }; - log::debug!("client_file_contents_request called, data: {:?}", &data); - send_data(conn_id, data); - - 0 -} - -extern "C" fn client_file_contents_response( - _context: *mut CliprdrClientContext, - file_contents_response: *const CLIPRDR_FILE_CONTENTS_RESPONSE, -) -> UINT { - let conn_id; - let msg_flags; - let stream_id; - let requested_data; - unsafe { - conn_id = (*file_contents_response).connID as i32; - msg_flags = (*file_contents_response).msgFlags as i32; - stream_id = (*file_contents_response).streamId as i32; - if (*file_contents_response).requestedData.is_null() { - requested_data = Vec::new(); - } else { - requested_data = std::slice::from_raw_parts( - (*file_contents_response).requestedData, - (*file_contents_response).cbRequested as usize, - ) - .to_vec(); - } - } - let data = ClipboardFile::FileContentsResponse { - msg_flags, - stream_id, - requested_data, - }; - log::debug!( - "client_file_contents_response called, conn_id: {}, msg_flags: {}, stream_id: {}", - conn_id, - msg_flags, - stream_id - ); - send_data(conn_id, data); - - 0 } #[cfg(test)] diff --git a/libs/clipboard/src/platform/fuse.rs b/libs/clipboard/src/platform/fuse.rs new file mode 100644 index 000000000..6c5bbcdd2 --- /dev/null +++ b/libs/clipboard/src/platform/fuse.rs @@ -0,0 +1,1168 @@ +//! fuse server implement +//! we use fuse to provide file readers, warping data transfer to file interfaces +//! +//! # Name encoding +//! +//! There are different collection of characters forbidden in file names on different platforms: +//! - windows: `\/:*?"<>|` +//! - macos: `:/` +//! - linux: `/` +//! +//! what makes it troublesome is windows also used '\' as path separator. +//! +//! For now, we transfer all file names with windows separators, UTF-16 encoded. +//! *Need a way to transfer file names with '\' safely*. +//! Maybe we can use URL encoded file names and '/' seperators as a new standard, while keep the support to old schemes. +//! +//! # Note +//! - all files on FS should be read only, and mark the owner to be the current user +//! - any write operations, hard links, and symbolic links on the FS should be denied + +use std::{ + collections::{BTreeMap, HashMap}, + ffi::OsString, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicBool, AtomicU64, Ordering}, + mpsc::{Receiver, Sender}, + Arc, + }, + time::{Duration, SystemTime}, +}; + +use fuser::{ReplyDirectory, FUSE_ROOT_ID}; +use hbb_common::{ + bytes::{Buf, Bytes}, + log, +}; +use parking_lot::{Condvar, Mutex}; +use utf16string::WStr; + +use crate::{send_data, ClipboardFile, CliprdrError}; + +use super::LDAP_EPOCH_DELTA; + +/// fuse server ready retry max times +const READ_RETRY: i32 = 3; + +/// block size for fuse, align to our asynchronic request size over FileContentsRequest. +pub const BLOCK_SIZE: u32 = 4 * 1024 * 1024; + +/// read only permission +const PERM_READ: u16 = 0o444; +/// read and write permission +const PERM_RW: u16 = 0o644; +/// only self can read and readonly +const PERM_SELF_RO: u16 = 0o400; +/// rwx +const PERM_RWX: u16 = 0o755; +/// max length of file name +const MAX_NAME_LEN: usize = 255; + +/// fuse client +/// this is a proxy to the fuse server +#[derive(Debug)] +pub struct FuseClient { + server: Arc>, +} + +impl fuser::Filesystem for FuseClient { + fn init( + &mut self, + req: &fuser::Request<'_>, + config: &mut fuser::KernelConfig, + ) -> Result<(), libc::c_int> { + let mut server = self.server.lock(); + server.init(req, config) + } + + fn lookup( + &mut self, + req: &fuser::Request<'_>, + parent: u64, + name: &std::ffi::OsStr, + reply: fuser::ReplyEntry, + ) { + let mut server = self.server.lock(); + server.lookup(req, parent, name, reply) + } + + fn opendir(&mut self, req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) { + let mut server = self.server.lock(); + server.opendir(req, ino, flags, reply) + } + + fn readdir( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + reply: fuser::ReplyDirectory, + ) { + let mut server = self.server.lock(); + server.readdir(req, ino, fh, offset, reply) + } + + fn releasedir( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + _flags: i32, + reply: fuser::ReplyEmpty, + ) { + let mut server = self.server.lock(); + server.releasedir(req, ino, fh, _flags, reply) + } + + fn open(&mut self, req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) { + let mut server = self.server.lock(); + server.open(req, ino, flags, reply) + } + + fn read( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + size: u32, + flags: i32, + lock_owner: Option, + reply: fuser::ReplyData, + ) { + let mut server = self.server.lock(); + server.read(req, ino, fh, offset, size, flags, lock_owner, reply) + } + + fn release( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + _flags: i32, + _lock_owner: Option, + _flush: bool, + reply: fuser::ReplyEmpty, + ) { + let mut server = self.server.lock(); + server.release(req, ino, fh, _flags, _lock_owner, _flush, reply) + } + + fn getattr(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) { + let mut server = self.server.lock(); + server.getattr(req, ino, reply) + } +} + +/// fuse server +/// provides a read-only file system +#[derive(Debug)] +pub(crate) struct FuseServer { + generation: AtomicU64, + files: Vec, + // file handle counter + file_handle_counter: AtomicU64, + // timeout + timeout: Duration, + // file read reply channel + rx: Receiver, +} + +impl FuseServer { + /// create a new fuse server + pub fn new(timeout: Duration) -> (Self, Sender) { + let (tx, rx) = std::sync::mpsc::channel(); + ( + Self { + generation: AtomicU64::new(0), + files: Vec::new(), + file_handle_counter: AtomicU64::new(0), + timeout, + rx, + }, + tx, + ) + } + + pub fn client(server: Arc>) -> FuseClient { + FuseClient { server } + } +} + +impl FuseServer { + pub fn load_file_list(&mut self, files: Vec) -> Result<(), CliprdrError> { + let tree = FuseNode::build_tree(files)?; + self.files = tree; + self.generation.fetch_add(1, Ordering::Relaxed); + Ok(()) + } +} + +impl fuser::Filesystem for FuseServer { + fn init( + &mut self, + _req: &fuser::Request<'_>, + _config: &mut fuser::KernelConfig, + ) -> Result<(), libc::c_int> { + if self.files.is_empty() { + // create a root file + let root = FuseNode::new_root(); + self.files.push(root); + } + Ok(()) + } + + fn lookup( + &mut self, + _req: &fuser::Request<'_>, + parent: u64, + name: &std::ffi::OsStr, + reply: fuser::ReplyEntry, + ) { + if name.len() > MAX_NAME_LEN { + log::debug!("fuse: name too long"); + reply.error(libc::ENAMETOOLONG); + return; + } + + let entries = &self.files; + + let generation = self.generation.load(Ordering::Relaxed); + + let parent_entry = match entries.get(parent as usize - 1) { + Some(f) => f, + None => { + log::error!("fuse: parent not found"); + reply.error(libc::ENOENT); + return; + } + }; + + if parent_entry.attributes.kind != FileType::Directory { + log::error!("fuse: parent is not a directory"); + + reply.error(libc::ENOTDIR); + return; + } + + let children_inodes = &parent_entry.children; + + for inode in children_inodes.iter().copied() { + let child = &entries[inode as usize - 1]; + let entry_name = OsString::from(&child.name); + + if &entry_name.as_os_str() == &name { + let ttl = std::time::Duration::new(0, 0); + reply.entry(&ttl, &(&child.attributes).into(), generation); + log::debug!("fuse: found child"); + return; + } + } + // error + reply.error(libc::ENOENT); + log::debug!("fuse: child not found"); + } + + fn opendir( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + _flags: i32, + reply: fuser::ReplyOpen, + ) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: opendir: entry not found"); + return; + }; + if entry.attributes.kind != FileType::Directory { + reply.error(libc::ENOTDIR); + log::error!("fuse: opendir: entry is not a directory"); + return; + } + // in gc, deny open + if entry.marked() { + log::error!("fuse: opendir: entry is in gc"); + reply.error(libc::EBUSY); + return; + } + + let fh = self.alloc_fd(); + entry.add_handler(fh); + reply.opened(fh, 0); + } + + fn readdir( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + mut reply: ReplyDirectory, + ) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: readdir: entry not found"); + return; + }; + if !entry.have_handler(fh) { + reply.error(libc::EBADF); + log::error!("fuse: readdir: entry has no such handler"); + return; + } + if entry.attributes.kind != FileType::Directory { + reply.error(libc::ENOTDIR); + log::error!("fuse: readdir: entry is not a directory"); + return; + } + + let offset = offset as usize; + let mut entries = Vec::new(); + + let self_entry = (ino, FileType::Directory, OsString::from(".")); + entries.push(self_entry); + + if let Some(parent_inode) = entry.parent { + entries.push((parent_inode, FileType::Directory, OsString::from(".."))); + } + + for inode in entry.children.iter().copied() { + let child = &files[inode as usize - 1]; + let kind = child.attributes.kind; + let name = OsString::from(&child.name); + let child_entry = (inode, kind, name.to_owned()); + entries.push(child_entry); + } + + for (i, entry) in entries.into_iter().enumerate().skip(offset) { + if reply.add(entry.0, i as i64 + 1, entry.1.into(), entry.2) { + break; + } + } + + reply.ok(); + } + + fn releasedir( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + fh: u64, + _flags: i32, + reply: fuser::ReplyEmpty, + ) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: releasedir: entry not found"); + return; + }; + if entry.attributes.kind != FileType::Directory { + reply.error(libc::ENOTDIR); + log::error!("fuse: releasedir: entry is not a directory"); + return; + } + if !entry.have_handler(fh) { + reply.error(libc::EBADF); + log::error!("fuse: releasedir: entry has no such handler"); + return; + } + + let _ = entry.unregister_handler(fh); + reply.ok(); + } + + fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, _flags: i32, reply: fuser::ReplyOpen) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: open: entry not found"); + return; + }; + + // todo: support link file + if entry.attributes.kind != FileType::File { + reply.error(libc::ENFILE); + log::error!("fuse: open: entry is not a file"); + return; + } + + // check gc + if entry.marked() { + reply.error(libc::EBUSY); + log::error!("fuse: open: entry is in gc"); + return; + } + + let fh = self.alloc_fd(); + entry.add_handler(fh); + reply.opened(fh, 0); + } + + // todo: implement retry + fn read( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + size: u32, + _flags: i32, + _lock_owner: Option, + reply: fuser::ReplyData, + ) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: read: entry not found"); + return; + }; + if !entry.have_handler(fh) { + reply.error(libc::EBADF); + log::error!("fuse: read: entry has no such handler"); + return; + } + if entry.attributes.kind != FileType::File { + reply.error(libc::ENFILE); + log::error!("fuse: read: entry is not a file"); + return; + } + + if entry.marked() { + reply.error(libc::EBUSY); + log::error!("fuse: read: entry is in gc"); + return; + } + + let bytes = match self.read_node(entry, offset, size) { + Ok(b) => b, + Err(e) => { + log::error!("failed to read entry: {:?}", e); + reply.error(libc::EIO); + return; + } + }; + + reply.data(bytes.as_slice()); + } + + fn release( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + fh: u64, + _flags: i32, + _lock_owner: Option, + _flush: bool, + reply: fuser::ReplyEmpty, + ) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: release: entry not found"); + return; + }; + + if entry.unregister_handler(fh).is_err() { + reply.error(libc::EBADF); + log::error!("fuse: release: entry has no such handler"); + return; + } + reply.ok(); + } + + fn getattr(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: getattr: entry not found"); + return; + }; + + let attr = (&entry.attributes).into(); + reply.attr(&std::time::Duration::default(), &attr) + } +} + +impl FuseServer { + // get files and directory path right in root of FUSE fs + pub fn list_root(&self) -> Vec { + let files = &self.files; + let children = &files[0].children; + let mut paths = Vec::with_capacity(children.len()); + for inode in children.iter().copied() { + let idx = inode as usize - 1; + paths.push(PathBuf::from(&files[idx].name)); + } + paths + } + + /// allocate a new file descriptor + fn alloc_fd(&self) -> u64 { + self.file_handle_counter.fetch_add(1, Ordering::Relaxed) + } + + fn read_node( + &self, + node: &FuseNode, + offset: i64, + size: u32, + ) -> Result, std::io::Error> { + // todo: async and concurrent read, generate stream_id per request + log::debug!( + "reading {:?} offset {} size {} on stream: {}", + node.name, + offset, + size, + node.stream_id + ); + + let cb_requested = unsafe { + // convert `size` from u32 to i32 + // yet with same bit representation + std::mem::transmute::(size) + }; + + let (n_position_high, n_position_low) = + ((offset >> 32) as i32, (offset & (u32::MAX as i64)) as i32); + let request = ClipboardFile::FileContentsRequest { + stream_id: node.stream_id, + list_index: node.index as i32, + dw_flags: 2, + n_position_low, + n_position_high, + cb_requested, + have_clip_data_id: false, + clip_data_id: 0, + }; + + send_data(node.conn_id, request.clone()); + + log::debug!( + "waiting for read reply for {:?} on stream: {}", + node.name, + node.stream_id + ); + + let mut retry_times = 0; + + loop { + let reply = self.rx.recv_timeout(self.timeout).map_err(|e| { + log::error!("failed to receive file list from channel: {:?}", e); + std::io::Error::new(std::io::ErrorKind::TimedOut, e) + })?; + + match reply { + ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + requested_data, + } => { + if stream_id != node.stream_id { + log::debug!("stream id mismatch, ignore"); + continue; + } + + if msg_flags & 1 == 0 { + retry_times += 1; + if retry_times > READ_RETRY { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "failure request", + )); + } + + send_data(node.conn_id, request.clone()); + continue; + } + return Ok(requested_data); + } + _ => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "invalid reply", + )) + } + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileDescription { + pub conn_id: i32, + pub name: PathBuf, + pub kind: FileType, + pub atime: SystemTime, + pub last_modified: SystemTime, + pub last_metadata_changed: SystemTime, + pub creation_time: SystemTime, + + pub size: u64, + + pub perm: u16, +} + +impl FileDescription { + fn parse_file_descriptor( + bytes: &mut Bytes, + conn_id: i32, + ) -> Result { + let flags = bytes.get_u32_le(); + // skip reserved 32 bytes + bytes.advance(32); + let attributes = bytes.get_u32_le(); + + // in original specification, this is 16 bytes reserved + // we use the last 4 bytes to store the file mode + // skip reserved 12 bytes + bytes.advance(12); + let perm = bytes.get_u32_le() as u16; + + // last write time from 1601-01-01 00:00:00, in 100ns + let last_write_time = bytes.get_u64_le(); + // file size + let file_size_high = bytes.get_u32_le(); + let file_size_low = bytes.get_u32_le(); + // utf16 file name, double \0 terminated, in 520 bytes block + // read with another pointer, and advance the main pointer + let block = bytes.clone(); + bytes.advance(520); + + let block = &block[..520]; + let wstr = WStr::from_utf16le(block).map_err(|e| { + log::error!("cannot convert file descriptor path: {:?}", e); + CliprdrError::ConversionFailure + })?; + + let from_unix = flags & 0x08 != 0; + + let valid_attributes = flags & 0x04 != 0; + if !valid_attributes { + return Err(CliprdrError::InvalidRequest { + description: "file description must have valid attributes".to_string(), + }); + } + + // todo: check normal, hidden, system, readonly, archive... + let directory = attributes & 0x10 != 0; + let normal = attributes == 0x80; + let hidden = attributes & 0x02 != 0; + let readonly = attributes & 0x01 != 0; + + let perm = if from_unix { + // as is + perm + // cannot set as is... + } else if normal { + PERM_RWX + } else if readonly { + PERM_READ + } else if hidden { + PERM_SELF_RO + } else if directory { + PERM_RWX + } else { + PERM_RW + }; + + let kind = if directory { + FileType::Directory + } else { + FileType::File + }; + + let valid_size = flags & 0x40 != 0; + let size = if valid_size { + ((file_size_high as u64) << 32) + file_size_low as u64 + } else { + 0 + }; + + let valid_write_time = flags & 0x20 != 0; + let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA { + let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100; + let last_write_time = Duration::from_nanos(last_write_time); + SystemTime::UNIX_EPOCH + last_write_time + } else { + SystemTime::UNIX_EPOCH + }; + + let name = wstr.to_utf8().replace('\\', "/"); + let name = PathBuf::from(name.trim_end_matches('\0')); + + let desc = FileDescription { + conn_id, + name, + kind, + atime: last_modified, + last_modified, + last_metadata_changed: last_modified, + + creation_time: last_modified, + size, + perm, + }; + + Ok(desc) + } + + /// parse file descriptions from a format data response PDU + /// which containing a CSPTR_FILEDESCRIPTORW indicated format data + pub fn parse_file_descriptors( + file_descriptor_pdu: Vec, + conn_id: i32, + ) -> Result, CliprdrError> { + let mut data = Bytes::from(file_descriptor_pdu); + if data.remaining() < 4 { + return Err(CliprdrError::InvalidRequest { + description: "file descriptor request with infficient length".to_string(), + }); + } + + let count = data.get_u32_le() as usize; + if data.remaining() == 0 && count == 0 { + return Ok(Vec::new()); + } + + if data.remaining() != 592 * count { + return Err(CliprdrError::InvalidRequest { + description: "file descriptor request with invalid length".to_string(), + }); + } + + let mut files = Vec::with_capacity(count); + for _ in 0..count { + let desc = Self::parse_file_descriptor(&mut data, conn_id)?; + files.push(desc); + } + + Ok(files) + } +} + +/// a node in the FUSE file tree +#[derive(Debug)] +struct FuseNode { + /// connection id + pub conn_id: i32, + + // todo: use stream_id to identify a FileContents request-reply + // instead of a whole file + /// stream id + pub stream_id: i32, + + /// file index in peer's file list + /// NOTE: + /// it is NOT the same as inode, this is the index in the file list + pub index: usize, + + /// parent inode + pub parent: Option, + + /// file name + pub name: String, + /// file attributes + pub attributes: InodeAttributes, + /// children inodes + pub children: Vec, + + /// marked gc + pub file_handlers: FileHandles, +} + +impl FuseNode { + pub fn from_description(inode: Inode, desc: FileDescription) -> Self { + Self { + conn_id: desc.conn_id, + stream_id: rand::random(), + index: inode as usize - 2, + name: desc.name.to_str().unwrap().to_owned(), + parent: None, + attributes: InodeAttributes::from_description(inode, desc), + children: Vec::new(), + file_handlers: FileHandles::new(), + } + } + + pub fn new_root() -> Self { + Self { + conn_id: 0, + stream_id: rand::random(), + index: 0, + name: String::from("/"), + parent: None, + attributes: InodeAttributes::new_root(), + children: Vec::new(), + file_handlers: FileHandles::new(), + } + } + + #[allow(unused)] + pub fn is_file(&self) -> bool { + self.attributes.kind == FileType::File + } + + pub fn marked(&self) -> bool { + self.file_handlers.marked() + } + + pub fn add_handler(&self, fh: u64) { + self.file_handlers.add_handler(fh) + } + + pub fn unregister_handler(&self, fh: u64) -> Result<(), std::io::Error> { + self.file_handlers.unregister(fh) + } + + pub fn have_handler(&self, fh: u64) -> bool { + self.file_handlers.have_handler(fh) + } + + /// add a child inode + fn add_child(&mut self, inode: Inode) { + self.children.push(inode); + } + + /// calculate the file tree from a pre-ordered file list + /// ## implement detail: + /// - a new root entry will be prepended to the list + /// - all file names will be trimed to the last component + pub fn build_tree(files: Vec) -> Result, CliprdrError> { + // capacity set to file count + 1 (root) + let mut tree_list = Vec::with_capacity(files.len() + 1); + let root = Self::new_root(); + tree_list.push(root); + + // build the tree first + // root map, name -> inode + let mut sub_root_map = HashMap::new(); + sub_root_map.insert(Path::new("/").to_path_buf(), FUSE_ROOT_ID); + sub_root_map.insert(Path::new("").to_path_buf(), FUSE_ROOT_ID); + + for file in files.into_iter() { + let name = file.name.clone(); + + let inode = tree_list.len() as u64 + FUSE_ROOT_ID; + let parent_inode = match name.parent() { + Some(parent) => sub_root_map.get(parent).copied().unwrap_or(FUSE_ROOT_ID), + None => { + // parent should be root + FUSE_ROOT_ID + } + }; + tree_list[parent_inode as usize - 1].add_child(inode); + + let base_name = name.file_name().ok_or_else(|| { + let err = std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("invalid file name {}", file.name.display()), + ); + CliprdrError::FileError { + path: file.name.clone(), + err, + } + })?; + + let mut desc = file.clone(); + + if desc.kind == FileType::Directory { + sub_root_map.insert(desc.name.clone(), inode); + } + + desc.name = Path::new(base_name).to_path_buf(); + + let mut fuse_node = FuseNode::from_description(inode, desc); + fuse_node.parent = Some(parent_inode); + tree_list.push(fuse_node); + } + Ok(tree_list) + } +} + +pub type Inode = u64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileType { + File, + Directory, + // todo: support symlink + Symlink, +} + +impl From for fuser::FileType { + fn from(value: FileType) -> Self { + match value { + FileType::File => Self::RegularFile, + FileType::Directory => Self::Directory, + FileType::Symlink => Self::Symlink, + } + } +} + +#[derive(Debug, Clone)] +pub struct InodeAttributes { + inode: Inode, + size: u64, + // file reference meta + // should be the only mutable field in this struct + last_accessed: std::time::SystemTime, + last_modified: std::time::SystemTime, + last_metadata_changed: std::time::SystemTime, + creation_time: std::time::SystemTime, + kind: FileType, + perm: u16, + + // not implemented + _xattrs: BTreeMap, Vec>, +} + +impl InodeAttributes { + pub fn new(inode: u64, size: u64, perm: u16, kind: FileType) -> Self { + Self { + inode, + size, + last_accessed: std::time::SystemTime::now(), + last_modified: std::time::SystemTime::now(), + last_metadata_changed: std::time::SystemTime::now(), + creation_time: std::time::SystemTime::now(), + kind, + perm, + _xattrs: BTreeMap::new(), + } + } + + pub fn from_description(inode: u64, desc: FileDescription) -> Self { + Self { + inode, + size: desc.size, + last_modified: desc.last_modified, + last_metadata_changed: desc.last_metadata_changed, + creation_time: desc.creation_time, + last_accessed: SystemTime::now(), + kind: desc.kind, + perm: desc.perm, + + _xattrs: BTreeMap::new(), + } + } + + pub fn new_root() -> Self { + Self::new(FUSE_ROOT_ID, 0, PERM_RWX, FileType::Directory) + } + + pub fn access(&mut self) { + self.last_accessed = std::time::SystemTime::now(); + } +} + +impl From<&InodeAttributes> for fuser::FileAttr { + fn from(value: &InodeAttributes) -> Self { + let blocks = if value.size % BLOCK_SIZE as u64 == 0 { + value.size / BLOCK_SIZE as u64 + } else { + value.size / BLOCK_SIZE as u64 + 1 + }; + Self { + ino: value.inode, + size: value.size, + blocks, + atime: value.last_accessed, + mtime: value.last_modified, + ctime: value.last_metadata_changed, + crtime: value.creation_time, + kind: value.kind.into(), + + // read only + perm: value.perm, + + nlink: 1, + // set to current user + uid: unsafe { libc::getuid() }, + // set to current user + gid: unsafe { libc::getgid() }, + rdev: 0, + blksize: BLOCK_SIZE, + // todo: support macos flags + flags: 0, + } + } +} + +#[derive(Debug)] +struct FileHandles { + waiter: Condvar, + handlers: Mutex>, + gc: AtomicBool, +} + +impl FileHandles { + pub fn new() -> Self { + Self { + waiter: Condvar::new(), + // the vector in handlers is sorted, from small to big + // prove: + // - later allocated handler will be bigger than previous ones + // - new handlers will append to the end of the vector + // - dropping old handlers won't affect the ordering + handlers: Mutex::new(Vec::new()), + gc: AtomicBool::new(false), + } + } + + pub fn add_handler(&self, fh: u64) { + if self.marked() { + panic!("adding new handler to a marked ref counter"); + } + self.handlers.lock().push(fh); + } + + pub fn marked(&self) -> bool { + self.gc.load(Ordering::Relaxed) + } + + pub fn have_handler(&self, handler: u64) -> bool { + let handlers = self.handlers.lock(); + handlers.binary_search(&handler).is_ok() + } + + pub fn unregister(&self, handler: u64) -> Result<(), std::io::Error> { + let mut handlers = self.handlers.lock(); + + let Ok(idx) = handlers.binary_search(&handler) else { + let e = std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid handler"); + return Err(e); + }; + + handlers.remove(idx); + self.waiter.notify_all(); + Ok(()) + } +} + +#[cfg(test)] +mod fuse_test { + use std::str::FromStr; + + use super::*; + + // todo: more tests needed! + fn desc_gen(name: &str, kind: FileType) -> FileDescription { + FileDescription { + conn_id: 0, + name: PathBuf::from(name), + kind, + atime: SystemTime::UNIX_EPOCH, + last_modified: SystemTime::UNIX_EPOCH, + last_metadata_changed: SystemTime::UNIX_EPOCH, + creation_time: SystemTime::UNIX_EPOCH, + + size: 0, + perm: 0, + } + } + fn generate_descriptions(prefix: &str) -> Vec { + let (d0_path, f0_path, f1_path, d1_path, f2_path, f3_path) = if prefix.is_empty() { + ( + "folder0".to_string(), + "folder0/file0".to_string(), + "folder0/file1".to_string(), + "folder1".to_string(), + "folder1/file2".to_string(), + "folder1/📄3".to_string(), + ) + } else { + ( + format!("{}/folder0", prefix), + format!("{}/folder0/file0", prefix), + format!("{}/folder0/file1", prefix), + format!("{}/folder1", prefix), + format!("{}/folder1/file2", prefix), + format!("{}/folder1/📄3", prefix), + ) + }; + let folder0 = desc_gen(&d0_path, FileType::Directory); + let file0 = desc_gen(&f0_path, FileType::File); + let file1 = desc_gen(&f1_path, FileType::File); + let folder1 = desc_gen(&d1_path, FileType::Directory); + let file2 = desc_gen(&f2_path, FileType::File); + let file3 = desc_gen(&f3_path, FileType::File); + + vec![folder0, file0, file1, folder1, file2, file3] + } + + fn build_tree(prefix: &str) { + let source_list = generate_descriptions(prefix); + + let build_res = FuseNode::build_tree(source_list); + assert!(build_res.is_ok()); + let tree_list = build_res.unwrap(); + + assert_eq!(tree_list.len(), 7); + + assert_eq!(tree_list[0].name, "/"); + assert_eq!(tree_list[1].name, "folder0"); + assert_eq!(tree_list[2].name, "file0"); + assert_eq!(tree_list[3].name, "file1"); + assert_eq!(tree_list[4].name, "folder1"); + assert_eq!(tree_list[5].name, "file2"); + assert_eq!(tree_list[6].name, "📄3"); + + assert_eq!(tree_list[0].children, vec![2, 5]); + assert_eq!(tree_list[1].children, vec![3, 4]); + assert!(tree_list[2].children.is_empty()); + assert!(tree_list[3].children.is_empty()); + assert_eq!(tree_list[4].children, vec![6, 7]); + assert!(tree_list[5].children.is_empty()); + assert!(tree_list[6].children.is_empty()); + + for (idx, node) in tree_list.iter().skip(1).enumerate() { + assert_eq!(idx, node.index) + } + } + + fn build_single_file(prefix: &str) { + let raw_name = "衬衫的价格为 9 镑 15 便士.txt"; + let f_name = if prefix == "" { + raw_name.to_string() + } else { + prefix.to_string() + "/" + raw_name + }; + let desc = desc_gen(&f_name, FileType::File); + let tree = FuseNode::build_tree(vec![desc]).unwrap(); + + assert_eq!(tree.len(), 2); + assert_eq!(tree[0].name, "/"); + assert_eq!(tree[0].children, vec![2]); + + assert_eq!(tree[1].name, raw_name); + assert_eq!(tree[1].index, 0); + assert_eq!(tree[1].attributes.kind, FileType::File); + } + + #[test] + fn test_parse_single() { + build_single_file(""); + build_single_file("/"); + build_single_file("test"); + build_single_file("/test"); + build_single_file("🗂"); + build_single_file("/🗂"); + } + + #[test] + fn test_parse_tree() { + build_tree(""); + build_tree("/"); + build_tree("test"); + build_tree("/test"); + build_tree("/test/test"); + build_tree("🗂"); + build_tree("/🗂"); + build_tree("🗂/test"); + } +} diff --git a/libs/clipboard/src/platform/mod.rs b/libs/clipboard/src/platform/mod.rs new file mode 100644 index 000000000..9f76fa8bb --- /dev/null +++ b/libs/clipboard/src/platform/mod.rs @@ -0,0 +1,86 @@ +use crate::{CliprdrError, CliprdrServiceContext}; + +#[cfg(target_os = "windows")] +pub mod windows; +#[cfg(target_os = "windows")] +pub fn create_cliprdr_context( + enable_files: bool, + enable_others: bool, + response_wait_timeout_secs: u32, +) -> crate::ResultType> { + let boxed = + windows::create_cliprdr_context(enable_files, enable_others, response_wait_timeout_secs)? + as Box<_>; + Ok(boxed) +} + +#[cfg(feature = "unix-file-copy-paste")] +#[cfg(any(target_os = "linux", target_os = "macos"))] +/// use FUSE for file pasting on these platforms +pub mod fuse; +#[cfg(feature = "unix-file-copy-paste")] +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub mod unix; +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub fn create_cliprdr_context( + _enable_files: bool, + _enable_others: bool, + _response_wait_timeout_secs: u32, +) -> crate::ResultType> { + #[cfg(feature = "unix-file-copy-paste")] + { + use std::{fs::Permissions, os::unix::prelude::PermissionsExt}; + + use hbb_common::{config::APP_NAME, log}; + + if !_enable_files { + return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); + } + + let timeout = std::time::Duration::from_secs(_response_wait_timeout_secs as u64); + + let app_name = APP_NAME.read().unwrap().clone(); + + let mnt_path = format!("/tmp/{}/{}", app_name, "cliprdr"); + + // this function must be called after the main IPC is up + std::fs::create_dir(&mnt_path).ok(); + std::fs::set_permissions(&mnt_path, Permissions::from_mode(0o777)).ok(); + + log::info!("clear previously mounted cliprdr FUSE"); + if let Err(e) = std::process::Command::new("umount").arg(&mnt_path).status() { + log::warn!("umount {:?} may fail: {:?}", mnt_path, e); + } + + let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse().unwrap())?; + log::debug!("start cliprdr FUSE"); + unix_ctx.run().expect("failed to start cliprdr FUSE"); + + Ok(Box::new(unix_ctx) as Box<_>) + } + #[cfg(not(feature = "unix-file-copy-paste"))] + return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); +} + +struct DummyCliprdrContext {} + +impl CliprdrServiceContext for DummyCliprdrContext { + fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { + Ok(()) + } + fn empty_clipboard(&mut self, _conn_id: i32) -> Result { + Ok(true) + } + fn server_clip_file( + &mut self, + _conn_id: i32, + _msg: crate::ClipboardFile, + ) -> Result<(), crate::CliprdrError> { + Ok(()) + } +} + +#[cfg(feature = "unix-file-copy-paste")] +// begin of epoch used by microsoft +// 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00 +const LDAP_EPOCH_DELTA: u64 = 116444772610000000; diff --git a/libs/clipboard/src/platform/unix/local_file.rs b/libs/clipboard/src/platform/unix/local_file.rs new file mode 100644 index 000000000..c9186c4fa --- /dev/null +++ b/libs/clipboard/src/platform/unix/local_file.rs @@ -0,0 +1,367 @@ +use std::{ + collections::HashSet, + fs::File, + io::{BufRead, BufReader, Read, Seek}, + os::unix::prelude::PermissionsExt, + path::PathBuf, + sync::atomic::{AtomicU64, Ordering}, + time::SystemTime, +}; + +use hbb_common::{ + bytes::{BufMut, BytesMut}, + log, +}; +use utf16string::WString; + +use crate::{ + platform::{fuse::BLOCK_SIZE, LDAP_EPOCH_DELTA}, + CliprdrError, +}; + +/// has valid file attributes +const FLAGS_FD_ATTRIBUTES: u32 = 0x04; +/// has valid file size +const FLAGS_FD_SIZE: u32 = 0x40; +/// has valid last write time +const FLAGS_FD_LAST_WRITE: u32 = 0x20; +/// show progress +const FLAGS_FD_PROGRESSUI: u32 = 0x4000; +/// transferred from unix, contains file mode +/// P.S. this flag is not used in windows +const FLAGS_FD_UNIX_MODE: u32 = 0x08; + +#[derive(Debug)] +pub(super) struct LocalFile { + pub path: PathBuf, + + pub handle: Option>, + pub offset: AtomicU64, + + pub name: String, + pub size: u64, + pub last_write_time: SystemTime, + pub is_dir: bool, + pub perm: u32, + pub read_only: bool, + pub hidden: bool, + pub system: bool, + pub archive: bool, + pub normal: bool, +} + +impl LocalFile { + pub fn try_open(path: &PathBuf) -> Result { + let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { + path: path.clone(), + err: e, + })?; + let size = mt.len() as u64; + let is_dir = mt.is_dir(); + let read_only = mt.permissions().readonly(); + let system = false; + let hidden = path.to_string_lossy().starts_with('.'); + let archive = false; + let normal = !(is_dir || read_only || system || hidden || archive); + let last_write_time = mt.modified().unwrap_or(SystemTime::UNIX_EPOCH); + + let perm = mt.permissions().mode(); + + let name = path + .display() + .to_string() + .trim_start_matches('/') + .replace('/', "\\"); + + // NOTE: open files lazily + let handle = None; + let offset = AtomicU64::new(0); + + Ok(Self { + name, + path: path.clone(), + handle, + offset, + size, + last_write_time, + is_dir, + read_only, + system, + hidden, + perm, + archive, + normal, + }) + } + pub fn as_bin(&self) -> Vec { + let mut buf = BytesMut::with_capacity(592); + + let read_only_flag = if self.read_only { 0x1 } else { 0 }; + let hidden_flag = if self.hidden { 0x2 } else { 0 }; + let system_flag = if self.system { 0x4 } else { 0 }; + let directory_flag = if self.is_dir { 0x10 } else { 0 }; + let archive_flag = if self.archive { 0x20 } else { 0 }; + let normal_flag = if self.normal { 0x80 } else { 0 }; + + let file_attributes: u32 = read_only_flag + | hidden_flag + | system_flag + | directory_flag + | archive_flag + | normal_flag; + + let win32_time = self + .last_write_time + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64 + / 100 + + LDAP_EPOCH_DELTA; + + let size_high = (self.size >> 32) as u32; + let size_low = (self.size & (u32::MAX as u64)) as u32; + + let path = self.path.to_string_lossy().to_string(); + + let wstr: WString = WString::from(&path); + let name = wstr.as_bytes(); + + log::trace!( + "put file to list: name_len {}, name {}", + name.len(), + &self.name + ); + + let flags = FLAGS_FD_SIZE + | FLAGS_FD_LAST_WRITE + | FLAGS_FD_ATTRIBUTES + | FLAGS_FD_PROGRESSUI + | FLAGS_FD_UNIX_MODE; + + // flags, 4 bytes + buf.put_u32_le(flags); + // 32 bytes reserved + buf.put(&[0u8; 32][..]); + // file attributes, 4 bytes + buf.put_u32_le(file_attributes); + + // NOTE: this is not used in windows + // in the specification, this is 16 bytes reserved + // lets use the last 4 bytes to store the file mode + // + // 12 bytes reserved + buf.put(&[0u8; 12][..]); + // file permissions, 4 bytes + buf.put_u32_le(self.perm); + + // last write time, 8 bytes + buf.put_u64_le(win32_time); + // file size (high) + buf.put_u32_le(size_high); + // file size (low) + buf.put_u32_le(size_low); + // put name and padding to 520 bytes + let name_len = name.len(); + buf.put(name); + buf.put(&vec![0u8; 520 - name_len][..]); + + buf.to_vec() + } + + #[inline] + pub fn load_handle(&mut self) -> Result<(), CliprdrError> { + if !self.is_dir && self.handle.is_none() { + let handle = std::fs::File::open(&self.path).map_err(|e| CliprdrError::FileError { + path: self.path.clone(), + err: e, + })?; + let mut reader = BufReader::with_capacity(BLOCK_SIZE as usize * 2, handle); + reader.fill_buf().map_err(|e| CliprdrError::FileError { + path: self.path.clone(), + err: e, + })?; + self.handle = Some(reader); + }; + Ok(()) + } + + pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> { + self.load_handle()?; + + let handle = self.handle.as_mut().unwrap(); + + if offset != self.offset.load(Ordering::Relaxed) { + handle + .seek(std::io::SeekFrom::Start(offset)) + .map_err(|e| CliprdrError::FileError { + path: self.path.clone(), + err: e, + })?; + } + handle + .read_exact(buf) + .map_err(|e| CliprdrError::FileError { + path: self.path.clone(), + err: e, + })?; + let new_offset = offset + (buf.len() as u64); + self.offset.store(new_offset, Ordering::Relaxed); + + // gc file handle + if new_offset >= self.size { + self.offset.store(0, Ordering::Relaxed); + self.handle = None; + } + + Ok(()) + } +} + +pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, CliprdrError> { + fn constr_file_lst( + path: &PathBuf, + file_list: &mut Vec, + visited: &mut HashSet, + ) -> Result<(), CliprdrError> { + // prevent fs loop + if visited.contains(path) { + return Ok(()); + } + visited.insert(path.clone()); + + let local_file = LocalFile::try_open(path)?; + file_list.push(local_file); + + let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { + path: path.clone(), + err: e, + })?; + + if mt.is_dir() { + let dir = std::fs::read_dir(path).unwrap(); + for entry in dir { + let entry = entry.unwrap(); + let path = entry.path(); + constr_file_lst(&path, file_list, visited)?; + } + } + Ok(()) + } + + let mut file_list = Vec::new(); + let mut visited = HashSet::new(); + + for path in paths { + constr_file_lst(path, &mut file_list, &mut visited)?; + } + Ok(file_list) +} + +#[cfg(test)] +mod file_list_test { + use std::{path::PathBuf, sync::atomic::AtomicU64}; + + use hbb_common::bytes::{BufMut, BytesMut}; + + use crate::{platform::fuse::FileDescription, CliprdrError}; + + use super::LocalFile; + + #[inline] + fn generate_tree(prefix: &str) -> Vec { + // generate a tree of local files, no handles + // - / + // |- a.txt + // |- b + // |- c.txt + #[inline] + fn generate_file(path: &str, name: &str, is_dir: bool) -> LocalFile { + LocalFile { + path: PathBuf::from(path), + handle: None, + name: name.to_string(), + size: 0, + offset: AtomicU64::new(0), + last_write_time: std::time::SystemTime::UNIX_EPOCH, + read_only: false, + is_dir, + perm: 0o754, + hidden: false, + system: false, + archive: false, + normal: false, + } + } + + let p = prefix; + + let (r_path, a_path, b_path, c_path) = if !prefix.is_empty() { + ( + p.to_string(), + format!("{}/a.txt", p), + format!("{}/b", p), + format!("{}/b/c.txt", p), + ) + } else { + ( + ".".to_string(), + "a.txt".to_string(), + "b".to_string(), + "b/c.txt".to_string(), + ) + }; + + let root = generate_file(&r_path, ".", true); + let a = generate_file(&a_path, "a.txt", false); + let b = generate_file(&b_path, "b", true); + let c = generate_file(&c_path, "c.txt", false); + + vec![root, a, b, c] + } + + fn as_bin_parse_test(prefix: &str) -> Result<(), CliprdrError> { + let tree = generate_tree(prefix); + let mut pdu = BytesMut::with_capacity(4 + 592 * tree.len()); + pdu.put_u32_le(tree.len() as u32); + for file in tree { + pdu.put(file.as_bin().as_slice()); + } + + let parsed = FileDescription::parse_file_descriptors(pdu.to_vec(), 0)?; + assert_eq!(parsed.len(), 4); + + if !prefix.is_empty() { + assert_eq!(parsed[0].name.to_str().unwrap(), format!("{}", prefix)); + assert_eq!( + parsed[1].name.to_str().unwrap(), + format!("{}/a.txt", prefix) + ); + assert_eq!(parsed[2].name.to_str().unwrap(), format!("{}/b", prefix)); + assert_eq!( + parsed[3].name.to_str().unwrap(), + format!("{}/b/c.txt", prefix) + ); + } else { + assert_eq!(parsed[0].name.to_str().unwrap(), "."); + assert_eq!(parsed[1].name.to_str().unwrap(), "a.txt"); + assert_eq!(parsed[2].name.to_str().unwrap(), "b"); + assert_eq!(parsed[3].name.to_str().unwrap(), "b/c.txt"); + } + + assert!(parsed[0].perm & 0o777 == 0o754); + assert!(parsed[1].perm & 0o777 == 0o754); + assert!(parsed[2].perm & 0o777 == 0o754); + assert!(parsed[3].perm & 0o777 == 0o754); + + Ok(()) + } + + #[test] + fn test_parse_file_descriptors() -> Result<(), CliprdrError> { + as_bin_parse_test("")?; + as_bin_parse_test("/")?; + as_bin_parse_test("test")?; + as_bin_parse_test("/test")?; + Ok(()) + } +} diff --git a/libs/clipboard/src/platform/unix/mod.rs b/libs/clipboard/src/platform/unix/mod.rs new file mode 100644 index 000000000..cd499d0d8 --- /dev/null +++ b/libs/clipboard/src/platform/unix/mod.rs @@ -0,0 +1,593 @@ +use std::{ + path::PathBuf, + sync::{mpsc::Sender, Arc}, + time::Duration, +}; + +use dashmap::DashMap; +use fuser::MountOption; +use hbb_common::{ + bytes::{BufMut, BytesMut}, + log, +}; +use lazy_static::lazy_static; +use parking_lot::Mutex; + +use crate::{ + platform::{fuse::FileDescription, unix::local_file::construct_file_list}, + send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, +}; + +use self::local_file::LocalFile; +#[cfg(target_os = "linux")] +use self::url::{encode_path_to_uri, parse_plain_uri_list}; + +use super::fuse::FuseServer; + +#[cfg(target_os = "linux")] +pub mod x11; + +pub mod local_file; + +#[cfg(target_os = "linux")] +pub mod url; + +// not actual format id, just a placeholder +const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334; +const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW"; +// not actual format id, just a placeholder +const FILECONTENTS_FORMAT_ID: i32 = 49267; +const FILECONTENTS_FORMAT_NAME: &str = "FileContents"; + +lazy_static! { + static ref REMOTE_FORMAT_MAP: DashMap = DashMap::from_iter( + [ + ( + FILEDESCRIPTOR_FORMAT_ID, + FILEDESCRIPTORW_FORMAT_NAME.to_string() + ), + (FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME.to_string()) + ] + .iter() + .cloned() + ); +} + +fn get_local_format(remote_id: i32) -> Option { + REMOTE_FORMAT_MAP.get(&remote_id).map(|s| s.clone()) +} + +fn add_remote_format(local_name: &str, remote_id: i32) { + REMOTE_FORMAT_MAP.insert(remote_id, local_name.to_string()); +} + +trait SysClipboard: Send + Sync { + fn start(&self); + + fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>; + fn get_file_list(&self) -> Vec; +} + +#[cfg(target_os = "linux")] +fn get_sys_clipboard(ignore_path: &PathBuf) -> Result, CliprdrError> { + #[cfg(feature = "wayland")] + { + unimplemented!() + } + #[cfg(not(feature = "wayland"))] + { + pub use x11::*; + let x11_clip = X11Clipboard::new(ignore_path)?; + Ok(Box::new(x11_clip) as Box<_>) + } +} + +#[cfg(target_os = "macos")] +fn get_sys_clipboard(ignore_path: &PathBuf) -> Result, CliprdrError> { + unimplemented!() +} + +#[derive(Debug)] +enum FileContentsRequest { + Size { + stream_id: i32, + file_idx: usize, + }, + + Range { + stream_id: i32, + file_idx: usize, + offset: u64, + length: u64, + }, +} + +pub struct ClipboardContext { + pub fuse_mount_point: PathBuf, + /// stores fuse background session handle + fuse_handle: Mutex>, + + /// a sender of clipboard file contents pdu to fuse server + fuse_tx: Sender, + fuse_server: Arc>, + + clipboard: Arc, + local_files: Mutex>, +} + +impl ClipboardContext { + pub fn new(timeout: Duration, mount_path: PathBuf) -> Result { + // assert mount path exists + let fuse_mount_point = mount_path.canonicalize().map_err(|e| { + log::error!("failed to canonicalize mount path: {:?}", e); + CliprdrError::CliprdrInit + })?; + + let (fuse_server, fuse_tx) = FuseServer::new(timeout); + + let fuse_server = Arc::new(Mutex::new(fuse_server)); + + let clipboard = get_sys_clipboard(&fuse_mount_point)?; + let clipboard = Arc::from(clipboard) as Arc<_>; + let local_files = Mutex::new(vec![]); + + Ok(Self { + fuse_mount_point, + fuse_server, + fuse_tx, + fuse_handle: Mutex::new(None), + clipboard, + local_files, + }) + } + + pub fn run(&self) -> Result<(), CliprdrError> { + if !self.is_stopped() { + return Ok(()); + } + + let mut fuse_handle = self.fuse_handle.lock(); + + let mount_path = &self.fuse_mount_point; + + let mnt_opts = [ + MountOption::FSName("rustdesk-cliprdr-fs".to_string()), + MountOption::NoAtime, + MountOption::RO, + ]; + log::info!( + "mounting clipboard FUSE to {}", + self.fuse_mount_point.display() + ); + + let new_handle = fuser::spawn_mount2( + FuseServer::client(self.fuse_server.clone()), + mount_path, + &mnt_opts, + ) + .map_err(|e| { + log::error!("failed to mount cliprdr fuse: {:?}", e); + CliprdrError::CliprdrInit + })?; + *fuse_handle = Some(new_handle); + + let clipboard = self.clipboard.clone(); + + std::thread::spawn(move || { + log::debug!("start listening clipboard"); + clipboard.start(); + }); + + Ok(()) + } + + /// set clipboard data from file list + pub fn set_clipboard(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { + let prefix = self.fuse_mount_point.clone(); + let paths: Vec = paths.iter().cloned().map(|p| prefix.join(p)).collect(); + log::debug!("setting clipboard with paths: {:?}", paths); + self.clipboard.set_file_list(&paths)?; + log::debug!("clipboard set, paths: {:?}", paths); + Ok(()) + } + + fn serve_file_contents( + &self, + conn_id: i32, + request: FileContentsRequest, + ) -> Result<(), CliprdrError> { + let mut file_list = self.local_files.lock(); + + let (file_idx, file_contents_resp) = match request { + FileContentsRequest::Size { + stream_id, + file_idx, + } => { + log::debug!("file contents (size) requested from conn: {}", conn_id); + let Some(file) = file_list.get(file_idx) else { + log::error!( + "invalid file index {} requested from conn: {}", + file_idx, + conn_id + ); + resp_file_contents_fail(conn_id, stream_id); + + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid file index {} requested from conn: {}", + file_idx, conn_id + ), + }); + }; + + log::debug!( + "conn {} requested file-{}: {}", + conn_id, + file_idx, + file.name + ); + + let size = file.size; + ( + file_idx, + ClipboardFile::FileContentsResponse { + msg_flags: 0x1, + stream_id, + requested_data: size.to_le_bytes().to_vec(), + }, + ) + } + FileContentsRequest::Range { + stream_id, + file_idx, + offset, + length, + } => { + log::debug!( + "file contents (range from {} length {}) request from conn: {}", + offset, + length, + conn_id + ); + let Some(file) = file_list.get_mut(file_idx) else { + log::error!( + "invalid file index {} requested from conn: {}", + file_idx, + conn_id + ); + resp_file_contents_fail(conn_id, stream_id); + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid file index {} requested from conn: {}", + file_idx, conn_id + ), + }); + }; + log::debug!( + "conn {} requested file-{}: {}", + conn_id, + file_idx, + file.name + ); + + if offset > file.size { + log::error!("invalid reading offset requested from conn: {}", conn_id); + resp_file_contents_fail(conn_id, stream_id); + + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid reading offset requested from conn: {}", + conn_id + ), + }); + } + let read_size = if offset + length > file.size { + file.size - offset + } else { + length + }; + + let mut buf = vec![0u8; read_size as usize]; + + file.read_exact_at(&mut buf, offset)?; + + ( + file_idx, + ClipboardFile::FileContentsResponse { + msg_flags: 0x1, + stream_id, + requested_data: buf, + }, + ) + } + }; + + send_data(conn_id, file_contents_resp); + log::debug!("file contents sent to conn: {}", conn_id); + // hot reload next file + for next_file in file_list.iter_mut().skip(file_idx + 1) { + if !next_file.is_dir { + next_file.load_handle()?; + break; + } + } + Ok(()) + } +} + +fn resp_file_contents_fail(conn_id: i32, stream_id: i32) { + let resp = ClipboardFile::FileContentsResponse { + msg_flags: 0x2, + stream_id, + requested_data: vec![], + }; + send_data(conn_id, resp) +} + +impl ClipboardContext { + pub fn is_stopped(&self) -> bool { + self.fuse_handle.lock().is_none() + } + + pub fn sync_local_files(&self) -> Result<(), CliprdrError> { + let mut local_files = self.local_files.lock(); + let clipboard_files = self.clipboard.get_file_list(); + let local_file_list: Vec = local_files.iter().map(|f| f.path.clone()).collect(); + if local_file_list == clipboard_files { + return Ok(()); + } + let new_files = construct_file_list(&clipboard_files)?; + *local_files = new_files; + Ok(()) + } + + pub fn serve(&self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { + log::debug!("serve clipboard file from conn: {}", conn_id); + if self.is_stopped() { + log::debug!("cliprdr stopped, restart it"); + self.run()?; + } + match msg { + ClipboardFile::NotifyCallback { .. } => { + unreachable!() + } + ClipboardFile::MonitorReady => { + log::debug!("server_monitor_ready called"); + + self.send_file_list(conn_id)?; + + Ok(()) + } + + ClipboardFile::FormatList { format_list } => { + log::debug!("server_format_list called"); + // filter out "FileGroupDescriptorW" and "FileContents" + let fmt_lst: Vec<(i32, String)> = format_list + .into_iter() + .filter(|(_, name)| { + name == FILEDESCRIPTORW_FORMAT_NAME || name == FILECONTENTS_FORMAT_NAME + }) + .collect(); + if fmt_lst.len() != 2 { + log::debug!("no supported formats"); + return Ok(()); + } + log::debug!("supported formats: {:?}", fmt_lst); + let file_contents_id = fmt_lst + .iter() + .find(|(_, name)| name == FILECONTENTS_FORMAT_NAME) + .map(|(id, _)| *id) + .unwrap(); + let file_descriptor_id = fmt_lst + .iter() + .find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME) + .map(|(id, _)| *id) + .unwrap(); + + add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id); + add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id); + + // sync file system from peer + let data = ClipboardFile::FormatDataRequest { + requested_format_id: file_descriptor_id, + }; + send_data(conn_id, data); + + Ok(()) + } + ClipboardFile::FormatListResponse { msg_flags } => { + log::debug!("server_format_list_response called"); + if msg_flags != 0x1 { + send_format_list(conn_id) + } else { + Ok(()) + } + } + ClipboardFile::FormatDataRequest { + requested_format_id, + } => { + log::debug!("server_format_data_request called"); + let Some(format) = get_local_format(requested_format_id) else { + log::error!( + "got unsupported format data request: id={} from conn={}", + requested_format_id, + conn_id + ); + resp_format_data_failure(conn_id); + return Ok(()); + }; + + if format == FILEDESCRIPTORW_FORMAT_NAME { + self.send_file_list(conn_id)?; + } else if format == FILECONTENTS_FORMAT_NAME { + log::error!( + "try to read file contents with FormatDataRequest from conn={}", + conn_id + ); + resp_format_data_failure(conn_id); + } else { + log::error!( + "got unsupported format data request: id={} from conn={}", + requested_format_id, + conn_id + ); + resp_format_data_failure(conn_id); + } + Ok(()) + } + ClipboardFile::FormatDataResponse { + msg_flags, + format_data, + } => { + log::debug!( + "server_format_data_response called, msg_flags={}", + msg_flags + ); + + if msg_flags != 0x1 { + resp_format_data_failure(conn_id); + return Ok(()); + } + + log::debug!("parsing file descriptors"); + // this must be a file descriptor format data + let files = FileDescription::parse_file_descriptors(format_data, conn_id)?; + + let paths = { + let mut fuse_guard = self.fuse_server.lock(); + fuse_guard.load_file_list(files)?; + + fuse_guard.list_root() + }; + + log::debug!("load file list: {:?}", paths); + self.set_clipboard(&paths)?; + Ok(()) + } + ClipboardFile::FileContentsResponse { .. } => { + log::debug!("server_file_contents_response called"); + // we don't know its corresponding request, no resend can be performed + self.fuse_tx.send(msg).map_err(|e| { + log::error!("failed to send file contents response to fuse: {:?}", e); + CliprdrError::ClipboardInternalError + })?; + Ok(()) + } + ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + .. + } => { + log::debug!("server_file_contents_request called"); + let fcr = if dw_flags == 0x1 { + FileContentsRequest::Size { + stream_id, + file_idx: list_index as usize, + } + } else if dw_flags == 0x2 { + let offset = (n_position_high as u64) << 32 | n_position_low as u64; + let length = cb_requested as u64; + + FileContentsRequest::Range { + stream_id, + file_idx: list_index as usize, + offset, + length, + } + } else { + log::error!("got invalid FileContentsRequest from conn={}", conn_id); + resp_file_contents_fail(conn_id, stream_id); + return Ok(()); + }; + + self.serve_file_contents(conn_id, fcr) + } + } + } + + fn send_file_list(&self, conn_id: i32) -> Result<(), CliprdrError> { + self.sync_local_files()?; + + let file_list = self.local_files.lock(); + send_file_list(&*file_list, conn_id) + } +} + +impl CliprdrServiceContext for ClipboardContext { + fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { + // unmount the fuse + if let Some(fuse_handle) = self.fuse_handle.lock().take() { + fuse_handle.join(); + } + // we don't stop the clipboard, keep listening in case of restart + Ok(()) + } + + fn empty_clipboard(&mut self, _conn_id: i32) -> Result { + self.clipboard.set_file_list(&[])?; + Ok(true) + } + + fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { + self.serve(conn_id, msg) + } +} + +fn resp_format_data_failure(conn_id: i32) { + let data = ClipboardFile::FormatDataResponse { + msg_flags: 0x2, + format_data: vec![], + }; + send_data(conn_id, data) +} + +fn send_format_list(conn_id: i32) -> Result<(), CliprdrError> { + log::debug!("send format list to remote, conn={}", conn_id); + let fd_format_name = get_local_format(FILEDESCRIPTOR_FORMAT_ID) + .unwrap_or(FILEDESCRIPTORW_FORMAT_NAME.to_string()); + let fc_format_name = + get_local_format(FILECONTENTS_FORMAT_ID).unwrap_or(FILECONTENTS_FORMAT_NAME.to_string()); + let format_list = ClipboardFile::FormatList { + format_list: vec![ + (FILEDESCRIPTOR_FORMAT_ID, fd_format_name), + (FILECONTENTS_FORMAT_ID, fc_format_name), + ], + }; + + send_data(conn_id, format_list); + log::debug!("format list to remote dispatched, conn={}", conn_id); + Ok(()) +} + +fn build_file_list_pdu(files: &[LocalFile]) -> Vec { + let mut data = BytesMut::with_capacity(4 + 592 * files.len()); + data.put_u32_le(files.len() as u32); + for file in files.iter() { + data.put(file.as_bin().as_slice()); + } + + data.to_vec() +} + +fn send_file_list(files: &[LocalFile], conn_id: i32) -> Result<(), CliprdrError> { + log::debug!( + "send file list to remote, conn={}, list={:?}", + conn_id, + files.iter().map(|f| f.path.display()).collect::>() + ); + + let format_data = build_file_list_pdu(files); + + send_data( + conn_id, + ClipboardFile::FormatDataResponse { + msg_flags: 1, + format_data, + }, + ); + Ok(()) +} diff --git a/libs/clipboard/src/platform/unix/url.rs b/libs/clipboard/src/platform/unix/url.rs new file mode 100644 index 000000000..ce97810c4 --- /dev/null +++ b/libs/clipboard/src/platform/unix/url.rs @@ -0,0 +1,75 @@ +use std::path::{Path, PathBuf}; + +use crate::CliprdrError; + +// on x11, path will be encode as +// "/home/rustdesk/pictures/🖼️.png" -> "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png" +// url encode and decode is needed +const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/'); + +pub(super) fn encode_path_to_uri(path: &PathBuf) -> String { + let encoded = percent_encoding::percent_encode(path.to_str().unwrap().as_bytes(), &ENCODE_SET) + .to_string(); + format!("file://{}", encoded) +} + +pub(super) fn parse_uri_to_path(encoded_uri: &str) -> Result { + let encoded_path = encoded_uri.trim_start_matches("file://"); + let path_str = percent_encoding::percent_decode_str(encoded_path) + .decode_utf8() + .map_err(|_| CliprdrError::ConversionFailure)?; + let path_str = path_str.to_string(); + + Ok(Path::new(&path_str).to_path_buf()) +} + +// helper parse function +// convert 'text/uri-list' data to a list of valid Paths +// # Note +// - none utf8 data will lead to error +pub(super) fn parse_plain_uri_list(v: Vec) -> Result, CliprdrError> { + let text = String::from_utf8(v).map_err(|_| CliprdrError::ConversionFailure)?; + parse_uri_list(&text) +} + +// helper parse function +// convert 'text/uri-list' data to a list of valid Paths +// # Note +// - none utf8 data will lead to error +pub(super) fn parse_uri_list(text: &str) -> Result, CliprdrError> { + let mut list = Vec::new(); + + for line in text.lines() { + if !line.starts_with("file://") { + continue; + } + let decoded = parse_uri_to_path(line)?; + list.push(decoded) + } + Ok(list) +} + +#[cfg(test)] +mod uri_test { + #[test] + fn test_conversion() { + let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png"); + let uri = super::encode_path_to_uri(&path); + assert_eq!( + uri, + "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png" + ); + let convert_back = super::parse_uri_to_path(&uri).unwrap(); + assert_eq!(path, convert_back); + } + + #[test] + fn parse_list() { + let uri_list = r#"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png +file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png +"#; + let list = super::parse_uri_list(uri_list.into()).unwrap(); + assert!(list.len() == 2); + assert_eq!(list[0], list[1]); + } +} diff --git a/libs/clipboard/src/platform/unix/x11.rs b/libs/clipboard/src/platform/unix/x11.rs new file mode 100644 index 000000000..4ca3a2c0b --- /dev/null +++ b/libs/clipboard/src/platform/unix/x11.rs @@ -0,0 +1,162 @@ +use std::{collections::BTreeSet, path::PathBuf}; + +use hbb_common::log; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use x11_clipboard::Clipboard; +use x11rb::protocol::xproto::Atom; + +use crate::{platform::unix::send_format_list, CliprdrError}; + +use super::{encode_path_to_uri, parse_plain_uri_list, SysClipboard}; + +static X11_CLIPBOARD: OnceCell = OnceCell::new(); + +fn get_clip() -> Result<&'static Clipboard, CliprdrError> { + X11_CLIPBOARD.get_or_try_init(|| Clipboard::new().map_err(|_| CliprdrError::CliprdrInit)) +} + +pub struct X11Clipboard { + ignore_path: PathBuf, + text_uri_list: Atom, + gnome_copied_files: Atom, + nautilus_clipboard: Atom, + + former_file_list: Mutex>, +} + +impl X11Clipboard { + pub fn new(ignore_path: &PathBuf) -> Result { + let clipboard = get_clip()?; + let text_uri_list = clipboard + .setter + .get_atom("text/uri-list") + .map_err(|_| CliprdrError::CliprdrInit)?; + let gnome_copied_files = clipboard + .setter + .get_atom("x-special/gnome-copied-files") + .map_err(|_| CliprdrError::CliprdrInit)?; + let nautilus_clipboard = clipboard + .setter + .get_atom("x-special/nautilus-clipboard") + .map_err(|_| CliprdrError::CliprdrInit)?; + Ok(Self { + ignore_path: ignore_path.to_owned(), + text_uri_list, + gnome_copied_files, + nautilus_clipboard, + former_file_list: Mutex::new(vec![]), + }) + } + + fn load(&self, target: Atom) -> Result, CliprdrError> { + let clip = get_clip()?.setter.atoms.clipboard; + let prop = get_clip()?.setter.atoms.property; + // NOTE: + // # why not use `load_wait` + // load_wait is likely to wait forever, which is not what we want + let res = get_clip()?.load_wait(clip, target, prop); + match res { + Ok(res) => Ok(res), + Err(x11_clipboard::error::Error::UnexpectedType(_)) => Ok(vec![]), + Err(x11_clipboard::error::Error::Timeout) => { + log::debug!("x11 clipboard get content timeout."); + Err(CliprdrError::ClipboardInternalError) + } + Err(e) => { + log::debug!("x11 clipboard get content fail: {:?}", e); + Err(CliprdrError::ClipboardInternalError) + } + } + } + + fn store_batch(&self, batch: Vec<(Atom, Vec)>) -> Result<(), CliprdrError> { + let clip = get_clip()?.setter.atoms.clipboard; + log::debug!("try to store clipboard content"); + get_clip()? + .store_batch(clip, batch) + .map_err(|_| CliprdrError::ClipboardInternalError) + } + + fn wait_file_list(&self) -> Result>, CliprdrError> { + let v = self.load(self.text_uri_list)?; + let p = parse_plain_uri_list(v)?; + Ok(Some(p)) + } +} + +impl SysClipboard for X11Clipboard { + fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { + *self.former_file_list.lock() = paths.to_vec(); + + let uri_list: Vec = paths.iter().map(encode_path_to_uri).collect(); + let uri_list = uri_list.join("\n"); + let text_uri_list_data = uri_list.as_bytes().to_vec(); + let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat(); + let batch = vec![ + (self.text_uri_list, text_uri_list_data), + (self.gnome_copied_files, gnome_copied_files_data.clone()), + (self.nautilus_clipboard, gnome_copied_files_data), + ]; + self.store_batch(batch) + .map_err(|_| CliprdrError::ClipboardInternalError) + } + + fn start(&self) { + { + // clear cached file list + *self.former_file_list.lock() = vec![]; + } + loop { + let sth = match self.wait_file_list() { + Ok(sth) => sth, + Err(e) => { + log::warn!("failed to get file list from clipboard: {}", e); + std::thread::sleep(std::time::Duration::from_millis(100)); + continue; + } + }; + + let Some(paths) = sth else { + // just sleep + std::thread::sleep(std::time::Duration::from_millis(100)); + continue; + }; + + let filtered = paths + .into_iter() + .filter(|pb| !pb.starts_with(&self.ignore_path)) + .collect::>(); + + if filtered.is_empty() { + std::thread::sleep(std::time::Duration::from_millis(100)); + continue; + } + + { + let mut former = self.former_file_list.lock(); + + let filtered_st: BTreeSet<_> = filtered.iter().collect(); + let former_st = former.iter().collect::>(); + if filtered_st == former_st { + std::thread::sleep(std::time::Duration::from_millis(100)); + continue; + } + + *former = filtered; + } + + if let Err(e) = send_format_list(0) { + log::warn!("failed to send format list: {}", e); + break; + } + + std::thread::sleep(std::time::Duration::from_millis(100)); + } + log::debug!("stop listening file related atoms on clipboard"); + } + + fn get_file_list(&self) -> Vec { + self.former_file_list.lock().clone() + } +} diff --git a/libs/clipboard/src/platform/windows.rs b/libs/clipboard/src/platform/windows.rs new file mode 100644 index 000000000..1576b4a78 --- /dev/null +++ b/libs/clipboard/src/platform/windows.rs @@ -0,0 +1,1219 @@ +//! windows implementation +#![allow(dead_code)] +#![allow(non_camel_case_types)] +#![allow(unused_variables)] +#![allow(non_snake_case)] +#![allow(deref_nullptr)] + +use std::{ + boxed::Box, + ffi::{CStr, CString}, + result::Result, +}; + +use crate::{ + allow_err, log, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType, + ERR_CODE_INVALID_PARAMETER, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, +}; + +// only used error code will be recorded here +/// success +const CHANNEL_RC_OK: u32 = 0; +/// error code from WinError.h +/// success +const ERROR_SUCCESS: u32 = 0; +/// allocation failure +const CHANNEL_RC_NO_MEMORY: u32 = 12; +/// error code from WinError.h +/// used by FreeRDP to represent errors. +const ERROR_INTERNAL_ERROR: u32 = 0x54F; + +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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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 connID: 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; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _NOTIFICATION_MESSAGE { + pub r#type: UINT32, // 0 - info, 1 - warning, 2 - error + pub msg: *const BYTE, + pub details: *const BYTE, +} +pub type NOTIFICATION_MESSAGE = _NOTIFICATION_MESSAGE; +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 pcNotifyClipboardMsg = ::std::option::Option< + unsafe extern "C" fn(connID: UINT32, msg: *const NOTIFICATION_MESSAGE) -> 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 IsStopped: BOOL, + pub ResponseWaitTimeoutSecs: UINT32, + pub ServerCapabilities: pcCliprdrServerCapabilities, + pub ClientCapabilities: pcCliprdrClientCapabilities, + pub MonitorReady: pcCliprdrMonitorReady, + pub TempDirectory: pcCliprdrTempDirectory, + pub NotifyClipboardMsg: pcNotifyClipboardMsg, + 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; + pub(crate) fn empty_cliprdr(context: *mut CliprdrClientContext, connID: UINT32) -> BOOL; +} + +unsafe impl Send for CliprdrClientContext {} + +unsafe impl Sync for CliprdrClientContext {} + +impl CliprdrClientContext { + pub fn create( + enable_files: bool, + enable_others: bool, + response_wait_timeout_secs: u32, + notify_callback: pcNotifyClipboardMsg, + 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 }, + IsStopped: FALSE, + ResponseWaitTimeoutSecs: response_wait_timeout_secs, + ServerCapabilities: None, + ClientCapabilities: None, + MonitorReady: None, + TempDirectory: None, + NotifyClipboardMsg: notify_callback, + 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"); + } + } + } +} + +impl CliprdrServiceContext for CliprdrClientContext { + fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { + self.IsStopped = TRUE; + Ok(()) + } + + fn empty_clipboard(&mut self, conn_id: i32) -> Result { + Ok(empty_clipboard(self, conn_id)) + } + + fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { + let ret = server_clip_file(self, conn_id, msg); + ret_to_result(ret) + } +} + +fn ret_to_result(ret: u32) -> Result<(), CliprdrError> { + match ret { + #[allow(unreachable_patterns)] + // CHANNEL_RC_OK is unreachable, but ignore it + ERROR_SUCCESS | CHANNEL_RC_OK => Ok(()), + CHANNEL_RC_NO_MEMORY => Err(CliprdrError::CliprdrOutOfMemory), + ERROR_INTERNAL_ERROR => Err(CliprdrError::ClipboardInternalError), + e => Err(CliprdrError::Unknown(e)), + } +} +pub fn empty_clipboard(context: &mut CliprdrClientContext, conn_id: i32) -> bool { + unsafe { TRUE == empty_cliprdr(context, conn_id as u32) } +} + +pub fn server_clip_file( + context: &mut CliprdrClientContext, + conn_id: i32, + msg: ClipboardFile, +) -> u32 { + let mut ret = 0; + match msg { + ClipboardFile::NotifyCallback { .. } => { + // unreachable + } + ClipboardFile::MonitorReady => { + log::debug!("server_monitor_ready called"); + ret = server_monitor_ready(context, conn_id); + log::debug!( + "server_monitor_ready called, conn_id {}, return {}", + conn_id, + ret + ); + } + ClipboardFile::FormatList { format_list } => { + log::debug!( + "server_format_list called, conn_id {}, format_list: {:?}", + conn_id, + &format_list + ); + ret = server_format_list(context, conn_id, format_list); + log::debug!( + "server_format_list called, conn_id {}, return {}", + conn_id, + ret + ); + } + ClipboardFile::FormatListResponse { msg_flags } => { + log::debug!("server_format_list_response called"); + ret = server_format_list_response(context, conn_id, msg_flags); + log::debug!( + "server_format_list_response called, conn_id {}, msg_flags {}, return {}", + conn_id, + msg_flags, + ret + ); + } + ClipboardFile::FormatDataRequest { + requested_format_id, + } => { + log::debug!("server_format_data_request called"); + ret = server_format_data_request(context, conn_id, requested_format_id); + log::debug!( + "server_format_data_request called, conn_id {}, requested_format_id {}, return {}", + conn_id, + requested_format_id, + ret + ); + } + ClipboardFile::FormatDataResponse { + msg_flags, + format_data, + } => { + log::debug!("server_format_data_response called"); + ret = server_format_data_response(context, conn_id, msg_flags, format_data); + log::debug!( + "server_format_data_response called, conn_id {}, msg_flags: {}, return {}", + conn_id, + msg_flags, + ret + ); + } + ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + have_clip_data_id, + clip_data_id, + } => { + log::debug!("server_file_contents_request called"); + ret = server_file_contents_request( + context, + conn_id, + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + have_clip_data_id, + clip_data_id, + ); + log::debug!("server_file_contents_request called, conn_id {}, stream_id: {}, list_index {}, dw_flags {}, n_position_low {}, n_position_high {}, cb_requested {}, have_clip_data_id {}, clip_data_id {}, return {}", conn_id, + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + have_clip_data_id, + clip_data_id, + ret + ); + } + ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + requested_data, + } => { + log::debug!("server_file_contents_response called"); + ret = server_file_contents_response( + context, + conn_id, + msg_flags, + stream_id, + requested_data, + ); + log::debug!("server_file_contents_response called, conn_id {}, msg_flags {}, stream_id {}, return {}", + conn_id, + msg_flags, + stream_id, + ret + ); + } + } + ret +} + +pub fn server_monitor_ready(context: &mut CliprdrClientContext, conn_id: i32) -> u32 { + unsafe { + let monitor_ready = CLIPRDR_MONITOR_READY { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: 0 as UINT16, + dataLen: 0 as UINT32, + }; + if let Some(f) = context.MonitorReady { + let ret = f(context, &monitor_ready); + ret as u32 + } else { + ERR_CODE_SERVER_FUNCTION_NONE + } + } +} + +pub fn server_format_list( + context: &mut CliprdrClientContext, + conn_id: i32, + format_list: Vec<(i32, String)>, +) -> u32 { + unsafe { + let num_formats = format_list.len() as UINT32; + let mut formats = format_list + .into_iter() + .map(|format| { + if format.1.is_empty() { + CLIPRDR_FORMAT { + formatId: format.0 as UINT32, + formatName: 0 as *mut _, + } + } else { + let n = match CString::new(format.1) { + Ok(n) => n, + Err(_) => CString::new("").unwrap(), + }; + CLIPRDR_FORMAT { + formatId: format.0 as UINT32, + formatName: n.into_raw(), + } + } + }) + .collect::>(); + + let format_list = CLIPRDR_FORMAT_LIST { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: 0 as UINT16, + dataLen: 0 as UINT32, + numFormats: num_formats, + formats: formats.as_mut_ptr(), + }; + + let ret = if let Some(f) = context.ServerFormatList { + f(context, &format_list) + } else { + ERR_CODE_SERVER_FUNCTION_NONE + }; + + for f in formats { + if !f.formatName.is_null() { + // retake pointer to free memory + let _ = CString::from_raw(f.formatName); + } + } + + ret as u32 + } +} + +pub fn server_format_list_response( + context: &mut CliprdrClientContext, + conn_id: i32, + msg_flags: i32, +) -> u32 { + unsafe { + let format_list_response = CLIPRDR_FORMAT_LIST_RESPONSE { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: msg_flags as UINT16, + dataLen: 0 as UINT32, + }; + + if let Some(f) = context.ServerFormatListResponse { + f(context, &format_list_response) + } else { + ERR_CODE_SERVER_FUNCTION_NONE + } + } +} + +pub fn server_format_data_request( + context: &mut CliprdrClientContext, + conn_id: i32, + requested_format_id: i32, +) -> u32 { + unsafe { + let format_data_request = CLIPRDR_FORMAT_DATA_REQUEST { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: 0 as UINT16, + dataLen: 0 as UINT32, + requestedFormatId: requested_format_id as UINT32, + }; + if let Some(f) = context.ServerFormatDataRequest { + f(context, &format_data_request) + } else { + ERR_CODE_SERVER_FUNCTION_NONE + } + } +} + +pub fn server_format_data_response( + context: &mut CliprdrClientContext, + conn_id: i32, + msg_flags: i32, + mut format_data: Vec, +) -> u32 { + unsafe { + let format_data_response = CLIPRDR_FORMAT_DATA_RESPONSE { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: msg_flags as UINT16, + dataLen: format_data.len() as UINT32, + requestedFormatData: format_data.as_mut_ptr(), + }; + if let Some(f) = context.ServerFormatDataResponse { + f(context, &format_data_response) + } else { + ERR_CODE_SERVER_FUNCTION_NONE + } + } +} + +pub fn server_file_contents_request( + context: &mut CliprdrClientContext, + conn_id: i32, + stream_id: i32, + list_index: i32, + dw_flags: i32, + n_position_low: i32, + n_position_high: i32, + cb_requested: i32, + have_clip_data_id: bool, + clip_data_id: i32, +) -> u32 { + unsafe { + let file_contents_request = CLIPRDR_FILE_CONTENTS_REQUEST { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: 0 as UINT16, + dataLen: 0 as UINT32, + streamId: stream_id as UINT32, + listIndex: list_index as UINT32, + dwFlags: dw_flags as UINT32, + nPositionLow: n_position_low as UINT32, + nPositionHigh: n_position_high as UINT32, + cbRequested: cb_requested as UINT32, + haveClipDataId: if have_clip_data_id { TRUE } else { FALSE }, + clipDataId: clip_data_id as UINT32, + }; + if let Some(f) = context.ServerFileContentsRequest { + f(context, &file_contents_request) + } else { + ERR_CODE_SERVER_FUNCTION_NONE + } + } +} + +pub fn server_file_contents_response( + context: &mut CliprdrClientContext, + conn_id: i32, + msg_flags: i32, + stream_id: i32, + mut requested_data: Vec, +) -> u32 { + unsafe { + let file_contents_response = CLIPRDR_FILE_CONTENTS_RESPONSE { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: msg_flags as UINT16, + dataLen: 4 + requested_data.len() as UINT32, + streamId: stream_id as UINT32, + cbRequested: requested_data.len() as UINT32, + requestedData: requested_data.as_mut_ptr(), + }; + if let Some(f) = context.ServerFileContentsResponse { + f(context, &file_contents_response) + } else { + ERR_CODE_SERVER_FUNCTION_NONE + } + } +} + +pub fn create_cliprdr_context( + enable_files: bool, + enable_others: bool, + response_wait_timeout_secs: u32, +) -> ResultType> { + Ok(CliprdrClientContext::create( + enable_files, + enable_others, + response_wait_timeout_secs, + Some(notify_callback), + 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 notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE) -> UINT { + log::debug!("notify_callback called"); + let data = unsafe { + let msg = &*msg; + let details = if msg.details.is_null() { + Ok("") + } else { + CStr::from_ptr(msg.details as _).to_str() + }; + match (CStr::from_ptr(msg.msg as _).to_str(), details) { + (Ok(m), Ok(d)) => { + let msgtype = format!( + "custom-{}-nocancel-nook-hasclose", + if msg.r#type == 0 { + "info" + } else if msg.r#type == 1 { + "warn" + } else { + "error" + } + ); + let title = "Clipboard"; + let text = if d.is_empty() { + m.to_string() + } else { + format!("{} {}", m, d) + }; + ClipboardFile::NotifyCallback { + r#type: msgtype, + title: title.to_string(), + text, + } + } + _ => { + log::error!("notify_callback: failed to convert msg"); + return ERR_CODE_INVALID_PARAMETER; + } + } + }; + // no need to handle result here + send_data(conn_id as _, data); + + 0 +} + +extern "C" fn client_format_list( + _context: *mut CliprdrClientContext, + clip_format_list: *const CLIPRDR_FORMAT_LIST, +) -> UINT { + let conn_id; + let mut format_list: Vec<(i32, String)> = Vec::new(); + unsafe { + let mut i = 0u32; + while i < (*clip_format_list).numFormats { + let format_data = &(*(*clip_format_list).formats.offset(i as isize)); + if format_data.formatName.is_null() { + format_list.push((format_data.formatId as i32, "".to_owned())); + } 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() + } + }; + format_list.push((format_data.formatId as i32, format_name)); + } + // log::debug!("format list item {}: format id: {}, format name: {}", i, format_data.formatId, &format_name); + i += 1; + } + conn_id = (*clip_format_list).connID as i32; + } + log::debug!( + "client_format_list called, client id: {}, format_list: {:?}", + conn_id, + &format_list + ); + let data = ClipboardFile::FormatList { format_list }; + // no need to handle result here + if conn_id == 0 { + // msg_channel is used for debug, VEC_MSG_CHANNEL cannot be inspected by the debugger. + let msg_channel = VEC_MSG_CHANNEL.read().unwrap(); + msg_channel + .iter() + .for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone()))); + } else { + send_data(conn_id, data); + } + + 0 +} + +extern "C" fn client_format_list_response( + _context: *mut CliprdrClientContext, + format_list_response: *const CLIPRDR_FORMAT_LIST_RESPONSE, +) -> UINT { + let conn_id; + let msg_flags; + unsafe { + conn_id = (*format_list_response).connID as i32; + msg_flags = (*format_list_response).msgFlags as i32; + } + log::debug!( + "client_format_list_response called, client id: {}, msg_flags: {}", + conn_id, + msg_flags + ); + let data = ClipboardFile::FormatListResponse { msg_flags }; + send_data(conn_id, data); + + 0 +} + +extern "C" fn client_format_data_request( + _context: *mut CliprdrClientContext, + format_data_request: *const CLIPRDR_FORMAT_DATA_REQUEST, +) -> UINT { + let conn_id; + let requested_format_id; + unsafe { + conn_id = (*format_data_request).connID as i32; + requested_format_id = (*format_data_request).requestedFormatId as i32; + } + let data = ClipboardFile::FormatDataRequest { + requested_format_id, + }; + log::debug!( + "client_format_data_request called, conn_id: {}, requested_format_id: {}", + conn_id, + requested_format_id + ); + // no need to handle result here + send_data(conn_id, data); + + 0 +} + +extern "C" fn client_format_data_response( + _context: *mut CliprdrClientContext, + format_data_response: *const CLIPRDR_FORMAT_DATA_RESPONSE, +) -> UINT { + let conn_id; + let msg_flags; + let format_data; + unsafe { + conn_id = (*format_data_response).connID as i32; + msg_flags = (*format_data_response).msgFlags as i32; + if (*format_data_response).requestedFormatData.is_null() { + format_data = Vec::new(); + } else { + format_data = std::slice::from_raw_parts( + (*format_data_response).requestedFormatData, + (*format_data_response).dataLen as usize, + ) + .to_vec(); + } + } + log::debug!( + "client_format_data_response called, client id: {}, msg_flags: {}", + conn_id, + msg_flags + ); + let data = ClipboardFile::FormatDataResponse { + msg_flags, + format_data, + }; + send_data(conn_id, data); + + 0 +} + +extern "C" fn client_file_contents_request( + _context: *mut CliprdrClientContext, + file_contents_request: *const CLIPRDR_FILE_CONTENTS_REQUEST, +) -> UINT { + // 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 conn_id; + let stream_id; + let list_index; + let dw_flags; + let n_position_low; + let n_position_high; + let cb_requested; + let have_clip_data_id; + let clip_data_id; + unsafe { + conn_id = (*file_contents_request).connID as i32; + stream_id = (*file_contents_request).streamId as i32; + list_index = (*file_contents_request).listIndex as i32; + dw_flags = (*file_contents_request).dwFlags as i32; + n_position_low = (*file_contents_request).nPositionLow as i32; + n_position_high = (*file_contents_request).nPositionHigh as i32; + cb_requested = (*file_contents_request).cbRequested as i32; + have_clip_data_id = (*file_contents_request).haveClipDataId == TRUE; + clip_data_id = (*file_contents_request).clipDataId as i32; + } + let data = ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + have_clip_data_id, + clip_data_id, + }; + log::debug!("client_file_contents_request called, data: {:?}", &data); + send_data(conn_id, data); + + 0 +} + +extern "C" fn client_file_contents_response( + _context: *mut CliprdrClientContext, + file_contents_response: *const CLIPRDR_FILE_CONTENTS_RESPONSE, +) -> UINT { + let conn_id; + let msg_flags; + let stream_id; + let requested_data; + unsafe { + conn_id = (*file_contents_response).connID as i32; + msg_flags = (*file_contents_response).msgFlags as i32; + stream_id = (*file_contents_response).streamId as i32; + if (*file_contents_response).requestedData.is_null() { + requested_data = Vec::new(); + } else { + requested_data = std::slice::from_raw_parts( + (*file_contents_response).requestedData, + (*file_contents_response).cbRequested as usize, + ) + .to_vec(); + } + } + let data = ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + requested_data, + }; + log::debug!( + "client_file_contents_response called, conn_id: {}, msg_flags: {}, stream_id: {}", + conn_id, + msg_flags, + stream_id + ); + send_data(conn_id, data); + + 0 +} diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index db7a4c5b7..ca5ffee3b 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -7,14 +7,14 @@ use std::{ }, }; -#[cfg(windows)] -use clipboard::{cliprdr::CliprdrClientContext, empty_clipboard, ContextSend}; +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +use clipboard::ContextSend; use crossbeam_queue::ArrayQueue; #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::sleep; #[cfg(not(target_os = "ios"))] use hbb_common::tokio::sync::mpsc::error::TryRecvError; -#[cfg(windows)] +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::{ allow_err, @@ -34,7 +34,7 @@ use hbb_common::{ sync::mpsc, time::{self, Duration, Instant, Interval}, }, - Stream, + ResultType, Stream, }; use scrap::CodecFormat; @@ -66,7 +66,7 @@ pub struct Remote { last_update_jobs_status: (Instant, HashMap), is_connected: bool, first_frame: bool, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] client_conn_id: i32, // used for file clipboard data_count: Arc, frame_count_map: Arc>>, @@ -101,7 +101,7 @@ impl Remote { last_update_jobs_status: (Instant::now(), Default::default()), is_connected: false, first_frame: false, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] client_conn_id: 0, data_count: Arc::new(AtomicUsize::new(0)), frame_count_map, @@ -146,24 +146,25 @@ impl Remote { } // just build for now - #[cfg(not(windows))] + #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] let (_tx_holder, rx) = mpsc::unbounded_channel(); - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] let mut rx_clip_client_lock = Arc::new(TokioMutex::new(rx)); - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] { let is_conn_not_default = self.handler.is_file_transfer() || self.handler.is_port_forward() || self.handler.is_rdp(); if !is_conn_not_default { + log::debug!("get cliprdr client for conn_id {}", self.client_conn_id); (self.client_conn_id, rx_clip_client_lock) = clipboard::get_rx_cliprdr_client(&self.handler.id); }; } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] let mut rx_clip_client = rx_clip_client_lock.lock().await; let mut status_timer = time::interval(Duration::new(1, 0)); @@ -209,8 +210,8 @@ impl Remote { } } _msg = rx_clip_client.recv() => { - #[cfg(windows)] - self.handle_local_clipboard_msg(&mut peer, _msg).await; + #[cfg(any(target_os="windows", target_os="linux", target_os = "macos"))] + self.handle_local_clipboard_msg(&mut peer, _msg).await; } _ = self.timer.tick() => { if last_recv_time.elapsed() >= SEC30 { @@ -277,17 +278,18 @@ impl Remote { Client::try_stop_clipboard(&self.handler.id); } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] if _set_disconnected_ok { let conn_id = self.client_conn_id; - ContextSend::proc(|context: &mut CliprdrClientContext| -> u32 { - empty_clipboard(context, conn_id); - 0 + log::debug!("try empty cliprdr for conn_id {}", conn_id); + let _ = ContextSend::proc(|context| -> ResultType<()> { + context.empty_clipboard(conn_id)?; + Ok(()) }); } } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] async fn handle_local_clipboard_msg( &self, peer: &mut crate::client::FramedStream, @@ -315,7 +317,12 @@ impl Remote { if stop { ContextSend::set_is_stopped(); } else { - allow_err!(peer.send(&crate::clipboard_file::clip_2_msg(clip)).await); + if let Err(e) = ContextSend::make_sure_enabled() { + log::error!("failed to restart clipboard context: {}", e); + }; + log::debug!("Send system clipboard message to remote"); + let msg = crate::clipboard_file::clip_2_msg(clip); + allow_err!(peer.send(&msg).await); } } }, @@ -1069,7 +1076,6 @@ impl Remote { } Some(login_response::Union::PeerInfo(pi)) => { self.handler.handle_peer_info(pi); - #[cfg(not(feature = "flutter"))] self.check_clipboard_file_context(); if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { #[cfg(feature = "flutter")] @@ -1140,7 +1146,7 @@ impl Remote { } } } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] Some(message::Union::Cliprdr(clip)) => { self.handle_cliprdr_msg(clip); } @@ -1700,9 +1706,14 @@ impl Remote { true } - #[cfg(not(feature = "flutter"))] fn check_clipboard_file_context(&self) { - #[cfg(windows)] + #[cfg(any( + target_os = "windows", + all( + feature = "unix-file-copy-paste", + any(target_os = "linux", target_os = "macos") + ) + ))] { let enabled = *self.handler.server_file_transfer_enabled.read().unwrap() && self.handler.lc.read().unwrap().enable_file_transfer.v; @@ -1710,8 +1721,9 @@ impl Remote { } } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] fn handle_cliprdr_msg(&self, clip: hbb_common::message_proto::Cliprdr) { + log::debug!("handling cliprdr msg from server peer"); #[cfg(feature = "flutter")] if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { if self.client_conn_id @@ -1721,18 +1733,26 @@ impl Remote { } } - if let Some(clip) = crate::clipboard_file::msg_2_clip(clip) { - let is_stopping_allowed = clip.is_stopping_allowed_from_peer(); - let file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_transfer.v; - let stop = is_stopping_allowed && !file_transfer_enabled; - log::debug!( + let Some(clip) = crate::clipboard_file::msg_2_clip(clip) else { + log::warn!("failed to decode cliprdr msg from server peer"); + return; + }; + + let is_stopping_allowed = clip.is_stopping_allowed_from_peer(); + let file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_transfer.v; + let stop = is_stopping_allowed && !file_transfer_enabled; + log::debug!( "Process clipboard message from server peer, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}", stop, is_stopping_allowed, file_transfer_enabled); - if !stop { - ContextSend::proc(|context: &mut CliprdrClientContext| -> u32 { - clipboard::server_clip_file(context, self.client_conn_id, clip) - }); - } + if !stop { + if let Err(e) = ContextSend::make_sure_enabled() { + log::error!("failed to restart clipboard context: {}", e); + }; + let _ = ContextSend::proc(|context| -> ResultType<()> { + context + .server_clip_file(self.client_conn_id, clip) + .map_err(|e| e.into()) + }); } } } diff --git a/src/common.rs b/src/common.rs index cf75fab1b..27e32c98a 100644 --- a/src/common.rs +++ b/src/common.rs @@ -11,9 +11,118 @@ pub enum GrabState { Exit, } -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(any( + target_os = "android", + target_os = "ios", + all(target_os = "linux", feature = "unix-file-copy-paste") +)))] pub use arboard::Clipboard as ClipboardContext; +#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] +static X11_CLIPBOARD: once_cell::sync::OnceCell = + once_cell::sync::OnceCell::new(); + +#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] +fn get_clipboard() -> Result<&'static x11_clipboard::Clipboard, String> { + X11_CLIPBOARD + .get_or_try_init(|| x11_clipboard::Clipboard::new()) + .map_err(|e| e.to_string()) +} + +#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] +pub struct ClipboardContext { + string_setter: x11rb::protocol::xproto::Atom, + string_getter: x11rb::protocol::xproto::Atom, + text_uri_list: x11rb::protocol::xproto::Atom, + + clip: x11rb::protocol::xproto::Atom, + prop: x11rb::protocol::xproto::Atom, +} + +#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] +fn parse_plain_uri_list(v: Vec) -> Result { + let text = String::from_utf8(v).map_err(|_| "ConversionFailure".to_owned())?; + let mut list = String::new(); + for line in text.lines() { + if !line.starts_with("file://") { + continue; + } + let decoded = percent_encoding::percent_decode_str(line) + .decode_utf8() + .map_err(|_| "ConversionFailure".to_owned())?; + list = list + "\n" + decoded.trim_start_matches("file://"); + } + list = list.trim().to_owned(); + Ok(list) +} + +#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] +impl ClipboardContext { + pub fn new() -> Result { + let clipboard = get_clipboard()?; + let string_getter = clipboard + .getter + .get_atom("UTF8_STRING") + .map_err(|e| e.to_string())?; + let string_setter = clipboard + .setter + .get_atom("UTF8_STRING") + .map_err(|e| e.to_string())?; + let text_uri_list = clipboard + .getter + .get_atom("text/uri-list") + .map_err(|e| e.to_string())?; + let prop = clipboard.getter.atoms.property; + let clip = clipboard.getter.atoms.clipboard; + Ok(Self { + text_uri_list, + string_setter, + string_getter, + clip, + prop, + }) + } + + pub fn get_text(&mut self) -> Result { + let clip = self.clip; + let prop = self.prop; + + const TIMEOUT: std::time::Duration = std::time::Duration::from_millis(120); + + let text_content = get_clipboard()? + .load(clip, self.string_getter, prop, TIMEOUT) + .map_err(|e| e.to_string())?; + + let file_urls = get_clipboard()?.load(clip, self.text_uri_list, prop, TIMEOUT); + + if file_urls.is_err() || file_urls.as_ref().unwrap().is_empty() { + log::trace!("clipboard get text, no file urls"); + return String::from_utf8(text_content).map_err(|e| e.to_string()); + } + + let file_urls = parse_plain_uri_list(file_urls.unwrap())?; + + let text_content = String::from_utf8(text_content).map_err(|e| e.to_string())?; + + if text_content.trim() == file_urls.trim() { + log::trace!("clipboard got text but polluted"); + return Err(String::from("polluted text")); + } + + Ok(text_content) + } + + pub fn set_text(&mut self, content: String) -> Result<(), String> { + let clip = self.clip; + + let value = content.clone().into_bytes(); + get_clipboard()? + .store(clip, self.string_setter, value) + .map_err(|e| e.to_string())?; + Ok(()) + } +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::compress::decompress; use hbb_common::{ diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 872d782d8..dc4a904d4 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1725,6 +1725,17 @@ pub fn main_use_texture_render() -> SyncReturn { } } +pub fn main_has_file_clipboard() -> SyncReturn { + let ret = cfg!(any( + target_os = "windows", + all( + feature = "unix-file-copy-paste", + any(target_os = "linux", target_os = "macos") + ) + )); + SyncReturn(ret) +} + pub fn cm_init() { #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::flutter::connection_manager::cm_init(); diff --git a/src/lib.rs b/src/lib.rs index beeb6e46a..8aa520579 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,7 @@ mod ui_session_interface; mod hbbs_http; -#[cfg(windows)] +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] pub mod clipboard_file; #[cfg(windows)] diff --git a/src/server/connection.rs b/src/server/connection.rs index 6c524fa49..4bbfd6211 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,5 +1,5 @@ use super::{input_service::*, *}; -#[cfg(windows)] +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use crate::clipboard_file::*; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::update_clipboard; @@ -192,7 +192,7 @@ pub struct Connection { // by peer disable_audio: bool, // by peer - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] enable_file_transfer: bool, // by peer audio_sender: Option, @@ -330,7 +330,7 @@ impl Connection { show_remote_cursor: false, ip: "".to_owned(), disable_audio: false, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] enable_file_transfer: false, disable_clipboard: false, disable_keyboard: false, @@ -479,7 +479,7 @@ impl Connection { ipc::Data::RawMessage(bytes) => { allow_err!(conn.stream.send_raw(bytes).await); } - #[cfg(windows)] + #[cfg(any(target_os="windows", target_os="linux"))] ipc::Data::ClipboardFile(clip) => { allow_err!(conn.stream.send(&clip_2_msg(clip)).await); } @@ -1032,7 +1032,7 @@ impl Connection { pi.hostname = DEVICE_NAME.lock().unwrap().clone(); pi.platform = "Android".into(); } - #[cfg(any(target_os = "linux", target_os = "windows"))] + #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] let mut platform_additions = serde_json::Map::new(); #[cfg(target_os = "linux")] { @@ -1062,7 +1062,18 @@ impl Connection { } } - #[cfg(any(target_os = "linux", target_os = "windows"))] + #[cfg(any( + target_os = "windows", + all( + any(target_os = "linux", target_os = "macos"), + feature = "unix-file-copy-paste" + ) + ))] + { + platform_additions.insert("has_file_clipboard".into(), json!(true)); + } + + #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] if !platform_additions.is_empty() { pi.platform_additions = serde_json::to_string(&platform_additions).unwrap_or("".into()); } @@ -1236,7 +1247,7 @@ impl Connection { self.audio && !self.disable_audio } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] fn file_transfer_enabled(&self) -> bool { self.file && self.enable_file_transfer } @@ -1806,8 +1817,9 @@ impl Connection { } Some(message::Union::Cliprdr(_clip)) => { - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] if let Some(clip) = msg_2_clip(_clip) { + log::debug!("got clipfile from client peer"); self.send_to_cm(ipc::Data::ClipboardFile(clip)) } } @@ -2389,7 +2401,7 @@ impl Connection { } } } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] if let Ok(q) = o.enable_file_transfer.enum_value() { if q != BoolOption::NotSet { self.enable_file_transfer = q == BoolOption::Yes; diff --git a/src/ui/cm.html b/src/ui/cm.html index aabaa0294..86641907e 100644 --- a/src/ui/cm.html +++ b/src/ui/cm.html @@ -18,4 +18,4 @@ - \ No newline at end of file + diff --git a/src/ui/header.tis b/src/ui/header.tis index 029dd2629..b0b21bc5e 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -196,7 +196,7 @@ class Header: Reactor.Component { {!cursor_embedded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • }
  • {svg_checkmark}{translate('Show quality monitor')}
  • {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} - {is_win && pi.platform == 'Windows' && file_enabled ?
  • {svg_checkmark}{translate('Allow file copy and paste')}
  • : ""} + {(is_win && pi.platform == "Windows") && file_enabled ?
  • {svg_checkmark}{translate('Allow file copy and paste')}
  • : ""} {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} {keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index da36646e6..a20997dc9 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -1,6 +1,6 @@ #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] use std::iter::FromIterator; -#[cfg(windows)] +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use std::sync::Arc; use std::{ collections::HashMap, @@ -15,11 +15,11 @@ use std::{ use crate::ipc::Connection; #[cfg(not(any(target_os = "ios")))] use crate::ipc::{self, Data}; -#[cfg(windows)] -use clipboard::{cliprdr::CliprdrClientContext, empty_clipboard, ContextSend}; +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +use clipboard::ContextSend; #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::tokio::sync::mpsc::unbounded_channel; -#[cfg(windows)] +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::{ allow_err, @@ -34,6 +34,7 @@ use hbb_common::{ sync::mpsc::{self, UnboundedSender}, task::spawn_blocking, }, + ResultType, }; use serde_derive::Serialize; @@ -69,9 +70,9 @@ struct IpcTaskRunner { close: bool, running: bool, conn_id: i32, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] file_transfer_enabled: bool, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] file_transfer_enabled_peer: bool, } @@ -164,7 +165,7 @@ impl ConnectionManager { } #[inline] - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] fn is_authorized(&self, id: i32) -> bool { CLIENTS .read() @@ -185,11 +186,11 @@ impl ConnectionManager { .map(|c| c.disconnected = true); } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] { - ContextSend::proc(|context: &mut CliprdrClientContext| -> u32 { - empty_clipboard(context, id); - 0 + let _ = ContextSend::proc(|context| -> ResultType<()> { + context.empty_clipboard(id)?; + Ok(()) }); } @@ -328,31 +329,35 @@ impl IpcTaskRunner { // for tmp use, without real conn id let mut write_jobs: Vec = Vec::new(); - #[cfg(windows)] + + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] let is_authorized = self.cm.is_authorized(self.conn_id); - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] let rx_clip1; let mut rx_clip; let _tx_clip; - #[cfg(windows)] - if is_authorized { + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + if self.conn_id > 0 && is_authorized { + log::debug!("Clipboard is enabled from client peer: type 1"); rx_clip1 = clipboard::get_rx_cliprdr_server(self.conn_id); rx_clip = rx_clip1.lock().await; } else { + log::debug!("Clipboard is enabled from client peer, actually useless: type 2"); let rx_clip2; (_tx_clip, rx_clip2) = unbounded_channel::(); rx_clip1 = Arc::new(TokioMutex::new(rx_clip2)); rx_clip = rx_clip1.lock().await; } - #[cfg(not(windows))] + #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] { (_tx_clip, rx_clip) = unbounded_channel::(); } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] { if ContextSend::is_enabled() { + log::debug!("Clipboard is enabled"); allow_err!( self.stream .send(&Data::ClipboardFile(clipboard::ClipboardFile::MonitorReady)) @@ -377,7 +382,7 @@ impl IpcTaskRunner { log::debug!("conn_id: {}", id); self.cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, from_switch,self.tx.clone()); self.conn_id = id; - #[cfg(windows)] + #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] { self.file_transfer_enabled = _file_transfer_enabled; } @@ -420,7 +425,7 @@ impl IpcTaskRunner { } #[cfg(not(any(target_os = "android", target_os = "ios")))] Data::ClipboardFile(_clip) => { - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os="linux", target_os = "macos"))] { let is_stopping_allowed = _clip.is_stopping_allowed_from_peer(); let is_clipboard_enabled = ContextSend::is_enabled(); @@ -437,14 +442,15 @@ impl IpcTaskRunner { continue; } let conn_id = self.conn_id; - ContextSend::proc(|context: &mut CliprdrClientContext| -> u32 { - clipboard::server_clip_file(context, conn_id, _clip) + let _ = ContextSend::proc(|context| -> ResultType<()> { + context.server_clip_file(conn_id, _clip) + .map_err(|e| e.into()) }); } } } Data::ClipboardFileEnabled(_enabled) => { - #[cfg(windows)] + #[cfg(any(target_os= "windows",target_os ="linux"))] { self.file_transfer_enabled_peer = _enabled; } @@ -477,12 +483,13 @@ impl IpcTaskRunner { } } Some(data) = self.rx.recv() => { - if self.stream.send(&data).await.is_err() { + if let Err(e) = self.stream.send(&data).await { + log::error!("error encountered in IPC task, quitting: {}", e); break; } match &data { Data::SwitchPermission{name: _name, enabled: _enabled} => { - #[cfg(windows)] + #[cfg(any(target_os="linux", target_os="windows"))] if _name == "file" { self.file_transfer_enabled = *_enabled; } @@ -497,7 +504,7 @@ impl IpcTaskRunner { }, clip_file = rx_clip.recv() => match clip_file { Some(_clip) => { - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os ="linux", target_os = "macos"))] { let is_stopping_allowed = _clip.is_stopping_allowed(); let is_clipboard_enabled = ContextSend::is_enabled(); @@ -536,9 +543,9 @@ impl IpcTaskRunner { close: true, running: true, conn_id: 0, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] file_transfer_enabled: false, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] file_transfer_enabled_peer: false, }; @@ -568,7 +575,13 @@ pub async fn start_ipc(cm: ConnectionManager) { } }); - #[cfg(target_os = "windows")] + #[cfg(any( + target_os = "windows", + all( + any(target_os = "linux", target_os = "macos"), + feature = "unix-file-copy-paste" + ), + ))] ContextSend::enable(Config::get_option("enable-file-transfer").is_empty()); match ipc::new_listener("_cm").await { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 7ce79f366..7cabdf790 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -1007,7 +1007,8 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver Session { #[tokio::main(flavor = "current_thread")] pub async fn io_loop(handler: Session, round: u32) { // It is ok to call this function multiple times. - #[cfg(target_os = "windows")] + #[cfg(any( + target_os = "windows", + all( + any(target_os = "linux", target_os = "macos"), + feature = "unix-file-copy-paste" + ) + ))] if !handler.is_file_transfer() && !handler.is_port_forward() { clipboard::ContextSend::enable(true); }