feat(windows): implement registry manipulation for file associations
- Add comprehensive error types for registry operations - Implement ProgIdKind enum with Display and FromStr traits - Create ApplicationVisitor and ClassesVisitor structs - Add ExtKey methods for linking, unlinking and querying ProgId - Implement ProgIdKey creation and deletion in registry - Add safe delete function to prevent accidental registry cleanup - Introduce Display and FromStr implementations for ExtKey and ProgIdKey - Organize registry access through scoped and viewed key opening - Enable setting default "Open With" verbs for file extensions - Support both user and system level registry modifications
This commit is contained in:
@ -1,6 +1,10 @@
|
||||
//! 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::convert::Infallible;
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error as TeError;
|
||||
@ -8,15 +12,18 @@ use winreg::RegKey;
|
||||
use winreg::enums::{
|
||||
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
|
||||
};
|
||||
use crate::extra::windows::{Ext, ProgId};
|
||||
use crate::utilities;
|
||||
|
||||
// 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),
|
||||
}
|
||||
|
||||
/// The result type used in this crate.
|
||||
@ -102,16 +109,113 @@ impl From<Scope> for View {
|
||||
|
||||
// endregion
|
||||
|
||||
// region ProgId Kind
|
||||
// region: ProgId Kind
|
||||
|
||||
/// The variant of ProgId for the compatibility
|
||||
/// with those software which do not follow Microsoft suggestions.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
enum ProgIdKind {
|
||||
pub enum ProgIdKind {
|
||||
/// Other ProgId which not follow Microsoft standards.
|
||||
Other(String),
|
||||
/// Standard ProgId.
|
||||
Std(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 = Infallible;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
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
|
||||
@ -125,39 +229,89 @@ pub struct ExtKey {
|
||||
ext: Ext,
|
||||
}
|
||||
|
||||
impl ExtKey {
|
||||
fn open_ext_parent_key(&self) -> Result<RegKey> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn open_ext_key(&self) -> Result<RegKey> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn try_open_ext_key(&self) -> Result<Option<RegKey>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtKey {
|
||||
/// Set the default "Open With" of this file extension to given ProgId.
|
||||
pub fn link_prog_id(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
|
||||
todo!()
|
||||
pub fn link(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
|
||||
// Open Classes key
|
||||
let classes = ClassesVisitor::open_with_scope(scope)?;
|
||||
|
||||
// Open or create this extension key
|
||||
let (subkey, _) = classes.create_subkey_with_flags(self.ext.to_string(), KEY_WRITE)?;
|
||||
// Set the default way to open this file extension
|
||||
subkey.set_value("", &prog_id.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_prog_id(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
|
||||
todo!()
|
||||
pub fn unlink(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
|
||||
use winreg_extra::{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, 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.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_prog_id(&self, view: View) -> Result<Option<ProgIdKey>> {
|
||||
todo!()
|
||||
pub fn query(&self, view: View) -> Result<Option<ProgIdKey>> {
|
||||
use winreg_extra::{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, self.ext.to_string(), KEY_READ)? {
|
||||
Some(subkey) => {
|
||||
// Try get associated ProgId if possible
|
||||
match try_get_value::<String, _>(&subkey, "")? {
|
||||
Some(value) => {
|
||||
Some(ProgIdKey::from_str(value.as_str()).expect("unexpected Infallable"))
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
// Okey
|
||||
Ok(rv)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ExtKey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.ext)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ExtKey {
|
||||
type Err = <Ext as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
Ok(Self {
|
||||
ext: Ext::from_str(s)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,8 +325,44 @@ pub struct ProgIdKey {
|
||||
}
|
||||
|
||||
impl ProgIdKey {
|
||||
/// Create ProgId into Registry in given scope with given parameters
|
||||
pub fn create(&self, scope: Scope, command: &str) -> Result<()> {
|
||||
let classes = ClassesVisitor::open_with_scope(scope)?;
|
||||
let (subkey, _) = classes.create_subkey_with_flags(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::safe_delete_subkey_all;
|
||||
|
||||
let classes = ClassesVisitor::open_with_scope(scope)?;
|
||||
safe_delete_subkey_all(&classes, self.prog_id.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ProgIdKey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.prog_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ProgIdKey {
|
||||
type Err = <ProgIdKind as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
Ok(Self {
|
||||
prog_id: ProgIdKind::from_str(s)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@ -59,6 +59,17 @@ pub fn try_get_value<T: FromRegValue, N: AsRef<OsStr>>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Passing empty string to `delete_subkey_all` may cause
|
||||
/// that the whole parent tree are deleted, not the subkey.
|
||||
/// So we create this "safe" function to prevent this horrible scenarios.
|
||||
pub fn safe_delete_subkey_all<P: AsRef<OsStr>>(regkey: &RegKey, path: P) -> std::io::Result<()> {
|
||||
if path.as_ref().is_empty() {
|
||||
Err(std::io::Error::other("dangerous Registry delete_subkey_all"))
|
||||
} else {
|
||||
regkey.delete_subkey_all(path)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Expand String
|
||||
|
||||
Reference in New Issue
Block a user