1
0

refactor: commit code which idk when i write them

This commit is contained in:
2026-04-03 15:02:16 +08:00
parent 796b2efb1c
commit 7d8757c0ec
10 changed files with 659 additions and 557 deletions

View File

@@ -1,7 +0,0 @@
//! The extension for some existing crates.
//! Some imported crates are not enough for my project,
//! so I need create something to enrich them.
pub mod winreg;
pub mod windows;

View File

@@ -4,498 +4,96 @@
#[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 mod extra;
pub mod utilities; pub mod utilities;
pub mod assoc; pub mod winconcept;
pub mod win32ext;
pub mod winregext;
use assoc::{Ext, ProgId}; use std::collections::HashMap;
use indexmap::{IndexMap, IndexSet};
use regex::Regex;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::sync::LazyLock;
use thiserror::Error as TeError; use thiserror::Error as TeError;
use winreg::RegKey;
use winreg::enums::{
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
};
// region: Error Types /// Error occurs in this module.
/// All possible error occurs in this crate.
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
pub enum Error { pub enum Error {}
#[error("error occurs when manipulating with Registry: {0}")]
BadRegOper(#[from] std::io::Error),
#[error("{0}")]
CastOsStr(#[from] utilities::CastOsStrError),
#[error("{0}")]
ParseExt(#[from] assoc::ParseExtError),
#[error("no administrative privilege")] /// Result type used in this module.
NoPrivilege, type Result<T> = std::result::Result<T, Error>;
#[error("given identifier \"{0}\" of application is invalid")]
BadIdentifier(String),
#[error("given full path to application is invalid")]
BadFullAppPath,
#[error("manner \"{0}\" is already registered")]
DupManner(String),
#[error("file extension \"{0}\" is already registered")]
DupExt(String),
#[error("the token of manner is invalid")]
InvalidMannerToken,
#[error("the token of file extension is invalid")]
InvalidExtToken,
}
/// The result type used in this crate. /// Schema is the sketchpad of complete Program.
pub type Result<T> = std::result::Result<T, Error>; ///
/// We will create a Schema first, fill some properties, add file extensions,
// endregion /// then convert it into immutable Program for following using.
// 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) -> std::result::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.
pub 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 !utilities::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: Program
/// The struct representing a complete program for registration and unregistration.
#[derive(Debug)] #[derive(Debug)]
pub struct Program { pub struct Schema {
/// The identifier of this program.
identifier: String, identifier: String,
/// The fully qualified path to the application. path: String,
full_path: PathBuf, clsid: String,
/// The collection holding all manners of this program. icons: HashMap<String, String>,
manners: IndexSet<String>, behaviors: HashMap<String, String>,
/// The collection holding all file extensions supported by this program. exts: HashMap<String, SchemaExt>,
/// The key is file estension and value is its associated manner for opening it.
exts: IndexMap<Ext, Token>,
} }
impl Program { /// Internal used struct as the Schema file extensions hashmap value type.
/// Create a new registrar for following operations. #[derive(Debug)]
/// struct SchemaExt {
/// `identifier` is the unique name of this program. name: String,
/// If should only contain digits and alphabet chars, icon: String,
/// and should not start with any digits. behavior: String,
/// For example, "MyApp" is okey but following names are not okey: }
///
/// - `My App`
/// - `3DViewer`
/// - `我的Qt程序` (means "My Qt App" in English)
///
/// 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: &str) -> Result<Self> {
// Check identifier
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9]*$").unwrap());
if !RE.is_match(identifier) {
return Err(Error::BadIdentifier(identifier.to_string()));
}
// Everything is okey, build self.
Ok(Self {
identifier: identifier.to_string(),
// The error type of PathBuf FromStr trait is Infallible,
// so it must be okey and we can use unwrap safely.
full_path: full_path.parse().unwrap(),
manners: IndexSet::new(),
exts: IndexMap::new(),
})
}
/// Add manner provided by this program. impl Schema {
pub fn add_manner(&mut self, manner: &str) -> Result<Token> { pub fn new() -> Self {
// TODO: Use wincmd::CmdArgs instead of String. Self {
// Create manner from string identifier: String::new(),
let manner = manner.to_string(); path: String::new(),
// Backup a stringfied manner for error output. clsid: String::new(),
let manner_str = manner.to_string(); icons: HashMap::new(),
// Insert manner. behaviors: HashMap::new(),
let idx = self.manners.len(); exts: HashMap::new(),
if self.manners.insert(manner) {
Ok(idx)
} else {
Err(Error::DupManner(manner_str))
} }
} }
/// Get the string display of manner represented by given token pub fn set_identifier(&mut self, identifier: &str) -> Result<()> {}
pub fn get_manner_str(&self, token: Token) -> Option<String> {
self.manners.get_index(token).map(|s| s.clone()) pub fn set_path(&mut self, exe_path: &str) -> Result<()> {}
pub fn set_clsid(&mut self, clsid: &str) -> Result<()> {}
pub fn add_icon(&mut self, name: &str, value: &str) -> Result<()> {}
pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<()> {}
pub fn add_ext(
&mut self,
ext: &str,
ext_name: &str,
ext_icon: &str,
ext_behavior: &str,
) -> Result<()> {
} }
/// Add file extension supported by this program and its associated manner. pub fn into_program(self) -> Result<Program> {
pub fn add_ext(&mut self, ext: &str, token: Token) -> Result<Token> { Program::new(self)
// Check manner token
if let None = self.manners.get_index(token) {
return Err(Error::InvalidMannerToken);
}
// Create extension from string
let ext = Ext::new(ext)?;
// 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 string display of file extension represented by given token /// Program is a complete and immutable program representer
pub fn get_ext_str(&self, token: Token) -> Option<String> { pub struct Program {}
self.exts.get_index(token).map(|p| p.0.to_string())
impl TryFrom<Schema> for Program {
type Error = Error;
fn try_from(value: Schema) -> std::result::Result<Self, Self::Error> {
Self::new(value)
} }
} }
impl Program { impl Program {
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths"; pub fn new(schema: Schema) -> Result<Self> {}
const APPLICATIONS: &str = "Software\\Classes\\Applications";
/// Register this application.
pub fn register(&self, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::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("", &utilities::path_to_str(&self.full_path)?)?;
subkey.set_value("Path", &utilities::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 !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(), &"")?;
}
}
// Create ProgId subkeys
debug_println!("Adding ProgId subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
for (ext, manner_token) in self.exts.iter() {
let manner = self.manners.get_index(*manner_token).ok_or(Error::InvalidMannerToken)?;
let prog_id = self.build_prog_id(ext);
debug_println!("Adding ProgId \"{0}\" subkey...", prog_id.to_string());
let (subkey, _) = subkey_parent.create_subkey_with_flags(prog_id.to_string(), KEY_READ)?;
let (subkey_verb, _) = subkey.create_subkey_with_flags("open", KEY_READ)?;
let (subkey_command, _) = subkey_verb.create_subkey_with_flags("command", KEY_WRITE)?;
subkey_command.set_value("", manner)?;
}
// Okey
utilities::notify_assoc_changed();
Ok(())
}
/// Unregister this application.
pub fn unregister(&self, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::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)?;
// Remove ProgId subkeys
debug_println!("Removing ProgId subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
for ext in self.exts.keys() {
let prog_id = self.build_prog_id(ext);
debug_println!("Removing ProgId \"{0}\" subkey...", prog_id.to_string());
subkey_parent.delete_subkey_all(prog_id.to_string())?;
}
// Okey
utilities::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) -> Result<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 {
const CLASSES: &str = "Software\\Classes";
/// Set the default "open with" of given token associated extension to this program. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub fn link_ext(&self, ext: Token, scope: Scope) -> Result<()> { pub struct ExtKey {
// Check privilege inner: winconcept::Ext
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch file extension and build ProgId from it
let (ext, _) = match self.exts.get_index(ext) {
Some(v) => v,
None => return Err(Error::InvalidExtToken),
};
let prog_id = self.build_prog_id(ext);
// Fetch root key and navigate to Classes
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
// Open or create this extension key
let (subkey, _) = classes.create_subkey_with_flags(ext.to_string(), KEY_WRITE)?;
// Set the default way to open this file extension
subkey.set_value("", &prog_id.to_string())?;
// Okey
Ok(())
}
/// Remove this program from the default "open with" of given token associated extension.
///
/// If the default "open with" of given extension is not our program,
/// or there is no such file extension, this function do nothing.
pub fn unlink_ext(&self, ext: Token, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch file extension and build ProgId from it
let (ext, _) = match self.exts.get_index(ext) {
Some(v) => v,
None => return Err(Error::InvalidExtToken),
};
let prog_id = self.build_prog_id(ext);
// Fetch root key and navigate to Classes
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
// Open key for this extension.
// If there is no such key, return directly.
if let Some(subkey) =
extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_WRITE)?
{
// Only delete the default key if it is equal to our ProgId
if let Some(value) = extra::winreg::try_get_value::<String, _>(&subkey, "")? {
if value == prog_id.to_string() {
// Delete the default key.
subkey.delete_value("")?;
}
}
}
// Okey
Ok(())
}
/// Query the default "open with" of given token associated extension.
///
/// This function will return its associated ProgId if it is existing.
pub fn query_ext(&self, ext: Token, view: View) -> Result<Option<ProgId>> {
// Fetch file extension
let (ext, _) = match self.exts.get_index(ext) {
Some(v) => v,
None => return Err(Error::InvalidExtToken),
};
// Fetch root key and navigate to Classes
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(Self::CLASSES, KEY_READ)?,
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?,
};
// Open key for this extension if possible
let rv =
match extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_READ)? {
Some(subkey) => {
// Try get associated ProgId if possible
match extra::winreg::try_get_value::<String, _>(&subkey, "")? {
Some(value) => Some(ProgId::from(value.as_str())),
None => None,
}
}
None => None,
};
// Okey
Ok(rv)
}
} }
impl Program {
/// Extract the file name part from full path to application,
/// which was used in Registry path component.
fn extract_file_name(&self) -> Result<&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(Error::BadFullAppPath)
}
/// Extract the start in path from full path to application,
/// which basically is the stem of full path.
fn extract_start_in(&self) -> Result<&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(Error::BadFullAppPath)
}
/// Build ProgId from identifier and given file extension.
fn build_prog_id(&self, ext: &Ext) -> ProgId {
ProgId::Std(assoc::StdProgId::new(
&self.identifier,
&utilities::capitalize_first_ascii(ext.inner()),
None,
))
}
}
// endregion

