diff --git a/wfassoc/src/assoc.rs b/wfassoc/src/assoc.rs index da2adcf..07b0784 100644 --- a/wfassoc/src/assoc.rs +++ b/wfassoc/src/assoc.rs @@ -236,11 +236,3 @@ impl Display for Clsid { } // endregion - -// region: Icon Resource - -// endregion - -// region: String Resource - -// endregion diff --git a/wfassoc/src/extra/windows.rs b/wfassoc/src/extra/windows.rs index 238563e..da7d97b 100644 --- a/wfassoc/src/extra/windows.rs +++ b/wfassoc/src/extra/windows.rs @@ -9,9 +9,383 @@ use std::path::Path; use std::str::FromStr; use std::sync::LazyLock; use thiserror::Error as TeError; +use uuid::Uuid; use widestring::{WideCString, WideChar, WideStr}; use windows_sys::Win32::UI::WindowsAndMessaging::HICON; +// region: File Extension + +/// The error occurs when constructing Ext with bad body. +#[derive(Debug, TeError)] +#[error("given file extension body \"{inner}\" is invalid")] +pub struct BadExtBodyError { + /// The clone of string which is not a valid file extension body. + inner: String, +} + +impl BadExtBodyError { + /// Create new error instance. + fn new(inner: &str) -> Self { + Self { + inner: inner.to_string(), + } + } +} + +/// 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. + /// + /// `body` is the body of file extension (excluding dot). + /// If you want to create this struct with ordinary extension string like `.jpg`, + /// please use `from_str()` instead. + pub fn new(body: &str) -> Result { + // Check whether given body has dot or empty + if body.is_empty() || body.contains('.') { + Err(BadExtBodyError::new(body)) + } else { + Ok(Self { + body: body.to_string(), + }) + } + } + + /// 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 { + /// The clone of string which is not a valid file extension. + inner: String, +} + +impl ParseExtError { + /// Create new error instance. + 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::new(&v[1]).expect("unexpected dot in Ext body")), + None => Err(ParseExtError::new(s)), + } + } +} + +// endregion + +// region: Programmatic Identifiers (ProgId) + +/// The error occurs when constructing ProgId. +#[derive(Debug, TeError)] +#[error("given ProgId part \"{inner}\" is invalid")] +pub struct BadProgIdPartError { + /// The clone of string which is not a valid ProgId part. + inner: String, +} + +impl BadProgIdPartError { + /// Create new error instance. + fn new(s: &str) -> Self { + Self { + inner: s.to_string(), + } + } +} + +/// The ProgId exactly follows Microsoft suggested +/// `[Vendor or Application].[Component].[Version]` format. +/// +/// However, most of applications do no follow this standard, +/// this scenario is not convered by this struct in there. +/// It should be done in other place. +/// Additionally, `[Version]` part is optional. +/// +/// Reference: +/// - https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids +/// - https://learn.microsoft.com/en-us/windows/win32/com/-progid--key +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ProgId { + /// The vendor part of ProgId. + vendor: String, + /// The component part of ProgId. + component: String, + /// The optional version part of ProgId. + version: Option, +} + +impl ProgId { + /// Create a new ProgId with given parts. + pub fn new( + vendor: &str, + component: &str, + version: Option, + ) -> Result { + // Check whether vendor or component part is empty or has dot + if vendor.is_empty() || vendor.contains('.') { + Err(BadProgIdPartError::new(vendor)) + } else if component.is_empty() || component.contains('.') { + Err(BadProgIdPartError::new(component)) + } else { + Ok(Self { + vendor: vendor.to_string(), + component: component.to_string(), + version, + }) + } + } + + /// Get the vendor part of standard ProgId. + pub fn get_vendor(&self) -> &str { + &self.vendor + } + + /// Get the component part of standard ProgId. + pub fn get_component(&self) -> &str { + &self.component + } + + /// Get the version part of standard ProgId. + pub fn get_version(&self) -> Option { + self.version + } +} + +/// The error occurs when parsing ProgId. +#[derive(Debug, TeError)] +#[error("given ProgId \"{inner}\" is invalid")] +pub struct ParseProgIdError { + /// The clone of string which is not a valid ProgId. + inner: String, +} + +impl ParseProgIdError { + /// Create new error instance. + fn new(s: &str) -> Self { + Self { + inner: s.to_string(), + } + } +} + +impl Display for ProgId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.version { + Some(version) => write!(f, "{}.{}.{}", self.vendor, self.component, version), + None => write!(f, "{}.{}", self.vendor, self.component), + } + } +} + +impl FromStr for ProgId { + type Err = ParseProgIdError; + + fn from_str(s: &str) -> Result { + static RE: LazyLock = + LazyLock::new(|| Regex::new(r"^([^\.]+)\.([^\.]+)(\.([0-9]+))?$").unwrap()); + let caps = RE.captures(s); + if let Some(caps) = caps { + let vendor = &caps[1]; + let component = &caps[2]; + let version = match caps.get(4) { + Some(sv) => Some( + sv.as_str() + .parse::() + .map_err(|_| ParseProgIdError::new(s))?, + ), + None => None, + }; + Ok(Self::new(vendor, component, version).expect("unexpected bad part of ProgId")) + } else { + Err(ParseProgIdError::new(s)) + } + } +} + +// endregion + +// region: CLSID + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +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: Expand String + +/// Error occurs when creating Expand String. +#[derive(Debug, TeError)] +#[error("given string is not an expand string")] +pub struct ParseExpandStrError {} + +impl ParseExpandStrError { + fn new() -> Self { + Self {} + } +} + +/// Error occurs when expand Expand String +#[derive(Debug, TeError)] +#[error("error occurs when expanding expand string")] +pub enum ExpandEnvVarError { + /// Given string has embedded NUL. + EmbeddedNul(#[from] widestring::error::ContainsNul), + /// The encoding of string is invalid. + BadEncoding(#[from] widestring::error::Utf16Error), + /// Error occurs when int type casting. + BadIntCast(#[from] std::num::TryFromIntError), + /// Integeral arithmatic downflow. + Underflow, + /// Error occurs when executing Win32 expand function. + ExpandFunction, + /// Some environment vairable are not expanded. + NoEnvVar, +} + +/// The struct representing an Expand String, +/// which contain environment variable in string, +/// like `%LOCALAPPDATA%\SomeApp.exe`. +pub struct ExpandString { + inner: String, +} + +impl ExpandString { + const VAR_RE: LazyLock = LazyLock::new(|| Regex::new(r"%[a-zA-Z0-9_]+%").unwrap()); +} + +impl ExpandString { + /// Create a new expand string + pub fn new(s: &str) -> Result { + Self::from_str(s) + } + + /// Expand the variables located in this string + /// and produce the final usable string. + pub fn expand_string(&self) -> Result { + use windows_sys::Win32::System::Environment::ExpandEnvironmentStringsW; + + // Fetch the size of expand result + let source = WideCString::from_str(self.inner.as_str())?; + let size = unsafe { ExpandEnvironmentStringsW(source.as_ptr(), std::ptr::null_mut(), 0) }; + if size == 0 { + return Err(ExpandEnvVarError::ExpandFunction); + } + let size_no_nul = size.checked_sub(1).ok_or(ExpandEnvVarError::Underflow)?; + + // Allocate buffer for it. + let len: usize = size.try_into()?; + let len_no_nul = len.checked_sub(1).ok_or(ExpandEnvVarError::Underflow)?; + let mut buffer = vec![0; len]; + // Receive result + let size = + unsafe { ExpandEnvironmentStringsW(source.as_ptr(), buffer.as_mut_ptr(), size_no_nul) }; + if size == 0 { + return Err(ExpandEnvVarError::ExpandFunction); + } + + // Cast result as Rust string + let wstr = unsafe { WideStr::from_ptr(buffer.as_ptr(), len_no_nul) }; + let rv = wstr.to_string()?; + + // If the final string still has environment variable, + // we think we fail to expand it. + if Self::VAR_RE.is_match(rv.as_str()) { + Err(ExpandEnvVarError::NoEnvVar) + } else { + Ok(rv) + } + } +} + +impl Display for ExpandString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + +impl FromStr for ExpandString { + type Err = ParseExpandStrError; + + fn from_str(s: &str) -> Result { + if Self::VAR_RE.is_match(s) { + Ok(Self { + inner: s.to_string(), + }) + } else { + Err(ParseExpandStrError::new()) + } + } +} + +// endregion + // region: Windows Resource Reference String // region: Icon Reference String @@ -359,114 +733,6 @@ impl StrRc { // endregion -// region: Expand String - -/// Error occurs when creating Expand String. -#[derive(Debug, TeError)] -#[error("given string is not an expand string")] -pub struct ParseExpandStrError {} - -impl ParseExpandStrError { - fn new() -> Self { - Self {} - } -} - -/// Error occurs when expand Expand String -#[derive(Debug, TeError)] -#[error("error occurs when expanding expand string")] -pub enum ExpandEnvVarError { - /// Given string has embedded NUL. - EmbeddedNul(#[from] widestring::error::ContainsNul), - /// The encoding of string is invalid. - BadEncoding(#[from] widestring::error::Utf16Error), - /// Error occurs when int type casting. - BadIntCast(#[from] std::num::TryFromIntError), - /// Integeral arithmatic downflow. - Underflow, - /// Error occurs when executing Win32 expand function. - ExpandFunction, - /// Some environment vairable are not expanded. - NoEnvVar, -} - -/// The struct representing an Expand String, -/// which contain environment variable in string, -/// like `%LOCALAPPDATA%\SomeApp.exe`. -pub struct ExpandString { - inner: String, -} - -impl ExpandString { - const VAR_RE: LazyLock = LazyLock::new(|| Regex::new(r"%[a-zA-Z0-9_]+%").unwrap()); -} - -impl ExpandString { - /// Create a new expand string - pub fn new(s: &str) -> Result { - Self::from_str(s) - } - - /// Expand the variables located in this string - /// and produce the final usable string. - pub fn expand_string(&self) -> Result { - use windows_sys::Win32::System::Environment::ExpandEnvironmentStringsW; - - // Fetch the size of expand result - let source = WideCString::from_str(self.inner.as_str())?; - let size = unsafe { ExpandEnvironmentStringsW(source.as_ptr(), std::ptr::null_mut(), 0) }; - if size == 0 { - return Err(ExpandEnvVarError::ExpandFunction); - } - let size_no_nul = size.checked_sub(1).ok_or(ExpandEnvVarError::Underflow)?; - - // Allocate buffer for it. - let len: usize = size.try_into()?; - let len_no_nul = len.checked_sub(1).ok_or(ExpandEnvVarError::Underflow)?; - let mut buffer = vec![0; len]; - // Receive result - let size = - unsafe { ExpandEnvironmentStringsW(source.as_ptr(), buffer.as_mut_ptr(), size_no_nul) }; - if size == 0 { - return Err(ExpandEnvVarError::ExpandFunction); - } - - // Cast result as Rust string - let wstr = unsafe { WideStr::from_ptr(buffer.as_ptr(), len_no_nul) }; - let rv = wstr.to_string()?; - - // If the final string still has environment variable, - // we think we fail to expand it. - if Self::VAR_RE.is_match(rv.as_str()) { - Err(ExpandEnvVarError::NoEnvVar) - } else { - Ok(rv) - } - } -} - -impl Display for ExpandString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.inner) - } -} - -impl FromStr for ExpandString { - type Err = ParseExpandStrError; - - fn from_str(s: &str) -> Result { - if Self::VAR_RE.is_match(s) { - Ok(Self { - inner: s.to_string(), - }) - } else { - Err(ParseExpandStrError::new()) - } - } -} - -// endregion - // region: Windows Commandline // region: Cmd Lexer diff --git a/wfassoc/tests/extra_windows.rs b/wfassoc/tests/extra_windows.rs index 032623e..ee02400 100644 --- a/wfassoc/tests/extra_windows.rs +++ b/wfassoc/tests/extra_windows.rs @@ -1,6 +1,90 @@ -use std::path::Path; +use std::{path::Path, str::FromStr}; use wfassoc::extra::windows::*; +#[test] +fn test_ex_new() { + fn ok_tester(s: &str, probe: &str) { + let rv = Ext::new(s); + assert!(rv.is_ok()); + let rv = rv.unwrap(); + assert_eq!(rv.to_string(), probe); + } + fn err_tester(s: &str) { + let rv = Ext::new(s); + assert!(rv.is_err()); + } + + ok_tester("jpg", ".jpg"); + err_tester(".jpg"); + err_tester(""); +} + +#[test] +fn test_ext_parse() { + fn ok_tester(s: &str, probe: &str) { + let rv = Ext::from_str(s); + assert!(rv.is_ok()); + let rv = rv.unwrap(); + assert_eq!(rv.inner(), probe); + } + fn err_tester(s: &str) { + let rv = Ext::from_str(s); + assert!(rv.is_err()); + } + + ok_tester(".jpg", "jpg"); + err_tester(".jar.disabled"); + err_tester("jar"); +} + +#[test] +fn test_prog_id_new() { + fn ok_tester(vendor: &str, component: &str, version: Option, probe: &str) { + let rv = ProgId::new(vendor, component, version); + assert!(rv.is_ok()); + let rv = rv.unwrap(); + assert_eq!(rv.to_string(), probe); + } + fn err_tester(vendor: &str, component: &str, version: Option) { + let rv = ProgId::new(vendor, component, version); + assert!(rv.is_err()); + } + + ok_tester("PowerPoint", "Template", Some(12), "PowerPoint.Template.12"); + err_tester("", "MyApp", None); + err_tester("Me", "", None); + err_tester("M.e", "MyApp", None); + err_tester("Me", "My.App", None); +} + +#[test] +fn test_prog_id_parse() { + fn ok_tester(s: &str, probe_vendor: &str, probe_component: &str, probe_version: Option) { + let rv = ProgId::from_str(s); + assert!(rv.is_ok()); + let rv =rv.unwrap(); + assert_eq!(rv.get_vendor(), probe_vendor); + assert_eq!(rv.get_component(), probe_component); + assert_eq!(rv.get_version(), probe_version); + } + fn err_tester(s: &str) { + let rv = ProgId::from_str(s); + assert!(rv.is_err()); + } + + ok_tester("VSCode.c++", "VSCode", "c++", None); + ok_tester("PowerPoint.Template.12", "PowerPoint", "Template", Some(12)); + err_tester("Me.MyApp."); + err_tester("WMP11.AssocFile.3G2"); + err_tester("What the f*ck?"); +} + +#[test] +fn test_clsid() { + fn ok_tester(s: &str) {} + fn err_tester(s: &str) {} +} + #[test] fn test_icon_ref_str() { fn ok_tester(s: &str, probe: (&str, u32)) { @@ -15,7 +99,10 @@ fn test_icon_ref_str() { assert!(rv.is_err()); } - ok_tester(r#"%SystemRoot%\System32\imageres.dll,-72"#, (r#"%SystemRoot%\System32\imageres.dll"#, 72)); + ok_tester( + r#"%SystemRoot%\System32\imageres.dll,-72"#, + (r#"%SystemRoot%\System32\imageres.dll"#, 72), + ); err_tester(r#"C:\Windows\Cursors\aero_arrow.cur"#); err_tester(r#"@%SystemRoot%\System32\shell32.dll,-30596"#); } @@ -34,7 +121,10 @@ fn test_str_ref_str() { assert!(rv.is_err()); } - ok_tester(r#"@%SystemRoot%\System32\shell32.dll,-30596"#, (r#"%SystemRoot%\System32\shell32.dll"#, 30596)); + ok_tester( + r#"@%SystemRoot%\System32\shell32.dll,-30596"#, + (r#"%SystemRoot%\System32\shell32.dll"#, 30596), + ); err_tester(r#"This is my application, OK?"#); err_tester(r#"%SystemRoot%\System32\imageres.dll,-72"#); }