diff --git a/Cargo.lock b/Cargo.lock index 8d2d0a9e..3d227feb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,6 +383,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.90", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -479,6 +499,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + [[package]] name = "byteorder" version = "1.5.0" @@ -515,6 +541,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -570,6 +605,17 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.20" @@ -1098,6 +1144,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "half" version = "2.4.1" @@ -1283,6 +1335,16 @@ version = "0.2.160" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0b21006cd1874ae9e650973c565615676dc4a274c965bb0a73796dac838ce4f" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if 1.0.0", + "windows-link 0.2.1", +] + [[package]] name = "linux-keyutils" version = "0.2.4" @@ -1307,11 +1369,10 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -1363,6 +1424,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -1418,6 +1485,16 @@ dependencies = [ "memoffset 0.9.1", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1573,9 +1650,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1583,15 +1660,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -1605,6 +1682,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1887,6 +1970,10 @@ dependencies = [ "tracing-appender", "tracing-subscriber", "tracing-test", + "widestring", + "windows", + "winfsp", + "winfsp-sys", ] [[package]] @@ -1942,6 +2029,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2669,6 +2762,12 @@ dependencies = [ "winsafe", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -2700,6 +2799,114 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -2758,6 +2965,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2848,6 +3064,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winfsp" +version = "0.12.4+winfsp-2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51325fc20d218567a903cd5c382ad7d5a765f6860043f9c7d9ee1e907c5b75f6" +dependencies = [ + "bytemuck", + "parking_lot", + "paste", + "static_assertions", + "thiserror 2.0.6", + "widestring", + "windows", + "winfsp-sys", +] + +[[package]] +name = "winfsp-sys" +version = "0.12.1+winfsp-2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d42acc30c105f8d33507f556398237c738b681271c775a3b94eefd29d2b8c77e" +dependencies = [ + "bindgen", +] + [[package]] name = "winnow" version = "0.5.40" diff --git a/Cargo.toml b/Cargo.toml index 8484ba24..b94940ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,12 @@ criterion = { version = "0.5.1", features = ["html_reports"] } [target.'cfg(target_os = "linux")'.dependencies] fuse3 = { version = "0.8.1", features = ["tokio-runtime", "unprivileged"] } +[target.'cfg(target_os = "windows")'.dependencies] +winfsp = "0.12" +winfsp-sys = "0.12" +windows = { version = "0.61.3", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_IO"] } +widestring = "1.2.1" + [[bench]] name = "crypto_read" harness = false diff --git a/src/main.rs b/src/main.rs index d8986e0d..e49bd596 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,12 +2,12 @@ use anyhow::Result; mod keyring; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "windows"))] mod run; #[tokio::main] async fn main() -> Result<()> { - #[cfg(any(target_os = "macos", target_os = "windows"))] + #[cfg(target_os = "macos")] { eprintln!("he he, not yet ready for this platform, but soon my friend, soon :)"); eprintln!("Bye!"); @@ -20,6 +20,6 @@ async fn main() -> Result<()> { return Ok(()); } - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "windows"))] run::run().await } diff --git a/src/mount.rs b/src/mount.rs index 9eb5aa3d..77fbcb50 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -15,11 +15,18 @@ use linux::MountHandleInnerImpl; #[cfg(target_os = "linux")] use linux::MountPointImpl; -#[cfg(not(target_os = "linux"))] +#[cfg(target_os = "windows")] +mod windows; +#[cfg(target_os = "windows")] +use windows::MountHandleInnerImpl; +#[cfg(target_os = "windows")] +use windows::MountPointImpl; + +#[cfg(not(any(target_os = "linux", target_os = "windows")))] mod dummy; -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "windows")))] use dummy::MountHandleInnerImpl; -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "windows")))] use dummy::MountPointImpl; #[async_trait] diff --git a/src/mount/windows.rs b/src/mount/windows.rs new file mode 100644 index 00000000..c01a7da4 --- /dev/null +++ b/src/mount/windows.rs @@ -0,0 +1,915 @@ +use async_trait::async_trait; +use shush_rs::{ExposeSecret, SecretString}; +use std::ffi::c_void; +use std::future::Future; +use std::io; +use std::path::PathBuf; +use std::pin::Pin; +use std::str::FromStr; +use std::sync::Arc; + +use std::task::{Context, Poll}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tokio::runtime::Runtime; +use tracing::{debug, error, info, trace}; +use widestring::U16CStr; +use winfsp::filesystem::{ + DirBuffer, DirInfo, DirMarker, FileInfo, FileSecurity, FileSystemContext, OpenFileInfo, + VolumeInfo, WideNameInfo, +}; +use winfsp::host::{FileSystemHost, VolumeParams}; +use windows::Win32::Foundation::{ + STATUS_ACCESS_DENIED, STATUS_DIRECTORY_NOT_EMPTY, STATUS_DISK_CORRUPT_ERROR, + STATUS_FILE_IS_A_DIRECTORY, STATUS_INVALID_HANDLE, STATUS_INVALID_PARAMETER, + STATUS_IO_DEVICE_ERROR, STATUS_MEDIA_WRITE_PROTECTED, STATUS_NOT_A_DIRECTORY, + STATUS_OBJECT_NAME_COLLISION, STATUS_OBJECT_NAME_NOT_FOUND, +}; +use windows::Win32::Storage::FileSystem::{FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_NORMAL}; +use winfsp_sys::{FILE_ACCESS_RIGHTS, FILE_FLAGS_AND_ATTRIBUTES}; + +use crate::crypto::Cipher; +use crate::encryptedfs::{ + CreateFileAttr, EncryptedFs, FileAttr, FileType, FsError, FsResult, PasswordProvider, +}; +use crate::mount; +use crate::mount::{MountHandleInner, MountPoint}; + +const WINDOWS_TICK: u64 = 10_000_000; +const SEC_TO_UNIX_EPOCH: u64 = 11_644_473_600; +const FSP_CLEANUP_DELETE: u32 = 0x01; + +pub struct EncryptedFsFileContext { + ino: u64, + fh: Option, + is_directory: bool, + dir_buffer: DirBuffer, +} + +struct EncryptedFsWinFsp { + fs: Arc, + runtime: Runtime, + read_only: bool, +} + +impl EncryptedFsWinFsp { + pub fn new(fs: Arc, read_only: bool) -> Self { + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("Failed to create Tokio runtime"); + + Self { + fs, + runtime, + read_only, + } + } + + fn path_to_inode(&self, path: &U16CStr) -> winfsp::Result { + let path_str = path + .to_string() + .map_err(|_| STATUS_OBJECT_NAME_NOT_FOUND)?; + + if path_str.is_empty() || path_str == "\\" { + return Ok(1); + } + + let path_str = path_str.trim_start_matches('\\'); + let components: Vec<&str> = path_str.split('\\').filter(|s| !s.is_empty()).collect(); + let mut current_ino = 1u64; + + for component in components { + let secret_name = + SecretString::from_str(component).map_err(|_| STATUS_OBJECT_NAME_NOT_FOUND)?; + + let result = self + .runtime + .block_on(async { self.fs.find_by_name(current_ino, &secret_name).await }); + + match result { + Ok(Some(attr)) => current_ino = attr.ino, + Ok(None) => return Err(STATUS_OBJECT_NAME_NOT_FOUND.into()), + Err(e) => { + debug!("path_to_inode failed: {}", e); + return Err(STATUS_OBJECT_NAME_NOT_FOUND.into()); + } + } + } + + Ok(current_ino) + } + + fn path_to_parent_and_name(&self, path: &U16CStr) -> winfsp::Result<(u64, String)> { + let path_str = path + .to_string() + .map_err(|_| STATUS_OBJECT_NAME_NOT_FOUND)?; + + let path_str = path_str.trim_start_matches('\\'); + let components: Vec<&str> = path_str.split('\\').filter(|s| !s.is_empty()).collect(); + + if components.is_empty() { + return Err(STATUS_OBJECT_NAME_NOT_FOUND.into()); + } + + let name = components.last().unwrap().to_string(); + let parent_components = &components[..components.len() - 1]; + let mut parent_ino = 1u64; + + for component in parent_components { + let secret_name = + SecretString::from_str(component).map_err(|_| STATUS_OBJECT_NAME_NOT_FOUND)?; + + let result = self + .runtime + .block_on(async { self.fs.find_by_name(parent_ino, &secret_name).await }); + + match result { + Ok(Some(attr)) => parent_ino = attr.ino, + Ok(None) => return Err(STATUS_OBJECT_NAME_NOT_FOUND.into()), + Err(e) => { + debug!("path_to_parent_and_name failed: {}", e); + return Err(STATUS_OBJECT_NAME_NOT_FOUND.into()); + } + } + } + + Ok((parent_ino, name)) + } + + fn attr_to_file_info(attr: &FileAttr) -> FileInfo { + let mut file_info = FileInfo::default(); + + file_info.file_attributes = if attr.kind == FileType::Directory { + FILE_ATTRIBUTE_DIRECTORY.0 + } else { + FILE_ATTRIBUTE_NORMAL.0 + }; + + file_info.file_size = attr.size; + file_info.allocation_size = attr.blocks * attr.blksize as u64; + file_info.creation_time = system_time_to_filetime(attr.crtime); + file_info.last_access_time = system_time_to_filetime(attr.atime); + file_info.last_write_time = system_time_to_filetime(attr.mtime); + file_info.change_time = system_time_to_filetime(attr.ctime); + file_info.index_number = attr.ino; + + file_info + } + + fn refresh_file_info(&self, ino: u64, file_info: &mut FileInfo) { + if let Ok(attr) = self.runtime.block_on(async { self.fs.get_attr(ino).await }) { + *file_info = Self::attr_to_file_info(&attr); + } + } +} + +fn system_time_to_filetime(time: SystemTime) -> u64 { + match time.duration_since(UNIX_EPOCH) { + Ok(duration) => { + let secs = duration.as_secs(); + let nanos = duration.subsec_nanos() as u64; + (secs + SEC_TO_UNIX_EPOCH) * WINDOWS_TICK + nanos / 100 + } + Err(_) => 0, + } +} + +fn filetime_to_system_time(filetime: u64) -> SystemTime { + if filetime == 0 { + return UNIX_EPOCH; + } + + let secs = filetime / WINDOWS_TICK; + let nanos = ((filetime % WINDOWS_TICK) * 100) as u32; + + if secs >= SEC_TO_UNIX_EPOCH { + UNIX_EPOCH + Duration::new(secs - SEC_TO_UNIX_EPOCH, nanos) + } else { + UNIX_EPOCH + } +} + +impl FileSystemContext for EncryptedFsWinFsp { + type FileContext = EncryptedFsFileContext; + + fn get_volume_info(&self, out_volume_info: &mut VolumeInfo) -> winfsp::Result<()> { + trace!("get_volume_info"); + + // Report virtual volume size. These are placeholder values since encrypted + // filesystem size depends on underlying storage, not a fixed allocation. + // 100GB total / 50GB free provides reasonable defaults for Windows Explorer display. + out_volume_info.total_size = 1024 * 1024 * 1024 * 100; + out_volume_info.free_size = 1024 * 1024 * 1024 * 50; + out_volume_info.set_volume_label("rencfs"); + + Ok(()) + } + + fn get_security_by_name( + &self, + file_name: &U16CStr, + _security_descriptor: Option<&mut [c_void]>, + _resolve_reparse_points: impl FnOnce(&U16CStr) -> Option, + ) -> winfsp::Result { + trace!("get_security_by_name: {:?}", file_name.to_string()); + + let ino = self.path_to_inode(file_name)?; + let result = self + .runtime + .block_on(async { self.fs.get_attr(ino).await }); + + match result { + Ok(attr) => { + let attributes = if attr.kind == FileType::Directory { + FILE_ATTRIBUTE_DIRECTORY.0 + } else { + FILE_ATTRIBUTE_NORMAL.0 + }; + + Ok(FileSecurity { + attributes, + reparse: false, + sz_security_descriptor: 0, + }) + } + Err(e) => { + debug!("get_security_by_name failed: {}", e); + Err(STATUS_OBJECT_NAME_NOT_FOUND.into()) + } + } + } + + fn open( + &self, + file_name: &U16CStr, + _create_options: u32, + _granted_access: FILE_ACCESS_RIGHTS, + file_info: &mut OpenFileInfo, + ) -> winfsp::Result { + debug!("open: {:?}", file_name.to_string()); + + let ino = self.path_to_inode(file_name)?; + let result = self + .runtime + .block_on(async { self.fs.get_attr(ino).await }); + + let attr = result.map_err(|e| { + debug!("open: get_attr failed: {}", e); + STATUS_OBJECT_NAME_NOT_FOUND + })?; + let is_directory = attr.kind == FileType::Directory; + + let fh = if !is_directory { + let read = true; + let write = !self.read_only; + + let handle_result = self + .runtime + .block_on(async { self.fs.open(ino, read, write).await }); + + Some(handle_result.map_err(|e| { + error!("Failed to open file: {}", e); + STATUS_ACCESS_DENIED + })?) + } else { + None + }; + + *file_info.as_mut() = Self::attr_to_file_info(&attr); + + Ok(EncryptedFsFileContext { + ino, + fh, + is_directory, + dir_buffer: DirBuffer::new(), + }) + } + + fn close(&self, context: Self::FileContext) { + debug!("close: ino={}", context.ino); + + if let Some(fh) = context.fh { + if let Err(e) = self.runtime.block_on(async { self.fs.release(fh).await }) { + error!("Failed to release handle {}: {}", fh, e); + } + } + } + + fn read( + &self, + context: &Self::FileContext, + buffer: &mut [u8], + offset: u64, + ) -> winfsp::Result { + trace!( + "read: ino={}, offset={}, len={}", + context.ino, + offset, + buffer.len() + ); + + let fh = context.fh.ok_or(STATUS_ACCESS_DENIED)?; + + let result = self + .runtime + .block_on(async { self.fs.read(context.ino, offset, buffer, fh).await }); + + match result { + Ok(bytes_read) => Ok(bytes_read as u32), + Err(e) => { + error!("Read error: {}", e); + let status = match &e { + FsError::InodeNotFound | FsError::NotFound(_) => STATUS_OBJECT_NAME_NOT_FOUND, + FsError::InvalidFileHandle => STATUS_INVALID_HANDLE, + FsError::InvalidInodeType => STATUS_FILE_IS_A_DIRECTORY, + FsError::InvalidInput(_) => STATUS_INVALID_PARAMETER, + FsError::ReadOnly => STATUS_MEDIA_WRITE_PROTECTED, + FsError::Crypto { .. } => STATUS_DISK_CORRUPT_ERROR, + FsError::Io { source, .. } => match source.kind() { + io::ErrorKind::PermissionDenied => STATUS_ACCESS_DENIED, + io::ErrorKind::NotFound => STATUS_OBJECT_NAME_NOT_FOUND, + _ => STATUS_IO_DEVICE_ERROR, + }, + _ => STATUS_IO_DEVICE_ERROR, + }; + Err(status.into()) + } + } + } + + fn write( + &self, + context: &Self::FileContext, + buffer: &[u8], + offset: u64, + _write_to_eof: bool, + _constrained_io: bool, + file_info: &mut FileInfo, + ) -> winfsp::Result { + trace!( + "write: ino={}, offset={}, len={}", + context.ino, + offset, + buffer.len() + ); + + if self.read_only { + return Err(STATUS_ACCESS_DENIED.into()); + } + + let fh = context.fh.ok_or(STATUS_ACCESS_DENIED)?; + + let result = self + .runtime + .block_on(async { self.fs.write(context.ino, offset, buffer, fh).await }); + + match result { + Ok(bytes_written) => { + self.refresh_file_info(context.ino, file_info); + Ok(bytes_written as u32) + } + Err(e) => { + error!("Write error: {}", e); + Err(STATUS_ACCESS_DENIED.into()) + } + } + } + + fn flush( + &self, + context: Option<&Self::FileContext>, + file_info: &mut FileInfo, + ) -> winfsp::Result<()> { + // None means volume flush - no-op for encrypted fs + let Some(context) = context else { + trace!("flush: volume flush (no-op)"); + return Ok(()); + }; + + trace!("flush: ino={}", context.ino); + + if let Some(fh) = context.fh { + if let Err(e) = self.runtime.block_on(async { self.fs.flush(fh).await }) { + error!("Flush error: {}", e); + return Err(STATUS_ACCESS_DENIED.into()); + } + } + + self.refresh_file_info(context.ino, file_info); + Ok(()) + } + + fn get_file_info( + &self, + context: &Self::FileContext, + file_info: &mut FileInfo, + ) -> winfsp::Result<()> { + trace!("get_file_info: ino={}", context.ino); + + let result = self + .runtime + .block_on(async { self.fs.get_attr(context.ino).await }); + + match result { + Ok(attr) => { + *file_info = Self::attr_to_file_info(&attr); + Ok(()) + } + Err(e) => { + debug!("get_file_info failed: {}", e); + Err(STATUS_OBJECT_NAME_NOT_FOUND.into()) + } + } + } + + fn set_basic_info( + &self, + context: &Self::FileContext, + _file_attributes: u32, + creation_time: u64, + last_access_time: u64, + last_write_time: u64, + _change_time: u64, + file_info: &mut FileInfo, + ) -> winfsp::Result<()> { + trace!("set_basic_info: ino={}", context.ino); + + if self.read_only { + return Err(STATUS_ACCESS_DENIED.into()); + } + + let mut set_attr = crate::encryptedfs::SetFileAttr::default(); + + if creation_time != 0 { + set_attr = set_attr.with_crtime(filetime_to_system_time(creation_time)); + } + if last_access_time != 0 { + set_attr = set_attr.with_atime(filetime_to_system_time(last_access_time)); + } + if last_write_time != 0 { + set_attr = set_attr.with_mtime(filetime_to_system_time(last_write_time)); + } + + if let Err(e) = self + .runtime + .block_on(async { self.fs.set_attr(context.ino, set_attr).await }) + { + error!("set_basic_info error: {}", e); + return Err(STATUS_ACCESS_DENIED.into()); + } + + self.refresh_file_info(context.ino, file_info); + Ok(()) + } + + fn set_file_size( + &self, + context: &Self::FileContext, + new_size: u64, + _set_allocation_size: bool, + file_info: &mut FileInfo, + ) -> winfsp::Result<()> { + trace!("set_file_size: ino={}, new_size={}", context.ino, new_size); + + if self.read_only { + return Err(STATUS_ACCESS_DENIED.into()); + } + + let result = self + .runtime + .block_on(async { self.fs.set_len(context.ino, new_size).await }); + + if let Err(e) = result { + error!("set_file_size error: {}", e); + return Err(STATUS_ACCESS_DENIED.into()); + } + + self.refresh_file_info(context.ino, file_info); + Ok(()) + } + + fn read_directory( + &self, + context: &Self::FileContext, + _pattern: Option<&U16CStr>, + marker: DirMarker, + buffer: &mut [u8], + ) -> winfsp::Result { + trace!("read_directory: ino={}", context.ino); + + if !context.is_directory { + return Err(STATUS_NOT_A_DIRECTORY.into()); + } + + let reset = marker.is_none(); + let lock = context.dir_buffer.acquire(reset, None)?; + + if reset { + let result = self + .runtime + .block_on(async { self.fs.read_dir_plus(context.ino).await }); + + let entries = match result { + Ok(iter) => iter, + Err(e) => { + error!("read_directory error: {}", e); + return Err(STATUS_ACCESS_DENIED.into()); + } + }; + + for entry in entries { + if let Ok(entry) = entry { + let name = entry.name.expose_secret(); + let mut dir_info: DirInfo<255> = DirInfo::new(); + dir_info.set_name(name.as_str())?; + + let fi = dir_info.file_info_mut(); + fi.file_attributes = if entry.kind == FileType::Directory { + FILE_ATTRIBUTE_DIRECTORY.0 + } else { + FILE_ATTRIBUTE_NORMAL.0 + }; + fi.file_size = entry.attr.size; + fi.allocation_size = entry.attr.blocks * entry.attr.blksize as u64; + fi.creation_time = system_time_to_filetime(entry.attr.crtime); + fi.last_access_time = system_time_to_filetime(entry.attr.atime); + fi.last_write_time = system_time_to_filetime(entry.attr.mtime); + fi.change_time = system_time_to_filetime(entry.attr.ctime); + fi.index_number = entry.ino; + + lock.write(&mut dir_info)?; + } + } + } + + drop(lock); + Ok(context.dir_buffer.read(marker, buffer)) + } + + fn create( + &self, + file_name: &U16CStr, + _create_options: u32, + _granted_access: FILE_ACCESS_RIGHTS, + file_attributes: FILE_FLAGS_AND_ATTRIBUTES, + _security_descriptor: Option<&[c_void]>, + _allocation_size: u64, + _extra_buffer: Option<&[u8]>, + _extra_buffer_is_reparse_point: bool, + file_info: &mut OpenFileInfo, + ) -> winfsp::Result { + debug!( + "create: {:?}, attributes={:?}", + file_name.to_string(), + file_attributes + ); + + if self.read_only { + return Err(STATUS_ACCESS_DENIED.into()); + } + + let (parent_ino, name) = self.path_to_parent_and_name(file_name)?; + let secret_name = + SecretString::from_str(&name).map_err(|_| STATUS_OBJECT_NAME_NOT_FOUND)?; + + let is_directory = (file_attributes & FILE_ATTRIBUTE_DIRECTORY.0) != 0; + + let attr = if is_directory { + // uid/gid = 0: Windows doesn't use Unix ownership semantics. + // These values are ignored by WinFSP but required by EncryptedFs. + CreateFileAttr { + kind: FileType::Directory, + perm: 0o755, + uid: 0, + gid: 0, + rdev: 0, + flags: 0, + } + } else { + CreateFileAttr { + kind: FileType::RegularFile, + perm: 0o644, + uid: 0, + gid: 0, + rdev: 0, + flags: 0, + } + }; + + let result = self.runtime.block_on(async { + self.fs + .create(parent_ino, &secret_name, attr, true, true) + .await + }); + + match result { + Ok((fh, created_attr)) => { + *file_info.as_mut() = Self::attr_to_file_info(&created_attr); + + let file_handle = if is_directory { + // Directories don't need an open file handle in WinFSP. + // Release the handle returned by create() to prevent resource leak. + if let Err(e) = self.runtime.block_on(async { self.fs.release(fh).await }) { + error!("Failed to release directory handle {}: {}", fh, e); + } + None + } else { + Some(fh) + }; + + Ok(EncryptedFsFileContext { + ino: created_attr.ino, + fh: file_handle, + is_directory, + dir_buffer: DirBuffer::new(), + }) + } + Err(FsError::AlreadyExists) => Err(STATUS_OBJECT_NAME_COLLISION.into()), + Err(e) => { + error!("create error: {}", e); + Err(STATUS_ACCESS_DENIED.into()) + } + } + } + + fn set_delete( + &self, + context: &Self::FileContext, + file_name: &U16CStr, + delete_file: bool, + ) -> winfsp::Result<()> { + trace!( + "set_delete: ino={}, delete_file={}, name={:?}", + context.ino, + delete_file, + file_name.to_string() + ); + + if !delete_file { + return Ok(()); + } + + if self.read_only { + return Err(STATUS_ACCESS_DENIED.into()); + } + + if context.is_directory { + let result = self.runtime.block_on(async { self.fs.len(context.ino) }); + + match result { + Ok(count) => { + if count > 0 { + return Err(STATUS_DIRECTORY_NOT_EMPTY.into()); + } + } + Err(e) => { + error!("set_delete: failed to check directory length: {}", e); + return Err(STATUS_ACCESS_DENIED.into()); + } + } + } + + Ok(()) + } + + fn cleanup(&self, context: &Self::FileContext, file_name: Option<&U16CStr>, flags: u32) { + trace!("cleanup: ino={}, flags={}", context.ino, flags); + + if let Some(fh) = context.fh { + if let Err(e) = self.runtime.block_on(async { self.fs.flush(fh).await }) { + error!("cleanup: flush error: {}", e); + } + } + + if (flags & FSP_CLEANUP_DELETE) != 0 { + if let Some(file_name) = file_name { + match self.path_to_parent_and_name(file_name) { + Ok((parent_ino, name)) => { + match SecretString::from_str(&name) { + Ok(secret_name) => { + let result = if context.is_directory { + self.runtime + .block_on(async { self.fs.remove_dir(parent_ino, &secret_name).await }) + } else { + self.runtime + .block_on(async { self.fs.remove_file(parent_ino, &secret_name).await }) + }; + + if let Err(e) = result { + error!("cleanup: failed to delete file/dir: {}", e); + } + } + Err(e) => { + error!("cleanup: failed to convert name to SecretString: {}", e); + } + } + } + Err(e) => { + error!("cleanup: failed to resolve parent and name: {}", e); + } + } + } + } + } + + fn overwrite( + &self, + context: &Self::FileContext, + _file_attributes: FILE_FLAGS_AND_ATTRIBUTES, + _replace_file_attributes: bool, + _allocation_size: u64, + _ea: Option<&[u8]>, + file_info: &mut FileInfo, + ) -> winfsp::Result<()> { + trace!("overwrite: ino={}", context.ino); + + if self.read_only { + return Err(STATUS_ACCESS_DENIED.into()); + } + + let result = self + .runtime + .block_on(async { self.fs.set_len(context.ino, 0).await }); + + if let Err(e) = result { + error!("overwrite error: {}", e); + return Err(STATUS_ACCESS_DENIED.into()); + } + + self.refresh_file_info(context.ino, file_info); + Ok(()) + } + + fn rename( + &self, + _context: &Self::FileContext, + file_name: &U16CStr, + new_file_name: &U16CStr, + _replace_if_exists: bool, + ) -> winfsp::Result<()> { + debug!( + "rename: {:?} -> {:?}", + file_name.to_string(), + new_file_name.to_string() + ); + + if self.read_only { + return Err(STATUS_ACCESS_DENIED.into()); + } + + let (old_parent_ino, old_name) = self.path_to_parent_and_name(file_name)?; + let (new_parent_ino, new_name) = self.path_to_parent_and_name(new_file_name)?; + + let old_secret_name = + SecretString::from_str(&old_name).map_err(|_| STATUS_OBJECT_NAME_NOT_FOUND)?; + let new_secret_name = + SecretString::from_str(&new_name).map_err(|_| STATUS_OBJECT_NAME_NOT_FOUND)?; + + let result = self.runtime.block_on(async { + self.fs + .rename( + old_parent_ino, + &old_secret_name, + new_parent_ino, + &new_secret_name, + ) + .await + }); + + match result { + Ok(()) => Ok(()), + Err(FsError::NotEmpty) => Err(STATUS_DIRECTORY_NOT_EMPTY.into()), + Err(e) => { + error!("rename error: {}", e); + Err(STATUS_ACCESS_DENIED.into()) + } + } + } +} + +#[allow(clippy::struct_excessive_bools)] +pub struct MountPointImpl { + mountpoint: PathBuf, + data_dir: PathBuf, + password_provider: Option>, + cipher: Cipher, + #[allow(dead_code)] + allow_root: bool, + #[allow(dead_code)] + allow_other: bool, + read_only: bool, +} + +#[async_trait] +impl MountPoint for MountPointImpl { + fn new( + mountpoint: PathBuf, + data_dir: PathBuf, + password_provider: Box, + cipher: Cipher, + allow_root: bool, + allow_other: bool, + read_only: bool, + ) -> Self { + Self { + mountpoint, + data_dir, + password_provider: Some(password_provider), + cipher, + allow_root, + allow_other, + read_only, + } + } + + async fn mount(mut self) -> FsResult { + info!("Mounting rencfs on Windows at {:?}", self.mountpoint); + + winfsp::winfsp_init_or_die(); + + let fs = EncryptedFs::new( + self.data_dir.clone(), + self.password_provider.take().ok_or_else(|| FsError::Other("Mount already called"))?, + self.cipher, + self.read_only, + ) + .await?; + + let winfsp_fs = EncryptedFsWinFsp::new(fs, self.read_only); + + let mut volume_params = VolumeParams::default(); + volume_params.filesystem_name("rencfs"); + if self.read_only { + volume_params.read_only_volume(true); + } + + let mut host = FileSystemHost::new(volume_params, winfsp_fs).map_err(|e| { + error!("Failed to create FileSystemHost: {:?}", e); + FsError::Other("Failed to create FileSystemHost") + })?; + + let mount_point_str = self + .mountpoint + .to_str() + .ok_or_else(|| FsError::Other("Invalid mount point path"))?; + + host.mount(mount_point_str).map_err(|e| { + error!("Failed to mount filesystem at {}: {:?}", mount_point_str, e); + FsError::Other("Failed to mount filesystem") + })?; + + host.start().map_err(|e| { + error!("Failed to start filesystem service: {:?}", e); + FsError::Other("Failed to start filesystem service") + })?; + + info!( + "rencfs mounted successfully on Windows at {}", + mount_point_str + ); + + Ok(mount::MountHandle { + inner: MountHandleInnerImpl { + host: Some(host), + mountpoint: self.mountpoint.clone(), + }, + }) + } +} + +pub(in crate::mount) struct MountHandleInnerImpl { + host: Option>, + mountpoint: PathBuf, +} + +impl Future for MountHandleInnerImpl { + type Output = io::Result<()>; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + // WinFSP mount is already active after host.start(), so return Ready immediately + Poll::Ready(Ok(())) + } +} + +#[async_trait] +impl MountHandleInner for MountHandleInnerImpl { + async fn unmount(mut self) -> io::Result<()> { + info!("Unmounting rencfs from {:?}", self.mountpoint); + + if let Some(host) = self.host.take() { + drop(host); + } + + Ok(()) + } +} + +pub fn umount(mountpoint: &str) -> io::Result<()> { + info!("Windows unmount requested for {}", mountpoint); + // WinFSP does not support external unmount requests. + // Unmounting is handled by dropping the FileSystemHost in MountHandleInnerImpl::unmount(). + Err(io::Error::new( + io::ErrorKind::Unsupported, + "External unmount not supported on Windows. Use the mount handle to unmount.", + )) +}