367 lines
10 KiB
Rust
367 lines
10 KiB
Rust
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<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(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 = 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<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::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<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,
|
|
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(())
|
|
}
|
|
}
|