1
0

Compare commits

...

6 Commits

Author SHA1 Message Date
4b1f85c2f3 update README 2025-10-15 13:22:53 +08:00
eee91d8498 write ext shit 2025-10-15 13:15:29 +08:00
c4b825f7f6 refactor wfassoc 2025-10-13 22:07:42 +08:00
034e9017be write shit 2025-10-13 15:50:48 +08:00
07a8c6a11d write shit 2025-10-10 20:54:44 +08:00
f7d92243c9 add decl for ProgId 2025-10-10 14:23:01 +08:00
7 changed files with 974 additions and 218 deletions

27
Cargo.lock generated
View File

@ -168,6 +168,12 @@ dependencies = [
"litrs",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
@ -178,12 +184,28 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -495,6 +517,7 @@ dependencies = [
name = "wfassoc"
version = "0.1.0"
dependencies = [
"indexmap",
"regex",
"thiserror",
"uuid",
@ -544,9 +567,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-link"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"

View File

@ -6,6 +6,23 @@
## Introduction
* wfassoc: Core Rust library. Rust programmer can directly utilize it.
* wfassoc_dylib: A dynamic library exposed for C/C++ and other languages users.
* wfassoc_exec: A executable configuring file assocation according to user given profile and request.
* `wfassoc`: Core Rust library. Rust programmer can directly utilize it.
* `wfassoc_dylib`: A dynamic library exposed for C/C++ and other languages users.
* `wfassoc_exec`: A executable configuring file assocation according to user given profile and request.
If you are a programmer who just want to silently set your file association for user,
`wfassoc_exec` would be your best choice.
The only things you need to do is that write a TOML description file,
and provide it and `wfassoc_exec` with your executable.
By executing `wfassoc_exec` with this TOML file in your executable,
you can simply achieve this goal and analyze its return value to check whether it success.
However, if you are prefering taht let user decide which file associations should be created,
even have an UI displaying all current file association related with this program (like 7-Zip File Manager does),
you can choose `wfassoc_dylib` for your solution.
`wfassoc_dylib` expose all essential functions for this task.
And you can build it with your own program.
At last, if all scenarios above can not cover your requirements,
you can utilize `wfassoc` directly in Rust, change all essential code in your favor,
and finally produce the application which onlt suit for yourself.

View File

@ -8,7 +8,8 @@ license = "SPDX:MIT"
[dependencies]
thiserror = { workspace = true }
windows-sys = { version = "0.60.2", features = ["Win32_Security", "Win32_System_SystemServices"]}
winreg = "0.55.0"
windows-sys = { version = "0.60.2", features = ["Win32_Security", "Win32_System_SystemServices", "Win32_UI_Shell"] }
winreg = { version = "0.55.0", features = ["transactions"] }
indexmap = "2.11.4"
regex = "1.11.3"
uuid = "1.18.1"

View File

@ -5,50 +5,59 @@
compile_error!("Crate wfassoc is only supported on Windows.");
use regex::Regex;
use std::ffi::OsStr;
use std::fmt::Display;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::LazyLock;
use thiserror::Error as TeError;
use indexmap::{IndexMap, IndexSet};
use winreg::RegKey;
use winreg::enums::{
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
};
// region: Error Types
/// All possible error occurs in this crate.
#[derive(Debug, TeError)]
pub enum Error {
#[error(
"can not register because lack essential privilege. please consider running with Administrator role"
)]
pub enum WfError {
#[error("no administrative privilege")]
NoPrivilege,
#[error("{0}")]
BadFileExt(#[from] ParseFileExtError),
#[error("{0}")]
BadExecRc(#[from] ParseExecRcError),
#[error("error occurs when manipulating with Registry: {0}")]
BadRegOper(#[from] std::io::Error),
#[error("given full path to application is invalid")]
BadFullAppPath,
#[error("failed when casting OS string into string")]
BadOsStrCast,
#[error("file extension {0} is already registered")]
DupExt(String),
}
/// The result type used in this crate.
pub type WfResult<T> = Result<T, WfError>;
// endregion
// region: Basic Types
// region: Utilities
/// The scope where wfassoc will register and unregister.
#[derive(Debug, Copy, Clone)]
pub enum Scope {
/// Scope for current user.
User,
/// Scope for all users under this computer.
System,
/// The println macro only works on Debug mode
/// for tracing the execution of some important functions.
macro_rules! debug_println {
// For no argument.
() => {
if cfg!(debug_assertions) {
println!();
}
/// The view when wfassoc querying infomations.
#[derive(Debug, Copy, Clone)]
pub enum View {
/// The view of current user.
User,
/// The view of system.
System,
/// Hybrid view of User and System.
/// It can be seen as that we use System first and then use User to override any existing items.
Hybrid,
};
// For one or more arguments like println!.
($($arg:tt)*) => {
if cfg!(debug_assertions) {
println!($($arg)*);
}
};
}
/// Check whether current process has administrative privilege.
@ -57,7 +66,7 @@ pub enum View {
/// Return true if it is, otherwise false.
///
/// Reference: https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
pub fn has_privilege() -> bool {
fn has_privilege() -> bool {
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Security::{
AllocateAndInitializeSid, CheckTokenMembership, FreeSid, PSID, SECURITY_NT_AUTHORITY,
@ -104,220 +113,346 @@ pub fn has_privilege() -> bool {
is_member != 0
}
/// Notify Windows that some file associations are changed, and should refresh them.
/// This function must be called once you change any file associations.
fn notify_assoc_changed() -> () {
use windows_sys::Win32::UI::Shell::{SHCNE_ASSOCCHANGED, SHCNF_IDLIST, SHChangeNotify};
unsafe {
SHChangeNotify(
SHCNE_ASSOCCHANGED as i32,
SHCNF_IDLIST,
Default::default(),
Default::default(),
)
}
}
/// Try casting given &Path into &str.
fn path_to_str(path: &Path) -> WfResult<&str> {
path.to_str().ok_or(WfError::BadOsStrCast)
}
/// Try casting given &OsStr into &str.
fn osstr_to_str(osstr: &OsStr) -> WfResult<&str> {
osstr.to_str().ok_or(WfError::BadOsStrCast)
}
// endregion
// region: Types
/// The token for access registered items in Program.
/// This is usually returned when you registering them.
pub type Token = usize;
/// The scope where wfassoc will register and unregister application.
#[derive(Debug, Copy, Clone)]
pub enum Scope {
/// Scope for current user.
User,
/// Scope for all users under this computer.
System,
}
/// The error occurs when cast View into Scope.
#[derive(Debug, TeError)]
#[error("hybrid View can not be cast into Scope")]
pub struct TryFromViewError {}
impl TryFromViewError {
fn new() -> Self {
Self {}
}
}
impl TryFrom<View> for Scope {
type Error = TryFromViewError;
fn try_from(value: View) -> Result<Self, Self::Error> {
match value {
View::User => Ok(Self::User),
View::System => Ok(Self::System),
View::Hybrid => Err(TryFromViewError::new()),
}
}
}
impl Scope {
/// Check whether we have enough privilege when operating in current scope.
/// If we have, return true, otherwise false.
fn has_privilege(&self) -> bool {
// If we operate on System, and we do not has privilege,
// we think we do not have privilege, otherwise,
// there is no privilege required.
!matches!(self, Self::System if !has_privilege())
}
}
/// The view when wfassoc querying file extension association.
#[derive(Debug, Copy, Clone)]
pub enum View {
/// The view of current user.
User,
/// The view of system.
System,
/// Hybrid view of User and System.
/// It can be seen as that we use System first and then use User to override any existing items.
Hybrid,
}
impl From<Scope> for View {
fn from(value: Scope) -> Self {
match value {
Scope::User => Self::User,
Scope::System => Self::System,
}
}
}
// endregion
// region: File Extension
/// The struct representing an file extension which must start with dot (`.`)
/// and followed by at least one arbitrary characters.
#[derive(Debug, Clone)]
pub struct FileExt {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Ext {
/// The body of file extension (excluding dot).
inner: String,
body: String,
}
impl FileExt {
pub fn new(file_ext: &str) -> Result<Self, ParseFileExtError> {
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\.([^\.]+)$").unwrap());
match RE.captures(file_ext) {
Some(v) => Ok(Self {
inner: v[1].to_string(),
}),
None => Err(ParseFileExtError::new()),
}
impl Ext {
/// Create an new file extension.
pub fn new(raw: &str) -> Result<Self, ParseExtError> {
Self::from_str(raw)
}
pub fn query(&self, view: View) -> Option<FileExtAssoc> {
FileExtAssoc::new(self, view)
/// 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 is illegal")]
pub struct ParseFileExtError {}
#[error("given file extension name \"{inner}\" is invalid")]
pub struct ParseExtError {
inner: String
}
impl ParseFileExtError {
fn new() -> Self {
Self {}
impl ParseExtError {
fn new(inner: &str) -> Self {
Self { inner: inner.to_string() }
}
}
impl Display for FileExt {
impl Display for Ext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, ".{}", self.inner)
write!(f, ".{}", self.body)
}
}
impl FromStr for FileExt {
type Err = ParseFileExtError;
impl FromStr for Ext {
type Err = ParseExtError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\.([^\.]+)$").unwrap());
match RE.captures(s) {
Some(v) => Ok(Self {
body: v[1].to_string(),
}),
None => Err(ParseExtError::new(s)),
}
}
/// The association infomations of specific file extension.
#[derive(Debug)]
pub struct FileExtAssoc {
default: String,
open_with_progids: Vec<String>,
}
impl FileExtAssoc {
fn new(file_ext: &FileExt, view: View) -> Option<Self> {
use winreg::RegKey;
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ};
// navigate to extension container
let hk = match view {
View::User => RegKey::predef(HKEY_CURRENT_USER),
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let classes = match view {
View::User | View::System => hk
.open_subkey_with_flags("Software\\Classes", KEY_READ)
.unwrap(),
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ).unwrap(),
};
// open extension key if possible
let thisext = match classes.open_subkey_with_flags(file_ext.to_string(), KEY_READ) {
Ok(v) => v,
Err(_) => return None,
};
// fetch extension infos.
let default = thisext.get_value("").unwrap_or(String::new());
let open_with_progids =
if let Ok(progids) = thisext.open_subkey_with_flags("OpenWithProdIds", KEY_READ) {
progids.enum_keys().map(|x| x.unwrap()).filter(|k| !k.is_empty()).collect()
} else {
Vec::new()
};
Some(Self {
default,
open_with_progids,
})
}
pub fn get_default(&self) -> &str {
&self.default
}
pub fn len_open_with_progid(&self) -> usize {
self.open_with_progids.len()
}
pub fn iter_open_with_progids(&self) -> impl Iterator<Item = &str> {
self.open_with_progids.iter().map(|s| s.as_str())
}
}
// endregion
// region: Executable Resource
/// The struct representing an Windows executable resources path like
/// `path_to_file.exe,1`.
pub struct ExecRc {
/// The path to binary for finding resources.
binary: PathBuf,
/// The inner index of resources.
index: u32,
}
impl ExecRc {
pub fn new(res_str: &str) -> Result<Self, ParseExecRcError> {
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([^,]+),([0-9]+)$").unwrap());
let caps = RE.captures(res_str);
if let Some(caps) = caps {
let binary = PathBuf::from_str(&caps[1])?;
let index = caps[2].parse::<u32>()?;
Ok(Self { binary, index })
} else {
Err(ParseExecRcError::NoCapture)
}
}
}
/// The error occurs when try parsing string into ExecRc.
#[derive(Debug, TeError)]
#[error("given string is not a valid executable resource string")]
pub enum ParseExecRcError {
/// Given string is not matched with format.
NoCapture,
/// Fail to convert executable part into path.
BadBinaryPath(#[from] std::convert::Infallible),
/// Fail to convert index part into valid number.
BadIndex(#[from] std::num::ParseIntError),
}
impl FromStr for ExecRc {
type Err = ParseExecRcError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
ExecRc::new(s)
}
}
impl Display for ExecRc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{},{}", self.binary.to_str().unwrap(), self.index)
}
}
// endregion
// /// The struct representing an Windows acceptable Prgram ID,
// /// which looks like `Program.Document.2`
// pub struct ProgId {
// inner: String,
// }
// impl ProgId {
// pub fn new(prog_id: &str) -> Self {
// Self {
// inner: prog_id.to_string(),
// }
// }
// }
// region: Program
// /// The struct representing a complete Win32 program.
// pub struct Program {
// file_exts: Vec<FileExt>,
// }
/// The struct representing a complete program for registration and unregistration.
pub struct Program {
/// The fully qualified path to the application.
full_path: PathBuf,
/// Optional default icon resource for overriding.
///
/// TODO: Use specialized IconRc struct instead.
default_icon: Option<String>,
/// Optional friendly app name for overriding.
///
/// TODO: Use specialized StringRc for overriding.
friendly_app_name: Option<String>,
// impl Program {
// /// Create a program descriptor.
// pub fn new() -> Self {
// Self {
// file_exts: Vec::new(),
// }
// }
// }
/// The collection holding all file extensions supported by this program.
exts: IndexSet<Ext>,
}
// impl Program {
// /// Register program in this computer
// pub fn register(&self, kind: RegisterKind) -> Result<(), Error> {
// todo!("pretend to register >_<...")
// }
impl Program {
/// Create a new registrar for following operations.
///
/// `full_path` is the fully qualified path to the application.
///
/// `default_icon` is an optional icon resource replacing the default one
/// fetched from the first icon resource of your executable application.
///
/// `friendly_app_name` also is an optional string or string resource replacing
/// the info fetched from executable application's version information.
pub fn new(
full_path: &Path,
default_icon: Option<&str>,
friendly_app_name: Option<&str>,
) -> Self {
Self {
full_path: full_path.to_path_buf(),
default_icon: default_icon.map(|s| s.to_string()),
friendly_app_name: friendly_app_name.map(|s| s.to_string()),
exts: IndexSet::new(),
}
}
// /// Unregister program from this computer.
// pub fn unregister(&self) -> Result<(), Error> {
// todo!("pretend to unregister >_<...")
// }
// }
/// Add file extension supported by this program.
pub fn add_ext(&mut self, ext: &Ext) -> WfResult<()> {
if self.exts.insert(ext.clone()) {
Ok(())
} else {
Err(WfError::DupExt(ext.to_string()))
}
}
}
// impl Program {
// /// Query file extension infos which this program want to associate with.
// pub fn query(&self) -> Result<(), Error> {
// todo!("pretend to query >_<...")
// }
// }
impl Program {
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
const APPLICATIONS: &str = "Software\\Classes\\Applications";
/// Register this application.
pub fn register(&self, scope: Scope) -> WfResult<()> {
// Check privilege
if !scope.has_privilege() {
return Err(WfError::NoPrivilege);
}
// Fetch root key.
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
// Fetch file name and start in path.
let file_name = self.extract_file_name()?;
let start_in = self.extract_start_in()?;
// Create App Paths subkey
debug_println!("Adding App Paths subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
// Write App Paths values
subkey.set_value("", &path_to_str(&self.full_path)?)?;
subkey.set_value("Path", &osstr_to_str(&start_in)?)?;
// Create Applications subkey
debug_println!("Adding Applications subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
// Write Applications values
if let Some(default_icon) = &self.default_icon {
subkey.set_value("DefaultIcon", default_icon)?;
}
if let Some(friendly_app_name) = &self.friendly_app_name {
subkey.set_value("FriendlyAppName", friendly_app_name)?;
}
if !self.exts.is_empty() {
let (supported_types, _) = subkey.create_subkey_with_flags("SupportedTypes", KEY_WRITE)?;
for ext in &self.exts {
supported_types.set_value(ext.to_string(), &"")?;
}
}
// Okey
notify_assoc_changed();
Ok(())
}
/// Unregister this application.
pub fn unregister(&self, scope: Scope) -> WfResult<()> {
// Check privilege
if !scope.has_privilege() {
return Err(WfError::NoPrivilege);
}
// Fetch root key and file name.
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let file_name = self.extract_file_name()?;
// Remove App Paths subkey
debug_println!("Removing App Paths subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_WRITE)?;
subkey_parent.delete_subkey_all(file_name)?;
// Remove Applications subkey
debug_println!("Removing Applications subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
subkey_parent.delete_subkey_all(file_name)?;
// Okey
notify_assoc_changed();
Ok(())
}
/// Check whether this application has been registered.
///
/// Please note that this is a rough check and do not validate any data.
pub fn is_registered(&self, scope: Scope) -> WfResult<bool> {
// Fetch root key and file name.
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let file_name = self.extract_file_name()?;
// Check App Paths subkey.
debug_println!("Checking App Paths subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
if let Err(_) = subkey_parent.open_subkey_with_flags(file_name, KEY_READ) {
return Ok(false);
}
// Check Application subkey.
debug_println!("Checking Applications subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
if let Err(_) = subkey_parent.open_subkey_with_flags(file_name, KEY_READ) {
return Ok(false);
}
// Both subkeys are roughly existing.
Ok(true)
}
}
impl Program {
/// Extract the file name part from full path to application,
/// which was used in Registry path component.
fn extract_file_name(&self) -> WfResult<&OsStr> {
// Get the file name part and make sure it is not empty.
// Empty checker is CRUCIAL!
self.full_path
.file_name()
.and_then(|p| if p.is_empty() { None } else { Some(p) })
.ok_or(WfError::BadFullAppPath)
}
/// Extract the start in path from full path to application,
/// which basically is the stem of full path.
fn extract_start_in(&self) -> WfResult<&OsStr> {
// Get parent part and make sure it is not empty
// Empty checker is CRUCIAL!
self.full_path
.parent()
.map(|p| p.as_os_str())
.and_then(|p| if p.is_empty() { None } else { Some(p) })
.ok_or(WfError::BadFullAppPath)
}
}
// endregion

View File

@ -1,2 +0,0 @@
use super::error::{Error, Result};
use super::components::*;

582
wfassoc/src/shit.rs Normal file
View File

@ -0,0 +1,582 @@
/// The expand of winreg crate according to our module requirements.
mod winregex;
use regex::Regex;
use std::fmt::Display;
use std::str::FromStr;
use std::sync::LazyLock;
use thiserror::Error as TeError;
use winreg::RegKey;
// region: Error Types
/// All possible error occurs in this crate.
#[derive(Debug, TeError)]
pub enum Error {
#[error(
"can not register because lack essential privilege. please consider running with Administrator role"
)]
NoPrivilege,
#[error("{0}")]
Register(#[from] std::io::Error),
#[error("{0}")]
BadFileExt(#[from] ParseFileExtError),
#[error("{0}")]
BadProgId(#[from] ParseProgIdError),
}
// endregion
// region: Privilege, Scope and View
/// Check whether current process has administrative privilege.
///
/// It usually means that checking whether current process is running as Administrator.
/// Return true if it is, otherwise false.
///
/// Reference: https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
pub fn has_privilege() -> bool {
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Security::{
AllocateAndInitializeSid, CheckTokenMembership, FreeSid, PSID, SECURITY_NT_AUTHORITY,
};
use windows_sys::Win32::System::SystemServices::{
DOMAIN_ALIAS_RID_ADMINS, SECURITY_BUILTIN_DOMAIN_RID,
};
use windows_sys::core::BOOL;
let nt_authority = SECURITY_NT_AUTHORITY.clone();
let mut administrators_group: PSID = PSID::default();
let success: BOOL = unsafe {
AllocateAndInitializeSid(
&nt_authority,
2,
SECURITY_BUILTIN_DOMAIN_RID as u32,
DOMAIN_ALIAS_RID_ADMINS as u32,
0,
0,
0,
0,
0,
0,
&mut administrators_group,
)
};
if success == 0 {
panic!("Win32 AllocateAndInitializeSid() failed");
}
let mut is_member: BOOL = BOOL::default();
let success: BOOL =
unsafe { CheckTokenMembership(HANDLE::default(), administrators_group, &mut is_member) };
unsafe {
FreeSid(administrators_group);
}
if success == 0 {
panic!("Win32 CheckTokenMembership() failed");
}
is_member != 0
}
/// The scope where wfassoc will register and unregister.
#[derive(Debug, Copy, Clone)]
pub enum Scope {
/// Scope for current user.
User,
/// Scope for all users under this computer.
System,
}
/// The view when wfassoc querying infomations.
#[derive(Debug, Copy, Clone)]
pub enum View {
/// The view of current user.
User,
/// The view of system.
System,
/// Hybrid view of User and System.
/// It can be seen as that we use System first and then use User to override any existing items.
Hybrid,
}
/// The error occurs when cast View into Scope.
#[derive(Debug, TeError)]
#[error("hybrid view can not be cast into any scope")]
pub struct TryFromViewError {}
impl TryFromViewError {
fn new() -> Self {
Self {}
}
}
impl From<Scope> for View {
fn from(value: Scope) -> Self {
match value {
Scope::User => Self::User,
Scope::System => Self::System,
}
}
}
impl TryFrom<View> for Scope {
type Error = TryFromViewError;
fn try_from(value: View) -> Result<Self, Self::Error> {
match value {
View::User => Ok(Self::User),
View::System => Ok(Self::System),
View::Hybrid => Err(TryFromViewError::new()),
}
}
}
impl Scope {
/// Check whether we have enough privilege when operating in current scope.
/// If we have, simply return, otherwise return error.
fn check_privilege(&self) -> Result<(), Error> {
if matches!(self, Self::System if !has_privilege()) {
Err(Error::NoPrivilege)
} else {
Ok(())
}
}
}
// endregion
// region: File Extension
/// The struct representing an file extension which must start with dot (`.`)
/// and followed by at least one arbitrary characters.
#[derive(Debug, Clone)]
pub struct FileExt {
/// The body of file extension (excluding dot).
inner: String,
}
impl FileExt {
pub fn new(file_ext: &str) -> Result<Self, ParseFileExtError> {
Self::from_str(file_ext)
}
}
/// The error occurs when try parsing string into FileExt.
#[derive(Debug, TeError)]
#[error("given file extension is invalid")]
pub struct ParseFileExtError {}
impl ParseFileExtError {
fn new() -> Self {
Self {}
}
}
impl Display for FileExt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, ".{}", self.inner)
}
}
impl FromStr for FileExt {
type Err = ParseFileExtError;
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 {
inner: v[1].to_string(),
}),
None => Err(ParseFileExtError::new()),
}
}
}
impl FileExt {
fn open_scope(&self, scope: Scope) -> Result<RegKey, Error> {
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE};
// check privilege
scope.check_privilege()?;
// get the root key
let hk = match scope {
Scope::User => RegKey::predef(HKEY_CURRENT_USER),
Scope::System => RegKey::predef(HKEY_LOCAL_MACHINE),
};
// navigate to classes
let classes = hk.open_subkey_with_flags("Software\\Classes", KEY_READ | KEY_WRITE)?;
// okey
Ok(classes)
}
fn open_view(&self, view: View) -> Result<Option<RegKey>, Error> {
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ};
// navigate to extension container
let hk = match view {
View::User => RegKey::predef(HKEY_CURRENT_USER),
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let classes = match view {
View::User | View::System => {
hk.open_subkey_with_flags("Software\\Classes", KEY_READ)?
}
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?,
};
// check whether there is this ext
classes.
// open extension key if possible
let thisext = classes.open_subkey_with_flags(file_ext.to_string(), KEY_READ)?;
// okey
Ok(classes)
}
pub fn get_current(&self, view: View) -> Option<ProgId> {
todo!()
}
pub fn set_current(&mut self, scope: Scope, prog_id: Option<&ProgId>) -> Result<(), Error> {
scope.check_privilege()?;
todo!()
}
pub fn iter_open_with(&self, view: View) -> Result<impl Iterator<Item = ProgId>, Error> {
let viewer = match self.open_view(view)? {
Some(viewer) => viewer,
None => return Ok(std::iter::empty::<ProgId>()),
};
let it = winregex::iter_sz_keys(&viewer);
let it = winregex::exclude_default_key(it);
Ok(it.map(|s| ProgId::from(s.as_str())))
}
pub fn insert_open_with(&mut self, scope: Scope, prog_id: &ProgId) -> Result<(), Error> {
scope.check_privilege()?;
todo!()
}
pub fn flash_open_with(
&mut self,
scope: Scope,
prog_ids: impl Iterator<Item = ProgId>,
) -> Result<(), Error> {
scope.check_privilege()?;
todo!()
}
}
/// The association infomations of specific file extension.
#[derive(Debug)]
pub struct FileExtAssoc {
default: String,
open_with_progids: Vec<String>,
}
impl FileExtAssoc {
fn new(file_ext: &FileExt, view: View) -> Option<Self> {
use winreg::RegKey;
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ};
// navigate to extension container
let hk = match view {
View::User => RegKey::predef(HKEY_CURRENT_USER),
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let classes = match view {
View::User | View::System => hk
.open_subkey_with_flags("Software\\Classes", KEY_READ)
.unwrap(),
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ).unwrap(),
};
// open extension key if possible
let thisext = match classes.open_subkey_with_flags(file_ext.to_string(), KEY_READ) {
Ok(v) => v,
Err(_) => return None,
};
// fetch extension infos.
let default = thisext.get_value("").unwrap_or(String::new());
let open_with_progids =
if let Ok(progids) = thisext.open_subkey_with_flags("OpenWithProdIds", KEY_READ) {
progids
.enum_keys()
.map(|x| x.unwrap())
.filter(|k| !k.is_empty())
.collect()
} else {
Vec::new()
};
Some(Self {
default,
open_with_progids,
})
}
pub fn get_default(&self) -> &str {
&self.default
}
pub fn len_open_with_progid(&self) -> usize {
self.open_with_progids.len()
}
pub fn iter_open_with_progids(&self) -> impl Iterator<Item = &str> {
self.open_with_progids.iter().map(|s| s.as_str())
}
}
// endregion
// region: Executable Resource
// /// The struct representing an Windows executable resources path like
// /// `path_to_file.exe,1`.
// pub struct ExecRc {
// /// The path to binary for finding resources.
// binary: PathBuf,
// /// The inner index of resources.
// index: u32,
// }
// impl ExecRc {
// pub fn new(res_str: &str) -> Result<Self, ParseExecRcError> {
// static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([^,]+),([0-9]+)$").unwrap());
// let caps = RE.captures(res_str);
// if let Some(caps) = caps {
// let binary = PathBuf::from_str(&caps[1])?;
// let index = caps[2].parse::<u32>()?;
// Ok(Self { binary, index })
// } else {
// Err(ParseExecRcError::NoCapture)
// }
// }
// }
// /// The error occurs when try parsing string into ExecRc.
// #[derive(Debug, TeError)]
// #[error("given string is not a valid executable resource string")]
// pub enum ParseExecRcError {
// /// Given string is not matched with format.
// NoCapture,
// /// Fail to convert executable part into path.
// BadBinaryPath(#[from] std::convert::Infallible),
// /// Fail to convert index part into valid number.
// BadIndex(#[from] std::num::ParseIntError),
// }
// impl FromStr for ExecRc {
// type Err = ParseExecRcError;
// fn from_str(s: &str) -> Result<Self, Self::Err> {
// ExecRc::new(s)
// }
// }
// impl Display for ExecRc {
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// write!(f, "{},{}", self.binary.to_str().unwrap(), self.index)
// }
// }
// endregion
// region: Programmatic Identifiers (ProgId)
/// The struct representing Programmatic Identifiers (ProgId).
///
/// Because there is optional part in standard ProgId, and not all software developers
/// are willing to following Microsoft suggestions, there is no strict constaint for ProgId.
/// So this struct is actually an enum which holding any possible ProgId format.
///
/// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids
pub enum ProgId {
Plain(String),
Loose(LosseProgId),
Strict(StrictProgId),
}
impl From<&str> for ProgId {
fn from(s: &str) -> Self {
// match it for strict ProgId first
if let Ok(v) = StrictProgId::from_str(s) {
return Self::Strict(v);
}
// then match for loose ProgId
if let Ok(v) = LosseProgId::from_str(s) {
return Self::Loose(v);
}
// fallback with plain
Self::Plain(s.to_string())
}
}
impl Display for ProgId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProgId::Plain(v) => v.fmt(f),
ProgId::Loose(v) => v.fmt(f),
ProgId::Strict(v) => v.fmt(f),
}
}
}
/// The error occurs when parsing ProgId.
#[derive(Debug, TeError)]
#[error("given ProgId string is invalid")]
pub struct ParseProgIdError {}
impl ParseProgIdError {
fn new() -> Self {
Self {}
}
}
/// The ProgId similar with strict ProgId, but no version part.
pub struct LosseProgId {
vendor: String,
component: String,
}
impl LosseProgId {
pub fn new(vendor: &str, component: &str) -> Self {
Self {
vendor: vendor.to_string(),
component: component.to_string(),
}
}
pub fn get_vendor(&self) -> &str {
&self.vendor
}
pub fn get_component(&self) -> &str {
&self.component
}
}
impl FromStr for LosseProgId {
type Err = ParseProgIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)$").unwrap());
let caps = RE.captures(s);
if let Some(caps) = caps {
let vendor = &caps[1];
let component = &caps[2];
Ok(Self::new(vendor, component))
} else {
Err(ParseProgIdError::new())
}
}
}
impl Display for LosseProgId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.vendor, self.component)
}
}
/// The ProgId exactly follows `[Vendor or Application].[Component].[Version]` format.
pub struct StrictProgId {
vendor: String,
component: String,
version: u32,
}
impl StrictProgId {
pub fn new(vendor: &str, component: &str, version: u32) -> Self {
Self {
vendor: vendor.to_string(),
component: component.to_string(),
version,
}
}
pub fn get_vendor(&self) -> &str {
&self.vendor
}
pub fn get_component(&self) -> &str {
&self.component
}
pub fn get_version(&self) -> u32 {
self.version
}
}
impl FromStr for StrictProgId {
type Err = ParseProgIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)\.([0-9]+)$").unwrap());
let caps = RE.captures(s);
if let Some(caps) = caps {
let vendor = &caps[1];
let component = &caps[2];
let version = caps[3]
.parse::<u32>()
.map_err(|_| ParseProgIdError::new())?;
Ok(Self::new(vendor, component, version))
} else {
Err(ParseProgIdError::new())
}
}
}
impl Display for StrictProgId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.vendor, self.component, self.version)
}
}
// endregion
// region: Program
// /// The struct representing a complete Win32 program.
// pub struct Program {
// file_exts: Vec<FileExt>,
// }
// impl Program {
// /// Create a program descriptor.
// pub fn new() -> Self {
// Self {
// file_exts: Vec::new(),
// }
// }
// }
// impl Program {
// /// Register program in this computer
// pub fn register(&self, kind: RegisterKind) -> Result<(), Error> {
// todo!("pretend to register >_<...")
// }
// /// Unregister program from this computer.
// pub fn unregister(&self) -> Result<(), Error> {
// todo!("pretend to unregister >_<...")
// }
// }
// impl Program {
// /// Query file extension infos which this program want to associate with.
// pub fn query(&self) -> Result<(), Error> {
// todo!("pretend to query >_<...")
// }
// }
// endregion

View File

@ -2,7 +2,7 @@ use clap::{Parser, Subcommand};
use comfy_table::Table;
use std::process;
use thiserror::Error as TeError;
use wfassoc::{Error as WfError, FileExt, Scope, View};
use wfassoc::{Error as WfError, Ext, Scope, View};
// region: Basic Types
@ -95,7 +95,7 @@ fn run_query(cli: Cli) -> Result<()> {
".kra", ".xcf", ".avif", ".qoi", ".apng", ".exr",
];
for ext in exts.iter().map(|e| FileExt::new(e).unwrap()) {
for ext in exts.iter().map(|e| Ext::new(e).unwrap()) {
if let Some(ext_assoc) = ext.query(View::Hybrid) {
println!("{:?}", ext_assoc)
}