refactor: seperate highlevel into 2 individual files
This commit is contained in:
@@ -65,7 +65,7 @@ fn stringified_exts_to_indices(
|
||||
|
||||
// If star is present alone, return fixed list from zero to the maximum ext index.
|
||||
if has_star {
|
||||
return Ok((0..program.get_ext_count()).collect());
|
||||
return Ok((0..program.exts_len()).collect());
|
||||
}
|
||||
|
||||
// Convert each extension name to index using program.find_ext()
|
||||
@@ -138,7 +138,7 @@ fn run_ext_list(
|
||||
) -> Result<()> {
|
||||
// Fetch info
|
||||
let mut ext_list: HashMap<String, Option<String>> = HashMap::new();
|
||||
for index in 0..program.get_ext_count() {
|
||||
for index in 0..program.exts_len() {
|
||||
let ext = program.get_ext(index)?;
|
||||
let status = program.query_ext(view, index)?;
|
||||
ext_list.insert(ext.dotted_inner(), status.map(|s| s.get_name().to_string()));
|
||||
|
||||
@@ -1,61 +1,4 @@
|
||||
use crate::{
|
||||
lowlevel, utilities,
|
||||
win32::{self, concept},
|
||||
};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
pub use lowlevel::{Scope, View};
|
||||
|
||||
// region: Error Type
|
||||
|
||||
/// Error occurs when operating with [Schema].
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum SchemaError {
|
||||
#[error("duplicate key: {0}")]
|
||||
DuplicateKey(String),
|
||||
}
|
||||
|
||||
/// Error occurs when trying converting [Schema] into [Program].
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum ParseProgramError {
|
||||
#[error("{0}")]
|
||||
BadExtBody(#[from] concept::BadExtBodyError),
|
||||
#[error("{0}")]
|
||||
BadProgIdPart(#[from] concept::BadProgIdPartError),
|
||||
#[error("{0}")]
|
||||
BadFileName(#[from] concept::BadFileNameError),
|
||||
#[error("{0}")]
|
||||
ParseCmdLine(#[from] concept::ParseCmdLineError),
|
||||
#[error("{0}")]
|
||||
CastOsStr(#[from] utilities::CastOsStrError),
|
||||
#[error("given path doesn't has legal file name part")]
|
||||
NoFileNamePart,
|
||||
#[error("given path doesn't has legal directory part")]
|
||||
NoDirNamePart,
|
||||
#[error("given identifier is not presented in dict")]
|
||||
NoSuchIdentifier,
|
||||
#[error("extension name should not be empty")]
|
||||
EmptyExtension,
|
||||
#[error("given program identifier is not allowed")]
|
||||
BadIdentifier,
|
||||
}
|
||||
|
||||
/// Error occurs when operating with [Program].
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum ProgramError {
|
||||
#[error("{0}")]
|
||||
Lowlevel(#[from] lowlevel::Error),
|
||||
#[error("given index is invalid")]
|
||||
BadIndex,
|
||||
}
|
||||
|
||||
// endregion
|
||||
use crate::lowlevel;
|
||||
|
||||
// region: Utilities
|
||||
|
||||
@@ -78,712 +21,13 @@ macro_rules! debug_println {
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Schema
|
||||
// region: Exposed Stuff
|
||||
|
||||
// region: Schema Body
|
||||
mod schema;
|
||||
mod program;
|
||||
|
||||
/// The sketchpad of complete [Program].
|
||||
///
|
||||
/// In suggested usage, we will create a [Schema] first,
|
||||
/// fill some essential and optional properties,
|
||||
/// then add file extensions which we need.
|
||||
/// And finally convert it into immutable [Program] for formal using.
|
||||
#[derive(Debug)]
|
||||
pub struct Schema {
|
||||
identifier: String,
|
||||
path: String,
|
||||
clsid: String,
|
||||
|
||||
name: Option<String>,
|
||||
icon: Option<String>,
|
||||
behavior: Option<String>,
|
||||
|
||||
strs: HashMap<String, String>,
|
||||
icons: HashMap<String, String>,
|
||||
behaviors: HashMap<String, String>,
|
||||
exts: HashMap<String, SchemaExt>,
|
||||
}
|
||||
|
||||
impl Schema {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
identifier: String::new(),
|
||||
path: String::new(),
|
||||
clsid: String::new(),
|
||||
name: None,
|
||||
icon: None,
|
||||
behavior: None,
|
||||
strs: HashMap::new(),
|
||||
icons: HashMap::new(),
|
||||
behaviors: HashMap::new(),
|
||||
exts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the identifier of the schema.
|
||||
///
|
||||
/// The identifier is used to build ProgId.
|
||||
/// So it should starts with an ASCII letter and followed by zero or more ASCII letters, digits, underline and hyphen.
|
||||
/// And it should not be empty.
|
||||
pub fn set_identifier(&mut self, identifier: &str) -> () {
|
||||
self.identifier = identifier.to_string();
|
||||
}
|
||||
|
||||
/// Set the absolute path to the executable file.
|
||||
pub fn set_path(&mut self, exe_path: &str) -> () {
|
||||
self.path = exe_path.to_string();
|
||||
}
|
||||
|
||||
pub fn set_clsid(&mut self, clsid: &str) -> () {
|
||||
self.clsid = clsid.to_string();
|
||||
}
|
||||
|
||||
pub fn set_name(&mut self, name: Option<&str>) -> () {
|
||||
self.name = name.map(|n| n.to_string());
|
||||
}
|
||||
|
||||
pub fn set_icon(&mut self, icon: Option<&str>) -> () {
|
||||
self.icon = icon.map(|i| i.to_string());
|
||||
}
|
||||
|
||||
pub fn set_behavior(&mut self, behavior: Option<&str>) -> () {
|
||||
self.behavior = behavior.map(|b| b.to_string());
|
||||
}
|
||||
|
||||
pub fn add_str(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
|
||||
match self.strs.insert(name.to_string(), value.to_string()) {
|
||||
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_icon(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
|
||||
match self.icons.insert(name.to_string(), value.to_string()) {
|
||||
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
|
||||
match self.behaviors.insert(name.to_string(), value.to_string()) {
|
||||
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a file extension to the schema.
|
||||
///
|
||||
/// The parameter `ext` is the file extension without leading dot `.`.
|
||||
pub fn add_ext(
|
||||
&mut self,
|
||||
ext: &str,
|
||||
ext_name: &str,
|
||||
ext_icon: &str,
|
||||
ext_behavior: &str,
|
||||
) -> Result<(), SchemaError> {
|
||||
match self.exts.insert(
|
||||
ext.to_string(),
|
||||
SchemaExt::new(ext_name, ext_icon, ext_behavior),
|
||||
) {
|
||||
Some(_) => Err(SchemaError::DuplicateKey(ext.to_string())),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try converting [Schema] into [Program].
|
||||
///
|
||||
/// This is equivalent to [Program::new].
|
||||
pub fn into_program(self) -> Result<Program, ParseProgramError> {
|
||||
Program::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Schema Internals
|
||||
|
||||
/// Internal used struct as the Schema file extensions hashmap value type.
|
||||
#[derive(Debug)]
|
||||
struct SchemaExt {
|
||||
name: String,
|
||||
icon: String,
|
||||
behavior: String,
|
||||
}
|
||||
|
||||
impl SchemaExt {
|
||||
fn new(name: &str, icon: &str, behavior: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
icon: icon.to_string(),
|
||||
behavior: behavior.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Program
|
||||
|
||||
// region: Program Body
|
||||
|
||||
/// Program is a complete and immutable program representer
|
||||
pub struct Program {
|
||||
app_paths_key: lowlevel::AppPathsKey,
|
||||
applications_key: lowlevel::ApplicationsKey,
|
||||
app_path: String,
|
||||
app_dir_path: String,
|
||||
name: Option<Arc<ProgramStr>>,
|
||||
icon: Option<Arc<ProgramIcon>>,
|
||||
behavior: Option<Arc<ProgramBehavior>>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
strs: Vec<Arc<ProgramStr>>,
|
||||
#[allow(dead_code)]
|
||||
icons: Vec<Arc<ProgramIcon>>,
|
||||
#[allow(dead_code)]
|
||||
behaviors: Vec<Arc<ProgramBehavior>>,
|
||||
|
||||
ext_keys: Vec<ProgramProgIdExtKey>,
|
||||
ext_keys_map: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl TryFrom<Schema> for Program {
|
||||
type Error = ParseProgramError;
|
||||
|
||||
fn try_from(value: Schema) -> Result<Self, Self::Error> {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
/// Extract the file name part from full path to application,
|
||||
/// which was used in Registry path component.
|
||||
fn extract_file_name(full_path: &Path) -> Result<&OsStr, ParseProgramError> {
|
||||
full_path
|
||||
.file_name()
|
||||
.ok_or(ParseProgramError::NoFileNamePart)
|
||||
}
|
||||
|
||||
/// Extract the start in path from full path to application,
|
||||
/// which basically is the stem of full path.
|
||||
fn extract_dir_path(full_path: &Path) -> Result<&OsStr, ParseProgramError> {
|
||||
full_path
|
||||
.parent()
|
||||
.map(|p| p.as_os_str())
|
||||
.ok_or(ParseProgramError::NoDirNamePart)
|
||||
}
|
||||
|
||||
fn flat_hashmap<V, U, F>(
|
||||
hashmap: &HashMap<String, V>,
|
||||
f: F,
|
||||
) -> Result<(Vec<U>, HashMap<String, usize>), ParseProgramError>
|
||||
where
|
||||
F: Fn(&V) -> Result<U, ParseProgramError>,
|
||||
{
|
||||
let mut indexmap: HashMap<String, usize> = HashMap::with_capacity(hashmap.len());
|
||||
let mut vector: Vec<U> = Vec::with_capacity(hashmap.len());
|
||||
for (key, value) in hashmap.into_iter() {
|
||||
indexmap.insert(key.clone(), vector.len());
|
||||
vector.push(f(value)?);
|
||||
}
|
||||
Ok((vector, indexmap))
|
||||
}
|
||||
|
||||
fn resolve_index<T>(
|
||||
key: &String,
|
||||
vector: &Vec<Arc<T>>,
|
||||
index_map: &HashMap<String, usize>,
|
||||
) -> Result<Arc<T>, ParseProgramError> {
|
||||
match index_map.get(key) {
|
||||
Some(index) => Ok(vector
|
||||
.get(*index)
|
||||
.expect("unexpected invalid index")
|
||||
.clone()),
|
||||
None => Err(ParseProgramError::NoSuchIdentifier),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build ProgId from identifier and given file extension.
|
||||
fn build_progid(
|
||||
identifier: &str,
|
||||
ext: &str,
|
||||
) -> Result<lowlevel::LosseProgId, ParseProgramError> {
|
||||
// Use Regex to check identifier
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^[a-zA-Z][a-zA-Z0-9_-]*$").expect("unexpected bad regex pattern string")
|
||||
});
|
||||
let identifier = match RE.captures(identifier) {
|
||||
Some(_) => identifier,
|
||||
None => return Err(ParseProgramError::BadIdentifier),
|
||||
};
|
||||
|
||||
// Capitalize first ASCII of ext
|
||||
let ext = utilities::capitalize_first_ascii(ext);
|
||||
|
||||
// Build strict ProgId
|
||||
let progid = concept::ProgId::new(identifier, &ext, None)?;
|
||||
// Then build losse ProgId
|
||||
let losse_progid: lowlevel::LosseProgId = progid.into();
|
||||
// Return built result
|
||||
Ok(losse_progid)
|
||||
}
|
||||
|
||||
/// Try converting [Schema] into [Program].
|
||||
///
|
||||
/// During this process, some checks will be performed to ensure the validity of the data.
|
||||
/// For example, the reference to icon, name, or behavior must exist in their respective dictionaries.
|
||||
/// The identifier must be suit for building ProgId.
|
||||
pub fn new(schema: Schema) -> Result<Self, ParseProgramError> {
|
||||
// Extract file name part and directory name part respectively.
|
||||
let schema_path = Path::new(&schema.path);
|
||||
let app_path = schema.path.clone();
|
||||
let app_file_name = Self::extract_file_name(schema_path)?;
|
||||
let app_file_name = String::from(utilities::osstr_to_str(app_file_name)?);
|
||||
let app_dir_path = Self::extract_dir_path(schema_path)?;
|
||||
let app_dir_path = String::from(utilities::osstr_to_str(app_dir_path)?);
|
||||
// Build app paths key and applications key respectively
|
||||
let key = concept::FileName::new(&app_file_name)?;
|
||||
let app_paths_key = lowlevel::AppPathsKey::new(key.clone());
|
||||
let applications_key = lowlevel::ApplicationsKey::new(key.clone());
|
||||
|
||||
// Build string, icon and behavior list,
|
||||
// and build mapper at the same time.
|
||||
let (strs, strs_index_map) = Self::flat_hashmap(&schema.strs, |entry| {
|
||||
let str_res_variant: lowlevel::StrResVariant = entry.as_str().into();
|
||||
let program_str = ProgramStr {
|
||||
inner: str_res_variant,
|
||||
};
|
||||
Ok(Arc::new(program_str))
|
||||
})?;
|
||||
let (icons, icons_index_map) = Self::flat_hashmap(&schema.icons, |entry| {
|
||||
let icon_res_variant: lowlevel::IconResVariant = entry.as_str().into();
|
||||
let program_icon = ProgramIcon {
|
||||
inner: icon_res_variant,
|
||||
};
|
||||
Ok(Arc::new(program_icon))
|
||||
})?;
|
||||
let (behaviors, behaviors_index_map) = Self::flat_hashmap(&schema.behaviors, |entry| {
|
||||
// We simply always use "Open" verb.
|
||||
let cmdline: concept::CmdLine = entry.as_str().parse()?;
|
||||
let verb = concept::Verb::OPEN();
|
||||
let shell_verb = lowlevel::ShellVerb::new(verb, cmdline);
|
||||
let program_behavior = ProgramBehavior { inner: shell_verb };
|
||||
Ok(Arc::new(program_behavior))
|
||||
})?;
|
||||
|
||||
// Setup default name, icon and behavior
|
||||
let name = schema
|
||||
.name
|
||||
.map(|name| Self::resolve_index(&name, &strs, &strs_index_map))
|
||||
.transpose()?;
|
||||
let icon = schema
|
||||
.icon
|
||||
.map(|icon| Self::resolve_index(&icon, &icons, &icons_index_map))
|
||||
.transpose()?;
|
||||
let behavior = schema
|
||||
.behavior
|
||||
.map(|behavior| Self::resolve_index(&behavior, &behaviors, &behaviors_index_map))
|
||||
.transpose()?;
|
||||
|
||||
// We build ext keys
|
||||
let mut ext_keys: Vec<ProgramProgIdExtKey> = Vec::with_capacity(schema.exts.len());
|
||||
for (key, value) in &schema.exts {
|
||||
// Build ProgId first.
|
||||
let progid = Self::build_progid(&schema.identifier, key.as_str())?;
|
||||
// Then build ProgId key.
|
||||
let progid_key = lowlevel::ProgIdKey::new(progid);
|
||||
|
||||
// Build essential fields for program ProgId key struct.
|
||||
let name = Self::resolve_index(&value.name, &strs, &strs_index_map)?;
|
||||
let icon = Self::resolve_index(&value.icon, &icons, &icons_index_map)?;
|
||||
let behavior = Self::resolve_index(&value.behavior, &behaviors, &behaviors_index_map)?;
|
||||
|
||||
// Build Ext.
|
||||
let ext = concept::Ext::new(key.as_str())?;
|
||||
let ext_key = lowlevel::ExtKey::new(ext);
|
||||
|
||||
// Create program ProgId Ext key struct
|
||||
let progid_ext_key = ProgramProgIdExtKey {
|
||||
ext_key,
|
||||
progid_key,
|
||||
name,
|
||||
icon,
|
||||
behavior,
|
||||
};
|
||||
|
||||
// Add them into list
|
||||
ext_keys.push(progid_ext_key);
|
||||
}
|
||||
// The build ext keys map
|
||||
let ext_keys_map = ext_keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, ext)| (ext.ext_key.inner().inner().to_string(), i))
|
||||
.collect();
|
||||
|
||||
// Everything is okey
|
||||
Ok(Self {
|
||||
app_paths_key,
|
||||
applications_key,
|
||||
app_path,
|
||||
app_dir_path,
|
||||
name,
|
||||
icon,
|
||||
behavior,
|
||||
strs,
|
||||
icons,
|
||||
behaviors,
|
||||
ext_keys,
|
||||
ext_keys_map,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
pub fn resolve_name(&self) -> Result<String, ProgramError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn resolve_icon(&self) -> Result<concept::IconRc, ProgramError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn get_ext_count(&self) -> usize {
|
||||
self.ext_keys.len()
|
||||
}
|
||||
|
||||
pub fn get_ext(&self, index: usize) -> Result<&concept::Ext, ProgramError> {
|
||||
match self.ext_keys.get(index) {
|
||||
Some(program_key) => {
|
||||
let ext_key = &program_key.ext_key;
|
||||
Ok(ext_key.inner())
|
||||
}
|
||||
None => Err(ProgramError::BadIndex),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_ext(&self, body: &str) -> Option<usize> {
|
||||
self.ext_keys_map.get(body).copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
/// Register this application.
|
||||
///
|
||||
/// If there is complete or partial registration of this application
|
||||
/// (partial registration may occurs when registration failed),
|
||||
/// this function does nothing.
|
||||
pub fn register(&mut self, scope: Scope) -> Result<(), ProgramError> {
|
||||
// Create App Paths subkey
|
||||
debug_println!("Adding App Paths subkey...");
|
||||
self.app_paths_key.ensure(scope)?;
|
||||
// Write App Paths values
|
||||
self.app_paths_key.set_default(scope, &self.app_path)?;
|
||||
self.app_paths_key.set_path(scope, &self.app_dir_path)?;
|
||||
|
||||
// Create Applications subkey
|
||||
debug_println!("Adding Applications subkey...");
|
||||
self.applications_key.ensure(scope)?;
|
||||
// Write Applications values
|
||||
self.applications_key
|
||||
.set_shell_verb(scope, self.behavior.as_ref().map(|beh| &beh.inner))?;
|
||||
self.applications_key
|
||||
.set_default_icon(scope, self.icon.as_ref().map(|ico| &ico.inner))?;
|
||||
self.applications_key
|
||||
.set_friendly_app_name(scope, self.name.as_ref().map(|name| &name.inner))?;
|
||||
let exts: Vec<&concept::Ext> = self
|
||||
.ext_keys
|
||||
.iter()
|
||||
.map(|key| key.ext_key.inner())
|
||||
.collect();
|
||||
self.applications_key
|
||||
.set_supported_types(scope, Some(&exts))?;
|
||||
self.applications_key.set_no_open_with(scope, true)?;
|
||||
|
||||
// Create ProgId subkeys one by one
|
||||
debug_println!("Adding ProgId subkey...");
|
||||
for program_key in &mut self.ext_keys {
|
||||
let progid_key = &mut program_key.progid_key;
|
||||
debug_println!(
|
||||
"Adding ProgId \"{0}\" subkey...",
|
||||
progid_key.inner().to_string()
|
||||
);
|
||||
|
||||
// Create ProgId subkey
|
||||
progid_key.ensure(scope)?;
|
||||
// Write ProgId values
|
||||
let name = Some(&program_key.name.inner);
|
||||
progid_key.set_default(scope, name)?;
|
||||
progid_key.set_shell_verb(scope, Some(&program_key.behavior.inner))?;
|
||||
progid_key.set_friendly_type_name(scope, name)?;
|
||||
progid_key.set_default_icon(scope, Some(&program_key.icon.inner))?;
|
||||
|
||||
// Add this progid to file extension "open with" list.
|
||||
let ext_key = &mut program_key.ext_key;
|
||||
ext_key.add_into_open_with_progids(scope, progid_key.inner())?;
|
||||
}
|
||||
|
||||
// Everything is okey.
|
||||
// Notify changes and return
|
||||
win32::utilities::notify_assoc_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unregister this application.
|
||||
///
|
||||
/// No matter whether there is registration of this application,
|
||||
/// this function always make sure that there is no registration after running this function.
|
||||
pub fn unregister(&mut self, scope: Scope) -> Result<(), ProgramError> {
|
||||
// Delete App Paths subkey
|
||||
debug_println!("Deleting App Paths subkey...");
|
||||
self.app_paths_key.delete(scope)?;
|
||||
|
||||
// Delete Applications subkey
|
||||
debug_println!("Deleting Applications subkey...");
|
||||
self.applications_key.delete(scope)?;
|
||||
|
||||
// Delete ProgId subkeys one by one.
|
||||
debug_println!("Adding ProgId subkey...");
|
||||
for program_key in &mut self.ext_keys {
|
||||
let progid_key = &mut program_key.progid_key;
|
||||
debug_println!(
|
||||
"Deleting ProgId \"{0}\" subkey...",
|
||||
progid_key.inner().to_string()
|
||||
);
|
||||
|
||||
// YYC MARK:
|
||||
// According to Microsoft document, when uninstalling application,
|
||||
// there is no need to reset the default open way of file extension.
|
||||
// So we simply remove it from "open with" list.
|
||||
|
||||
// Remove this ProgId from file extension "open with" list.
|
||||
let ext_key = &mut program_key.ext_key;
|
||||
ext_key.remove_from_open_with_progids(scope, progid_key.inner())?;
|
||||
|
||||
// Delete ProgId subkey
|
||||
progid_key.delete(scope)?;
|
||||
}
|
||||
|
||||
// Everything is okey.
|
||||
// Notify changes and return
|
||||
win32::utilities::notify_assoc_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check whether this application has been registered in given view.
|
||||
///
|
||||
/// Please note that this is a rough check and do not validate any data.
|
||||
///
|
||||
/// The return value only ensures the pre-requirement of `register` and `unregister`.
|
||||
pub fn is_registered(&self, scope: Scope) -> Result<bool, ProgramError> {
|
||||
// Check App Paths subkey.
|
||||
debug_println!("Checking App Paths subkey...");
|
||||
if !self.app_paths_key.is_exist(scope)? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check Application subkey.
|
||||
debug_println!("Checking Applications subkey...");
|
||||
if !self.applications_key.is_exist(scope.into())? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check ProgId subkey.
|
||||
debug_println!("Checking ProgId subkey...");
|
||||
for program_key in &self.ext_keys {
|
||||
let progid_key = &program_key.progid_key;
|
||||
debug_println!(
|
||||
"Checking ProgId \"{0}\" subkey...",
|
||||
progid_key.inner().to_string()
|
||||
);
|
||||
|
||||
if !progid_key.is_exist(scope.into())? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Every subkeys are roughly existing.
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn link_ext(&mut self, scope: Scope, index: usize) -> Result<(), ProgramError> {
|
||||
match self.ext_keys.get_mut(index) {
|
||||
Some(program_key) => {
|
||||
let ext_key = &mut program_key.ext_key;
|
||||
let progid_key = &program_key.progid_key;
|
||||
debug_println!(
|
||||
"Linking ProgId \"{0}\" to extension \"{1}\" subkey...",
|
||||
progid_key.inner().to_string(),
|
||||
ext_key.inner().to_string()
|
||||
);
|
||||
|
||||
// Before setting it, we must make sure this extension is existing
|
||||
ext_key.ensure(scope)?;
|
||||
ext_key.set_default(scope, Some(progid_key.inner()))?;
|
||||
}
|
||||
None => return Err(ProgramError::BadIndex),
|
||||
};
|
||||
|
||||
// Everything is okey.
|
||||
// Notify changes and return
|
||||
win32::utilities::notify_assoc_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unlink_ext(&mut self, scope: Scope, index: usize) -> Result<(), ProgramError> {
|
||||
match self.ext_keys.get_mut(index) {
|
||||
Some(program_key) => {
|
||||
let ext_key = &mut program_key.ext_key;
|
||||
debug_println!(
|
||||
"Unlinking for extension \"{0}\" subkey...",
|
||||
ext_key.inner().to_string()
|
||||
);
|
||||
|
||||
// Before setting it, we must make sure this extension is existing
|
||||
ext_key.ensure(scope)?;
|
||||
ext_key.set_default(scope, None)?;
|
||||
}
|
||||
None => return Err(ProgramError::BadIndex),
|
||||
}
|
||||
|
||||
// Everything is okey.
|
||||
// Notify changes and return
|
||||
win32::utilities::notify_assoc_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn query_ext(
|
||||
&self,
|
||||
view: View,
|
||||
index: usize,
|
||||
) -> Result<Option<ProgramExtStatus>, ProgramError> {
|
||||
match self.ext_keys.get(index) {
|
||||
Some(program_key) => {
|
||||
let ext_key = &program_key.ext_key;
|
||||
debug_println!(
|
||||
"Querying for extension \"{0}\"subkey...",
|
||||
ext_key.inner().to_string()
|
||||
);
|
||||
|
||||
// If there is no such extension key, return None about this extension.
|
||||
if !ext_key.is_exist(view)? {
|
||||
return Ok(None);
|
||||
}
|
||||
// Let we fetch its associated default ProgId.
|
||||
// If there is no such key, return None instead.
|
||||
let progid = match ext_key.get_default(view)? {
|
||||
Some(progid) => progid,
|
||||
None => return Ok(None),
|
||||
};
|
||||
// Now we build ProgId key from gotten association
|
||||
let progid_key = lowlevel::ProgIdKey::new(progid);
|
||||
// If this associated ProgId key is not presented,
|
||||
// we return None instead.
|
||||
if !progid_key.is_exist(view)? {
|
||||
return Ok(None);
|
||||
}
|
||||
// Now try fetch its diaplay name in modern way first.
|
||||
// If there is no modern way, use legacy way instead.
|
||||
// If there is still no display name, use ProgId self instead as display name.
|
||||
let mut name: Option<String> = None;
|
||||
if let None = name {
|
||||
name = progid_key
|
||||
.get_friendly_type_name(view)?
|
||||
.map(|name| name.extract().ok())
|
||||
.flatten();
|
||||
}
|
||||
if let None = name {
|
||||
name = progid_key
|
||||
.get_default(view)?
|
||||
.map(|name| name.extract().ok())
|
||||
.flatten();
|
||||
}
|
||||
let name = name.unwrap_or(progid_key.inner().to_string());
|
||||
// Now try to fetch icon.
|
||||
let icon = progid_key
|
||||
.get_default_icon(view)?
|
||||
.map(|ico| ico.extract(concept::IconSizeKind::Small).ok())
|
||||
.flatten();
|
||||
|
||||
// Okey, return it.
|
||||
Ok(Some(ProgramExtStatus::new(name, icon)))
|
||||
}
|
||||
None => Err(ProgramError::BadIndex),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Program Exposed Structs
|
||||
|
||||
/// Exposed struct representing the default associated program of specific file extension.
|
||||
///
|
||||
/// The data including the diaplay name and icon.
|
||||
pub struct ProgramExtStatus {
|
||||
name: String,
|
||||
icon: Option<concept::IconRc>,
|
||||
}
|
||||
|
||||
impl ProgramExtStatus {
|
||||
fn new(name: String, icon: Option<concept::IconRc>) -> Self {
|
||||
Self { name, icon }
|
||||
}
|
||||
|
||||
/// Get the display name of this program.
|
||||
///
|
||||
/// The program provided display name will be used firstly.
|
||||
/// If this program has no display name, the stringified ProgId will be used instead.
|
||||
pub fn get_name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
/// Get the icon of this program.
|
||||
///
|
||||
/// Due to the icon is optional, if there is no icon, return None.
|
||||
pub fn get_icon(&self) -> Option<&concept::IconRc> {
|
||||
self.icon.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Program Internals
|
||||
|
||||
/// Internal used enum presenting a Program string resource.
|
||||
#[derive(Debug)]
|
||||
struct ProgramStr {
|
||||
inner: lowlevel::StrResVariant,
|
||||
}
|
||||
|
||||
/// Internal used enum presenting a Program icon resource.
|
||||
#[derive(Debug)]
|
||||
struct ProgramIcon {
|
||||
inner: lowlevel::IconResVariant,
|
||||
}
|
||||
|
||||
/// Internal used enum presenting a Program behavior (command line setups).
|
||||
#[derive(Debug)]
|
||||
struct ProgramBehavior {
|
||||
inner: lowlevel::ShellVerb,
|
||||
}
|
||||
|
||||
/// Internal used struct presenting a Program ProgId and associated file extension.
|
||||
///
|
||||
/// Another reason combine ProgId and Ext is that we can't operate ProgId in mutable mode,
|
||||
/// due to the internal immutable of Rc in Rust.
|
||||
#[derive(Debug)]
|
||||
struct ProgramProgIdExtKey {
|
||||
ext_key: lowlevel::ExtKey,
|
||||
|
||||
progid_key: lowlevel::ProgIdKey,
|
||||
name: Arc<ProgramStr>,
|
||||
icon: Arc<ProgramIcon>,
|
||||
behavior: Arc<ProgramBehavior>,
|
||||
}
|
||||
|
||||
// endregion
|
||||
pub use schema::{Schema, SchemaError};
|
||||
pub use program::{Program, ParseProgramError, ProgramError, ProgramExtStatus};
|
||||
pub use lowlevel::{Scope, View};
|
||||
|
||||
// endregion
|
||||
|
||||
613
wfassoc/src/highlevel/program.rs
Normal file
613
wfassoc/src/highlevel/program.rs
Normal file
@@ -0,0 +1,613 @@
|
||||
use super::Schema;
|
||||
use crate::{
|
||||
lowlevel::{self, Scope, View},
|
||||
utilities,
|
||||
win32::{self, concept},
|
||||
};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
// region: Error Type
|
||||
|
||||
/// Error occurs when trying converting [Schema] into [Program].
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum ParseProgramError {
|
||||
#[error("{0}")]
|
||||
BadExtBody(#[from] concept::BadExtBodyError),
|
||||
#[error("{0}")]
|
||||
BadProgIdPart(#[from] concept::BadProgIdPartError),
|
||||
#[error("{0}")]
|
||||
BadFileName(#[from] concept::BadFileNameError),
|
||||
#[error("{0}")]
|
||||
ParseCmdLine(#[from] concept::ParseCmdLineError),
|
||||
#[error("{0}")]
|
||||
CastOsStr(#[from] utilities::CastOsStrError),
|
||||
#[error("given path doesn't has legal file name part")]
|
||||
NoFileNamePart,
|
||||
#[error("given path doesn't has legal directory part")]
|
||||
NoDirNamePart,
|
||||
#[error("given identifier is not presented in dict")]
|
||||
NoSuchIdentifier,
|
||||
#[error("extension name should not be empty")]
|
||||
EmptyExtension,
|
||||
#[error("given program identifier is not allowed")]
|
||||
BadIdentifier,
|
||||
}
|
||||
|
||||
/// Error occurs when operating with [Program].
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum ProgramError {
|
||||
#[error("{0}")]
|
||||
Lowlevel(#[from] lowlevel::Error),
|
||||
#[error("given index is invalid")]
|
||||
BadIndex,
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Program
|
||||
|
||||
/// Program is a complete and immutable program representer
|
||||
pub struct Program {
|
||||
app_paths_key: lowlevel::AppPathsKey,
|
||||
applications_key: lowlevel::ApplicationsKey,
|
||||
app_path: String,
|
||||
app_dir_path: String,
|
||||
name: Option<Arc<ProgramStr>>,
|
||||
icon: Option<Arc<ProgramIcon>>,
|
||||
behavior: Option<Arc<ProgramBehavior>>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
strs: Vec<Arc<ProgramStr>>,
|
||||
#[allow(dead_code)]
|
||||
icons: Vec<Arc<ProgramIcon>>,
|
||||
#[allow(dead_code)]
|
||||
behaviors: Vec<Arc<ProgramBehavior>>,
|
||||
|
||||
ext_keys: Vec<ProgramProgIdExtKey>,
|
||||
ext_keys_map: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl TryFrom<Schema> for Program {
|
||||
type Error = ParseProgramError;
|
||||
|
||||
fn try_from(value: Schema) -> Result<Self, Self::Error> {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
/// Extract the file name part from full path to application,
|
||||
/// which was used in Registry path component.
|
||||
fn extract_file_name(full_path: &Path) -> Result<&OsStr, ParseProgramError> {
|
||||
full_path
|
||||
.file_name()
|
||||
.ok_or(ParseProgramError::NoFileNamePart)
|
||||
}
|
||||
|
||||
/// Extract the start in path from full path to application,
|
||||
/// which basically is the stem of full path.
|
||||
fn extract_dir_path(full_path: &Path) -> Result<&OsStr, ParseProgramError> {
|
||||
full_path
|
||||
.parent()
|
||||
.map(|p| p.as_os_str())
|
||||
.ok_or(ParseProgramError::NoDirNamePart)
|
||||
}
|
||||
|
||||
fn flat_hashmap<V, U, F>(
|
||||
hashmap: &HashMap<String, V>,
|
||||
f: F,
|
||||
) -> Result<(Vec<U>, HashMap<String, usize>), ParseProgramError>
|
||||
where
|
||||
F: Fn(&V) -> Result<U, ParseProgramError>,
|
||||
{
|
||||
let mut indexmap: HashMap<String, usize> = HashMap::with_capacity(hashmap.len());
|
||||
let mut vector: Vec<U> = Vec::with_capacity(hashmap.len());
|
||||
for (key, value) in hashmap.into_iter() {
|
||||
indexmap.insert(key.clone(), vector.len());
|
||||
vector.push(f(value)?);
|
||||
}
|
||||
Ok((vector, indexmap))
|
||||
}
|
||||
|
||||
fn resolve_index<T>(
|
||||
key: &str,
|
||||
vector: &Vec<Arc<T>>,
|
||||
index_map: &HashMap<String, usize>,
|
||||
) -> Result<Arc<T>, ParseProgramError> {
|
||||
match index_map.get(key) {
|
||||
Some(index) => Ok(vector
|
||||
.get(*index)
|
||||
.expect("unexpected invalid index")
|
||||
.clone()),
|
||||
None => Err(ParseProgramError::NoSuchIdentifier),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build ProgId from identifier and given file extension.
|
||||
fn build_progid(
|
||||
identifier: &str,
|
||||
ext: &str,
|
||||
) -> Result<lowlevel::LosseProgId, ParseProgramError> {
|
||||
// Use Regex to check identifier
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^[a-zA-Z][a-zA-Z0-9_-]*$").expect("unexpected bad regex pattern string")
|
||||
});
|
||||
let identifier = match RE.captures(identifier) {
|
||||
Some(_) => identifier,
|
||||
None => return Err(ParseProgramError::BadIdentifier),
|
||||
};
|
||||
|
||||
// Capitalize first ASCII of ext
|
||||
let ext = utilities::capitalize_first_ascii(ext);
|
||||
|
||||
// Build strict ProgId
|
||||
let progid = concept::ProgId::new(identifier, &ext, None)?;
|
||||
// Then build losse ProgId
|
||||
let losse_progid: lowlevel::LosseProgId = progid.into();
|
||||
// Return built result
|
||||
Ok(losse_progid)
|
||||
}
|
||||
|
||||
/// Try converting [Schema] into [Program].
|
||||
///
|
||||
/// During this process, some checks will be performed to ensure the validity of the data.
|
||||
/// For example, the reference to icon, name, or behavior must exist in their respective dictionaries.
|
||||
/// The identifier must be suit for building ProgId.
|
||||
pub fn new(schema: Schema) -> Result<Self, ParseProgramError> {
|
||||
// Extract file name part and directory name part respectively.
|
||||
let schema_path = Path::new(schema.get_path());
|
||||
let app_path = schema.get_path().to_string();
|
||||
let app_file_name = Self::extract_file_name(schema_path)?;
|
||||
let app_file_name = String::from(utilities::osstr_to_str(app_file_name)?);
|
||||
let app_dir_path = Self::extract_dir_path(schema_path)?;
|
||||
let app_dir_path = String::from(utilities::osstr_to_str(app_dir_path)?);
|
||||
// Build app paths key and applications key respectively
|
||||
let key = concept::FileName::new(&app_file_name)?;
|
||||
let app_paths_key = lowlevel::AppPathsKey::new(key.clone());
|
||||
let applications_key = lowlevel::ApplicationsKey::new(key.clone());
|
||||
|
||||
// Build string, icon and behavior list,
|
||||
// and build mapper at the same time.
|
||||
let (strs, strs_index_map) = Self::flat_hashmap(schema.get_strs(), |entry| {
|
||||
let str_res_variant: lowlevel::StrResVariant = entry.as_str().into();
|
||||
let program_str = ProgramStr {
|
||||
inner: str_res_variant,
|
||||
};
|
||||
Ok(Arc::new(program_str))
|
||||
})?;
|
||||
let (icons, icons_index_map) = Self::flat_hashmap(schema.get_icons(), |entry| {
|
||||
let icon_res_variant: lowlevel::IconResVariant = entry.as_str().into();
|
||||
let program_icon = ProgramIcon {
|
||||
inner: icon_res_variant,
|
||||
};
|
||||
Ok(Arc::new(program_icon))
|
||||
})?;
|
||||
let (behaviors, behaviors_index_map) =
|
||||
Self::flat_hashmap(schema.get_behaviors(), |entry| {
|
||||
// We simply always use "Open" verb.
|
||||
let cmdline: concept::CmdLine = entry.as_str().parse()?;
|
||||
let verb = concept::Verb::OPEN();
|
||||
let shell_verb = lowlevel::ShellVerb::new(verb, cmdline);
|
||||
let program_behavior = ProgramBehavior { inner: shell_verb };
|
||||
Ok(Arc::new(program_behavior))
|
||||
})?;
|
||||
|
||||
// Setup default name, icon and behavior
|
||||
let name = schema
|
||||
.get_name()
|
||||
.map(|name| Self::resolve_index(name, &strs, &strs_index_map))
|
||||
.transpose()?;
|
||||
let icon = schema
|
||||
.get_icon()
|
||||
.map(|icon| Self::resolve_index(icon, &icons, &icons_index_map))
|
||||
.transpose()?;
|
||||
let behavior = schema
|
||||
.get_behavior()
|
||||
.map(|behavior| Self::resolve_index(behavior, &behaviors, &behaviors_index_map))
|
||||
.transpose()?;
|
||||
|
||||
// We build ext keys
|
||||
let mut ext_keys: Vec<ProgramProgIdExtKey> = Vec::with_capacity(schema.get_exts().len());
|
||||
for (key, value) in schema.get_exts() {
|
||||
// Build ProgId first.
|
||||
let progid = Self::build_progid(schema.get_identifier(), key.as_str())?;
|
||||
// Then build ProgId key.
|
||||
let progid_key = lowlevel::ProgIdKey::new(progid);
|
||||
|
||||
// Build essential fields for program ProgId key struct.
|
||||
let name = Self::resolve_index(value.get_name(), &strs, &strs_index_map)?;
|
||||
let icon = Self::resolve_index(value.get_icon(), &icons, &icons_index_map)?;
|
||||
let behavior =
|
||||
Self::resolve_index(value.get_behavior(), &behaviors, &behaviors_index_map)?;
|
||||
|
||||
// Build Ext.
|
||||
let ext = concept::Ext::new(key.as_str())?;
|
||||
let ext_key = lowlevel::ExtKey::new(ext);
|
||||
|
||||
// Create program ProgId Ext key struct
|
||||
let progid_ext_key = ProgramProgIdExtKey {
|
||||
ext_key,
|
||||
progid_key,
|
||||
name,
|
||||
icon,
|
||||
behavior,
|
||||
};
|
||||
|
||||
// Add them into list
|
||||
ext_keys.push(progid_ext_key);
|
||||
}
|
||||
// The build ext keys map
|
||||
let ext_keys_map = ext_keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, ext)| (ext.ext_key.inner().inner().to_string(), i))
|
||||
.collect();
|
||||
|
||||
// Everything is okey
|
||||
Ok(Self {
|
||||
app_paths_key,
|
||||
applications_key,
|
||||
app_path,
|
||||
app_dir_path,
|
||||
name,
|
||||
icon,
|
||||
behavior,
|
||||
strs,
|
||||
icons,
|
||||
behaviors,
|
||||
ext_keys,
|
||||
ext_keys_map,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
pub fn resolve_name(&self) -> Result<String, ProgramError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn resolve_icon(&self) -> Result<concept::IconRc, ProgramError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn exts_len(&self) -> usize {
|
||||
self.ext_keys.len()
|
||||
}
|
||||
|
||||
pub fn get_ext(&self, index: usize) -> Result<&concept::Ext, ProgramError> {
|
||||
match self.ext_keys.get(index) {
|
||||
Some(program_key) => {
|
||||
let ext_key = &program_key.ext_key;
|
||||
Ok(ext_key.inner())
|
||||
}
|
||||
None => Err(ProgramError::BadIndex),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_ext(&self, body: &str) -> Option<usize> {
|
||||
self.ext_keys_map.get(body).copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
/// Register this application.
|
||||
///
|
||||
/// If there is complete or partial registration of this application
|
||||
/// (partial registration may occurs when registration failed),
|
||||
/// this function does nothing.
|
||||
pub fn register(&mut self, scope: Scope) -> Result<(), ProgramError> {
|
||||
// Create App Paths subkey
|
||||
debug_println!("Adding App Paths subkey...");
|
||||
self.app_paths_key.ensure(scope)?;
|
||||
// Write App Paths values
|
||||
self.app_paths_key.set_default(scope, &self.app_path)?;
|
||||
self.app_paths_key.set_path(scope, &self.app_dir_path)?;
|
||||
|
||||
// Create Applications subkey
|
||||
debug_println!("Adding Applications subkey...");
|
||||
self.applications_key.ensure(scope)?;
|
||||
// Write Applications values
|
||||
self.applications_key
|
||||
.set_shell_verb(scope, self.behavior.as_ref().map(|beh| &beh.inner))?;
|
||||
self.applications_key
|
||||
.set_default_icon(scope, self.icon.as_ref().map(|ico| &ico.inner))?;
|
||||
self.applications_key
|
||||
.set_friendly_app_name(scope, self.name.as_ref().map(|name| &name.inner))?;
|
||||
let exts: Vec<&concept::Ext> = self
|
||||
.ext_keys
|
||||
.iter()
|
||||
.map(|key| key.ext_key.inner())
|
||||
.collect();
|
||||
self.applications_key
|
||||
.set_supported_types(scope, Some(&exts))?;
|
||||
self.applications_key.set_no_open_with(scope, true)?;
|
||||
|
||||
// Create ProgId subkeys one by one
|
||||
debug_println!("Adding ProgId subkey...");
|
||||
for program_key in &mut self.ext_keys {
|
||||
let progid_key = &mut program_key.progid_key;
|
||||
debug_println!(
|
||||
"Adding ProgId \"{0}\" subkey...",
|
||||
progid_key.inner().to_string()
|
||||
);
|
||||
|
||||
// Create ProgId subkey
|
||||
progid_key.ensure(scope)?;
|
||||
// Write ProgId values
|
||||
let name = Some(&program_key.name.inner);
|
||||
progid_key.set_default(scope, name)?;
|
||||
progid_key.set_shell_verb(scope, Some(&program_key.behavior.inner))?;
|
||||
progid_key.set_friendly_type_name(scope, name)?;
|
||||
progid_key.set_default_icon(scope, Some(&program_key.icon.inner))?;
|
||||
|
||||
// Add this progid to file extension "open with" list.
|
||||
let ext_key = &mut program_key.ext_key;
|
||||
ext_key.add_into_open_with_progids(scope, progid_key.inner())?;
|
||||
}
|
||||
|
||||
// Everything is okey.
|
||||
// Notify changes and return
|
||||
win32::utilities::notify_assoc_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unregister this application.
|
||||
///
|
||||
/// No matter whether there is registration of this application,
|
||||
/// this function always make sure that there is no registration after running this function.
|
||||
pub fn unregister(&mut self, scope: Scope) -> Result<(), ProgramError> {
|
||||
// Delete App Paths subkey
|
||||
debug_println!("Deleting App Paths subkey...");
|
||||
self.app_paths_key.delete(scope)?;
|
||||
|
||||
// Delete Applications subkey
|
||||
debug_println!("Deleting Applications subkey...");
|
||||
self.applications_key.delete(scope)?;
|
||||
|
||||
// Delete ProgId subkeys one by one.
|
||||
debug_println!("Adding ProgId subkey...");
|
||||
for program_key in &mut self.ext_keys {
|
||||
let progid_key = &mut program_key.progid_key;
|
||||
debug_println!(
|
||||
"Deleting ProgId \"{0}\" subkey...",
|
||||
progid_key.inner().to_string()
|
||||
);
|
||||
|
||||
// YYC MARK:
|
||||
// According to Microsoft document, when uninstalling application,
|
||||
// there is no need to reset the default open way of file extension.
|
||||
// So we simply remove it from "open with" list.
|
||||
|
||||
// Remove this ProgId from file extension "open with" list.
|
||||
let ext_key = &mut program_key.ext_key;
|
||||
ext_key.remove_from_open_with_progids(scope, progid_key.inner())?;
|
||||
|
||||
// Delete ProgId subkey
|
||||
progid_key.delete(scope)?;
|
||||
}
|
||||
|
||||
// Everything is okey.
|
||||
// Notify changes and return
|
||||
win32::utilities::notify_assoc_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check whether this application has been registered in given view.
|
||||
///
|
||||
/// Please note that this is a rough check and do not validate any data.
|
||||
///
|
||||
/// The return value only ensures the pre-requirement of `register` and `unregister`.
|
||||
pub fn is_registered(&self, scope: Scope) -> Result<bool, ProgramError> {
|
||||
// Check App Paths subkey.
|
||||
debug_println!("Checking App Paths subkey...");
|
||||
if !self.app_paths_key.is_exist(scope)? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check Application subkey.
|
||||
debug_println!("Checking Applications subkey...");
|
||||
if !self.applications_key.is_exist(scope.into())? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check ProgId subkey.
|
||||
debug_println!("Checking ProgId subkey...");
|
||||
for program_key in &self.ext_keys {
|
||||
let progid_key = &program_key.progid_key;
|
||||
debug_println!(
|
||||
"Checking ProgId \"{0}\" subkey...",
|
||||
progid_key.inner().to_string()
|
||||
);
|
||||
|
||||
if !progid_key.is_exist(scope.into())? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Every subkeys are roughly existing.
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn link_ext(&mut self, scope: Scope, index: usize) -> Result<(), ProgramError> {
|
||||
match self.ext_keys.get_mut(index) {
|
||||
Some(program_key) => {
|
||||
let ext_key = &mut program_key.ext_key;
|
||||
let progid_key = &program_key.progid_key;
|
||||
debug_println!(
|
||||
"Linking ProgId \"{0}\" to extension \"{1}\" subkey...",
|
||||
progid_key.inner().to_string(),
|
||||
ext_key.inner().to_string()
|
||||
);
|
||||
|
||||
// Before setting it, we must make sure this extension is existing
|
||||
ext_key.ensure(scope)?;
|
||||
ext_key.set_default(scope, Some(progid_key.inner()))?;
|
||||
}
|
||||
None => return Err(ProgramError::BadIndex),
|
||||
};
|
||||
|
||||
// Everything is okey.
|
||||
// Notify changes and return
|
||||
win32::utilities::notify_assoc_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unlink_ext(&mut self, scope: Scope, index: usize) -> Result<(), ProgramError> {
|
||||
match self.ext_keys.get_mut(index) {
|
||||
Some(program_key) => {
|
||||
let ext_key = &mut program_key.ext_key;
|
||||
debug_println!(
|
||||
"Unlinking for extension \"{0}\" subkey...",
|
||||
ext_key.inner().to_string()
|
||||
);
|
||||
|
||||
// Before setting it, we must make sure this extension is existing
|
||||
ext_key.ensure(scope)?;
|
||||
ext_key.set_default(scope, None)?;
|
||||
}
|
||||
None => return Err(ProgramError::BadIndex),
|
||||
}
|
||||
|
||||
// Everything is okey.
|
||||
// Notify changes and return
|
||||
win32::utilities::notify_assoc_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn query_ext(
|
||||
&self,
|
||||
view: View,
|
||||
index: usize,
|
||||
) -> Result<Option<ProgramExtStatus>, ProgramError> {
|
||||
match self.ext_keys.get(index) {
|
||||
Some(program_key) => {
|
||||
let ext_key = &program_key.ext_key;
|
||||
debug_println!(
|
||||
"Querying for extension \"{0}\"subkey...",
|
||||
ext_key.inner().to_string()
|
||||
);
|
||||
|
||||
// If there is no such extension key, return None about this extension.
|
||||
if !ext_key.is_exist(view)? {
|
||||
return Ok(None);
|
||||
}
|
||||
// Let we fetch its associated default ProgId.
|
||||
// If there is no such key, return None instead.
|
||||
let progid = match ext_key.get_default(view)? {
|
||||
Some(progid) => progid,
|
||||
None => return Ok(None),
|
||||
};
|
||||
// Now we build ProgId key from gotten association
|
||||
let progid_key = lowlevel::ProgIdKey::new(progid);
|
||||
// If this associated ProgId key is not presented,
|
||||
// we return None instead.
|
||||
if !progid_key.is_exist(view)? {
|
||||
return Ok(None);
|
||||
}
|
||||
// Now try fetch its diaplay name in modern way first.
|
||||
// If there is no modern way, use legacy way instead.
|
||||
// If there is still no display name, use ProgId self instead as display name.
|
||||
let mut name: Option<String> = None;
|
||||
if let None = name {
|
||||
name = progid_key
|
||||
.get_friendly_type_name(view)?
|
||||
.map(|name| name.extract().ok())
|
||||
.flatten();
|
||||
}
|
||||
if let None = name {
|
||||
name = progid_key
|
||||
.get_default(view)?
|
||||
.map(|name| name.extract().ok())
|
||||
.flatten();
|
||||
}
|
||||
let name = name.unwrap_or(progid_key.inner().to_string());
|
||||
// Now try to fetch icon.
|
||||
let icon = progid_key
|
||||
.get_default_icon(view)?
|
||||
.map(|ico| ico.extract(concept::IconSizeKind::Small).ok())
|
||||
.flatten();
|
||||
|
||||
// Okey, return it.
|
||||
Ok(Some(ProgramExtStatus::new(name, icon)))
|
||||
}
|
||||
None => Err(ProgramError::BadIndex),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Internal Stuff
|
||||
|
||||
/// Internal used enum presenting a Program string resource.
|
||||
#[derive(Debug)]
|
||||
struct ProgramStr {
|
||||
inner: lowlevel::StrResVariant,
|
||||
}
|
||||
|
||||
/// Internal used enum presenting a Program icon resource.
|
||||
#[derive(Debug)]
|
||||
struct ProgramIcon {
|
||||
inner: lowlevel::IconResVariant,
|
||||
}
|
||||
|
||||
/// Internal used enum presenting a Program behavior (command line setups).
|
||||
#[derive(Debug)]
|
||||
struct ProgramBehavior {
|
||||
inner: lowlevel::ShellVerb,
|
||||
}
|
||||
|
||||
/// Internal used struct presenting a Program ProgId and associated file extension.
|
||||
///
|
||||
/// Another reason combine ProgId and Ext is that we can't operate ProgId in mutable mode,
|
||||
/// due to the internal immutable of Rc in Rust.
|
||||
#[derive(Debug)]
|
||||
struct ProgramProgIdExtKey {
|
||||
ext_key: lowlevel::ExtKey,
|
||||
|
||||
progid_key: lowlevel::ProgIdKey,
|
||||
name: Arc<ProgramStr>,
|
||||
icon: Arc<ProgramIcon>,
|
||||
behavior: Arc<ProgramBehavior>,
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Exposed Stuff
|
||||
|
||||
/// Exposed struct representing the default associated program of specific file extension.
|
||||
///
|
||||
/// The data including the diaplay name and icon.
|
||||
pub struct ProgramExtStatus {
|
||||
name: String,
|
||||
icon: Option<concept::IconRc>,
|
||||
}
|
||||
|
||||
impl ProgramExtStatus {
|
||||
fn new(name: String, icon: Option<concept::IconRc>) -> Self {
|
||||
Self { name, icon }
|
||||
}
|
||||
|
||||
/// Get the display name of this program.
|
||||
///
|
||||
/// The program provided display name will be used firstly.
|
||||
/// If this program has no display name, the stringified ProgId will be used instead.
|
||||
pub fn get_name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
/// Get the icon of this program.
|
||||
///
|
||||
/// Due to the icon is optional, if there is no icon, return None.
|
||||
pub fn get_icon(&self) -> Option<&concept::IconRc> {
|
||||
self.icon.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
215
wfassoc/src/highlevel/schema.rs
Normal file
215
wfassoc/src/highlevel/schema.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error as TeError;
|
||||
use super::{Program, ParseProgramError};
|
||||
|
||||
// region: Error Type
|
||||
|
||||
/// Error occurs when operating with [Schema].
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum SchemaError {
|
||||
#[error("duplicate key: {0}")]
|
||||
DuplicateKey(String),
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Schema
|
||||
|
||||
/// The sketchpad of complete [Program].
|
||||
///
|
||||
/// In suggested usage, we will create a [Schema] first,
|
||||
/// fill some essential and optional properties,
|
||||
/// then add file extensions which we need.
|
||||
/// And finally convert it into immutable [Program] for formal using.
|
||||
#[derive(Debug)]
|
||||
pub struct Schema {
|
||||
identifier: String,
|
||||
path: String,
|
||||
clsid: String,
|
||||
|
||||
name: Option<String>,
|
||||
icon: Option<String>,
|
||||
behavior: Option<String>,
|
||||
|
||||
strs: HashMap<String, String>,
|
||||
icons: HashMap<String, String>,
|
||||
behaviors: HashMap<String, String>,
|
||||
exts: HashMap<String, SchemaExt>,
|
||||
}
|
||||
|
||||
impl Schema {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
identifier: String::new(),
|
||||
path: String::new(),
|
||||
clsid: String::new(),
|
||||
name: None,
|
||||
icon: None,
|
||||
behavior: None,
|
||||
strs: HashMap::new(),
|
||||
icons: HashMap::new(),
|
||||
behaviors: HashMap::new(),
|
||||
exts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try converting [Schema] into [Program].
|
||||
///
|
||||
/// This is equivalent to [Program::new].
|
||||
pub fn into_program(self) -> Result<Program, ParseProgramError> {
|
||||
Program::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Schema {
|
||||
/// Set the identifier of the schema.
|
||||
///
|
||||
/// The identifier is used to build ProgId.
|
||||
/// So it should starts with an ASCII letter and followed by zero or more ASCII letters, digits, underline and hyphen.
|
||||
/// And it should not be empty.
|
||||
pub fn set_identifier(&mut self, identifier: &str) -> () {
|
||||
self.identifier = identifier.to_string();
|
||||
}
|
||||
|
||||
/// Set the absolute path to the executable file.
|
||||
pub fn set_path(&mut self, exe_path: &str) -> () {
|
||||
self.path = exe_path.to_string();
|
||||
}
|
||||
|
||||
pub fn set_clsid(&mut self, clsid: &str) -> () {
|
||||
self.clsid = clsid.to_string();
|
||||
}
|
||||
|
||||
pub fn set_name(&mut self, name: Option<&str>) -> () {
|
||||
self.name = name.map(|n| n.to_string());
|
||||
}
|
||||
|
||||
pub fn set_icon(&mut self, icon: Option<&str>) -> () {
|
||||
self.icon = icon.map(|i| i.to_string());
|
||||
}
|
||||
|
||||
pub fn set_behavior(&mut self, behavior: Option<&str>) -> () {
|
||||
self.behavior = behavior.map(|b| b.to_string());
|
||||
}
|
||||
|
||||
pub fn add_str(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
|
||||
match self.strs.insert(name.to_string(), value.to_string()) {
|
||||
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_icon(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
|
||||
match self.icons.insert(name.to_string(), value.to_string()) {
|
||||
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
|
||||
match self.behaviors.insert(name.to_string(), value.to_string()) {
|
||||
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a file extension to the schema.
|
||||
///
|
||||
/// The parameter `ext` is the file extension without leading dot `.`.
|
||||
pub fn add_ext(
|
||||
&mut self,
|
||||
ext: &str,
|
||||
ext_name: &str,
|
||||
ext_icon: &str,
|
||||
ext_behavior: &str,
|
||||
) -> Result<(), SchemaError> {
|
||||
match self.exts.insert(
|
||||
ext.to_string(),
|
||||
SchemaExt::new(ext_name, ext_icon, ext_behavior),
|
||||
) {
|
||||
Some(_) => Err(SchemaError::DuplicateKey(ext.to_string())),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Schema {
|
||||
pub(super) fn get_identifier(&self) -> &str {
|
||||
&self.identifier
|
||||
}
|
||||
|
||||
pub(super) fn get_path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub(super) fn get_clsid(&self) -> &str {
|
||||
&self.clsid
|
||||
}
|
||||
|
||||
pub(super) fn get_name(&self) -> Option<&str> {
|
||||
self.name.as_ref().map(|v| v.as_str())
|
||||
}
|
||||
|
||||
pub(super) fn get_icon(&self) -> Option<&str> {
|
||||
self.icon.as_ref().map(|v| v.as_str())
|
||||
}
|
||||
|
||||
pub(super) fn get_behavior(&self) -> Option<&str> {
|
||||
self.icon.as_ref().map(|v| v.as_str())
|
||||
}
|
||||
|
||||
pub(super) fn get_strs(&self) -> &HashMap<String, String> {
|
||||
&self.strs
|
||||
}
|
||||
|
||||
pub(super) fn get_icons(&self) -> &HashMap<String, String> {
|
||||
&self.icons
|
||||
}
|
||||
|
||||
pub(super) fn get_behaviors(&self) -> &HashMap<String, String> {
|
||||
&self.behaviors
|
||||
}
|
||||
|
||||
pub(super) fn get_exts(&self) -> &HashMap<String, SchemaExt> {
|
||||
&self.exts
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Internal Stuff
|
||||
|
||||
/// Internal used struct as the Schema file extensions hashmap value type.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SchemaExt {
|
||||
name: String,
|
||||
icon: String,
|
||||
behavior: String,
|
||||
}
|
||||
|
||||
impl SchemaExt {
|
||||
fn new(name: &str, icon: &str, behavior: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
icon: icon.to_string(),
|
||||
behavior: behavior.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SchemaExt {
|
||||
pub(super) fn get_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub(super) fn get_icon(&self) -> &str {
|
||||
&self.icon
|
||||
}
|
||||
|
||||
pub(super) fn get_behavior(&self) -> &str {
|
||||
&self.behavior
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -5,8 +5,6 @@ use crate::win32::{concept, regext};
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
|
||||
|
||||
// region: App Paths Key
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct AppPathsKey {
|
||||
key_name: concept::FileName,
|
||||
@@ -147,5 +145,3 @@ impl AppPathsKey {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -6,8 +6,6 @@ use crate::win32::{concept, regext};
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
|
||||
|
||||
// region: Applications Key
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ApplicationsKey {
|
||||
key_name: concept::FileName,
|
||||
@@ -327,5 +325,3 @@ impl ApplicationsKey {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -6,8 +6,6 @@ use crate::win32::{concept, regext};
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
|
||||
|
||||
// region: File Extension Key
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ExtKey {
|
||||
ext: concept::Ext,
|
||||
@@ -90,10 +88,12 @@ impl ExtKey {
|
||||
|
||||
// YYC MARK:
|
||||
// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#setting-optional-subkeys-and-file-type-extension-attributes
|
||||
//
|
||||
|
||||
// TODO:
|
||||
// We do not support "Content Type" and "PerceivedType"
|
||||
// because current interface are enough to use,
|
||||
// and these types has not been made as concept struct in Rust.
|
||||
// We may expand these in future.
|
||||
|
||||
fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
|
||||
self.open_view_for_read(view)?
|
||||
@@ -209,5 +209,3 @@ impl ExtKey {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -6,8 +6,6 @@ use crate::win32::regext;
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
|
||||
|
||||
// region: ProgId Key
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ProgIdKey {
|
||||
progid: LosseProgId,
|
||||
@@ -90,9 +88,11 @@ impl ProgIdKey {
|
||||
|
||||
// YYC MARK:
|
||||
// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids#programmatic-identifier-elements-used-by-file-associations
|
||||
//
|
||||
|
||||
// TODO:
|
||||
// Currently we only support (Default), FriendlyTypeName and DefaultIcon
|
||||
// to just cover the basic usage.
|
||||
// We may expand these in future.
|
||||
|
||||
fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
|
||||
self.open_view_for_read(view)?
|
||||
@@ -291,5 +291,3 @@ impl ProgIdKey {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
Reference in New Issue
Block a user