refact: file copy&paste, cross platform (no macOS) (#10671)

* feat: unix, file copy&paste

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact: unix file c&p, check peer version

Signed-off-by: fufesou <linlong1266@gmail.com>

* Update pubspec.yaml

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
This commit is contained in:
fufesou
2025-02-04 20:33:02 +08:00
committed by GitHub
parent a27fa43081
commit fbba8f0b34
42 changed files with 2026 additions and 1778 deletions

View File

@@ -34,7 +34,6 @@ parking_lot = {version = "0.12"}
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
rand = {version = "0.8", optional = true}
fuser = {version = "0.13", optional = true}
libc = {version = "0.2", optional = true}
dashmap = {version ="5.5", optional = true}
utf16string = {version = "0.2", optional = true}
@@ -44,6 +43,7 @@ once_cell = {version = "1.18", optional = true}
percent-encoding = {version ="2.3", optional = true}
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
fuser = {version = "0.15", default-features = false, optional = true}
[target.'cfg(target_os = "macos")'.dependencies]
cacao = {git="https://github.com/clslaid/cacao", branch = "feat/set-file-urls", optional = true}

View File

@@ -1,24 +1,23 @@
#[allow(dead_code)]
use std::{
path::PathBuf,
sync::{Arc, Mutex, RwLock},
};
use std::sync::{Arc, Mutex, RwLock};
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
use hbb_common::{allow_err, bail};
#[cfg(target_os = "windows")]
use hbb_common::ResultType;
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
use hbb_common::{allow_err, log};
use hbb_common::{
lazy_static,
tokio::sync::{
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
Mutex as TokioMutex,
},
ResultType,
};
use serde_derive::{Deserialize, Serialize};
use thiserror::Error;
#[cfg(target_os = "windows")]
pub mod context_send;
pub mod platform;
#[cfg(target_os = "windows")]
pub use context_send::*;
#[cfg(target_os = "windows")]
@@ -28,8 +27,10 @@ const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002;
#[cfg(target_os = "windows")]
const ERR_CODE_SEND_MSG: u32 = 0x00000003;
#[cfg(target_os = "windows")]
pub(crate) use platform::create_cliprdr_context;
// to-do: This trait may be removed, because unix file copy paste does not need it.
/// Ability to handle Clipboard File from remote rustdesk client
///
/// # Note
@@ -41,7 +42,6 @@ pub trait CliprdrServiceContext: Send + Sync {
fn set_is_stopped(&mut self) -> Result<(), CliprdrError>;
/// clear the content on clipboard
fn empty_clipboard(&mut self, conn_id: i32) -> Result<bool, CliprdrError>;
/// run as a server for clipboard RPC
fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError>;
}
@@ -63,9 +63,11 @@ pub enum CliprdrError {
#[error("failure to read clipboard")]
OpenClipboard,
#[error("failure to read file metadata or content")]
FileError { path: PathBuf, err: std::io::Error },
FileError { path: String, err: std::io::Error },
#[error("invalid request")]
InvalidRequest { description: String },
#[error("common request")]
CommonError { description: String },
#[error("unknown cliprdr error")]
Unknown(u32),
}
@@ -199,37 +201,53 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc<TokioMutex<UnboundedReceiver<C
}
}
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
pub fn remove_channel_by_conn_id(conn_id: i32) {
let mut lock = VEC_MSG_CHANNEL.write().unwrap();
if let Some(index) = lock.iter().position(|x| x.conn_id == conn_id) {
lock.remove(index);
}
}
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
#[inline]
fn send_data(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
pub fn send_data(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> {
#[cfg(target_os = "windows")]
return send_data_to_channel(conn_id, data);
#[cfg(not(target_os = "windows"))]
if conn_id == 0 {
send_data_to_all(data);
let _ = send_data_to_all(data);
Ok(())
} else {
send_data_to_channel(conn_id, data);
send_data_to_channel(conn_id, data)
}
}
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
#[inline]
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> {
if let Some(msg_channel) = VEC_MSG_CHANNEL
.read()
.unwrap()
.iter()
.find(|x| x.conn_id == conn_id)
{
msg_channel.sender.send(data)?;
Ok(())
msg_channel
.sender
.send(data)
.map_err(|e| CliprdrError::CommonError {
description: e.to_string(),
})
} else {
bail!("conn_id not found");
Err(CliprdrError::InvalidRequest {
description: "conn_id not found".to_string(),
})
}
}
#[inline]
#[cfg(target_os = "windows")]
pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) {
use hbb_common::log;
// Need more tests to see if it's necessary to handle the error.
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
if msg_channel.conn_id != conn_id {
allow_err!(msg_channel.sender.send(data.clone()));
@@ -237,14 +255,13 @@ pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) {
}
}
#[cfg(feature = "unix-file-copy-paste")]
#[inline]
fn send_data_to_all(data: ClipboardFile) -> ResultType<()> {
#[cfg(feature = "unix-file-copy-paste")]
fn send_data_to_all(data: ClipboardFile) {
// Need more tests to see if it's necessary to handle the error.
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
allow_err!(msg_channel.sender.send(data.clone()));
}
Ok(())
}
#[cfg(test)]

View File

@@ -1,6 +1,3 @@
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::{CliprdrError, CliprdrServiceContext};
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(target_os = "windows")]
@@ -16,76 +13,4 @@ pub fn create_cliprdr_context(
}
#[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<Box<dyn crate::CliprdrServiceContext>> {
#[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()?)?;
log::debug!("start cliprdr FUSE");
unix_ctx.run()?;
Ok(Box::new(unix_ctx) as Box<_>)
}
#[cfg(not(feature = "unix-file-copy-paste"))]
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
struct DummyCliprdrContext {}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl CliprdrServiceContext for DummyCliprdrContext {
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
Ok(())
}
fn empty_clipboard(&mut self, _conn_id: i32) -> Result<bool, CliprdrError> {
Ok(true)
}
fn server_clip_file(
&mut self,
_conn_id: i32,
_msg: crate::ClipboardFile,
) -> Result<(), crate::CliprdrError> {
Ok(())
}
}
#[cfg(feature = "unix-file-copy-paste")]
#[cfg(any(target_os = "linux", target_os = "macos"))]
// 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;

