1
0

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.
This commit is contained in:
2025-10-20 14:41:50 +08:00
parent 26d867d42f
commit 84a29c862b
2 changed files with 127 additions and 7 deletions

View File

@ -9,11 +9,12 @@ license = "SPDX:MIT"
[dependencies] [dependencies]
thiserror = { workspace = true } thiserror = { workspace = true }
windows-sys = { version = "0.60.2", features = [ windows-sys = { version = "0.60.2", features = [
"Win32_Security",
"Win32_System_SystemServices",
"Win32_UI_Shell", "Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging", "Win32_UI_WindowsAndMessaging",
"Win32_Security",
"Win32_System_Environment",
"Win32_System_Registry", "Win32_System_Registry",
"Win32_System_SystemServices",
] } ] }
winreg = { version = "0.55.0", features = ["transactions"] } winreg = { version = "0.55.0", features = ["transactions"] }
widestring = "1.2.1" widestring = "1.2.1"

View File

@ -2,20 +2,139 @@
//! These features are not implemented in any crates (as I known scope) //! These features are not implemented in any crates (as I known scope)
//! and should be manually implemented for our file association use. //! and should be manually implemented for our file association use.
use regex::Regex;
use std::fmt::Display;
use std::path::Path; use std::path::Path;
use std::str::FromStr;
use std::sync::LazyLock;
use thiserror::Error as TeError; 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::Shell::ExtractIconExW;
use windows_sys::Win32::UI::WindowsAndMessaging::{DestroyIcon, HICON}; 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<WideChar>),
/// 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<WideChar>),
/// 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<Regex> = LazyLock::new(|| Regex::new(r"%[a-zA-Z0-9_]+%").unwrap());
}
impl ExpandString {
/// Create a new expand string
pub fn new(s: &str) -> Result<Self, BadExpandStrError> {
Self::from_str(s)
}
/// Expand the variables located in this string
/// and produce the final usable string.
pub fn expand_string(&self) -> Result<String, ExpandEnvVarError> {
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<Self, Self::Err> {
if Self::VAR_RE.is_match(s) {
Ok(Self {
inner: s.to_string(),
})
} else {
Err(BadExpandStrError::new())
}
}
}
// endregion
// region: Icon // region: Icon
/// Error occurs when loading icon. /// Error occurs when loading icon.
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
#[error("error occurs when loading icon")] #[error("error occurs when loading icon")]
pub enum LoadIconError { pub enum LoadIconError {
EmbeddedNul(#[from] widestring::error::ContainsNul<widestring::WideChar>), /// Given path has embedded NUL.
Other, EmbeddedNul(#[from] widestring::error::ContainsNul<WideChar>),
/// Error occurs when executing Win32 extract function.
ExtractIcon,
} }
/// The size kind of loaded icon /// The size kind of loaded icon
@ -50,13 +169,13 @@ impl Icon {
}; };
if rv != 1 || icon.is_null() { if rv != 1 || icon.is_null() {
Err(LoadIconError::Other) Err(LoadIconError::ExtractIcon)
} else { } else {
Ok(Self { icon }) Ok(Self { icon })
} }
} }
pub fn from_raw(hicon: HICON) -> Self { pub unsafe fn from_raw(hicon: HICON) -> Self {
Self { icon: hicon } Self { icon: hicon }
} }