From a3456e9fddbaec737edc406a62c913255224518c Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Wed, 29 Oct 2025 10:31:00 +0800 Subject: [PATCH] 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 --- wfassoc/src/assoc.rs | 242 ++++++++++++++++++++++++++++++++---- wfassoc/src/extra/winreg.rs | 11 ++ 2 files changed, 227 insertions(+), 26 deletions(-) diff --git a/wfassoc/src/assoc.rs b/wfassoc/src/assoc.rs index c91dbcb..f457380 100644 --- a/wfassoc/src/assoc.rs +++ b/wfassoc/src/assoc.rs @@ -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 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 { + Ok(match s.parse::() { + 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 { + 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 { + 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 { + // 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 { + // 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 { - todo!() - } - - fn open_ext_key(&self) -> Result { - todo!() - } - - fn try_open_ext_key(&self) -> Result> { - 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::(&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> { - todo!() + pub fn query(&self, view: View) -> Result> { + 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::(&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 = ::Err; + + fn from_str(s: &str) -> std::result::Result { + 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 = ::Err; + + fn from_str(s: &str) -> std::result::Result { + Ok(Self { + prog_id: ProgIdKind::from_str(s)?, + }) + } +} // endregion diff --git a/wfassoc/src/extra/winreg.rs b/wfassoc/src/extra/winreg.rs index 95c9c6f..3f96d43 100644 --- a/wfassoc/src/extra/winreg.rs +++ b/wfassoc/src/extra/winreg.rs @@ -59,6 +59,17 @@ pub fn try_get_value>( } } +/// 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>(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