1
0

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
This commit is contained in:
2025-10-19 12:10:55 +08:00
parent 4f0f9670cb
commit f37b4b6652
2 changed files with 127 additions and 16 deletions

View File

@ -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<Token> {
// 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))
}
}

View File

@ -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<T>
where
T: Iterator<Item = char>,
{
is_first: bool,
iter: T,
}
impl<T> CapitalizeFirstAscii<T>
where
T: Iterator<Item = char>,
{
fn new(iter: T) -> Self {
Self {
is_first: false,
iter,
}
}
}
impl<T> Iterator for CapitalizeFirstAscii<T>
where
T: Iterator<Item = char>,
{
type Item = char;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|c| {
if self.is_first {
self.is_first = false;
c.to_ascii_uppercase()
} else {
c.to_ascii_lowercase()
}
})
}
}
impl<T> FusedIterator for CapitalizeFirstAscii<T> where T: Iterator<Item = char> + FusedIterator {}
pub fn capitalize_first_ascii(s: &str) -> String {
CapitalizeFirstAscii::new(s.chars()).collect()
}
// endregion