From d0dc22794eb8e5380b640fe3b6b8b954159a0184 Mon Sep 17 00:00:00 2001 From: ClSlaid Date: Thu, 19 Oct 2023 20:01:44 +0800 Subject: [PATCH] patch: fix file list parsing Signed-off-by: ClSlaid --- libs/clipboard/src/platform/fuse.rs | 38 ++- .../src/platform/linux/local_file.rs | 277 ++++++++++++++++++ libs/clipboard/src/platform/linux/mod.rs | 253 +--------------- libs/clipboard/src/platform/linux/url.rs | 65 ++++ 4 files changed, 384 insertions(+), 249 deletions(-) create mode 100644 libs/clipboard/src/platform/linux/local_file.rs create mode 100644 libs/clipboard/src/platform/linux/url.rs diff --git a/libs/clipboard/src/platform/fuse.rs b/libs/clipboard/src/platform/fuse.rs index cf0263309..b401c3d3a 100644 --- a/libs/clipboard/src/platform/fuse.rs +++ b/libs/clipboard/src/platform/fuse.rs @@ -632,7 +632,7 @@ impl FileDescription { CliprdrError::ConversionFailure })?; - let valid_attributes = flags & 0x01 != 0; + let valid_attributes = flags & 0x04 != 0; if !valid_attributes { return Err(CliprdrError::InvalidRequest { description: "file description must have valid attributes".to_string(), @@ -648,14 +648,14 @@ impl FileDescription { FileType::File }; - let valid_size = flags & 0x80 != 0; + let valid_size = flags & 0x40 != 0; let size = if valid_size { ((file_size_high as u64) << 32) + file_size_low as u64 } else { 0 }; - let valid_write_time = flags & 0x100 != 0; + let valid_write_time = flags & 0x20 != 0; let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA { let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100; let last_write_time = Duration::from_nanos(last_write_time); @@ -665,7 +665,7 @@ impl FileDescription { }; let name = wstr.to_utf8().replace('\\', "/"); - let name = PathBuf::from(name); + let name = PathBuf::from(name.trim_end_matches('\0')); let desc = FileDescription { conn_id, @@ -1015,11 +1015,25 @@ mod fuse_test { // todo: more tests needed! fn generate_descriptions() -> Vec { - let folder0 = FileDescription::new("folder0", FileType::Directory, 0, 0); - let file0 = FileDescription::new("folder0/file0", FileType::File, 1, 0); - let file1 = FileDescription::new("folder0/file1", FileType::File, 1, 0); - let folder1 = FileDescription::new("folder1", FileType::Directory, 0, 0); - let file2 = FileDescription::new("folder1/file2", FileType::File, 4, 0); + fn desc_gen(name: &str, kind: FileType) -> FileDescription { + FileDescription { + conn_id: 0, + name: PathBuf::from(name), + kind, + atime: SystemTime::UNIX_EPOCH, + last_modified: SystemTime::UNIX_EPOCH, + last_metadata_changed: SystemTime::UNIX_EPOCH, + creation_time: SystemTime::UNIX_EPOCH, + + size: 0, + perm: 0, + } + } + 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] } @@ -1053,15 +1067,15 @@ mod fuse_test { assert_eq!(tree_list[1].children, vec![3, 4]); 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].children, vec![]); + assert!(tree_list[3].children.is_empty()); assert_eq!(tree_list[4].name, "folder1"); // inode 5 assert_eq!(tree_list[4].children, vec![6]); assert_eq!(tree_list[5].name, "file2"); // inode 6 - assert_eq!(tree_list[5].children, vec![]); + assert!(tree_list[5].children.is_empty()); } } diff --git a/libs/clipboard/src/platform/linux/local_file.rs b/libs/clipboard/src/platform/linux/local_file.rs new file mode 100644 index 000000000..710224c1e --- /dev/null +++ b/libs/clipboard/src/platform/linux/local_file.rs @@ -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, + + pub name: String, + pub size: u64, + pub last_write_time: SystemTime, + pub is_dir: bool, + pub read_only: bool, + pub hidden: bool, + pub system: bool, + pub archive: bool, + pub normal: bool, +} + +impl LocalFile { + pub fn try_open(path: &PathBuf) -> Result { + let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { + path: path.clone(), + err: e, + })?; + let size = mt.len() as u64; + let is_dir = mt.is_dir(); + let read_only = mt.permissions().readonly(); + let system = false; + let hidden = false; + let archive = false; + let normal = !is_dir; + let last_write_time = mt.modified().unwrap_or(SystemTime::UNIX_EPOCH); + + let name = path + .display() + .to_string() + .trim_start_matches('/') + .replace('/', "\\"); + + let handle = if is_dir { + None + } else { + let file = std::fs::File::open(path).map_err(|e| CliprdrError::FileError { + path: path.clone(), + err: e, + })?; + let reader = file; + Some(reader) + }; + + Ok(Self { + name, + path: path.clone(), + handle, + size, + last_write_time, + is_dir, + read_only, + system, + hidden, + archive, + normal, + }) + } + pub fn as_bin(&self) -> Vec { + let mut buf = BytesMut::with_capacity(592); + + let read_only_flag = if self.read_only { 0x1 } else { 0 }; + let hidden_flag = if self.hidden { 0x2 } else { 0 }; + let system_flag = if self.system { 0x4 } else { 0 }; + let directory_flag = if self.is_dir { 0x10 } else { 0 }; + let archive_flag = if self.archive { 0x20 } else { 0 }; + let normal_flag = if self.normal { 0x80 } else { 0 }; + + let file_attributes: u32 = read_only_flag + | hidden_flag + | system_flag + | directory_flag + | archive_flag + | normal_flag; + + let win32_time = self + .last_write_time + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64 + / 100 + + LDAP_EPOCH_DELTA; + + let size_high = (self.size >> 32) as u32; + let size_low = (self.size & (u32::MAX as u64)) as u32; + + let path = self.path.to_string_lossy().to_string(); + + let wstr: WString = 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, CliprdrError> { + fn constr_file_lst( + path: &PathBuf, + file_list: &mut Vec, + visited: &mut HashSet, + ) -> Result<(), CliprdrError> { + // prevent fs loop + if visited.contains(path) { + return Ok(()); + } + visited.insert(path.clone()); + + let local_file = LocalFile::try_open(path)?; + file_list.push(local_file); + + let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { + path: path.clone(), + err: e, + })?; + + if mt.is_dir() { + let dir = std::fs::read_dir(path).unwrap(); + for entry in dir { + let entry = entry.unwrap(); + let path = entry.path(); + constr_file_lst(&path, file_list, visited)?; + } + } + Ok(()) + } + + let mut file_list = Vec::new(); + let mut visited = HashSet::new(); + + for path in paths { + constr_file_lst(path, &mut file_list, &mut visited)?; + } + Ok(file_list) +} + +#[cfg(test)] +mod file_list_test { + use std::path::PathBuf; + + use hbb_common::bytes::{BufMut, BytesMut}; + + use crate::{platform::fuse::FileDescription, CliprdrError}; + + use super::LocalFile; + + #[inline] + fn generate_tree(prefix: &str) -> Vec { + // generate a tree of local files, no handles + // - / + // |- a.txt + // |- b + // |- c.txt + #[inline] + fn generate_file(path: &str, name: &str, is_dir: bool) -> LocalFile { + LocalFile { + path: PathBuf::from(path), + handle: None, + name: name.to_string(), + size: 0, + 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(()) + } +} diff --git a/libs/clipboard/src/platform/linux/mod.rs b/libs/clipboard/src/platform/linux/mod.rs index a37db0f3f..0f5b7d763 100644 --- a/libs/clipboard/src/platform/linux/mod.rs +++ b/libs/clipboard/src/platform/linux/mod.rs @@ -1,11 +1,4 @@ -use std::{ - collections::HashSet, - fs::File, - os::unix::prelude::FileExt, - path::{Path, PathBuf}, - sync::Arc, - time::{Duration, SystemTime}, -}; +use std::{os::unix::prelude::FileExt, path::PathBuf, sync::Arc, time::Duration}; use dashmap::DashMap; use fuser::MountOption; @@ -15,17 +8,23 @@ use hbb_common::{ }; use lazy_static::lazy_static; use parking_lot::Mutex; -use utf16string::WString; 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"))] pub mod x11; +pub mod local_file; +pub mod url; + // not actual format id, just a placeholder const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334; const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW"; @@ -76,231 +75,6 @@ fn get_sys_clipboard(ignore_path: &PathBuf) -> Result, 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 { - 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) -> Result, CliprdrError> { - let text = String::from_utf8(v).map_err(|_| CliprdrError::ConversionFailure)?; - parse_uri_list(&text) -} - -// helper parse function -// convert 'text/uri-list' data to a list of valid Paths -// # Note -// - none utf8 data will lead to error -fn parse_uri_list(text: &str) -> Result, CliprdrError> { - let mut list = Vec::new(); - - for line in text.lines() { - if !line.starts_with("file://") { - continue; - } - let decoded = parse_uri_to_path(line)?; - list.push(decoded) - } - Ok(list) -} - -#[derive(Debug)] -struct LocalFile { - pub path: PathBuf, - pub handle: Option, - - pub name: String, - pub size: u64, - pub last_write_time: SystemTime, - pub is_dir: bool, - pub read_only: bool, - pub hidden: bool, - pub system: bool, - pub archive: bool, - pub normal: bool, -} - -impl LocalFile { - pub fn try_open(path: &PathBuf) -> Result { - let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { - path: path.clone(), - err: e, - })?; - let size = mt.len() as u64; - let is_dir = mt.is_dir(); - let read_only = mt.permissions().readonly(); - let system = false; - let hidden = false; - let archive = false; - let normal = !is_dir; - let last_write_time = mt.modified().unwrap_or(SystemTime::UNIX_EPOCH); - - let name = path - .display() - .to_string() - .trim_start_matches('/') - .replace('/', "\\"); - - let handle = if is_dir { - None - } else { - let file = std::fs::File::open(path).map_err(|e| CliprdrError::FileError { - path: path.clone(), - err: e, - })?; - let reader = file; - Some(reader) - }; - - Ok(Self { - name, - path: path.clone(), - handle, - size, - last_write_time, - is_dir, - read_only, - system, - hidden, - archive, - normal, - }) - } - pub fn as_bin(&self) -> Vec { - let mut buf = BytesMut::with_capacity(592); - - let read_only_flag = if self.read_only { 0x1 } else { 0 }; - let hidden_flag = if self.hidden { 0x2 } else { 0 }; - let system_flag = if self.system { 0x4 } else { 0 }; - let directory_flag = if self.is_dir { 0x10 } else { 0 }; - let archive_flag = if self.archive { 0x20 } else { 0 }; - let normal_flag = if self.normal { 0x80 } else { 0 }; - - let file_attributes: u32 = read_only_flag - | hidden_flag - | system_flag - | directory_flag - | archive_flag - | normal_flag; - - let win32_time = self - .last_write_time - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() as u64 - / 100 - + LDAP_EPOCH_DELTA; - - let size_high = (self.size >> 32) as u32; - let size_low = (self.size & (u32::MAX as u64)) as u32; - - let wstr: WString = WString::from(&self.name); - let name = wstr.as_bytes(); - - 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, CliprdrError> { - fn constr_file_lst( - path: &PathBuf, - file_list: &mut Vec, - visited: &mut HashSet, - ) -> Result<(), CliprdrError> { - // prevent fs loop - if visited.contains(path) { - return Ok(()); - } - visited.insert(path.clone()); - - let local_file = LocalFile::try_open(path)?; - file_list.push(local_file); - - let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { - path: path.clone(), - err: e, - })?; - if mt.is_dir() { - let dir = std::fs::read_dir(path).unwrap(); - for entry in dir { - let entry = entry.unwrap(); - let path = entry.path(); - constr_file_lst(&path, file_list, visited)?; - } - } - Ok(()) - } - - let mut file_list = Vec::new(); - let mut visited = HashSet::new(); - - for path in paths { - constr_file_lst(path, &mut file_list, &mut visited)?; - } - Ok(file_list) -} - #[derive(Debug)] enum FileContentsRequest { Size { @@ -623,13 +397,17 @@ impl ClipboardContext { msg_flags, format_data, } => { - log::debug!("server_format_data_response called"); + log::debug!( + "server_format_data_response called, msg_flags={}", + msg_flags + ); if msg_flags != 0x1 { resp_format_data_failure(conn_id); return Ok(()); } + log::debug!("parsing file descriptors"); // this must be a file descriptor format data let files = FileDescription::parse_file_descriptors(format_data.into(), conn_id)?; @@ -640,6 +418,7 @@ impl ClipboardContext { fuse_guard.list_root() }; + log::debug!("load file list: {:?}", paths); self.set_clipboard(&paths)?; Ok(()) } diff --git a/libs/clipboard/src/platform/linux/url.rs b/libs/clipboard/src/platform/linux/url.rs new file mode 100644 index 000000000..401c8b5ba --- /dev/null +++ b/libs/clipboard/src/platform/linux/url.rs @@ -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 { + let encoded_path = encoded_uri.trim_start_matches("file://"); + let path_str = percent_encoding::percent_decode_str(encoded_path) + .decode_utf8() + .map_err(|_| CliprdrError::ConversionFailure)?; + let path_str = path_str.to_string(); + + Ok(Path::new(&path_str).to_path_buf()) +} + +// helper parse function +// convert 'text/uri-list' data to a list of valid Paths +// # Note +// - none utf8 data will lead to error +pub(super) fn parse_plain_uri_list(v: Vec) -> Result, CliprdrError> { + let text = String::from_utf8(v).map_err(|_| CliprdrError::ConversionFailure)?; + parse_uri_list(&text) +} + +// helper parse function +// convert 'text/uri-list' data to a list of valid Paths +// # Note +// - none utf8 data will lead to error +pub(super) fn parse_uri_list(text: &str) -> Result, CliprdrError> { + let mut list = Vec::new(); + + for line in text.lines() { + if !line.starts_with("file://") { + continue; + } + let decoded = parse_uri_to_path(line)?; + list.push(decoded) + } + Ok(list) +} + +#[cfg(test)] +mod uri_test { + #[test] + fn test_conversion() { + let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png"); + let uri = super::encode_path_to_uri(&path); + assert_eq!( + uri, + "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png" + ); + let convert_back = super::parse_uri_to_path(&uri).unwrap(); + assert_eq!(path, convert_back); + } +}