From aa90fa12bf6c4f0417a1fab1dfb7492aabb48891 Mon Sep 17 00:00:00 2001 From: Robert0303-C Date: Tue, 31 Mar 2026 09:46:36 +0800 Subject: [PATCH] Research and implementation for Windows support using WinFSP This commit adds initial Windows filesystem support using the WinFSP (Windows File System Proxy) library. WinFSP provides a FUSE-like interface for implementing filesystems on Windows. Changes: - Added winfsp dependency for Windows target in Cargo.toml - Created src/mount/windows.rs with EncryptedFsWinFsp implementation that wraps EncryptedFs with WinFSP's FileSystemContext trait - Updated src/mount.rs to use windows module on Windows - Updated src/main.rs to enable Windows support Note: WinFSP is GPLv3 licensed. The implementation provides: - File open/close operations - File read/write operations - Volume information - Basic file attributes Some operations (rename, delete, directory enumeration) are marked as TODO and need further implementation. The implementation follows the same pattern as the Linux FUSE implementation but adapted for WinFSP's API. See: https://github.com/xoriors/rencfs/issues/3 --- Cargo.toml | 4 + src/main.rs | 6 +- src/mount.rs | 13 +- src/mount/windows.rs | 574 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 591 insertions(+), 6 deletions(-) create mode 100644 src/mount/windows.rs diff --git a/Cargo.toml b/Cargo.toml index 8484ba24..d98a000c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,10 @@ 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 = { version = "0.12", features = ["async-io"] } +tokio = { version = "1.36", features = ["full"] } + [[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..c9e7f5d7 --- /dev/null +++ b/src/mount/windows.rs @@ -0,0 +1,574 @@ +//! Windows mount implementation using WinFSP +//! +//! This module provides Windows-specific mounting functionality using the WinFSP +//! (Windows File System Proxy) library, which provides a FUSE-like interface for +//! implementing file systems on Windows. +//! +//! WinFSP is released under the GNU GPLv3 license. For commercial licensing, +//! see: https://winfsp.dev/ + +use std::ffi::OsStr; +use std::future::Future; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use async_trait::async_trait; +use futures_util::FutureExt; +use shush_rs::{ExposeSecret, SecretString}; +use tracing::{debug, error, info, warn}; +use winfsp::filesystem::{FileInfo, OpenFileInfo}; +use winfsp::host::{FileSystemHost, VolumeParams}; +use winfsp::util::U16CString; + +use crate::crypto::Cipher; +use crate::encryptedfs::{EncryptedFs, FileAttr, FileType, FsResult, PasswordProvider}; +use crate::mount::{MountHandleInner, MountPoint}; + +/// Windows-specific file context representing an open file or directory +#[derive(Debug)] +pub struct WinFileContext { + /// Inode number in the encrypted filesystem + pub ino: u64, + /// Whether this is a directory + pub is_dir: bool, + /// Current position for directory enumeration + pub dir_position: u64, + /// File handle for operations that need it + pub fh: Option, +} + +impl WinFileContext { + pub fn new(ino: u64, is_dir: bool) -> Self { + Self { + ino, + is_dir, + dir_position: 0, + fh: None, + } + } +} + +/// Windows implementation of the encrypted filesystem using WinFSP +#[derive(Debug)] +pub struct EncryptedFsWinFsp { + /// The underlying encrypted filesystem + fs: Arc, + /// Whether the filesystem is read-only + read_only: bool, +} + +impl EncryptedFsWinFsp { + /// Create a new WinFSP-wrapped encrypted filesystem + pub async fn new( + data_dir: PathBuf, + password_provider: Box, + cipher: Cipher, + read_only: bool, + ) -> FsResult { + let fs = EncryptedFs::new(data_dir, password_provider, cipher, read_only).await?; + Ok(Self { fs: Arc::new(fs), read_only }) + } + + /// Get the underlying encrypted filesystem + pub fn get_fs(&self) -> Arc { + self.fs.clone() + } + + /// Check if filesystem is read-only + pub fn is_read_only(&self) -> bool { + self.read_only + } +} + +impl winfsp::filesystem::FileSystemContext for EncryptedFsWinFsp { + type FileContext = WinFileContext; + + fn get_security_by_name( + &self, + file_name: &[u16], + _security_descriptor: Option<&mut std::ffi::c_void>, + _reparse_point_resolver: impl FnOnce(&[u16]) -> Option, + ) -> winfsp::Result { + info!("get_security_by_name: {:?}", String::from_utf16_lossy(file_name)); + // For simplicity, return a default security descriptor + Ok(winfsp::filesystem::FileSecurity::new( + 0, // owner + 0, // group + 0, // dacl (null DACL - full access) + )) + } + + fn open( + &self, + file_name: &[u16], + _create_options: u32, + granted_access: u32, + file_info: &mut OpenFileInfo, + ) -> winfsp::Result { + let file_name_str = String::from_utf16_lossy(file_name); + debug!("open: {} (access: {:#x})", file_name_str, granted_access); + + // For root directory + if file_name_str.is_empty() || file_name_str == "\\" || file_name_str == "/" { + file_info.set_file_attributes(winfsp::filesystem::FILE_ATTRIBUTE_DIRECTORY); + return Ok(WinFileContext::new(crate::encryptedfs::ROOT_INODE, true)); + } + + // Strip leading backslash if present + let name = file_name_str.trim_start_matches('\\').trim_start_matches('/'); + + // Parse the path to get parent and filename + let parts: Vec<&str> = name.split('\\').filter(|s| !s.is_empty()).collect(); + if parts.is_empty() { + // Root + file_info.set_file_attributes(winfsp::filesystem::FILE_ATTRIBUTE_DIRECTORY); + return Ok(WinFileContext::new(crate::encryptedfs::ROOT_INODE, true)); + } + + let rt = tokio::runtime::Handle::current(); + let fs = self.fs.clone(); + let parent_ino = crate::encryptedfs::ROOT_INODE; + let file_name_secret = SecretString::from_str(name).map_err(|_| winfsp::Error::Other("Invalid filename"))?; + + // Lookup the file + let result = rt.block_on(async { + fs.find_by_name(parent_ino, &file_name_secret).await + }); + + match result { + Ok(Some(attr)) => { + file_info.set_file_attributes(file_type_to_winattr(attr.kind)); + let is_dir = attr.kind == FileType::Directory; + + // If opening for read/write, get a handle + let fh = if !is_dir { + let rt2 = tokio::runtime::Handle::current(); + let fs2 = fs.clone(); + let open_result = rt2.block_on(async { + let read = granted_access & 0x80000000 != 0 || granted_access & 0x40000000 != 0; // GENERIC_READ + let write = granted_access & 0x80000000 != 0 || granted_access & 0x20000000 != 0; // GENERIC_WRITE + if read || write { + fs2.open(attr.ino, read, write).await.ok() + } else { + None + } + }); + open_result + } else { + None + }; + + Ok(WinFileContext { + ino: attr.ino, + is_dir, + dir_position: 0, + fh, + }) + } + Ok(None) => Err(winfsp::Error::Other("File not found")), + Err(e) => { + error!("lookup error: {:?}", e); + Err(winfsp::Error::Other("Lookup failed")) + } + } + } + + fn close(&self, context: Self::FileContext) { + debug!("close: inode={}", context.ino); + // Release file handle if open + if let Some(fh) = context.fh { + let rt = tokio::runtime::Handle::current(); + let fs = self.fs.clone(); + rt.block_on(async { + if let Err(e) = fs.release(fh).await { + error!("release error: {:?}", e); + } + }); + } + } + + fn get_file_info( + &self, + context: &Self::FileContext, + file_info: &mut FileInfo, + ) -> winfsp::Result<()> { + debug!("get_file_info: inode={}", context.ino); + + let rt = tokio::runtime::Handle::current(); + let fs = self.fs.clone(); + let ino = context.ino; + + let result = rt.block_on(async { fs.get_attr(ino).await }); + + match result { + Ok(attr) => { + *file_info = file_attr_to_file_info(attr, &context.ino); + Ok(()) + } + Err(e) => { + error!("get_file_info error: {:?}", e); + Err(winfsp::Error::Other("Failed to get file info")) + } + } + } + + fn read( + &self, + context: &Self::FileContext, + buffer: &mut [u8], + offset: u64, + ) -> winfsp::Result { + debug!( + "read: inode={}, offset={}, size={}", + context.ino, + offset, + buffer.len() + ); + + if context.is_dir { + return Err(winfsp::Error::Other("Cannot read from directory")); + } + + // Ensure we have a file handle + let fh = match context.fh { + Some(fh) => fh, + None => { + // Try to open for reading + let rt = tokio::runtime::Handle::current(); + let fs = self.fs.clone(); + match rt.block_on(async { fs.open(context.ino, true, false).await }) { + Ok(fh) => fh, + Err(e) => { + error!("open for read error: {:?}", e); + return Err(winfsp::Error::Other("Failed to open file for reading")); + } + } + } + }; + + let rt = tokio::runtime::Handle::current(); + let fs = self.fs.clone(); + + let result = rt.block_on(async { + let mut buf = vec![0u8; buffer.len()]; + let read_len = fs.read(context.ino, offset, &mut buf, fh).await.map_err(|e| { + error!("read error: {:?}", e); + winfsp::Error::Other("Read failed") + })?; + + // Copy to output buffer + buffer[..read_len].copy_from_slice(&buf[..read_len]); + Ok::<_, winfsp::Error>(read_len) + }); + + result + } + + fn write( + &self, + context: &Self::FileContext, + buffer: &[u8], + offset: u64, + _write_to_eof: bool, + _constrained_io: bool, + file_info: &mut FileInfo, + ) -> winfsp::Result { + debug!( + "write: inode={}, offset={}, size={}", + context.ino, + offset, + buffer.len() + ); + + if self.read_only { + return Err(winfsp::Error::Other("Filesystem is read-only")); + } + + if context.is_dir { + return Err(winfsp::Error::Other("Cannot write to directory")); + } + + // Ensure we have a file handle open for writing + let fh = match context.fh { + Some(fh) => fh, + None => { + let rt = tokio::runtime::Handle::current(); + let fs = self.fs.clone(); + match rt.block_on(async { fs.open(context.ino, false, true).await }) { + Ok(fh) => fh, + Err(e) => { + error!("open for write error: {:?}", e); + return Err(winfsp::Error::Other("Failed to open file for writing")); + } + } + } + }; + + let rt = tokio::runtime::Handle::current(); + let fs = self.fs.clone(); + let ino = context.ino; + + let result = rt.block_on(async { + fs.write(ino, offset, buffer, fh).await.map_err(|e| { + error!("write error: {:?}", e); + winfsp::Error::Other("Write failed") + }) + }); + + match result { + Ok(len) => { + // Update file info with new size + if let Ok(attr) = rt.block_on(async { fs.get_attr(ino).await }) { + *file_info = file_attr_to_file_info(attr, &ino); + } + Ok(len as u32) + } + Err(e) => Err(e), + } + } + + fn get_volume_info( + &self, + out_volume_info: &mut winfsp::filesystem::VolumeInfo, + ) -> winfsp::Result<()> { + debug!("get_volume_info"); + out_volume_info.set_total_size(0); + out_volume_info.set_free_size(0); + out_volume_info.set_volume_label("rencfs".into()); + Ok(()) + } + + fn cleanup( + &self, + context: &Self::FileContext, + _file_name: Option<&[u16]>, + _flags: u32, + ) { + debug!("cleanup: inode={}", context.ino); + // Note: we don't close the handle here because close() will be called separately + } + + fn flush( + &self, + context: Option<&Self::FileContext>, + _file_info: &mut FileInfo, + ) -> winfsp::Result<()> { + debug!("flush: context={:?}", context.map(|c| c.ino)); + Ok(()) + } + + fn set_file_size( + &self, + context: &Self::FileContext, + new_size: u64, + _set_allocation_size: bool, + file_info: &mut FileInfo, + ) -> winfsp::Result<()> { + debug!("set_file_size: inode={}, size={}", context.ino, new_size); + + if self.read_only { + return Err(winfsp::Error::Other("Filesystem is read-only")); + } + + let rt = tokio::runtime::Handle::current(); + let fs = self.fs.clone(); + let ino = context.ino; + + match rt.block_on(async { fs.set_len(ino, new_size).await }) { + Ok(()) => { + // Update file info + if let Ok(attr) = rt.block_on(async { fs.get_attr(ino).await }) { + *file_info = file_attr_to_file_info(attr, &ino); + } + Ok(()) + } + Err(e) => { + error!("set_file_size error: {:?}", e); + Err(winfsp::Error::Other("Failed to set file size")) + } + } + } + + fn rename( + &self, + context: &Self::FileContext, + file_name: &[u16], + new_file_name: &[u16], + replace_if_exists: bool, + ) -> winfsp::Result<()> { + debug!( + "rename: inode={}, name={:?}, new_name={:?}, replace={}", + context.ino, + String::from_utf16_lossy(file_name), + String::from_utf16_lossy(new_file_name), + replace_if_exists + ); + + if self.read_only { + return Err(winfsp::Error::Other("Filesystem is read-only")); + } + + // TODO: Implement rename using self.fs.rename() + Err(winfsp::Error::Other("Rename not yet implemented")) + } + + fn delete( + &self, + context: &Self::FileContext, + _file_name: &[u16], + _delete_file: bool, + ) -> winfsp::Result<()> { + debug!("delete: inode={}", context.ino); + + if self.read_only { + return Err(winfsp::Error::Other("Filesystem is read-only")); + } + + // TODO: Implement delete using self.fs.remove_file() or self.fs.remove_dir() + Err(winfsp::Error::Other("Delete not yet implemented")) + } +} + +/// Convert a SecretString filename to a wide string for WinFSP +fn secret_to_wide(name: &SecretString) -> U16CString { + let s = name.expose_secret(); + U16CString::from_str(s).unwrap_or_else(|_| U16CString::from_str("*").unwrap()) +} + +/// Convert FileType to WinFSP file attributes +fn file_type_to_winattr(kind: FileType) -> u32 { + match kind { + FileType::Directory => winfsp::filesystem::FILE_ATTRIBUTE_DIRECTORY, + FileType::RegularFile => 0, + } +} + +/// Convert SystemTime to Windows FILETIME (100-nanosecond intervals since 1601-01-01) +fn system_time_to_filetime(time: std::time::SystemTime) -> u64 { + let duration = time + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default(); + // Windows FILETIME is relative to 1601-01-01, Unix to 1970-01-01 + // 116444736000000000 = seconds between 1601 and 1970 * 10_000_000 + const WINDOWS_TICK: u64 = 10_000_000; + const SECS_TO_1601: u64 = 11644473600; + (duration.as_secs() + SECS_TO_1601) * WINDOWS_TICK + (duration.subsec_nanos() as u64 / 100) +} + +/// Convert FileAttr to WinFSP FileInfo +fn file_attr_to_file_info(attr: FileAttr, _ino: &u64) -> FileInfo { + let file_attributes = file_type_to_winattr(attr.kind); + let reparse_tag = 0; // Not a reparse point + + FileInfo::new( + attr.ino, + file_attributes, + attr.size, + 0, // allocation_size (we don't use this) + system_time_to_filetime(attr.ctime), + system_time_to_filetime(attr.mtime), + system_time_to_filetime(attr.atime), + system_time_to_filetime(attr.crtime), + reparse_tag, + ) +} + +/// Mount point implementation for Windows +pub struct MountPointImpl { + mountpoint: PathBuf, + data_dir: PathBuf, + password_provider: Option>, + cipher: Cipher, + allow_root: bool, + 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 { + let fs = EncryptedFsWinFsp::new( + self.data_dir.clone(), + self.password_provider.take().unwrap(), + self.cipher, + self.read_only, + ) + .await?; + + // Configure volume parameters + let mut params = VolumeParams::new(); + params.set_file_system_name("rencfs"); + params.set_volume_label("rencfs"); + params.set_case_sensitive(false); // Windows is case-insensitive + params.set_read_only(self.read_only); + + // Create the filesystem host + let host = FileSystemHost::new(params, fs).map_err(|e| { + error!("Failed to create filesystem host: {:?}", e); + crate::encryptedfs::FsError::Other("Failed to create filesystem host") + })?; + + Ok(crate::mount::MountHandle { + inner: MountHandleInnerImpl { + inner: Some(host), + mountpoint: self.mountpoint, + }, + }) + } +} + +/// Handle for the mounted Windows filesystem +pub struct MountHandleInnerImpl { + /// The WinFSP filesystem host + inner: Option>, + /// The mountpoint path + mountpoint: PathBuf, +} + +impl Future for MountHandleInnerImpl { + type Output = std::io::Result<()>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + // The filesystem runs in a separate thread managed by WinFSP + // We just wait for the inner future to complete + if self.inner.is_some() { + // Keep the filesystem running + Poll::Pending + } else { + Poll::Ready(Ok(())) + } + } +} + +#[async_trait] +impl MountHandleInner for MountHandleInnerImpl { + async fn unmount(mut self) -> std::io::Result<()> { + info!("Unmounting Windows filesystem"); + if let Some(mut host) = self.inner.take() { + host.stop(); + } + Ok(()) + } +}