use std::{ collections::HashSet, fs::File, io::{BufRead, BufReader, Read, Seek}, os::unix::prelude::PermissionsExt, path::PathBuf, sync::atomic::{AtomicU64, Ordering}, time::SystemTime, }; use hbb_common::{ bytes::{BufMut, BytesMut}, log, }; use utf16string::WString; use crate::{ platform::{fuse::BLOCK_SIZE, LDAP_EPOCH_DELTA}, CliprdrError, }; /// has valid file attributes const FLAGS_FD_ATTRIBUTES: u32 = 0x04; /// has valid file size const FLAGS_FD_SIZE: u32 = 0x40; /// has valid last write time const FLAGS_FD_LAST_WRITE: u32 = 0x20; /// show progress const FLAGS_FD_PROGRESSUI: u32 = 0x4000; /// transferred from unix, contains file mode /// P.S. this flag is not used in windows const FLAGS_FD_UNIX_MODE: u32 = 0x08; #[derive(Debug)] pub(super) struct LocalFile { pub path: PathBuf, pub handle: Option>, 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(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 = 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, path: path.clone(), handle, offset, size, last_write_time, is_dir, read_only, system, hidden, perm, 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::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.clone(), err: e, })?; let mut reader = BufReader::with_capacity(BLOCK_SIZE as usize * 2, handle); reader.fill_buf().map_err(|e| CliprdrError::FileError { path: self.path.clone(), 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 handle = self.handle.as_mut().unwrap(); if offset != self.offset.load(Ordering::Relaxed) { handle .seek(std::io::SeekFrom::Start(offset)) .map_err(|e| CliprdrError::FileError { path: self.path.clone(), err: e, })?; } handle .read_exact(buf) .map_err(|e| CliprdrError::FileError { path: self.path.clone(), 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, 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, 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(()) } }