1
0

feat(windows): implement file extension and ProgId parsing structs

- Add Ext struct for handling file extensions with validation
- Implement ProgId struct following Microsoft's suggested format
- Create Clsid struct for handling CLSID with UUID parsing
- Add ExpandString struct for environment variable expansion
- Include comprehensive tests for new structs and parsing logic
- Remove duplicate ExpandString implementation from windows.rs
- Organize code regions for better readability and maintenance
This commit is contained in:
2025-10-28 11:12:13 +08:00
parent 6be27def80
commit 1342799303
3 changed files with 467 additions and 119 deletions

View File

@ -236,11 +236,3 @@ impl Display for Clsid {
} }
// endregion // endregion
// region: Icon Resource
// endregion
// region: String Resource
// endregion

View File

@ -9,9 +9,383 @@ use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use std::sync::LazyLock; use std::sync::LazyLock;
use thiserror::Error as TeError; use thiserror::Error as TeError;
use uuid::Uuid;
use widestring::{WideCString, WideChar, WideStr}; use widestring::{WideCString, WideChar, WideStr};
use windows_sys::Win32::UI::WindowsAndMessaging::HICON; 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<Self, BadExtBodyError> {
// 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<Self, Self::Err> {
static RE: LazyLock<Regex> = 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<u32>,
}
impl ProgId {
/// Create a new ProgId with given parts.
pub fn new(
vendor: &str,
component: &str,
version: Option<u32>,
) -> Result<Self, BadProgIdPartError> {
// 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<u32> {
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<Self, Self::Err> {
static RE: LazyLock<Regex> =
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::<u32>()
.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, ParseClsidError> {
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<Self, Self::Err> {
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<WideChar>),
/// 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<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, ParseExpandStrError> {
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(), 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<Self, Self::Err> {
if Self::VAR_RE.is_match(s) {
Ok(Self {
inner: s.to_string(),
})
} else {
Err(ParseExpandStrError::new())
}
}
}
// endregion
// region: Windows Resource Reference String // region: Windows Resource Reference String
// region: Icon Reference String // region: Icon Reference String
@ -359,114 +733,6 @@ impl StrRc {
// endregion // 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<WideChar>),
/// 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<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, ParseExpandStrError> {
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(), 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<Self, Self::Err> {
if Self::VAR_RE.is_match(s) {
Ok(Self {
inner: s.to_string(),
})
} else {
Err(ParseExpandStrError::new())
}
}
}
// endregion
// region: Windows Commandline // region: Windows Commandline
// region: Cmd Lexer // region: Cmd Lexer

View File

@ -1,6 +1,90 @@
use std::path::Path; use std::{path::Path, str::FromStr};
use wfassoc::extra::windows::*; 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<u32>, 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<u32>) {
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<u32>) {
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] #[test]
fn test_icon_ref_str() { fn test_icon_ref_str() {
fn ok_tester(s: &str, probe: (&str, u32)) { fn ok_tester(s: &str, probe: (&str, u32)) {
@ -15,7 +99,10 @@ fn test_icon_ref_str() {
assert!(rv.is_err()); 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#"C:\Windows\Cursors\aero_arrow.cur"#);
err_tester(r#"@%SystemRoot%\System32\shell32.dll,-30596"#); err_tester(r#"@%SystemRoot%\System32\shell32.dll,-30596"#);
} }
@ -34,7 +121,10 @@ fn test_str_ref_str() {
assert!(rv.is_err()); 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#"This is my application, OK?"#);
err_tester(r#"%SystemRoot%\System32\imageres.dll,-72"#); err_tester(r#"%SystemRoot%\System32\imageres.dll,-72"#);
} }