From 84a29c862b145d8aea160b65cbc3c6a4f4de7f9e Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Mon, 20 Oct 2025 14:41:50 +0800 Subject: [PATCH] feat(windows): add ExpandString for environment variable expansion Implement ExpandString struct to handle Windows environment variable expansion with proper error handling. Also reorganize windows-sys dependencies and improve icon loading error messages. --- wfassoc/Cargo.toml | 5 +- wfassoc/src/extra/windows.rs | 129 +++++++++++++++++++++++++++++++++-- 2 files changed, 127 insertions(+), 7 deletions(-) diff --git a/wfassoc/Cargo.toml b/wfassoc/Cargo.toml index a8785bc..f1a18cd 100644 --- a/wfassoc/Cargo.toml +++ b/wfassoc/Cargo.toml @@ -9,11 +9,12 @@ license = "SPDX:MIT" [dependencies] thiserror = { workspace = true } windows-sys = { version = "0.60.2", features = [ - "Win32_Security", - "Win32_System_SystemServices", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", + "Win32_Security", + "Win32_System_Environment", "Win32_System_Registry", + "Win32_System_SystemServices", ] } winreg = { version = "0.55.0", features = ["transactions"] } widestring = "1.2.1" diff --git a/wfassoc/src/extra/windows.rs b/wfassoc/src/extra/windows.rs index 255e38b..ebc2bf3 100644 --- a/wfassoc/src/extra/windows.rs +++ b/wfassoc/src/extra/windows.rs @@ -2,20 +2,139 @@ //! These features are not implemented in any crates (as I known scope) //! and should be manually implemented for our file association use. +use regex::Regex; +use std::fmt::Display; use std::path::Path; +use std::str::FromStr; +use std::sync::LazyLock; use thiserror::Error as TeError; -use widestring::WideCString; +use widestring::{WideCStr, WideCString, WideChar}; use windows_sys::Win32::UI::Shell::ExtractIconExW; use windows_sys::Win32::UI::WindowsAndMessaging::{DestroyIcon, HICON}; +// region: Expand String + +/// Error occurs when creating Expand String. +#[derive(Debug, TeError)] +#[error("given string is not an expand string")] +pub struct BadExpandStrError {} + +impl BadExpandStrError { + 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), + /// Error occurs when executing Win32 expand function. + ExpandFunction, + /// Error occurs when int type casting. + BadIntCast(#[from] std::num::TryFromIntError), + /// Integeral arithmatic downflow. + Underflow, + /// The C-format string is invalid. + BadString(#[from] widestring::error::NulError), + /// The encoding of string is invalid. + BadEncoding(#[from] widestring::error::Utf16Error), + /// 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(), Default::default(), 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 { WideCStr::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 = BadExpandStrError; + + fn from_str(s: &str) -> Result { + if Self::VAR_RE.is_match(s) { + Ok(Self { + inner: s.to_string(), + }) + } else { + Err(BadExpandStrError::new()) + } + } +} + +// endregion + // region: Icon /// Error occurs when loading icon. #[derive(Debug, TeError)] #[error("error occurs when loading icon")] pub enum LoadIconError { - EmbeddedNul(#[from] widestring::error::ContainsNul), - Other, + /// Given path has embedded NUL. + EmbeddedNul(#[from] widestring::error::ContainsNul), + /// Error occurs when executing Win32 extract function. + ExtractIcon, } /// The size kind of loaded icon @@ -50,13 +169,13 @@ impl Icon { }; if rv != 1 || icon.is_null() { - Err(LoadIconError::Other) + Err(LoadIconError::ExtractIcon) } else { Ok(Self { icon }) } } - pub fn from_raw(hicon: HICON) -> Self { + pub unsafe fn from_raw(hicon: HICON) -> Self { Self { icon: hicon } }