diff --git a/example/ppic.toml b/example/ppic.toml new file mode 100644 index 0000000..1721778 --- /dev/null +++ b/example/ppic.toml @@ -0,0 +1,25 @@ +identifier = "PineapplePicture" +path = 'C:\path\to\ppic.exe' +clsid = "{B5291320-FE7C-4069-BF87-A0AC327FCD20}" + +[manners] +common = '"C:\path\to\ppic.exe" "%1"' + +[exts] +".jpg" = "common" +".jfif" = "common" +".gif" = "common" +".bmp" = "common" +".png" = "common" +".ico" = "common" +".jpeg" = "common" +".tif" = "common" +".tiff" = "common" +".webp" = "common" +".svg" = "common" +".kra" = "common" +".xcf" = "common" +".avif" = "common" +".qoi" = "common" +".apng" = "common" +".exr" = "common" diff --git a/wfassoc/src/assoc.rs b/wfassoc/src/assoc.rs new file mode 100644 index 0000000..52fe283 --- /dev/null +++ b/wfassoc/src/assoc.rs @@ -0,0 +1,283 @@ +//! The module including all struct representing Windows file association concept, +//! like file extension, ProgId, CLSID and etc. + +use regex::Regex; +use std::fmt::Display; +use std::str::FromStr; +use std::sync::LazyLock; +use thiserror::Error as TeError; +use uuid::Uuid; + +// region: File Extension + +/// The struct representing an file extension which must start with dot (`.`) +/// and followed by at least one arbitrary characters. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Ext { + /// The body of file extension (excluding dot). + body: String, +} + +impl Ext { + /// Create an new file extension. + pub fn new(raw: &str) -> Result { + Self::from_str(raw) + } + + /// Get the body part of file extension (excluding dot) + pub fn inner(&self) -> &str { + &self.body + } +} + +/// The error occurs when try parsing string into FileExt. +#[derive(Debug, TeError)] +#[error("given file extension name \"{inner}\" is invalid")] +pub struct ParseExtError { + inner: String, +} + +impl ParseExtError { + fn new(inner: &str) -> Self { + Self { + inner: inner.to_string(), + } + } +} + +impl Display for Ext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, ".{}", self.body) + } +} + +impl FromStr for Ext { + type Err = ParseExtError; + + fn from_str(s: &str) -> Result { + static RE: LazyLock = LazyLock::new(|| Regex::new(r"^\.([^\.]+)$").unwrap()); + match RE.captures(s) { + Some(v) => Ok(Self { + body: v[1].to_string(), + }), + None => Err(ParseExtError::new(s)), + } + } +} + +// endregion + +// region: Programmatic Identifiers (ProgId) + +/// The struct representing Programmatic Identifiers (ProgId). +/// +/// Because there is optional part in standard ProgId, and not all software developers +/// are willing to following Microsoft suggestions, there is no strict constaint for ProgId. +/// So this struct is actually an enum which holding any possible ProgId format. +/// +/// Reference: +/// - https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids +/// - https://learn.microsoft.com/en-us/windows/win32/com/-progid--key +pub enum ProgId { + Plain(String), + Loose(LosseProgId), + Strict(StrictProgId), +} + +impl From<&str> for ProgId { + fn from(s: &str) -> Self { + // match it for strict ProgId first + if let Ok(v) = StrictProgId::from_str(s) { + return Self::Strict(v); + } + // then match for loose ProgId + if let Ok(v) = LosseProgId::from_str(s) { + return Self::Loose(v); + } + // fallback with plain + Self::Plain(s.to_string()) + } +} + +impl Display for ProgId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProgId::Plain(v) => v.fmt(f), + ProgId::Loose(v) => v.fmt(f), + ProgId::Strict(v) => v.fmt(f), + } + } +} + +/// The error occurs when parsing ProgId. +#[derive(Debug, TeError)] +#[error("given ProgId \"{inner}\" is invalid")] +pub struct ParseProgIdError { + inner: String, +} + +impl ParseProgIdError { + fn new(s: &str) -> Self { + Self { + inner: s.to_string(), + } + } +} + +/// The ProgId similar with strict ProgId, but no version part. +pub struct LosseProgId { + vendor: String, + component: String, +} + +impl LosseProgId { + pub fn new(vendor: &str, component: &str) -> Self { + Self { + vendor: vendor.to_string(), + component: component.to_string(), + } + } + + pub fn get_vendor(&self) -> &str { + &self.vendor + } + + pub fn get_component(&self) -> &str { + &self.component + } +} + +impl FromStr for LosseProgId { + type Err = ParseProgIdError; + + fn from_str(s: &str) -> Result { + static RE: LazyLock = + LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)$").unwrap()); + let caps = RE.captures(s); + if let Some(caps) = caps { + let vendor = &caps[1]; + let component = &caps[2]; + Ok(Self::new(vendor, component)) + } else { + Err(ParseProgIdError::new(s)) + } + } +} + +impl Display for LosseProgId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}", self.vendor, self.component) + } +} + +/// The ProgId exactly follows `[Vendor or Application].[Component].[Version]` format. +pub struct StrictProgId { + vendor: String, + component: String, + version: u32, +} + +impl StrictProgId { + pub fn new(vendor: &str, component: &str, version: u32) -> Self { + Self { + vendor: vendor.to_string(), + component: component.to_string(), + version, + } + } + + pub fn get_vendor(&self) -> &str { + &self.vendor + } + + pub fn get_component(&self) -> &str { + &self.component + } + + pub fn get_version(&self) -> u32 { + self.version + } +} + +impl FromStr for StrictProgId { + type Err = ParseProgIdError; + + fn from_str(s: &str) -> Result { + static RE: LazyLock = + LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)\.([0-9]+)$").unwrap()); + let caps = RE.captures(s); + if let Some(caps) = caps { + let vendor = &caps[1]; + let component = &caps[2]; + let version = caps[3] + .parse::() + .map_err(|_| ParseProgIdError::new(s))?; + Ok(Self::new(vendor, component, version)) + } else { + Err(ParseProgIdError::new(s)) + } + } +} + +impl Display for StrictProgId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.vendor, self.component, self.version) + } +} + +// endregion + +// region: CLSID + +pub struct Clsid { + inner: Uuid, +} + +impl Clsid { + pub fn new(uuid: &str) -> Result { + Self::from_str(uuid) + } + + // TODO: May add CLSID generator in there. +} + +/// The error occurs when parsing CLSID +#[derive(Debug, TeError)] +#[error("given string \"{inner}\" is invalid for uuid")] +pub struct ParseClsidError { + inner: String, +} + +impl ParseClsidError { + fn new(s: &str) -> Self { + Self { + inner: s.to_string(), + } + } +} + +impl FromStr for Clsid { + type Err = ParseClsidError; + + fn from_str(s: &str) -> Result { + Ok(Self { + inner: Uuid::parse_str(s).map_err(|_| ParseClsidError::new(s))?, + }) + } +} + +impl Display for Clsid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner.braced().to_string()) + } +} + +// endregion + +// region: Icon Resource + +// endregion + +// region: String Resource + +// endregion diff --git a/wfassoc/src/lib.rs b/wfassoc/src/lib.rs index 964b006..56bf815 100644 --- a/wfassoc/src/lib.rs +++ b/wfassoc/src/lib.rs @@ -4,14 +4,14 @@ #[cfg(not(target_os = "windows"))] compile_error!("Crate wfassoc is only supported on Windows."); -use regex::Regex; +pub(crate) mod assoc; +pub(crate) mod utilities; + +use indexmap::{IndexMap, IndexSet}; use std::ffi::OsStr; use std::fmt::Display; use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::LazyLock; use thiserror::Error as TeError; -use indexmap::{IndexMap, IndexSet}; use winreg::RegKey; use winreg::enums::{ HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE, @@ -21,121 +21,26 @@ use winreg::enums::{ /// All possible error occurs in this crate. #[derive(Debug, TeError)] -pub enum WfError { - #[error("no administrative privilege")] - NoPrivilege, +pub enum Error { #[error("error occurs when manipulating with Registry: {0}")] BadRegOper(#[from] std::io::Error), + #[error("{0}")] + CastOsStr(#[from] utilities::CastOsStrError), + #[error("no administrative privilege")] + NoPrivilege, #[error("given full path to application is invalid")] BadFullAppPath, - #[error("failed when casting OS string into string")] - BadOsStrCast, - - #[error("file extension {0} is already registered")] + #[error("manner \"{0}\" is already registered")] + DupManner(String), + #[error("file extension \"{0}\" is already registered")] DupExt(String), + #[error("the token of associated manner for file extension is invalid")] + InvalidAssocManner, } /// The result type used in this crate. -pub type WfResult = Result; - -// endregion - -// region: Utilities - -/// The println macro only works on Debug mode -/// for tracing the execution of some important functions. -macro_rules! debug_println { - // For no argument. - () => { - if cfg!(debug_assertions) { - println!(); - } - }; - // For one or more arguments like println!. - ($($arg:tt)*) => { - if cfg!(debug_assertions) { - println!($($arg)*); - } - }; -} - -/// 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 -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. -fn notify_assoc_changed() -> () { - use windows_sys::Win32::UI::Shell::{SHCNE_ASSOCCHANGED, SHCNF_IDLIST, SHChangeNotify}; - unsafe { - SHChangeNotify( - SHCNE_ASSOCCHANGED as i32, - SHCNF_IDLIST, - Default::default(), - Default::default(), - ) - } -} - -/// Try casting given &Path into &str. -fn path_to_str(path: &Path) -> WfResult<&str> { - path.to_str().ok_or(WfError::BadOsStrCast) -} - -/// Try casting given &OsStr into &str. -fn osstr_to_str(osstr: &OsStr) -> WfResult<&str> { - osstr.to_str().ok_or(WfError::BadOsStrCast) -} +pub type Result = std::result::Result; // endregion @@ -168,7 +73,7 @@ impl TryFromViewError { impl TryFrom for Scope { type Error = TryFromViewError; - fn try_from(value: View) -> Result { + fn try_from(value: View) -> std::result::Result { match value { View::User => Ok(Self::User), View::System => Ok(Self::System), @@ -184,7 +89,7 @@ impl Scope { // 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 !has_privilege()) + !matches!(self, Self::System if !utilities::has_privilege()) } } @@ -211,113 +116,111 @@ impl From for View { // endregion -// region: File Extension +// region: Manner -/// The struct representing an file extension which must start with dot (`.`) -/// and followed by at least one arbitrary characters. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Ext { - /// The body of file extension (excluding dot). - body: String, +/// The struct representing a program manner. +/// Manner usually mean the way to open files, +/// or more preciously, the consititution of command arguments passed to program. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Manner { + argv: String, } -impl Ext { - /// Create an new file extension. - pub fn new(raw: &str) -> Result { - Self::from_str(raw) - } - - /// Get the body part of file extension (excluding dot) - pub fn inner(&self) -> &str { - &self.body - } -} - -/// The error occurs when try parsing string into FileExt. -#[derive(Debug, TeError)] -#[error("given file extension name \"{inner}\" is invalid")] -pub struct ParseExtError { - inner: String -} - -impl ParseExtError { - fn new(inner: &str) -> Self { - Self { inner: inner.to_string() } - } -} - -impl Display for Ext { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, ".{}", self.body) - } -} - -impl FromStr for Ext { - type Err = ParseExtError; - - fn from_str(s: &str) -> Result { - static RE: LazyLock = LazyLock::new(|| Regex::new(r"^\.([^\.]+)$").unwrap()); - match RE.captures(s) { - Some(v) => Ok(Self { - body: v[1].to_string(), - }), - None => Err(ParseExtError::new(s)), +impl Manner { + pub fn new(argv: &str) -> Self { + Self { + argv: argv.to_string(), } } } +impl Display for Manner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.argv) + } +} + // endregion // region: Program /// The struct representing a complete program for registration and unregistration. pub struct Program { + /// The identifier of this program. + identifier: String, /// The fully qualified path to the application. full_path: PathBuf, - /// Optional default icon resource for overriding. - /// - /// TODO: Use specialized IconRc struct instead. - default_icon: Option, - /// Optional friendly app name for overriding. - /// - /// TODO: Use specialized StringRc for overriding. - friendly_app_name: Option, - + /// The collection holding all manners of this program. + manners: IndexSet, /// The collection holding all file extensions supported by this program. - exts: IndexSet, + /// 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最时尚` + /// + /// More preciously, `identifier` will be used as the vendor part of ProgId. + /// /// `full_path` is the fully qualified path to the application. - /// - /// `default_icon` is an optional icon resource replacing the default one - /// fetched from the first icon resource of your executable application. - /// - /// `friendly_app_name` also is an optional string or string resource replacing - /// the info fetched from executable application's version information. - pub fn new( - full_path: &Path, - default_icon: Option<&str>, - friendly_app_name: Option<&str>, - ) -> Self { + pub fn new(identifier: &str, full_path: &Path) -> Self { + // TODO: Add checker for identifier Self { + identifier: identifier.to_string(), full_path: full_path.to_path_buf(), - default_icon: default_icon.map(|s| s.to_string()), - friendly_app_name: friendly_app_name.map(|s| s.to_string()), - exts: IndexSet::new(), + manners: IndexSet::new(), + exts: IndexMap::new(), } } - /// Add file extension supported by this program. - pub fn add_ext(&mut self, ext: &Ext) -> WfResult<()> { - if self.exts.insert(ext.clone()) { - Ok(()) + /// Add manner provided by this program. + pub fn add_manner(&mut self, manner: Manner) -> Result { + // 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(WfError::DupExt(ext.to_string())) + Err(Error::DupExt(manner_str)) } } + + /// Get the reference to manner with given token. + pub fn get_manner(&self, token: Token) -> Option<&Manner> { + self.manners.get_index(token) + } + + /// Add file extension supported by this program and its associated manner. + pub fn add_ext(&mut self, ext: assoc::Ext, token: Token) -> Result { + // Check manner token + if let None = self.get_manner(token) { + return Err(Error::InvalidAssocManner); + } + // 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 reference to file extension with given token. + pub fn get_ext(&self, token: Token) -> Option<&assoc::Ext> { + self.exts.get_index(token).map(|p| p.0) + } } impl Program { @@ -325,10 +228,10 @@ impl Program { const APPLICATIONS: &str = "Software\\Classes\\Applications"; /// Register this application. - pub fn register(&self, scope: Scope) -> WfResult<()> { + pub fn register(&self, scope: Scope) -> Result<()> { // Check privilege if !scope.has_privilege() { - return Err(WfError::NoPrivilege); + return Err(Error::NoPrivilege); } // Fetch root key. @@ -345,37 +248,32 @@ impl Program { 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("", &path_to_str(&self.full_path)?)?; - subkey.set_value("Path", &osstr_to_str(&start_in)?)?; + 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 let Some(default_icon) = &self.default_icon { - subkey.set_value("DefaultIcon", default_icon)?; - } - if let Some(friendly_app_name) = &self.friendly_app_name { - subkey.set_value("FriendlyAppName", friendly_app_name)?; - } if !self.exts.is_empty() { - let (supported_types, _) = subkey.create_subkey_with_flags("SupportedTypes", KEY_WRITE)?; - for ext in &self.exts { + let (supported_types, _) = + subkey.create_subkey_with_flags("SupportedTypes", KEY_WRITE)?; + for ext in self.exts.keys() { supported_types.set_value(ext.to_string(), &"")?; } } // Okey - notify_assoc_changed(); + utilities::notify_assoc_changed(); Ok(()) } /// Unregister this application. - pub fn unregister(&self, scope: Scope) -> WfResult<()> { + pub fn unregister(&self, scope: Scope) -> Result<()> { // Check privilege if !scope.has_privilege() { - return Err(WfError::NoPrivilege); + return Err(Error::NoPrivilege); } // Fetch root key and file name. @@ -396,14 +294,14 @@ impl Program { subkey_parent.delete_subkey_all(file_name)?; // Okey - notify_assoc_changed(); + 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) -> WfResult { + 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, @@ -433,25 +331,25 @@ impl Program { impl Program { /// Extract the file name part from full path to application, /// which was used in Registry path component. - fn extract_file_name(&self) -> WfResult<&OsStr> { + 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(WfError::BadFullAppPath) + .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) -> WfResult<&OsStr> { + 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(WfError::BadFullAppPath) + .ok_or(Error::BadFullAppPath) } } diff --git a/wfassoc/src/utilities.rs b/wfassoc/src/utilities.rs new file mode 100644 index 0000000..f2ed2ad --- /dev/null +++ b/wfassoc/src/utilities.rs @@ -0,0 +1,119 @@ +//! The module containing useful stuff used in this crate. + +use std::ffi::OsStr; +use std::path::Path; +use thiserror::Error as TeError; + +/// The println macro only works on Debug mode +/// for tracing the execution of some important functions. +#[macro_export] +macro_rules! debug_println { + // For no argument. + () => { + if cfg!(debug_assertions) { + println!(); + } + }; + // For one or more arguments like println!. + ($($arg:tt)*) => { + if cfg!(debug_assertions) { + println!($($arg)*); + } + }; +} + +// 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, + Default::default(), + Default::default(), + ) + } +} + +// endregion + +// region OS String Related + +/// The error occurs when casting `OsStr` into `str`. +#[derive(Debug, TeError)] +#[error("failed when casting OS string into string")] +pub struct CastOsStrError {} + +impl CastOsStrError { + fn new() -> Self { + Self {} + } +} + +/// Try casting given &Path into &str. +pub fn path_to_str(path: &Path) -> Result<&str, CastOsStrError> { + path.to_str().ok_or(CastOsStrError::new()) +} + +/// Try casting given &OsStr into &str. +pub fn osstr_to_str(osstr: &OsStr) -> Result<&str, CastOsStrError> { + osstr.to_str().ok_or(CastOsStrError::new()) +} + +// endregion