501
wfassoc/src/lib_old.rs Normal file
View File

@@ -0,0 +1,501 @@
//! This crate provide utilities fetching and manilupating Windows file association.
//! All code under crate are following Microsoft document: https://learn.microsoft.com/en-us/windows/win32/shell/customizing-file-types-bumper
#[cfg(not(target_os = "windows"))]
compile_error!("Crate wfassoc is only supported on Windows.");
pub mod extra;
pub mod utilities;
pub mod assoc;
use assoc::{Ext, ProgId};
use indexmap::{IndexMap, IndexSet};
use regex::Regex;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::sync::LazyLock;
use thiserror::Error as TeError;
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("error occurs when manipulating with Registry: {0}")]
BadRegOper(#[from] std::io::Error),
#[error("{0}")]
CastOsStr(#[from] utilities::CastOsStrError),
#[error("{0}")]
ParseExt(#[from] assoc::ParseExtError),
#[error("no administrative privilege")]
NoPrivilege,
#[error("given identifier \"{0}\" of application is invalid")]
BadIdentifier(String),
#[error("given full path to application is invalid")]
BadFullAppPath,
#[error("manner \"{0}\" is already registered")]
DupManner(String),
#[error("file extension \"{0}\" is already registered")]
DupExt(String),
#[error("the token of manner is invalid")]
InvalidMannerToken,
#[error("the token of file extension is invalid")]
InvalidExtToken,
}
/// The result type used in this crate.
pub type Result<T> = std::result::Result<T, Error>;
// 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) -> std::result::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.
pub 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 !utilities::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: Program
/// The struct representing a complete program for registration and unregistration.
#[derive(Debug)]
pub struct Program {
/// The identifier of this program.
identifier: String,
/// The fully qualified path to the application.
full_path: PathBuf,
/// The collection holding all manners of this program.
manners: IndexSet<String>,
/// 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<Ext, Token>,
}
impl Program {
/// Create a new registrar for following operations.
///
/// `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程序` (means "My Qt App" in English)
///
/// 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: &str) -> Result<Self> {
// Check identifier
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9]*$").unwrap());
if !RE.is_match(identifier) {
return Err(Error::BadIdentifier(identifier.to_string()));
}
// Everything is okey, build self.
Ok(Self {
identifier: identifier.to_string(),
// The error type of PathBuf FromStr trait is Infallible,
// so it must be okey and we can use unwrap safely.
full_path: full_path.parse().unwrap(),
manners: IndexSet::new(),
exts: IndexMap::new(),
})
}
/// Add manner provided by this program.
pub fn add_manner(&mut self, manner: &str) -> Result<Token> {
// TODO: Use wincmd::CmdArgs instead of String.
// Create manner from string
let manner = manner.to_string();
// 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::DupManner(manner_str))
}
}
/// Get the string display of manner represented by given token
pub fn get_manner_str(&self, token: Token) -> Option<String> {
self.manners.get_index(token).map(|s| s.clone())
}
/// Add file extension supported by this program and its associated manner.
pub fn add_ext(&mut self, ext: &str, token: Token) -> Result<Token> {
// Check manner token
if let None = self.manners.get_index(token) {
return Err(Error::InvalidMannerToken);
}
// Create extension from string
let ext = Ext::new(ext)?;
// 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 string display of file extension represented by given token
pub fn get_ext_str(&self, token: Token) -> Option<String> {
self.exts.get_index(token).map(|p| p.0.to_string())
}
}
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) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::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("", &utilities::path_to_str(&self.full_path)?)?;
subkey.set_value("Path", &utilities::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 !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(), &"")?;
}
}
// Create ProgId subkeys
debug_println!("Adding ProgId subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
for (ext, manner_token) in self.exts.iter() {
let manner = self.manners.get_index(*manner_token).ok_or(Error::InvalidMannerToken)?;
let prog_id = self.build_prog_id(ext);
debug_println!("Adding ProgId \"{0}\" subkey...", prog_id.to_string());
let (subkey, _) = subkey_parent.create_subkey_with_flags(prog_id.to_string(), KEY_READ)?;
let (subkey_verb, _) = subkey.create_subkey_with_flags("open", KEY_READ)?;
let (subkey_command, _) = subkey_verb.create_subkey_with_flags("command", KEY_WRITE)?;
subkey_command.set_value("", manner)?;
}
// Okey
utilities::notify_assoc_changed();
Ok(())
}
/// Unregister this application.
pub fn unregister(&self, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::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)?;
// Remove ProgId subkeys
debug_println!("Removing ProgId subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
for ext in self.exts.keys() {
let prog_id = self.build_prog_id(ext);
debug_println!("Removing ProgId \"{0}\" subkey...", prog_id.to_string());
subkey_parent.delete_subkey_all(prog_id.to_string())?;
}
// Okey
utilities::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) -> Result<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 {
const CLASSES: &str = "Software\\Classes";
/// Set the default "open with" of given token associated extension to this program.
pub fn link_ext(&self, ext: Token, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch file extension and build ProgId from it
let (ext, _) = match self.exts.get_index(ext) {
Some(v) => v,
None => return Err(Error::InvalidExtToken),
};
let prog_id = self.build_prog_id(ext);
// Fetch root key and navigate to Classes
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
// Open or create this extension key
let (subkey, _) = classes.create_subkey_with_flags(ext.to_string(), KEY_WRITE)?;
// Set the default way to open this file extension
subkey.set_value("", &prog_id.to_string())?;
// Okey
Ok(())
}
/// Remove this program from the default "open with" of given token associated extension.
///
/// If the default "open with" of given extension is not our program,
/// or there is no such file extension, this function do nothing.
pub fn unlink_ext(&self, ext: Token, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch file extension and build ProgId from it
let (ext, _) = match self.exts.get_index(ext) {
Some(v) => v,
None => return Err(Error::InvalidExtToken),
};
let prog_id = self.build_prog_id(ext);
// Fetch root key and navigate to Classes
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
// Open key for this extension.
// If there is no such key, return directly.
if let Some(subkey) =
extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_WRITE)?
{
// Only delete the default key if it is equal to our ProgId
if let Some(value) = extra::winreg::try_get_value::<String, _>(&subkey, "")? {
if value == prog_id.to_string() {
// Delete the default key.
subkey.delete_value("")?;
}
}
}
// Okey
Ok(())
}
/// Query the default "open with" of given token associated extension.
///
/// This function will return its associated ProgId if it is existing.
pub fn query_ext(&self, ext: Token, view: View) -> Result<Option<ProgId>> {
// Fetch file extension
let (ext, _) = match self.exts.get_index(ext) {
Some(v) => v,
None => return Err(Error::InvalidExtToken),
};
// Fetch root key and navigate to Classes
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(Self::CLASSES, KEY_READ)?,
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?,
};
// Open key for this extension if possible
let rv =
match extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_READ)? {
Some(subkey) => {
// Try get associated ProgId if possible
match extra::winreg::try_get_value::<String, _>(&subkey, "")? {
Some(value) => Some(ProgId::from(value.as_str())),
None => None,
}
}
None => None,
};
// Okey
Ok(rv)
}
}
impl Program {
/// Extract the file name part from full path to application,
/// which was used in Registry path component.
fn extract_file_name(&self) -> Result<&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(Error::BadFullAppPath)
}
/// Extract the start in path from full path to application,
/// which basically is the stem of full path.
fn extract_start_in(&self) -> Result<&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(Error::BadFullAppPath)
}
/// Build ProgId from identifier and given file extension.
fn build_prog_id(&self, ext: &Ext) -> ProgId {
ProgId::Std(assoc::StdProgId::new(
&self.identifier,
&utilities::capitalize_first_ascii(ext.inner()),
None,
))
}
}
// endregion

