Compare commits
3 Commits
119a4d0341
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 930a84a0f6 | |||
| 447d94fdd6 | |||
| f0bd2c0b73 |
@@ -1,5 +1,7 @@
|
||||
# WFassoc Core
|
||||
|
||||
## Usage
|
||||
|
||||
This crate provides low level API and high level API for manipulating Windows file associations at the same time.
|
||||
For the convenient use of this project, the root module of this crate re-expose high level API.
|
||||
So programmers can directly use them.
|
||||
@@ -10,3 +12,21 @@ Oppositely, for visiting low level API, please use `lowlevel` module.
|
||||
|
||||
If you are a programmer who want to take a deep into the internal implementations,
|
||||
see `win32` module and its submodules for detail.
|
||||
|
||||
## Test Notes
|
||||
|
||||
Some tests of this crate may be dangerous because they need to manipulate Windows Registry.
|
||||
So it is highlt recommend that run these tests in sandbox environment.
|
||||
In detailed words, you should run `cargo test --no-run` to build all test first,
|
||||
then fetch the path to executable tests according to this command shown on console.
|
||||
Then execute these executable tests in your sandbox for testing this crate.
|
||||
Additionally, some tests also need Administration permission for testing,
|
||||
because it requires write permission in HKLM.
|
||||
|
||||
If you do not test it with sandbox and administrative environment,
|
||||
test program will assert paniked and tell you how to resolve these issues.
|
||||
|
||||
The reason why do not run `cargo test` in sandbox environment directly,
|
||||
is that `cargo` can not find built tests located in host machine.
|
||||
It will try to fetch all dependencies again and rebuild test in sandbox entirely.
|
||||
So we use this complex way for testing.
|
||||
|
||||
@@ -402,6 +402,7 @@ impl Program {
|
||||
|
||||
// Add this progid to file extension "open with" list.
|
||||
let ext_key = &mut program_key.ext_key;
|
||||
ext_key.ensure(scope)?;
|
||||
ext_key.add_into_open_with_progids(scope, progid_key.inner())?;
|
||||
}
|
||||
|
||||
@@ -438,9 +439,12 @@ impl Program {
|
||||
// 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.
|
||||
// Remove this ProgId from file extension "open with" list,
|
||||
// if this file extension is existing
|
||||
let ext_key = &mut program_key.ext_key;
|
||||
ext_key.remove_from_open_with_progids(scope, progid_key.inner())?;
|
||||
if ext_key.is_exist(scope.into())? {
|
||||
ext_key.remove_from_open_with_progids(scope, progid_key.inner())?;
|
||||
}
|
||||
|
||||
// Delete ProgId subkey
|
||||
progid_key.delete(scope)?;
|
||||
|
||||
@@ -156,7 +156,7 @@ impl Schema {
|
||||
}
|
||||
|
||||
pub(super) fn get_behavior(&self) -> Option<&str> {
|
||||
self.icon.as_ref().map(|v| v.as_str())
|
||||
self.behavior.as_ref().map(|v| v.as_str())
|
||||
}
|
||||
|
||||
pub(super) fn get_strs(&self) -> &HashMap<String, String> {
|
||||
|
||||
@@ -62,7 +62,7 @@ impl AppPathsKey {
|
||||
Ok(key.is_some())
|
||||
}
|
||||
|
||||
/// Ensure this application key is presented in App Paths.
|
||||
/// Ensure this application key is presented in App Paths key.
|
||||
///
|
||||
/// Return true if we newly create this key,
|
||||
/// otherwise false indicating there already is an existing key.
|
||||
@@ -79,15 +79,16 @@ impl AppPathsKey {
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete this application key from App Paths.
|
||||
/// Delete this application key from App Paths key.
|
||||
///
|
||||
/// If there is no such key in App Paths,
|
||||
/// this function does nothing.
|
||||
pub fn delete(&mut self, scope: Scope) -> Result<()> {
|
||||
/// Return true if we successfully delete this key,
|
||||
/// otherwise false indicating there is no such key (already deleted).
|
||||
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
|
||||
let key = self.open_scope_for_write(scope)?;
|
||||
key.parent_key
|
||||
.delete_subkey_all(regext::blank_path_guard(self.key_name.inner())?)?;
|
||||
Ok(())
|
||||
Ok(regext::arbitrarily_delete_subkey_all(
|
||||
&key.parent_key,
|
||||
regext::blank_path_guard(self.key_name.inner())?,
|
||||
)?)
|
||||
}
|
||||
|
||||
fn open_scope_for_getter(&self, scope: Scope) -> Result<RegKey> {
|
||||
|
||||
@@ -68,6 +68,10 @@ impl ApplicationsKey {
|
||||
Ok(key.is_some())
|
||||
}
|
||||
|
||||
/// Ensure this application key is presented in Applications key.
|
||||
///
|
||||
/// Return true if we newly create this key,
|
||||
/// otherwise false indicating there already is an existing key.
|
||||
pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
|
||||
let key = self.open_scope_for_write(scope)?;
|
||||
if let None = key.this_key {
|
||||
@@ -81,11 +85,16 @@ impl ApplicationsKey {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, scope: Scope) -> Result<()> {
|
||||
/// Delete this application key from Applications key.
|
||||
///
|
||||
/// Return true if we successfully delete this key,
|
||||
/// otherwise false indicating there is no such key (already deleted).
|
||||
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
|
||||
let key = self.open_scope_for_write(scope)?;
|
||||
key.parent_key
|
||||
.delete_subkey_all(regext::blank_path_guard(self.key_name.inner())?)?;
|
||||
Ok(())
|
||||
Ok(regext::arbitrarily_delete_subkey_all(
|
||||
&key.parent_key,
|
||||
regext::blank_path_guard(self.key_name.inner())?,
|
||||
)?)
|
||||
}
|
||||
|
||||
// YYC MARK:
|
||||
@@ -176,7 +185,7 @@ impl ApplicationsKey {
|
||||
}
|
||||
None => {
|
||||
// Delete shell and its all subkey.
|
||||
key.delete_subkey_all(Self::NAMEOF_SHELL_VERB_PART1)?;
|
||||
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SHELL_VERB_PART1)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +226,7 @@ impl ApplicationsKey {
|
||||
}
|
||||
None => {
|
||||
// Delete shell and its all subkey.
|
||||
key.delete_subkey_all(Self::NAMEOF_DEFAULT_ICON_PART1)?;
|
||||
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_DEFAULT_ICON_PART1)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +257,7 @@ impl ApplicationsKey {
|
||||
}
|
||||
None => {
|
||||
// Delete this key
|
||||
key.delete_value(Self::NAMEOF_FRIENDLY_APP_NAME)?;
|
||||
regext::arbitrarily_delete_value(&key, Self::NAMEOF_FRIENDLY_APP_NAME)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +307,7 @@ impl ApplicationsKey {
|
||||
}
|
||||
None => {
|
||||
// Delete this subkey.
|
||||
key.delete_subkey_all(Self::NAMEOF_SUPPORTED_TYPES)?;
|
||||
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SUPPORTED_TYPES)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +329,7 @@ impl ApplicationsKey {
|
||||
if flag {
|
||||
key.set_value(Self::NAMEOF_NO_OPEN_WITH, &"")?;
|
||||
} else {
|
||||
key.delete_value(Self::NAMEOF_NO_OPEN_WITH)?;
|
||||
regext::arbitrarily_delete_value(&key, Self::NAMEOF_NO_OPEN_WITH)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ impl ExtKey {
|
||||
Ok(key.is_some())
|
||||
}
|
||||
|
||||
/// Ensure this file extension key is presented in Classes key.
|
||||
///
|
||||
/// Return true if we newly create this key,
|
||||
/// otherwise false indicating there already is an existing key.
|
||||
pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
|
||||
let key = self.open_scope_for_write(scope)?;
|
||||
if let None = key.this_key {
|
||||
@@ -79,16 +83,21 @@ impl ExtKey {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, scope: Scope) -> Result<()> {
|
||||
/// Delete this file extension key from Classes key.
|
||||
///
|
||||
/// Return true if we successfully delete this key,
|
||||
/// otherwise false indicating there is no such key (already deleted).
|
||||
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
|
||||
let key = self.open_scope_for_write(scope)?;
|
||||
key.parent_key
|
||||
.delete_subkey_all(regext::blank_path_guard(self.ext.dotted_inner())?)?;
|
||||
Ok(())
|
||||
Ok(regext::arbitrarily_delete_subkey_all(
|
||||
&key.parent_key,
|
||||
regext::blank_path_guard(self.ext.dotted_inner())?,
|
||||
)?)
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -127,7 +136,7 @@ impl ExtKey {
|
||||
}
|
||||
None => {
|
||||
// Delete this key
|
||||
key.delete_value(Self::NAMEOF_DEFAULT)?;
|
||||
regext::arbitrarily_delete_value(&key, Self::NAMEOF_DEFAULT)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +214,10 @@ impl ExtKey {
|
||||
None => return Ok(()),
|
||||
};
|
||||
// Remove given key
|
||||
open_with_progids_key.delete_value(pid.to_string())?;
|
||||
regext::arbitrarily_delete_value(
|
||||
&open_with_progids_key,
|
||||
regext::blank_path_guard(pid.to_string())?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ impl ProgIdKey {
|
||||
Ok(key.is_some())
|
||||
}
|
||||
|
||||
/// Ensure this ProgId key is presented in Classes key.
|
||||
///
|
||||
/// Return true if we newly create this key,
|
||||
/// otherwise false indicating there already is an existing key.
|
||||
pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
|
||||
let key = self.open_scope_for_write(scope)?;
|
||||
if let None = key.this_key {
|
||||
@@ -79,18 +83,23 @@ impl ProgIdKey {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, scope: Scope) -> Result<()> {
|
||||
/// Delete this ProgId key from Classes key.
|
||||
///
|
||||
/// Return true if we successfully delete this key,
|
||||
/// otherwise false indicating there is no such key (already deleted).
|
||||
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
|
||||
let key = self.open_scope_for_write(scope)?;
|
||||
key.parent_key
|
||||
.delete_subkey_all(regext::blank_path_guard(self.progid.to_string())?)?;
|
||||
Ok(())
|
||||
Ok(regext::arbitrarily_delete_subkey_all(
|
||||
&key.parent_key,
|
||||
regext::blank_path_guard(self.progid.to_string())?,
|
||||
)?)
|
||||
}
|
||||
|
||||
// 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
|
||||
// Currently we only support (Default), FriendlyTypeName and DefaultIcon
|
||||
// to just cover the basic usage.
|
||||
// We may expand these in future.
|
||||
|
||||
@@ -129,7 +138,7 @@ impl ProgIdKey {
|
||||
}
|
||||
None => {
|
||||
// Delete this key
|
||||
key.delete_value(Self::NAMEOF_DEFAULT)?;
|
||||
regext::arbitrarily_delete_value(&key, Self::NAMEOF_DEFAULT)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +218,7 @@ impl ProgIdKey {
|
||||
}
|
||||
None => {
|
||||
// Delete shell and its all subkey.
|
||||
key.delete_subkey_all(Self::NAMEOF_SHELL_VERB_PART1)?;
|
||||
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SHELL_VERB_PART1)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +252,7 @@ impl ProgIdKey {
|
||||
}
|
||||
None => {
|
||||
// Delete this key
|
||||
key.delete_value(Self::NAMEOF_FRIENDLY_TYPE_NAME)?;
|
||||
regext::arbitrarily_delete_value(&key, Self::NAMEOF_FRIENDLY_TYPE_NAME)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +293,7 @@ impl ProgIdKey {
|
||||
}
|
||||
None => {
|
||||
// Delete shell and its all subkey.
|
||||
key.delete_subkey_all(Self::NAMEOF_DEFAULT_ICON_PART1)?;
|
||||
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_DEFAULT_ICON_PART1)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,12 +61,60 @@ pub fn try_get_value<T: FromRegValue, N: AsRef<OsStr>>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete all tree of given path of given key anyway.
|
||||
///
|
||||
/// This function was invented to fix the shortcoming of [RegKey::delete_subkey_all].
|
||||
/// This function always delete given path of given key no matter it is existing.
|
||||
/// Oppositely, [RegKey::delete_subkey_all] will return error if there is no such path.
|
||||
///
|
||||
/// Return true if we successfully delete this key,
|
||||
/// otherwise false indicating there is no such key (already deleted).
|
||||
pub fn arbitrarily_delete_subkey_all<P: AsRef<OsStr>>(
|
||||
regkey: &RegKey,
|
||||
path: P,
|
||||
) -> std::io::Result<bool> {
|
||||
match regkey.delete_subkey_all(path) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(e) => match e.raw_os_error() {
|
||||
Some(errno) => match errno as u32 {
|
||||
ERROR_FILE_NOT_FOUND => Ok(false),
|
||||
_ => Err(e),
|
||||
},
|
||||
_ => Err(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete given value key of given key anyway.
|
||||
///
|
||||
/// This function was invented to fix the shortcoming of [RegKey::delete_value].
|
||||
/// This function always delete given value key of given key no matter it is existing.
|
||||
/// Oppositely, [RegKey::delete_value] will return error if there is no such value key.
|
||||
///
|
||||
/// Return true if we successfully delete this value key,
|
||||
/// otherwise false indicating there is no such value key (already deleted).
|
||||
pub fn arbitrarily_delete_value<N: AsRef<OsStr>>(
|
||||
regkey: &RegKey,
|
||||
name: N,
|
||||
) -> std::io::Result<bool> {
|
||||
match regkey.delete_value(name) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(e) => match e.raw_os_error() {
|
||||
Some(errno) => match errno as u32 {
|
||||
ERROR_FILE_NOT_FOUND => Ok(false),
|
||||
_ => Err(e),
|
||||
},
|
||||
_ => Err(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the name of only subkey in given key.
|
||||
///
|
||||
/// If there is only one subkey in given key, the return value is its name.
|
||||
/// If there is no any subkey, or has multiple subkeys, return None instead.
|
||||
/// If error occurs when fetching data, return Err(_).
|
||||
///
|
||||
///
|
||||
/// This is usually used for ShellVerb fetching.
|
||||
pub fn get_sole_subkey_name(regkey: &RegKey) -> std::io::Result<Option<String>> {
|
||||
let mut subkey_enumerator = regkey.enum_keys();
|
||||
@@ -85,7 +133,7 @@ pub fn get_sole_subkey_name(regkey: &RegKey) -> std::io::Result<Option<String>>
|
||||
}
|
||||
|
||||
/// Get the name list of all "string" subkeys in given key.
|
||||
///
|
||||
///
|
||||
/// This is usually used for "OpenWithProgIds" subkey.
|
||||
pub fn get_all_string_subkey_names(regkey: &RegKey) -> std::io::Result<Vec<String>> {
|
||||
regkey
|
||||
@@ -105,11 +153,14 @@ pub fn get_all_string_subkey_names(regkey: &RegKey) -> std::io::Result<Vec<Strin
|
||||
}
|
||||
|
||||
/// Delete all contents, including values and subkeys of given key.
|
||||
///
|
||||
///
|
||||
/// Deleting all contents of given key rely on giving a special parameter to [RegKey::delete_subkey_all].
|
||||
/// This is very dangerous and may be used by accident.
|
||||
/// So I create this to explicitly indicate this behavior and avoid any mis-type in code.
|
||||
pub fn clean_all_contents(regkey: &RegKey) -> std::io::Result<()> {
|
||||
// There is no possibility that this key do not existing,
|
||||
// because what we are cleaning is self content.
|
||||
// So directly use delete_subkey_all is okey.
|
||||
regkey.delete_subkey_all("")
|
||||
}
|
||||
|
||||
@@ -135,7 +186,7 @@ impl BlankPathError {
|
||||
/// Because it will cause unexpected behavior that returning key self, rather than subkey.
|
||||
/// This is VERY dangerous especially for those registry delete functions.
|
||||
/// So I create this function to prevent any harmful blank path was passed into registry function.
|
||||
///
|
||||
///
|
||||
/// This function MUST be used for the value, whose content can not be confirmed at compile time,
|
||||
/// and it will be passed to get/set value, or create/delete key functions.
|
||||
pub fn blank_path_guard<P: AsRef<OsStr>>(path: P) -> std::result::Result<P, BlankPathError> {
|
||||
|
||||
@@ -9,8 +9,19 @@ pub fn check_sandbox() {
|
||||
std::env::var("SANDBOXIE").is_ok(),
|
||||
concat!(
|
||||
"Non-sandbox environment detected. ",
|
||||
"Executing these test in non-sandbox environment is VERY dangerous. ",
|
||||
"Please set \"SANDBOXIE\" environment variable to explicitly indicate you are running these test in sandbox environment."
|
||||
"Executing these tests in non-sandbox environment is VERY dangerous. ",
|
||||
"Please set \"SANDBOXIE\" environment variable to explicitly indicate you are running these tests in sandbox environment."
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn check_privilege() {
|
||||
assert!(
|
||||
wfassoc::win32::utilities::has_privilege(),
|
||||
concat!(
|
||||
"You are running test without privilege. ",
|
||||
"These tests must be run with some privilege because it need to manipulate Windows Registry. ",
|
||||
"Please give it privilege in your sandbox environment."
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ fn make_valid_schema() -> Schema {
|
||||
schema.set_clsid(CLSID);
|
||||
schema.add_str("main_name", "Passoc Application").unwrap();
|
||||
schema.add_str("ext_name", "Pacfg File").unwrap();
|
||||
schema.add_icon("main_icon", r"notepad.exe,0").unwrap();
|
||||
schema.add_icon("ext_icon", r"notepad.exe,0").unwrap();
|
||||
schema.add_icon("main_icon", "notepad.exe,0").unwrap();
|
||||
schema.add_icon("ext_icon", "notepad.exe,0").unwrap();
|
||||
schema.add_behavior("main_behavior", "notepad.exe %1").unwrap();
|
||||
schema.add_behavior("ext_behavior", "notepad.exe %1").unwrap();
|
||||
schema.set_name(Some("main_name"));
|
||||
@@ -29,21 +29,20 @@ fn make_valid_schema() -> Schema {
|
||||
#[test]
|
||||
fn test_schema() {
|
||||
common::check_sandbox();
|
||||
common::check_privilege();
|
||||
|
||||
// valid schema -> valid program
|
||||
let schema = make_valid_schema();
|
||||
let rv = schema.into_program();
|
||||
assert!(rv.is_ok());
|
||||
|
||||
// missing identifier
|
||||
let mut schema = Schema::new();
|
||||
schema.set_path(APP_PATH);
|
||||
// missing essential parts (schema, path and etc)
|
||||
let schema = Schema::new();
|
||||
let rv = schema.into_program();
|
||||
assert!(rv.is_err());
|
||||
|
||||
// invalid path
|
||||
let mut schema = Schema::new();
|
||||
schema.set_identifier(IDENTIFIER);
|
||||
let mut schema = make_valid_schema();
|
||||
schema.set_path(r"C:\");
|
||||
let rv = schema.into_program();
|
||||
assert!(rv.is_err());
|
||||
@@ -93,6 +92,7 @@ fn test_schema() {
|
||||
#[test]
|
||||
fn test_program() {
|
||||
common::check_sandbox();
|
||||
common::check_privilege();
|
||||
|
||||
fn tester(scope: Scope, view: View) {
|
||||
// build program
|
||||
|
||||
@@ -22,6 +22,7 @@ static VERB: LazyLock<ShellVerb> = LazyLock::new(|| {
|
||||
#[test]
|
||||
fn test_app_paths_key() {
|
||||
common::check_sandbox();
|
||||
common::check_privilege();
|
||||
|
||||
static APP_PATH: &str = r"C:\Program Files\Passoc\passoc.exe";
|
||||
static APP_DIR: &str = r"C:\Program Files\Passoc";
|
||||
@@ -75,6 +76,7 @@ fn test_app_paths_key() {
|
||||
#[test]
|
||||
fn test_applications_key() {
|
||||
common::check_sandbox();
|
||||
common::check_privilege();
|
||||
|
||||
static FRIENDLY_APP_NAME: LazyLock<StrResVariant> =
|
||||
LazyLock::new(|| "Passoc Application".into());
|
||||
@@ -177,6 +179,7 @@ fn test_applications_key() {
|
||||
#[test]
|
||||
fn test_ext_key() {
|
||||
common::check_sandbox();
|
||||
common::check_privilege();
|
||||
|
||||
fn tester(scope: Scope, view: View) {
|
||||
let mut key = ExtKey::new(EXT.clone());
|
||||
@@ -246,6 +249,7 @@ fn test_ext_key() {
|
||||
#[test]
|
||||
fn test_prog_id_key() {
|
||||
common::check_sandbox();
|
||||
common::check_privilege();
|
||||
|
||||
static LEGACY_NAME: LazyLock<StrResVariant> = LazyLock::new(|| "Passoc Pacfg File".into());
|
||||
static FRIENDLY_TYPE_NAME: LazyLock<StrResVariant> =
|
||||
|
||||
Reference in New Issue
Block a user