diff --git a/Cargo.lock b/Cargo.lock index fc30e4d..87c3555 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,12 +187,6 @@ dependencies = [ "litrs", ] -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - [[package]] name = "equivalent" version = "1.0.2" @@ -255,15 +249,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" @@ -752,7 +737,6 @@ name = "wfassoc" version = "0.1.0" dependencies = [ "indexmap", - "itertools", "regex", "thiserror", "uuid", diff --git a/README.md b/README.md index 73264b0..a975c0a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Wfassoc +# WFAssoc **W**indows **F**ile **Assoc**iation Library diff --git a/wfassoc/Cargo.toml b/wfassoc/Cargo.toml index edc7eaf..c75066a 100644 --- a/wfassoc/Cargo.toml +++ b/wfassoc/Cargo.toml @@ -20,6 +20,5 @@ windows-sys = { version = "0.60.2", features = [ winreg = { version = "0.55.0", features = ["transactions"] } widestring = "1.2.1" indexmap = "2.11.4" -itertools = "0.14.0" regex = "1.11.3" uuid = { version = "1.18.1", features = ["v4"] } diff --git a/wfassoc/src/winconcept.rs b/wfassoc/src/concept.rs similarity index 68% rename from wfassoc/src/winconcept.rs rename to wfassoc/src/concept.rs index 7de3c99..4e51471 100644 --- a/wfassoc/src/winconcept.rs +++ b/wfassoc/src/concept.rs @@ -1,11 +1,9 @@ //! 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) +//! These features are not implemented in any crates (as far as I know) //! and should be manually implemented for our file association use. -use itertools::Itertools; use regex::Regex; use std::fmt::Display; -use std::path::Path; use std::str::FromStr; use std::sync::LazyLock; use thiserror::Error as TeError; @@ -43,9 +41,9 @@ pub struct Ext { impl Ext { /// Create an new file extension. /// - /// `body` is the body of file extension (excluding dot). + /// `body` is the body of file extension (excluding dot, such as `jpg`). /// If you want to create this struct with ordinary extension string like `.jpg`, - /// please use `from_str()` instead. + /// please use `Ext::from_str()` or `parse::()` instead. pub fn new(body: &str) -> Result { // Check whether given body has dot or empty if body.is_empty() || body.contains('.') { @@ -124,10 +122,11 @@ impl BadProgIdPartError { /// The ProgId exactly follows Microsoft suggested /// `[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 in other place. -/// Additionally, `[Version]` part is optional. +/// It should be done by other structs in other places. /// /// Reference: /// - https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids @@ -238,7 +237,8 @@ impl FromStr for ProgId { /// The struct representing Windows CLSID looks like /// `{26EE0668-A00A-44D7-9371-BEB064C98683}` (case insensitive). -/// The curly brace is the essential part. +/// +/// Please note that the curly brace is the essential part of CLSID. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Clsid { inner: Uuid, @@ -289,6 +289,363 @@ impl Display for Clsid { // endregion +// region: Windows Resource Reference String + +// region: Icon Reference String + +/// Error occurs when given string is not a valid Icon Reference String. +#[derive(Debug, TeError)] +#[error("given string \"{inner}\" is not a valid Icon Reference String")] +pub struct ParseIconRefStrError { + /// The clone of string which is not a valid Icon Reference String + inner: String, +} + +impl ParseIconRefStrError { + /// Create new error instance. + fn new(s: &str) -> Self { + Self { + inner: s.to_string(), + } + } +} + +/// 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 { + /// The path part of this reference string. + /// And it can be expandable. + path: String, + /// The index part of this reference string. + index: u32, +} + +impl IconRefStr { + /// Create a new Icon Reference String. + /// + /// `path` is the path to the icon resource file and it can be one of following types: + /// + /// * Absolute path: `C:\Windows\System32\imageres.dll` + /// * Relative path: `imageres.dll` + /// * Expandable Path: `%SystemRoot%\System32\imageres.dll` + /// + /// And it also can be quoted like: `"C:\Program Files\MyApp\MyApp.dll"` + /// + /// `index` is the index of the icon in the resource file. + pub fn new(path: &str, index: u32) -> Self { + Self { + path: path.to_string(), + index, + } + } + + /// Get the path part of this reference string. + /// + /// This path can be absolute path, relative path or expandable path. + pub fn get_path(&self) -> &str { + &self.path + } + + /// Get the index part of this reference string. + pub fn get_index(&self) -> u32 { + self.index + } +} + +impl Display for IconRefStr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{},-{}", self.path, self.index) + } +} + +impl FromStr for IconRefStr { + type Err = ParseIconRefStrError; + + fn from_str(s: &str) -> Result { + 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]; + let index = caps + .get(2) + .and_then(|sv| sv.as_str().parse::().ok()) + .ok_or(ParseIconRefStrError::new(s))?; + Ok(Self::new(path, index)) + } else { + Err(ParseIconRefStrError::new(s)) + } + } +} + +// endregion + +// region: String Reference String + +/// Error occurs when given string is not a valid String Reference String. +#[derive(Debug, TeError)] +#[error("given string \"{inner}\" is not a valid String Reference String")] +pub struct ParseStrRefStrError { + /// The clone of string which is not a valid String Reference String + inner: String, +} + +impl ParseStrRefStrError { + /// Create new error instance. + fn new(s: &str) -> Self { + Self { + inner: s.to_string(), + } + } +} + +/// 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 { + /// The path part of this reference string. + /// And it can be expandable. + path: String, + /// The index part of this reference string. + index: u32, +} + +impl StrRefStr { + /// Create a new Icon Reference String. + /// + /// `path` is the path to the string resource file. + /// `index` is the index of the string in the resource file. + /// For the detail of these parameters, please see IconRefStr. + pub fn new(path: &str, index: u32) -> Self { + Self { + path: path.to_string(), + index, + } + } + + /// Get the path part of this reference string. + /// + /// This path can be absolute path, relative path or expandable path. + pub fn get_path(&self) -> &str { + &self.path + } + + /// Get the index part of this reference string. + pub fn get_index(&self) -> u32 { + self.index + } +} + +impl Display for StrRefStr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "@{},-{}", self.path, self.index) + } +} + +impl FromStr for StrRefStr { + type Err = ParseStrRefStrError; + + fn from_str(s: &str) -> Result { + 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]; + let index = caps + .get(2) + .and_then(|sv| sv.as_str().parse::().ok()) + .ok_or(ParseStrRefStrError::new(s))?; + Ok(Self::new(path, index)) + } else { + Err(ParseStrRefStrError::new(s)) + } + } +} + +// endregion + +// endregion + +// region: Windows Resource + +// region: Icon Resource + +/// Error occurs when loading icon. +#[derive(Debug, TeError)] +#[error("error occurs when loading icon resource")] +pub enum LoadIconRcError { + /// Given path has embedded NUL. + EmbeddedNul(#[from] widestring::error::ContainsNul), + /// Error occurs when executing Win32 extract function. + ExtractIcon, +} + +/// The size kind of loaded icon +#[derive(Debug, Clone, Copy)] +pub enum IconSizeKind { + /// Small Icon + Small, + /// Large Icon + Large, +} + +/// The struct representing a loaded icon resource. +pub struct IconRc { + icon: HICON, +} + +impl IconRc { + /// Load icon from executable or `.ico` file. + /// + /// If you want to extract icon from `.ico` file, please pass `0` to `index` parameter. + /// Otherwise `index` is the icon resource index located in executable. + pub fn new(file: &str, index: u32, kind: IconSizeKind) -> Result { + use windows_sys::Win32::UI::Shell::ExtractIconExW; + + let mut icon = HICON::default(); + let icon_ptr = &mut icon as *mut HICON; + let file = WideCString::from_str(file)?; + let index = index as i32; + + let rv = unsafe { + match kind { + IconSizeKind::Small => { + ExtractIconExW(file.as_ptr(), index, std::ptr::null_mut(), icon_ptr, 1) + } + IconSizeKind::Large => { + ExtractIconExW(file.as_ptr(), index, icon_ptr, std::ptr::null_mut(), 1) + } + } + }; + + if rv != 1 || icon.is_null() { + Err(LoadIconRcError::ExtractIcon) + } else { + Ok(Self { icon }) + } + } + + /// An alias to default constructor. + /// It automatically handle the index parameter for you + /// when loading `.ico` file, rather than executable file. + pub fn with_ico_file(file: &str, kind: IconSizeKind) -> Result { + Self::new(file, 0, kind) + } + + pub unsafe fn from_raw(hicon: HICON) -> Self { + Self { icon: hicon } + } + + pub fn into_raw(self) -> HICON { + self.icon + } +} + +impl IconRc { + pub fn get_icon(&self) -> HICON { + self.icon + } +} + +impl Drop for IconRc { + fn drop(&mut self) { + use windows_sys::Win32::UI::WindowsAndMessaging::DestroyIcon; + + if !self.icon.is_null() { + unsafe { + DestroyIcon(self.icon); + } + } + } +} + +// endregion + +// region: String Resource + +/// Error occurs when loading string. +#[derive(Debug, TeError)] +#[error("error occurs when loading string resource")] +pub enum LoadStrRcError { + /// Given path has embedded NUL. + EmbeddedNul(#[from] widestring::error::ContainsNul), + /// The encoding of string is invalid. + BadEncoding(#[from] widestring::error::Utf16Error), + /// Error when casting integer + CastInteger(#[from] std::num::TryFromIntError), + /// Can no load library including string resource. + LoadLibrary, + /// Fail to load string resource from file. + LoadString, +} + +/// The struct representing a loaded string resource. +pub struct StrRc { + inner: String, +} + +impl StrRc { + pub fn new(file: &str, index: u32) -> Result { + use windows_sys::Win32::Foundation::FreeLibrary; + use windows_sys::Win32::System::LibraryLoader::{ + LOAD_LIBRARY_AS_DATAFILE, LOAD_LIBRARY_AS_IMAGE_RESOURCE, LoadLibraryExW, + }; + use windows_sys::Win32::UI::WindowsAndMessaging::LoadStringW; + use windows_sys::core::PWSTR; + + // Load library first + let file = WideCString::from_str(file)?; + let hmodule = unsafe { + LoadLibraryExW( + file.as_ptr(), + std::ptr::null_mut(), + LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE, + ) + }; + if hmodule.is_null() { + return Err(LoadStrRcError::LoadLibrary); + } + + // Load string + let mut buffer: *const u16 = std::ptr::null(); + let buffer_ptr = &mut buffer as *mut *const u16 as PWSTR; + let char_count = unsafe { LoadStringW(hmodule, index, buffer_ptr, 0) }; + // We write this function to make sure following "FreeLibrary" must be executed. + fn load_string(buffer: *const u16, char_count: i32) -> Result { + if char_count == 0 { + Err(LoadStrRcError::LoadString) + } else { + let buffer = unsafe { WideStr::from_ptr(buffer, char_count.try_into()?) }; + Ok(buffer.to_string()?) + } + } + let res_str = load_string(buffer, char_count); + + // Unload library + unsafe { FreeLibrary(hmodule) }; + + // Return value + res_str.map(|s| Self { inner: s }) + } +} + +impl StrRc { + pub fn get_string(&self) -> &str { + &self.inner + } +} + +// endregion + +// endregion + // region: Expand String /// Error occurs when creating Expand String. @@ -400,640 +757,3 @@ impl FromStr for ExpandString { } // endregion - -// region: Windows Resource Reference String - -// region: Icon Reference String - -/// Error occurs when given string is not a valid Icon Reference String. -#[derive(Debug, TeError)] -#[error("given string \"{inner}\" is not a valid Icon Reference String")] -pub struct ParseIconRefStrError { - /// The clone of string which is not a valid Icon Reference String - inner: String, -} - -impl ParseIconRefStrError { - /// Create new error instance. - fn new(s: &str) -> Self { - Self { - inner: s.to_string(), - } - } -} - -/// The struct representing an Icon Reference String -/// looks like `%SystemRoot%\System32\imageres.dll,-72`. -pub struct IconRefStr { - /// The path part of this reference string. - /// And it can be expandable. - path: String, - /// The index part of this reference string. - index: u32, -} - -impl IconRefStr { - /// Create a new Icon Reference String. - /// - /// `path` is the path to the icon resource file and it can be one of following types: - /// - /// * Absolute path: `C:\Windows\System32\imageres.dll` - /// * Relative path: `imageres.dll` - /// * Expandable Path: `%SystemRoot%\System32\imageres.dll` - /// - /// And it also can be quoted like: `"C:\Program Files\MyApp\MyApp.dll"` - /// - /// `index` is the index of the icon in the resource file. - pub fn new(path: &str, index: u32) -> Self { - Self { - path: path.to_string(), - index, - } - } - - /// Get the path part of this reference string. - /// - /// This path can be absolute path, relative path or expandable path. - pub fn get_path(&self) -> &str { - &self.path - } - - /// Get the index part of this reference string. - pub fn get_index(&self) -> u32 { - self.index - } -} - -impl Display for IconRefStr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{},-{}", self.path, self.index) - } -} - -impl FromStr for IconRefStr { - type Err = ParseIconRefStrError; - - fn from_str(s: &str) -> Result { - 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]; - let index = caps - .get(2) - .and_then(|sv| sv.as_str().parse::().ok()) - .ok_or(ParseIconRefStrError::new(s))?; - Ok(Self::new(path, index)) - } else { - Err(ParseIconRefStrError::new(s)) - } - } -} - -// endregion - -// region: String Reference String - -/// Error occurs when given string is not a valid String Reference String. -#[derive(Debug, TeError)] -#[error("given string \"{inner}\" is not a valid String Reference String")] -pub struct ParseStrRefStrError { - /// The clone of string which is not a valid String Reference String - inner: String, -} - -impl ParseStrRefStrError { - /// Create new error instance. - fn new(s: &str) -> Self { - Self { - inner: s.to_string(), - } - } -} - -/// The struct representing an String Reference String -/// looks like `@%SystemRoot%\System32\shell32.dll,-30596`. -pub struct StrRefStr { - /// The path part of this reference string. - /// And it can be expandable. - path: String, - /// The index part of this reference string. - index: u32, -} - -impl StrRefStr { - /// Create a new Icon Reference String. - /// - /// `path` is the path to the string resource file. - /// `index` is the index of the string in the resource file. - /// For the detail of these parameters, please see IconRefStr. - pub fn new(path: &str, index: u32) -> Self { - Self { - path: path.to_string(), - index, - } - } - - /// Get the path part of this reference string. - /// - /// This path can be absolute path, relative path or expandable path. - pub fn get_path(&self) -> &str { - &self.path - } - - /// Get the index part of this reference string. - pub fn get_index(&self) -> u32 { - self.index - } -} - -impl Display for StrRefStr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "@{},-{}", self.path, self.index) - } -} - -impl FromStr for StrRefStr { - type Err = ParseStrRefStrError; - - fn from_str(s: &str) -> Result { - 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]; - let index = caps - .get(2) - .and_then(|sv| sv.as_str().parse::().ok()) - .ok_or(ParseStrRefStrError::new(s))?; - Ok(Self::new(path, index)) - } else { - Err(ParseStrRefStrError::new(s)) - } - } -} - -// endregion - -// endregion - -// region: Windows Resource - -// region: Icon Resource - -/// Error occurs when loading icon. -#[derive(Debug, TeError)] -#[error("error occurs when loading icon resource")] -pub enum LoadIconRcError { - /// Given path has embedded NUL. - EmbeddedNul(#[from] widestring::error::ContainsNul), - /// Error occurs when executing Win32 extract function. - ExtractIcon, -} - -/// The size kind of loaded icon -#[derive(Debug, Clone, Copy)] -pub enum IconSizeKind { - /// Small Icon - Small, - /// Large Icon - Large, -} - -/// The struct representing a loaded icon resource. -pub struct IconRc { - icon: HICON, -} - -impl IconRc { - /// Load icon from executable or `.ico` file. - /// - /// If you want to extract icon from `.ico` file, please pass `0` to `index` parameter. - /// Otherwise `index` is the icon resource index located in executable. - pub fn new(file: &Path, index: u32, kind: IconSizeKind) -> Result { - use windows_sys::Win32::UI::Shell::ExtractIconExW; - - let mut icon = HICON::default(); - let icon_ptr = &mut icon as *mut HICON; - let file = WideCString::from_os_str(file.as_os_str())?; - let index = index as i32; - - let rv = unsafe { - match kind { - IconSizeKind::Small => { - ExtractIconExW(file.as_ptr(), index, std::ptr::null_mut(), icon_ptr, 1) - } - IconSizeKind::Large => { - ExtractIconExW(file.as_ptr(), index, icon_ptr, std::ptr::null_mut(), 1) - } - } - }; - - if rv != 1 || icon.is_null() { - Err(LoadIconRcError::ExtractIcon) - } else { - Ok(Self { icon }) - } - } - - /// An alias to default constructor. - /// It automatically handle the index parameter for you - /// when loading `.ico` file, rather than executable file. - pub fn with_ico_file(file: &Path, kind: IconSizeKind) -> Result { - Self::new(file, 0, kind) - } - - pub unsafe fn from_raw(hicon: HICON) -> Self { - Self { icon: hicon } - } - - pub fn into_raw(self) -> HICON { - self.icon - } -} - -impl IconRc { - pub fn get_icon(&self) -> HICON { - self.icon - } -} - -impl Drop for IconRc { - fn drop(&mut self) { - use windows_sys::Win32::UI::WindowsAndMessaging::DestroyIcon; - - if !self.icon.is_null() { - unsafe { - DestroyIcon(self.icon); - } - } - } -} - -// endregion - -// region: String Resource - -/// Error occurs when loading string. -#[derive(Debug, TeError)] -#[error("error occurs when loading string resource")] -pub enum LoadStrRcError { - /// Given path has embedded NUL. - EmbeddedNul(#[from] widestring::error::ContainsNul), - /// The encoding of string is invalid. - BadEncoding(#[from] widestring::error::Utf16Error), - /// Error when casting integer - CastInteger(#[from] std::num::TryFromIntError), - /// Can no load library including string resource. - LoadLibrary, - /// Fail to load string resource from file. - LoadString, -} - -pub struct StrRc { - inner: String, -} - -impl StrRc { - pub fn new(file: &Path, index: u32) -> Result { - use windows_sys::Win32::Foundation::FreeLibrary; - use windows_sys::Win32::System::LibraryLoader::{ - LOAD_LIBRARY_AS_DATAFILE, LOAD_LIBRARY_AS_IMAGE_RESOURCE, LoadLibraryExW, - }; - use windows_sys::Win32::UI::WindowsAndMessaging::LoadStringW; - use windows_sys::core::PWSTR; - - // Load library first - let file = WideCString::from_os_str(file.as_os_str())?; - let hmodule = unsafe { - LoadLibraryExW( - file.as_ptr(), - std::ptr::null_mut(), - LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE, - ) - }; - if hmodule.is_null() { - return Err(LoadStrRcError::LoadLibrary); - } - - // Load string - let mut buffer: *const u16 = std::ptr::null(); - let buffer_ptr = &mut buffer as *mut *const u16 as PWSTR; - let char_count = unsafe { LoadStringW(hmodule, index, buffer_ptr, 0) }; - // We write this function to make sure following "FreeLibrary" must be executed. - fn load_string(buffer: *const u16, char_count: i32) -> Result { - if char_count == 0 { - Err(LoadStrRcError::LoadString) - } else { - let buffer = unsafe { WideStr::from_ptr(buffer, char_count.try_into()?) }; - Ok(buffer.to_string()?) - } - } - let res_str = load_string(buffer, char_count); - - // Unload library - unsafe { FreeLibrary(hmodule) }; - - // Return value - res_str.map(|s| Self { inner: s }) - } -} - -impl StrRc { - pub fn get_string(&self) -> &str { - &self.inner - } -} - -// endregion - -// endregion - -// region: Windows Commandline - -// region: Cmd Lexer - -/// The lexer for Windows commandline argument split. -/// -/// Reference: https://learn.microsoft.com/en-us/cpp/cpp/main-function-command-line-args?view=msvc-170#parsing-c-command-line-arguments -pub struct CmdLexer> { - chars: std::iter::Peekable, - finished: bool, -} - -impl> CmdLexer { - pub fn new(iter: I) -> Self { - Self { - chars: iter.peekable(), - finished: false, - } - } -} - -impl> Iterator for CmdLexer { - type Item = String; - - fn next(&mut self) -> Option { - if self.finished { - return None; - } - - let mut token = String::new(); - let mut in_quotes = false; - - loop { - match self.chars.next() { - Some(c) => match c { - // Handle whitespace - ' ' | '\t' | '\n' | '\x0b' if !in_quotes => { - // Skip leading whitespace before token - if token.is_empty() { - continue; - } else { - // End of current token - break; - } - } - - // Handle backslash - '\\' => { - // Eat backslash as much as possible and count it. - let mut backslashes: usize = 1; - while self.chars.peek().copied() == Some('\\') { - self.chars.next(); - backslashes += 1; - } - - if self.chars.peek().copied() == Some('"') { - // Rule: backslashes before a double quote are special - - // consume the " - let quote = self.chars.next().unwrap(); - - // Even number: backslashes become half, quote is delimiter - // Odd number: backslashes become half, quote is literal - let num_slashes = backslashes / 2; - let is_literal_quote = backslashes % 2 != 0; - - token.push_str(&"\\".repeat(num_slashes)); - if is_literal_quote { - token.push(quote); - } else { - in_quotes = !in_quotes; - } - } else { - // Not followed by quote: treat all backslashes literally - token.push_str(&"\\".repeat(backslashes)); - } - } - - // Handle quote - '"' => { - // Check if it's an escaped quote inside quotes: "" becomes " - if in_quotes && self.chars.peek() == Some(&'"') { - self.chars.next(); // consume second " - token.push('"'); - } else { - // Toggle quote state - in_quotes = !in_quotes; - } - } - - // Regular character - _ => { - token.push(c); - } - }, - - None => { - self.finished = true; - break; - } - } - } - - // If we're at EOF and token is empty, return None - if token.is_empty() && self.finished { - None - } else { - Some(token) - } - } -} - -// endregion - -// region: Cmd Path - -/// The struct representing a single commandline argument. -#[derive(Debug)] -pub struct CmdArg { - /// The not quoted value hold by this argument. - inner: String, -} - -impl CmdArg { - /// Construct a commandline argument from user input string (may quoted string). - pub fn new(s: &str) -> Result { - Self::from_str(s) - } - - /// Construct a commandline argument with direct inner value (not quoted string). - pub fn with_inner(s: &str) -> Self { - Self { - inner: s.to_string(), - } - } - - /// Get the real value hold by this commandline argument (not quoted string). - pub fn get_inner(&self) -> &str { - &self.inner - } - - /// Get the quoted string of this argument - /// so that you can append it into your built full commandline string. - /// - /// `force` is an indication of whether we should quote the argument - /// even if it does not contain any characters that would ordinarily require quoting. - /// - /// If you just want to get the stored string of this, - /// please use `to_string()` instead. - /// - /// Reference: https://learn.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way - pub fn to_quoted_string(&self, force: bool) -> String { - // Unless forced, don't quote if the argument doesn't contain special characters - let mut quoted_arg = String::with_capacity(self.inner.len()); - - if !force - && !self.inner.is_empty() - && !self - .inner - .chars() - .any(|c| matches!(c, ' ' | '\t' | '\n' | '\x0b' | '"')) - { - quoted_arg.push_str(&self.inner); - } else { - quoted_arg.push('"'); - - let mut chars = self.inner.chars(); - loop { - let mut c = chars.next(); - let mut backslash_count: usize = 0; - - // Count consecutive backslashes - while c == Some('\\') { - c = chars.next(); - backslash_count += 1; - } - - if let None = c { - // Escape all backslashes, but let the terminating - // double quotation mark we add below be interpreted - // as a metacharacter. - quoted_arg.push_str(&"\\".repeat(backslash_count * 2)); - break; - } else if c == Some('"') { - // Escape all backslashes and the following - // double quotation mark. - quoted_arg.push_str(&"\\".repeat(backslash_count * 2 + 1)); - quoted_arg.push(c.unwrap()); - } else { - // Backslashes aren't special here. - quoted_arg.push_str(&"\\".repeat(backslash_count)); - quoted_arg.push(c.unwrap()); - } - } - - quoted_arg.push('"'); - } - - return quoted_arg; - } -} - -/// Error occurs when creating commandline argument. -#[derive(Debug, TeError)] -pub enum ParseCmdArgError { - #[error("given string is not a commandline argument")] - NoArg, - #[error("given string may contain multiple commandline arguments")] - MultiArg, -} - -impl FromStr for CmdArg { - type Err = ParseCmdArgError; - - fn from_str(s: &str) -> Result { - let mut lexer = CmdLexer::new(s.chars()); - let inner = match lexer.next() { - Some(v) => v, - None => return Err(ParseCmdArgError::NoArg), - }; - if let Some(_) = lexer.next() { - return Err(ParseCmdArgError::MultiArg); - } - - Ok(Self { inner }) - } -} - -impl Display for CmdArg { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.inner) - } -} - -// endregion - -// region: Cmd Arguments - -/// The struct representing a single commandline argument. -#[derive(Debug)] -pub struct CmdArgs { - /// The list of arguments - args: Vec, -} - -impl CmdArgs { - pub fn new(s: &str) -> Self { - Self::from_str(s).expect("Infallible failed") - } - - pub fn with_inner(args: impl Iterator) -> Self { - Self { - args: args.collect(), - } - } - - pub fn get_inner(&self) -> &[CmdArg] { - &self.args - } - - /// Build the string which can be recognised by Windows Cmd - /// with proper escape. - pub fn to_quoted_string(&self) -> String { - self.args - .iter() - // We set "force" to false to prevent any switches are quoted. - .map(|a| a.to_quoted_string(false)) - .join(" ") - } -} - -impl FromStr for CmdArgs { - type Err = std::convert::Infallible; - - fn from_str(s: &str) -> Result { - Ok(Self { - args: CmdLexer::new(s.chars()) - .map(|a| CmdArg::with_inner(a.as_str())) - .collect(), - }) - } -} - -// endregion - -// endregion diff --git a/wfassoc/src/lib.rs b/wfassoc/src/lib.rs index e4f34db..f457771 100644 --- a/wfassoc/src/lib.rs +++ b/wfassoc/src/lib.rs @@ -5,95 +5,98 @@ compile_error!("Crate wfassoc is only supported on Windows."); pub mod utilities; -pub mod winconcept; -pub mod win32ext; -pub mod winregext; +pub mod concept; -use std::collections::HashMap; -use thiserror::Error as TeError; +// pub mod utilities; +// pub mod winconcept; +// pub mod win32ext; +// pub mod winregext; -/// Error occurs in this module. -#[derive(Debug, TeError)] -pub enum Error {} +// use std::collections::HashMap; +// use thiserror::Error as TeError; -/// Result type used in this module. -type Result = std::result::Result; +// /// Error occurs in this module. +// #[derive(Debug, TeError)] +// pub enum Error {} -/// 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, -} +// /// Result type used in this module. +// type Result = std::result::Result; -/// Internal used struct as the Schema file extensions hashmap value type. -#[derive(Debug)] -struct SchemaExt { - name: String, - icon: String, - behavior: String, -} +// /// 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, +// } -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(), - } - } +// /// Internal used struct as the Schema file extensions hashmap value type. +// #[derive(Debug)] +// struct SchemaExt { +// name: String, +// icon: String, +// behavior: String, +// } - pub fn set_identifier(&mut self, identifier: &str) -> Result<()> {} +// 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(), +// } +// } - pub fn set_path(&mut self, exe_path: &str) -> Result<()> {} +// pub fn set_identifier(&mut self, identifier: &str) -> Result<()> {} - pub fn set_clsid(&mut self, clsid: &str) -> Result<()> {} +// pub fn set_path(&mut self, exe_path: &str) -> Result<()> {} - pub fn add_icon(&mut self, name: &str, value: &str) -> Result<()> {} +// pub fn set_clsid(&mut self, clsid: &str) -> Result<()> {} - pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<()> {} +// pub fn add_icon(&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<()> { - } +// pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<()> {} - pub fn into_program(self) -> Result { - Program::new(self) - } -} +// pub fn add_ext( +// &mut self, +// ext: &str, +// ext_name: &str, +// ext_icon: &str, +// ext_behavior: &str, +// ) -> Result<()> { +// } -/// Program is a complete and immutable program representer -pub struct Program {} +// pub fn into_program(self) -> Result { +// Program::new(self) +// } +// } -impl TryFrom for Program { - type Error = Error; +// /// Program is a complete and immutable program representer +// pub struct Program {} - fn try_from(value: Schema) -> std::result::Result { - Self::new(value) - } -} +// impl TryFrom for Program { +// type Error = Error; -impl Program { - pub fn new(schema: Schema) -> Result {} -} +// fn try_from(value: Schema) -> std::result::Result { +// Self::new(value) +// } +// } + +// impl Program { +// pub fn new(schema: Schema) -> Result {} +// } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ExtKey { - inner: winconcept::Ext -} +// #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +// pub struct ExtKey { +// inner: winconcept::Ext +// } diff --git a/wfassoc/src/utilities.rs b/wfassoc/src/utilities.rs index d69225d..4ee9816 100644 --- a/wfassoc/src/utilities.rs +++ b/wfassoc/src/utilities.rs @@ -12,13 +12,13 @@ macro_rules! debug_println { // For no argument. () => { if cfg!(debug_assertions) { - println!(); + eprintln!(); } }; // For one or more arguments like println!. ($($arg:tt)*) => { if cfg!(debug_assertions) { - println!($($arg)*); + eprintln!($($arg)*); } }; } @@ -27,7 +27,7 @@ macro_rules! debug_println { /// The error occurs when casting `OsStr` into `str`. #[derive(Debug, TeError)] -#[error("failed when casting OS string into string")] +#[error("fail to cast OS string into string")] pub struct CastOsStrError {} impl CastOsStrError { diff --git a/wfassoc/tests/winconcept.rs b/wfassoc/tests/concept.rs similarity index 62% rename from wfassoc/tests/winconcept.rs rename to wfassoc/tests/concept.rs index d421b6a..8fad225 100644 --- a/wfassoc/tests/winconcept.rs +++ b/wfassoc/tests/concept.rs @@ -1,8 +1,10 @@ -use std::{path::Path, str::FromStr}; -use wfassoc::winconcept::*; +use std::str::FromStr; +use wfassoc::concept::*; + +// region: File Extension #[test] -fn test_ex_new() { +fn test_ext_new() { fn ok_tester(s: &str, probe: &str) { let rv = Ext::new(s); assert!(rv.is_ok()); @@ -37,6 +39,10 @@ fn test_ext_parse() { err_tester("jar"); } +// endregion + +// region: Programmatic Identifiers + #[test] fn test_prog_id_new() { fn ok_tester(vendor: &str, component: &str, version: Option, probe: &str) { @@ -50,6 +56,7 @@ fn test_prog_id_new() { assert!(rv.is_err()); } + ok_tester("VSCode", "c++", None, "VSCode.c++"); ok_tester("PowerPoint", "Template", Some(12), "PowerPoint.Template.12"); err_tester("", "MyApp", None); err_tester("Me", "", None); @@ -79,11 +86,17 @@ fn test_prog_id_parse() { err_tester("What the f*ck?"); } +// endregion + +// region: CLSID + #[test] fn test_clsid() { fn ok_tester(s: &str) { let rv = Clsid::from_str(s); assert!(rv.is_ok()); + let rv = rv.unwrap(); + assert_eq!(s.to_lowercase(), rv.to_string().to_lowercase()); } fn err_tester(s: &str) { let rv = Clsid::from_str(s); @@ -96,6 +109,12 @@ fn test_clsid() { err_tester("{26EE0668A00A-44D7-9371-BEB064C98683}"); } +// endregion + +// region: Windows Resource Reference String + +// region: Icon Reference String + #[test] fn test_icon_ref_str() { fn ok_tester(s: &str, probe: (&str, u32)) { @@ -115,9 +134,14 @@ fn test_icon_ref_str() { (r#"%SystemRoot%\System32\imageres.dll"#, 72), ); err_tester(r#"C:\Windows\Cursors\aero_arrow.cur"#); + err_tester(r#"This is my application, OK?"#); err_tester(r#"@%SystemRoot%\System32\shell32.dll,-30596"#); } +// endregion + +// region: String Reference String + #[test] fn test_str_ref_str() { fn ok_tester(s: &str, probe: (&str, u32)) { @@ -140,29 +164,57 @@ fn test_str_ref_str() { err_tester(r#"%SystemRoot%\System32\imageres.dll,-72"#); } +// endregion + +// endregion + +// region: Windows Resource + +// region: Icon Resource + #[test] fn test_icon_rc() { - fn tester(file: &str, index: u32) { - let icon = IconRc::new(Path::new(file), index, IconSizeKind::Small); + fn ok_tester(file: &str, index: u32) { + let icon = IconRc::new(file, index, IconSizeKind::Small); assert!(icon.is_ok()) } + fn err_tester(file: &str, index: u32) { + let icon = IconRc::new(file, index, IconSizeKind::Small); + assert!(icon.is_err()) + } // We pick it from "jpegfile" ProgId - tester("imageres.dll", 72); - tester("notepad.exe", 0); + ok_tester("imageres.dll", 72); + ok_tester("notepad.exe", 0); + err_tester("this-executable-must-inexisting.exe", 114514); } +// endregion + +// region: String Resource + #[test] fn test_str_rc() { - fn tester(file: &str, index: u32) { - let rv = StrRc::new(Path::new(file), index); + fn ok_tester(file: &str, index: u32) { + let rv = StrRc::new(file, index); assert!(rv.is_ok()); } + fn err_tester(file: &str, index: u32) { + let rv = StrRc::new(file, index); + assert!(rv.is_err()); + } - // We pick from "jpegfile" ProgId - tester("shell32.dll", 30596); + // We pick it from "jpegfile" ProgId + ok_tester("shell32.dll", 30596); + err_tester("this-executable-must-inexisting.exe", 114514); } +// endregion + +// endregion + +// region: Expand String + #[test] fn test_expand_string() { fn tester(s: &str) { @@ -177,91 +229,4 @@ fn test_expand_string() { tester(r#"%SystemRoot%\System32\shell32.dll"#); } -#[test] -fn test_cmd_args() { - // Declare tester - fn tester(s: &str, probe: &[&'static str]) { - let rv = CmdArgs::new(s); - let inner = rv.get_inner(); - assert_eq!(inner.len(), probe.len()); - - let n = inner.len(); - for i in 0..n { - assert_eq!(inner[i].get_inner(), probe[i]); - } - } - - // Normal cases - tester( - "MyApp.exe --config ppic.toml", - &["MyApp.exe", "--config", "ppic.toml"], - ); - tester( - r#""C:\Program Files\MyApp\MyApp.exe" --config ppic.toml"#, - &[ - r#"C:\Program Files\MyApp\MyApp.exe"#, - "--config", - "ppic.toml", - ], - ); - - // Microsoft shitty cases. - tester(r#""abc" d e"#, &[r#"abc"#, r#"d"#, r#"e"#]); - tester(r#"a\\b d"e f"g h"#, &[r#"a\\b"#, r#"de fg"#, r#"h"#]); - tester(r#"a\\\"b c d"#, &[r#"a\"b"#, r#"c"#, r#"d"#]); - tester(r#"a\\\\"b c" d e"#, &[r#"a\\b c"#, r#"d"#, r#"e"#]); - tester(r#"a"b"" c d"#, &[r#"ab" c d"#]); -} - -#[test] -fn test_cmd_arg() { - // Declare tester - fn ok_tester(s: &str, probe: &str) { - let rv = CmdArg::new(s); - assert!(rv.is_ok()); - let rv = rv.unwrap(); - assert_eq!(rv.get_inner(), probe); - } - fn err_tester(s: &str) { - let rv = CmdArg::new(s); - assert!(rv.is_err()); - } - - // Normal test - ok_tester( - r#""C:\Program Files\MyApp\MyApp.exe""#, - r#"C:\Program Files\MyApp\MyApp.exe"#, - ); - err_tester("MyApp.exe --config ppic.toml"); - err_tester(""); -} - -#[test] -fn test_cmd_arg_quoted_string() { - fn tester(s: &str, probe: &str) { - let rv = CmdArg::with_inner(s); - assert_eq!(rv.to_quoted_string(true), probe); - } - - tester( - r#"C:\Program Files\MyApp\MyApp.exe"#, - r#""C:\Program Files\MyApp\MyApp.exe""#, - ); -} - -#[test] -fn test_cmd_args_quoted_string() { - fn tester(args: &[&str], probe: &str) { - let rv = CmdArgs::with_inner(args.iter().map(|s| CmdArg::with_inner(s))); - assert_eq!(rv.to_quoted_string(), probe); - } - - tester( - &[ - r#"C:\Program Files\MyApp\MyApp.exe"#, - "--config", - "ppic.toml", - ], - r#""C:\Program Files\MyApp\MyApp.exe" --config ppic.toml"#, - ); -} +// endregion