View File

@@ -0,0 +1,188 @@
use super::{FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_UNIX_MODE, LDAP_EPOCH_DELTA};
use crate::CliprdrError;
use hbb_common::{
bytes::{Buf, Bytes},
log,
};
use std::{
path::PathBuf,
time::{Duration, SystemTime},
};
use utf16string::WStr;
pub type Inode = u64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType {
File,
Directory,
// todo: support symlink
Symlink,
}
/// read only permission
pub const PERM_READ: u16 = 0o444;
/// read and write permission
pub const PERM_RW: u16 = 0o644;
/// only self can read and readonly
pub const PERM_SELF_RO: u16 = 0o400;
/// rwx
pub const PERM_RWX: u16 = 0o755;
/// max length of file name
pub const MAX_NAME_LEN: usize = 255;
#[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<FileDescription, CliprdrError> {
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 & FLAGS_FD_UNIX_MODE != 0;
let valid_attributes = flags & FLAGS_FD_ATTRIBUTES != 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
};
// to-do: use `let valid_size = flags & FLAGS_FD_SIZE != 0;`
// We use `true` to for compatibility with Windows.
// let valid_size = flags & FLAGS_FD_SIZE != 0;
let valid_size = true;
let size = if valid_size {
((file_size_high as u64) << 32) + file_size_low as u64
} else {
0
};
let valid_write_time = flags & FLAGS_FD_LAST_WRITE != 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<u8>,
conn_id: i32,
) -> Result<Vec<Self>, 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)
}
}

View File

