1
0

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:
2025-10-29 10:31:00 +08:00
parent 3f1a070b65
commit a3456e9fdd
2 changed files with 227 additions and 26 deletions

View File

@ -1,6 +1,10 @@
//! The module including all struct representing Windows file association concept, //! The module including all struct representing Windows file association concept,
//! like file extension, ProgId, CLSID and etc. //! 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::fmt::Display;
use std::str::FromStr; use std::str::FromStr;
use thiserror::Error as TeError; use thiserror::Error as TeError;
@ -8,15 +12,18 @@ use winreg::RegKey;
use winreg::enums::{ use winreg::enums::{
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE, 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 // region: Error Types
/// All possible error occurs in this crate. /// 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}")]
ParseExt(#[from] crate::extra::windows::ParseExtError),
#[error("{0}")]
ParseProgId(#[from] crate::extra::windows::ParseProgIdError),
} }
/// The result type used in this crate. /// The result type used in this crate.
@ -102,16 +109,113 @@ impl From<Scope> for View {
// endregion // endregion
// region ProgId Kind // region: ProgId Kind
/// The variant of ProgId for the compatibility /// The variant of ProgId for the compatibility
/// with those software which do not follow Microsoft suggestions. /// with those software which do not follow Microsoft suggestions.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum ProgIdKind { pub enum ProgIdKind {
/// Other ProgId which not follow Microsoft standards. /// Other ProgId which not follow Microsoft standards.
Other(String), Other(String),
/// Standard ProgId. /// 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 // endregion
@ -125,39 +229,89 @@ pub struct ExtKey {
ext: Ext, 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 { impl ExtKey {
/// Set the default "Open With" of this file extension to given ProgId. /// Set the default "Open With" of this file extension to given ProgId.
pub fn link_prog_id(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> { pub fn link(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
todo!() // 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. /// Reset the default "Open With" of this file extension to blank.
/// ///
/// If the default "Open With" of this file extension is not given ProgId, /// If the default "Open With" of this file extension is not given ProgId,
/// or there is no such file extension, this function do nothing. /// or there is no such file extension, this function do nothing.
pub fn unlink_prog_id(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> { pub fn unlink(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
todo!() 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. /// Query the default "Open With" of this file extension associated ProgId.
/// ///
/// This function will return its associated ProgId if "Open With" was set. /// This function will return its associated ProgId if "Open With" was set.
pub fn query_prog_id(&self, view: View) -> Result<Option<ProgIdKey>> { pub fn query(&self, view: View) -> Result<Option<ProgIdKey>> {
todo!() 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 { 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 // endregion

View File

@ -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 // endregion
// region: Expand String // region: Expand String