Compare commits
3 Commits
c4b825f7f6
...
master
Author | SHA1 | Date | |
---|---|---|---|
dab91f1581 | |||
4b1f85c2f3 | |||
eee91d8498 |
23
Cargo.lock
generated
23
Cargo.lock
generated
@ -168,6 +168,12 @@ dependencies = [
|
|||||||
"litrs",
|
"litrs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@ -178,12 +184,28 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.1"
|
version = "1.70.1"
|
||||||
@ -495,6 +517,7 @@ dependencies = [
|
|||||||
name = "wfassoc"
|
name = "wfassoc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
"regex",
|
"regex",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
23
README.md
23
README.md
@ -6,6 +6,23 @@
|
|||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
* wfassoc: Core Rust library. Rust programmer can directly utilize it.
|
* `wfassoc`: Core Rust library. Rust programmer can directly utilize it.
|
||||||
* wfassoc_dylib: A dynamic library exposed for C/C++ and other languages users.
|
* `wfassoc_dylib`: A dynamic library exposed for C/C++ and other languages users.
|
||||||
* wfassoc_exec: A executable configuring file assocation according to user given profile and request.
|
* `wfassoc_exec`: A executable configuring file assocation according to user given profile and request.
|
||||||
|
|
||||||
|
If you are a programmer who just want to silently set your file association for user,
|
||||||
|
`wfassoc_exec` would be your best choice.
|
||||||
|
The only things you need to do is that write a TOML description file,
|
||||||
|
and provide it and `wfassoc_exec` with your executable.
|
||||||
|
By executing `wfassoc_exec` with this TOML file in your executable,
|
||||||
|
you can simply achieve this goal and analyze its return value to check whether it success.
|
||||||
|
|
||||||
|
However, if you are prefering taht let user decide which file associations should be created,
|
||||||
|
even have an UI displaying all current file association related with this program (like 7-Zip File Manager does),
|
||||||
|
you can choose `wfassoc_dylib` for your solution.
|
||||||
|
`wfassoc_dylib` expose all essential functions for this task.
|
||||||
|
And you can build it with your own program.
|
||||||
|
|
||||||
|
At last, if all scenarios above can not cover your requirements,
|
||||||
|
you can utilize `wfassoc` directly in Rust, change all essential code in your favor,
|
||||||
|
and finally produce the application which onlt suit for yourself.
|
||||||
|
25
example/ppic.toml
Normal file
25
example/ppic.toml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
identifier = "PineapplePicture"
|
||||||
|
path = 'C:\path\to\ppic.exe'
|
||||||
|
clsid = "{B5291320-FE7C-4069-BF87-A0AC327FCD20}"
|
||||||
|
|
||||||
|
[manners]
|
||||||
|
common = '"C:\path\to\ppic.exe" "%1"'
|
||||||
|
|
||||||
|
[exts]
|
||||||
|
".jpg" = "common"
|
||||||
|
".jfif" = "common"
|
||||||
|
".gif" = "common"
|
||||||
|
".bmp" = "common"
|
||||||
|
".png" = "common"
|
||||||
|
".ico" = "common"
|
||||||
|
".jpeg" = "common"
|
||||||
|
".tif" = "common"
|
||||||
|
".tiff" = "common"
|
||||||
|
".webp" = "common"
|
||||||
|
".svg" = "common"
|
||||||
|
".kra" = "common"
|
||||||
|
".xcf" = "common"
|
||||||
|
".avif" = "common"
|
||||||
|
".qoi" = "common"
|
||||||
|
".apng" = "common"
|
||||||
|
".exr" = "common"
|
@ -8,7 +8,8 @@ license = "SPDX:MIT"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
windows-sys = { version = "0.60.2", features = ["Win32_Security", "Win32_System_SystemServices"] }
|
windows-sys = { version = "0.60.2", features = ["Win32_Security", "Win32_System_SystemServices", "Win32_UI_Shell"] }
|
||||||
winreg = { version = "0.55.0", features = ["transactions"] }
|
winreg = { version = "0.55.0", features = ["transactions"] }
|
||||||
|
indexmap = "2.11.4"
|
||||||
regex = "1.11.3"
|
regex = "1.11.3"
|
||||||
uuid = "1.18.1"
|
uuid = "1.18.1"
|
||||||
|
283
wfassoc/src/assoc.rs
Normal file
283
wfassoc/src/assoc.rs
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
//! The module including all struct representing Windows file association concept,
|
||||||
|
//! like file extension, ProgId, CLSID and etc.
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
use thiserror::Error as TeError;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
// region: File Extension
|
||||||
|
|
||||||
|
/// The struct representing an file extension which must start with dot (`.`)
|
||||||
|
/// and followed by at least one arbitrary characters.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct Ext {
|
||||||
|
/// The body of file extension (excluding dot).
|
||||||
|
body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ext {
|
||||||
|
/// Create an new file extension.
|
||||||
|
pub fn new(raw: &str) -> Result<Self, ParseExtError> {
|
||||||
|
Self::from_str(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the body part of file extension (excluding dot)
|
||||||
|
pub fn inner(&self) -> &str {
|
||||||
|
&self.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The error occurs when try parsing string into FileExt.
|
||||||
|
#[derive(Debug, TeError)]
|
||||||
|
#[error("given file extension name \"{inner}\" is invalid")]
|
||||||
|
pub struct ParseExtError {
|
||||||
|
inner: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseExtError {
|
||||||
|
fn new(inner: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: inner.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Ext {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, ".{}", self.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Ext {
|
||||||
|
type Err = ParseExtError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\.([^\.]+)$").unwrap());
|
||||||
|
match RE.captures(s) {
|
||||||
|
Some(v) => Ok(Self {
|
||||||
|
body: v[1].to_string(),
|
||||||
|
}),
|
||||||
|
None => Err(ParseExtError::new(s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Programmatic Identifiers (ProgId)
|
||||||
|
|
||||||
|
/// The struct representing Programmatic Identifiers (ProgId).
|
||||||
|
///
|
||||||
|
/// Because there is optional part in standard ProgId, and not all software developers
|
||||||
|
/// are willing to following Microsoft suggestions, there is no strict constaint for ProgId.
|
||||||
|
/// So this struct is actually an enum which holding any possible ProgId format.
|
||||||
|
///
|
||||||
|
/// Reference:
|
||||||
|
/// - https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids
|
||||||
|
/// - https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
|
||||||
|
pub enum ProgId {
|
||||||
|
Plain(String),
|
||||||
|
Loose(LosseProgId),
|
||||||
|
Strict(StrictProgId),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for ProgId {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
// match it for strict ProgId first
|
||||||
|
if let Ok(v) = StrictProgId::from_str(s) {
|
||||||
|
return Self::Strict(v);
|
||||||
|
}
|
||||||
|
// then match for loose ProgId
|
||||||
|
if let Ok(v) = LosseProgId::from_str(s) {
|
||||||
|
return Self::Loose(v);
|
||||||
|
}
|
||||||
|
// fallback with plain
|
||||||
|
Self::Plain(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ProgId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ProgId::Plain(v) => v.fmt(f),
|
||||||
|
ProgId::Loose(v) => v.fmt(f),
|
||||||
|
ProgId::Strict(v) => v.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The error occurs when parsing ProgId.
|
||||||
|
#[derive(Debug, TeError)]
|
||||||
|
#[error("given ProgId \"{inner}\" is invalid")]
|
||||||
|
pub struct ParseProgIdError {
|
||||||
|
inner: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseProgIdError {
|
||||||
|
fn new(s: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: s.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ProgId similar with strict ProgId, but no version part.
|
||||||
|
pub struct LosseProgId {
|
||||||
|
vendor: String,
|
||||||
|
component: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LosseProgId {
|
||||||
|
pub fn new(vendor: &str, component: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
vendor: vendor.to_string(),
|
||||||
|
component: component.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_vendor(&self) -> &str {
|
||||||
|
&self.vendor
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_component(&self) -> &str {
|
||||||
|
&self.component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for LosseProgId {
|
||||||
|
type Err = ParseProgIdError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
static RE: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)$").unwrap());
|
||||||
|
let caps = RE.captures(s);
|
||||||
|
if let Some(caps) = caps {
|
||||||
|
let vendor = &caps[1];
|
||||||
|
let component = &caps[2];
|
||||||
|
Ok(Self::new(vendor, component))
|
||||||
|
} else {
|
||||||
|
Err(ParseProgIdError::new(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for LosseProgId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}.{}", self.vendor, self.component)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ProgId exactly follows `[Vendor or Application].[Component].[Version]` format.
|
||||||
|
pub struct StrictProgId {
|
||||||
|
vendor: String,
|
||||||
|
component: String,
|
||||||
|
version: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrictProgId {
|
||||||
|
pub fn new(vendor: &str, component: &str, version: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
vendor: vendor.to_string(),
|
||||||
|
component: component.to_string(),
|
||||||
|
version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_vendor(&self) -> &str {
|
||||||
|
&self.vendor
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_component(&self) -> &str {
|
||||||
|
&self.component
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_version(&self) -> u32 {
|
||||||
|
self.version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for StrictProgId {
|
||||||
|
type Err = ParseProgIdError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
static RE: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)\.([0-9]+)$").unwrap());
|
||||||
|
let caps = RE.captures(s);
|
||||||
|
if let Some(caps) = caps {
|
||||||
|
let vendor = &caps[1];
|
||||||
|
let component = &caps[2];
|
||||||
|
let version = caps[3]
|
||||||
|
.parse::<u32>()
|
||||||
|
.map_err(|_| ParseProgIdError::new(s))?;
|
||||||
|
Ok(Self::new(vendor, component, version))
|
||||||
|
} else {
|
||||||
|
Err(ParseProgIdError::new(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for StrictProgId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}.{}.{}", self.vendor, self.component, self.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: CLSID
|
||||||
|
|
||||||
|
pub struct Clsid {
|
||||||
|
inner: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clsid {
|
||||||
|
pub fn new(uuid: &str) -> Result<Self, ParseClsidError> {
|
||||||
|
Self::from_str(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: May add CLSID generator in there.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The error occurs when parsing CLSID
|
||||||
|
#[derive(Debug, TeError)]
|
||||||
|
#[error("given string \"{inner}\" is invalid for uuid")]
|
||||||
|
pub struct ParseClsidError {
|
||||||
|
inner: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseClsidError {
|
||||||
|
fn new(s: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: s.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Clsid {
|
||||||
|
type Err = ParseClsidError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: Uuid::parse_str(s).map_err(|_| ParseClsidError::new(s))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Clsid {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.inner.braced().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Icon Resource
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: String Resource
|
||||||
|
|
||||||
|
// endregion
|
@ -4,36 +4,51 @@
|
|||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
compile_error!("Crate wfassoc is only supported on Windows.");
|
compile_error!("Crate wfassoc is only supported on Windows.");
|
||||||
|
|
||||||
|
pub(crate) mod assoc;
|
||||||
|
pub(crate) mod utilities;
|
||||||
|
|
||||||
|
use indexmap::{IndexMap, IndexSet};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
|
use std::fmt::Display;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use thiserror::Error as TeError;
|
use thiserror::Error as TeError;
|
||||||
use winreg::RegKey;
|
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 winreg::transaction::Transaction;
|
|
||||||
|
|
||||||
// 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 WfError {
|
pub enum Error {
|
||||||
#[error("no administrative privilege")]
|
|
||||||
NoPrivilege,
|
|
||||||
#[error("error occurs when manipulating with Registry: {0}")]
|
#[error("error occurs when manipulating with Registry: {0}")]
|
||||||
BadRegOper(#[from] std::io::Error),
|
BadRegOper(#[from] std::io::Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
CastOsStr(#[from] utilities::CastOsStrError),
|
||||||
|
|
||||||
|
#[error("no administrative privilege")]
|
||||||
|
NoPrivilege,
|
||||||
#[error("given full path to application is invalid")]
|
#[error("given full path to application is invalid")]
|
||||||
BadFullAppPath,
|
BadFullAppPath,
|
||||||
#[error("failed when casting path or OS string into string")]
|
#[error("manner \"{0}\" is already registered")]
|
||||||
BadOsStrCast,
|
DupManner(String),
|
||||||
|
#[error("file extension \"{0}\" is already registered")]
|
||||||
|
DupExt(String),
|
||||||
|
#[error("the token of associated manner for file extension is invalid")]
|
||||||
|
InvalidAssocManner,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The result type used in this crate.
|
/// The result type used in this crate.
|
||||||
pub type WfResult<T> = Result<T, WfError>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region: Scope and View
|
// region: Types
|
||||||
|
|
||||||
|
/// The token for access registered items in Program.
|
||||||
|
/// This is usually returned when you registering them.
|
||||||
|
pub type Token = usize;
|
||||||
|
|
||||||
/// The scope where wfassoc will register and unregister application.
|
/// The scope where wfassoc will register and unregister application.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
@ -46,7 +61,7 @@ pub enum Scope {
|
|||||||
|
|
||||||
/// The error occurs when cast View into Scope.
|
/// The error occurs when cast View into Scope.
|
||||||
#[derive(Debug, TeError)]
|
#[derive(Debug, TeError)]
|
||||||
#[error("hybrid view can not be cast into any scope")]
|
#[error("hybrid View can not be cast into Scope")]
|
||||||
pub struct TryFromViewError {}
|
pub struct TryFromViewError {}
|
||||||
|
|
||||||
impl TryFromViewError {
|
impl TryFromViewError {
|
||||||
@ -58,7 +73,7 @@ impl TryFromViewError {
|
|||||||
impl TryFrom<View> for Scope {
|
impl TryFrom<View> for Scope {
|
||||||
type Error = TryFromViewError;
|
type Error = TryFromViewError;
|
||||||
|
|
||||||
fn try_from(value: View) -> Result<Self, Self::Error> {
|
fn try_from(value: View) -> std::result::Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
View::User => Ok(Self::User),
|
View::User => Ok(Self::User),
|
||||||
View::System => Ok(Self::System),
|
View::System => Ok(Self::System),
|
||||||
@ -74,7 +89,7 @@ impl Scope {
|
|||||||
// If we operate on System, and we do not has privilege,
|
// If we operate on System, and we do not has privilege,
|
||||||
// we think we do not have privilege, otherwise,
|
// we think we do not have privilege, otherwise,
|
||||||
// there is no privilege required.
|
// there is no privilege required.
|
||||||
!matches!(self, Self::System if !has_privilege())
|
!matches!(self, Self::System if !utilities::has_privilege())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,113 +116,124 @@ impl From<Scope> for View {
|
|||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region: Utilities
|
// region: Manner
|
||||||
|
|
||||||
/// The println macro only works on Debug mode
|
/// The struct representing a program manner.
|
||||||
/// for tracing the execution of some important functions.
|
/// Manner usually mean the way to open files,
|
||||||
macro_rules! debug_println {
|
/// or more preciously, the consititution of command arguments passed to program.
|
||||||
// For no argument.
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
() => {
|
pub struct Manner {
|
||||||
if cfg!(debug_assertions) {
|
argv: String,
|
||||||
println!();
|
}
|
||||||
|
|
||||||
|
impl Manner {
|
||||||
|
pub fn new(argv: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
argv: argv.to_string(),
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
// For one or more arguments like println!.
|
|
||||||
($($arg:tt)*) => {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
println!($($arg)*);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether current process has administrative privilege.
|
impl Display for Manner {
|
||||||
///
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
/// It usually means that checking whether current process is running as Administrator.
|
write!(f, "{}", self.argv)
|
||||||
/// Return true if it is, otherwise false.
|
|
||||||
///
|
|
||||||
/// Reference: https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
|
|
||||||
fn has_privilege() -> bool {
|
|
||||||
use windows_sys::Win32::Foundation::HANDLE;
|
|
||||||
use windows_sys::Win32::Security::{
|
|
||||||
AllocateAndInitializeSid, CheckTokenMembership, FreeSid, PSID, SECURITY_NT_AUTHORITY,
|
|
||||||
};
|
|
||||||
use windows_sys::Win32::System::SystemServices::{
|
|
||||||
DOMAIN_ALIAS_RID_ADMINS, SECURITY_BUILTIN_DOMAIN_RID,
|
|
||||||
};
|
|
||||||
use windows_sys::core::BOOL;
|
|
||||||
|
|
||||||
let nt_authority = SECURITY_NT_AUTHORITY.clone();
|
|
||||||
let mut administrators_group: PSID = PSID::default();
|
|
||||||
let success: BOOL = unsafe {
|
|
||||||
AllocateAndInitializeSid(
|
|
||||||
&nt_authority,
|
|
||||||
2,
|
|
||||||
SECURITY_BUILTIN_DOMAIN_RID as u32,
|
|
||||||
DOMAIN_ALIAS_RID_ADMINS as u32,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
&mut administrators_group,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if success == 0 {
|
|
||||||
panic!("Win32 AllocateAndInitializeSid() failed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut is_member: BOOL = BOOL::default();
|
|
||||||
let success: BOOL =
|
|
||||||
unsafe { CheckTokenMembership(HANDLE::default(), administrators_group, &mut is_member) };
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
FreeSid(administrators_group);
|
|
||||||
}
|
|
||||||
|
|
||||||
if success == 0 {
|
|
||||||
panic!("Win32 CheckTokenMembership() failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
is_member != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try casting given &Path into &str.
|
|
||||||
fn path_to_str(path: &Path) -> WfResult<&str> {
|
|
||||||
path.to_str().ok_or(WfError::BadOsStrCast)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try casting given &OsStr into &str.
|
|
||||||
fn osstr_to_str(osstr: &OsStr) -> WfResult<&str> {
|
|
||||||
osstr.to_str().ok_or(WfError::BadOsStrCast)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region: Registrar
|
// region: Program
|
||||||
|
|
||||||
/// The core registrar for register and unregister application.
|
/// The struct representing a complete program for registration and unregistration.
|
||||||
pub struct Registrar {
|
pub struct Program {
|
||||||
|
/// The identifier of this program.
|
||||||
|
identifier: String,
|
||||||
/// The fully qualified path to the application.
|
/// The fully qualified path to the application.
|
||||||
full_path: PathBuf,
|
full_path: PathBuf,
|
||||||
|
/// The collection holding all manners of this program.
|
||||||
|
manners: IndexSet<Manner>,
|
||||||
|
/// The collection holding all file extensions supported by this program.
|
||||||
|
/// The key is file estension and value is its associated manner for opening it.
|
||||||
|
exts: IndexMap<assoc::Ext, Token>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Registrar {
|
impl Program {
|
||||||
/// Create a new registrar for following operations.
|
/// Create a new registrar for following operations.
|
||||||
pub fn new(full_path: &Path) -> Self {
|
///
|
||||||
|
/// `identifier` is the unique name of this program.
|
||||||
|
/// If should only contain digits and alphabet chars,
|
||||||
|
/// and should not start with any digits.
|
||||||
|
/// For example, "MyApp" is okey but following names are not okey:
|
||||||
|
///
|
||||||
|
/// - `My App`
|
||||||
|
/// - `3DViewer`
|
||||||
|
/// - `我的Qt最时尚`
|
||||||
|
///
|
||||||
|
/// More preciously, `identifier` will be used as the vendor part of ProgId.
|
||||||
|
///
|
||||||
|
/// `full_path` is the fully qualified path to the application.
|
||||||
|
pub fn new(identifier: &str, full_path: &Path) -> Self {
|
||||||
|
// TODO: Add checker for identifier
|
||||||
Self {
|
Self {
|
||||||
|
identifier: identifier.to_string(),
|
||||||
full_path: full_path.to_path_buf(),
|
full_path: full_path.to_path_buf(),
|
||||||
|
manners: IndexSet::new(),
|
||||||
|
exts: IndexMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add manner provided by this program.
|
||||||
|
pub fn add_manner(&mut self, manner: Manner) -> Result<Token> {
|
||||||
|
// Backup a stringfied manner for error output.
|
||||||
|
let manner_str = manner.to_string();
|
||||||
|
// Insert manner.
|
||||||
|
let idx = self.manners.len();
|
||||||
|
if self.manners.insert(manner) {
|
||||||
|
Ok(idx)
|
||||||
|
} else {
|
||||||
|
Err(Error::DupExt(manner_str))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the reference to manner with given token.
|
||||||
|
pub fn get_manner(&self, token: Token) -> Option<&Manner> {
|
||||||
|
self.manners.get_index(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add file extension supported by this program and its associated manner.
|
||||||
|
pub fn add_ext(&mut self, ext: assoc::Ext, token: Token) -> Result<Token> {
|
||||||
|
// Check manner token
|
||||||
|
if let None = self.get_manner(token) {
|
||||||
|
return Err(Error::InvalidAssocManner);
|
||||||
|
}
|
||||||
|
// Backup a stringfied extension for error output.
|
||||||
|
let ext_str = ext.to_string();
|
||||||
|
// Insert file extension
|
||||||
|
let idx = self.exts.len();
|
||||||
|
if let None = self.exts.insert(ext, token) {
|
||||||
|
Ok(idx)
|
||||||
|
} else {
|
||||||
|
Err(Error::DupExt(ext_str))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the reference to file extension with given token.
|
||||||
|
pub fn get_ext(&self, token: Token) -> Option<&assoc::Ext> {
|
||||||
|
self.exts.get_index(token).map(|p| p.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Registrar {
|
impl Program {
|
||||||
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
|
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
|
||||||
const APPLICATIONS: &str = "Software\\Classes\\Applications";
|
const APPLICATIONS: &str = "Software\\Classes\\Applications";
|
||||||
|
|
||||||
/// Register this application.
|
/// Register this application.
|
||||||
pub fn register(&self, scope: Scope) -> WfResult<()> {
|
pub fn register(&self, scope: Scope) -> Result<()> {
|
||||||
|
// Check privilege
|
||||||
|
if !scope.has_privilege() {
|
||||||
|
return Err(Error::NoPrivilege);
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch root key.
|
// Fetch root key.
|
||||||
let hk = RegKey::predef(match scope {
|
let hk = RegKey::predef(match scope {
|
||||||
Scope::User => HKEY_CURRENT_USER,
|
Scope::User => HKEY_CURRENT_USER,
|
||||||
@ -222,21 +248,34 @@ impl Registrar {
|
|||||||
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
|
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
|
||||||
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
|
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
|
||||||
// Write App Paths values
|
// Write App Paths values
|
||||||
subkey.set_value("", &path_to_str(&self.full_path)?)?;
|
subkey.set_value("", &utilities::path_to_str(&self.full_path)?)?;
|
||||||
subkey.set_value("Path", &osstr_to_str(&start_in)?)?;
|
subkey.set_value("Path", &utilities::osstr_to_str(&start_in)?)?;
|
||||||
|
|
||||||
// Create Applications subkey
|
// Create Applications subkey
|
||||||
debug_println!("Adding Applications subkey...");
|
debug_println!("Adding Applications subkey...");
|
||||||
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
|
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
|
||||||
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
|
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
|
||||||
// Write Applications values
|
// Write Applications values
|
||||||
subkey.set_value("FriendlyAppName", &"WoW!")?;
|
if !self.exts.is_empty() {
|
||||||
|
let (supported_types, _) =
|
||||||
|
subkey.create_subkey_with_flags("SupportedTypes", KEY_WRITE)?;
|
||||||
|
for ext in self.exts.keys() {
|
||||||
|
supported_types.set_value(ext.to_string(), &"")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Okey
|
||||||
|
utilities::notify_assoc_changed();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unregister this application.
|
/// Unregister this application.
|
||||||
pub fn unregister(&self, scope: Scope) -> WfResult<()> {
|
pub fn unregister(&self, scope: Scope) -> Result<()> {
|
||||||
|
// Check privilege
|
||||||
|
if !scope.has_privilege() {
|
||||||
|
return Err(Error::NoPrivilege);
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch root key and file name.
|
// Fetch root key and file name.
|
||||||
let hk = RegKey::predef(match scope {
|
let hk = RegKey::predef(match scope {
|
||||||
Scope::User => HKEY_CURRENT_USER,
|
Scope::User => HKEY_CURRENT_USER,
|
||||||
@ -255,13 +294,14 @@ impl Registrar {
|
|||||||
subkey_parent.delete_subkey_all(file_name)?;
|
subkey_parent.delete_subkey_all(file_name)?;
|
||||||
|
|
||||||
// Okey
|
// Okey
|
||||||
|
utilities::notify_assoc_changed();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether this application has been registered.
|
/// Check whether this application has been registered.
|
||||||
///
|
///
|
||||||
/// Please note that this is a rough check and do not validate any data.
|
/// Please note that this is a rough check and do not validate any data.
|
||||||
pub fn is_registered(&self, scope: Scope) -> WfResult<bool> {
|
pub fn is_registered(&self, scope: Scope) -> Result<bool> {
|
||||||
// Fetch root key and file name.
|
// Fetch root key and file name.
|
||||||
let hk = RegKey::predef(match scope {
|
let hk = RegKey::predef(match scope {
|
||||||
Scope::User => HKEY_CURRENT_USER,
|
Scope::User => HKEY_CURRENT_USER,
|
||||||
@ -288,26 +328,28 @@ impl Registrar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Registrar {
|
impl Program {
|
||||||
/// Extract the file name part from full path to application,
|
/// Extract the file name part from full path to application,
|
||||||
/// which was used in Registry path component.
|
/// which was used in Registry path component.
|
||||||
fn extract_file_name(&self) -> WfResult<&OsStr> {
|
fn extract_file_name(&self) -> Result<&OsStr> {
|
||||||
// Get the file name part and make sure it is not empty
|
// Get the file name part and make sure it is not empty.
|
||||||
|
// Empty checker is CRUCIAL!
|
||||||
self.full_path
|
self.full_path
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|p| if p.is_empty() { None } else { Some(p) })
|
.and_then(|p| if p.is_empty() { None } else { Some(p) })
|
||||||
.ok_or(WfError::BadFullAppPath)
|
.ok_or(Error::BadFullAppPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the start in path from full path to application,
|
/// Extract the start in path from full path to application,
|
||||||
/// which basically is the stem of full path.
|
/// which basically is the stem of full path.
|
||||||
fn extract_start_in(&self) -> WfResult<&OsStr> {
|
fn extract_start_in(&self) -> Result<&OsStr> {
|
||||||
// Get parent part and make sure it is not empty
|
// Get parent part and make sure it is not empty
|
||||||
|
// Empty checker is CRUCIAL!
|
||||||
self.full_path
|
self.full_path
|
||||||
.parent()
|
.parent()
|
||||||
.map(|p| p.as_os_str())
|
.map(|p| p.as_os_str())
|
||||||
.and_then(|p| if p.is_empty() { None } else { Some(p) })
|
.and_then(|p| if p.is_empty() { None } else { Some(p) })
|
||||||
.ok_or(WfError::BadFullAppPath)
|
.ok_or(Error::BadFullAppPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
119
wfassoc/src/utilities.rs
Normal file
119
wfassoc/src/utilities.rs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
//! The module containing useful stuff used in this crate.
|
||||||
|
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::path::Path;
|
||||||
|
use thiserror::Error as TeError;
|
||||||
|
|
||||||
|
/// The println macro only works on Debug mode
|
||||||
|
/// for tracing the execution of some important functions.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! debug_println {
|
||||||
|
// For no argument.
|
||||||
|
() => {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// For one or more arguments like println!.
|
||||||
|
($($arg:tt)*) => {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
println!($($arg)*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// region: Windows Related
|
||||||
|
|
||||||
|
/// Check whether current process has administrative privilege.
|
||||||
|
///
|
||||||
|
/// It usually means that checking whether current process is running as Administrator.
|
||||||
|
/// Return true if it is, otherwise false.
|
||||||
|
///
|
||||||
|
/// Reference: https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
|
||||||
|
pub fn has_privilege() -> bool {
|
||||||
|
use windows_sys::Win32::Foundation::HANDLE;
|
||||||
|
use windows_sys::Win32::Security::{
|
||||||
|
AllocateAndInitializeSid, CheckTokenMembership, FreeSid, PSID, SECURITY_NT_AUTHORITY,
|
||||||
|
};
|
||||||
|
use windows_sys::Win32::System::SystemServices::{
|
||||||
|
DOMAIN_ALIAS_RID_ADMINS, SECURITY_BUILTIN_DOMAIN_RID,
|
||||||
|
};
|
||||||
|
use windows_sys::core::BOOL;
|
||||||
|
|
||||||
|
let nt_authority = SECURITY_NT_AUTHORITY.clone();
|
||||||
|
let mut administrators_group: PSID = PSID::default();
|
||||||
|
let success: BOOL = unsafe {
|
||||||
|
AllocateAndInitializeSid(
|
||||||
|
&nt_authority,
|
||||||
|
2,
|
||||||
|
SECURITY_BUILTIN_DOMAIN_RID as u32,
|
||||||
|
DOMAIN_ALIAS_RID_ADMINS as u32,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
&mut administrators_group,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if success == 0 {
|
||||||
|
panic!("Win32 AllocateAndInitializeSid() failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut is_member: BOOL = BOOL::default();
|
||||||
|
let success: BOOL =
|
||||||
|
unsafe { CheckTokenMembership(HANDLE::default(), administrators_group, &mut is_member) };
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
FreeSid(administrators_group);
|
||||||
|
}
|
||||||
|
|
||||||
|
if success == 0 {
|
||||||
|
panic!("Win32 CheckTokenMembership() failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
is_member != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Notify Windows that some file associations are changed, and should refresh them.
|
||||||
|
/// This function must be called once you change any file associations.
|
||||||
|
pub fn notify_assoc_changed() -> () {
|
||||||
|
use windows_sys::Win32::UI::Shell::{SHCNE_ASSOCCHANGED, SHCNF_IDLIST, SHChangeNotify};
|
||||||
|
unsafe {
|
||||||
|
SHChangeNotify(
|
||||||
|
SHCNE_ASSOCCHANGED as i32,
|
||||||
|
SHCNF_IDLIST,
|
||||||
|
Default::default(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region OS String Related
|
||||||
|
|
||||||
|
/// The error occurs when casting `OsStr` into `str`.
|
||||||
|
#[derive(Debug, TeError)]
|
||||||
|
#[error("failed when casting OS string into string")]
|
||||||
|
pub struct CastOsStrError {}
|
||||||
|
|
||||||
|
impl CastOsStrError {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try casting given &Path into &str.
|
||||||
|
pub fn path_to_str(path: &Path) -> Result<&str, CastOsStrError> {
|
||||||
|
path.to_str().ok_or(CastOsStrError::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try casting given &OsStr into &str.
|
||||||
|
pub fn osstr_to_str(osstr: &OsStr) -> Result<&str, CastOsStrError> {
|
||||||
|
osstr.to_str().ok_or(CastOsStrError::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
@ -2,7 +2,7 @@ use clap::{Parser, Subcommand};
|
|||||||
use comfy_table::Table;
|
use comfy_table::Table;
|
||||||
use std::process;
|
use std::process;
|
||||||
use thiserror::Error as TeError;
|
use thiserror::Error as TeError;
|
||||||
use wfassoc::{Error as WfError, FileExt, Scope, View};
|
use wfassoc::{Error as WfError, Ext, Scope, View};
|
||||||
|
|
||||||
// region: Basic Types
|
// region: Basic Types
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ fn run_query(cli: Cli) -> Result<()> {
|
|||||||
".kra", ".xcf", ".avif", ".qoi", ".apng", ".exr",
|
".kra", ".xcf", ".avif", ".qoi", ".apng", ".exr",
|
||||||
];
|
];
|
||||||
|
|
||||||
for ext in exts.iter().map(|e| FileExt::new(e).unwrap()) {
|
for ext in exts.iter().map(|e| Ext::new(e).unwrap()) {
|
||||||
if let Some(ext_assoc) = ext.query(View::Hybrid) {
|
if let Some(ext_assoc) = ext.query(View::Hybrid) {
|
||||||
println!("{:?}", ext_assoc)
|
println!("{:?}", ext_assoc)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user