diff --git a/wfassoc/src/lib.rs b/wfassoc/src/lib.rs index f457771..986714c 100644 --- a/wfassoc/src/lib.rs +++ b/wfassoc/src/lib.rs @@ -5,98 +5,212 @@ compile_error!("Crate wfassoc is only supported on Windows."); pub mod utilities; -pub mod concept; +pub mod win32; -// pub mod utilities; -// pub mod winconcept; -// pub mod win32ext; -// pub mod winregext; +use std::fmt::Display; +use std::str::FromStr; +use thiserror::Error as TeError; +use winreg::RegKey; -// use std::collections::HashMap; -// use thiserror::Error as TeError; +// region: Error Process -// /// Error occurs in this module. -// #[derive(Debug, TeError)] -// pub enum Error {} +/// Error occurs in this module. +#[derive(Debug, TeError)] +pub enum Error { + #[error("can not perform this operation because lack essential privilege.")] + NoPrivilege, + #[error("{0}")] + BadRegOp(#[from] std::io::Error), + #[error("{0}")] + UnexpectedBlankKey(#[from] win32::regext::BlankPathError), +} -// /// Result type used in this module. -// type Result = std::result::Result; +// endregion -// /// Schema is the sketchpad of complete Program. -// /// -// /// We will create a Schema first, fill some properties, add file extensions, -// /// then convert it into immutable Program for following using. -// #[derive(Debug)] -// pub struct Schema { -// identifier: String, -// path: String, -// clsid: String, -// icons: HashMap, -// behaviors: HashMap, -// exts: HashMap, -// } +// region: Scope and View -// /// Internal used struct as the Schema file extensions hashmap value type. -// #[derive(Debug)] -// struct SchemaExt { -// name: String, -// icon: String, -// behavior: String, -// } +/// The scope where wfassoc will register and unregister. +#[derive(Debug, Copy, Clone)] +pub enum Scope { + /// Scope for current user. + User, + /// Scope for all users under this computer. + System, +} -// impl Schema { -// pub fn new() -> Self { -// Self { -// identifier: String::new(), -// path: String::new(), -// clsid: String::new(), -// icons: HashMap::new(), -// behaviors: HashMap::new(), -// exts: HashMap::new(), -// } -// } +/// The view when wfassoc querying infomations. +#[derive(Debug, Copy, Clone)] +pub enum View { + /// The view of current user. + User, + /// The view of system. + System, + /// Hybrid view of User and System. + /// It can be seen as that we use System first and then use User to override any existing items. + Hybrid, +} -// pub fn set_identifier(&mut self, identifier: &str) -> Result<()> {} +/// The error occurs when cast View into Scope. +#[derive(Debug, TeError)] +#[error("hybrid view can not be cast into any scope")] +pub struct TryFromViewError {} -// pub fn set_path(&mut self, exe_path: &str) -> Result<()> {} +impl TryFromViewError { + fn new() -> Self { + Self {} + } +} -// pub fn set_clsid(&mut self, clsid: &str) -> Result<()> {} +impl From for View { + fn from(value: Scope) -> Self { + match value { + Scope::User => Self::User, + Scope::System => Self::System, + } + } +} -// pub fn add_icon(&mut self, name: &str, value: &str) -> Result<()> {} +impl TryFrom for Scope { + type Error = TryFromViewError; -// pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<()> {} + fn try_from(value: View) -> Result { + match value { + View::User => Ok(Self::User), + View::System => Ok(Self::System), + View::Hybrid => Err(TryFromViewError::new()), + } + } +} -// pub fn add_ext( -// &mut self, -// ext: &str, -// ext_name: &str, -// ext_icon: &str, -// ext_behavior: &str, -// ) -> Result<()> { -// } +impl Scope { + /// Check whether we have enough privilege when operating in current scope. + /// If we have, simply return, otherwise return error. + fn check_privilege(&self) -> Result<(), Error> { + if matches!(self, Self::System if !win32::utilities::has_privilege()) { + Err(Error::NoPrivilege) + } else { + Ok(()) + } + } +} -// pub fn into_program(self) -> Result { -// Program::new(self) -// } -// } +// endregion -// /// Program is a complete and immutable program representer -// pub struct Program {} +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AppPathsKey { + key: win32::concept::FileName, +} -// impl TryFrom for Program { -// type Error = Error; +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ApplicationsKey { + key: win32::concept::FileName, +} -// fn try_from(value: Schema) -> std::result::Result { -// Self::new(value) -// } -// } +// region: File Extension Key -// impl Program { -// pub fn new(schema: Schema) -> Result {} -// } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ExtKey { + ext: win32::concept::Ext, +} + +impl ExtKey { + fn open_scope(&self, scope: Scope) -> Result, Error> { + use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE}; + + // check privilege + scope.check_privilege()?; + // get the root key + let hk = match scope { + Scope::User => RegKey::predef(HKEY_CURRENT_USER), + Scope::System => RegKey::predef(HKEY_LOCAL_MACHINE), + }; + // navigate to classes + let classes = hk.open_subkey_with_flags("Software\\Classes", KEY_READ | KEY_WRITE)?; + // open extension key if possible + let thisext = win32::regext::try_open_subkey_with_flags( + &classes, + win32::regext::blank_path_guard(self.ext.dotted_inner())?, + KEY_READ | KEY_WRITE, + )?; + // okey + Ok(thisext) + } + + fn open_view(&self, view: View) -> Result, Error> { + use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ}; + + // navigate to extension container + let hk = match view { + View::User => RegKey::predef(HKEY_CURRENT_USER), + View::System => RegKey::predef(HKEY_LOCAL_MACHINE), + View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT), + }; + let classes = match view { + View::User | View::System => { + hk.open_subkey_with_flags("Software\\Classes", KEY_READ)? + } + View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?, + }; + // open extension key if possible + let thisext = win32::regext::try_open_subkey_with_flags( + &classes, + win32::regext::blank_path_guard(self.ext.dotted_inner())?, + KEY_READ, + )?; + // okey + Ok(thisext) + } -// #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -// pub struct ExtKey { -// inner: winconcept::Ext -// } +} + +// endregion + +// region: ProgId Key + +// region: Losse ProgId + +/// The enum representing a losse Programmatic Identifiers (ProgId). +/// +/// In real world, not all software developers are willing to following Microsoft suggestions to use ProgId, +/// They use string which do not have any regulation as ProgId. +/// This enum is designed for handling this scenario. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum LosseProgId { + Plain(String), + Strict(win32::concept::ProgId), +} + +impl Display for LosseProgId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LosseProgId::Plain(v) => write!(f, "{}", v), + LosseProgId::Strict(v) => write!(f, "{}", v), + } + } +} + +impl From<&str> for LosseProgId { + fn from(s: &str) -> Self { + // match it for standard ProgId first + if let Ok(v) = win32::concept::ProgId::from_str(s) { + Self::Strict(v) + } else { + // fallback with other + Self::Plain(s.to_string()) + } + } +} + +// endregion + +// region: ProgId Key + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ProgIdKey { + progid: LosseProgId, +} + +// endregion + +// endregion diff --git a/wfassoc/src/utilities.rs b/wfassoc/src/utilities.rs index 4ee9816..e5c7c06 100644 --- a/wfassoc/src/utilities.rs +++ b/wfassoc/src/utilities.rs @@ -23,7 +23,7 @@ macro_rules! debug_println { }; } -// region OS String Related +// region: OS String Related /// The error occurs when casting `OsStr` into `str`. #[derive(Debug, TeError)] diff --git a/wfassoc/src/win32.rs b/wfassoc/src/win32.rs new file mode 100644 index 0000000..2788102 --- /dev/null +++ b/wfassoc/src/win32.rs @@ -0,0 +1,3 @@ +pub mod concept; +pub mod utilities; +pub mod regext; \ No newline at end of file diff --git a/wfassoc/src/concept.rs b/wfassoc/src/win32/concept.rs similarity index 91% rename from wfassoc/src/concept.rs rename to wfassoc/src/win32/concept.rs index 4e51471..62d48b5 100644 --- a/wfassoc/src/concept.rs +++ b/wfassoc/src/win32/concept.rs @@ -59,6 +59,12 @@ impl Ext { pub fn inner(&self) -> &str { &self.body } + + /// Get the body part of file extension (with leading dot) + pub fn dotted_inner(&self) -> String { + // Reuse Display trait result + self.to_string() + } } /// The error occurs when try parsing string into FileExt. @@ -123,7 +129,7 @@ impl BadProgIdPartError { /// `[Vendor or Application].[Component].[Version]` format. /// /// Additionally, `[Version]` part is optional. -/// +/// /// However, most of applications do no follow this standard, /// this scenario is not convered by this struct in there. /// It should be done by other structs in other places. @@ -237,7 +243,7 @@ impl FromStr for ProgId { /// The struct representing Windows CLSID looks like /// `{26EE0668-A00A-44D7-9371-BEB064C98683}` (case insensitive). -/// +/// /// Please note that the curly brace is the essential part of CLSID. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Clsid { @@ -312,7 +318,7 @@ impl ParseIconRefStrError { /// The struct representing an Icon Reference String /// looks like `%SystemRoot%\System32\imageres.dll,-72`. -/// +/// /// As far as I know, the minus token `-` does nothing in this string. /// The following number is just the index. pub struct IconRefStr { @@ -405,7 +411,7 @@ impl ParseStrRefStrError { /// The struct representing an String Reference String /// looks like `@%SystemRoot%\System32\shell32.dll,-30596`. -/// +/// /// As far as I know, the minus token `-` does nothing in this string. /// The following number is just the index. pub struct StrRefStr { @@ -757,3 +763,67 @@ impl FromStr for ExpandString { } // endregion + +// region: File Name + +pub type BadFileNameError = ParseFileNameError; + +/// The struct representing a legal Windows file name. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FileName { + /// The validated legal Windows file name. + filename: String, +} + +impl FileName { + /// Create a new file name. + /// + /// `filename` is the file name for validating. + /// This function will validate whether given file name is legal in Windows, + /// in other words, checking whether it contain illegal words or Windows reserved name in given name + /// such as `?`, `COM` and etc. + pub fn new(filename: &str) -> Result { + Self::from_str(filename) + } + + /// Get the validated file name. + pub fn inner(&self) -> &str { + &self.filename + } +} + +/// The error occurs when constructing FileName with bad file name. +#[derive(Debug, TeError)] +#[error("given file name is illegal in Windows")] +pub enum ParseFileNameError { + /// Given string has embedded NUL. + EmbeddedNul(#[from] widestring::error::ContainsNul), + /// Given string has illegal char as Windows file name. + InvalidChar, +} + +impl Display for FileName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.filename.fmt(f) + } +} + +impl FromStr for FileName { + type Err = ParseFileNameError; + + fn from_str(s: &str) -> Result { + use windows_sys::Win32::UI::Shell::PathCleanupSpec; + + // Make buffer and call function. + let mut spec = WideCString::from_str(s)?; + let rv = unsafe { PathCleanupSpec(std::ptr::null(), spec.as_mut_ptr()) }; + if rv != 0 { + return Err(ParseFileNameError::InvalidChar); + } + + // Build self and return + Ok(Self { filename: s.to_string() }) + } +} + +// endregion diff --git a/wfassoc/src/winregext.rs b/wfassoc/src/win32/regext.rs similarity index 98% rename from wfassoc/src/winregext.rs rename to wfassoc/src/win32/regext.rs index 49a8773..647c7e9 100644 --- a/wfassoc/src/winregext.rs +++ b/wfassoc/src/win32/regext.rs @@ -14,7 +14,7 @@ use winreg::types::FromRegValue; /// Get the subkey with given name. /// /// If error occurs when fetching given subkey, it return `Err(...)`, -/// otherwise, it will return `Ok(Some(...))` if aubkey is existing, +/// otherwise, it will return `Ok(Some(...))` if subkey is existing, /// or `Ok(None)` if there is no suchsub key. /// /// Comparing with the function provided by winreg, diff --git a/wfassoc/src/win32ext.rs b/wfassoc/src/win32/utilities.rs similarity index 100% rename from wfassoc/src/win32ext.rs rename to wfassoc/src/win32/utilities.rs diff --git a/wfassoc/tests/concept.rs b/wfassoc/tests/concept.rs index 8fad225..526f0c4 100644 --- a/wfassoc/tests/concept.rs +++ b/wfassoc/tests/concept.rs @@ -1,5 +1,5 @@ use std::str::FromStr; -use wfassoc::concept::*; +use wfassoc::win32::concept::*; // region: File Extension @@ -230,3 +230,25 @@ fn test_expand_string() { } // endregion + +// region: File Name + +#[test] +fn test_file_name() { + fn ok_tester(s: &str) { + let rv = FileName::from_str(s); + assert!(rv.is_ok()); + let rv = rv.unwrap(); + assert_eq!(s, rv.inner()); + } + fn err_tester(s: &str) { + let rv = FileName::from_str(s); + assert!(rv.is_err()); + } + + ok_tester("GoodExecutable.exe"); + err_tester("*.?xaml"); + err_tester(r#"\\\lol///"#); +} + +// endregion