From 25cf36a9487009fc745e875b696206cc4aef9416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E7=95=A5?= Date: Fri, 8 Sep 2023 19:39:00 +0800 Subject: [PATCH] feat: add x11 clipboard support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 蔡略 --- Cargo.lock | 4 +- libs/clipboard/Cargo.toml | 3 +- libs/clipboard/build.rs | 44 +- libs/clipboard/src/lib.rs | 17 +- libs/clipboard/src/platform/fuse.rs | 831 ++++++++++++----------- libs/clipboard/src/platform/linux/mod.rs | 664 ++++++++++++++++-- libs/clipboard/src/platform/linux/x11.rs | 68 +- libs/clipboard/src/platform/mod.rs | 46 +- src/client/io_loop.rs | 30 +- src/lib.rs | 2 +- src/tray.rs | 4 +- src/ui_cm_interface.rs | 32 +- 12 files changed, 1233 insertions(+), 512 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 46dfdb2ac..d382f9353 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -946,6 +946,7 @@ dependencies = [ "thiserror", "utf16string", "x11-clipboard", + "x11rb 0.12.0", ] [[package]] @@ -7269,8 +7270,7 @@ dependencies = [ [[package]] name = "x11-clipboard" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41aca1115b1f195f21c541c5efb423470848d48143127d0f07f8b90c27440df" +source = "git+https://github.com/clslaid/x11-clipboard?branch=feat/store-batch#5fc2e73bc01ada3681159b34cf3ea8f0d14cd904" dependencies = [ "x11rb 0.12.0", ] diff --git a/libs/clipboard/Cargo.toml b/libs/clipboard/Cargo.toml index 72b182dbc..827503b4f 100644 --- a/libs/clipboard/Cargo.toml +++ b/libs/clipboard/Cargo.toml @@ -18,6 +18,7 @@ hbb_common = { path = "../hbb_common" } parking_lot = {version = "0.12"} [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] +x11rb = {version = "0.12", features = ["all-extensions"]} rand = {version = "0.8"} fuser = {version = "0.13"} libc = {version = "0.2"} @@ -25,4 +26,4 @@ rayon = {version = "1.7"} dashmap = "5.5" percent-encoding = "2.3" utf16string = "0.2" -x11-clipboard = "0.8" +x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch"} 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/lib.rs b/libs/clipboard/src/lib.rs index 229d0027f..15570d71f 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -1,5 +1,6 @@ +#[allow(dead_code)] use std::{ - ffi::{CStr, CString}, + path::PathBuf, sync::{Arc, Mutex, RwLock}, }; @@ -18,7 +19,9 @@ 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; @@ -33,7 +36,7 @@ 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) -> bool; + 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>; @@ -51,12 +54,16 @@ pub enum CliprdrError { ClipboardInternalError, #[error("cliprdr occupied")] ClipboardOccupied, - #[error("content not available")] - ContentNotAvailable, #[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 { description: String }, + Unknown(u32), } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/libs/clipboard/src/platform/fuse.rs b/libs/clipboard/src/platform/fuse.rs index a517543a5..3f0215bc7 100644 --- a/libs/clipboard/src/platform/fuse.rs +++ b/libs/clipboard/src/platform/fuse.rs @@ -21,11 +21,9 @@ use std::{ collections::{BTreeMap, HashMap}, ffi::OsString, - ops::DerefMut, path::{Path, PathBuf}, sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, - mpsc::{Receiver, Sender}, Arc, }, time::{Duration, SystemTime}, @@ -41,18 +39,14 @@ use parking_lot::{Condvar, Mutex, RwLock}; use rayon::prelude::*; use utf16string::WStr; -use crate::ClipboardFile; +use crate::{ClipboardFile, CliprdrError}; + +use super::LDAP_EPOCH_DELTA; /// block size for fuse, align to our asynchronic request size over FileContentsRequest. /// /// Question: will this hint users to read data in this size? const BLOCK_SIZE: u32 = 128 * 1024; -/// format ID for file descriptor -/// -/// # Note -/// this is a custom format ID, not a standard one -/// still should be pinned to this value in our custom implementation -const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334; /// read only permission const PERM_READ: u16 = 0o444; @@ -108,7 +102,7 @@ impl PendingRequest { pub fn set(&self, content: ClipboardFile) { let mut guard = self.content.lock(); - guard.insert(content); + let _ = guard.insert(content); self.cvar.notify_all(); } } @@ -149,6 +143,115 @@ impl CliprdrTxnDispatcher { } } +/// this is a proxy type +/// to avoid occupy FuseServer with &mut self +#[derive(Debug)] +pub(crate) struct FuseClient { + server: Arc, +} + +impl FuseClient { + pub fn new(server: Arc) -> Self { + Self { server } + } +} + +impl fuser::Filesystem for FuseClient { + fn init( + &mut self, + _req: &fuser::Request<'_>, + _config: &mut fuser::KernelConfig, + ) -> Result<(), libc::c_int> { + log::debug!("init fuse server"); + + self.server.init(); + Ok(()) + } + + fn lookup( + &mut self, + _req: &Request, + parent: u64, + name: &std::ffi::OsStr, + reply: fuser::ReplyEntry, + ) { + log::debug!("lookup: parent={}, name={:?}", parent, name); + self.server.look_up(parent, name, reply) + } + + fn opendir(&mut self, _req: &Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) { + log::debug!("opendir: ino={}, flags={}", ino, flags); + self.server.opendir(ino, flags, reply) + } + + fn readdir( + &mut self, + _req: &Request<'_>, + ino: u64, + fh: u64, + offset: i64, + reply: ReplyDirectory, + ) { + log::debug!("readdir: ino={}, fh={}, offset={}", ino, fh, offset); + self.server.readdir(ino, fh, offset, reply) + } + + fn releasedir( + &mut self, + _req: &Request<'_>, + ino: u64, + fh: u64, + flags: i32, + reply: fuser::ReplyEmpty, + ) { + log::debug!("releasedir: ino={}, fh={}, flags={}", ino, fh, flags); + self.server.releasedir(ino, fh, flags, reply) + } + + fn open(&mut self, _req: &Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) { + log::debug!("open: ino={}, flags={}", ino, flags); + self.server.open(ino, flags, reply) + } + + fn read( + &mut self, + _req: &Request<'_>, + ino: u64, + fh: u64, + offset: i64, + size: u32, + flags: i32, + lock_owner: Option, + reply: fuser::ReplyData, + ) { + log::debug!( + "read: ino={}, fh={}, offset={}, size={}, flags={}", + ino, + fh, + offset, + size, + flags + ); + self.server + .read(ino, fh, offset, size, flags, lock_owner, reply) + } + + fn release( + &mut self, + _req: &Request<'_>, + ino: u64, + fh: u64, + flags: i32, + lock_owner: Option, + flush: bool, + reply: fuser::ReplyEmpty, + ) { + log::debug!("release: ino={}, fh={}, flush={}", ino, fh, flush); + self.server + .release(ino, fh, flags, lock_owner, flush, reply) + } +} + /// fuse server /// provides a read-only file system #[derive(Debug)] @@ -173,9 +276,7 @@ pub(crate) struct FuseServer { impl FuseServer { /// create a new fuse server - pub fn new(timeout_secs: u64) -> Self { - let timeout = Duration::from_secs(timeout_secs as u64); - + pub fn new(timeout: Duration) -> Self { Self { status: RwLock::new(Status::Active), dispatcher: CliprdrTxnDispatcher::default(), @@ -186,6 +287,273 @@ impl FuseServer { } } + pub fn client(self: &Arc) -> FuseClient { + FuseClient::new(self.clone()) + } + + pub fn init(&self) { + let mut w_guard = self.files.write(); + if w_guard.is_empty() { + // create a root file + let root = FuseNode::new_root(); + w_guard.push(root); + } + } + + pub fn look_up(&self, 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.read(); + + 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"); + return; + } + + pub fn opendir(&self, ino: u64, flags: i32, reply: fuser::ReplyOpen) { + let files = self.files.read(); + 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; + } + if flags & libc::O_RDONLY == 0 { + log::error!("fuse: entry is read only"); + reply.error(libc::EACCES); + return; + } + + let fh = self.alloc_fd(); + entry.add_handler(fh); + reply.opened(fh, 0); + return; + } + + pub fn readdir(&self, ino: u64, fh: u64, offset: i64, mut reply: ReplyDirectory) { + let files = self.files.read(); + 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(); + return; + } + + pub fn releasedir(&self, ino: u64, fh: u64, _flags: i32, reply: fuser::ReplyEmpty) { + let files = self.files.read(); + 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(); + return; + } + + pub fn open(&self, ino: u64, flags: i32, reply: fuser::ReplyOpen) { + let files = self.files.read(); + 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 flags + if flags & libc::O_RDONLY == 0 { + reply.error(libc::EACCES); + log::error!("fuse: open: entry is read only"); + 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); + return; + } + + pub fn read( + &self, + ino: u64, + fh: u64, + offset: i64, + size: u32, + flags: i32, + _lock_owner: Option, + reply: fuser::ReplyData, + ) { + let files = self.files.read(); + 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; + } + // check flags + if flags & libc::O_RDONLY == 0 { + reply.error(libc::EACCES); + log::error!("fuse: read: entry is read only"); + 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()); + } + + pub fn release( + &self, + ino: u64, + fh: u64, + _flags: i32, + _lock_owner: Option, + _flush: bool, + reply: fuser::ReplyEmpty, + ) { + let files = self.files.read(); + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: release: entry not found"); + return; + }; + + if let Err(_) = entry.unregister_handler(fh) { + reply.error(libc::EBADF); + log::error!("fuse: release: entry has no such handler"); + return; + } + reply.ok(); + return; + } + /// gc filesystem fn gc_files(&self) { { @@ -198,21 +566,26 @@ impl FuseServer { // received update after fetching complete // should fetch again if *status == Status::Building { - *status == Status::GcComplete; + *status = Status::GcComplete; return; } *status = Status::Gc; } let mut old = self.files.write(); - old.par_iter_mut().fold(|| (), |_, f| f.gc()); + let _ = old.par_iter_mut().fold(|| (), |_, f| f.gc()); let mut status = self.status.write(); *status = Status::GcComplete; } /// fetch file list from remote - fn sync_file_system(&self, conn_id: i32) -> Result { + fn sync_file_system( + &self, + conn_id: i32, + file_group_format_id: i32, + _file_contents_format_id: i32, + ) -> Result { { let mut status = self.status.write(); if *status != Status::GcComplete { @@ -223,7 +596,7 @@ impl FuseServer { // request file list let request = ClipboardFile::FormatDataRequest { - requested_format_id: FILEDESCRIPTOR_FORMAT_ID, + requested_format_id: file_group_format_id, }; let rx = self.dispatcher.send(conn_id, request); let resp = rx.recv_timeout(self.timeout); @@ -232,38 +605,29 @@ impl FuseServer { msg_flags, format_data, }) => { - if msg_flags != 0 { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "clipboard FUSE server: failed to fetch file list", - )); + if msg_flags != 0x1 { + log::error!("clipboard FUSE server: received unexpected response flags"); + return Err(CliprdrError::ClipboardInternalError); } let descs = FileDescription::parse_file_descriptors(format_data, conn_id)?; descs } Ok(_) => { + log::error!("clipboard FUSE server: received unexpected response type"); // rollback status let mut status = self.status.write(); *status = Status::GcComplete; - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "clipboard FUSE server: invalid response to format data request", - )); + return Err(CliprdrError::ClipboardInternalError); } Err(e) => { + log::error!("clipboard FUSE server: failed to fetch file list, {:?}", e); // rollback status let mut status = self.status.write(); *status = Status::GcComplete; - return Err(std::io::Error::new( - std::io::ErrorKind::TimedOut, - format!( - "clipboard FUSE server: timeout when waiting for format data response, {}", - e - ), - )); + return Err(CliprdrError::ClipboardInternalError); } }; @@ -287,10 +651,9 @@ impl FuseServer { *status = Status::GcComplete; } - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "clipboard FUSE server: failed to fetch file size".to_string(), - )); + log::error!("clipboard FUSE server: failed to fetch file size"); + + return Err(CliprdrError::ClipboardInternalError); } // replace current file system @@ -385,9 +748,14 @@ impl FuseServer { /// /// movements: Building -> Active, Building -> GcComplete /// - pub fn update_files(&self, conn_id: i32) -> Result { + pub fn update_files( + &self, + conn_id: i32, + file_group_format_id: i32, + file_contents_format_id: i32, + ) -> Result { self.gc_files(); - self.sync_file_system(conn_id) + self.sync_file_system(conn_id, file_group_format_id, file_contents_format_id) } pub fn recv(&self, conn_id: i32, clip_file: ClipboardFile) { @@ -399,18 +767,6 @@ impl FuseServer { self.file_handle_counter.fetch_add(1, Ordering::Relaxed) } - /// find a file by name - fn find_inode_by_name(&self, name: &str) -> Option { - if name == "/" { - return Some(1); - } - let read = self.files.read(); - return read - .iter() - .position(|f| f.name == name) - .map(|i| i as Inode + 1); - } - // synchronize metadata with remote fn sync_node_size(&self, node: &mut FuseNode) -> Result<(), std::io::Error> { log::debug!( @@ -487,7 +843,7 @@ impl FuseServer { Ok(()) } - pub fn read_node( + fn read_node( &self, node: &FuseNode, offset: i64, @@ -561,303 +917,6 @@ impl FuseServer { } } -impl fuser::Filesystem for FuseServer { - fn init( - &mut self, - _req: &fuser::Request<'_>, - _config: &mut fuser::KernelConfig, - ) -> Result<(), libc::c_int> { - log::debug!("init fuse server"); - - let mut w_guard = self.files.write(); - if w_guard.is_empty() { - // create a root file - let root = FuseNode::new_root(); - w_guard.push(root); - } - Ok(()) - } - - fn lookup( - &mut self, - req: &Request, - parent: u64, - name: &std::ffi::OsStr, - reply: fuser::ReplyEntry, - ) { - log::debug!("lookup: parent={}, name={:?}", parent, name); - if name.len() > MAX_NAME_LEN { - log::debug!("fuse: name too long"); - reply.error(libc::ENAMETOOLONG); - return; - } - - let entries = self.files.read(); - - 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]; - if &child.name == &name.to_string_lossy() { - 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"); - return; - } - - fn opendir(&mut self, _req: &Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) { - log::debug!("opendir: ino={}, flags={}", ino, flags); - - let files = self.files.read(); - 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; - } - if flags & libc::O_RDONLY == 0 { - log::error!("fuse: entry is read only"); - reply.error(libc::EACCES); - return; - } - - let fh = self.alloc_fd(); - entry.add_handler(fh); - reply.opened(fh, 0); - return; - } - - fn readdir( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - offset: i64, - mut reply: ReplyDirectory, - ) { - log::debug!("readdir: ino={}, fh={}, offset={}", ino, fh, offset); - - let files = self.files.read(); - 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 mut 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(); - return; - } - - fn releasedir( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - flags: i32, - reply: fuser::ReplyEmpty, - ) { - let files = self.files.read(); - 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; - } - - entry.unregister_handler(fh); - reply.ok(); - return; - } - - fn open(&mut self, _req: &Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) { - let files = self.files.read(); - 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 flags - if flags & libc::O_RDONLY == 0 { - reply.error(libc::EACCES); - log::error!("fuse: open: entry is read only"); - 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); - return; - } - - fn read( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - offset: i64, - size: u32, - flags: i32, - lock_owner: Option, - reply: fuser::ReplyData, - ) { - let files = self.files.read(); - 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; - } - // check flags - if flags & libc::O_RDONLY == 0 { - reply.error(libc::EACCES); - log::error!("fuse: read: entry is read only"); - 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: &Request<'_>, - ino: u64, - fh: u64, - _flags: i32, - _lock_owner: Option, - _flush: bool, - reply: fuser::ReplyEmpty, - ) { - let files = self.files.read(); - let Some(entry) = files.get(ino as usize - 1) else { - reply.error(libc::ENOENT); - log::error!("fuse: release: entry not found"); - return; - }; - - if let Err(_) = entry.unregister_handler(fh) { - reply.error(libc::EBADF); - log::error!("fuse: release: entry has no such handler"); - return; - } - reply.ok(); - return; - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct FileDescription { pub conn_id: i32, @@ -874,26 +933,10 @@ pub struct FileDescription { } impl FileDescription { - pub fn new(name: &str, kind: FileType, size: u64, conn_id: i32) -> Self { - Self { - conn_id, - size, - name: PathBuf::from(name), - kind, - atime: SystemTime::now(), - last_modified: SystemTime::now(), - last_metadata_changed: SystemTime::now(), - creation_time: SystemTime::now(), - perm: PERM_READ, - } - } fn parse_file_descriptor( bytes: &mut Bytes, conn_id: i32, - ) -> Result { - // 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; + ) -> Result { let flags = bytes.get_u32_le(); // skip reserved 32 bytes bytes.advance(32); @@ -911,15 +954,16 @@ impl FileDescription { bytes.advance(520); let block = &block[..520]; - let wstr = WStr::from_utf16le(block) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + let wstr = WStr::from_utf16le(block).map_err(|e| { + log::error!("cannot convert file descriptor path: {:?}", e); + CliprdrError::ConversionFailure + })?; let valid_attributes = flags & 0x01 != 0; if !valid_attributes { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "only valid attributes are supported", - )); + return Err(CliprdrError::InvalidRequest { + description: "file description must have valid attributes".to_string(), + }); } // todo: check normal, hidden, system, readonly, archive... @@ -971,13 +1015,12 @@ impl FileDescription { pub fn parse_file_descriptors( file_descriptor_pdu: Vec, conn_id: i32, - ) -> Result, std::io::Error> { + ) -> Result, CliprdrError> { let mut data = Bytes::from(file_descriptor_pdu); if data.remaining() < 4 { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "invalid file descriptor pdu", - )); + return Err(CliprdrError::InvalidRequest { + description: "file descriptor request with infficient length".to_string(), + }); } let count = data.get_u32_le() as usize; @@ -986,10 +1029,9 @@ impl FileDescription { } if data.remaining() != 592 * count { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "invalid file descriptor pdu", - )); + return Err(CliprdrError::InvalidRequest { + description: "file descriptor request with invalid length".to_string(), + }); } let mut files = Vec::with_capacity(count); @@ -1073,10 +1115,6 @@ impl FuseNode { self.attributes.kind == FileType::File } - pub fn is_dir(&self) -> bool { - self.attributes.kind == FileType::Directory - } - pub fn marked(&self) -> bool { self.file_handlers.marked() } @@ -1107,7 +1145,7 @@ impl FuseNode { /// ## 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, std::io::Error> { + pub fn build_tree(files: Vec) -> Result, CliprdrError> { let mut tree_list = Vec::with_capacity(files.len() + 1); let root = Self::new_root(); tree_list.push(root); @@ -1125,12 +1163,7 @@ impl FuseNode { let FileDescription { name, .. } = file.clone(); let parent_inode = match name.parent() { - Some(parent) => sub_root_map.get(parent).cloned().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("parent path {} not found", parent.display()), - ) - })?, + Some(parent) => sub_root_map[parent], None => { // parent should be root FUSE_ROOT_ID @@ -1143,11 +1176,13 @@ impl FuseNode { sub_root_map.insert(name.clone(), inode); } - let base_name = name.file_name().ok_or_else(|| { - std::io::Error::new( + let f_name = name.clone(); + let base_name = f_name.file_name().ok_or_else(|| { + let err = std::io::Error::new( std::io::ErrorKind::InvalidData, format!("invalid file name {}", name.display()), - ) + ); + CliprdrError::FileError { path: name, err } })?; file.name = Path::new(base_name).to_path_buf(); @@ -1193,7 +1228,7 @@ pub struct InodeAttributes { kind: FileType, // not implemented - xattrs: BTreeMap, Vec>, + _xattrs: BTreeMap, Vec>, } impl InodeAttributes { @@ -1206,7 +1241,7 @@ impl InodeAttributes { last_metadata_changed: std::time::SystemTime::now(), creation_time: std::time::SystemTime::now(), kind, - xattrs: BTreeMap::new(), + _xattrs: BTreeMap::new(), } } @@ -1220,7 +1255,7 @@ impl InodeAttributes { last_accessed: SystemTime::now(), kind: desc.kind, - xattrs: BTreeMap::new(), + _xattrs: BTreeMap::new(), } } diff --git a/libs/clipboard/src/platform/linux/mod.rs b/libs/clipboard/src/platform/linux/mod.rs index dc812ada4..fa0da68cf 100644 --- a/libs/clipboard/src/platform/linux/mod.rs +++ b/libs/clipboard/src/platform/linux/mod.rs @@ -1,21 +1,54 @@ use std::{ + collections::HashSet, + fs::File, + os::unix::prelude::FileExt, path::{Path, PathBuf}, - time::Duration, + sync::{atomic::AtomicBool, Arc}, + time::{Duration, SystemTime}, }; -use crate::CliprdrError; +use dashmap::DashMap; +use fuser::MountOption; +use hbb_common::{ + bytes::{BufMut, BytesMut}, + log, +}; +use lazy_static::lazy_static; +use parking_lot::RwLock; +use utf16string::WString; -use super::fuse::{self, FuseServer}; +use crate::{send_data, ClipboardFile, CliprdrError, CliprdrServiceContext}; + +use super::{fuse::FuseServer, LDAP_EPOCH_DELTA}; #[cfg(not(feature = "wayland"))] pub mod x11; -trait SysClipboard { +// 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::new(); +} + +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 wait_file_list(&self) -> Result, CliprdrError>; fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>; } -fn get_sys_clipboard() -> Box { +fn get_sys_clipboard() -> Result, CliprdrError> { #[cfg(feature = "wayland")] { unimplemented!() @@ -23,7 +56,8 @@ fn get_sys_clipboard() -> Box { #[cfg(not(feature = "wayland"))] { pub use x11::*; - X11Clipboard::new() + let x11_clip = X11Clipboard::new()?; + Ok(Box::new(x11_clip) as Box<_>) } } @@ -72,18 +106,6 @@ fn parse_plain_uri_list(v: Vec) -> Result, CliprdrError> { parse_uri_list(&text) } -// helper parse function -// convert "x-special/gnome-copied-files", "x-special/x-kde-cutselection" and "x-special/nautilus-clipboard" data to a list of valid Paths -// # Note -// - none utf8 data will lead to error -fn parse_de_uri_list(v: Vec) -> Result, CliprdrError> { - let text = String::from_utf8(v).map_err(|_| CliprdrError::ConversionFailure)?; - let plain_list = text - .trim_start_matches("copy\n") - .trim_start_matches("cut\n"); - parse_uri_list(plain_list) -} - // helper parse function // convert 'text/uri-list' data to a list of valid Paths // # Note @@ -92,6 +114,9 @@ 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) } @@ -99,37 +124,590 @@ fn parse_uri_list(text: &str) -> Result, CliprdrError> { } #[derive(Debug)] -pub struct ClipboardContext { - pub stop: bool, - pub fuse_mount_point: PathBuf, - pub fuse_server: FuseServer, - pub file_list: HashSet, - pub clipboard: Clipboard, +struct LocalFile { + pub path: PathBuf, + pub handle: Option, - pub bkg_session: fuser::BackgroundSession, + pub name: String, + pub size: u64, + pub last_write_time: SystemTime, + pub is_dir: bool, + 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 = false; + let archive = false; + let normal = !is_dir; + let last_write_time = mt.modified().unwrap_or(SystemTime::UNIX_EPOCH); + + let name = path + .display() + .to_string() + .trim_start_matches('/') + .replace('/', "\\"); + + let handle = if is_dir { + None + } else { + let file = std::fs::File::open(path).map_err(|e| CliprdrError::FileError { + path: path.clone(), + err: e, + })?; + let reader = file; + Some(reader) + }; + + Ok(Self { + name, + path: path.clone(), + handle, + size, + last_write_time, + is_dir, + read_only, + system, + hidden, + 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 wstr: WString = WString::from(&self.name); + let name = wstr.as_bytes(); + + let flags = 0x4064; + + // 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); + // 16 bytes reserved + buf.put(&[0u8; 16][..]); + // 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() + } +} + +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) +} + +#[derive(Debug)] +enum FileContentsRequest { + Size { + stream_id: i32, + file_idx: usize, + }, + + Range { + stream_id: i32, + file_idx: usize, + offset: u64, + length: u64, + }, +} + +/// this is a proxy type for the clipboard context +pub struct CliprdrClient { + pub context: Arc, +} + +impl CliprdrServiceContext for CliprdrClient { + fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { + self.context.set_is_stopped() + } + + fn empty_clipboard(&mut self, conn_id: i32) -> Result { + self.context.empty_clipboard(conn_id) + } + + fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { + self.context.serve(conn_id, msg) + } +} + +pub struct ClipboardContext { + pub stop: AtomicBool, + pub fuse_mount_point: PathBuf, + fuse_server: Arc, + file_list: RwLock>, + clipboard: Arc, } impl ClipboardContext { - fn new(timeout: Duration, mount_path: PathBuf) -> Result { + pub fn new(timeout: Duration, mount_path: PathBuf) -> Result { // assert mount path exists - let mountpoint = mount_path - .canonicalize() - .map_err(|e| CliprdrError::Unknown { - description: format!("invalid mount point: {:?}", e), - })?; - let fuse_server = FuseServer::new(timeout); - let mnt_opts = [ - fuser::MountOption::FSName("clipboard".to_string()), - fuser::MountOption::NoAtime, - fuser::MountOption::RO, - fuser::MountOption::NoExec, - ]; - let bkg_session = fuser::spawn_mount2(fuse_server, mountpoint, &mnt_opts).map_err(|e| { - CliprdrError::Unknown { - description: format!("failed to mount fuse: {:?}", e), - } + let fuse_mount_point = mount_path.canonicalize().map_err(|e| { + log::error!("failed to canonicalize mount path: {:?}", e); + CliprdrError::CliprdrInit })?; - log::debug!("mounting clipboard fuse to {}", mount_path.display()); + let fuse_server = Arc::new(FuseServer::new(timeout)); + let clipboard = get_sys_clipboard()?; + let clipboard = Arc::from(clipboard); + let file_list = RwLock::new(vec![]); + + Ok(Self { + stop: AtomicBool::new(false), + fuse_mount_point, + fuse_server, + file_list, + clipboard, + }) + } + + pub fn client(self: Arc) -> CliprdrClient { + CliprdrClient { context: self } + } + + // mount and run fuse server, blocking + pub fn mount(&self) -> Result<(), CliprdrError> { + let mount_opts = [ + MountOption::FSName("rustdesk-cliprdr-fs".to_string()), + MountOption::RO, + MountOption::NoAtime, + ]; + let fuse_client = self.fuse_server.client(); + fuser::mount2(fuse_client, self.fuse_mount_point.clone(), &mount_opts).map_err(|e| { + log::error!("failed to mount fuse: {:?}", e); + CliprdrError::CliprdrInit + }) + } + + pub fn listen_clipboard(&self) -> Result<(), CliprdrError> { + while let Ok(v) = self.clipboard.wait_file_list() { + let filtered: Vec<_> = v + .into_iter() + .filter(|pb| !pb.starts_with(&self.fuse_mount_point)) + .collect(); + if filtered.is_empty() { + continue; + } + + // construct format list update and send + let data = ClipboardFile::FormatList { + format_list: vec![ + ( + FILEDESCRIPTOR_FORMAT_ID, + FILEDESCRIPTORW_FORMAT_NAME.to_string(), + ), + (FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME.to_string()), + ], + }; + + send_data(0, data) + } + Ok(()) + } + + fn send_format_list(&self, conn_id: i32) -> Result<(), CliprdrError> { + let data = self.clipboard.wait_file_list()?; + let filtered: Vec<_> = data + .into_iter() + .filter(|pb| !pb.starts_with(&self.fuse_mount_point)) + .collect(); + if filtered.is_empty() { + return Ok(()); + } + + let format_list = ClipboardFile::FormatList { + format_list: vec![ + ( + FILEDESCRIPTOR_FORMAT_ID, + FILEDESCRIPTORW_FORMAT_NAME.to_string(), + ), + (FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME.to_string()), + ], + }; + + send_data(conn_id, format_list); + Ok(()) + } + + fn send_file_list(&self, conn_id: i32) -> Result<(), CliprdrError> { + let data = self.clipboard.wait_file_list()?; + let filtered: Vec<_> = data + .into_iter() + .filter(|pb| !pb.starts_with(&self.fuse_mount_point)) + .collect(); + + let files = construct_file_list(filtered.as_slice())?; + + let mut data = BytesMut::with_capacity(4 + 592 * files.len()); + data.put_u32_le(filtered.len() as u32); + for file in files.iter() { + data.put(file.as_bin().as_slice()); + } + + { + let mut w_list = self.file_list.write(); + *w_list = files; + } + + let format_data = data.to_vec(); + + send_data( + conn_id, + ClipboardFile::FormatDataResponse { + msg_flags: 1, + format_data, + }, + ); + Ok(()) + } + + fn serve_file_contents( + &self, + conn_id: i32, + request: FileContentsRequest, + ) -> Result<(), CliprdrError> { + log::debug!("file contents (range) requested from conn: {}", conn_id); + let file_contents_req = match request { + FileContentsRequest::Size { + stream_id, + file_idx, + } => { + let file_list = self.file_list.read(); + 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.name); + + let size = file.size; + ClipboardFile::FileContentsResponse { + msg_flags: 0x1, + stream_id, + requested_data: size.to_le_bytes().to_vec(), + } + } + FileContentsRequest::Range { + stream_id, + file_idx, + offset, + length, + } => { + let file_list = self.file_list.read(); + 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.name); + + let Some(handle) = &file.handle 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!( + "request to read directory on index {} as file from conn: {}", + file_idx, conn_id + ), + }); + }; + + 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]; + + handle + .read_exact_at(&mut buf, offset) + .map_err(|e| CliprdrError::FileError { + path: file.path.clone(), + err: e, + })?; + + ClipboardFile::FileContentsResponse { + msg_flags: 0x1, + stream_id, + requested_data: buf, + } + } + }; + + send_data(conn_id, file_contents_req); + log::debug!("file contents sent to conn: {}", conn_id); + 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 set_is_stopped(&self) -> Result<(), CliprdrError> { + // do nothing + Ok(()) + } + + pub fn empty_clipboard(&self, conn_id: i32) -> Result { + // gc all files, the clipboard is going to shutdown + self.fuse_server + .update_files(conn_id, FILEDESCRIPTOR_FORMAT_ID, FILECONTENTS_FORMAT_ID) + } + + pub fn serve(&self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { + match msg { + ClipboardFile::NotifyCallback { .. } => { + unreachable!() + } + ClipboardFile::MonitorReady => { + log::debug!("server_monitor_ready called"); + + // ignore capabilities for now + + self.send_file_list(0)?; + Ok(()) + } + + ClipboardFile::FormatList { format_list } => { + // 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); + self.fuse_server + .update_files(conn_id, file_descriptor_id, file_contents_id)?; + Ok(()) + } + ClipboardFile::FormatListResponse { msg_flags } => { + if msg_flags != 0x1 { + self.send_format_list(conn_id) + } else { + Ok(()) + } + } + ClipboardFile::FormatDataRequest { + requested_format_id, + } => { + 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(requested_format_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 { .. } + | ClipboardFile::FileContentsResponse { .. } => { + self.fuse_server.recv(conn_id, msg); + Ok(()) + } + ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + .. + } => { + 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 resp_format_data_failure(conn_id: i32) { + let data = ClipboardFile::FormatDataResponse { + msg_flags: 0x2, + format_data: vec![], + }; + send_data(conn_id, data) +} diff --git a/libs/clipboard/src/platform/linux/x11.rs b/libs/clipboard/src/platform/linux/x11.rs index 73ec71ea9..151b4ae8a 100644 --- a/libs/clipboard/src/platform/linux/x11.rs +++ b/libs/clipboard/src/platform/linux/x11.rs @@ -1,7 +1,69 @@ -use super::SysClipboard; +use std::path::PathBuf; -pub struct X11Clipboard {} +use x11_clipboard::Clipboard; +use x11rb::protocol::xproto::Atom; + +use crate::CliprdrError; + +use super::{encode_path_to_uri, parse_plain_uri_list, SysClipboard}; + +pub struct X11Clipboard { + text_uri_list: Atom, + gnome_copied_files: Atom, + clipboard: Clipboard, +} + +impl X11Clipboard { + pub fn new() -> Result { + let clipboard = Clipboard::new().map_err(|_| CliprdrError::CliprdrInit)?; + 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)?; + Ok(Self { + text_uri_list, + gnome_copied_files, + clipboard, + }) + } + + fn load(&self, target: Atom) -> Result, CliprdrError> { + let clip = self.clipboard.setter.atoms.clipboard; + let prop = self.clipboard.setter.atoms.property; + self.clipboard + .load_wait(clip, target, prop) + .map_err(|_| CliprdrError::ConversionFailure) + } + + fn store_batch(&self, batch: Vec<(Atom, Vec)>) -> Result<(), CliprdrError> { + let clip = self.clipboard.setter.atoms.clipboard; + self.clipboard + .store_batch(clip, batch) + .map_err(|_| CliprdrError::ClipboardInternalError) + } +} impl SysClipboard for X11Clipboard { - todo!() + fn wait_file_list(&self) -> Result, CliprdrError> { + let v = self.load(self.text_uri_list)?; + // loading 'text/uri-list' should be enough? + parse_plain_uri_list(v) + } + + fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { + let uri_list: Vec = paths.iter().map(|pb| encode_path_to_uri(pb)).collect(); + let uri_list = uri_list.join("\n"); + let text_uri_list_data = uri_list.as_bytes().to_vec(); + let gnome_copied_files_data = vec!["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), + ]; + self.store_batch(batch) + .map_err(|_| CliprdrError::ClipboardInternalError) + } } diff --git a/libs/clipboard/src/platform/mod.rs b/libs/clipboard/src/platform/mod.rs index b2f5eb44a..14f686772 100644 --- a/libs/clipboard/src/platform/mod.rs +++ b/libs/clipboard/src/platform/mod.rs @@ -1,4 +1,4 @@ -use parking_lot::{Condvar, Mutex}; +use crate::{CliprdrError, CliprdrServiceContext}; #[cfg(target_os = "windows")] pub mod windows; @@ -19,8 +19,48 @@ pub mod linux; #[cfg(target_os = "linux")] pub fn create_cliprdr_context( enable_files: bool, - enable_others: bool, + _enable_others: bool, response_wait_timeout_secs: u32, ) -> crate::ResultType> { - unimplemented!() + use std::sync::Arc; + + if !enable_files { + return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); + } + + let timeout = std::time::Duration::from_secs(response_wait_timeout_secs as u64); + let mut tmp_path = std::env::temp_dir(); + tmp_path.push("rustdesk-cliprdr"); + let rd_mnt = tmp_path; + std::fs::create_dir(rd_mnt.clone())?; + let linux_ctx = Arc::new(linux::ClipboardContext::new(timeout, rd_mnt)?); + + let fuse_ctx = linux_ctx.clone(); + std::thread::spawn(move || fuse_ctx.mount()); + let clipboard_listen_ctx = linux_ctx.clone(); + std::thread::spawn(move || clipboard_listen_ctx.listen_clipboard()); + + Ok(Box::new(linux_ctx.client()) 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(()) + } +} + +// 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/src/client/io_loop.rs b/src/client/io_loop.rs index ab47f5698..46c0a319d 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -5,7 +5,7 @@ use std::sync::{ Arc, }; -#[cfg(windows)] +#[cfg(any(target_os = "windows", target_os = "linux"))] use clipboard::ContextSend; use crossbeam_queue::ArrayQueue; use hbb_common::config::{PeerConfig, TransferSerde}; @@ -20,7 +20,7 @@ use hbb_common::rendezvous_proto::ConnType; 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"))] use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::tokio::{ self, @@ -57,7 +57,7 @@ pub struct Remote { timer: Interval, last_update_jobs_status: (Instant, HashMap), first_frame: bool, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] client_conn_id: i32, // used for file clipboard data_count: Arc, frame_count: Arc, @@ -91,7 +91,7 @@ impl Remote { timer: time::interval(SEC30), last_update_jobs_status: (Instant::now(), Default::default()), first_frame: false, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] client_conn_id: 0, data_count: Arc::new(AtomicUsize::new(0)), frame_count, @@ -131,14 +131,14 @@ impl Remote { } // just build for now - #[cfg(not(windows))] + #[cfg(not(any(target_os = "windows", target_os = "linux")))] let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] let (_tx_holder, rx) = mpsc::unbounded_channel(); - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] let mut rx_clip_client_lock = Arc::new(TokioMutex::new(rx)); - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] { let is_conn_not_default = self.handler.is_file_transfer() || self.handler.is_port_forward() @@ -148,7 +148,7 @@ impl Remote { clipboard::get_rx_cliprdr_client(&self.handler.session_id); }; } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] let mut rx_clip_client = rx_clip_client_lock.lock().await; let mut status_timer = time::interval(Duration::new(1, 0)); @@ -204,7 +204,7 @@ impl Remote { } } _msg = rx_clip_client.recv() => { - #[cfg(windows)] + #[cfg(any(target_os="windows", target_os="linux"))] match _msg { Some(clip) => match clip { clipboard::ClipboardFile::NotifyCallback{r#type, title, text} => { @@ -278,11 +278,11 @@ impl Remote { #[cfg(not(any(target_os = "android", target_os = "ios")))] Client::try_stop_clipboard(&self.handler.session_id); - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] { let conn_id = self.client_conn_id; let _ = ContextSend::proc(|context| -> ResultType<()> { - context.empty_clipboard(conn_id); + context.empty_clipboard(conn_id)?; Ok(()) }); } @@ -1031,7 +1031,7 @@ impl Remote { } } } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] Some(message::Union::Cliprdr(clip)) => { self.handle_cliprdr_msg(clip); } @@ -1551,7 +1551,7 @@ impl Remote { #[cfg(not(feature = "flutter"))] fn check_clipboard_file_context(&self) { - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] { let enabled = *self.handler.server_file_transfer_enabled.read().unwrap() && self.handler.lc.read().unwrap().enable_file_transfer.v; @@ -1559,7 +1559,7 @@ impl Remote { } } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] fn handle_cliprdr_msg(&self, clip: hbb_common::message_proto::Cliprdr) { #[cfg(feature = "flutter")] if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { diff --git a/src/lib.rs b/src/lib.rs index 9648a6dc1..50ac4351e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,7 +59,7 @@ mod ui_session_interface; mod hbbs_http; -#[cfg(windows)] +#[cfg(any(target_os = "windows", target_os = "linux"))] pub mod clipboard_file; #[cfg(windows)] diff --git a/src/tray.rs b/src/tray.rs index 23b7e9b0c..ae5afe1e7 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -63,8 +63,8 @@ pub fn make_tray() -> hbb_common::ResultType<()> { let open_func = move || { #[cfg(not(feature = "flutter"))] { - crate::run_me::<&str>(vec![]).ok(); - return; + crate::run_me::<&str>(vec![]).ok(); + return; } #[cfg(target_os = "macos")] crate::platform::macos::handle_application_should_open_untitled_file(); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 2acf76eff..a8778ac1a 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"))] 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)] +#[cfg(any(target_os = "windows", target_os = "linux"))] 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"))] use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::{ allow_err, @@ -71,9 +71,9 @@ struct IpcTaskRunner { running: bool, authorized: bool, conn_id: i32, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] file_transfer_enabled: bool, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] file_transfer_enabled_peer: bool, } @@ -174,10 +174,10 @@ impl ConnectionManager { .map(|c| c.disconnected = true); } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] { let _ = ContextSend::proc(|context| -> ResultType<()> { - context.empty_clipboard(id); + context.empty_clipboard(id)?; Ok(()) }); } @@ -318,11 +318,11 @@ 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"))] let rx_clip1; let mut rx_clip; let _tx_clip; - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] if self.conn_id > 0 && self.authorized { rx_clip1 = clipboard::get_rx_cliprdr_server(self.conn_id); rx_clip = rx_clip1.lock().await; @@ -332,12 +332,12 @@ impl IpcTaskRunner { 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")))] { (_tx_clip, rx_clip) = unbounded_channel::(); } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] { if ContextSend::is_enabled() { allow_err!( @@ -402,7 +402,7 @@ impl IpcTaskRunner { } #[cfg(not(any(target_os = "android", target_os = "ios")))] Data::ClipboardFile(_clip) => { - #[cfg(windows)] + #[cfg(any(windows, linux))] { let is_stopping_allowed = _clip.is_stopping_allowed_from_peer(); let is_clipboard_enabled = ContextSend::is_enabled(); @@ -423,7 +423,7 @@ impl IpcTaskRunner { } } Data::ClipboardFileEnabled(_enabled) => { - #[cfg(windows)] + #[cfg(any(target_os= "windows",target_os ="linux"))] { self.file_transfer_enabled_peer = _enabled; } @@ -468,7 +468,7 @@ impl IpcTaskRunner { } clip_file = rx_clip.recv() => match clip_file { Some(_clip) => { - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os ="linux"))] { let is_stopping_allowed = _clip.is_stopping_allowed(); let is_clipboard_enabled = ContextSend::is_enabled(); @@ -505,9 +505,9 @@ impl IpcTaskRunner { running: true, authorized: false, conn_id: 0, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] file_transfer_enabled: false, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] file_transfer_enabled_peer: false, };