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"))]
|
#[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
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
|
// 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
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)
|
//! 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];
|
||||||
@@ -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;
|
||||||
@@ -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() {
|
||||||
Reference in New Issue
Block a user