1
0

Compare commits

..

3 Commits

Author SHA1 Message Date
dab91f1581 split into individual modules 2025-10-16 15:13:38 +08:00
4b1f85c2f3 update README 2025-10-15 13:22:53 +08:00
eee91d8498 write ext shit 2025-10-15 13:15:29 +08:00
8 changed files with 621 additions and 111 deletions

23
Cargo.lock generated
View File

@ -168,6 +168,12 @@ dependencies = [
"litrs", "litrs",
] ]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.14" version = "0.3.14"
@ -178,12 +184,28 @@ dependencies = [
"windows-sys 0.60.2", "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]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 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]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@ -495,6 +517,7 @@ dependencies = [
name = "wfassoc" name = "wfassoc"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"indexmap",
"regex", "regex",
"thiserror", "thiserror",
"uuid", "uuid",

View File

@ -6,6 +6,23 @@
## Introduction ## Introduction
* wfassoc: Core Rust library. Rust programmer can directly utilize it. * `wfassoc`: Core Rust library. Rust programmer can directly utilize it.
* wfassoc_dylib: A dynamic library exposed for C/C++ and other languages users. * `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_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.

25
example/ppic.toml Normal file
View File

@ -0,0 +1,25 @@
identifier = "PineapplePicture"
path = 'C:\path\to\ppic.exe'
clsid = "{B5291320-FE7C-4069-BF87-A0AC327FCD20}"
[manners]
common = '"C:\path\to\ppic.exe" "%1"'
[exts]
".jpg" = "common"
".jfif" = "common"
".gif" = "common"
".bmp" = "common"
".png" = "common"
".ico" = "common"
".jpeg" = "common"
".tif" = "common"
".tiff" = "common"
".webp" = "common"
".svg" = "common"
".kra" = "common"
".xcf" = "common"
".avif" = "common"
".qoi" = "common"
".apng" = "common"
".exr" = "common"

View File

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

283
wfassoc/src/assoc.rs Normal file
View File

@ -0,0 +1,283 @@
//! The module including all struct representing Windows file association concept,
//! like file extension, ProgId, CLSID and etc.
use regex::Regex;
use std::fmt::Display;
use std::str::FromStr;
use std::sync::LazyLock;
use thiserror::Error as TeError;
use uuid::Uuid;
// 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, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Ext {
/// The body of file extension (excluding dot).
body: String,
}
impl Ext {
/// Create an new file extension.
pub fn new(raw: &str) -> Result<Self, ParseExtError> {
Self::from_str(raw)
}
/// 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 {
inner: String,
}
impl ParseExtError {
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 {
body: v[1].to_string(),
}),
None => Err(ParseExtError::new(s)),
}
}
}
// 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
/// - https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
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 \"{inner}\" is invalid")]
pub struct ParseProgIdError {
inner: String,
}
impl ParseProgIdError {
fn new(s: &str) -> Self {
Self {
inner: s.to_string(),
}
}
}
/// 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(s))
}
}
}
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(s))?;
Ok(Self::new(vendor, component, version))
} else {
Err(ParseProgIdError::new(s))
}
}
}
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: CLSID
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: Icon Resource
// endregion
// region: String Resource
// endregion

View File

@ -4,36 +4,51 @@
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
compile_error!("Crate wfassoc is only supported on Windows."); compile_error!("Crate wfassoc is only supported on Windows.");
pub(crate) mod assoc;
pub(crate) mod utilities;
use indexmap::{IndexMap, IndexSet};
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fmt::Display;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use thiserror::Error as TeError; use thiserror::Error as TeError;
use winreg::RegKey; use winreg::RegKey;
use winreg::enums::{ use winreg::enums::{
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE, HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
}; };
use winreg::transaction::Transaction;
// region: Error Types // region: Error Types
/// All possible error occurs in this crate. /// All possible error occurs in this crate.
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
pub enum WfError { pub enum Error {
#[error("no administrative privilege")]
NoPrivilege,
#[error("error occurs when manipulating with Registry: {0}")] #[error("error occurs when manipulating with Registry: {0}")]
BadRegOper(#[from] std::io::Error), BadRegOper(#[from] std::io::Error),
#[error("{0}")]
CastOsStr(#[from] utilities::CastOsStrError),
#[error("no administrative privilege")]
NoPrivilege,
#[error("given full path to application is invalid")] #[error("given full path to application is invalid")]
BadFullAppPath, BadFullAppPath,
#[error("failed when casting path or OS string into string")] #[error("manner \"{0}\" is already registered")]
BadOsStrCast, DupManner(String),
#[error("file extension \"{0}\" is already registered")]
DupExt(String),
#[error("the token of associated manner for file extension is invalid")]
InvalidAssocManner,
} }
/// The result type used in this crate. /// The result type used in this crate.
pub type WfResult<T> = Result<T, WfError>; pub type Result<T> = std::result::Result<T, Error>;
// endregion // endregion
// region: Scope and View // 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. /// The scope where wfassoc will register and unregister application.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
@ -46,7 +61,7 @@ pub enum Scope {
/// The error occurs when cast View into Scope. /// The error occurs when cast View into Scope.
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
#[error("hybrid view can not be cast into any scope")] #[error("hybrid View can not be cast into Scope")]
pub struct TryFromViewError {} pub struct TryFromViewError {}
impl TryFromViewError { impl TryFromViewError {
@ -58,7 +73,7 @@ impl TryFromViewError {
impl TryFrom<View> for Scope { impl TryFrom<View> for Scope {
type Error = TryFromViewError; type Error = TryFromViewError;
fn try_from(value: View) -> Result<Self, Self::Error> { fn try_from(value: View) -> std::result::Result<Self, Self::Error> {
match value { match value {
View::User => Ok(Self::User), View::User => Ok(Self::User),
View::System => Ok(Self::System), View::System => Ok(Self::System),
@ -74,7 +89,7 @@ impl Scope {
// If we operate on System, and we do not has privilege, // If we operate on System, and we do not has privilege,
// we think we do not have privilege, otherwise, // we think we do not have privilege, otherwise,
// there is no privilege required. // there is no privilege required.
!matches!(self, Self::System if !has_privilege()) !matches!(self, Self::System if !utilities::has_privilege())
} }
} }
@ -101,113 +116,124 @@ impl From<Scope> for View {
// endregion // endregion
// region: Utilities // region: Manner
/// The println macro only works on Debug mode /// The struct representing a program manner.
/// for tracing the execution of some important functions. /// Manner usually mean the way to open files,
macro_rules! debug_println { /// or more preciously, the consititution of command arguments passed to program.
// For no argument. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
() => { pub struct Manner {
if cfg!(debug_assertions) { argv: String,
println!(); }
impl Manner {
pub fn new(argv: &str) -> Self {
Self {
argv: argv.to_string(),
} }
}; }
// For one or more arguments like println!.
($($arg:tt)*) => {
if cfg!(debug_assertions) {
println!($($arg)*);
}
};
} }
/// Check whether current process has administrative privilege. impl Display for Manner {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// It usually means that checking whether current process is running as Administrator. write!(f, "{}", self.argv)
/// Return true if it is, otherwise false.
///
/// Reference: https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
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
}
/// 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 // endregion
// region: Registrar // region: Program
/// The core registrar for register and unregister application. /// The struct representing a complete program for registration and unregistration.
pub struct Registrar { pub struct Program {
/// The identifier of this program.
identifier: String,
/// The fully qualified path to the application. /// The fully qualified path to the application.
full_path: PathBuf, full_path: PathBuf,
/// The collection holding all manners of this program.
manners: IndexSet<Manner>,
/// The collection holding all file extensions supported by this program.
/// The key is file estension and value is its associated manner for opening it.
exts: IndexMap<assoc::Ext, Token>,
} }
impl Registrar { impl Program {
/// Create a new registrar for following operations. /// Create a new registrar for following operations.
pub fn new(full_path: &Path) -> Self { ///
/// `identifier` is the unique name of this program.
/// If should only contain digits and alphabet chars,
/// and should not start with any digits.
/// For example, "MyApp" is okey but following names are not okey:
///
/// - `My App`
/// - `3DViewer`
/// - `我的Qt最时尚`
///
/// More preciously, `identifier` will be used as the vendor part of ProgId.
///
/// `full_path` is the fully qualified path to the application.
pub fn new(identifier: &str, full_path: &Path) -> Self {
// TODO: Add checker for identifier
Self { Self {
identifier: identifier.to_string(),
full_path: full_path.to_path_buf(), full_path: full_path.to_path_buf(),
manners: IndexSet::new(),
exts: IndexMap::new(),
} }
} }
/// Add manner provided by this program.
pub fn add_manner(&mut self, manner: Manner) -> Result<Token> {
// Backup a stringfied manner for error output.
let manner_str = manner.to_string();
// Insert manner.
let idx = self.manners.len();
if self.manners.insert(manner) {
Ok(idx)
} else {
Err(Error::DupExt(manner_str))
}
}
/// Get the reference to manner with given token.
pub fn get_manner(&self, token: Token) -> Option<&Manner> {
self.manners.get_index(token)
}
/// Add file extension supported by this program and its associated manner.
pub fn add_ext(&mut self, ext: assoc::Ext, token: Token) -> Result<Token> {
// Check manner token
if let None = self.get_manner(token) {
return Err(Error::InvalidAssocManner);
}
// Backup a stringfied extension for error output.
let ext_str = ext.to_string();
// Insert file extension
let idx = self.exts.len();
if let None = self.exts.insert(ext, token) {
Ok(idx)
} else {
Err(Error::DupExt(ext_str))
}
}
/// Get the reference to file extension with given token.
pub fn get_ext(&self, token: Token) -> Option<&assoc::Ext> {
self.exts.get_index(token).map(|p| p.0)
}
} }
impl Registrar { impl Program {
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths"; const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
const APPLICATIONS: &str = "Software\\Classes\\Applications"; const APPLICATIONS: &str = "Software\\Classes\\Applications";
/// Register this application. /// Register this application.
pub fn register(&self, scope: Scope) -> WfResult<()> { pub fn register(&self, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch root key. // Fetch root key.
let hk = RegKey::predef(match scope { let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER, Scope::User => HKEY_CURRENT_USER,
@ -222,21 +248,34 @@ impl Registrar {
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?; 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)?; let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
// Write App Paths values // Write App Paths values
subkey.set_value("", &path_to_str(&self.full_path)?)?; subkey.set_value("", &utilities::path_to_str(&self.full_path)?)?;
subkey.set_value("Path", &osstr_to_str(&start_in)?)?; subkey.set_value("Path", &utilities::osstr_to_str(&start_in)?)?;
// Create Applications subkey // Create Applications subkey
debug_println!("Adding Applications subkey..."); debug_println!("Adding Applications subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?; let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?; let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
// Write Applications values // Write Applications values
subkey.set_value("FriendlyAppName", &"WoW!")?; if !self.exts.is_empty() {
let (supported_types, _) =
subkey.create_subkey_with_flags("SupportedTypes", KEY_WRITE)?;
for ext in self.exts.keys() {
supported_types.set_value(ext.to_string(), &"")?;
}
}
// Okey
utilities::notify_assoc_changed();
Ok(()) Ok(())
} }
/// Unregister this application. /// Unregister this application.
pub fn unregister(&self, scope: Scope) -> WfResult<()> { pub fn unregister(&self, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch root key and file name. // Fetch root key and file name.
let hk = RegKey::predef(match scope { let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER, Scope::User => HKEY_CURRENT_USER,
@ -255,13 +294,14 @@ impl Registrar {
subkey_parent.delete_subkey_all(file_name)?; subkey_parent.delete_subkey_all(file_name)?;
// Okey // Okey
utilities::notify_assoc_changed();
Ok(()) Ok(())
} }
/// Check whether this application has been registered. /// Check whether this application has been registered.
/// ///
/// Please note that this is a rough check and do not validate any data. /// Please note that this is a rough check and do not validate any data.
pub fn is_registered(&self, scope: Scope) -> WfResult<bool> { pub fn is_registered(&self, scope: Scope) -> Result<bool> {
// Fetch root key and file name. // Fetch root key and file name.
let hk = RegKey::predef(match scope { let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER, Scope::User => HKEY_CURRENT_USER,
@ -288,26 +328,28 @@ impl Registrar {
} }
} }
impl Registrar { impl Program {
/// Extract the file name part from full path to application, /// Extract the file name part from full path to application,
/// which was used in Registry path component. /// which was used in Registry path component.
fn extract_file_name(&self) -> WfResult<&OsStr> { fn extract_file_name(&self) -> Result<&OsStr> {
// Get the file name part and make sure it is not empty // Get the file name part and make sure it is not empty.
// Empty checker is CRUCIAL!
self.full_path self.full_path
.file_name() .file_name()
.and_then(|p| if p.is_empty() { None } else { Some(p) }) .and_then(|p| if p.is_empty() { None } else { Some(p) })
.ok_or(WfError::BadFullAppPath) .ok_or(Error::BadFullAppPath)
} }
/// Extract the start in path from full path to application, /// Extract the start in path from full path to application,
/// which basically is the stem of full path. /// which basically is the stem of full path.
fn extract_start_in(&self) -> WfResult<&OsStr> { fn extract_start_in(&self) -> Result<&OsStr> {
// Get parent part and make sure it is not empty // Get parent part and make sure it is not empty
// Empty checker is CRUCIAL!
self.full_path self.full_path
.parent() .parent()
.map(|p| p.as_os_str()) .map(|p| p.as_os_str())
.and_then(|p| if p.is_empty() { None } else { Some(p) }) .and_then(|p| if p.is_empty() { None } else { Some(p) })
.ok_or(WfError::BadFullAppPath) .ok_or(Error::BadFullAppPath)
} }
} }

119
wfassoc/src/utilities.rs Normal file
View File

@ -0,0 +1,119 @@
//! The module containing useful stuff used in this crate.
use std::ffi::OsStr;
use std::path::Path;
use thiserror::Error as TeError;
/// The println macro only works on Debug mode
/// for tracing the execution of some important functions.
#[macro_export]
macro_rules! debug_println {
// For no argument.
() => {
if cfg!(debug_assertions) {
println!();
}
};
// For one or more arguments like println!.
($($arg:tt)*) => {
if cfg!(debug_assertions) {
println!($($arg)*);
}
};
}
// region: Windows Related
/// 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
}
/// Notify Windows that some file associations are changed, and should refresh them.
/// This function must be called once you change any file associations.
pub 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(),
)
}
}
// endregion
// region OS String Related
/// The error occurs when casting `OsStr` into `str`.
#[derive(Debug, TeError)]
#[error("failed when casting OS string into string")]
pub struct CastOsStrError {}
impl CastOsStrError {
fn new() -> Self {
Self {}
}
}
/// Try casting given &Path into &str.
pub fn path_to_str(path: &Path) -> Result<&str, CastOsStrError> {
path.to_str().ok_or(CastOsStrError::new())
}
/// Try casting given &OsStr into &str.
pub fn osstr_to_str(osstr: &OsStr) -> Result<&str, CastOsStrError> {
osstr.to_str().ok_or(CastOsStrError::new())
}
// endregion

View File

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