refactor: commit code which idk when i write them
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -4,498 +4,96 @@
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
compile_error!("Crate wfassoc is only supported on Windows.");
|
||||
|
||||
pub mod extra;
|
||||
pub mod utilities;
|
||||
pub mod assoc;
|
||||
pub mod winconcept;
|
||||
pub mod win32ext;
|
||||
pub mod winregext;
|
||||
|
||||
use assoc::{Ext, ProgId};
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use regex::Regex;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
use std::collections::HashMap;
|
||||
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.
|
||||
/// Error occurs in this module.
|
||||
#[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),
|
||||
pub enum Error {}
|
||||
|
||||
#[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,
|
||||
}
|
||||
/// Result type used in this module.
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// 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.
|
||||
/// Schema is the sketchpad of complete Program.
|
||||
///
|
||||
/// We will create a Schema first, fill some properties, add file extensions,
|
||||
/// then convert it into immutable Program for following using.
|
||||
#[derive(Debug)]
|
||||
pub struct Program {
|
||||
/// The identifier of this program.
|
||||
pub struct Schema {
|
||||
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>,
|
||||
path: String,
|
||||
clsid: String,
|
||||
icons: HashMap<String, String>,
|
||||
behaviors: HashMap<String, String>,
|
||||
exts: HashMap<String, SchemaExt>,
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
/// Internal used struct as the Schema file extensions hashmap value type.
|
||||
#[derive(Debug)]
|
||||
struct SchemaExt {
|
||||
name: String,
|
||||
icon: String,
|
||||
behavior: 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))
|
||||
impl Schema {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
identifier: String::new(),
|
||||
path: String::new(),
|
||||
clsid: String::new(),
|
||||
icons: HashMap::new(),
|
||||
behaviors: HashMap::new(),
|
||||
exts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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())
|
||||
pub fn set_identifier(&mut self, identifier: &str) -> Result<()> {}
|
||||
|
||||
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 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);
|
||||
pub fn into_program(self) -> Result<Program> {
|
||||
Program::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
/// Program is a complete and immutable program representer
|
||||
pub struct Program {}
|
||||
|
||||
/// 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 TryFrom<Schema> for Program {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Schema) -> std::result::Result<Self, Self::Error> {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
pub fn new(schema: Schema) -> Result<Self> {}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ExtKey {
|
||||
inner: winconcept::Ext
|
||||
}
|
||||
|
||||
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
501
wfassoc/src/lib_old.rs
Normal 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
|
||||
@@ -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
|
||||
|
||||
/// The error occurs when casting `OsStr` into `str`.
|
||||
|
||||
70
wfassoc/src/win32ext.rs
Normal file
70
wfassoc/src/win32ext.rs
Normal 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
//! and should be manually implemented for our file association use.
|
||||
|
||||
@@ -90,7 +90,9 @@ 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());
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^\.([^\.]+)$").expect("unexpected bad regex pattern string")
|
||||
});
|
||||
match RE.captures(s) {
|
||||
Some(v) => Ok(Self::new(&v[1]).expect("unexpected dot in Ext body")),
|
||||
None => Err(ParseExtError::new(s)),
|
||||
@@ -207,8 +209,10 @@ impl FromStr for ProgId {
|
||||
type Err = ParseProgIdError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
static RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^([^\.]+)\.([^\.]+)(\.([0-9]+))?$").unwrap());
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^([^\.]+)\.([^\.]+)(\.([0-9]+))?$")
|
||||
.expect("unexpected bad regex pattern string")
|
||||
});
|
||||
let caps = RE.captures(s);
|
||||
if let Some(caps) = caps {
|
||||
let vendor = &caps[1];
|
||||
@@ -234,7 +238,7 @@ impl FromStr for ProgId {
|
||||
|
||||
/// The struct representing Windows CLSID looks like
|
||||
/// `{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)]
|
||||
pub struct Clsid {
|
||||
inner: Uuid,
|
||||
@@ -324,7 +328,11 @@ pub struct 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 {
|
||||
@@ -466,8 +474,9 @@ impl FromStr for IconRefStr {
|
||||
type Err = ParseIconRefStrError;
|
||||
|
||||
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);
|
||||
if let Some(caps) = caps {
|
||||
let path = &caps[1];
|
||||
@@ -549,7 +558,9 @@ impl FromStr for StrRefStr {
|
||||
type Err = ParseStrRefStrError;
|
||||
|
||||
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);
|
||||
if let Some(caps) = caps {
|
||||
let path = &caps[1];
|
||||
@@ -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::ops::Deref;
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{path::Path, str::FromStr};
|
||||
use wfassoc::extra::windows::*;
|
||||
use wfassoc::winconcept::*;
|
||||
|
||||
#[test]
|
||||
fn test_ex_new() {
|
||||
Reference in New Issue
Block a user