Initial commit: RustDesk 1.4.7 macOS desktop port

- Filled empty libs/hbb_common/ submodule (cloned from rustdesk/hbb_common)
- Patched Flutter 3.44 / Dart 3.12 compatibility:
  * flutter/lib/generated_bridge.dart: asTypedList with cast<>, DartPort=Int64
  * flutter/lib/common.dart: DialogTheme->DialogThemeData, TabBarTheme->TabBarThemeData
  * flutter/pubspec.yaml: extended_text 14.0.0->15.0.2, google_fonts override 5.0.0
  * flutter/macos/Runner/Configs/Release.xcconfig: EXCLUDED_ARCHS=x86_64
- Build verified: cargo check + cargo build --features flutter + cargo build --release --features flutter
- Verified flutter build macos --debug and --release both produce working .app
- Verified .dmg installer (27MB arm64) created via hdiutil
- Build deps: Xcode 26.5, CocoaPods 1.16.2, Flutter 3.44, VCPKG arm64-osx
This commit is contained in:
xuwenwei
2026-06-08 21:42:20 +08:00
parent 3d8db09123
commit d3b6026bfd
840 changed files with 291780 additions and 1 deletions
+57
View File
@@ -0,0 +1,57 @@
[package]
name = "clipboard"
version = "0.1.0"
edition = "2021"
build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
cc = "1.0"
[features]
default = []
unix-file-copy-paste = [
"dep:x11rb",
"dep:x11-clipboard",
"dep:rand",
"dep:fuser",
"dep:libc",
"dep:dashmap",
"dep:percent-encoding",
"dep:utf16string",
"dep:once_cell",
"dep:cacao"
]
[dependencies]
thiserror = "1.0"
lazy_static = "1.4"
serde = "1.0"
serde_derive = "1.0"
hbb_common = { path = "../hbb_common" }
parking_lot = {version = "0.12"}
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
rand = {version = "0.8", optional = true}
libc = {version = "0.2", optional = true}
dashmap = {version ="5.5", optional = true}
utf16string = {version = "0.2", optional = true}
once_cell = {version = "1.18", optional = true}
[target.'cfg(target_os = "linux")'.dependencies]
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}
# Use `relax-void-encoding`, as that allows us to pass `c_void` instead of implementing `Encode` correctly for `&CGImageRef`
objc2 = { version = "0.5.1", features = ["relax-void-encoding"] }
objc2-foundation = { version = "0.2.0", features = ["NSArray", "NSString", "NSEnumerator", "NSGeometry", "NSProgress"] }
objc2-app-kit = { version = "0.2.0", features = ["NSPasteboard", "NSPasteboardItem", "NSImage", "NSFilePromiseProvider"] }
uuid = { version = "1.3", features = ["v4"] }
fsevent = "2.1.2"
dirs = "5.0"
xattr = "1.4.0"
+161
View File
@@ -0,0 +1,161 @@
# clipboard
Copy files and text through network.
Main lowlevel logic from [FreeRDP](https://github.com/FreeRDP/FreeRDP).
To enjoy file copy and paste feature on Linux/OSX,
please build with `unix-file-copy-paste` feature.
TODO: Move this lib to a separate project.
## How it works
Terminologies:
- cliprdr: this module
- local: the endpoint which initiates a file copy events
- remote: the endpoint which paste the file copied from `local`
The main algorithm of copying and pasting files is from
[Remote Desktop Protocol: Clipboard Virtual Channel Extension](https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-RDPECLIP/%5bMS-RDPECLIP%5d.pdf),
and could be concluded as:
0. local and remote notify each other that it's ready.
1. local subscribes/listening to the system's clipboard for file copy
2. local once got file copy event, notice the remote
3. remote confirms receive and try pulls the file list
4. local updates its file-list, the remote flushes pulled file list to the clipboard
5. remote OS or desktop manager initiates a paste, making other programs reading
clipboard files. Convert those reading requests to RPCs
- on Windows, all file reading will go through the stream file API
- on Linux/OSX, FUSE is used for converting reading requests to RPCs
- in case of local clipboard been transferred back
and leading to a dead loop,
all file copy event pointing at the FUSE directory will be ignored
6. finishing pasting all files one by one.
In a perspective of network data transferring:
```mermaid
sequenceDiagram
participant l as Local
participant r as Remote
note over l, r: Initialize
l ->> r: Monitor Ready
r ->> l: Monitor Ready
loop Get clipboard update
l ->> r: Format List (I got update)
r ->> l: Format List Response (notified)
r ->> l: Format Data Request (requests file list)
activate l
note left of l: Retrieve file list from system clipboard
l ->> r: Format Data Response (containing file list)
deactivate l
note over r: Update system clipboard with received file list
end
loop Some application requests copied files
note right of r: application reads file from x to x+y
note over r: the file is the a-th file on list
r ->> l: File Contents Request (read file a offset x size y)
activate l
note left of l: Find a-th file on list, read from x to x+y
l ->> r: File Contents Response (contents of file a offset x size y)
deactivate l
end
```
Note: In actual implementation, both sides could play send clipboard update
and request file contents.
There is no such limitation that only local can update clipboard
and copy files to remote.
## impl
### windows
![scene](./docs/assets/scene3.png)
![A1->B1](./docs/assets/win_A_B.png)
![B1->A1](./docs/assets/win_B_A.png)
The protocol was originally designed as an extension of the Windows RDP,
so the specific message packages fits windows well.
When starting cliprdr, a thread is spawned to create an invisible window
and to subscribe to OLE clipboard events.
The window's callback (see `cliprdr_proc` in `src/windows/wf_cliprdr.c`) was
set to handle a variety of events.
Detailed implementation is shown in pictures above.
### Linux/OSX
The Cliprdr Server implementation has mainly 3 parts:
- Clipboard Client
- Local File list
- FUSE server
#### Clipboard Client
The clipboard client has a thread polling for file urls on clipboard.
If the client found any updates of file urls,
after filtering out those pointing to our FUSE directory or duplicated,
send format list directly to remote.
The cliprdr server also uses clipboard client for setting clipboard,
or retrieve paths from system.
#### Local File List
The local file list is a temporary list of file metadata.
When receiving file contents PDU from peer, the server picks
out the file requested and open it for reading if necessary.
Also when receiving Format Data Request PDU from remote asking for file list,
the local file list should be rebuilt from file list retrieved from Clipboard Client.
Some caching and preloading could be done on it since applications are likely to read
on the list sequentially.
#### FUSE server
The FUSE server could convert POSIX file reading request to File Contents
Request/Response RPCs.
When received file list from remote,
the FUSE server will figure out the file system tree and rearrange its content.
#### Groceries
- The protocol was originally implemented for windows,
so paths in PDU will all be converted to DOS formats in UTF-16 LE encoding,
and datetimes will be converted to LDAP timestamp instead of
unix timestamp
```text
UNIX
/usr/bin/rustdesk
->
DOS
\usr\bin\rustdesk
```
- To better fit for preserving permissions on unix-like platforms,
a reserved area of FileDescriptor PDU
- you may notice
the mountpoint is still occupied after the application quits.
That's because the FUSE server was not mounted with `AUTO_UNMOUNT`.
- It's hard to implement gressful shutdown for a multi-processed program
- `AUTO_UNMOUNT` was not enabled by default and requires enable
`user_allow_other` in configure. Letting users edit such global
configuration to use this feature might not be a good idea.
- use [`umount()`](https://man7.org/linux/man-pages/man2/umount.2.html)
syscall to unmount will also require that option.
- we currently directly call [`umount`](https://man7.org/linux/man-pages/man8/umount.8.html)
program to unmount dangling FUSE server. It worked perfectly for now.
+35
View File
@@ -0,0 +1,35 @@
#[cfg(target_os = "windows")]
fn build_c_impl() {
let mut build = cc::Build::new();
build.file("src/windows/wf_cliprdr.c");
{
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());
}
build.compile("mycliprdr");
}
println!("cargo:rerun-if-changed=src/windows/wf_cliprdr.c");
}
fn main() {
#[cfg(target_os = "windows")]
build_c_impl();
}
+247
View File
@@ -0,0 +1,247 @@
#ifndef WF_CLIPRDR_H__
#define WF_CLIPRDR_H__
#ifdef __cplusplus
extern "C"
{
#endif
typedef signed char INT8, *PINT8;
typedef signed short INT16, *PINT16;
typedef signed int INT32, *PINT32;
typedef unsigned char UINT8, *PUINT8;
typedef unsigned short UINT16, *PUINT16;
typedef unsigned int UINT32, *PUINT32;
typedef unsigned int UINT;
typedef int BOOL;
typedef unsigned char BYTE;
/* Clipboard Messages */
#define DEFINE_CLIPRDR_HEADER_COMMON() \
UINT32 connID; \
UINT16 msgType; \
UINT16 msgFlags; \
UINT32 dataLen
struct _CLIPRDR_HEADER
{
DEFINE_CLIPRDR_HEADER_COMMON();
};
typedef struct _CLIPRDR_HEADER CLIPRDR_HEADER;
struct _CLIPRDR_CAPABILITY_SET
{
UINT16 capabilitySetType;
UINT16 capabilitySetLength;
};
typedef struct _CLIPRDR_CAPABILITY_SET CLIPRDR_CAPABILITY_SET;
struct _CLIPRDR_GENERAL_CAPABILITY_SET
{
UINT16 capabilitySetType;
UINT16 capabilitySetLength;
UINT32 version;
UINT32 generalFlags;
};
typedef struct _CLIPRDR_GENERAL_CAPABILITY_SET CLIPRDR_GENERAL_CAPABILITY_SET;
struct _CLIPRDR_CAPABILITIES
{
DEFINE_CLIPRDR_HEADER_COMMON();
UINT32 cCapabilitiesSets;
CLIPRDR_CAPABILITY_SET *capabilitySets;
};
typedef struct _CLIPRDR_CAPABILITIES CLIPRDR_CAPABILITIES;
struct _CLIPRDR_MONITOR_READY
{
DEFINE_CLIPRDR_HEADER_COMMON();
};
typedef struct _CLIPRDR_MONITOR_READY CLIPRDR_MONITOR_READY;
struct _CLIPRDR_TEMP_DIRECTORY
{
DEFINE_CLIPRDR_HEADER_COMMON();
char szTempDir[520];
};
typedef struct _CLIPRDR_TEMP_DIRECTORY CLIPRDR_TEMP_DIRECTORY;
struct _CLIPRDR_FORMAT
{
UINT32 formatId;
char *formatName;
};
typedef struct _CLIPRDR_FORMAT CLIPRDR_FORMAT;
struct _CLIPRDR_FORMAT_LIST
{
DEFINE_CLIPRDR_HEADER_COMMON();
UINT32 numFormats;
CLIPRDR_FORMAT *formats;
};
typedef struct _CLIPRDR_FORMAT_LIST CLIPRDR_FORMAT_LIST;
struct _CLIPRDR_FORMAT_LIST_RESPONSE
{
DEFINE_CLIPRDR_HEADER_COMMON();
};
typedef struct _CLIPRDR_FORMAT_LIST_RESPONSE CLIPRDR_FORMAT_LIST_RESPONSE;
struct _CLIPRDR_LOCK_CLIPBOARD_DATA
{
DEFINE_CLIPRDR_HEADER_COMMON();
UINT32 clipDataId;
};
typedef struct _CLIPRDR_LOCK_CLIPBOARD_DATA CLIPRDR_LOCK_CLIPBOARD_DATA;
struct _CLIPRDR_UNLOCK_CLIPBOARD_DATA
{
DEFINE_CLIPRDR_HEADER_COMMON();
UINT32 clipDataId;
};
typedef struct _CLIPRDR_UNLOCK_CLIPBOARD_DATA CLIPRDR_UNLOCK_CLIPBOARD_DATA;
struct _CLIPRDR_FORMAT_DATA_REQUEST
{
DEFINE_CLIPRDR_HEADER_COMMON();
UINT32 requestedFormatId;
};
typedef struct _CLIPRDR_FORMAT_DATA_REQUEST CLIPRDR_FORMAT_DATA_REQUEST;
struct _CLIPRDR_FORMAT_DATA_RESPONSE
{
DEFINE_CLIPRDR_HEADER_COMMON();
const BYTE *requestedFormatData;
};
typedef struct _CLIPRDR_FORMAT_DATA_RESPONSE CLIPRDR_FORMAT_DATA_RESPONSE;
struct _CLIPRDR_FILE_CONTENTS_REQUEST
{
DEFINE_CLIPRDR_HEADER_COMMON();
UINT32 streamId;
UINT32 listIndex;
UINT32 dwFlags;
UINT32 nPositionLow;
UINT32 nPositionHigh;
UINT32 cbRequested;
BOOL haveClipDataId;
UINT32 clipDataId;
};
typedef struct _CLIPRDR_FILE_CONTENTS_REQUEST CLIPRDR_FILE_CONTENTS_REQUEST;
struct _CLIPRDR_FILE_CONTENTS_RESPONSE
{
DEFINE_CLIPRDR_HEADER_COMMON();
UINT32 streamId;
UINT32 cbRequested;
const BYTE *requestedData;
};
typedef struct _CLIPRDR_FILE_CONTENTS_RESPONSE CLIPRDR_FILE_CONTENTS_RESPONSE;
typedef struct _cliprdr_client_context CliprdrClientContext;
struct _NOTIFICATION_MESSAGE
{
// 0 - info, 1 - warning, 2 - error
UINT32 type;
char *msg;
char *details;
};
typedef struct _NOTIFICATION_MESSAGE NOTIFICATION_MESSAGE;
typedef UINT (*pcCliprdrServerCapabilities)(CliprdrClientContext *context,
const CLIPRDR_CAPABILITIES *capabilities);
typedef UINT (*pcCliprdrClientCapabilities)(CliprdrClientContext *context,
const CLIPRDR_CAPABILITIES *capabilities);
typedef UINT (*pcCliprdrMonitorReady)(CliprdrClientContext *context,
const CLIPRDR_MONITOR_READY *monitorReady);
typedef UINT (*pcCliprdrTempDirectory)(CliprdrClientContext *context,
const CLIPRDR_TEMP_DIRECTORY *tempDirectory);
typedef UINT (*pcNotifyClipboardMsg)(UINT32 connID, const NOTIFICATION_MESSAGE *msg);
typedef UINT (*pcHandleClipboardFiles)(UINT32 connID, size_t nFiles, WCHAR **fileNames);
typedef UINT (*pcCliprdrClientFormatList)(CliprdrClientContext *context,
const CLIPRDR_FORMAT_LIST *formatList);
typedef UINT (*pcCliprdrServerFormatList)(CliprdrClientContext *context,
const CLIPRDR_FORMAT_LIST *formatList);
typedef UINT (*pcCliprdrClientFormatListResponse)(
CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse);
typedef UINT (*pcCliprdrServerFormatListResponse)(
CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse);
typedef UINT (*pcCliprdrClientLockClipboardData)(
CliprdrClientContext *context, const CLIPRDR_LOCK_CLIPBOARD_DATA *lockClipboardData);
typedef UINT (*pcCliprdrServerLockClipboardData)(
CliprdrClientContext *context, const CLIPRDR_LOCK_CLIPBOARD_DATA *lockClipboardData);
typedef UINT (*pcCliprdrClientUnlockClipboardData)(
CliprdrClientContext *context, const CLIPRDR_UNLOCK_CLIPBOARD_DATA *unlockClipboardData);
typedef UINT (*pcCliprdrServerUnlockClipboardData)(
CliprdrClientContext *context, const CLIPRDR_UNLOCK_CLIPBOARD_DATA *unlockClipboardData);
typedef UINT (*pcCliprdrClientFormatDataRequest)(
CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest);
typedef UINT (*pcCliprdrServerFormatDataRequest)(
CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest);
typedef UINT (*pcCliprdrClientFormatDataResponse)(
CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse);
typedef UINT (*pcCliprdrServerFormatDataResponse)(
CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse);
typedef UINT (*pcCliprdrClientFileContentsRequest)(
CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest);
typedef UINT (*pcCliprdrServerFileContentsRequest)(
CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest);
typedef UINT (*pcCliprdrClientFileContentsResponse)(
CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse);
typedef UINT (*pcCliprdrServerFileContentsResponse)(
CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse);
// TODO: hide more members of clipboard context
struct _cliprdr_client_context
{
void *Custom;
BOOL EnableFiles;
BOOL EnableOthers;
BOOL IsStopped;
UINT32 ResponseWaitTimeoutSecs;
pcCliprdrServerCapabilities ServerCapabilities;
pcCliprdrClientCapabilities ClientCapabilities;
pcCliprdrMonitorReady MonitorReady;
pcCliprdrTempDirectory TempDirectory;
pcNotifyClipboardMsg NotifyClipboardMsg;
pcHandleClipboardFiles HandleClipboardFiles;
pcCliprdrClientFormatList ClientFormatList;
pcCliprdrServerFormatList ServerFormatList;
pcCliprdrClientFormatListResponse ClientFormatListResponse;
pcCliprdrServerFormatListResponse ServerFormatListResponse;
pcCliprdrClientLockClipboardData ClientLockClipboardData;
pcCliprdrServerLockClipboardData ServerLockClipboardData;
pcCliprdrClientUnlockClipboardData ClientUnlockClipboardData;
pcCliprdrServerUnlockClipboardData ServerUnlockClipboardData;
pcCliprdrClientFormatDataRequest ClientFormatDataRequest;
pcCliprdrServerFormatDataRequest ServerFormatDataRequest;
pcCliprdrClientFormatDataResponse ClientFormatDataResponse;
pcCliprdrServerFormatDataResponse ServerFormatDataResponse;
pcCliprdrClientFileContentsRequest ClientFileContentsRequest;
pcCliprdrServerFileContentsRequest ServerFileContentsRequest;
pcCliprdrClientFileContentsResponse ClientFileContentsResponse;
pcCliprdrServerFileContentsResponse ServerFileContentsResponse;
UINT32 LastRequestedFormatId;
};
#ifdef __cplusplus
}
#endif
#endif // WF_CLIPRDR_H__
+79
View File
@@ -0,0 +1,79 @@
use hbb_common::{log, ResultType};
use std::{ops::Deref, sync::Mutex};
use crate::CliprdrServiceContext;
const CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS: u32 = 30;
lazy_static::lazy_static! {
static ref CONTEXT_SEND: ContextSend = ContextSend::default();
}
#[derive(Default)]
pub struct ContextSend(Mutex<Option<Box<dyn CliprdrServiceContext>>>);
impl Deref for ContextSend {
type Target = Mutex<Option<Box<dyn CliprdrServiceContext>>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ContextSend {
#[inline]
pub fn is_enabled() -> bool {
CONTEXT_SEND.lock().unwrap().is_some()
}
pub fn set_is_stopped() {
let _res = Self::proc(|c| c.set_is_stopped().map_err(|e| e.into()));
}
pub fn enable(enabled: bool) {
let mut lock = CONTEXT_SEND.lock().unwrap();
if enabled {
if lock.is_some() {
return;
}
match crate::create_cliprdr_context(true, false, CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS) {
Ok(context) => {
log::info!("clipboard context for file transfer created.");
*lock = Some(context)
}
Err(err) => {
log::error!(
"create clipboard context for file transfer: {}",
err.to_string()
);
}
}
} else if let Some(_clp) = lock.take() {
*lock = None;
log::info!("clipboard context for file transfer destroyed.");
}
}
/// make sure the clipboard context is enabled.
pub fn make_sure_enabled() -> ResultType<()> {
let mut lock = CONTEXT_SEND.lock().unwrap();
if lock.is_some() {
return Ok(());
}
let ctx = crate::create_cliprdr_context(true, false, CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS)?;
*lock = Some(ctx);
log::info!("clipboard context for file transfer recreated.");
Ok(())
}
pub fn proc<F: FnOnce(&mut Box<dyn CliprdrServiceContext>) -> ResultType<()>>(
f: F,
) -> ResultType<()> {
let mut lock = CONTEXT_SEND.lock().unwrap();
match lock.as_mut() {
Some(context) => f(context),
None => Ok(()),
}
}
}
+298
View File
@@ -0,0 +1,298 @@
use std::sync::{Arc, Mutex, RwLock};
#[cfg(any(
target_os = "windows",
all(target_os = "macos", feature = "unix-file-copy-paste")
))]
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,
},
};
use serde_derive::{Deserialize, Serialize};
use thiserror::Error;
#[cfg(any(
target_os = "windows",
all(target_os = "macos", feature = "unix-file-copy-paste")
))]
pub mod context_send;
pub mod platform;
#[cfg(any(
target_os = "windows",
all(target_os = "macos", feature = "unix-file-copy-paste")
))]
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;
#[cfg(target_os = "windows")]
const ERR_CODE_SEND_MSG: u32 = 0x00000003;
#[cfg(any(
target_os = "windows",
all(target_os = "macos", feature = "unix-file-copy-paste")
))]
pub(crate) use platform::create_cliprdr_context;
pub struct ProgressPercent {
pub percent: f64,
pub is_canceled: bool,
pub is_failed: bool,
}
// 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
/// There actually should be 2 parts to implement a useable clipboard file service,
/// but this only contains the RPC server part.
/// The local listener and transport part is too platform specific to wrap up in typeclasses.
pub trait CliprdrServiceContext: Send + Sync {
/// set to be stopped
fn set_is_stopped(&mut self) -> Result<(), CliprdrError>;
/// clear the content on clipboard
fn empty_clipboard(&mut self, conn_id: i32) -> Result<bool, CliprdrError>;
/// run as a server for clipboard RPC
fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError>;
/// get the progress of the paste task.
fn get_progress_percent(&self) -> Option<ProgressPercent>;
/// cancel the paste task.
fn cancel(&mut self);
}
#[derive(Error, Debug)]
pub enum CliprdrError {
#[error("invalid cliprdr name")]
CliprdrName,
#[error("failed to init cliprdr")]
CliprdrInit,
#[error("cliprdr out of memory")]
CliprdrOutOfMemory,
#[error("cliprdr internal error")]
ClipboardInternalError,
#[error("cliprdr occupied")]
ClipboardOccupied,
#[error("conversion failure")]
ConversionFailure,
#[error("failure to read clipboard")]
OpenClipboard,
#[error("failure to read file metadata or content, path: {path}, err: {err}")]
FileError { path: String, err: std::io::Error },
#[error("invalid request: {description}")]
InvalidRequest { description: String },
#[error("common request: {description}")]
CommonError { description: String },
#[error("unknown cliprdr error")]
Unknown(u32),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum ClipboardFile {
NotifyCallback {
r#type: String,
title: String,
text: String,
},
MonitorReady,
FormatList {
format_list: Vec<(i32, String)>,
},
FormatListResponse {
msg_flags: i32,
},
FormatDataRequest {
requested_format_id: i32,
},
FormatDataResponse {
msg_flags: i32,
format_data: Vec<u8>,
},
FileContentsRequest {
stream_id: i32,
list_index: i32,
dw_flags: i32,
n_position_low: i32,
n_position_high: i32,
cb_requested: i32,
have_clip_data_id: bool,
clip_data_id: i32,
},
FileContentsResponse {
msg_flags: i32,
stream_id: i32,
requested_data: Vec<u8>,
},
TryEmpty,
Files {
files: Vec<(String, u64)>,
},
}
struct MsgChannel {
peer_id: String,
conn_id: i32,
#[allow(dead_code)]
sender: UnboundedSender<ClipboardFile>,
receiver: Arc<TokioMutex<UnboundedReceiver<ClipboardFile>>>,
}
lazy_static::lazy_static! {
static ref VEC_MSG_CHANNEL: RwLock<Vec<MsgChannel>> = Default::default();
static ref CLIENT_CONN_ID_COUNTER: Mutex<i32> = Mutex::new(0);
}
impl ClipboardFile {
pub fn is_stopping_allowed(&self) -> bool {
matches!(
self,
ClipboardFile::MonitorReady
| ClipboardFile::FormatList { .. }
| ClipboardFile::FormatDataRequest { .. }
)
}
pub fn is_beginning_message(&self) -> bool {
matches!(
self,
ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. }
)
}
}
pub fn get_client_conn_id(peer_id: &str) -> Option<i32> {
VEC_MSG_CHANNEL
.read()
.unwrap()
.iter()
.find(|x| x.peer_id == peer_id)
.map(|x| x.conn_id)
}
fn get_conn_id() -> i32 {
let mut lock = CLIENT_CONN_ID_COUNTER.lock().unwrap();
*lock += 1;
*lock
}
pub fn get_rx_cliprdr_client(
peer_id: &str,
) -> (i32, Arc<TokioMutex<UnboundedReceiver<ClipboardFile>>>) {
let mut lock = VEC_MSG_CHANNEL.write().unwrap();
match lock.iter().find(|x| x.peer_id == peer_id) {
Some(msg_channel) => (msg_channel.conn_id, msg_channel.receiver.clone()),
None => {
let (sender, receiver) = unbounded_channel();
let receiver = Arc::new(TokioMutex::new(receiver));
let receiver2 = receiver.clone();
let conn_id = get_conn_id();
let msg_channel = MsgChannel {
peer_id: peer_id.to_owned(),
conn_id,
sender,
receiver,
};
lock.push(msg_channel);
(conn_id, receiver2)
}
}
}
pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc<TokioMutex<UnboundedReceiver<ClipboardFile>>> {
let mut lock = VEC_MSG_CHANNEL.write().unwrap();
match lock.iter().find(|x| x.conn_id == conn_id) {
Some(msg_channel) => msg_channel.receiver.clone(),
None => {
let (sender, receiver) = unbounded_channel();
let receiver = Arc::new(TokioMutex::new(receiver));
let receiver2 = receiver.clone();
let msg_channel = MsgChannel {
peer_id: "".to_string(),
conn_id,
sender,
receiver,
};
lock.push(msg_channel);
receiver2
}
}
}
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]
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 {
let _ = send_data_to_all(data);
Ok(())
} else {
send_data_to_channel(conn_id, data)
}
}
#[inline]
#[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)
.map_err(|e| CliprdrError::CommonError {
description: e.to_string(),
})
} else {
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) {
// 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()));
}
}
}
#[inline]
#[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()));
}
}
#[cfg(test)]
mod tests {
// #[test]
// fn test_cliprdr_run() {
// super::cliprdr_run();
// }
}
+26
View File
@@ -0,0 +1,26 @@
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(target_os = "windows")]
pub fn create_cliprdr_context(
enable_files: bool,
enable_others: bool,
response_wait_timeout_secs: u32,
) -> crate::ResultType<Box<dyn crate::CliprdrServiceContext>> {
let boxed =
windows::create_cliprdr_context(enable_files, enable_others, response_wait_timeout_secs)?
as Box<_>;
Ok(boxed)
}
#[cfg(feature = "unix-file-copy-paste")]
pub mod unix;
#[cfg(all(feature = "unix-file-copy-paste", 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>> {
let boxed = unix::macos::pasteboard_context::create_pasteboard_context()? as Box<_>;
Ok(boxed)
}
@@ -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 serde_derive::{Deserialize, Serialize};
use std::{
path::PathBuf,
time::{Duration, SystemTime},
};
use utf16string::WStr;
#[cfg(target_os = "linux")]
pub type Inode = u64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
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;
#[allow(dead_code)]
/// max length of file name
pub const MAX_NAME_LEN: usize = 255;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
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)
}
}
File diff suppressed because it is too large Load Diff
@@ -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())
}
}
@@ -0,0 +1,387 @@
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,
io::{BufRead, BufReader, Read, Seek},
os::unix::prelude::PermissionsExt,
path::{Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
time::SystemTime,
};
use utf16string::WString;
#[derive(Debug)]
pub(super) struct LocalFile {
pub relative_root: PathBuf,
pub path: PathBuf,
pub handle: Option<BufReader<File>>,
pub offset: AtomicU64,
pub name: String,
pub size: u64,
pub last_write_time: SystemTime,
pub is_dir: bool,
pub perm: u32,
pub read_only: bool,
pub hidden: bool,
pub system: bool,
pub archive: bool,
pub normal: bool,
}
impl LocalFile {
pub fn try_open(relative_root: &Path, path: &Path) -> Result<Self, CliprdrError> {
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
path: path.to_string_lossy().to_string(),
err: e,
})?;
let size = mt.len() as u64;
let is_dir = mt.is_dir();
let read_only = mt.permissions().readonly();
let system = false;
let hidden = path.to_string_lossy().starts_with('.');
let archive = false;
let normal = !(is_dir || read_only || system || hidden || archive);
let last_write_time = mt.modified().unwrap_or(SystemTime::UNIX_EPOCH);
let perm = mt.permissions().mode();
let name = path
.display()
.to_string()
.trim_start_matches('/')
.replace('/', "\\");
// NOTE: open files lazily
let handle = None;
let offset = AtomicU64::new(0);
Ok(Self {
name,
relative_root: relative_root.to_path_buf(),
path: path.to_path_buf(),
handle,
offset,
size,
last_write_time,
is_dir,
read_only,
system,
hidden,
perm,
archive,
normal,
})
}
pub fn as_bin(&self) -> Vec<u8> {
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_or_default()
.as_nanos() as u64
/ 100
+ LDAP_EPOCH_DELTA;
let size_high = (self.size >> 32) as u32;
let size_low = (self.size & (u32::MAX as u64)) as u32;
let path = self
.path
.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();
log::trace!(
"put file to list: name_len {}, name {}",
name.len(),
&self.name
);
let flags = FLAGS_FD_SIZE
| FLAGS_FD_LAST_WRITE
| FLAGS_FD_ATTRIBUTES
| FLAGS_FD_PROGRESSUI
| FLAGS_FD_UNIX_MODE;
// flags, 4 bytes
buf.put_u32_le(flags);
// 32 bytes reserved
buf.put(&[0u8; 32][..]);
// file attributes, 4 bytes
buf.put_u32_le(file_attributes);
// NOTE: this is not used in windows
// in the specification, this is 16 bytes reserved
// lets use the last 4 bytes to store the file mode
//
// 12 bytes reserved
buf.put(&[0u8; 12][..]);
// file permissions, 4 bytes
buf.put_u32_le(self.perm);
// last write time, 8 bytes
buf.put_u64_le(win32_time);
// file size (high)
buf.put_u32_le(size_high);
// file size (low)
buf.put_u32_le(size_low);
// put name and padding to 520 bytes
let name_len = name.len();
buf.put(name);
buf.put(&vec![0u8; 520 - name_len][..]);
buf.to_vec()
}
#[inline]
pub fn load_handle(&mut self) -> Result<(), CliprdrError> {
if !self.is_dir && self.handle.is_none() {
let handle = std::fs::File::open(&self.path).map_err(|e| CliprdrError::FileError {
path: self.path.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.to_string_lossy().to_string(),
err: e,
})?;
self.handle = Some(reader);
};
Ok(())
}
pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> {
self.load_handle()?;
let 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.to_string_lossy().to_string(),
err: e,
})?;
}
handle
.read_exact(buf)
.map_err(|e| CliprdrError::FileError {
path: self.path.to_string_lossy().to_string(),
err: e,
})?;
let new_offset = offset + (buf.len() as u64);
self.offset.store(new_offset, Ordering::Relaxed);
// gc file handle
if new_offset >= self.size {
self.offset.store(0, Ordering::Relaxed);
self.handle = None;
}
Ok(())
}
}
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
fn constr_file_lst(
relative_root: &Path,
path: &Path,
file_list: &mut Vec<LocalFile>,
visited: &mut HashSet<PathBuf>,
) -> Result<(), CliprdrError> {
// prevent fs loop
if visited.contains(path) {
return Ok(());
}
visited.insert(path.to_path_buf());
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.to_string_lossy().to_string(),
err: e,
})?;
if mt.is_dir() {
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.map_err(|e| CliprdrError::FileError {
path: path.to_string_lossy().to_string(),
err: e,
})?;
let path = entry.path();
constr_file_lst(relative_root, &path, file_list, visited)?;
}
}
Ok(())
}
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(&relative_root, path, &mut file_list, &mut visited)?;
}
Ok(file_list)
}
#[cfg(test)]
mod file_list_test {
use std::{path::PathBuf, sync::atomic::AtomicU64};
use hbb_common::bytes::{BufMut, BytesMut};
use crate::{platform::unix::filetype::FileDescription, CliprdrError};
use super::LocalFile;
#[inline]
fn generate_tree(prefix: &str) -> Vec<LocalFile> {
// generate a tree of local files, no handles
// - /
// |- a.txt
// |- b
// |- c.txt
#[inline]
fn generate_file(path: &str, name: &str, is_dir: bool) -> LocalFile {
LocalFile {
relative_root: PathBuf::from("."),
path: PathBuf::from(path),
handle: None,
name: name.to_string(),
size: 0,
offset: AtomicU64::new(0),
last_write_time: std::time::SystemTime::UNIX_EPOCH,
read_only: false,
is_dir,
perm: 0o754,
hidden: false,
system: false,
archive: false,
normal: false,
}
}
let p = prefix;
let (r_path, a_path, b_path, c_path) = if !prefix.is_empty() {
(
p.to_string(),
format!("{}/a.txt", p),
format!("{}/b", p),
format!("{}/b/c.txt", p),
)
} else {
(
".".to_string(),
"a.txt".to_string(),
"b".to_string(),
"b/c.txt".to_string(),
)
};
let root = generate_file(&r_path, ".", true);
let a = generate_file(&a_path, "a.txt", false);
let b = generate_file(&b_path, "b", true);
let c = generate_file(&c_path, "c.txt", false);
vec![root, a, b, c]
}
fn as_bin_parse_test(prefix: &str) -> Result<(), CliprdrError> {
let tree = generate_tree(prefix);
let mut pdu = BytesMut::with_capacity(4 + 592 * tree.len());
pdu.put_u32_le(tree.len() as u32);
for file in tree {
pdu.put(file.as_bin().as_slice());
}
let parsed = FileDescription::parse_file_descriptors(pdu.to_vec(), 0)?;
assert_eq!(parsed.len(), 4);
if !prefix.is_empty() {
assert_eq!(parsed[0].name.to_str().unwrap(), format!("{}", prefix));
assert_eq!(
parsed[1].name.to_str().unwrap(),
format!("{}/a.txt", prefix)
);
assert_eq!(parsed[2].name.to_str().unwrap(), format!("{}/b", prefix));
assert_eq!(
parsed[3].name.to_str().unwrap(),
format!("{}/b/c.txt", prefix)
);
} else {
assert_eq!(parsed[0].name.to_str().unwrap(), ".");
assert_eq!(parsed[1].name.to_str().unwrap(), "a.txt");
assert_eq!(parsed[2].name.to_str().unwrap(), "b");
assert_eq!(parsed[3].name.to_str().unwrap(), "b/c.txt");
}
assert!(parsed[0].perm & 0o777 == 0o754);
assert!(parsed[1].perm & 0o777 == 0o754);
assert!(parsed[2].perm & 0o777 == 0o754);
assert!(parsed[3].perm & 0o777 == 0o754);
Ok(())
}
#[test]
fn test_parse_file_descriptors() -> Result<(), CliprdrError> {
as_bin_parse_test("")?;
as_bin_parse_test("/")?;
as_bin_parse_test("test")?;
as_bin_parse_test("/test")?;
Ok(())
}
}
@@ -0,0 +1,25 @@
# File pate on macOS
MacOS cannot use `fuse` because of [macfuse is not supported by default](https://github.com/macfuse/macfuse/wiki/Getting-Started#enabling-support-for-third-party-kernel-extensions-apple-silicon-macs).
1. Use a temporary file `/tmp/rustdesk_<uuid>` as a placeholder in the pasteboard.
2. Uses `fsevent` to observe files paste operation. Then perform pasting files.
## Files
### `pasteboard_context.rs`
The context manager of the paste operations.
### `item_data_provider.rs`
1. Set pasteboard item.
2. Create temp file in `/tmp/.rustdesk_*`.
### `paste_observer.rs`
Use `fsevent` to observe the paste operation with the source file `/tmp/.rustdesk_*`.
### `paste_task.rs`
Perform the paste.
@@ -0,0 +1,77 @@
use super::pasteboard_context::{PasteObserverInfo, TEMP_FILE_PREFIX};
use objc2::{
declare_class, msg_send_id, mutability,
rc::Id,
runtime::{NSObject, NSObjectProtocol},
ClassType, DeclaredClass,
};
use objc2_app_kit::{
NSPasteboard, NSPasteboardItem, NSPasteboardItemDataProvider, NSPasteboardType,
NSPasteboardTypeFileURL,
};
use objc2_foundation::NSString;
use std::{io::Result, sync::mpsc::Sender};
pub(super) struct Ivars {
task_info: PasteObserverInfo,
tx: Sender<Result<PasteObserverInfo>>,
}
declare_class!(
pub(super) struct PasteboardFileUrlProvider;
unsafe impl ClassType for PasteboardFileUrlProvider {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
const NAME: &'static str = "PasteboardFileUrlProvider";
}
impl DeclaredClass for PasteboardFileUrlProvider {
type Ivars = Ivars;
}
unsafe impl NSObjectProtocol for PasteboardFileUrlProvider {}
unsafe impl NSPasteboardItemDataProvider for PasteboardFileUrlProvider {
#[method(pasteboard:item:provideDataForType:)]
#[allow(non_snake_case)]
unsafe fn pasteboard_item_provideDataForType(
&self,
_pasteboard: Option<&NSPasteboard>,
item: &NSPasteboardItem,
r#type: &NSPasteboardType,
) {
if r#type == NSPasteboardTypeFileURL {
let path = format!("/tmp/{}{}", TEMP_FILE_PREFIX, uuid::Uuid::new_v4().to_string());
match std::fs::File::create(&path) {
Ok(_) => {
let url = format!("file:///{}", &path);
item.setString_forType(&NSString::from_str(&url), &NSPasteboardTypeFileURL);
let mut task_info = self.ivars().task_info.clone();
task_info.source_path = path;
self.ivars().tx.send(Ok(task_info)).ok();
}
Err(e) => {
self.ivars().tx.send(Err(e)).ok();
}
}
}
}
// #[method(pasteboardFinishedWithDataProvider:)]
// unsafe fn pasteboardFinishedWithDataProvider(&self, pasteboard: &NSPasteboard) {
// }
}
unsafe impl PasteboardFileUrlProvider {}
);
pub(super) fn create_pasteboard_file_url_provider(
task_info: PasteObserverInfo,
tx: Sender<Result<PasteObserverInfo>>,
) -> Id<PasteboardFileUrlProvider> {
let provider = PasteboardFileUrlProvider::alloc();
let provider = provider.set_ivars(Ivars { task_info, tx });
let provider: Id<PasteboardFileUrlProvider> = unsafe { msg_send_id![super(provider), init] };
provider
}
@@ -0,0 +1,14 @@
mod item_data_provider;
mod paste_observer;
mod paste_task;
pub mod pasteboard_context;
pub fn should_handle_msg(msg: &crate::ClipboardFile) -> bool {
matches!(
msg,
crate::ClipboardFile::FormatList { .. }
| crate::ClipboardFile::FormatDataResponse { .. }
| crate::ClipboardFile::FileContentsResponse { .. }
| crate::ClipboardFile::TryEmpty
)
}
@@ -0,0 +1,179 @@
use super::pasteboard_context::PasteObserverInfo;
use fsevent::{self, StreamFlags};
use hbb_common::{bail, log, ResultType};
use std::{
sync::{
mpsc::{channel, Receiver, RecvTimeoutError, Sender},
Arc, Mutex,
},
thread,
time::Duration,
};
enum FseventControl {
Start,
Stop,
Exit,
}
struct FseventThreadInfo {
tx: Sender<FseventControl>,
handle: thread::JoinHandle<()>,
}
pub struct PasteObserver {
exit: Arc<Mutex<bool>>,
observer_info: Arc<Mutex<Option<PasteObserverInfo>>>,
tx_handle_fsevent_thread: Option<FseventThreadInfo>,
handle_observer_thread: Option<thread::JoinHandle<()>>,
}
impl Drop for PasteObserver {
fn drop(&mut self) {
*self.exit.lock().unwrap() = true;
if let Some(handle_observer_thread) = self.handle_observer_thread.take() {
handle_observer_thread.join().ok();
}
if let Some(tx_handle_fsevent_thread) = self.tx_handle_fsevent_thread.take() {
tx_handle_fsevent_thread.tx.send(FseventControl::Exit).ok();
tx_handle_fsevent_thread.handle.join().ok();
}
}
}
impl PasteObserver {
const OBSERVE_TIMEOUT: Duration = Duration::from_secs(30);
pub fn new() -> Self {
Self {
exit: Arc::new(Mutex::new(false)),
observer_info: Default::default(),
tx_handle_fsevent_thread: None,
handle_observer_thread: None,
}
}
pub fn init(&mut self, cb_pasted: fn(&PasteObserverInfo) -> ()) -> ResultType<()> {
let Some(home_dir) = dirs::home_dir() else {
bail!("No home dir is set, do not observe.");
};
let (tx_observer, rx_observer) = channel::<fsevent::Event>();
let handle_observer = Self::init_thread_observer(
self.exit.clone(),
self.observer_info.clone(),
rx_observer,
cb_pasted,
);
self.handle_observer_thread = Some(handle_observer);
let (tx_control, rx_control) = channel::<FseventControl>();
let handle_fsevent = Self::init_thread_fsevent(
home_dir.to_string_lossy().to_string(),
tx_observer,
rx_control,
);
self.tx_handle_fsevent_thread = Some(FseventThreadInfo {
tx: tx_control,
handle: handle_fsevent,
});
Ok(())
}
#[inline]
fn get_file_from_path(path: &String) -> String {
let last_slash = path.rfind('/').or_else(|| path.rfind('\\'));
match last_slash {
Some(index) => path[index + 1..].to_string(),
None => path.clone(),
}
}
fn init_thread_observer(
exit: Arc<Mutex<bool>>,
observer_info: Arc<Mutex<Option<PasteObserverInfo>>>,
rx_observer: Receiver<fsevent::Event>,
cb_pasted: fn(&PasteObserverInfo) -> (),
) -> thread::JoinHandle<()> {
thread::spawn(move || loop {
match rx_observer.recv_timeout(Duration::from_millis(300)) {
Ok(event) => {
if (event.flag & StreamFlags::ITEM_CREATED) != StreamFlags::NONE
&& (event.flag & StreamFlags::ITEM_REMOVED) == StreamFlags::NONE
&& (event.flag & StreamFlags::IS_FILE) != StreamFlags::NONE
{
let source_file = observer_info
.lock()
.unwrap()
.as_ref()
.map(|x| Self::get_file_from_path(&x.source_path));
if let Some(source_file) = source_file {
let file = Self::get_file_from_path(&event.path);
if source_file == file {
if let Some(observer_info) = observer_info.lock().unwrap().as_mut()
{
observer_info.target_path = event.path.clone();
cb_pasted(observer_info);
}
}
}
}
}
Err(_) => {
if *(exit.lock().unwrap()) {
break;
}
}
}
})
}
fn new_fsevent(home_dir: String, tx_observer: Sender<fsevent::Event>) -> fsevent::FsEvent {
let mut evt = fsevent::FsEvent::new(vec![home_dir.to_string()]);
evt.observe_async(tx_observer).ok();
evt
}
fn init_thread_fsevent(
home_dir: String,
tx_observer: Sender<fsevent::Event>,
rx_control: Receiver<FseventControl>,
) -> thread::JoinHandle<()> {
log::debug!("fsevent observe dir: {}", &home_dir);
thread::spawn(move || {
let mut fsevent = None;
loop {
match rx_control.recv_timeout(Self::OBSERVE_TIMEOUT) {
Ok(FseventControl::Start) => {
if fsevent.is_none() {
fsevent =
Some(Self::new_fsevent(home_dir.clone(), tx_observer.clone()));
}
}
Ok(FseventControl::Stop) | Err(RecvTimeoutError::Timeout) => {
let _ = fsevent.as_mut().map(|e| e.shutdown_observe());
fsevent = None;
}
Ok(FseventControl::Exit) | Err(RecvTimeoutError::Disconnected) => {
break;
}
}
}
log::info!("fsevent thread exit");
let _ = fsevent.as_mut().map(|e| e.shutdown_observe());
})
}
pub fn start(&mut self, observer_info: PasteObserverInfo) {
if let Some(tx_handle_fsevent_thread) = self.tx_handle_fsevent_thread.as_ref() {
self.observer_info.lock().unwrap().replace(observer_info);
tx_handle_fsevent_thread.tx.send(FseventControl::Start).ok();
}
}
pub fn stop(&mut self) {
if let Some(tx_handle_fsevent_thread) = &self.tx_handle_fsevent_thread {
self.observer_info = Default::default();
tx_handle_fsevent_thread.tx.send(FseventControl::Stop).ok();
}
}
}
@@ -0,0 +1,639 @@
use crate::{
platform::unix::{FileDescription, FileType, BLOCK_SIZE},
send_data, ClipboardFile, CliprdrError, ProgressPercent,
};
use hbb_common::{allow_err, log, tokio::time::Instant};
use std::{
cmp::min,
fs::{File, FileTimes},
io::{BufWriter, Write},
os::macos::fs::FileTimesExt,
path::{Path, PathBuf},
sync::{
mpsc::{Receiver, RecvTimeoutError},
Arc, Mutex,
},
thread,
time::{Duration, SystemTime},
};
const RECV_RETRY_TIMES: usize = 3;
const DOWNLOAD_EXTENSION: &str = "rddownload";
const RECEIVE_WAIT_TIMEOUT: Duration = Duration::from_millis(5_000);
// https://stackoverflow.com/a/15112784/1926020
// "1984-01-24 08:00:00 +0000"
const TIMESTAMP_FOR_FILE_PROGRESS_COMPLETED: u64 = 443779200;
const ATTR_PROGRESS_FRACTION_COMPLETED: &str = "com.apple.progress.fractionCompleted";
pub struct FileContentsResponse {
pub conn_id: i32,
pub msg_flags: i32,
pub stream_id: i32,
pub requested_data: Vec<u8>,
}
#[derive(Debug)]
struct PasteTaskProgress {
// Use list index to identify the file
// `list_index` is also used as the stream id
list_index: i32,
offset: u64,
total_size: u64,
current_size: u64,
last_sent_time: Instant,
download_file_index: i32,
download_file_size: u64,
download_file_path: String,
download_file_current_size: u64,
file_handle: Option<BufWriter<File>>,
error: Option<CliprdrError>,
is_canceled: bool,
}
struct PasteTaskHandle {
progress: PasteTaskProgress,
target_dir: PathBuf,
files: Vec<FileDescription>,
}
pub struct PasteTask {
exit: Arc<Mutex<bool>>,
handle: Arc<Mutex<Option<PasteTaskHandle>>>,
handle_worker: Option<thread::JoinHandle<()>>,
}
impl Drop for PasteTask {
fn drop(&mut self) {
*self.exit.lock().unwrap() = true;
if let Some(handle_worker) = self.handle_worker.take() {
handle_worker.join().ok();
}
}
}
impl PasteTask {
const INVALID_FILE_INDEX: i32 = -1;
pub fn new(rx_file_contents: Receiver<FileContentsResponse>) -> Self {
let exit = Arc::new(Mutex::new(false));
let handle = Arc::new(Mutex::new(None));
let handle_worker =
Self::init_worker_thread(exit.clone(), handle.clone(), rx_file_contents);
Self {
handle,
exit,
handle_worker: Some(handle_worker),
}
}
pub fn start(&mut self, target_dir: PathBuf, files: Vec<FileDescription>) {
let mut task_lock = self.handle.lock().unwrap();
if task_lock
.as_ref()
.map(|x| !x.is_finished())
.unwrap_or(false)
{
log::error!("Previous paste task is not finished, ignore new request.");
return;
}
let total_size = files.iter().map(|f| f.size).sum();
let mut task_handle = PasteTaskHandle {
progress: PasteTaskProgress {
list_index: -1,
offset: 0,
total_size,
current_size: 0,
last_sent_time: Instant::now(),
download_file_index: Self::INVALID_FILE_INDEX,
download_file_size: 0,
download_file_path: "".to_owned(),
download_file_current_size: 0,
file_handle: None,
error: None,
is_canceled: false,
},
target_dir,
files,
};
task_handle.update_next(0).ok();
if task_handle.is_finished() {
task_handle.on_finished();
} else {
if let Err(e) = task_handle.send_file_contents_request() {
log::error!("Failed to send file contents request, error: {}", &e);
task_handle.on_error(e);
}
}
*task_lock = Some(task_handle);
}
pub fn cancel(&self) {
let mut task_handle = self.handle.lock().unwrap();
if let Some(task_handle) = task_handle.as_mut() {
task_handle.progress.is_canceled = true;
task_handle.on_cancelled();
}
}
fn init_worker_thread(
exit: Arc<Mutex<bool>>,
handle: Arc<Mutex<Option<PasteTaskHandle>>>,
rx_file_contents: Receiver<FileContentsResponse>,
) -> thread::JoinHandle<()> {
thread::spawn(move || {
let mut retry_count = 0;
loop {
if *exit.lock().unwrap() {
break;
}
match rx_file_contents.recv_timeout(Duration::from_millis(300)) {
Ok(file_contents) => {
let mut task_lock = handle.lock().unwrap();
let Some(task_handle) = task_lock.as_mut() else {
continue;
};
if task_handle.is_finished() {
continue;
}
if file_contents.stream_id != task_handle.progress.list_index {
// ignore invalid stream id
continue;
} else if file_contents.msg_flags != 0x01 {
retry_count += 1;
if retry_count > RECV_RETRY_TIMES {
task_handle.progress.error = Some(CliprdrError::InvalidRequest {
description: format!(
"Failed to read file contents, stream id: {}, msg_flags: {}",
file_contents.stream_id,
file_contents.msg_flags
),
});
}
} else {
let resp_list_index = file_contents.stream_id;
let Some(file) = &task_handle.files.get(resp_list_index as usize)
else {
// unreachable
// Because `task_handle.progress.list_index >= task_handle.files.len()` should always be false
log::warn!(
"Invalid response list index: {}, file length: {}",
resp_list_index,
task_handle.files.len()
);
continue;
};
if file.conn_id != file_contents.conn_id {
// unreachable
// We still add log here to make sure we can see the error message when it happens.
log::error!(
"Invalid response conn id: {}, expected: {}",
file_contents.conn_id,
file.conn_id
);
continue;
}
if let Err(e) = task_handle.handle_file_contents_response(file_contents)
{
log::error!("Failed to handle file contents response: {}", &e);
task_handle.on_error(e);
}
}
if !task_handle.is_finished() {
if let Err(e) = task_handle.send_file_contents_request() {
log::error!("Failed to send file contents request: {}", &e);
task_handle.on_error(e);
}
} else {
retry_count = 0;
task_handle.on_finished();
}
}
Err(RecvTimeoutError::Timeout) => {
let mut task_lock = handle.lock().unwrap();
if let Some(task_handle) = task_lock.as_mut() {
if task_handle.check_receive_timemout() {
retry_count = 0;
task_handle.on_finished();
}
}
}
Err(RecvTimeoutError::Disconnected) => {
break;
}
}
}
})
}
pub fn is_finished(&self) -> bool {
self.handle
.lock()
.unwrap()
.as_ref()
.map(|handle| handle.is_finished())
.unwrap_or(true)
}
pub fn progress_percent(&self) -> Option<ProgressPercent> {
self.handle
.lock()
.unwrap()
.as_ref()
.map(|handle| handle.progress_percent())
}
}
impl PasteTaskHandle {
fn update_next(&mut self, size: u64) -> Result<(), CliprdrError> {
if self.is_finished() {
return Ok(());
}
self.progress.current_size += size;
let is_start = self.progress.list_index == -1;
if is_start || (self.progress.offset + size) >= self.progress.download_file_size {
if !is_start {
self.on_done();
}
for i in (self.progress.list_index + 1)..self.files.len() as i32 {
let Some(file_desc) = self.files.get(i as usize) else {
return Err(CliprdrError::InvalidRequest {
description: format!("Invalid file index: {}", i),
});
};
match file_desc.kind {
FileType::File => {
if file_desc.size == 0 {
if let Some(new_file_path) =
Self::get_new_filename(&self.target_dir, file_desc)
{
if let Ok(f) = std::fs::File::create(&new_file_path) {
f.set_len(0).ok();
Self::set_file_metadata(&f, file_desc);
}
};
} else {
self.progress.list_index = i;
self.progress.offset = 0;
self.open_new_writer()?;
break;
}
}
FileType::Directory => {
let path = self.target_dir.join(&file_desc.name);
if !path.exists() {
std::fs::create_dir_all(path).ok();
}
}
FileType::Symlink => {
// to-do: handle symlink
}
}
}
} else {
self.progress.offset += size;
self.progress.download_file_current_size += size;
self.update_progress_completed(None);
}
if self.progress.file_handle.is_none() {
self.progress.list_index = self.files.len() as i32;
self.progress.offset = 0;
self.progress.download_file_size = 0;
self.progress.download_file_current_size = 0;
}
Ok(())
}
fn start_progress_completed(&self) {
if let Some(file) = self.progress.file_handle.as_ref() {
let creation_time =
SystemTime::UNIX_EPOCH + Duration::from_secs(TIMESTAMP_FOR_FILE_PROGRESS_COMPLETED);
file.get_ref()
.set_times(FileTimes::new().set_created(creation_time))
.ok();
xattr::set(
&self.progress.download_file_path,
ATTR_PROGRESS_FRACTION_COMPLETED,
"0.0".as_bytes(),
)
.ok();
}
}
fn update_progress_completed(&mut self, fraction_completed: Option<f64>) {
let fraction_completed = fraction_completed.unwrap_or_else(|| {
let current_size = self.progress.download_file_current_size as f64;
let total_size = self.progress.download_file_size as f64;
if total_size > 0.0 {
current_size / total_size
} else {
1.0
}
});
xattr::set(
&self.progress.download_file_path,
ATTR_PROGRESS_FRACTION_COMPLETED,
&fraction_completed.to_string().as_bytes(),
)
.ok();
}
#[inline]
fn remove_progress_completed(path: &str) {
if !path.is_empty() {
xattr::remove(path, ATTR_PROGRESS_FRACTION_COMPLETED).ok();
}
}
fn open_new_writer(&mut self) -> Result<(), CliprdrError> {
let Some(file) = &self.files.get(self.progress.list_index as usize) else {
return Err(CliprdrError::InvalidRequest {
description: format!(
"Invalid file index: {}, file count: {}",
self.progress.list_index,
self.files.len()
),
});
};
let original_file_path = self
.target_dir
.join(&file.name)
.to_string_lossy()
.to_string();
let Some(download_file_path) = Self::get_first_filename(
format!("{}.{}", original_file_path, DOWNLOAD_EXTENSION),
file.kind,
) else {
return Err(CliprdrError::CommonError {
description: format!("Failed to get download file path: {}", original_file_path),
});
};
let Some(download_path_parent) = Path::new(&download_file_path).parent() else {
return Err(CliprdrError::CommonError {
description: format!(
"Failed to get parent of the download file path: {}",
original_file_path
),
});
};
if !download_path_parent.exists() {
if let Err(e) = std::fs::create_dir_all(download_path_parent) {
return Err(CliprdrError::FileError {
path: download_path_parent.to_string_lossy().to_string(),
err: e,
});
}
}
match std::fs::File::create(&download_file_path) {
Ok(handle) => {
let writer = BufWriter::with_capacity(BLOCK_SIZE as usize * 2, handle);
self.progress.download_file_index = self.progress.list_index;
self.progress.download_file_size = file.size;
self.progress.download_file_path = download_file_path;
self.progress.download_file_current_size = 0;
self.progress.file_handle = Some(writer);
self.start_progress_completed();
}
Err(e) => {
self.progress.error = Some(CliprdrError::FileError {
path: download_file_path,
err: e,
});
}
};
Ok(())
}
fn get_first_filename(path: String, r#type: FileType) -> Option<String> {
let p = Path::new(&path);
if !p.exists() {
return Some(path);
} else {
for i in 1..9999999 {
let new_path = match r#type {
FileType::File => {
if let Some(ext) = p.extension() {
let new_name = format!(
"{}-{}.{}",
p.file_stem().unwrap_or_default().to_string_lossy(),
i,
ext.to_string_lossy()
);
p.with_file_name(new_name).to_string_lossy().to_string()
} else {
format!("{} ({})", path, i)
}
}
FileType::Directory => format!("{} ({})", path, i),
FileType::Symlink => {
// to-do: handle symlink
return None;
}
};
if !Path::new(&new_path).exists() {
return Some(new_path);
}
}
}
// unreachable
None
}
fn progress_percent(&self) -> ProgressPercent {
let percent = self.progress.current_size as f64 / self.progress.total_size as f64;
ProgressPercent {
percent,
is_canceled: self.progress.is_canceled,
is_failed: self.progress.error.is_some(),
}
}
fn is_finished(&self) -> bool {
self.progress.is_canceled
|| self.progress.error.is_some()
|| self.progress.list_index >= self.files.len() as i32
}
fn check_receive_timemout(&mut self) -> bool {
if !self.is_finished() {
if self.progress.last_sent_time.elapsed() > RECEIVE_WAIT_TIMEOUT {
self.progress.error = Some(CliprdrError::InvalidRequest {
description: "Failed to read file contents".to_string(),
});
return true;
}
}
false
}
fn on_finished(&mut self) {
if self.progress.error.is_some() {
self.on_cancelled();
} else {
self.on_done();
}
if self.progress.current_size != self.progress.total_size {
self.progress.error = Some(CliprdrError::InvalidRequest {
description: "Failed to download all files".to_string(),
});
}
}
fn on_error(&mut self, error: CliprdrError) {
self.progress.error = Some(error);
self.on_cancelled();
}
fn on_cancelled(&mut self) {
self.progress.file_handle = None;
std::fs::remove_file(&self.progress.download_file_path).ok();
}
fn on_done(&mut self) {
self.update_progress_completed(Some(1.0));
Self::remove_progress_completed(&self.progress.download_file_path);
let Some(file) = self.progress.file_handle.as_mut() else {
return;
};
if self.progress.download_file_index == PasteTask::INVALID_FILE_INDEX {
return;
}
if let Err(e) = file.flush() {
log::error!("Failed to flush file: {:?}", e);
}
self.progress.file_handle = None;
let Some(file_desc) = self.files.get(self.progress.download_file_index as usize) else {
// unreachable
log::error!(
"Failed to get file description: {}",
self.progress.download_file_index
);
return;
};
let Some(rename_to_path) = Self::get_new_filename(&self.target_dir, file_desc) else {
return;
};
match std::fs::rename(&self.progress.download_file_path, &rename_to_path) {
Ok(_) => Self::set_file_metadata2(&rename_to_path, file_desc),
Err(e) => {
log::error!("Failed to rename file: {:?}", e);
}
}
self.progress.download_file_path = "".to_owned();
self.progress.download_file_index = PasteTask::INVALID_FILE_INDEX;
}
fn get_new_filename(target_dir: &PathBuf, file_desc: &FileDescription) -> Option<String> {
let mut rename_to_path = target_dir
.join(&file_desc.name)
.to_string_lossy()
.to_string();
if Path::new(&rename_to_path).exists() {
let Some(new_path) = Self::get_first_filename(rename_to_path.clone(), file_desc.kind)
else {
log::error!("Failed to get new file name: {}", &rename_to_path);
return None;
};
rename_to_path = new_path;
}
Some(rename_to_path)
}
#[inline]
fn set_file_metadata(f: &File, file_desc: &FileDescription) {
let times = FileTimes::new()
.set_accessed(file_desc.atime)
.set_modified(file_desc.last_modified)
.set_created(file_desc.creation_time);
f.set_times(times).ok();
}
#[inline]
fn set_file_metadata2(path: &str, file_desc: &FileDescription) {
let times = FileTimes::new()
.set_accessed(file_desc.atime)
.set_modified(file_desc.last_modified)
.set_created(file_desc.creation_time);
File::options()
.write(true)
.open(path)
.map(|f| f.set_times(times))
.ok();
}
fn send_file_contents_request(&mut self) -> Result<(), CliprdrError> {
if self.is_finished() {
return Ok(());
}
let stream_id = self.progress.list_index;
let list_index = self.progress.list_index;
let Some(file) = &self.files.get(list_index as usize) else {
// unreachable
return Err(CliprdrError::InvalidRequest {
description: format!("Invalid file index: {}", list_index),
});
};
let cb_requested = min(BLOCK_SIZE as u64, file.size - self.progress.offset);
let conn_id = file.conn_id;
let (n_position_high, n_position_low) = (
(self.progress.offset >> 32) as i32,
(self.progress.offset & (u32::MAX as u64)) as i32,
);
let request = ClipboardFile::FileContentsRequest {
stream_id,
list_index,
dw_flags: 2,
n_position_low,
n_position_high,
cb_requested: cb_requested as _,
have_clip_data_id: false,
clip_data_id: 0,
};
allow_err!(send_data(conn_id, request));
self.progress.last_sent_time = Instant::now();
Ok(())
}
fn handle_file_contents_response(
&mut self,
file_contents: FileContentsResponse,
) -> Result<(), CliprdrError> {
if let Some(file) = self.progress.file_handle.as_mut() {
let data = file_contents.requested_data.as_slice();
let mut write_len = 0;
while write_len < data.len() {
match file.write(&data[write_len..]) {
Ok(len) => {
write_len += len;
}
Err(e) => {
return Err(CliprdrError::FileError {
path: self.progress.download_file_path.clone(),
err: e,
});
}
}
}
self.update_next(write_len as _)?;
} else {
return Err(CliprdrError::FileError {
path: self.progress.download_file_path.clone(),
err: std::io::Error::new(std::io::ErrorKind::NotFound, "file handle is not opened"),
});
}
Ok(())
}
}
@@ -0,0 +1,460 @@
use super::{
item_data_provider::create_pasteboard_file_url_provider,
paste_observer::PasteObserver,
paste_task::{FileContentsResponse, PasteTask},
};
use crate::{
platform::unix::{
filetype::FileDescription, FILECONTENTS_FORMAT_NAME, FILEDESCRIPTORW_FORMAT_NAME,
},
send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ProgressPercent,
};
use hbb_common::{allow_err, bail, log, ResultType};
use objc2::{msg_send_id, rc::autoreleasepool, rc::Id, runtime::ProtocolObject, ClassType};
use objc2_app_kit::{NSPasteboard, NSPasteboardTypeFileURL};
use objc2_foundation::{NSArray, NSString};
use std::{
io,
path::Path,
sync::{
mpsc::{channel, Receiver, RecvTimeoutError, Sender},
Arc, Mutex,
},
thread,
time::Duration,
};
lazy_static::lazy_static! {
static ref PASTE_OBSERVER_INFO: Arc<Mutex<Option<PasteObserverInfo>>> = Default::default();
}
pub const TEMP_FILE_PREFIX: &str = ".rustdesk_";
#[derive(Default, Debug, Clone, PartialEq)]
pub(super) struct PasteObserverInfo {
pub file_descriptor_id: i32,
pub conn_id: i32,
pub source_path: String,
pub target_path: String,
}
impl PasteObserverInfo {
fn exit_msg() -> Self {
Self::default()
}
}
struct ContextInfo {
tx: Sender<io::Result<PasteObserverInfo>>,
handle: thread::JoinHandle<()>,
}
pub struct PasteboardContext {
pasteboard: Id<NSPasteboard>,
observer: Arc<Mutex<PasteObserver>>,
tx_handle: Option<ContextInfo>,
tx_remove_file: Option<Sender<String>>,
remove_file_handle: Option<thread::JoinHandle<()>>,
tx_paste_task: Sender<FileContentsResponse>,
paste_task: Arc<Mutex<PasteTask>>,
}
unsafe impl Send for PasteboardContext {}
unsafe impl Sync for PasteboardContext {}
impl Drop for PasteboardContext {
fn drop(&mut self) {
self.observer.lock().unwrap().stop();
if let Some(tx_handle) = self.tx_handle.take() {
if tx_handle.tx.send(Ok(PasteObserverInfo::exit_msg())).is_ok() {
tx_handle.handle.join().ok();
}
}
}
}
impl CliprdrServiceContext for PasteboardContext {
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
Ok(())
}
fn empty_clipboard(&mut self, conn_id: i32) -> Result<bool, CliprdrError> {
Ok(self.empty_clipboard_(conn_id))
}
fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> {
self.server_clip_file_(conn_id, msg)
}
fn get_progress_percent(&self) -> Option<ProgressPercent> {
self.paste_task.lock().unwrap().progress_percent()
}
fn cancel(&mut self) {
self.paste_task.lock().unwrap().cancel();
}
}
impl PasteboardContext {
fn init(&mut self) {
let (tx_remove_file, rx_remove_file) = channel();
let handle_remove_file = Self::init_thread_remove_file(rx_remove_file);
self.tx_remove_file = Some(tx_remove_file.clone());
self.remove_file_handle = Some(handle_remove_file);
let (tx, rx) = channel();
let observer: Arc<Mutex<PasteObserver>> = self.observer.clone();
let handle = Self::init_thread_observer(tx_remove_file, rx, observer);
self.tx_handle = Some(ContextInfo { tx, handle });
}
fn init_thread_observer(
tx_remove_file: Sender<String>,
rx: Receiver<io::Result<PasteObserverInfo>>,
observer: Arc<Mutex<PasteObserver>>,
) -> thread::JoinHandle<()> {
let exit_msg = PasteObserverInfo::exit_msg();
thread::spawn(move || loop {
match rx.recv() {
Ok(Ok(task_info)) => {
if task_info == exit_msg {
log::debug!("pasteboard item data provider: exit");
break;
}
tx_remove_file.send(task_info.source_path.clone()).ok();
observer.lock().unwrap().start(task_info);
}
Ok(Err(e)) => {
log::error!("pasteboard item data provider, inner error: {e}");
}
Err(e) => {
log::error!("pasteboard item data provider, error: {e}");
break;
}
}
})
}
fn init_thread_remove_file(rx: Receiver<String>) -> thread::JoinHandle<()> {
thread::spawn(move || {
let mut cur_file: Option<String> = None;
loop {
match rx.recv_timeout(Duration::from_secs(30)) {
Ok(path) => {
if let Some(file) = cur_file.take() {
if !file.is_empty() {
std::fs::remove_file(&file).ok();
}
}
if !path.is_empty() {
cur_file = Some(path);
}
}
Err(e) => {
if let Some(file) = cur_file.take() {
if !file.is_empty() {
std::fs::remove_file(&file).ok();
}
}
if e == RecvTimeoutError::Disconnected {
break;
}
}
}
}
})
}
// Just removing the file can also make paste option in the context menu disappear.
fn empty_clipboard_(&mut self, _conn_id: i32) -> bool {
self.tx_remove_file
.as_ref()
.map(|tx| tx.send("".to_string()).ok());
true
}
fn temp_files_count() -> usize {
let mut count = 0;
if let Ok(entries) = std::fs::read_dir("/tmp") {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_file() {
if let Some(file_name) = path.file_name() {
if let Some(file_name_str) = file_name.to_str() {
if file_name_str.starts_with(TEMP_FILE_PREFIX) {
count += 1;
}
}
}
}
}
}
}
count
}
fn server_clip_file_(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> {
match msg {
ClipboardFile::FormatList { format_list } => {
let temp_files = Self::temp_files_count();
if temp_files >= 3 {
// The temp files should be 0 or 1 in normal case.
// We should not continue to paste files if there are more than 3 temp files.
return Err(CliprdrError::CommonError {
description: format!(
"too many temp files, current: {}, limit: {}",
temp_files, 3
),
});
}
let task_lock = self.paste_task.lock().unwrap();
if !task_lock.is_finished() {
return Err(CliprdrError::CommonError {
description: "previous file paste task is not finished".to_string(),
});
}
self.handle_format_list(conn_id, format_list)?;
}
ClipboardFile::FormatDataResponse {
msg_flags,
format_data,
} => {
self.handle_format_data_response(conn_id, msg_flags, format_data)?;
}
ClipboardFile::FileContentsResponse {
msg_flags,
stream_id,
requested_data,
} => {
self.handle_file_contents_response(conn_id, msg_flags, stream_id, requested_data)?;
}
ClipboardFile::TryEmpty => self.handle_try_empty(conn_id),
_ => {}
}
Ok(())
}
fn handle_format_list(
&self,
conn_id: i32,
format_list: Vec<(i32, String)>,
) -> Result<(), CliprdrError> {
if let Some(tx_handle) = self.tx_handle.as_ref() {
if !format_list
.iter()
.find(|(_, name)| name == FILECONTENTS_FORMAT_NAME)
.map(|(id, _)| *id)
.is_some()
{
return Err(CliprdrError::CommonError {
description: "no file contents format found".to_string(),
});
};
let Some(file_descriptor_id) = format_list
.iter()
.find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME)
.map(|(id, _)| *id)
else {
return Err(CliprdrError::CommonError {
description: "no file descriptor format found".to_string(),
});
};
autoreleasepool(|_| self.set_clipboard_item(tx_handle, conn_id, file_descriptor_id))?;
} else {
return Err(CliprdrError::CommonError {
description: "pasteboard context is not inited".to_string(),
});
}
Ok(())
}
fn set_clipboard_item(
&self,
tx_handle: &ContextInfo,
conn_id: i32,
file_descriptor_id: i32,
) -> Result<(), CliprdrError> {
let tx = tx_handle.tx.clone();
let provider = create_pasteboard_file_url_provider(
PasteObserverInfo {
file_descriptor_id,
conn_id,
source_path: "".to_string(),
target_path: "".to_string(),
},
tx,
);
unsafe {
let types = NSArray::from_vec(vec![NSString::from_str(
&NSPasteboardTypeFileURL.to_string(),
)]);
let item = objc2_app_kit::NSPasteboardItem::new();
item.setDataProvider_forTypes(&ProtocolObject::from_id(provider), &types);
self.pasteboard.clearContents();
if !self
.pasteboard
.writeObjects(&Id::cast(NSArray::from_vec(vec![item])))
{
return Err(CliprdrError::CommonError {
description: "failed to write objects".to_string(),
});
}
}
Ok(())
}
fn handle_format_data_response(
&self,
conn_id: i32,
msg_flags: i32,
format_data: Vec<u8>,
) -> Result<(), CliprdrError> {
log::debug!("handle format data response, msg_flags: {msg_flags}");
if msg_flags != 0x1 {
// return failure message?
}
let mut task_lock = self.paste_task.lock().unwrap();
let target_dir = PASTE_OBSERVER_INFO
.lock()
.unwrap()
.as_ref()
.map(|task| task.target_path.clone());
// unreachable in normal case
let Some(target_dir) = target_dir.as_ref().map(|d| Path::new(d).parent()).flatten() else {
return Err(CliprdrError::CommonError {
description: "failed to get parent path".to_string(),
});
};
// unreachable in normal case
if !target_dir.exists() {
return Err(CliprdrError::CommonError {
description: "target path does not exist".to_string(),
});
}
let target_dir = target_dir.to_owned();
match FileDescription::parse_file_descriptors(format_data, conn_id) {
Ok(files) => {
task_lock.start(target_dir, files);
Ok(())
}
Err(e) => {
PASTE_OBSERVER_INFO
.lock()
.unwrap()
.replace(PasteObserverInfo::default());
Err(e)
}
}
}
fn handle_file_contents_response(
&self,
conn_id: i32,
msg_flags: i32,
stream_id: i32,
requested_data: Vec<u8>,
) -> Result<(), CliprdrError> {
log::debug!("handle file contents response");
self.tx_paste_task
.send(FileContentsResponse {
conn_id,
msg_flags,
stream_id,
requested_data,
})
.ok();
Ok(())
}
fn handle_try_empty(&mut self, conn_id: i32) {
log::debug!("empty_clipboard called");
let ret = self.empty_clipboard_(conn_id);
log::debug!(
"empty_clipboard called, conn_id {}, return {}",
conn_id,
ret
);
}
}
fn handle_paste_result(task_info: &PasteObserverInfo) {
log::info!(
"file {} is pasted to {}",
&task_info.source_path,
&task_info.target_path
);
if Path::new(&task_info.target_path).parent().is_none() {
log::error!(
"failed to get parent path of {}, no need to perform pasting",
&task_info.target_path
);
return;
}
PASTE_OBSERVER_INFO
.lock()
.unwrap()
.replace(task_info.clone());
// to-do: add a timeout to clear data in `PASTE_OBSERVER_INFO`.
std::fs::remove_file(&task_info.source_path).ok();
std::fs::remove_file(&task_info.target_path).ok();
let data = ClipboardFile::FormatDataRequest {
requested_format_id: task_info.file_descriptor_id,
};
allow_err!(send_data(task_info.conn_id as _, data));
}
#[inline]
pub fn create_pasteboard_context() -> ResultType<Box<PasteboardContext>> {
let pasteboard: Option<Id<NSPasteboard>> =
unsafe { msg_send_id![NSPasteboard::class(), generalPasteboard] };
let Some(pasteboard) = pasteboard else {
bail!("failed to get general pasteboard");
};
let mut observer = PasteObserver::new();
observer.init(handle_paste_result)?;
let (tx, rx) = channel();
let mut context = Box::new(PasteboardContext {
pasteboard,
observer: Arc::new(Mutex::new(observer)),
tx_handle: None,
tx_remove_file: None,
remove_file_handle: None,
tx_paste_task: tx,
paste_task: Arc::new(Mutex::new(PasteTask::new(rx))),
});
context.init();
Ok(context)
}
#[cfg(test)]
mod tests {
#[test]
fn test_temp_files_count() {
let mut c = super::PasteboardContext::temp_files_count();
let mut created_files = vec![];
for _ in 0..10 {
let path = format!(
"/tmp/{}{}",
super::TEMP_FILE_PREFIX,
uuid::Uuid::new_v4().to_string()
);
if std::fs::File::create(&path).is_ok() {
created_files.push(path);
c += 1;
}
}
assert_eq!(c, super::PasteboardContext::temp_files_count());
// Clean up the created files.
for file in created_files {
std::fs::remove_file(&file).ok();
}
}
}
+58
View File
@@ -0,0 +1,58 @@
use dashmap::DashMap;
use lazy_static::lazy_static;
mod filetype;
pub use filetype::{FileDescription, FileType};
/// use FUSE for file pasting on these platforms
#[cfg(target_os = "linux")]
pub mod fuse;
#[cfg(target_os = "macos")]
pub mod macos;
pub mod local_file;
pub mod serv_files;
/// 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
pub const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334;
pub const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW";
// not actual format id, just a placeholder
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(
[
(
FILEDESCRIPTOR_FORMAT_ID,
FILEDESCRIPTORW_FORMAT_NAME.to_string()
),
(FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME.to_string())
]
.iter()
.cloned()
);
}
#[inline]
pub fn get_local_format(remote_id: i32) -> Option<String> {
REMOTE_FORMAT_MAP.get(&remote_id).map(|s| s.clone())
}
@@ -0,0 +1,271 @@
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, usize};
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>,
first_file_index: usize,
files_pdu: Vec<u8>,
}
impl ClipFiles {
fn clear(&mut self) {
self.files.clear();
self.file_list.clear();
self.first_file_index = usize::MAX;
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.first_file_index = self
.file_list
.iter()
.position(|f| !f.path.is_dir())
.unwrap_or(usize::MAX);
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 get_files_for_audit(&self, request: &FileContentsRequest) -> Option<ClipboardFile> {
if let FileContentsRequest::Range {
file_idx, offset, ..
} = request
{
if *file_idx == self.first_file_index && *offset == 0 {
let files: Vec<(String, u64)> = self
.file_list
.iter()
.filter_map(|f| {
if f.path.is_file() {
Some((f.path.to_string_lossy().to_string(), f.size))
} else {
None
}
})
.collect::<_>();
if files.is_empty() {
return None;
} else {
return Some(ClipboardFile::Files { files });
}
}
}
None
}
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,
) -> Vec<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 vec![Err(CliprdrError::InvalidRequest {
description: format!("got invalid FileContentsRequest, dw_flats: {dw_flags}"),
})];
};
let mut clip_files = CLIP_FILES.lock();
let mut res = vec![];
if let Some(files_res) = clip_files.get_files_for_audit(&fcr) {
res.push(Ok(files_res));
}
res.push(clip_files.serve_file_contents(conn_id, fcr));
res
}
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()
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff