From f37b4b66528b29da642d8214a0eb5671944c141f Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Sun, 19 Oct 2025 12:10:55 +0800 Subject: [PATCH] feat(registry): implement file extension linking in Windows registry - Add `link_ext` and `unlink_ext` methods to Program for managing file associations - Introduce new error variants for invalid tokens - Add utility function `capitalize_first_ascii` for ProgId generation - Implement registry key operations for user and system scopes --- wfassoc/src/lib.rs | 94 +++++++++++++++++++++++++++++++++------- wfassoc/src/utilities.rs | 49 +++++++++++++++++++++ 2 files changed, 127 insertions(+), 16 deletions(-) diff --git a/wfassoc/src/lib.rs b/wfassoc/src/lib.rs index 333d476..3e161bd 100644 --- a/wfassoc/src/lib.rs +++ b/wfassoc/src/lib.rs @@ -42,8 +42,10 @@ pub enum Error { DupManner(String), #[error("file extension \"{0}\" is already registered")] DupExt(String), - #[error("the token of associated manner for file extension is invalid")] - InvalidAssocManner, + #[error("the token of manner is invalid")] + InvalidMannerToken, + #[error("the token of file extension is invalid")] + InvalidExtToken, } /// The result type used in this crate. @@ -194,7 +196,7 @@ impl Program { pub fn add_ext(&mut self, ext: &str, token: Token) -> Result { // Check manner token if let None = self.manners.get_index(token) { - return Err(Error::InvalidAssocManner); + return Err(Error::InvalidMannerToken); } // Create extension from string @@ -321,6 +323,74 @@ impl Program { } } +impl Program { + const CLASSES: &str = "Software\\Classes"; + + /// Set the default "open with" of given 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 extension. + /// + /// If the default "open with" of given extension is not our program, + /// 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. + let subkey = classes.open_subkey_with_flags(ext.to_string(), KEY_WRITE)?; + // Delete the default key. + subkey.delete_value("")?; + + // Okey + Ok(()) + } +} + impl Program { /// Extract the file name part from full path to application, /// which was used in Registry path component. @@ -344,20 +414,12 @@ impl Program { .and_then(|p| if p.is_empty() { None } else { Some(p) }) .ok_or(Error::BadFullAppPath) } -} -impl Program { - /// Set the default "open with" of given extension to this program. - pub fn link_ext(&self, ext: Token) -> Result<()> { - todo!() - } - - /// Remove this program from the default "open with" of given extension. - /// - /// If the default "open with" of given extension is not our program, - /// this function do nothing. - pub fn unlink_ext(&self, ext: Token) -> Result<()> { - todo!() + /// Build ProgId from identifier and given file extension. + fn build_prog_id(&self, ext: &assoc::Ext) -> assoc::ProgId { + let vendor = utilities::capitalize_first_ascii(&self.identifier); + let component = utilities::capitalize_first_ascii(ext.inner()); + assoc::ProgId::Loose(assoc::LosseProgId::new(&vendor, &component)) } } diff --git a/wfassoc/src/utilities.rs b/wfassoc/src/utilities.rs index f2ed2ad..7e4dfbc 100644 --- a/wfassoc/src/utilities.rs +++ b/wfassoc/src/utilities.rs @@ -1,6 +1,7 @@ //! The module containing useful stuff used in this crate. use std::ffi::OsStr; +use std::iter::FusedIterator; use std::path::Path; use thiserror::Error as TeError; @@ -117,3 +118,51 @@ pub fn osstr_to_str(osstr: &OsStr) -> Result<&str, CastOsStrError> { } // endregion + +// region: Capitalize First ASCII Letter + +struct CapitalizeFirstAscii +where + T: Iterator, +{ + is_first: bool, + iter: T, +} + +impl CapitalizeFirstAscii +where + T: Iterator, +{ + fn new(iter: T) -> Self { + Self { + is_first: false, + iter, + } + } +} + +impl Iterator for CapitalizeFirstAscii +where + T: Iterator, +{ + type Item = char; + + fn next(&mut self) -> Option { + self.iter.next().map(|c| { + if self.is_first { + self.is_first = false; + c.to_ascii_uppercase() + } else { + c.to_ascii_lowercase() + } + }) + } +} + +impl FusedIterator for CapitalizeFirstAscii where T: Iterator + FusedIterator {} + +pub fn capitalize_first_ascii(s: &str) -> String { + CapitalizeFirstAscii::new(s.chars()).collect() +} + +// endregion