patch: fix file list parsing
Signed-off-by: ClSlaid <cailue@bupt.edu.cn>
This commit is contained in:
parent
169bbfd2db
commit
d0dc22794e
libs/clipboard/src/platform
@ -632,7 +632,7 @@ impl FileDescription {
|
|||||||
CliprdrError::ConversionFailure
|
CliprdrError::ConversionFailure
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let valid_attributes = flags & 0x01 != 0;
|
let valid_attributes = flags & 0x04 != 0;
|
||||||
if !valid_attributes {
|
if !valid_attributes {
|
||||||
return Err(CliprdrError::InvalidRequest {
|
return Err(CliprdrError::InvalidRequest {
|
||||||
description: "file description must have valid attributes".to_string(),
|
description: "file description must have valid attributes".to_string(),
|
||||||
@ -648,14 +648,14 @@ impl FileDescription {
|
|||||||
FileType::File
|
FileType::File
|
||||||
};
|
};
|
||||||
|
|
||||||
let valid_size = flags & 0x80 != 0;
|
let valid_size = flags & 0x40 != 0;
|
||||||
let size = if valid_size {
|
let size = if valid_size {
|
||||||
((file_size_high as u64) << 32) + file_size_low as u64
|
((file_size_high as u64) << 32) + file_size_low as u64
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
let valid_write_time = flags & 0x100 != 0;
|
let valid_write_time = flags & 0x20 != 0;
|
||||||
let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA {
|
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 = (last_write_time - LDAP_EPOCH_DELTA) * 100;
|
||||||
let last_write_time = Duration::from_nanos(last_write_time);
|
let last_write_time = Duration::from_nanos(last_write_time);
|
||||||
@ -665,7 +665,7 @@ impl FileDescription {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let name = wstr.to_utf8().replace('\\', "/");
|
let name = wstr.to_utf8().replace('\\', "/");
|
||||||
let name = PathBuf::from(name);
|
let name = PathBuf::from(name.trim_end_matches('\0'));
|
||||||
|
|
||||||
let desc = FileDescription {
|
let desc = FileDescription {
|
||||||
conn_id,
|
conn_id,
|
||||||
@ -1015,11 +1015,25 @@ mod fuse_test {
|
|||||||
// todo: more tests needed!
|
// todo: more tests needed!
|
||||||
|
|
||||||
fn generate_descriptions() -> Vec<FileDescription> {
|
fn generate_descriptions() -> Vec<FileDescription> {
|
||||||
let folder0 = FileDescription::new("folder0", FileType::Directory, 0, 0);
|
fn desc_gen(name: &str, kind: FileType) -> FileDescription {
|
||||||
let file0 = FileDescription::new("folder0/file0", FileType::File, 1, 0);
|
FileDescription {
|
||||||
let file1 = FileDescription::new("folder0/file1", FileType::File, 1, 0);
|
conn_id: 0,
|
||||||
let folder1 = FileDescription::new("folder1", FileType::Directory, 0, 0);
|
name: PathBuf::from(name),
|
||||||
let file2 = FileDescription::new("folder1/file2", FileType::File, 4, 0);
|
kind,
|
||||||
|
atime: SystemTime::UNIX_EPOCH,
|
||||||
|
last_modified: SystemTime::UNIX_EPOCH,
|
||||||
|
last_metadata_changed: SystemTime::UNIX_EPOCH,
|
||||||
|
creation_time: SystemTime::UNIX_EPOCH,
|
||||||
|
|
||||||
|
size: 0,
|
||||||
|
perm: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let folder0 = desc_gen("folder0", FileType::Directory);
|
||||||
|
let file0 = desc_gen("folder0/file0", FileType::File);
|
||||||
|
let file1 = desc_gen("folder0/file1", FileType::File);
|
||||||
|
let folder1 = desc_gen("folder1", FileType::Directory);
|
||||||
|
let file2 = desc_gen("folder1/file2", FileType::File);
|
||||||
|
|
||||||
vec![folder0, file0, file1, folder1, file2]
|
vec![folder0, file0, file1, folder1, file2]
|
||||||
}
|
}
|
||||||
@ -1053,15 +1067,15 @@ mod fuse_test {
|
|||||||
assert_eq!(tree_list[1].children, vec![3, 4]);
|
assert_eq!(tree_list[1].children, vec![3, 4]);
|
||||||
|
|
||||||
assert_eq!(tree_list[2].name, "file0"); // inode 3
|
assert_eq!(tree_list[2].name, "file0"); // inode 3
|
||||||
assert_eq!(tree_list[2].children, vec![]);
|
assert!(tree_list[2].children.is_empty());
|
||||||
|
|
||||||
assert_eq!(tree_list[3].name, "file1"); // inode 4
|
assert_eq!(tree_list[3].name, "file1"); // inode 4
|
||||||
assert_eq!(tree_list[3].children, vec![]);
|
assert!(tree_list[3].children.is_empty());
|
||||||
|
|
||||||
assert_eq!(tree_list[4].name, "folder1"); // inode 5
|
assert_eq!(tree_list[4].name, "folder1"); // inode 5
|
||||||
assert_eq!(tree_list[4].children, vec![6]);
|
assert_eq!(tree_list[4].children, vec![6]);
|
||||||
|
|
||||||
assert_eq!(tree_list[5].name, "file2"); // inode 6
|
assert_eq!(tree_list[5].name, "file2"); // inode 6
|
||||||
assert_eq!(tree_list[5].children, vec![]);
|
assert!(tree_list[5].children.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
277
libs/clipboard/src/platform/linux/local_file.rs
Normal file
277
libs/clipboard/src/platform/linux/local_file.rs
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
use std::{collections::HashSet, fs::File, path::PathBuf, time::SystemTime};
|
||||||
|
|
||||||
|
use hbb_common::{
|
||||||
|
bytes::{BufMut, BytesMut},
|
||||||
|
log,
|
||||||
|
};
|
||||||
|
use utf16string::WString;
|
||||||
|
|
||||||
|
use crate::{platform::LDAP_EPOCH_DELTA, CliprdrError};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(super) struct LocalFile {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub handle: Option<File>,
|
||||||
|
|
||||||
|
pub name: String,
|
||||||
|
pub size: u64,
|
||||||
|
pub last_write_time: SystemTime,
|
||||||
|
pub is_dir: bool,
|
||||||
|
pub read_only: bool,
|
||||||
|
pub hidden: bool,
|
||||||
|
pub system: bool,
|
||||||
|
pub archive: bool,
|
||||||
|
pub normal: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalFile {
|
||||||
|
pub fn try_open(path: &PathBuf) -> Result<Self, CliprdrError> {
|
||||||
|
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
|
||||||
|
path: path.clone(),
|
||||||
|
err: e,
|
||||||
|
})?;
|
||||||
|
let size = mt.len() as u64;
|
||||||
|
let is_dir = mt.is_dir();
|
||||||
|
let read_only = mt.permissions().readonly();
|
||||||
|
let system = false;
|
||||||
|
let hidden = false;
|
||||||
|
let archive = false;
|
||||||
|
let normal = !is_dir;
|
||||||
|
let last_write_time = mt.modified().unwrap_or(SystemTime::UNIX_EPOCH);
|
||||||
|
|
||||||
|
let name = path
|
||||||
|
.display()
|
||||||
|
.to_string()
|
||||||
|
.trim_start_matches('/')
|
||||||
|
.replace('/', "\\");
|
||||||
|
|
||||||
|
let handle = if is_dir {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let file = std::fs::File::open(path).map_err(|e| CliprdrError::FileError {
|
||||||
|
path: path.clone(),
|
||||||
|
err: e,
|
||||||
|
})?;
|
||||||
|
let reader = file;
|
||||||
|
Some(reader)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
name,
|
||||||
|
path: path.clone(),
|
||||||
|
handle,
|
||||||
|
size,
|
||||||
|
last_write_time,
|
||||||
|
is_dir,
|
||||||
|
read_only,
|
||||||
|
system,
|
||||||
|
hidden,
|
||||||
|
archive,
|
||||||
|
normal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn as_bin(&self) -> Vec<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()
|
||||||
|
.as_nanos() as u64
|
||||||
|
/ 100
|
||||||
|
+ LDAP_EPOCH_DELTA;
|
||||||
|
|
||||||
|
let size_high = (self.size >> 32) as u32;
|
||||||
|
let size_low = (self.size & (u32::MAX as u64)) as u32;
|
||||||
|
|
||||||
|
let path = self.path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let wstr: WString<utf16string::LE> = WString::from(&path);
|
||||||
|
let name = wstr.as_bytes();
|
||||||
|
|
||||||
|
log::debug!(
|
||||||
|
"put file to list: name_len {}, name {}",
|
||||||
|
name.len(),
|
||||||
|
&self.name
|
||||||
|
);
|
||||||
|
|
||||||
|
let flags = 0x4064;
|
||||||
|
|
||||||
|
// flags, 4 bytes
|
||||||
|
buf.put_u32_le(flags);
|
||||||
|
// 32 bytes reserved
|
||||||
|
buf.put(&[0u8; 32][..]);
|
||||||
|
// file attributes, 4 bytes
|
||||||
|
buf.put_u32_le(file_attributes);
|
||||||
|
// 16 bytes reserved
|
||||||
|
buf.put(&[0u8; 16][..]);
|
||||||
|
// last write time, 8 bytes
|
||||||
|
buf.put_u64_le(win32_time);
|
||||||
|
// file size (high)
|
||||||
|
buf.put_u32_le(size_high);
|
||||||
|
// file size (low)
|
||||||
|
buf.put_u32_le(size_low);
|
||||||
|
// put name and padding to 520 bytes
|
||||||
|
let name_len = name.len();
|
||||||
|
buf.put(name);
|
||||||
|
buf.put(&vec![0u8; 520 - name_len][..]);
|
||||||
|
|
||||||
|
buf.to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
|
||||||
|
fn constr_file_lst(
|
||||||
|
path: &PathBuf,
|
||||||
|
file_list: &mut Vec<LocalFile>,
|
||||||
|
visited: &mut HashSet<PathBuf>,
|
||||||
|
) -> Result<(), CliprdrError> {
|
||||||
|
// prevent fs loop
|
||||||
|
if visited.contains(path) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
visited.insert(path.clone());
|
||||||
|
|
||||||
|
let local_file = LocalFile::try_open(path)?;
|
||||||
|
file_list.push(local_file);
|
||||||
|
|
||||||
|
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
|
||||||
|
path: path.clone(),
|
||||||
|
err: e,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if mt.is_dir() {
|
||||||
|
let dir = std::fs::read_dir(path).unwrap();
|
||||||
|
for entry in dir {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
let path = entry.path();
|
||||||
|
constr_file_lst(&path, file_list, visited)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file_list = Vec::new();
|
||||||
|
let mut visited = HashSet::new();
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
constr_file_lst(path, &mut file_list, &mut visited)?;
|
||||||
|
}
|
||||||
|
Ok(file_list)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod file_list_test {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use hbb_common::bytes::{BufMut, BytesMut};
|
||||||
|
|
||||||
|
use crate::{platform::fuse::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 {
|
||||||
|
path: PathBuf::from(path),
|
||||||
|
handle: None,
|
||||||
|
name: name.to_string(),
|
||||||
|
size: 0,
|
||||||
|
last_write_time: std::time::SystemTime::UNIX_EPOCH,
|
||||||
|
read_only: false,
|
||||||
|
is_dir,
|
||||||
|
hidden: false,
|
||||||
|
system: false,
|
||||||
|
archive: false,
|
||||||
|
normal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = prefix;
|
||||||
|
|
||||||
|
let (r_path, a_path, b_path, c_path) = if "" != prefix {
|
||||||
|
(
|
||||||
|
format!("{}", p),
|
||||||
|
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 {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,4 @@
|
|||||||
use std::{
|
use std::{os::unix::prelude::FileExt, path::PathBuf, sync::Arc, time::Duration};
|
||||||
collections::HashSet,
|
|
||||||
fs::File,
|
|
||||||
os::unix::prelude::FileExt,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::Arc,
|
|
||||||
time::{Duration, SystemTime},
|
|
||||||
};
|
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use fuser::MountOption;
|
use fuser::MountOption;
|
||||||
@ -15,17 +8,23 @@ use hbb_common::{
|
|||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use utf16string::WString;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
platform::fuse::FileDescription, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext,
|
platform::{fuse::FileDescription, linux::local_file::construct_file_list},
|
||||||
|
send_data, ClipboardFile, CliprdrError, CliprdrServiceContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{fuse::FuseServer, LDAP_EPOCH_DELTA};
|
use self::local_file::LocalFile;
|
||||||
|
use self::url::{encode_path_to_uri, parse_plain_uri_list};
|
||||||
|
|
||||||
|
use super::fuse::FuseServer;
|
||||||
|
|
||||||
#[cfg(not(feature = "wayland"))]
|
#[cfg(not(feature = "wayland"))]
|
||||||
pub mod x11;
|
pub mod x11;
|
||||||
|
|
||||||
|
pub mod local_file;
|
||||||
|
pub mod url;
|
||||||
|
|
||||||
// not actual format id, just a placeholder
|
// not actual format id, just a placeholder
|
||||||
const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334;
|
const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334;
|
||||||
const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW";
|
const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW";
|
||||||
@ -76,231 +75,6 @@ fn get_sys_clipboard(ignore_path: &PathBuf) -> Result<Box<dyn SysClipboard>, Cli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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'/');
|
|
||||||
|
|
||||||
fn encode_path_to_uri(path: &PathBuf) -> String {
|
|
||||||
let encoded = percent_encoding::percent_encode(path.to_str().unwrap().as_bytes(), &ENCODE_SET)
|
|
||||||
.to_string();
|
|
||||||
format!("file://{}", encoded)
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod uri_test {
|
|
||||||
#[test]
|
|
||||||
fn test_conversion() {
|
|
||||||
let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png");
|
|
||||||
let uri = super::encode_path_to_uri(&path);
|
|
||||||
assert_eq!(
|
|
||||||
uri,
|
|
||||||
"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png"
|
|
||||||
);
|
|
||||||
let convert_back = super::parse_uri_to_path(&uri).unwrap();
|
|
||||||
assert_eq!(path, convert_back);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper parse function
|
|
||||||
// convert 'text/uri-list' data to a list of valid Paths
|
|
||||||
// # Note
|
|
||||||
// - none utf8 data will lead to error
|
|
||||||
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
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct LocalFile {
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub handle: Option<File>,
|
|
||||||
|
|
||||||
pub name: String,
|
|
||||||
pub size: u64,
|
|
||||||
pub last_write_time: SystemTime,
|
|
||||||
pub is_dir: bool,
|
|
||||||
pub read_only: bool,
|
|
||||||
pub hidden: bool,
|
|
||||||
pub system: bool,
|
|
||||||
pub archive: bool,
|
|
||||||
pub normal: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LocalFile {
|
|
||||||
pub fn try_open(path: &PathBuf) -> Result<Self, CliprdrError> {
|
|
||||||
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
|
|
||||||
path: path.clone(),
|
|
||||||
err: e,
|
|
||||||
})?;
|
|
||||||
let size = mt.len() as u64;
|
|
||||||
let is_dir = mt.is_dir();
|
|
||||||
let read_only = mt.permissions().readonly();
|
|
||||||
let system = false;
|
|
||||||
let hidden = false;
|
|
||||||
let archive = false;
|
|
||||||
let normal = !is_dir;
|
|
||||||
let last_write_time = mt.modified().unwrap_or(SystemTime::UNIX_EPOCH);
|
|
||||||
|
|
||||||
let name = path
|
|
||||||
.display()
|
|
||||||
.to_string()
|
|
||||||
.trim_start_matches('/')
|
|
||||||
.replace('/', "\\");
|
|
||||||
|
|
||||||
let handle = if is_dir {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let file = std::fs::File::open(path).map_err(|e| CliprdrError::FileError {
|
|
||||||
path: path.clone(),
|
|
||||||
err: e,
|
|
||||||
})?;
|
|
||||||
let reader = file;
|
|
||||||
Some(reader)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
name,
|
|
||||||
path: path.clone(),
|
|
||||||
handle,
|
|
||||||
size,
|
|
||||||
last_write_time,
|
|
||||||
is_dir,
|
|
||||||
read_only,
|
|
||||||
system,
|
|
||||||
hidden,
|
|
||||||
archive,
|
|
||||||
normal,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn as_bin(&self) -> Vec<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()
|
|
||||||
.as_nanos() as u64
|
|
||||||
/ 100
|
|
||||||
+ LDAP_EPOCH_DELTA;
|
|
||||||
|
|
||||||
let size_high = (self.size >> 32) as u32;
|
|
||||||
let size_low = (self.size & (u32::MAX as u64)) as u32;
|
|
||||||
|
|
||||||
let wstr: WString<utf16string::LE> = WString::from(&self.name);
|
|
||||||
let name = wstr.as_bytes();
|
|
||||||
|
|
||||||
log::debug!(
|
|
||||||
"put file to list: name_len {}, name {}",
|
|
||||||
name.len(),
|
|
||||||
&self.name
|
|
||||||
);
|
|
||||||
|
|
||||||
let flags = 0x4064;
|
|
||||||
|
|
||||||
// flags, 4 bytes
|
|
||||||
buf.put_u32_le(flags);
|
|
||||||
// 32 bytes reserved
|
|
||||||
buf.put(&[0u8; 32][..]);
|
|
||||||
// file attributes, 4 bytes
|
|
||||||
buf.put_u32_le(file_attributes);
|
|
||||||
// 16 bytes reserved
|
|
||||||
buf.put(&[0u8; 16][..]);
|
|
||||||
// last write time, 8 bytes
|
|
||||||
buf.put_u64_le(win32_time);
|
|
||||||
// file size (high)
|
|
||||||
buf.put_u32_le(size_high);
|
|
||||||
// file size (low)
|
|
||||||
buf.put_u32_le(size_low);
|
|
||||||
// put name and padding to 520 bytes
|
|
||||||
let name_len = name.len();
|
|
||||||
buf.put(name);
|
|
||||||
buf.put(&vec![0u8; 520 - name_len][..]);
|
|
||||||
|
|
||||||
buf.to_vec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
|
|
||||||
fn constr_file_lst(
|
|
||||||
path: &PathBuf,
|
|
||||||
file_list: &mut Vec<LocalFile>,
|
|
||||||
visited: &mut HashSet<PathBuf>,
|
|
||||||
) -> Result<(), CliprdrError> {
|
|
||||||
// prevent fs loop
|
|
||||||
if visited.contains(path) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
visited.insert(path.clone());
|
|
||||||
|
|
||||||
let local_file = LocalFile::try_open(path)?;
|
|
||||||
file_list.push(local_file);
|
|
||||||
|
|
||||||
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
|
|
||||||
path: path.clone(),
|
|
||||||
err: e,
|
|
||||||
})?;
|
|
||||||
if mt.is_dir() {
|
|
||||||
let dir = std::fs::read_dir(path).unwrap();
|
|
||||||
for entry in dir {
|
|
||||||
let entry = entry.unwrap();
|
|
||||||
let path = entry.path();
|
|
||||||
constr_file_lst(&path, file_list, visited)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file_list = Vec::new();
|
|
||||||
let mut visited = HashSet::new();
|
|
||||||
|
|
||||||
for path in paths {
|
|
||||||
constr_file_lst(path, &mut file_list, &mut visited)?;
|
|
||||||
}
|
|
||||||
Ok(file_list)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum FileContentsRequest {
|
enum FileContentsRequest {
|
||||||
Size {
|
Size {
|
||||||
@ -623,13 +397,17 @@ impl ClipboardContext {
|
|||||||
msg_flags,
|
msg_flags,
|
||||||
format_data,
|
format_data,
|
||||||
} => {
|
} => {
|
||||||
log::debug!("server_format_data_response called");
|
log::debug!(
|
||||||
|
"server_format_data_response called, msg_flags={}",
|
||||||
|
msg_flags
|
||||||
|
);
|
||||||
|
|
||||||
if msg_flags != 0x1 {
|
if msg_flags != 0x1 {
|
||||||
resp_format_data_failure(conn_id);
|
resp_format_data_failure(conn_id);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::debug!("parsing file descriptors");
|
||||||
// this must be a file descriptor format data
|
// this must be a file descriptor format data
|
||||||
let files = FileDescription::parse_file_descriptors(format_data.into(), conn_id)?;
|
let files = FileDescription::parse_file_descriptors(format_data.into(), conn_id)?;
|
||||||
|
|
||||||
@ -640,6 +418,7 @@ impl ClipboardContext {
|
|||||||
fuse_guard.list_root()
|
fuse_guard.list_root()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
log::debug!("load file list: {:?}", paths);
|
||||||
self.set_clipboard(&paths)?;
|
self.set_clipboard(&paths)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
65
libs/clipboard/src/platform/linux/url.rs
Normal file
65
libs/clipboard/src/platform/linux/url.rs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::CliprdrError;
|
||||||
|
|
||||||
|
// on x11, path will be encode as
|
||||||
|
// "/home/rustdesk/pictures/🖼️.png" -> "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png"
|
||||||
|
// url encode and decode is needed
|
||||||
|
const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/');
|
||||||
|
|
||||||
|
pub(super) fn encode_path_to_uri(path: &PathBuf) -> String {
|
||||||
|
let encoded = percent_encoding::percent_encode(path.to_str().unwrap().as_bytes(), &ENCODE_SET)
|
||||||
|
.to_string();
|
||||||
|
format!("file://{}", encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_uri_to_path(encoded_uri: &str) -> Result<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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user