feat: remove all old useless code
This commit is contained in:
@@ -1,391 +0,0 @@
|
|||||||
//! The module including all struct representing Windows file association concept,
|
|
||||||
//! like file extension, ProgId, CLSID and etc.
|
|
||||||
|
|
||||||
use crate::extra::windows::{Ext, ProgId};
|
|
||||||
use crate::extra::winreg as winreg_extra;
|
|
||||||
use crate::utilities;
|
|
||||||
use std::fmt::Display;
|
|
||||||
use std::str::FromStr;
|
|
||||||
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}")]
|
|
||||||
ParseExt(#[from] crate::extra::windows::ParseExtError),
|
|
||||||
#[error("{0}")]
|
|
||||||
ParseProgId(#[from] crate::extra::windows::ParseProgIdError),
|
|
||||||
#[error("{0}")]
|
|
||||||
ParseProgIdKind(#[from] ParseProgIdKindError),
|
|
||||||
#[error("{0}")]
|
|
||||||
BlankPath(#[from] crate::extra::winreg::BlankPathError),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The result type used in this crate.
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Data Types
|
|
||||||
|
|
||||||
/// The token for access registered items in Program.
|
|
||||||
/// This is usually returned when you registering them.
|
|
||||||
pub type Token = usize;
|
|
||||||
|
|
||||||
// region: Scope
|
|
||||||
|
|
||||||
/// 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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: View
|
|
||||||
|
|
||||||
/// 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: ProgId Kind
|
|
||||||
|
|
||||||
/// The error occurs when parsing ProgId kind.
|
|
||||||
#[derive(Debug, TeError)]
|
|
||||||
pub enum ParseProgIdKindError {
|
|
||||||
#[error("{0}")]
|
|
||||||
FromStd(#[from] crate::extra::windows::ParseProgIdError),
|
|
||||||
#[error("given ProgId is blank")]
|
|
||||||
BlankProgId,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The variant of ProgId for the compatibility
|
|
||||||
/// with those software which do not follow Microsoft suggestions.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub enum ProgIdKind {
|
|
||||||
/// Other ProgId which not follow Microsoft standards.
|
|
||||||
Other(String),
|
|
||||||
/// Standard ProgId.
|
|
||||||
Std(ProgId),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ProgIdKind {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
ProgIdKind::Other(v) => write!(f, "{}", v),
|
|
||||||
ProgIdKind::Std(prog_id) => write!(f, "{}", prog_id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for ProgIdKind {
|
|
||||||
type Err = ParseProgIdKindError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
|
||||||
if s.is_empty() {
|
|
||||||
Err(ParseProgIdKindError::BlankProgId)
|
|
||||||
} else {
|
|
||||||
Ok(match s.parse::<ProgId>() {
|
|
||||||
Ok(v) => Self::Std(v),
|
|
||||||
Err(_) => Self::Other(s.to_string()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Registry Visitor
|
|
||||||
|
|
||||||
// region: Application Visitor
|
|
||||||
|
|
||||||
/// The static struct for visiting "Application" in registry
|
|
||||||
pub struct ApplicationVisitor {}
|
|
||||||
|
|
||||||
impl ApplicationVisitor {
|
|
||||||
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
|
|
||||||
const APPLICATIONS: &str = "Software\\Classes\\Applications";
|
|
||||||
|
|
||||||
/// Open a readonly registry key to "App Paths" with given scope.
|
|
||||||
pub fn open_app_paths(scope: Scope) -> Result<RegKey> {
|
|
||||||
let hk = RegKey::predef(match scope {
|
|
||||||
Scope::User => HKEY_CURRENT_USER,
|
|
||||||
Scope::System => HKEY_LOCAL_MACHINE,
|
|
||||||
});
|
|
||||||
let app_paths = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
|
|
||||||
|
|
||||||
Ok(app_paths)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open a readonly registry key to "Applications" with given scope.
|
|
||||||
pub fn open_applications(scope: Scope) -> Result<RegKey> {
|
|
||||||
let hk = RegKey::predef(match scope {
|
|
||||||
Scope::User => HKEY_CURRENT_USER,
|
|
||||||
Scope::System => HKEY_LOCAL_MACHINE,
|
|
||||||
});
|
|
||||||
let applications = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
|
|
||||||
|
|
||||||
Ok(applications)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Classes Visitor
|
|
||||||
|
|
||||||
/// The static struct for visiting "Classes" in registry.
|
|
||||||
pub struct ClassesVisitor {}
|
|
||||||
|
|
||||||
impl ClassesVisitor {
|
|
||||||
const CLASSES: &str = "Software\\Classes";
|
|
||||||
|
|
||||||
/// Open a readonly registry key to "Classes" with given view.
|
|
||||||
pub fn open_with_view(view: View) -> Result<RegKey> {
|
|
||||||
// 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)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(classes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open a readonly registry key to "Classes" with given scope.
|
|
||||||
pub fn open_with_scope(scope: Scope) -> Result<RegKey> {
|
|
||||||
// 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)?;
|
|
||||||
|
|
||||||
Ok(classes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: File Extension Registry Key
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct ExtKey {
|
|
||||||
ext: Ext,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtKey {
|
|
||||||
/// Create new file extension registry key representer.
|
|
||||||
pub fn new(s: &str) -> Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
ext: Ext::from_str(s)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch the reference to inner extension representer.
|
|
||||||
pub fn as_inner(&self) -> &Ext {
|
|
||||||
&self.ext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtKey {
|
|
||||||
/// Set the default "Open With" of this file extension to given ProgId.
|
|
||||||
pub fn link(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
|
|
||||||
use winreg_extra::blank_path_guard;
|
|
||||||
|
|
||||||
// Open Classes key
|
|
||||||
let classes = ClassesVisitor::open_with_scope(scope)?;
|
|
||||||
|
|
||||||
// Open or create this extension key
|
|
||||||
let (subkey, _) =
|
|
||||||
classes.create_subkey_with_flags(blank_path_guard(self.ext.to_string())?, KEY_WRITE)?;
|
|
||||||
// Set the default way to open this file extension
|
|
||||||
subkey.set_value("", &prog_id.as_inner().to_string())?;
|
|
||||||
|
|
||||||
// Okey
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset the default "Open With" of this file extension to blank.
|
|
||||||
///
|
|
||||||
/// If the default "Open With" of this file extension is not given ProgId,
|
|
||||||
/// or there is no such file extension, this function do nothing.
|
|
||||||
pub fn unlink(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
|
|
||||||
use winreg_extra::{blank_path_guard, try_get_value, try_open_subkey_with_flags};
|
|
||||||
|
|
||||||
// Open Classes key
|
|
||||||
let classes = ClassesVisitor::open_with_scope(scope)?;
|
|
||||||
|
|
||||||
// Open key for this extension.
|
|
||||||
// If there is no such key, return directly.
|
|
||||||
if let Some(subkey) = try_open_subkey_with_flags(
|
|
||||||
&classes,
|
|
||||||
blank_path_guard(self.ext.to_string())?,
|
|
||||||
KEY_WRITE,
|
|
||||||
)? {
|
|
||||||
// Only delete the default key if it is equal to our ProgId
|
|
||||||
if let Some(value) = try_get_value::<String, _>(&subkey, "")? {
|
|
||||||
if value == prog_id.as_inner().to_string() {
|
|
||||||
// Delete the default key.
|
|
||||||
subkey.delete_value("")?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Okey
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Query the default "Open With" of this file extension associated ProgId.
|
|
||||||
///
|
|
||||||
/// This function will return its associated ProgId if "Open With" was set.
|
|
||||||
pub fn query(&self, view: View) -> Result<Option<ProgIdKey>> {
|
|
||||||
use winreg_extra::{blank_path_guard, try_get_value, try_open_subkey_with_flags};
|
|
||||||
|
|
||||||
// Open Classes key
|
|
||||||
let classes = ClassesVisitor::open_with_view(view)?;
|
|
||||||
|
|
||||||
// Open key for this extension if possible
|
|
||||||
let rv = match try_open_subkey_with_flags(
|
|
||||||
&classes,
|
|
||||||
blank_path_guard(self.ext.to_string())?,
|
|
||||||
KEY_READ,
|
|
||||||
)? {
|
|
||||||
Some(subkey) => {
|
|
||||||
// Try get associated ProgId if possible
|
|
||||||
match try_get_value::<String, _>(&subkey, "")? {
|
|
||||||
Some(value) => Some(ProgIdKey::new(value.as_str())?),
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Okey
|
|
||||||
Ok(rv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: ProgId Registry Key
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct ProgIdKey {
|
|
||||||
prog_id: ProgIdKind,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProgIdKey {
|
|
||||||
/// Create new ProgId registry representer.
|
|
||||||
pub fn new(s: &str) -> Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
prog_id: ProgIdKind::from_str(s)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch the reference to inner ProgId.
|
|
||||||
pub fn as_inner(&self) -> &ProgIdKind {
|
|
||||||
&self.prog_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProgIdKey {
|
|
||||||
/// Create ProgId into Registry in given scope with given parameters
|
|
||||||
pub fn create(&self, scope: Scope, command: &str) -> Result<()> {
|
|
||||||
use winreg_extra::blank_path_guard;
|
|
||||||
|
|
||||||
let classes = ClassesVisitor::open_with_scope(scope)?;
|
|
||||||
let (subkey, _) = classes
|
|
||||||
.create_subkey_with_flags(blank_path_guard(self.prog_id.to_string())?, KEY_WRITE)?;
|
|
||||||
|
|
||||||
// Create verb
|
|
||||||
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("", &command.to_string())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete this ProgId from registry in given scope.
|
|
||||||
pub fn delete(&self, scope: Scope) -> Result<()> {
|
|
||||||
use winreg_extra::blank_path_guard;
|
|
||||||
|
|
||||||
let classes = ClassesVisitor::open_with_scope(scope)?;
|
|
||||||
classes.delete_subkey_all(blank_path_guard(self.prog_id.to_string())?)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
@@ -1,501 +0,0 @@
|
|||||||
//! 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
|
|
||||||
@@ -1,582 +0,0 @@
|
|||||||
|
|
||||||
/// The expand of winreg crate according to our module requirements.
|
|
||||||
mod winregex;
|
|
||||||
|
|
||||||
use regex::Regex;
|
|
||||||
use std::fmt::Display;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
use thiserror::Error as TeError;
|
|
||||||
use winreg::RegKey;
|
|
||||||
|
|
||||||
// region: Error Types
|
|
||||||
|
|
||||||
/// All possible error occurs in this crate.
|
|
||||||
#[derive(Debug, TeError)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error(
|
|
||||||
"can not register because lack essential privilege. please consider running with Administrator role"
|
|
||||||
)]
|
|
||||||
NoPrivilege,
|
|
||||||
#[error("{0}")]
|
|
||||||
Register(#[from] std::io::Error),
|
|
||||||
#[error("{0}")]
|
|
||||||
BadFileExt(#[from] ParseFileExtError),
|
|
||||||
#[error("{0}")]
|
|
||||||
BadProgId(#[from] ParseProgIdError),
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Privilege, Scope and View
|
|
||||||
|
|
||||||
/// Check whether current process has administrative privilege.
|
|
||||||
///
|
|
||||||
/// It usually means that checking whether current process is running as Administrator.
|
|
||||||
/// Return true if it is, otherwise false.
|
|
||||||
///
|
|
||||||
/// Reference: https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
|
|
||||||
pub fn has_privilege() -> bool {
|
|
||||||
use windows_sys::Win32::Foundation::HANDLE;
|
|
||||||
use windows_sys::Win32::Security::{
|
|
||||||
AllocateAndInitializeSid, CheckTokenMembership, FreeSid, PSID, SECURITY_NT_AUTHORITY,
|
|
||||||
};
|
|
||||||
use windows_sys::Win32::System::SystemServices::{
|
|
||||||
DOMAIN_ALIAS_RID_ADMINS, SECURITY_BUILTIN_DOMAIN_RID,
|
|
||||||
};
|
|
||||||
use windows_sys::core::BOOL;
|
|
||||||
|
|
||||||
let nt_authority = SECURITY_NT_AUTHORITY.clone();
|
|
||||||
let mut administrators_group: PSID = PSID::default();
|
|
||||||
let success: BOOL = unsafe {
|
|
||||||
AllocateAndInitializeSid(
|
|
||||||
&nt_authority,
|
|
||||||
2,
|
|
||||||
SECURITY_BUILTIN_DOMAIN_RID as u32,
|
|
||||||
DOMAIN_ALIAS_RID_ADMINS as u32,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
&mut administrators_group,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if success == 0 {
|
|
||||||
panic!("Win32 AllocateAndInitializeSid() failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut is_member: BOOL = BOOL::default();
|
|
||||||
let success: BOOL =
|
|
||||||
unsafe { CheckTokenMembership(HANDLE::default(), administrators_group, &mut is_member) };
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
FreeSid(administrators_group);
|
|
||||||
}
|
|
||||||
|
|
||||||
if success == 0 {
|
|
||||||
panic!("Win32 CheckTokenMembership() failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
is_member != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The scope where wfassoc will register and unregister.
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub enum Scope {
|
|
||||||
/// Scope for current user.
|
|
||||||
User,
|
|
||||||
/// Scope for all users under this computer.
|
|
||||||
System,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The view when wfassoc querying infomations.
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub enum View {
|
|
||||||
/// The view of current user.
|
|
||||||
User,
|
|
||||||
/// The view of system.
|
|
||||||
System,
|
|
||||||
/// Hybrid view of User and System.
|
|
||||||
/// It can be seen as that we use System first and then use User to override any existing items.
|
|
||||||
Hybrid,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The error occurs when cast View into Scope.
|
|
||||||
#[derive(Debug, TeError)]
|
|
||||||
#[error("hybrid view can not be cast into any scope")]
|
|
||||||
pub struct TryFromViewError {}
|
|
||||||
|
|
||||||
impl TryFromViewError {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Scope> for View {
|
|
||||||
fn from(value: Scope) -> Self {
|
|
||||||
match value {
|
|
||||||
Scope::User => Self::User,
|
|
||||||
Scope::System => Self::System,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<View> for Scope {
|
|
||||||
type Error = TryFromViewError;
|
|
||||||
|
|
||||||
fn try_from(value: View) -> Result<Self, Self::Error> {
|
|
||||||
match value {
|
|
||||||
View::User => Ok(Self::User),
|
|
||||||
View::System => Ok(Self::System),
|
|
||||||
View::Hybrid => Err(TryFromViewError::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scope {
|
|
||||||
/// Check whether we have enough privilege when operating in current scope.
|
|
||||||
/// If we have, simply return, otherwise return error.
|
|
||||||
fn check_privilege(&self) -> Result<(), Error> {
|
|
||||||
if matches!(self, Self::System if !has_privilege()) {
|
|
||||||
Err(Error::NoPrivilege)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: File Extension
|
|
||||||
|
|
||||||
/// The struct representing an file extension which must start with dot (`.`)
|
|
||||||
/// and followed by at least one arbitrary characters.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct FileExt {
|
|
||||||
/// The body of file extension (excluding dot).
|
|
||||||
inner: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileExt {
|
|
||||||
pub fn new(file_ext: &str) -> Result<Self, ParseFileExtError> {
|
|
||||||
Self::from_str(file_ext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The error occurs when try parsing string into FileExt.
|
|
||||||
#[derive(Debug, TeError)]
|
|
||||||
#[error("given file extension is invalid")]
|
|
||||||
pub struct ParseFileExtError {}
|
|
||||||
|
|
||||||
impl ParseFileExtError {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for FileExt {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, ".{}", self.inner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for FileExt {
|
|
||||||
type Err = ParseFileExtError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\.([^\.]+)$").unwrap());
|
|
||||||
match RE.captures(s) {
|
|
||||||
Some(v) => Ok(Self {
|
|
||||||
inner: v[1].to_string(),
|
|
||||||
}),
|
|
||||||
None => Err(ParseFileExtError::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileExt {
|
|
||||||
fn open_scope(&self, scope: Scope) -> Result<RegKey, Error> {
|
|
||||||
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE};
|
|
||||||
|
|
||||||
// check privilege
|
|
||||||
scope.check_privilege()?;
|
|
||||||
// get the root key
|
|
||||||
let hk = match scope {
|
|
||||||
Scope::User => RegKey::predef(HKEY_CURRENT_USER),
|
|
||||||
Scope::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
|
||||||
};
|
|
||||||
// navigate to classes
|
|
||||||
let classes = hk.open_subkey_with_flags("Software\\Classes", KEY_READ | KEY_WRITE)?;
|
|
||||||
// okey
|
|
||||||
Ok(classes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_view(&self, view: View) -> Result<Option<RegKey>, Error> {
|
|
||||||
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ};
|
|
||||||
|
|
||||||
// navigate to extension container
|
|
||||||
let hk = match view {
|
|
||||||
View::User => RegKey::predef(HKEY_CURRENT_USER),
|
|
||||||
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
|
||||||
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
|
|
||||||
};
|
|
||||||
let classes = match view {
|
|
||||||
View::User | View::System => {
|
|
||||||
hk.open_subkey_with_flags("Software\\Classes", KEY_READ)?
|
|
||||||
}
|
|
||||||
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?,
|
|
||||||
};
|
|
||||||
// check whether there is this ext
|
|
||||||
classes.
|
|
||||||
// open extension key if possible
|
|
||||||
let thisext = classes.open_subkey_with_flags(file_ext.to_string(), KEY_READ)?;
|
|
||||||
// okey
|
|
||||||
Ok(classes)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_current(&self, view: View) -> Option<ProgId> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_current(&mut self, scope: Scope, prog_id: Option<&ProgId>) -> Result<(), Error> {
|
|
||||||
scope.check_privilege()?;
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iter_open_with(&self, view: View) -> Result<impl Iterator<Item = ProgId>, Error> {
|
|
||||||
let viewer = match self.open_view(view)? {
|
|
||||||
Some(viewer) => viewer,
|
|
||||||
None => return Ok(std::iter::empty::<ProgId>()),
|
|
||||||
};
|
|
||||||
let it = winregex::iter_sz_keys(&viewer);
|
|
||||||
let it = winregex::exclude_default_key(it);
|
|
||||||
|
|
||||||
Ok(it.map(|s| ProgId::from(s.as_str())))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_open_with(&mut self, scope: Scope, prog_id: &ProgId) -> Result<(), Error> {
|
|
||||||
scope.check_privilege()?;
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn flash_open_with(
|
|
||||||
&mut self,
|
|
||||||
scope: Scope,
|
|
||||||
prog_ids: impl Iterator<Item = ProgId>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
scope.check_privilege()?;
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The association infomations of specific file extension.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FileExtAssoc {
|
|
||||||
default: String,
|
|
||||||
open_with_progids: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileExtAssoc {
|
|
||||||
fn new(file_ext: &FileExt, view: View) -> Option<Self> {
|
|
||||||
use winreg::RegKey;
|
|
||||||
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ};
|
|
||||||
|
|
||||||
// navigate to extension container
|
|
||||||
let hk = match view {
|
|
||||||
View::User => RegKey::predef(HKEY_CURRENT_USER),
|
|
||||||
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
|
||||||
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
|
|
||||||
};
|
|
||||||
let classes = match view {
|
|
||||||
View::User | View::System => hk
|
|
||||||
.open_subkey_with_flags("Software\\Classes", KEY_READ)
|
|
||||||
.unwrap(),
|
|
||||||
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ).unwrap(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// open extension key if possible
|
|
||||||
let thisext = match classes.open_subkey_with_flags(file_ext.to_string(), KEY_READ) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// fetch extension infos.
|
|
||||||
let default = thisext.get_value("").unwrap_or(String::new());
|
|
||||||
let open_with_progids =
|
|
||||||
if let Ok(progids) = thisext.open_subkey_with_flags("OpenWithProdIds", KEY_READ) {
|
|
||||||
progids
|
|
||||||
.enum_keys()
|
|
||||||
.map(|x| x.unwrap())
|
|
||||||
.filter(|k| !k.is_empty())
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(Self {
|
|
||||||
default,
|
|
||||||
open_with_progids,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_default(&self) -> &str {
|
|
||||||
&self.default
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn len_open_with_progid(&self) -> usize {
|
|
||||||
self.open_with_progids.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iter_open_with_progids(&self) -> impl Iterator<Item = &str> {
|
|
||||||
self.open_with_progids.iter().map(|s| s.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Executable Resource
|
|
||||||
|
|
||||||
// /// The struct representing an Windows executable resources path like
|
|
||||||
// /// `path_to_file.exe,1`.
|
|
||||||
// pub struct ExecRc {
|
|
||||||
// /// The path to binary for finding resources.
|
|
||||||
// binary: PathBuf,
|
|
||||||
// /// The inner index of resources.
|
|
||||||
// index: u32,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl ExecRc {
|
|
||||||
// pub fn new(res_str: &str) -> Result<Self, ParseExecRcError> {
|
|
||||||
// static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([^,]+),([0-9]+)$").unwrap());
|
|
||||||
// let caps = RE.captures(res_str);
|
|
||||||
// if let Some(caps) = caps {
|
|
||||||
// let binary = PathBuf::from_str(&caps[1])?;
|
|
||||||
// let index = caps[2].parse::<u32>()?;
|
|
||||||
// Ok(Self { binary, index })
|
|
||||||
// } else {
|
|
||||||
// Err(ParseExecRcError::NoCapture)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// The error occurs when try parsing string into ExecRc.
|
|
||||||
// #[derive(Debug, TeError)]
|
|
||||||
// #[error("given string is not a valid executable resource string")]
|
|
||||||
// pub enum ParseExecRcError {
|
|
||||||
// /// Given string is not matched with format.
|
|
||||||
// NoCapture,
|
|
||||||
// /// Fail to convert executable part into path.
|
|
||||||
// BadBinaryPath(#[from] std::convert::Infallible),
|
|
||||||
// /// Fail to convert index part into valid number.
|
|
||||||
// BadIndex(#[from] std::num::ParseIntError),
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl FromStr for ExecRc {
|
|
||||||
// type Err = ParseExecRcError;
|
|
||||||
|
|
||||||
// fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
// ExecRc::new(s)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl Display for ExecRc {
|
|
||||||
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
// write!(f, "{},{}", self.binary.to_str().unwrap(), self.index)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Programmatic Identifiers (ProgId)
|
|
||||||
|
|
||||||
/// The struct representing Programmatic Identifiers (ProgId).
|
|
||||||
///
|
|
||||||
/// Because there is optional part in standard ProgId, and not all software developers
|
|
||||||
/// are willing to following Microsoft suggestions, there is no strict constaint for ProgId.
|
|
||||||
/// So this struct is actually an enum which holding any possible ProgId format.
|
|
||||||
///
|
|
||||||
/// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids
|
|
||||||
pub enum ProgId {
|
|
||||||
Plain(String),
|
|
||||||
Loose(LosseProgId),
|
|
||||||
Strict(StrictProgId),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for ProgId {
|
|
||||||
fn from(s: &str) -> Self {
|
|
||||||
// match it for strict ProgId first
|
|
||||||
if let Ok(v) = StrictProgId::from_str(s) {
|
|
||||||
return Self::Strict(v);
|
|
||||||
}
|
|
||||||
// then match for loose ProgId
|
|
||||||
if let Ok(v) = LosseProgId::from_str(s) {
|
|
||||||
return Self::Loose(v);
|
|
||||||
}
|
|
||||||
// fallback with plain
|
|
||||||
Self::Plain(s.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ProgId {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
ProgId::Plain(v) => v.fmt(f),
|
|
||||||
ProgId::Loose(v) => v.fmt(f),
|
|
||||||
ProgId::Strict(v) => v.fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The error occurs when parsing ProgId.
|
|
||||||
#[derive(Debug, TeError)]
|
|
||||||
#[error("given ProgId string is invalid")]
|
|
||||||
pub struct ParseProgIdError {}
|
|
||||||
|
|
||||||
impl ParseProgIdError {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The ProgId similar with strict ProgId, but no version part.
|
|
||||||
pub struct LosseProgId {
|
|
||||||
vendor: String,
|
|
||||||
component: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LosseProgId {
|
|
||||||
pub fn new(vendor: &str, component: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
vendor: vendor.to_string(),
|
|
||||||
component: component.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_vendor(&self) -> &str {
|
|
||||||
&self.vendor
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_component(&self) -> &str {
|
|
||||||
&self.component
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for LosseProgId {
|
|
||||||
type Err = ParseProgIdError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
static RE: LazyLock<Regex> =
|
|
||||||
LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)$").unwrap());
|
|
||||||
let caps = RE.captures(s);
|
|
||||||
if let Some(caps) = caps {
|
|
||||||
let vendor = &caps[1];
|
|
||||||
let component = &caps[2];
|
|
||||||
Ok(Self::new(vendor, component))
|
|
||||||
} else {
|
|
||||||
Err(ParseProgIdError::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for LosseProgId {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}.{}", self.vendor, self.component)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The ProgId exactly follows `[Vendor or Application].[Component].[Version]` format.
|
|
||||||
pub struct StrictProgId {
|
|
||||||
vendor: String,
|
|
||||||
component: String,
|
|
||||||
version: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StrictProgId {
|
|
||||||
pub fn new(vendor: &str, component: &str, version: u32) -> Self {
|
|
||||||
Self {
|
|
||||||
vendor: vendor.to_string(),
|
|
||||||
component: component.to_string(),
|
|
||||||
version,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_vendor(&self) -> &str {
|
|
||||||
&self.vendor
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_component(&self) -> &str {
|
|
||||||
&self.component
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_version(&self) -> u32 {
|
|
||||||
self.version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for StrictProgId {
|
|
||||||
type Err = ParseProgIdError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
static RE: LazyLock<Regex> =
|
|
||||||
LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)\.([0-9]+)$").unwrap());
|
|
||||||
let caps = RE.captures(s);
|
|
||||||
if let Some(caps) = caps {
|
|
||||||
let vendor = &caps[1];
|
|
||||||
let component = &caps[2];
|
|
||||||
let version = caps[3]
|
|
||||||
.parse::<u32>()
|
|
||||||
.map_err(|_| ParseProgIdError::new())?;
|
|
||||||
Ok(Self::new(vendor, component, version))
|
|
||||||
} else {
|
|
||||||
Err(ParseProgIdError::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for StrictProgId {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}.{}.{}", self.vendor, self.component, self.version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Program
|
|
||||||
|
|
||||||
// /// The struct representing a complete Win32 program.
|
|
||||||
// pub struct Program {
|
|
||||||
// file_exts: Vec<FileExt>,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl Program {
|
|
||||||
// /// Create a program descriptor.
|
|
||||||
// pub fn new() -> Self {
|
|
||||||
// Self {
|
|
||||||
// file_exts: Vec::new(),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl Program {
|
|
||||||
// /// Register program in this computer
|
|
||||||
// pub fn register(&self, kind: RegisterKind) -> Result<(), Error> {
|
|
||||||
// todo!("pretend to register >_<...")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// Unregister program from this computer.
|
|
||||||
// pub fn unregister(&self) -> Result<(), Error> {
|
|
||||||
// todo!("pretend to unregister >_<...")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl Program {
|
|
||||||
// /// Query file extension infos which this program want to associate with.
|
|
||||||
// pub fn query(&self) -> Result<(), Error> {
|
|
||||||
// todo!("pretend to query >_<...")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
Reference in New Issue
Block a user