@@ -31,33 +31,29 @@ use std::{
};
use fuser::{ReplyDirectory, FUSE_ROOT_ID};
use hbb_common::{
bytes::{Buf, Bytes},
log,
};
use hbb_common::log;
use parking_lot::{Condvar, Mutex};
use utf16string::WStr;
use crate::{send_data, ClipboardFile, CliprdrError};
use super::LDAP_EPOCH_DELTA;
use crate::{
platform::unix::{
filetype::{FileDescription, FileType, Inode, MAX_NAME_LEN, PERM_RWX},
BLOCK_SIZE,
},
send_data, ClipboardFile, CliprdrError,
};
/// 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;
impl From<FileType> for fuser::FileType {
fn from(value: FileType) -> Self {
match value {
FileType::File => Self::RegularFile,
FileType::Directory => Self::Directory,
FileType::Symlink => Self::Symlink,
}
}
}
/// fuse client
/// this is a proxy to the fuse server
@@ -150,9 +146,15 @@ impl fuser::Filesystem for FuseClient {
server.release(req, ino, fh, _flags, _lock_owner, _flush, reply)
}
fn getattr(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) {
fn getattr(
&mut self,
req: &fuser::Request<'_>,
ino: u64,
fh: Option<u64>,
reply: fuser::ReplyAttr,
) {
let mut server = self.server.lock();
server.getattr(req, ino, reply)
server.getattr(req, ino, fh, reply)
}
fn statfs(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyStatfs) {
@@ -247,7 +249,6 @@ impl fuser::Filesystem for FuseServer {
if parent_entry.attributes.kind != FileType::Directory {
log::error!("fuse: parent is not a directory");
reply.error(libc::ENOTDIR);
return;
}
@@ -480,7 +481,13 @@ impl fuser::Filesystem for FuseServer {
reply.ok();
}
fn getattr(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) {
fn getattr(
&mut self,
_req: &fuser::Request<'_>,
ino: u64,
_fh: Option<u64>,
reply: fuser::ReplyAttr,
) {
let files = &self.files;
let Some(entry) = files.get(ino as usize - 1) else {
reply.error(libc::ENOENT);
@@ -527,14 +534,6 @@ impl FuseServer {
size: u32,
) -> Result<Vec<u8>, 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
@@ -554,16 +553,14 @@ impl FuseServer {
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
);
send_data(node.conn_id, request.clone()).map_err(|e| {
log::error!("failed to send file list to channel: {:?}", e);
std::io::Error::new(std::io::ErrorKind::Other, e)
})?;
let mut retry_times = 0;
// to-do: more tests needed
loop {
let reply = self.rx.recv_timeout(self.timeout).map_err(|e| {
log::error!("failed to receive file list from channel: {:?}", e);
@@ -590,7 +587,10 @@ impl FuseServer {
));
}
send_data(node.conn_id, request.clone());
send_data(node.conn_id, request.clone()).map_err(|e| {
log::error!("failed to send file list to channel: {:?}", e);
std::io::Error::new(std::io::ErrorKind::Other, e)
})?;
continue;
}
return Ok(requested_data);
@@ -605,160 +605,6 @@ impl FuseServer {
}
}
}
#[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<FileDescription, CliprdrError> {
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<u8>,
conn_id: i32,
) -> Result<Vec<Self>, 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 {
@@ -881,7 +727,7 @@ impl FuseNode {
format!("invalid file name {}", file.name.display()),
);
CliprdrError::FileError {
path: file.name.clone(),
path: file.name.to_string_lossy().to_string(),
err,
}
})?;
@@ -902,26 +748,6 @@ impl FuseNode {
}
}
pub type Inode = u64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType {
File,
Directory,
// todo: support symlink
Symlink,
}
impl From<FileType> 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,
@@ -1064,8 +890,6 @@ impl FileHandles {
#[cfg(test)]
mod fuse_test {
use std::str::FromStr;
use super::*;
// todo: more tests needed!

View File

@@ -0,0 +1,225 @@
mod cs;
use super::filetype::FileDescription;
use crate::{ClipboardFile, CliprdrError};
use cs::FuseServer;
use fuser::MountOption;
use hbb_common::{config::APP_NAME, log};
use parking_lot::Mutex;
use std::{
path::PathBuf,
sync::{mpsc::Sender, Arc},
time::Duration,
};
lazy_static::lazy_static! {
static ref FUSE_MOUNT_POINT_CLIENT: Arc<String> = {
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-client");
// No need to run `canonicalize()` here.
Arc::new(mnt_path)
};
static ref FUSE_MOUNT_POINT_SERVER: Arc<String> = {
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-server");
// No need to run `canonicalize()` here.
Arc::new(mnt_path)
};
static ref FUSE_CONTEXT_CLIENT: Arc<Mutex<Option<FuseContext>>> = Arc::new(Mutex::new(None));
static ref FUSE_CONTEXT_SERVER: Arc<Mutex<Option<FuseContext>>> = Arc::new(Mutex::new(None));
}
static FUSE_TIMEOUT: Duration = Duration::from_secs(3);
pub fn get_exclude_paths(is_client: bool) -> Arc<String> {
if is_client {
FUSE_MOUNT_POINT_CLIENT.clone()
} else {
FUSE_MOUNT_POINT_SERVER.clone()
}
}
pub fn is_fuse_context_inited(is_client: bool) -> bool {
if is_client {
FUSE_CONTEXT_CLIENT.lock().is_some()
} else {
FUSE_CONTEXT_SERVER.lock().is_some()
}
}
pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
let mut fuse_context_lock = if is_client {
FUSE_CONTEXT_CLIENT.lock()
} else {
FUSE_CONTEXT_SERVER.lock()
};
if fuse_context_lock.is_some() {
return Ok(());
}
let mount_point = if is_client {
FUSE_MOUNT_POINT_CLIENT.clone()
} else {
FUSE_MOUNT_POINT_SERVER.clone()
};
let mount_point = std::path::PathBuf::from(&*mount_point);
let (server, tx) = FuseServer::new(FUSE_TIMEOUT);
let server = Arc::new(Mutex::new(server));
prepare_fuse_mount_point(&mount_point);
let mnt_opts = [
MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
MountOption::NoAtime,
MountOption::RO,
];
log::info!("mounting clipboard FUSE to {}", mount_point.display());
// to-do: ignore the error if the mount point is already mounted
// Because the sciter version uses separate processes as the controlling side.
let session = fuser::spawn_mount2(
FuseServer::client(server.clone()),
mount_point.clone(),
&mnt_opts,
)
.map_err(|e| {
log::error!("failed to mount cliprdr fuse: {:?}", e);
CliprdrError::CliprdrInit
})?;
let session = Mutex::new(Some(session));
let ctx = FuseContext {
server,
tx,
mount_point,
session,
conn_id: 0,
};
*fuse_context_lock = Some(ctx);
Ok(())
}
pub fn uninit_fuse_context(is_client: bool) {
uninit_fuse_context_(is_client)
}
pub fn format_data_response_to_urls(
is_client: bool,
format_data: Vec<u8>,
conn_id: i32,
) -> Result<Vec<String>, CliprdrError> {
let mut ctx = if is_client {
FUSE_CONTEXT_CLIENT.lock()
} else {
FUSE_CONTEXT_SERVER.lock()
};
ctx.as_mut()
.ok_or(CliprdrError::CliprdrInit)?
.format_data_response_to_urls(format_data, conn_id)
}
pub fn handle_file_content_response(
is_client: bool,
clip: ClipboardFile,
) -> Result<(), CliprdrError> {
// we don't know its corresponding request, no resend can be performed
let ctx = if is_client {
FUSE_CONTEXT_CLIENT.lock()
} else {
FUSE_CONTEXT_SERVER.lock()
};
ctx.as_ref()
.ok_or(CliprdrError::CliprdrInit)?
.tx
.send(clip)
.map_err(|e| {
log::error!("failed to send file contents response to fuse: {:?}", e);
CliprdrError::ClipboardInternalError
})?;
Ok(())
}
pub fn empty_local_files(is_client: bool, conn_id: i32) -> bool {
let ctx = if is_client {
FUSE_CONTEXT_CLIENT.lock()
} else {
FUSE_CONTEXT_SERVER.lock()
};
ctx.as_ref()
.map(|c| c.empty_local_files(conn_id))
.unwrap_or(false)
}
struct FuseContext {
server: Arc<Mutex<FuseServer>>,
tx: Sender<ClipboardFile>,
mount_point: PathBuf,
// stores fuse background session handle
session: Mutex<Option<fuser::BackgroundSession>>,
// Indicates the connection ID of that set the clipboard content
conn_id: i32,
}
// this function must be called after the main IPC is up
fn prepare_fuse_mount_point(mount_point: &PathBuf) {
use std::{
fs::{self, Permissions},
os::unix::prelude::PermissionsExt,
};
fs::create_dir(mount_point).ok();
fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok();
if let Err(e) = std::process::Command::new("umount")
.arg(mount_point)
.status()
{
log::warn!("umount {:?} may fail: {:?}", mount_point, e);
}
}
fn uninit_fuse_context_(is_client: bool) {
if is_client {
let _ = FUSE_CONTEXT_CLIENT.lock().take();
} else {
let _ = FUSE_CONTEXT_SERVER.lock().take();
}
}
impl Drop for FuseContext {
fn drop(&mut self) {
self.session.lock().take().map(|s| s.join());
log::info!("unmounting clipboard FUSE from {}", self.mount_point.display());
}
}
impl FuseContext {
pub fn empty_local_files(&self, conn_id: i32) -> bool {
if conn_id != 0 && self.conn_id != conn_id {
return false;
}
let mut fuse_guard = self.server.lock();
let _ = fuse_guard.load_file_list(vec![]);
true
}
pub fn format_data_response_to_urls(
&mut self,
format_data: Vec<u8>,
conn_id: i32,
) -> Result<Vec<String>, CliprdrError> {
let files = FileDescription::parse_file_descriptors(format_data, conn_id)?;
let paths = {
let mut fuse_guard = self.server.lock();
fuse_guard.load_file_list(files)?;
self.conn_id = conn_id;
fuse_guard.list_root()
};
let prefix = self.mount_point.clone();
Ok(paths
.into_iter()
.map(|p| prefix.join(p).to_string_lossy().to_string())
.collect())
}
}

View File

@@ -1,3 +1,15 @@
use super::{BLOCK_SIZE, LDAP_EPOCH_DELTA};
use crate::{
platform::unix::{
FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_PROGRESSUI, FLAGS_FD_SIZE,
FLAGS_FD_UNIX_MODE,
},
CliprdrError,
};
use hbb_common::{
bytes::{BufMut, BytesMut},
log,
};
use std::{
collections::HashSet,
fs::File,
@@ -7,32 +19,11 @@ use std::{
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 relative_root: PathBuf,
pub path: PathBuf,
pub handle: Option<BufReader<File>>,
@@ -51,9 +42,9 @@ pub(super) struct LocalFile {
}
impl LocalFile {
pub fn try_open(path: &Path) -> Result<Self, CliprdrError> {
pub fn try_open(relative_root: &Path, path: &Path) -> Result<Self, CliprdrError> {
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
path: path.clone(),
path: path.to_string_lossy().to_string(),
err: e,
})?;
let size = mt.len() as u64;
@@ -79,7 +70,8 @@ impl LocalFile {
Ok(Self {
name,
path: path.clone(),
relative_root: relative_root.to_path_buf(),
path: path.to_path_buf(),
handle,
offset,
size,
@@ -121,7 +113,12 @@ impl LocalFile {
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 path = self
.path
.strip_prefix(&self.relative_root)
.unwrap_or(&self.path)
.to_string_lossy()
.into_owned();
let wstr: WString<utf16string::LE> = WString::from(&path);
let name = wstr.as_bytes();
@@ -172,12 +169,12 @@ impl LocalFile {
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(),
path: self.path.to_string_lossy().to_string(),
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(),
path: self.path.to_string_lossy().to_string(),
err: e,
})?;
self.handle = Some(reader);
@@ -188,20 +185,25 @@ impl LocalFile {
pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> {
self.load_handle()?;
let handle = self.handle.as_mut()?;
let Some(handle) = self.handle.as_mut() else {
return Err(CliprdrError::FileError {
path: self.path.to_string_lossy().to_string(),
err: std::io::Error::new(std::io::ErrorKind::NotFound, "file handle not found"),
});
};
if offset != self.offset.load(Ordering::Relaxed) {
handle
.seek(std::io::SeekFrom::Start(offset))
.map_err(|e| CliprdrError::FileError {
path: self.path.clone(),
path: self.path.to_string_lossy().to_string(),
err: e,
})?;
}
handle
.read_exact(buf)
.map_err(|e| CliprdrError::FileError {
path: self.path.clone(),
path: self.path.to_string_lossy().to_string(),
err: e,
})?;
let new_offset = offset + (buf.len() as u64);
@@ -219,6 +221,7 @@ impl LocalFile {
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
fn constr_file_lst(
relative_root: &Path,
path: &Path,
file_list: &mut Vec<LocalFile>,
visited: &mut HashSet<PathBuf>,
@@ -227,22 +230,28 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
if visited.contains(path) {
return Ok(());
}
visited.insert(path.clone());
visited.insert(path.to_path_buf());
let local_file = LocalFile::try_open(path)?;
let local_file = LocalFile::try_open(relative_root, path)?;
file_list.push(local_file);
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
path: path.clone(),
path: path.to_string_lossy().to_string(),
err: e,
})?;
if mt.is_dir() {
let dir = std::fs::read_dir(path)?;
let dir = std::fs::read_dir(path).map_err(|e| CliprdrError::FileError {
path: path.to_string_lossy().to_string(),
err: e,
})?;
for entry in dir {
let entry = entry?;
let entry = entry.map_err(|e| CliprdrError::FileError {
path: path.to_string_lossy().to_string(),
err: e,
})?;
let path = entry.path();
constr_file_lst(&path, file_list, visited)?;
constr_file_lst(relative_root, &path, file_list, visited)?;
}
}
Ok(())
@@ -251,8 +260,18 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
let mut file_list = Vec::new();
let mut visited = HashSet::new();
let relative_root = paths
.first()
.ok_or(CliprdrError::InvalidRequest {
description: "empty file list".to_string(),
})?
.parent()
.ok_or(CliprdrError::InvalidRequest {
description: "empty parent".to_string(),
})?
.to_path_buf();
for path in paths {
constr_file_lst(path, &mut file_list, &mut visited)?;
constr_file_lst(&relative_root, path, &mut file_list, &mut visited)?;
}
Ok(file_list)
}
@@ -263,7 +282,7 @@ mod file_list_test {
use hbb_common::bytes::{BufMut, BytesMut};
use crate::{platform::fuse::FileDescription, CliprdrError};
use crate::{platform::unix::filetype::FileDescription, CliprdrError};
use super::LocalFile;
@@ -277,6 +296,7 @@ mod file_list_test {
#[inline]
fn generate_file(path: &str, name: &str, is_dir: bool) -> LocalFile {
LocalFile {
relative_root: PathBuf::from("."),
path: PathBuf::from(path),
handle: None,
name: name.to_string(),

View File

@@ -1,48 +1,38 @@
use std::{
path::{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;
mod filetype;
/// use FUSE for file pasting on these platforms
#[cfg(target_os = "linux")]
use self::url::{encode_path_to_uri, parse_plain_uri_list};
use super::fuse::FuseServer;
#[cfg(target_os = "linux")]
/// clipboard implementation of x11
pub mod x11;
#[cfg(target_os = "macos")]
/// clipboard implementation of macos
pub mod ns_clipboard;
pub mod fuse;
pub mod local_file;
pub mod serv_files;
#[cfg(target_os = "linux")]
pub mod url;
/// has valid file attributes
pub const FLAGS_FD_ATTRIBUTES: u32 = 0x04;
/// has valid file size
pub const FLAGS_FD_SIZE: u32 = 0x40;
/// has valid last write time
pub const FLAGS_FD_LAST_WRITE: u32 = 0x20;
/// show progress
pub const FLAGS_FD_PROGRESSUI: u32 = 0x4000;
/// transferred from unix, contains file mode
/// P.S. this flag is not used in windows
pub const FLAGS_FD_UNIX_MODE: u32 = 0x08;
// not actual format id, just a placeholder
const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334;
const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW";
pub const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334;
pub 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";
pub const FILECONTENTS_FORMAT_ID: i32 = 49267;
pub const FILECONTENTS_FORMAT_NAME: &str = "FileContents";
/// block size for fuse, align to our asynchronic request size over FileContentsRequest.
pub(crate) const BLOCK_SIZE: u32 = 4 * 1024 * 1024;
// 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;
lazy_static! {
static ref REMOTE_FORMAT_MAP: DashMap<i32, String> = DashMap::from_iter(
@@ -58,541 +48,7 @@ lazy_static! {
);
}
fn get_local_format(remote_id: i32) -> Option<String> {
#[inline]
pub fn get_local_format(remote_id: i32) -> Option<String> {
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<PathBuf>;
}
#[cfg(target_os = "linux")]
fn get_sys_clipboard(ignore_path: &Path) -> Result<Box<dyn SysClipboard>, CliprdrError> {
#[cfg(feature = "wayland")]
{
unimplemented!()
}
#[cfg(not(feature = "wayland"))]
{
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: &Path) -> Result<Box<dyn SysClipboard>, CliprdrError> {
use ns_clipboard::*;
let ns_pb = NsPasteboard::new(ignore_path)?;
Ok(Box::new(ns_pb) as Box<_>)
}
#[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<Option<fuser::BackgroundSession>>,
/// a sender of clipboard file contents pdu to fuse server
fuse_tx: Sender<ClipboardFile>,
fuse_server: Arc<Mutex<FuseServer>>,
clipboard: Arc<dyn SysClipboard>,
local_files: Mutex<Vec<LocalFile>>,
}
impl ClipboardContext {
pub fn new(timeout: Duration, mount_path: PathBuf) -> Result<Self, CliprdrError> {
// 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<PathBuf> = 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<PathBuf> = 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)?;
let file_descriptor_id = fmt_lst
.iter()
.find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME)
.map(|(id, _)| *id)?;
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<bool, CliprdrError> {
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<u8> {
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::<Vec<_>>()
);
let format_data = build_file_list_pdu(files);
send_data(
conn_id,
ClipboardFile::FormatDataResponse {
msg_flags: 1,
format_data,
},
);
Ok(())
}

View File

@@ -1,100 +0,0 @@
use std::{
collections::BTreeSet,
path::{Path, PathBuf},
};
use cacao::pasteboard::{Pasteboard, PasteboardName};
use hbb_common::log;
use parking_lot::Mutex;
use crate::{platform::unix::send_format_list, CliprdrError};
use super::SysClipboard;
#[inline]
fn wait_file_list() -> Option<Vec<PathBuf>> {
let pb = Pasteboard::named(PasteboardName::General);
pb.get_file_urls()
.ok()
.map(|v| v.into_iter().map(|nsurl| nsurl.pathbuf()).collect())
}
#[inline]
fn set_file_list(file_list: &[PathBuf]) -> Result<(), CliprdrError> {
let pb = Pasteboard::named(PasteboardName::General);
pb.set_files(file_list.to_vec())
.map_err(|_| CliprdrError::ClipboardInternalError)
}
pub struct NsPasteboard {
ignore_path: PathBuf,
former_file_list: Mutex<Vec<PathBuf>>,
}
impl NsPasteboard {
pub fn new(ignore_path: &Path) -> Result<Self, CliprdrError> {
Ok(Self {
ignore_path: ignore_path.to_owned(),
former_file_list: Mutex::new(vec![]),
})
}
}
impl SysClipboard for NsPasteboard {
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
*self.former_file_list.lock() = paths.to_vec();
set_file_list(paths)
}
fn start(&self) {
{
*self.former_file_list.lock() = vec![];
}
loop {
let file_list = match wait_file_list() {
Some(v) => v,
None => {
std::thread::sleep(std::time::Duration::from_millis(100));
continue;
}
};
let filtered = file_list
.into_iter()
.filter(|pb| !pb.starts_with(&self.ignore_path))
.collect::<Vec<_>>();
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::<BTreeSet<_>>();
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<PathBuf> {
self.former_file_list.lock().clone()
}
}

View File

@@ -0,0 +1,231 @@
use super::local_file::LocalFile;
use crate::{platform::unix::local_file::construct_file_list, ClipboardFile, CliprdrError};
use hbb_common::{
bytes::{BufMut, BytesMut},
log,
};
use parking_lot::Mutex;
use std::{path::PathBuf, sync::Arc};
lazy_static::lazy_static! {
// local files are cached, this value should not be changed when copying files
// Because `CliprdrFileContentsRequest` only contains the index of the file in the list.
// We need to keep the file list in the same order as the remote side.
// We may add a `FileId` field to `CliprdrFileContentsRequest` in the future.
static ref CLIP_FILES: Arc<Mutex<ClipFiles>> = Default::default();
}
#[derive(Debug)]
enum FileContentsRequest {
Size {
stream_id: i32,
file_idx: usize,
},
Range {
stream_id: i32,
file_idx: usize,
offset: u64,
length: u64,
},
}
#[derive(Default)]
struct ClipFiles {
files: Vec<String>,
file_list: Vec<LocalFile>,
files_pdu: Vec<u8>,
}
impl ClipFiles {
fn clear(&mut self) {
self.files.clear();
self.file_list.clear();
self.files_pdu.clear();
}
fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> {
let clipboard_paths = clipboard_files
.iter()
.map(|s| PathBuf::from(s))
.collect::<Vec<_>>();
self.file_list = construct_file_list(&clipboard_paths)?;
self.files = clipboard_files.to_vec();
Ok(())
}
fn build_file_list_pdu(&mut self) {
let mut data = BytesMut::with_capacity(4 + 592 * self.file_list.len());
data.put_u32_le(self.file_list.len() as u32);
for file in self.file_list.iter() {
data.put(file.as_bin().as_slice());
}
self.files_pdu = data.to_vec()
}
fn serve_file_contents(
&mut self,
conn_id: i32,
request: FileContentsRequest,
) -> Result<ClipboardFile, CliprdrError> {
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) = self.file_list.get(file_idx) else {
log::error!(
"invalid file index {} requested from conn: {}",
file_idx,
conn_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) = self.file_list.get_mut(file_idx) else {
log::error!(
"invalid file index {} requested from conn: {}",
file_idx,
conn_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);
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,
},
)
}
};
log::debug!("file contents sent to conn: {}", conn_id);
// hot reload next file
for next_file in self.file_list.iter_mut().skip(file_idx + 1) {
if !next_file.is_dir {
next_file.load_handle()?;
break;
}
}
Ok(file_contents_resp)
}
}
#[inline]
pub fn clear_files() {
CLIP_FILES.lock().clear();
}
pub fn read_file_contents(
conn_id: i32,
stream_id: i32,
list_index: i32,
dw_flags: i32,
n_position_low: i32,
n_position_high: i32,
cb_requested: i32,
) -> Result<ClipboardFile, CliprdrError> {
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 {
return Err(CliprdrError::InvalidRequest {
description: format!("got invalid FileContentsRequest, dw_flats: {dw_flags}"),
});
};
CLIP_FILES.lock().serve_file_contents(conn_id, fcr)
}
pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> {
let mut files_lock = CLIP_FILES.lock();
if files_lock.files == files {
return Ok(());
}
files_lock.sync_files(files)?;
Ok(files_lock.build_file_list_pdu())
}
pub fn get_file_list_pdu() -> Vec<u8> {
CLIP_FILES.lock().files_pdu.clone()
}

View File

@@ -1,75 +0,0 @@
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: &Path) -> io::Result<String> {
let encoded =
percent_encoding::percent_encode(path.to_str()?.as_bytes(), &ENCODE_SET).to_string();
format!("file://{}", encoded)
}
pub(super) fn parse_uri_to_path(encoded_uri: &str) -> Result<PathBuf, CliprdrError> {
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<u8>) -> Result<Vec<PathBuf>, 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<Vec<PathBuf>, 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).unwrap();
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]);
}
}

View File

@@ -1,171 +0,0 @@
use std::{
collections::BTreeSet,
path::{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<Clipboard> = 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<Vec<PathBuf>>,
}
impl X11Clipboard {
pub fn new(ignore_path: &Path) -> Result<Self, CliprdrError> {
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<Vec<u8>, 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<u8>)>) -> 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<Option<Vec<PathBuf>>, 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<String> = {
let mut v = Vec::new();
for path in paths {
v.push(encode_path_to_uri(path)?);
}
v
};
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::<Vec<_>>();
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::<BTreeSet<_>>();
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<PathBuf> {
self.former_file_list.lock().clone()
}
}

View File

@@ -614,6 +614,7 @@ fn ret_to_result(ret: u32) -> Result<(), CliprdrError> {
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) }
}

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk-portable-packer"
version = "1.3.7"
version = "1.3.8"
edition = "2021"
description = "RustDesk Remote Desktop"