View File

@@ -23,77 +23,6 @@ macro_rules! debug_println {
}; };
} }
// 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,
std::ptr::null(),
std::ptr::null(),
)
}
}
// endregion
// region OS String Related // region OS String Related
/// The error occurs when casting `OsStr` into `str`. /// The error occurs when casting `OsStr` into `str`.

70
wfassoc/src/win32ext.rs Normal file
View File

@@ -0,0 +1,70 @@
//! The module contains some Windows-specific functions for file associations.
//! These functions can not be grouped as Windows concept.
//! So they are placed in there as an independent module.
/// 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,
std::ptr::null(),
std::ptr::null(),
)
}
}

View File

@@ -1,4 +1,4 @@
//! This module expand Windows-related stuff by `windows-sys` crate. //! This module create some structs for Windows specific concepts by `windows-sys` crate.
//! 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.
@@ -90,7 +90,9 @@ impl FromStr for Ext {
type Err = ParseExtError; type Err = ParseExtError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\.([^\.]+)$").unwrap()); static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^\.([^\.]+)$").expect("unexpected bad regex pattern string")
});
match RE.captures(s) { match RE.captures(s) {
Some(v) => Ok(Self::new(&v[1]).expect("unexpected dot in Ext body")), Some(v) => Ok(Self::new(&v[1]).expect("unexpected dot in Ext body")),
None => Err(ParseExtError::new(s)), None => Err(ParseExtError::new(s)),
@@ -207,8 +209,10 @@ impl FromStr for ProgId {
type Err = ParseProgIdError; type Err = ParseProgIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> = static RE: LazyLock<Regex> = LazyLock::new(|| {
LazyLock::new(|| Regex::new(r"^([^\.]+)\.([^\.]+)(\.([0-9]+))?$").unwrap()); Regex::new(r"^([^\.]+)\.([^\.]+)(\.([0-9]+))?$")
.expect("unexpected bad regex pattern string")
});
let caps = RE.captures(s); let caps = RE.captures(s);
if let Some(caps) = caps { if let Some(caps) = caps {
let vendor = &caps[1]; let vendor = &caps[1];
@@ -234,7 +238,7 @@ impl FromStr for ProgId {
/// The struct representing Windows CLSID looks like /// The struct representing Windows CLSID looks like
/// `{26EE0668-A00A-44D7-9371-BEB064C98683}` (case insensitive). /// `{26EE0668-A00A-44D7-9371-BEB064C98683}` (case insensitive).
/// The brace is essential part. /// The curly brace is the essential part.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Clsid { pub struct Clsid {
inner: Uuid, inner: Uuid,
@@ -324,7 +328,11 @@ pub struct ExpandString {
} }
impl ExpandString { impl ExpandString {
const VAR_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"%[a-zA-Z0-9_]+%").unwrap()); /// Internal shared compiled regex pattern matching Variable,
/// the `%` braced string like `%SystemRoot%`.
const VAR_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"%[a-zA-Z0-9_]+%").expect("unexpected bad regex pattern string")
});
} }
impl ExpandString { impl ExpandString {
@@ -466,8 +474,9 @@ impl FromStr for IconRefStr {
type Err = ParseIconRefStrError; type Err = ParseIconRefStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> = static RE: LazyLock<Regex> = LazyLock::new(|| {
LazyLock::new(|| Regex::new(r"^([^,@].*),-([0-9]+)$").unwrap()); Regex::new(r"^([^,@].*),-([0-9]+)$").expect("unexpected bad regex pattern string")
});
let caps = RE.captures(s); let caps = RE.captures(s);
if let Some(caps) = caps { if let Some(caps) = caps {
let path = &caps[1]; let path = &caps[1];
@@ -549,7 +558,9 @@ impl FromStr for StrRefStr {
type Err = ParseStrRefStrError; type Err = ParseStrRefStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^@(.+),-([0-9]+)$").unwrap()); static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^@(.+),-([0-9]+)$").expect("unexpected bad regex pattern string")
});
let caps = RE.captures(s); let caps = RE.captures(s);
if let Some(caps) = caps { if let Some(caps) = caps {
let path = &caps[1]; let path = &caps[1];

View File

@@ -1,4 +1,4 @@
//! This module expand `winreg` crate to make it more suit for this crate. //! This module extend `winreg` crate to make it more suit for the usage of this crate.
use std::ffi::OsStr; use std::ffi::OsStr;
use std::ops::Deref; use std::ops::Deref;

View File

@@ -1,5 +1,5 @@
use std::{path::Path, str::FromStr}; use std::{path::Path, str::FromStr};
use wfassoc::extra::windows::*; use wfassoc::winconcept::*;
#[test] #[test]
fn test_ex_new() { fn test_ex_new() {