diff --git a/wfassoc/src/assoc.rs b/wfassoc/src/assoc_old.rs similarity index 100% rename from wfassoc/src/assoc.rs rename to wfassoc/src/assoc_old.rs diff --git a/wfassoc/src/extra.rs b/wfassoc/src/extra.rs deleted file mode 100644 index 3776c8a..0000000 --- a/wfassoc/src/extra.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! The extension for some existing crates. -//! Some imported crates are not enough for my project, -//! so I need create something to enrich them. - -pub mod winreg; -pub mod windows; - \ No newline at end of file diff --git a/wfassoc/src/lib.rs b/wfassoc/src/lib.rs index 1940099..e4f34db 100644 --- a/wfassoc/src/lib.rs +++ b/wfassoc/src/lib.rs @@ -4,498 +4,96 @@ #[cfg(not(target_os = "windows"))] compile_error!("Crate wfassoc is only supported on Windows."); -pub mod extra; pub mod utilities; -pub mod assoc; +pub mod winconcept; +pub mod win32ext; +pub mod winregext; -use assoc::{Ext, ProgId}; -use indexmap::{IndexMap, IndexSet}; -use regex::Regex; -use std::ffi::OsStr; -use std::path::PathBuf; -use std::sync::LazyLock; +use std::collections::HashMap; use thiserror::Error as TeError; -use winreg::RegKey; -use winreg::enums::{ - HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE, -}; -// region: Error Types - -/// All possible error occurs in this crate. +/// Error occurs in this module. #[derive(Debug, TeError)] -pub enum Error { - #[error("error occurs when manipulating with Registry: {0}")] - BadRegOper(#[from] std::io::Error), - #[error("{0}")] - CastOsStr(#[from] utilities::CastOsStrError), - #[error("{0}")] - ParseExt(#[from] assoc::ParseExtError), +pub enum Error {} - #[error("no administrative privilege")] - NoPrivilege, - #[error("given identifier \"{0}\" of application is invalid")] - BadIdentifier(String), - #[error("given full path to application is invalid")] - BadFullAppPath, - #[error("manner \"{0}\" is already registered")] - DupManner(String), - #[error("file extension \"{0}\" is already registered")] - DupExt(String), - #[error("the token of manner is invalid")] - InvalidMannerToken, - #[error("the token of file extension is invalid")] - InvalidExtToken, -} +/// Result type used in this module. +type Result = std::result::Result; -/// The result type used in this crate. -pub type Result = std::result::Result; - -// endregion - -// region: Types - -/// The token for access registered items in Program. -/// This is usually returned when you registering them. -pub type Token = usize; - -/// The scope where wfassoc will register and unregister application. -#[derive(Debug, Copy, Clone)] -pub enum Scope { - /// Scope for current user. - User, - /// Scope for all users under this computer. - System, -} - -/// The error occurs when cast View into Scope. -#[derive(Debug, TeError)] -#[error("hybrid View can not be cast into Scope")] -pub struct TryFromViewError {} - -impl TryFromViewError { - fn new() -> Self { - Self {} - } -} - -impl TryFrom for Scope { - type Error = TryFromViewError; - - fn try_from(value: View) -> std::result::Result { - match value { - View::User => Ok(Self::User), - View::System => Ok(Self::System), - View::Hybrid => Err(TryFromViewError::new()), - } - } -} - -impl Scope { - /// Check whether we have enough privilege when operating in current scope. - /// If we have, return true, otherwise false. - pub fn has_privilege(&self) -> bool { - // If we operate on System, and we do not has privilege, - // we think we do not have privilege, otherwise, - // there is no privilege required. - !matches!(self, Self::System if !utilities::has_privilege()) - } -} - -/// The view when wfassoc querying file extension association. -#[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, -} - -impl From for View { - fn from(value: Scope) -> Self { - match value { - Scope::User => Self::User, - Scope::System => Self::System, - } - } -} - -// endregion - -// region: Program - -/// The struct representing a complete program for registration and unregistration. +/// 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 Program { - /// The identifier of this program. +pub struct Schema { identifier: String, - /// The fully qualified path to the application. - full_path: PathBuf, - /// The collection holding all manners of this program. - manners: IndexSet, - /// The collection holding all file extensions supported by this program. - /// The key is file estension and value is its associated manner for opening it. - exts: IndexMap, + path: String, + clsid: String, + icons: HashMap, + behaviors: HashMap, + exts: HashMap, } -impl Program { - /// Create a new registrar for following operations. - /// - /// `identifier` is the unique name of this program. - /// If should only contain digits and alphabet chars, - /// and should not start with any digits. - /// For example, "MyApp" is okey but following names are not okey: - /// - /// - `My App` - /// - `3DViewer` - /// - `我的Qt程序` (means "My Qt App" in English) - /// - /// More preciously, `identifier` will be used as the vendor part of ProgId. - /// - /// `full_path` is the fully qualified path to the application. - pub fn new(identifier: &str, full_path: &str) -> Result { - // Check identifier - static RE: LazyLock = LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9]*$").unwrap()); - if !RE.is_match(identifier) { - return Err(Error::BadIdentifier(identifier.to_string())); - } - - // Everything is okey, build self. - Ok(Self { - identifier: identifier.to_string(), - // The error type of PathBuf FromStr trait is Infallible, - // so it must be okey and we can use unwrap safely. - full_path: full_path.parse().unwrap(), - manners: IndexSet::new(), - exts: IndexMap::new(), - }) - } +/// Internal used struct as the Schema file extensions hashmap value type. +#[derive(Debug)] +struct SchemaExt { + name: String, + icon: String, + behavior: String, +} - /// Add manner provided by this program. - pub fn add_manner(&mut self, manner: &str) -> Result { - // TODO: Use wincmd::CmdArgs instead of String. - // Create manner from string - let manner = manner.to_string(); - // Backup a stringfied manner for error output. - let manner_str = manner.to_string(); - // Insert manner. - let idx = self.manners.len(); - if self.manners.insert(manner) { - Ok(idx) - } else { - Err(Error::DupManner(manner_str)) +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(), } } - /// Get the string display of manner represented by given token - pub fn get_manner_str(&self, token: Token) -> Option { - self.manners.get_index(token).map(|s| s.clone()) + pub fn set_identifier(&mut self, identifier: &str) -> Result<()> {} + + pub fn set_path(&mut self, exe_path: &str) -> Result<()> {} + + pub fn set_clsid(&mut self, clsid: &str) -> Result<()> {} + + pub fn add_icon(&mut self, name: &str, value: &str) -> Result<()> {} + + pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<()> {} + + pub fn add_ext( + &mut self, + ext: &str, + ext_name: &str, + ext_icon: &str, + ext_behavior: &str, + ) -> Result<()> { } - /// Add file extension supported by this program and its associated manner. - pub fn add_ext(&mut self, ext: &str, token: Token) -> Result { - // Check manner token - if let None = self.manners.get_index(token) { - return Err(Error::InvalidMannerToken); - } - - // Create extension from string - let ext = Ext::new(ext)?; - // Backup a stringfied extension for error output. - let ext_str = ext.to_string(); - // Insert file extension - let idx = self.exts.len(); - if let None = self.exts.insert(ext, token) { - Ok(idx) - } else { - Err(Error::DupExt(ext_str)) - } + pub fn into_program(self) -> Result { + Program::new(self) } +} - /// Get the string display of file extension represented by given token - pub fn get_ext_str(&self, token: Token) -> Option { - self.exts.get_index(token).map(|p| p.0.to_string()) +/// Program is a complete and immutable program representer +pub struct Program {} + +impl TryFrom for Program { + type Error = Error; + + fn try_from(value: Schema) -> std::result::Result { + Self::new(value) } } impl Program { - const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths"; - const APPLICATIONS: &str = "Software\\Classes\\Applications"; - - /// Register this application. - pub fn register(&self, scope: Scope) -> Result<()> { - // Check privilege - if !scope.has_privilege() { - return Err(Error::NoPrivilege); - } - - // Fetch root key. - let hk = RegKey::predef(match scope { - Scope::User => HKEY_CURRENT_USER, - Scope::System => HKEY_LOCAL_MACHINE, - }); - // Fetch file name and start in path. - let file_name = self.extract_file_name()?; - let start_in = self.extract_start_in()?; - - // Create App Paths subkey - debug_println!("Adding App Paths subkey..."); - let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?; - let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?; - // Write App Paths values - subkey.set_value("", &utilities::path_to_str(&self.full_path)?)?; - subkey.set_value("Path", &utilities::osstr_to_str(&start_in)?)?; - - // Create Applications subkey - debug_println!("Adding Applications subkey..."); - let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?; - let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?; - // Write Applications values - if !self.exts.is_empty() { - let (supported_types, _) = - subkey.create_subkey_with_flags("SupportedTypes", KEY_WRITE)?; - for ext in self.exts.keys() { - supported_types.set_value(ext.to_string(), &"")?; - } - } - - // Create ProgId subkeys - debug_println!("Adding ProgId subkey..."); - let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?; - for (ext, manner_token) in self.exts.iter() { - let manner = self.manners.get_index(*manner_token).ok_or(Error::InvalidMannerToken)?; - let prog_id = self.build_prog_id(ext); - - debug_println!("Adding ProgId \"{0}\" subkey...", prog_id.to_string()); - let (subkey, _) = subkey_parent.create_subkey_with_flags(prog_id.to_string(), KEY_READ)?; - let (subkey_verb, _) = subkey.create_subkey_with_flags("open", KEY_READ)?; - let (subkey_command, _) = subkey_verb.create_subkey_with_flags("command", KEY_WRITE)?; - subkey_command.set_value("", manner)?; - } - - // Okey - utilities::notify_assoc_changed(); - Ok(()) - } - - /// Unregister this application. - pub fn unregister(&self, scope: Scope) -> Result<()> { - // Check privilege - if !scope.has_privilege() { - return Err(Error::NoPrivilege); - } - - // Fetch root key and file name. - let hk = RegKey::predef(match scope { - Scope::User => HKEY_CURRENT_USER, - Scope::System => HKEY_LOCAL_MACHINE, - }); - let file_name = self.extract_file_name()?; - - // Remove App Paths subkey - debug_println!("Removing App Paths subkey..."); - let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_WRITE)?; - subkey_parent.delete_subkey_all(file_name)?; - - // Remove Applications subkey - debug_println!("Removing Applications subkey..."); - let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?; - subkey_parent.delete_subkey_all(file_name)?; - - // Remove ProgId subkeys - debug_println!("Removing ProgId subkey..."); - let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?; - for ext in self.exts.keys() { - let prog_id = self.build_prog_id(ext); - - debug_println!("Removing ProgId \"{0}\" subkey...", prog_id.to_string()); - subkey_parent.delete_subkey_all(prog_id.to_string())?; - } - - // Okey - utilities::notify_assoc_changed(); - Ok(()) - } - - /// Check whether this application has been registered. - /// - /// Please note that this is a rough check and do not validate any data. - pub fn is_registered(&self, scope: Scope) -> Result { - // Fetch root key and file name. - let hk = RegKey::predef(match scope { - Scope::User => HKEY_CURRENT_USER, - Scope::System => HKEY_LOCAL_MACHINE, - }); - let file_name = self.extract_file_name()?; - - // Check App Paths subkey. - debug_println!("Checking App Paths subkey..."); - let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?; - if let Err(_) = subkey_parent.open_subkey_with_flags(file_name, KEY_READ) { - return Ok(false); - } - - // Check Application subkey. - debug_println!("Checking Applications subkey..."); - let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?; - if let Err(_) = subkey_parent.open_subkey_with_flags(file_name, KEY_READ) { - return Ok(false); - } - - // Both subkeys are roughly existing. - Ok(true) - } + pub fn new(schema: Schema) -> Result {} } -impl Program { - const CLASSES: &str = "Software\\Classes"; - /// Set the default "open with" of given token associated extension to this program. - pub fn link_ext(&self, ext: Token, scope: Scope) -> Result<()> { - // Check privilege - if !scope.has_privilege() { - return Err(Error::NoPrivilege); - } - - // Fetch file extension and build ProgId from it - let (ext, _) = match self.exts.get_index(ext) { - Some(v) => v, - None => return Err(Error::InvalidExtToken), - }; - let prog_id = self.build_prog_id(ext); - - // Fetch root key and navigate to Classes - let hk = RegKey::predef(match scope { - Scope::User => HKEY_CURRENT_USER, - Scope::System => HKEY_LOCAL_MACHINE, - }); - let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?; - - // Open or create this extension key - let (subkey, _) = classes.create_subkey_with_flags(ext.to_string(), KEY_WRITE)?; - // Set the default way to open this file extension - subkey.set_value("", &prog_id.to_string())?; - - // Okey - Ok(()) - } - - /// Remove this program from the default "open with" of given token associated extension. - /// - /// If the default "open with" of given extension is not our program, - /// or there is no such file extension, this function do nothing. - pub fn unlink_ext(&self, ext: Token, scope: Scope) -> Result<()> { - // Check privilege - if !scope.has_privilege() { - return Err(Error::NoPrivilege); - } - - // Fetch file extension and build ProgId from it - let (ext, _) = match self.exts.get_index(ext) { - Some(v) => v, - None => return Err(Error::InvalidExtToken), - }; - let prog_id = self.build_prog_id(ext); - - // Fetch root key and navigate to Classes - let hk = RegKey::predef(match scope { - Scope::User => HKEY_CURRENT_USER, - Scope::System => HKEY_LOCAL_MACHINE, - }); - let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?; - - // Open key for this extension. - // If there is no such key, return directly. - if let Some(subkey) = - extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_WRITE)? - { - // Only delete the default key if it is equal to our ProgId - if let Some(value) = extra::winreg::try_get_value::(&subkey, "")? { - if value == prog_id.to_string() { - // Delete the default key. - subkey.delete_value("")?; - } - } - } - - // Okey - Ok(()) - } - - /// Query the default "open with" of given token associated extension. - /// - /// This function will return its associated ProgId if it is existing. - pub fn query_ext(&self, ext: Token, view: View) -> Result> { - // Fetch file extension - let (ext, _) = match self.exts.get_index(ext) { - Some(v) => v, - None => return Err(Error::InvalidExtToken), - }; - - // Fetch root key and navigate to Classes - 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(Self::CLASSES, KEY_READ)?, - View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?, - }; - - // Open key for this extension if possible - let rv = - match extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_READ)? { - Some(subkey) => { - // Try get associated ProgId if possible - match extra::winreg::try_get_value::(&subkey, "")? { - Some(value) => Some(ProgId::from(value.as_str())), - None => None, - } - } - None => None, - }; - - // Okey - Ok(rv) - } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ExtKey { + inner: winconcept::Ext } - -impl Program { - /// Extract the file name part from full path to application, - /// which was used in Registry path component. - fn extract_file_name(&self) -> Result<&OsStr> { - // Get the file name part and make sure it is not empty. - // Empty checker is CRUCIAL! - self.full_path - .file_name() - .and_then(|p| if p.is_empty() { None } else { Some(p) }) - .ok_or(Error::BadFullAppPath) - } - - /// Extract the start in path from full path to application, - /// which basically is the stem of full path. - fn extract_start_in(&self) -> Result<&OsStr> { - // Get parent part and make sure it is not empty - // Empty checker is CRUCIAL! - self.full_path - .parent() - .map(|p| p.as_os_str()) - .and_then(|p| if p.is_empty() { None } else { Some(p) }) - .ok_or(Error::BadFullAppPath) - } - - /// Build ProgId from identifier and given file extension. - fn build_prog_id(&self, ext: &Ext) -> ProgId { - ProgId::Std(assoc::StdProgId::new( - &self.identifier, - &utilities::capitalize_first_ascii(ext.inner()), - None, - )) - } -} - -// endregion diff --git a/wfassoc/src/lib_old.rs b/wfassoc/src/lib_old.rs new file mode 100644 index 0000000..1940099 --- /dev/null +++ b/wfassoc/src/lib_old.rs @@ -0,0 +1,501 @@ +//! This crate provide utilities fetching and manilupating Windows file association. +//! All code under crate are following Microsoft document: https://learn.microsoft.com/en-us/windows/win32/shell/customizing-file-types-bumper + +#[cfg(not(target_os = "windows"))] +compile_error!("Crate wfassoc is only supported on Windows."); + +pub mod extra; +pub mod utilities; +pub mod assoc; + +use assoc::{Ext, ProgId}; +use indexmap::{IndexMap, IndexSet}; +use regex::Regex; +use std::ffi::OsStr; +use std::path::PathBuf; +use std::sync::LazyLock; +use thiserror::Error as TeError; +use winreg::RegKey; +use winreg::enums::{ + HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE, +}; + +// region: Error Types + +/// All possible error occurs in this crate. +#[derive(Debug, TeError)] +pub enum Error { + #[error("error occurs when manipulating with Registry: {0}")] + BadRegOper(#[from] std::io::Error), + #[error("{0}")] + CastOsStr(#[from] utilities::CastOsStrError), + #[error("{0}")] + ParseExt(#[from] assoc::ParseExtError), + + #[error("no administrative privilege")] + NoPrivilege, + #[error("given identifier \"{0}\" of application is invalid")] + BadIdentifier(String), + #[error("given full path to application is invalid")] + BadFullAppPath, + #[error("manner \"{0}\" is already registered")] + DupManner(String), + #[error("file extension \"{0}\" is already registered")] + DupExt(String), + #[error("the token of manner is invalid")] + InvalidMannerToken, + #[error("the token of file extension is invalid")] + InvalidExtToken, +} + +/// The result type used in this crate. +pub type Result = std::result::Result; + +// endregion + +// region: Types + +/// The token for access registered items in Program. +/// This is usually returned when you registering them. +pub type Token = usize; + +/// The scope where wfassoc will register and unregister application. +#[derive(Debug, Copy, Clone)] +pub enum Scope { + /// Scope for current user. + User, + /// Scope for all users under this computer. + System, +} + +/// The error occurs when cast View into Scope. +#[derive(Debug, TeError)] +#[error("hybrid View can not be cast into Scope")] +pub struct TryFromViewError {} + +impl TryFromViewError { + fn new() -> Self { + Self {} + } +} + +impl TryFrom for Scope { + type Error = TryFromViewError; + + fn try_from(value: View) -> std::result::Result { + match value { + View::User => Ok(Self::User), + View::System => Ok(Self::System), + View::Hybrid => Err(TryFromViewError::new()), + } + } +} + +impl Scope { + /// Check whether we have enough privilege when operating in current scope. + /// If we have, return true, otherwise false. + pub fn has_privilege(&self) -> bool { + // If we operate on System, and we do not has privilege, + // we think we do not have privilege, otherwise, + // there is no privilege required. + !matches!(self, Self::System if !utilities::has_privilege()) + } +} + +/// The view when wfassoc querying file extension association. +#[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, +} + +impl From for View { + fn from(value: Scope) -> Self { + match value { + Scope::User => Self::User, + Scope::System => Self::System, + } + } +} + +// endregion + +// region: Program + +/// The struct representing a complete program for registration and unregistration. +#[derive(Debug)] +pub struct Program { + /// The identifier of this program. + identifier: String, + /// The fully qualified path to the application. + full_path: PathBuf, + /// The collection holding all manners of this program. + manners: IndexSet, + /// The collection holding all file extensions supported by this program. + /// The key is file estension and value is its associated manner for opening it. + exts: IndexMap, +} + +impl Program { + /// Create a new registrar for following operations. + /// + /// `identifier` is the unique name of this program. + /// If should only contain digits and alphabet chars, + /// and should not start with any digits. + /// For example, "MyApp" is okey but following names are not okey: + /// + /// - `My App` + /// - `3DViewer` + /// - `我的Qt程序` (means "My Qt App" in English) + /// + /// More preciously, `identifier` will be used as the vendor part of ProgId. + /// + /// `full_path` is the fully qualified path to the application. + pub fn new(identifier: &str, full_path: &str) -> Result { + // Check identifier + static RE: LazyLock = LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9]*$").unwrap()); + if !RE.is_match(identifier) { + return Err(Error::BadIdentifier(identifier.to_string())); + } + + // Everything is okey, build self. + Ok(Self { + identifier: identifier.to_string(), + // The error type of PathBuf FromStr trait is Infallible, + // so it must be okey and we can use unwrap safely. + full_path: full_path.parse().unwrap(), + manners: IndexSet::new(), + exts: IndexMap::new(), + }) + } + + /// Add manner provided by this program. + pub fn add_manner(&mut self, manner: &str) -> Result { + // TODO: Use wincmd::CmdArgs instead of String. + // Create manner from string + let manner = manner.to_string(); + // Backup a stringfied manner for error output. + let manner_str = manner.to_string(); + // Insert manner. + let idx = self.manners.len(); + if self.manners.insert(manner) { + Ok(idx) + } else { + Err(Error::DupManner(manner_str)) + } + } + + /// Get the string display of manner represented by given token + pub fn get_manner_str(&self, token: Token) -> Option { + self.manners.get_index(token).map(|s| s.clone()) + } + + /// Add file extension supported by this program and its associated manner. + pub fn add_ext(&mut self, ext: &str, token: Token) -> Result { + // Check manner token + if let None = self.manners.get_index(token) { + return Err(Error::InvalidMannerToken); + } + + // Create extension from string + let ext = Ext::new(ext)?; + // Backup a stringfied extension for error output. + let ext_str = ext.to_string(); + // Insert file extension + let idx = self.exts.len(); + if let None = self.exts.insert(ext, token) { + Ok(idx) + } else { + Err(Error::DupExt(ext_str)) + } + } + + /// Get the string display of file extension represented by given token + pub fn get_ext_str(&self, token: Token) -> Option { + self.exts.get_index(token).map(|p| p.0.to_string()) + } +} + +impl Program { + const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths"; + const APPLICATIONS: &str = "Software\\Classes\\Applications"; + + /// Register this application. + pub fn register(&self, scope: Scope) -> Result<()> { + // Check privilege + if !scope.has_privilege() { + return Err(Error::NoPrivilege); + } + + // Fetch root key. + let hk = RegKey::predef(match scope { + Scope::User => HKEY_CURRENT_USER, + Scope::System => HKEY_LOCAL_MACHINE, + }); + // Fetch file name and start in path. + let file_name = self.extract_file_name()?; + let start_in = self.extract_start_in()?; + + // Create App Paths subkey + debug_println!("Adding App Paths subkey..."); + let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?; + let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?; + // Write App Paths values + subkey.set_value("", &utilities::path_to_str(&self.full_path)?)?; + subkey.set_value("Path", &utilities::osstr_to_str(&start_in)?)?; + + // Create Applications subkey + debug_println!("Adding Applications subkey..."); + let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?; + let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?; + // Write Applications values + if !self.exts.is_empty() { + let (supported_types, _) = + subkey.create_subkey_with_flags("SupportedTypes", KEY_WRITE)?; + for ext in self.exts.keys() { + supported_types.set_value(ext.to_string(), &"")?; + } + } + + // Create ProgId subkeys + debug_println!("Adding ProgId subkey..."); + let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?; + for (ext, manner_token) in self.exts.iter() { + let manner = self.manners.get_index(*manner_token).ok_or(Error::InvalidMannerToken)?; + let prog_id = self.build_prog_id(ext); + + debug_println!("Adding ProgId \"{0}\" subkey...", prog_id.to_string()); + let (subkey, _) = subkey_parent.create_subkey_with_flags(prog_id.to_string(), KEY_READ)?; + let (subkey_verb, _) = subkey.create_subkey_with_flags("open", KEY_READ)?; + let (subkey_command, _) = subkey_verb.create_subkey_with_flags("command", KEY_WRITE)?; + subkey_command.set_value("", manner)?; + } + + // Okey + utilities::notify_assoc_changed(); + Ok(()) + } + + /// Unregister this application. + pub fn unregister(&self, scope: Scope) -> Result<()> { + // Check privilege + if !scope.has_privilege() { + return Err(Error::NoPrivilege); + } + + // Fetch root key and file name. + let hk = RegKey::predef(match scope { + Scope::User => HKEY_CURRENT_USER, + Scope::System => HKEY_LOCAL_MACHINE, + }); + let file_name = self.extract_file_name()?; + + // Remove App Paths subkey + debug_println!("Removing App Paths subkey..."); + let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_WRITE)?; + subkey_parent.delete_subkey_all(file_name)?; + + // Remove Applications subkey + debug_println!("Removing Applications subkey..."); + let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?; + subkey_parent.delete_subkey_all(file_name)?; + + // Remove ProgId subkeys + debug_println!("Removing ProgId subkey..."); + let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?; + for ext in self.exts.keys() { + let prog_id = self.build_prog_id(ext); + + debug_println!("Removing ProgId \"{0}\" subkey...", prog_id.to_string()); + subkey_parent.delete_subkey_all(prog_id.to_string())?; + } + + // Okey + utilities::notify_assoc_changed(); + Ok(()) + } + + /// Check whether this application has been registered. + /// + /// Please note that this is a rough check and do not validate any data. + pub fn is_registered(&self, scope: Scope) -> Result { + // Fetch root key and file name. + let hk = RegKey::predef(match scope { + Scope::User => HKEY_CURRENT_USER, + Scope::System => HKEY_LOCAL_MACHINE, + }); + let file_name = self.extract_file_name()?; + + // Check App Paths subkey. + debug_println!("Checking App Paths subkey..."); + let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?; + if let Err(_) = subkey_parent.open_subkey_with_flags(file_name, KEY_READ) { + return Ok(false); + } + + // Check Application subkey. + debug_println!("Checking Applications subkey..."); + let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?; + if let Err(_) = subkey_parent.open_subkey_with_flags(file_name, KEY_READ) { + return Ok(false); + } + + // Both subkeys are roughly existing. + Ok(true) + } +} + +impl Program { + const CLASSES: &str = "Software\\Classes"; + + /// Set the default "open with" of given token associated extension to this program. + pub fn link_ext(&self, ext: Token, scope: Scope) -> Result<()> { + // Check privilege + if !scope.has_privilege() { + return Err(Error::NoPrivilege); + } + + // Fetch file extension and build ProgId from it + let (ext, _) = match self.exts.get_index(ext) { + Some(v) => v, + None => return Err(Error::InvalidExtToken), + }; + let prog_id = self.build_prog_id(ext); + + // Fetch root key and navigate to Classes + let hk = RegKey::predef(match scope { + Scope::User => HKEY_CURRENT_USER, + Scope::System => HKEY_LOCAL_MACHINE, + }); + let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?; + + // Open or create this extension key + let (subkey, _) = classes.create_subkey_with_flags(ext.to_string(), KEY_WRITE)?; + // Set the default way to open this file extension + subkey.set_value("", &prog_id.to_string())?; + + // Okey + Ok(()) + } + + /// Remove this program from the default "open with" of given token associated extension. + /// + /// If the default "open with" of given extension is not our program, + /// or there is no such file extension, this function do nothing. + pub fn unlink_ext(&self, ext: Token, scope: Scope) -> Result<()> { + // Check privilege + if !scope.has_privilege() { + return Err(Error::NoPrivilege); + } + + // Fetch file extension and build ProgId from it + let (ext, _) = match self.exts.get_index(ext) { + Some(v) => v, + None => return Err(Error::InvalidExtToken), + }; + let prog_id = self.build_prog_id(ext); + + // Fetch root key and navigate to Classes + let hk = RegKey::predef(match scope { + Scope::User => HKEY_CURRENT_USER, + Scope::System => HKEY_LOCAL_MACHINE, + }); + let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?; + + // Open key for this extension. + // If there is no such key, return directly. + if let Some(subkey) = + extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_WRITE)? + { + // Only delete the default key if it is equal to our ProgId + if let Some(value) = extra::winreg::try_get_value::(&subkey, "")? { + if value == prog_id.to_string() { + // Delete the default key. + subkey.delete_value("")?; + } + } + } + + // Okey + Ok(()) + } + + /// Query the default "open with" of given token associated extension. + /// + /// This function will return its associated ProgId if it is existing. + pub fn query_ext(&self, ext: Token, view: View) -> Result> { + // Fetch file extension + let (ext, _) = match self.exts.get_index(ext) { + Some(v) => v, + None => return Err(Error::InvalidExtToken), + }; + + // Fetch root key and navigate to Classes + 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(Self::CLASSES, KEY_READ)?, + View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?, + }; + + // Open key for this extension if possible + let rv = + match extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_READ)? { + Some(subkey) => { + // Try get associated ProgId if possible + match extra::winreg::try_get_value::(&subkey, "")? { + Some(value) => Some(ProgId::from(value.as_str())), + None => None, + } + } + None => None, + }; + + // Okey + Ok(rv) + } +} + +impl Program { + /// Extract the file name part from full path to application, + /// which was used in Registry path component. + fn extract_file_name(&self) -> Result<&OsStr> { + // Get the file name part and make sure it is not empty. + // Empty checker is CRUCIAL! + self.full_path + .file_name() + .and_then(|p| if p.is_empty() { None } else { Some(p) }) + .ok_or(Error::BadFullAppPath) + } + + /// Extract the start in path from full path to application, + /// which basically is the stem of full path. + fn extract_start_in(&self) -> Result<&OsStr> { + // Get parent part and make sure it is not empty + // Empty checker is CRUCIAL! + self.full_path + .parent() + .map(|p| p.as_os_str()) + .and_then(|p| if p.is_empty() { None } else { Some(p) }) + .ok_or(Error::BadFullAppPath) + } + + /// Build ProgId from identifier and given file extension. + fn build_prog_id(&self, ext: &Ext) -> ProgId { + ProgId::Std(assoc::StdProgId::new( + &self.identifier, + &utilities::capitalize_first_ascii(ext.inner()), + None, + )) + } +} + +// endregion diff --git a/wfassoc/src/shit.rs b/wfassoc/src/lib_old_old.rs similarity index 100% rename from wfassoc/src/shit.rs rename to wfassoc/src/lib_old_old.rs diff --git a/wfassoc/src/utilities.rs b/wfassoc/src/utilities.rs index aaba153..d69225d 100644 --- a/wfassoc/src/utilities.rs +++ b/wfassoc/src/utilities.rs @@ -23,77 +23,6 @@ macro_rules! debug_println { }; } -// region: Windows Related - -/// Check whether current process has administrative privilege. -/// -/// It usually means that checking whether current process is running as Administrator. -/// Return true if it is, otherwise false. -/// -/// Reference: https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership -pub fn has_privilege() -> bool { - use windows_sys::Win32::Foundation::HANDLE; - use windows_sys::Win32::Security::{ - AllocateAndInitializeSid, CheckTokenMembership, FreeSid, PSID, SECURITY_NT_AUTHORITY, - }; - use windows_sys::Win32::System::SystemServices::{ - DOMAIN_ALIAS_RID_ADMINS, SECURITY_BUILTIN_DOMAIN_RID, - }; - use windows_sys::core::BOOL; - - let nt_authority = SECURITY_NT_AUTHORITY.clone(); - let mut administrators_group: PSID = PSID::default(); - let success: BOOL = unsafe { - AllocateAndInitializeSid( - &nt_authority, - 2, - SECURITY_BUILTIN_DOMAIN_RID as u32, - DOMAIN_ALIAS_RID_ADMINS as u32, - 0, - 0, - 0, - 0, - 0, - 0, - &mut administrators_group, - ) - }; - - if success == 0 { - panic!("Win32 AllocateAndInitializeSid() failed"); - } - - let mut is_member: BOOL = BOOL::default(); - let success: BOOL = - unsafe { CheckTokenMembership(HANDLE::default(), administrators_group, &mut is_member) }; - - unsafe { - FreeSid(administrators_group); - } - - if success == 0 { - panic!("Win32 CheckTokenMembership() failed"); - } - - is_member != 0 -} - -/// Notify Windows that some file associations are changed, and should refresh them. -/// This function must be called once you change any file associations. -pub fn notify_assoc_changed() -> () { - use windows_sys::Win32::UI::Shell::{SHCNE_ASSOCCHANGED, SHCNF_IDLIST, SHChangeNotify}; - unsafe { - SHChangeNotify( - SHCNE_ASSOCCHANGED as i32, - SHCNF_IDLIST, - std::ptr::null(), - std::ptr::null(), - ) - } -} - -// endregion - // region OS String Related /// The error occurs when casting `OsStr` into `str`. diff --git a/wfassoc/src/win32ext.rs b/wfassoc/src/win32ext.rs new file mode 100644 index 0000000..e02381f --- /dev/null +++ b/wfassoc/src/win32ext.rs @@ -0,0 +1,70 @@ +//! The module contains some Windows-specific functions for file associations. +//! These functions can not be grouped as Windows concept. +//! So they are placed in there as an independent module. + +/// Check whether current process has administrative privilege. +/// +/// It usually means that checking whether current process is running as Administrator. +/// Return true if it is, otherwise false. +/// +/// Reference: https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership +pub fn has_privilege() -> bool { + use windows_sys::Win32::Foundation::HANDLE; + use windows_sys::Win32::Security::{ + AllocateAndInitializeSid, CheckTokenMembership, FreeSid, PSID, SECURITY_NT_AUTHORITY, + }; + use windows_sys::Win32::System::SystemServices::{ + DOMAIN_ALIAS_RID_ADMINS, SECURITY_BUILTIN_DOMAIN_RID, + }; + use windows_sys::core::BOOL; + + let nt_authority = SECURITY_NT_AUTHORITY.clone(); + let mut administrators_group: PSID = PSID::default(); + let success: BOOL = unsafe { + AllocateAndInitializeSid( + &nt_authority, + 2, + SECURITY_BUILTIN_DOMAIN_RID as u32, + DOMAIN_ALIAS_RID_ADMINS as u32, + 0, + 0, + 0, + 0, + 0, + 0, + &mut administrators_group, + ) + }; + + if success == 0 { + panic!("Win32 AllocateAndInitializeSid() failed"); + } + + let mut is_member: BOOL = BOOL::default(); + let success: BOOL = + unsafe { CheckTokenMembership(HANDLE::default(), administrators_group, &mut is_member) }; + + unsafe { + FreeSid(administrators_group); + } + + if success == 0 { + panic!("Win32 CheckTokenMembership() failed"); + } + + is_member != 0 +} + +/// Notify Windows that some file associations are changed, and should refresh them. +/// This function must be called once you change any file associations. +pub fn notify_assoc_changed() -> () { + use windows_sys::Win32::UI::Shell::{SHCNE_ASSOCCHANGED, SHCNF_IDLIST, SHChangeNotify}; + unsafe { + SHChangeNotify( + SHCNE_ASSOCCHANGED as i32, + SHCNF_IDLIST, + std::ptr::null(), + std::ptr::null(), + ) + } +} diff --git a/wfassoc/src/extra/windows.rs b/wfassoc/src/winconcept.rs similarity index 96% rename from wfassoc/src/extra/windows.rs rename to wfassoc/src/winconcept.rs index 7b3fa63..7de3c99 100644 --- a/wfassoc/src/extra/windows.rs +++ b/wfassoc/src/winconcept.rs @@ -1,4 +1,4 @@ -//! This module expand Windows-related stuff by `windows-sys` crate. +//! This module create some structs for Windows specific concepts by `windows-sys` crate. //! These features are not implemented in any crates (as I known scope) //! and should be manually implemented for our file association use. @@ -90,7 +90,9 @@ impl FromStr for Ext { type Err = ParseExtError; fn from_str(s: &str) -> Result { - static RE: LazyLock = LazyLock::new(|| Regex::new(r"^\.([^\.]+)$").unwrap()); + static RE: LazyLock = LazyLock::new(|| { + Regex::new(r"^\.([^\.]+)$").expect("unexpected bad regex pattern string") + }); match RE.captures(s) { Some(v) => Ok(Self::new(&v[1]).expect("unexpected dot in Ext body")), None => Err(ParseExtError::new(s)), @@ -207,8 +209,10 @@ impl FromStr for ProgId { type Err = ParseProgIdError; fn from_str(s: &str) -> Result { - static RE: LazyLock = - LazyLock::new(|| Regex::new(r"^([^\.]+)\.([^\.]+)(\.([0-9]+))?$").unwrap()); + static RE: LazyLock = LazyLock::new(|| { + Regex::new(r"^([^\.]+)\.([^\.]+)(\.([0-9]+))?$") + .expect("unexpected bad regex pattern string") + }); let caps = RE.captures(s); if let Some(caps) = caps { let vendor = &caps[1]; @@ -234,7 +238,7 @@ impl FromStr for ProgId { /// The struct representing Windows CLSID looks like /// `{26EE0668-A00A-44D7-9371-BEB064C98683}` (case insensitive). -/// The brace is essential part. +/// The curly brace is the essential part. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Clsid { inner: Uuid, @@ -324,7 +328,11 @@ pub struct ExpandString { } impl ExpandString { - const VAR_RE: LazyLock = LazyLock::new(|| Regex::new(r"%[a-zA-Z0-9_]+%").unwrap()); + /// Internal shared compiled regex pattern matching Variable, + /// the `%` braced string like `%SystemRoot%`. + const VAR_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"%[a-zA-Z0-9_]+%").expect("unexpected bad regex pattern string") + }); } impl ExpandString { @@ -466,8 +474,9 @@ impl FromStr for IconRefStr { type Err = ParseIconRefStrError; fn from_str(s: &str) -> Result { - static RE: LazyLock = - LazyLock::new(|| Regex::new(r"^([^,@].*),-([0-9]+)$").unwrap()); + static RE: LazyLock = LazyLock::new(|| { + Regex::new(r"^([^,@].*),-([0-9]+)$").expect("unexpected bad regex pattern string") + }); let caps = RE.captures(s); if let Some(caps) = caps { let path = &caps[1]; @@ -549,7 +558,9 @@ impl FromStr for StrRefStr { type Err = ParseStrRefStrError; fn from_str(s: &str) -> Result { - static RE: LazyLock = LazyLock::new(|| Regex::new(r"^@(.+),-([0-9]+)$").unwrap()); + static RE: LazyLock = LazyLock::new(|| { + Regex::new(r"^@(.+),-([0-9]+)$").expect("unexpected bad regex pattern string") + }); let caps = RE.captures(s); if let Some(caps) = caps { let path = &caps[1]; diff --git a/wfassoc/src/extra/winreg.rs b/wfassoc/src/winregext.rs similarity index 98% rename from wfassoc/src/extra/winreg.rs rename to wfassoc/src/winregext.rs index f56df09..49a8773 100644 --- a/wfassoc/src/extra/winreg.rs +++ b/wfassoc/src/winregext.rs @@ -1,4 +1,4 @@ -//! This module expand `winreg` crate to make it more suit for this crate. +//! This module extend `winreg` crate to make it more suit for the usage of this crate. use std::ffi::OsStr; use std::ops::Deref; diff --git a/wfassoc/tests/extra_windows.rs b/wfassoc/tests/winconcept.rs similarity index 99% rename from wfassoc/tests/extra_windows.rs rename to wfassoc/tests/winconcept.rs index 0c897d1..d421b6a 100644 --- a/wfassoc/tests/extra_windows.rs +++ b/wfassoc/tests/winconcept.rs @@ -1,5 +1,5 @@ use std::{path::Path, str::FromStr}; -use wfassoc::extra::windows::*; +use wfassoc::winconcept::*; #[test] fn test_ex_new() {