Compare commits
72
Commits
6096265017
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fcdf902f7 | ||
|
|
e3a1195c04 | ||
|
|
db69452213 | ||
|
|
a350a591ad | ||
|
|
6a354bd7a7 | ||
|
|
23d7045ec1 | ||
|
|
3492f93c83 | ||
|
|
d1de56a21d | ||
|
|
880f90211b | ||
|
|
1a44240f88 | ||
|
|
071347f4d4 | ||
|
|
821b865f2d | ||
|
|
50f01429b0 | ||
|
|
930a84a0f6 | ||
|
|
447d94fdd6 | ||
|
|
f0bd2c0b73 | ||
|
|
119a4d0341 | ||
|
|
53b40a4d2f | ||
|
|
0533cdab23 | ||
|
|
8c61aa1e1d | ||
|
|
77924b5937 | ||
|
|
2c811503a2 | ||
|
|
18a55272a3 | ||
|
|
1658127bdd | ||
|
|
72a8c13c1f | ||
|
|
aececd8e5d | ||
|
|
883cba901c | ||
|
|
a1874f3682 | ||
|
|
3c429b6c51 | ||
|
|
a4d2f7bc26 | ||
|
|
658806e9ff | ||
|
|
f8db414da3 | ||
|
|
bc419f8d5e | ||
|
|
3b0080849d | ||
|
|
53cc8edcfd | ||
|
|
1086039dcb | ||
|
|
ae0231fe69 | ||
|
|
4205ef18d3 | ||
|
|
bbcea1f4d2 | ||
|
|
91c7fdda70 | ||
|
|
f1a7cb89e5 | ||
|
|
fe9f839091 | ||
|
|
9d3467ea26 | ||
|
|
9d02a2187f | ||
|
|
c164bb73bc | ||
|
|
92ec83b8d7 | ||
|
|
8d7f96d499 | ||
|
|
684a24fcf8 | ||
|
|
5c182bb4d3 | ||
|
|
9e9aa282b1 | ||
|
|
76ddddb6cd | ||
|
|
f0e610f8c8 | ||
|
|
7d92f9a4a0 | ||
|
|
1a45b309e7 | ||
|
|
7f36375a73 | ||
|
|
a6322bff51 | ||
|
|
6804b96078 | ||
|
|
d4b52efee0 | ||
|
|
ec5c78e8ce | ||
|
|
e325ba08f1 | ||
|
|
03d17fad6e | ||
|
|
8f762928db | ||
|
|
94f868ebda | ||
|
|
2da84f8797 | ||
|
|
fe81541eeb | ||
|
|
3edb08efc7 | ||
|
|
edb9d0a14d | ||
|
|
c6fa89f15f | ||
|
|
01302214c2 | ||
|
|
e6ac59d85c | ||
|
|
1df7191a36 | ||
|
|
66dc8c2630 |
Generated
+62
-109
@@ -73,25 +73,6 @@ version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "cbindgen"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "975982cdb7ad6a142be15bdf84aea7ec6a9e5d4d797c004d43185b24cfe4e684"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"syn",
|
||||
"tempfile",
|
||||
"toml 0.8.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.3"
|
||||
@@ -203,12 +184,6 @@ dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
@@ -249,12 +224,6 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.81"
|
||||
@@ -304,6 +273,28 @@ version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
||||
dependencies = [
|
||||
"num_enum_derive",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -339,6 +330,15 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
@@ -420,12 +420,6 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -462,28 +456,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.3"
|
||||
@@ -493,12 +465,27 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slotmap"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "strfmt"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29fdc163db75f7b5ffa3daf0c5a7136fb0d4b2f35523cd1769da05e034159feb"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -516,19 +503,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.17"
|
||||
@@ -549,18 +523,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.8"
|
||||
@@ -569,22 +531,13 @@ checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned 1.0.3",
|
||||
"toml_datetime 0.7.3",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.7.3"
|
||||
@@ -596,15 +549,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
version = "0.23.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_write",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
@@ -617,12 +568,6 @@ dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.4"
|
||||
@@ -664,6 +609,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.1+wasi-0.2.4"
|
||||
@@ -738,6 +689,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"regex",
|
||||
"strfmt",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
"widestring",
|
||||
@@ -749,7 +701,8 @@ dependencies = [
|
||||
name = "wfassoc-cdylib"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cbindgen",
|
||||
"num_enum",
|
||||
"slotmap",
|
||||
"thiserror",
|
||||
"wfassoc",
|
||||
]
|
||||
@@ -762,7 +715,7 @@ dependencies = [
|
||||
"comfy-table",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"toml 0.9.8",
|
||||
"toml",
|
||||
"wfassoc",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022-2025 yyc12345
|
||||
Copyright (c) 2022-2026 yyc12345
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -4,6 +4,20 @@
|
||||
|
||||
**Work In Progress**
|
||||
|
||||
## Preface
|
||||
|
||||
The final goal of this repository is making a viable solution for programmer,
|
||||
who is familiar with GUI application development on POSIX system
|
||||
and want to find a workable way to setup file associations on Windows for their developed software
|
||||
without learning too much Win32 knowledge.
|
||||
|
||||
So considering this premise, this library will NOT assist those professional Windows developer
|
||||
for setting up those complex Windows shell functions, such as the interaction with Explorer preview panel,
|
||||
or adding extra entries in menu.
|
||||
|
||||
In brief words, this library only do one thing. Setup and check file associations for your application,
|
||||
and make it at least works.
|
||||
|
||||
## Introduction
|
||||
|
||||
* `wfassoc`: Core Rust library. Rust programmer can directly utilize it.
|
||||
|
||||
+198
-73
@@ -34,7 +34,7 @@ clsid = "{B5291320-FE7C-4069-BF87-A0AC327FCD20}"
|
||||
#
|
||||
# This key is optional. If you don't fill this key,
|
||||
# Windows will use your executable embedded Win32 resources for friendly name.
|
||||
name = "default"
|
||||
name = "self"
|
||||
|
||||
# The icon of this program.
|
||||
#
|
||||
@@ -42,7 +42,7 @@ name = "default"
|
||||
#
|
||||
# This key is optional. If you don't fill this key,
|
||||
# Windows will use the first icon embedded in your executable as icon.
|
||||
icon = "others"
|
||||
icon = "self"
|
||||
|
||||
# The default behavior of your program.
|
||||
#
|
||||
@@ -69,30 +69,41 @@ behavior = "generic"
|
||||
#
|
||||
# Due to the same use of back-slash in path part,
|
||||
# you can utilize TOML "literal strings" syntax to write this value easier.
|
||||
default = '@C:\path\to\ppic.exe,-1001'
|
||||
# self = '@C:\path\to\ppic.exe,-1001'
|
||||
|
||||
# The value of this key also can be a plain string like this commente statement,
|
||||
# if you don't want to touch Win32 resources and your program do not have any I18N requirements.
|
||||
# default = "Pineapple Picture"
|
||||
self = "Pineapple Pictures"
|
||||
|
||||
# And more string resources...
|
||||
jpg = '@C:\path\to\ppic.exe,-1011'
|
||||
gif = '@C:\path\to\ppic.exe,-1012'
|
||||
bmp = '@C:\path\to\ppic.exe,-1013'
|
||||
png = '@C:\path\to\ppic.exe,-1014'
|
||||
ico = '@C:\path\to\ppic.exe,-1015'
|
||||
jpeg = '@C:\path\to\ppic.exe,-1016'
|
||||
tif = '@C:\path\to\ppic.exe,-1017'
|
||||
tiff = '@C:\path\to\ppic.exe,-1018'
|
||||
webp = '@C:\path\to\ppic.exe,-1019'
|
||||
avif = '@C:\path\to\ppic.exe,-1020'
|
||||
svg = '@C:\path\to\ppic.exe,-1021'
|
||||
kra = '@C:\path\to\ppic.exe,-1022'
|
||||
xcf = '@C:\path\to\ppic.exe,-1023'
|
||||
qoi = '@C:\path\to\ppic.exe,-1024'
|
||||
apng = '@C:\path\to\ppic.exe,-1025'
|
||||
exr = '@C:\path\to\ppic.exe,-1026'
|
||||
tga = '@C:\path\to\ppic.exe,-1027'
|
||||
jpeg = 'JPEG Image'
|
||||
gif = 'GIF Animation Image'
|
||||
png = 'PNG Image'
|
||||
svg = 'Scalable Vector Graphics'
|
||||
webp = 'WebP Image'
|
||||
avif = 'AV1 Image File'
|
||||
ico = 'Windows Icon Image'
|
||||
icns = 'Apple Icon Image'
|
||||
psd = 'Photoshop Document'
|
||||
kra = 'Krita Document'
|
||||
xcf = 'GIMP Document'
|
||||
tga = 'TARGA Image'
|
||||
tif = 'Tagged Image File Format'
|
||||
bmp = 'Bitmap'
|
||||
wbmp = 'Wireless Bitmap'
|
||||
dds = 'DirectDraw Surface'
|
||||
hdr = 'High Dynamic Range Image'
|
||||
iff = 'Interchange File Format'
|
||||
pic = 'Softimage Picture'
|
||||
pcx = 'PiCture eXchange PC Paintbrush Image'
|
||||
qoi = 'Quite OK Image'
|
||||
ras = 'Sun Raster Image'
|
||||
ora = 'OpenRaster Image'
|
||||
ani = 'Animated Cursor'
|
||||
pfm = 'Portable Float Map'
|
||||
rgb = 'Silicon Graphics Image'
|
||||
pxr = 'Pixar Raster Image'
|
||||
sct = 'Scitex Continuous Tone'
|
||||
|
||||
# ======== Icon Resources ========
|
||||
|
||||
@@ -112,18 +123,17 @@ tga = '@C:\path\to\ppic.exe,-1027'
|
||||
#
|
||||
# Due to the same use of back-slash in path part,
|
||||
# you can utilize TOML "literal strings" syntax to write this value easier.
|
||||
png = 'C:\path\to\ppic.exe,-1'
|
||||
self = 'C:\path\to\ppic.exe,0'
|
||||
|
||||
# The value of this key also can be the absolute path to existing icon file like this commente statement.
|
||||
# png = 'C:\path\to\ppic\icons\png.ico'
|
||||
generic = 'C:\path\to\ppic\icons\generic.ico'
|
||||
|
||||
# Add more icon resources...
|
||||
svg = 'C:\path\to\ppic.exe,-2'
|
||||
gif = 'C:\path\to\ppic.exe,-3'
|
||||
jpg = 'C:\path\to\ppic.exe,-4'
|
||||
webp = 'C:\path\to\ppic.exe,-5'
|
||||
avif = 'C:\path\to\ppic.exe,-6'
|
||||
others = 'C:\path\to\ppic.exe,-61'
|
||||
gif = 'C:\path\to\ppic\icons\gif.ico'
|
||||
jpeg = 'C:\path\to\ppic\icons\jpeg.ico'
|
||||
png = 'C:\path\to\ppic\icons\png.ico'
|
||||
psd = 'C:\path\to\ppic\icons\psd.ico'
|
||||
svg = 'C:\path\to\ppic\icons\svg.ico'
|
||||
|
||||
# ======== Behaviors Collection ========
|
||||
|
||||
@@ -161,76 +171,191 @@ generic = '"C:\path\to\ppic.exe" "%1"'
|
||||
[exts.jpg]
|
||||
# First, we specify its name shown in Windows Explorer.
|
||||
# The value of this field is the token declared above in "strs" list.
|
||||
name = "jpg"
|
||||
name = "jpeg"
|
||||
# Then, speficy its associated icon.
|
||||
# The value of this field is the token declared above in "icons" list.
|
||||
icon = "jpg"
|
||||
icon = "jpeg"
|
||||
# At last, specify the behavior of this extension define decide how we open this file.
|
||||
# The value of this field is the token declared above in "behaviors" list.
|
||||
behavior = "generic"
|
||||
|
||||
# And more file extensions...
|
||||
[exts.jfif]
|
||||
name = "jfif"
|
||||
icon = "jpg"
|
||||
[exts.jpeg]
|
||||
name = "jpeg"
|
||||
icon = "jpeg"
|
||||
behavior = "generic"
|
||||
[exts.jfif]
|
||||
name = "jpeg"
|
||||
icon = "jpeg"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.gif]
|
||||
name = "gif"
|
||||
icon = "gif"
|
||||
behavior = "generic"
|
||||
[exts.bmp]
|
||||
name = "bmp"
|
||||
icon = "others"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.png]
|
||||
name = "png"
|
||||
icon = "png"
|
||||
behavior = "generic"
|
||||
[exts.ico]
|
||||
name = "ico"
|
||||
icon = "others"
|
||||
behavior = "generic"
|
||||
[exts.jpeg]
|
||||
name = "jpeg"
|
||||
icon = "jpg"
|
||||
behavior = "generic"
|
||||
[exts.tif]
|
||||
name = "tif"
|
||||
icon = "others"
|
||||
behavior = "generic"
|
||||
[exts.tiff]
|
||||
name = "tiff"
|
||||
icon = "others"
|
||||
behavior = "generic"
|
||||
[exts.webp]
|
||||
name = "webp"
|
||||
icon = "webp"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.svg]
|
||||
name = "svg"
|
||||
icon = "svg"
|
||||
behavior = "generic"
|
||||
[exts.kra]
|
||||
name = "kra"
|
||||
icon = "others"
|
||||
behavior = "generic"
|
||||
[exts.xcf]
|
||||
name = "xcf"
|
||||
icon = "others"
|
||||
|
||||
[exts.webp]
|
||||
name = "webp"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.avif]
|
||||
name = "avif"
|
||||
icon = "avif"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.ico]
|
||||
name = "ico"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.icns]
|
||||
name = "icns"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.psd]
|
||||
name = "psd"
|
||||
icon = "psd"
|
||||
behavior = "generic"
|
||||
[exts.psb]
|
||||
name = "psd"
|
||||
icon = "psd"
|
||||
behavior = "generic"
|
||||
[exts.pdd]
|
||||
name = "psd"
|
||||
icon = "psd"
|
||||
behavior = "generic"
|
||||
[exts.psdt]
|
||||
name = "psd"
|
||||
icon = "psd"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.kra]
|
||||
name = "kra"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.xcf]
|
||||
name = "xcf"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.tga]
|
||||
name = "tga"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.tif]
|
||||
name = "tif"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
[exts.tiff]
|
||||
name = "tif"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.bmp]
|
||||
name = "bmp"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.wbmp]
|
||||
name = "wbmp"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.dds]
|
||||
name = "dds"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.hdr]
|
||||
name = "hdr"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.iff]
|
||||
name = "iff"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.pic]
|
||||
name = "pic"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.pcx]
|
||||
name = "pcx"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.qoi]
|
||||
name = "qoi"
|
||||
icon = "others"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
[exts.apng]
|
||||
name = "apng"
|
||||
icon = "png"
|
||||
|
||||
[exts.ras]
|
||||
name = "ras"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
[exts.exr]
|
||||
name = "exr"
|
||||
icon = "others"
|
||||
[exts.sun]
|
||||
name = "ras"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.ora]
|
||||
name = "ora"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.ani]
|
||||
name = "ani"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.pfm]
|
||||
name = "pfm"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
[exts.phm]
|
||||
name = "pfm"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.rgb]
|
||||
name = "rgb"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
[exts.rgba]
|
||||
name = "rgb"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
[exts.bw]
|
||||
name = "rgb"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
[exts.sgi]
|
||||
name = "rgb"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.pxr]
|
||||
name = "pxr"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
[exts.sct]
|
||||
name = "sct"
|
||||
icon = "generic"
|
||||
behavior = "generic"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Pineapple Picture Association
|
||||
|
||||
TODO
|
||||
@@ -0,0 +1,36 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
project(qwfassoc_suite LANGUAGES CXX)
|
||||
|
||||
# Qt 6 requires C++17 at minimum.
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# Let CMake auto-process Q_OBJECT, .ui files and .qrc resources for every
|
||||
# subproject below.
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
# Make the bundled Findwfassoc.cmake module visible to find_package().
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||
|
||||
# Qt and LinguistTools are used by both subprojects, so they are looked up at
|
||||
# the top level. The same is true for wfassoc: although only the qwfassoc
|
||||
# library links against it directly, PUBLIC propagation from the library
|
||||
# target makes the dependency available to qwfassoc-standalone as well.
|
||||
# toml11 is only needed when the standalone executable is built, so it is
|
||||
# looked up conditionally below.
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets LinguistTools)
|
||||
find_package(wfassoc REQUIRED)
|
||||
|
||||
# The standalone executable is optional: embedders may want only the library.
|
||||
option(QWFASSOC_BUILD_STANDALONE "Build the qwfassoc-standalone executable" ON)
|
||||
|
||||
add_subdirectory(qwfassoc)
|
||||
|
||||
if(QWFASSOC_BUILD_STANDALONE)
|
||||
find_package(toml11 REQUIRED)
|
||||
add_subdirectory(qwfassoc-standalone)
|
||||
endif()
|
||||
@@ -0,0 +1,154 @@
|
||||
# qwfassoc (suite)
|
||||
|
||||
A Qt Widgets based GUI for the [wfassoc](../../wfassoc) library, split into a
|
||||
reusable shared library and a small standalone executable that exercises it.
|
||||
|
||||
The project is organized as two CMake subprojects:
|
||||
|
||||
```
|
||||
qwfassoc/ Parent directory (this README)
|
||||
├── CMakeLists.txt Top-level CMake; add_subdirectory's both
|
||||
│ subprojects and finds Qt, wfassoc, toml11
|
||||
├── cmake/
|
||||
│ ├── Findwfassoc.cmake Verbatim copy of wfassoc's Find module
|
||||
│ └── README.md Provenance notes for the copy
|
||||
├── qwfassoc/ Shared library subproject
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── i18n/
|
||||
│ │ └── qwfassoc_zh_CN.ts Empty placeholder translation file
|
||||
│ └── src/
|
||||
│ ├── qwfassoc_global.h QWFASSOC_EXPORT macro
|
||||
│ ├── scope.h Shared TargetScope enum
|
||||
│ ├── manifest.h Manifest data struct (no TOML dependency)
|
||||
│ ├── icon_utils.h/.cpp wfassocpp::HICON -> QPixmap conversion
|
||||
│ ├── application_widget.h/.cpp/.ui
|
||||
│ │ Install / uninstall widget
|
||||
│ └── association_widget.h/.cpp/.ui
|
||||
│ File associations widget
|
||||
└── qwfassoc-standalone/ Executable subproject
|
||||
├── CMakeLists.txt
|
||||
├── i18n/
|
||||
│ └── qwfassoc-standalone_zh_CN.ts
|
||||
│ Empty placeholder translation file
|
||||
└── src/
|
||||
├── main.cpp Entry point, CLI parsing, translator loading
|
||||
├── main_window.h/.cpp/.ui
|
||||
│ QDialog hosting the two widgets in a tab widget
|
||||
└── manifest_parser.h/.cpp
|
||||
TOML -> Manifest, Manifest -> Schema
|
||||
```
|
||||
|
||||
## Subprojects at a glance
|
||||
|
||||
### `qwfassoc` (shared library)
|
||||
|
||||
Exports two reusable widgets that wrap wfassoc:
|
||||
|
||||
* `qwfassoc::ApplicationWidget` — install / uninstall the program in the
|
||||
configured scope.
|
||||
* `qwfassoc::AssociationWidget` — stage and apply per-extension link / unlink
|
||||
operations.
|
||||
|
||||
Both widgets follow the **two-phase initialization** pattern expected by Qt
|
||||
Designer promoted widgets: the constructor only takes a `QWidget*` parent and
|
||||
leaves the widget disabled. A `setConfig(Config)` method injects the
|
||||
`wfassocpp::Program` pointer and `TargetScope` (and, for the association
|
||||
widget, whether the OK/Cancel buttons are visible). Each widget also exposes:
|
||||
|
||||
* a `refresh()` slot that re-queries the live wfassoc state, intended to be
|
||||
called by the host when another component has mutated the registry;
|
||||
* a `changed()` signal emitted whenever the widget itself mutates the
|
||||
registry (install / uninstall / apply);
|
||||
* (`AssociationWidget` only) a `finished(bool accepted)` signal emitted when
|
||||
the user clicks OK (after `changed()`) or Cancel, so the host can close the
|
||||
dialog.
|
||||
|
||||
The library also exposes the plain `qwfassoc::Manifest` data struct and a
|
||||
`qwfassoc::icon_utils::fromHicon()` helper, but the TOML parsing logic (which
|
||||
depends on toml11) lives in the standalone executable.
|
||||
|
||||
### `qwfassoc-standalone` (executable)
|
||||
|
||||
Reproduces the original tabbed wfassoc configurator by:
|
||||
|
||||
1. parsing `-c/--manifest <path>` and `-f/--for <user|system>` from the
|
||||
command line;
|
||||
2. building a `wfassocpp::Program` via `parseManifestFile` + `buildSchema`;
|
||||
3. hosting `ApplicationWidget` and `AssociationWidget` inside a `QTabWidget`
|
||||
in a `MainWindow` dialog;
|
||||
4. wiring the widgets' `changed()` and `finished()` signals so that any
|
||||
registry mutation refreshes both pages and OK/Cancel drive the dialog's
|
||||
acceptance.
|
||||
|
||||
## Requirements
|
||||
|
||||
* **CMake** 3.20 or newer (3.21+ recommended for `qt6_add_translations`).
|
||||
* A C++17 compiler.
|
||||
* **Qt 6** with the `Widgets` and `LinguistTools` components.
|
||||
* **wfassoc**, with `wfassoc_ROOT` pointing at an installed tree (see
|
||||
[`cmake/Findwfassoc.cmake`](cmake/Findwfassoc.cmake) for the expected
|
||||
directory layout).
|
||||
* **toml11** — only required when building the standalone executable.
|
||||
|
||||
## Building
|
||||
|
||||
```bat
|
||||
cmake -S . -B build ^
|
||||
-DCMAKE_PREFIX_PATH=C:\Qt\6.x.x\msvc2022_64 ^
|
||||
-Dwfassoc_ROOT=C:\path\to\wfassoc\install ^
|
||||
-Dtoml11_DIR=C:\path\to\toml11\share\toml11\cmake
|
||||
cmake --build build --config Release
|
||||
```
|
||||
|
||||
To skip the standalone executable (and the toml11 dependency):
|
||||
|
||||
```bat
|
||||
cmake -S . -B build -DQWFASSOC_BUILD_STANDALONE=OFF ...
|
||||
```
|
||||
|
||||
The standalone executable is `build/qwfassoc-standalone/Release/qwfassoc-standalone.exe`
|
||||
(or similar, depending on the generator); the library is
|
||||
`build/qwfassoc/Release/qwfassoc.dll`.
|
||||
|
||||
## Running the standalone executable
|
||||
|
||||
| Short | Long | Meaning |
|
||||
| ----- | ------------ | ------------------------------------------------------------------------ |
|
||||
| `-c` | `--manifest` | Path to the application manifest TOML file (see [`example/manifest/ppic.toml`](../manifest/ppic.toml)). |
|
||||
| `-f` | `--for` | Target scope: `user` or `system`. |
|
||||
|
||||
```bat
|
||||
qwfassoc-standalone -c C:\path\to\ppic.toml -f user
|
||||
```
|
||||
|
||||
## Internationalization
|
||||
|
||||
Source strings are English and every user-facing string is wrapped in `tr()`
|
||||
(in code) or is a plain `<string>` element in the `.ui` file (which `uic`
|
||||
wraps in `QCoreApplication::translate`).
|
||||
|
||||
Each subproject ships its own empty placeholder `.ts` file under its
|
||||
`i18n/` directory and registers it with `qt6_add_translations()`:
|
||||
|
||||
* `qwfassoc/i18n/qwfassoc_zh_CN.ts` — covers the library widgets.
|
||||
* `qwfassoc-standalone/i18n/qwfassoc-standalone_zh_CN.ts` — covers the
|
||||
executable-specific messages (CLI errors, tab titles, dialog window
|
||||
title, etc.).
|
||||
|
||||
At runtime, `installTranslators()` in `qwfassoc-standalone/src/main.cpp`
|
||||
loads both `.qm` files for the user's preferred UI language from the
|
||||
`:/i18n/` resource prefix. Translators are expected to fill in the `.ts`
|
||||
files; no actual translation work is performed by the build on its own.
|
||||
|
||||
## Notes and Limitations
|
||||
|
||||
* "Self" detection in the file-association table is based on comparing the
|
||||
display name returned by wfassoc with the display name this program would
|
||||
use. Two programs sharing the exact same display name could therefore be
|
||||
confused.
|
||||
* The system column in `AssociationWidget` is rendered disabled (using
|
||||
`Qt::ItemIsSelectable` without `Qt::ItemIsEnabled`) when `TargetScope` is
|
||||
`User`; the cells stay visible but cannot be clicked.
|
||||
* All errors originating from wfassoc are surfaced through `QMessageBox`
|
||||
dialogs; fatal errors during startup cause the process to exit with a
|
||||
non-zero status code.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Findwfassoc.cmake
|
||||
# ----------------
|
||||
# Find wfassoc library and headers.
|
||||
#
|
||||
# This module requires the user to set wfassoc_ROOT to the installation
|
||||
# directory of wfassoc. The directory structure under wfassoc_ROOT must be:
|
||||
# bin/ - contains wfassoc_cdylib.dll
|
||||
# include/ - contains wfassoc.h and wfassoc++.h
|
||||
# lib/ - contains wfassoc_cdylib.dll.lib (import library)
|
||||
#
|
||||
# This module defines the following variables:
|
||||
# wfassoc_FOUND - True if wfassoc was found
|
||||
# wfassoc_INCLUDE_DIRS - Path to wfassoc include directory
|
||||
# wfassoc_LIBRARIES - Path to wfassoc import library
|
||||
# wfassoc_DLL - Path to wfassoc DLL
|
||||
# wfassoc_ROOT - The root directory (user-provided)
|
||||
#
|
||||
# This module also creates the following imported targets:
|
||||
# wfassoc::wfassoc - Main wfassoc library (includes both include and link)
|
||||
#
|
||||
|
||||
set(wfassoc_FOUND FALSE)
|
||||
|
||||
# Require user to set wfassoc_ROOT
|
||||
if(NOT wfassoc_ROOT)
|
||||
message(FATAL_ERROR "wfassoc_ROOT must be set to the installation directory of wfassoc")
|
||||
endif()
|
||||
|
||||
# Check existence of required subdirectories
|
||||
if(NOT EXISTS ${wfassoc_ROOT})
|
||||
message(FATAL_ERROR "wfassoc_ROOT directory does not exist: ${wfassoc_ROOT}")
|
||||
endif()
|
||||
|
||||
set(wfassoc_INCLUDE_DIR ${wfassoc_ROOT}/include)
|
||||
set(wfassoc_LIB_DIR ${wfassoc_ROOT}/lib)
|
||||
set(wfassoc_BIN_DIR ${wfassoc_ROOT}/bin)
|
||||
|
||||
# Find header files
|
||||
if(EXISTS ${wfassoc_INCLUDE_DIR}/wfassoc.h AND EXISTS ${wfassoc_INCLUDE_DIR}/wfassoc++.h)
|
||||
set(wfassoc_INCLUDE_DIRS ${wfassoc_INCLUDE_DIR})
|
||||
else()
|
||||
message(SEND_ERROR "Missing wfassoc header files in ${wfassoc_INCLUDE_DIR}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Find import library (.lib)
|
||||
find_file(wfassoc_LIBRARIES
|
||||
NAMES wfassoc_cdylib.dll.lib
|
||||
PATHS ${wfassoc_LIB_DIR}
|
||||
NO_DEFAULT_PATH
|
||||
DOC "wfassoc import library"
|
||||
)
|
||||
|
||||
if(NOT wfassoc_LIBRARIES)
|
||||
message(SEND_ERROR "Missing wfassoc import library (wfassoc_cdylib.dll.lib) in ${wfassoc_LIB_DIR}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Find DLL file
|
||||
find_file(wfassoc_DLL
|
||||
NAMES wfassoc_cdylib.dll
|
||||
PATHS ${wfassoc_BIN_DIR}
|
||||
NO_DEFAULT_PATH
|
||||
DOC "wfassoc dynamic library"
|
||||
)
|
||||
|
||||
if(NOT wfassoc_DLL)
|
||||
message(SEND_ERROR "Missing wfassoc DLL (wfassoc_cdylib.dll) in ${wfassoc_BIN_DIR}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Everything found
|
||||
set(wfassoc_FOUND TRUE)
|
||||
|
||||
# Mark variables as advanced for ccmake/cmake-gui
|
||||
mark_as_advanced(wfassoc_INCLUDE_DIRS wfassoc_LIBRARIES wfassoc_DLL)
|
||||
|
||||
# Create imported target for wfassoc
|
||||
if(wfassoc_FOUND AND NOT TARGET wfassoc::wfassoc)
|
||||
add_library(wfassoc::wfassoc SHARED IMPORTED)
|
||||
|
||||
# Set include directories
|
||||
set_target_properties(wfassoc::wfassoc PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES ${wfassoc_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
# Set import library location
|
||||
set_target_properties(wfassoc::wfassoc PROPERTIES
|
||||
IMPORTED_IMPLIB "${wfassoc_LIBRARIES}"
|
||||
IMPORTED_LOCATION "${wfassoc_DLL}"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Optional: Print status message
|
||||
if(wfassoc_FOUND)
|
||||
message(STATUS "Found wfassoc:")
|
||||
message(STATUS " Root : ${wfassoc_ROOT}")
|
||||
message(STATUS " Include : ${wfassoc_INCLUDE_DIRS}")
|
||||
message(STATUS " Library : ${wfassoc_LIBRARIES}")
|
||||
message(STATUS " DLL : ${wfassoc_DLL}")
|
||||
endif()
|
||||
@@ -0,0 +1,19 @@
|
||||
# qwfassoc/cmake
|
||||
|
||||
This directory holds CMake helper modules used by the `qwfassoc` project.
|
||||
|
||||
## `Findwfassoc.cmake`
|
||||
|
||||
This file is a verbatim copy of the upstream `Findwfassoc.cmake` shipped with
|
||||
the wfassoc C dynamic library, located at:
|
||||
|
||||
```
|
||||
wfassoc-cdylib/cbindgen/Findwfassoc.cmake
|
||||
```
|
||||
|
||||
The copy is committed here so that `qwfassoc` can locate the wfassoc library
|
||||
through a standard `find_package(wfassoc)` call without depending on the source
|
||||
tree layout at configure time.
|
||||
|
||||
To keep this copy in sync with the upstream version, re-run the copy command
|
||||
shown above whenever `wfassoc-cdylib/cbindgen/Findwfassoc.cmake` is updated.
|
||||
@@ -0,0 +1,48 @@
|
||||
# qwfassoc-standalone: executable that uses the qwfassoc library to reproduce
|
||||
# the original tabbed wfassoc configurator.
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
set(QWFASSOC_STANDALONE_SOURCES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/main_window.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/manifest_parser.cpp"
|
||||
)
|
||||
|
||||
set(QWFASSOC_STANDALONE_HEADERS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/main_window.h"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/manifest_parser.h"
|
||||
)
|
||||
|
||||
set(QWFASSOC_STANDALONE_UI
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/main_window.ui"
|
||||
)
|
||||
|
||||
add_executable(qwfassoc-standalone WIN32
|
||||
${QWFASSOC_STANDALONE_SOURCES}
|
||||
${QWFASSOC_STANDALONE_HEADERS}
|
||||
${QWFASSOC_STANDALONE_UI}
|
||||
)
|
||||
|
||||
target_include_directories(qwfassoc-standalone PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src"
|
||||
)
|
||||
|
||||
target_link_libraries(qwfassoc-standalone PRIVATE
|
||||
qwfassoc
|
||||
Qt6::Widgets
|
||||
toml11::toml11
|
||||
)
|
||||
|
||||
# Translation pipeline for the standalone executable. The library's strings
|
||||
# are translated by qwfassoc's own .ts file; this one only covers the
|
||||
# executable-specific messages (CLI errors, tab titles, etc.).
|
||||
set(QWFASSOC_STANDALONE_TS_FILES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/i18n/qwfassoc-standalone_zh_CN.ts"
|
||||
)
|
||||
|
||||
qt6_add_translations(qwfassoc-standalone
|
||||
TS_FILES ${QWFASSOC_STANDALONE_TS_FILES}
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="zh_CN">
|
||||
</TS>
|
||||
@@ -0,0 +1,156 @@
|
||||
#include <QApplication>
|
||||
#include <QCommandLineOption>
|
||||
#include <QCommandLineParser>
|
||||
#include <QLocale>
|
||||
#include <QMessageBox>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QTranslator>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include <wfassoc++.h>
|
||||
|
||||
#include "main_window.h"
|
||||
#include "manifest_parser.h"
|
||||
#include "scope.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Context used for translatable strings that live outside of any QObject.
|
||||
constexpr const char* kTranslationContext = "qwfassoc-standalone";
|
||||
|
||||
// Show a modal error dialog with the given message and return a non-zero
|
||||
// exit code. Used for the various fatal conditions that may occur before the
|
||||
// main dialog can be shown.
|
||||
int fatal(QWidget* parent, const QString& message) {
|
||||
QMessageBox::critical(parent, QApplication::applicationName(), message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Convert the --for command line value to a TargetScope. Throws if the value
|
||||
// is not one of the accepted strings.
|
||||
qwfassoc::TargetScope parseScope(const QString& value) {
|
||||
const QString normalized = value.trimmed().toLower();
|
||||
if (normalized == QStringLiteral("user")) {
|
||||
return qwfassoc::TargetScope::User;
|
||||
}
|
||||
if (normalized == QStringLiteral("system")) {
|
||||
return qwfassoc::TargetScope::System;
|
||||
}
|
||||
throw std::runtime_error(
|
||||
"Invalid value for --for. Use \"user\" or \"system\".");
|
||||
}
|
||||
|
||||
// Install the translation(s) matching the user's preferred UI language, if
|
||||
// any. Both the qwfassoc library's .qm and this executable's .qm are loaded
|
||||
// (their .ts files live under the per-project i18n/ directories and are
|
||||
// embedded under the ":/i18n" resource prefix by qt6_add_translations).
|
||||
void installTranslators(QApplication& app) {
|
||||
const QStringList uiLanguages = QLocale::system().uiLanguages();
|
||||
for (const QString& locale : uiLanguages) {
|
||||
const QString name = QLocale(locale).name();
|
||||
|
||||
QTranslator* libTranslator = new QTranslator(&app);
|
||||
if (libTranslator->load(QStringLiteral(":/i18n/qwfassoc_") + name)) {
|
||||
app.installTranslator(libTranslator);
|
||||
}
|
||||
|
||||
QTranslator* appTranslator = new QTranslator(&app);
|
||||
if (appTranslator->load(
|
||||
QStringLiteral(":/i18n/qwfassoc-standalone_") + name)) {
|
||||
app.installTranslator(appTranslator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
QApplication::setApplicationName(QStringLiteral("qwfassoc-standalone"));
|
||||
|
||||
// Install available translations before any translatable string is
|
||||
// resolved so that tr() and QCoreApplication::translate() pick up the
|
||||
// right language.
|
||||
installTranslators(app);
|
||||
|
||||
QApplication::setApplicationDisplayName(
|
||||
QCoreApplication::translate(kTranslationContext,
|
||||
"qwfassoc - wfassoc Configurator"));
|
||||
|
||||
// Parse command line arguments using Qt's built-in parser.
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription(
|
||||
QCoreApplication::translate(
|
||||
kTranslationContext,
|
||||
"Qt-based GUI executable for the wfassoc library."));
|
||||
parser.addHelpOption();
|
||||
|
||||
QCommandLineOption manifestOption(
|
||||
QStringList() << QStringLiteral("c") << QStringLiteral("manifest"),
|
||||
QCoreApplication::translate(kTranslationContext,
|
||||
"Path to the application manifest TOML file."),
|
||||
QStringLiteral("manifest"));
|
||||
QCommandLineOption forOption(
|
||||
QStringList() << QStringLiteral("f") << QStringLiteral("for"),
|
||||
QCoreApplication::translate(kTranslationContext,
|
||||
"Target scope: \"user\" or \"system\"."),
|
||||
QStringLiteral("scope"));
|
||||
|
||||
parser.addOption(manifestOption);
|
||||
parser.addOption(forOption);
|
||||
parser.process(app);
|
||||
|
||||
// Validate that both mandatory options were provided with sane values.
|
||||
const QString manifestPath = parser.value(manifestOption);
|
||||
const QString forValue = parser.value(forOption);
|
||||
|
||||
if (manifestPath.isEmpty()) {
|
||||
return fatal(nullptr,
|
||||
QCoreApplication::translate(
|
||||
kTranslationContext,
|
||||
"The --manifest/-c option is required."));
|
||||
}
|
||||
if (forValue.isEmpty()) {
|
||||
return fatal(nullptr,
|
||||
QCoreApplication::translate(
|
||||
kTranslationContext,
|
||||
"The --for/-f option is required."));
|
||||
}
|
||||
|
||||
qwfassoc::TargetScope scope;
|
||||
try {
|
||||
scope = parseScope(forValue);
|
||||
} catch (const std::exception& e) {
|
||||
return fatal(nullptr, QString::fromUtf8(e.what()));
|
||||
}
|
||||
|
||||
// Initialize the wfassoc runtime. WFStartup must run before most other
|
||||
// wfassoc calls; if it fails we cannot proceed.
|
||||
if (!wfassoc::WFStartup()) {
|
||||
return fatal(nullptr,
|
||||
QString::fromUtf8(wfassoc::WFGetLastError()));
|
||||
}
|
||||
|
||||
// Build the manifest -> schema -> program pipeline and run the dialog.
|
||||
// Program construction consumes the schema (move) and performs the deep
|
||||
// validation (identifier format, dangling references, etc.).
|
||||
int exitCode = 0;
|
||||
try {
|
||||
qwfassoc::Manifest manifest =
|
||||
qwfassoc::parseManifestFile(manifestPath.toStdString());
|
||||
wfassocpp::Schema schema = qwfassoc::buildSchema(manifest);
|
||||
wfassocpp::Program program(std::move(schema));
|
||||
|
||||
qwfassoc::MainWindow window(std::move(program), scope);
|
||||
exitCode = window.exec();
|
||||
} catch (const std::exception& e) {
|
||||
wfassoc::WFShutdown();
|
||||
return fatal(nullptr, QString::fromUtf8(e.what()));
|
||||
}
|
||||
|
||||
wfassoc::WFShutdown();
|
||||
return exitCode;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "main_window.h"
|
||||
#include "ui_main_window.h"
|
||||
|
||||
#include "application_widget.h"
|
||||
#include "association_widget.h"
|
||||
#include "icon_utils.h"
|
||||
|
||||
#include <QTabWidget>
|
||||
|
||||
namespace qwfassoc {
|
||||
|
||||
MainWindow::MainWindow(wfassocpp::Program program,
|
||||
TargetScope scope,
|
||||
QWidget* parent)
|
||||
: QDialog(parent),
|
||||
ui_(new Ui::MainWindow),
|
||||
appTab_(nullptr),
|
||||
assocTab_(nullptr),
|
||||
program_(std::move(program)),
|
||||
scope_(scope) {
|
||||
ui_->setupUi(this);
|
||||
|
||||
// Resolve program metadata that several labels depend on.
|
||||
programName_ = QString::fromUtf8(program_.ResolveName());
|
||||
|
||||
{
|
||||
auto iconRc = program_.ResolveIcon();
|
||||
auto handle = iconRc.GetIcon();
|
||||
programIcon_ = icon_utils::fromHicon(handle);
|
||||
}
|
||||
|
||||
// Compose the window title and icon.
|
||||
setWindowTitle(tr("%1 Options").arg(programName_));
|
||||
if (!programIcon_.isNull()) {
|
||||
setWindowIcon(QIcon(programIcon_));
|
||||
}
|
||||
|
||||
// Build the two tab pages from the library widgets.
|
||||
appTab_ = new ApplicationWidget(this);
|
||||
assocTab_ = new AssociationWidget(this);
|
||||
|
||||
ui_->tabWidget->addTab(appTab_, tr("Applications"));
|
||||
ui_->tabWidget->addTab(assocTab_, tr("File Associations"));
|
||||
|
||||
// Two-phase initialization. The standalone executable wants the OK and
|
||||
// Cancel buttons visible because they drive dialog acceptance.
|
||||
appTab_->setConfig({&program_, scope_});
|
||||
assocTab_->setConfig({&program_, scope_, /*showOkCancelButtons=*/true});
|
||||
|
||||
// Wire widget signals so that any wfassoc change refreshes both pages,
|
||||
// and the association widget can request dialog closure.
|
||||
connect(appTab_, &ApplicationWidget::changed, this,
|
||||
&MainWindow::onAnyChanged);
|
||||
connect(assocTab_, &AssociationWidget::changed, this,
|
||||
&MainWindow::onAnyChanged);
|
||||
connect(assocTab_, &AssociationWidget::finished, this,
|
||||
&MainWindow::onFinished);
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() = default;
|
||||
|
||||
void MainWindow::onAnyChanged() {
|
||||
if (appTab_ != nullptr) {
|
||||
appTab_->refresh();
|
||||
}
|
||||
if (assocTab_ != nullptr) {
|
||||
assocTab_->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onFinished(bool accepted) {
|
||||
if (accepted) {
|
||||
accept();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace qwfassoc
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
#ifndef QWFASSOC_STANDALONE_MAIN_WINDOW_H_
|
||||
#define QWFASSOC_STANDALONE_MAIN_WINDOW_H_
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPixmap>
|
||||
#include <QString>
|
||||
|
||||
#include <wfassoc++.h>
|
||||
|
||||
#include "scope.h"
|
||||
|
||||
namespace Ui {
|
||||
class MainWindow;
|
||||
}
|
||||
|
||||
namespace qwfassoc {
|
||||
class ApplicationWidget;
|
||||
class AssociationWidget;
|
||||
}
|
||||
|
||||
namespace qwfassoc {
|
||||
|
||||
// Top-level dialog used by the qwfassoc-standalone executable. Hosts the two
|
||||
// reusable widgets from the qwfassoc library inside a QTabWidget and wires
|
||||
// their changed() / finished() signals together.
|
||||
class MainWindow : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(wfassocpp::Program program,
|
||||
TargetScope scope,
|
||||
QWidget* parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
private slots:
|
||||
// Called whenever one of the embedded widgets reports that wfassoc state
|
||||
// has changed. Refreshes both widgets so they stay in sync.
|
||||
void onAnyChanged();
|
||||
// Called when the association widget asks the dialog to close.
|
||||
void onFinished(bool accepted);
|
||||
|
||||
private:
|
||||
Ui::MainWindow* ui_;
|
||||
ApplicationWidget* appTab_;
|
||||
AssociationWidget* assocTab_;
|
||||
|
||||
wfassocpp::Program program_;
|
||||
TargetScope scope_;
|
||||
|
||||
QString programName_;
|
||||
QPixmap programIcon_;
|
||||
};
|
||||
|
||||
} // namespace qwfassoc
|
||||
|
||||
#endif // QWFASSOC_STANDALONE_MAIN_WINDOW_H_
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QDialog" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>480</width>
|
||||
<height>600</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>480</width>
|
||||
<height>600</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>480</width>
|
||||
<height>600</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Options</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="mainLayout">
|
||||
<property name="leftMargin">
|
||||
<number>9</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>9</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>9</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>9</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,144 @@
|
||||
#include "manifest_parser.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <toml.hpp>
|
||||
|
||||
namespace qwfassoc {
|
||||
|
||||
// region: TOML Parsing
|
||||
|
||||
Manifest parseManifestFile(const std::string& path) {
|
||||
toml::value root;
|
||||
try {
|
||||
root = toml::parse(path);
|
||||
} catch (const std::exception& e) {
|
||||
// toml::parse already produces a descriptive message including file
|
||||
// path and line number; just propagate it wrapped for context.
|
||||
throw std::runtime_error(
|
||||
std::string("Failed to parse manifest TOML file: ") + e.what());
|
||||
}
|
||||
|
||||
Manifest manifest;
|
||||
|
||||
// Helper lambda: read a string field, re-throwing with a clearer message.
|
||||
auto readString = [](const toml::value& v,
|
||||
const std::string& key) -> std::string {
|
||||
try {
|
||||
return toml::find<std::string>(v, key);
|
||||
} catch (const std::exception& e) {
|
||||
throw std::runtime_error(
|
||||
"Manifest field \"" + key +
|
||||
"\" is missing or is not a string: " + e.what());
|
||||
}
|
||||
};
|
||||
|
||||
// Required top-level scalar fields.
|
||||
manifest.identifier = readString(root, "identifier");
|
||||
manifest.path = readString(root, "path");
|
||||
manifest.clsid = readString(root, "clsid");
|
||||
|
||||
// Optional top-level scalar fields.
|
||||
if (root.contains("name")) {
|
||||
manifest.name = readString(root, "name");
|
||||
}
|
||||
if (root.contains("icon")) {
|
||||
manifest.icon = readString(root, "icon");
|
||||
}
|
||||
if (root.contains("behavior")) {
|
||||
manifest.behavior = readString(root, "behavior");
|
||||
}
|
||||
|
||||
// Helper lambda: copy a TOML table of {string -> string} into a std::map.
|
||||
auto readStringTable =
|
||||
[](const toml::value& parent,
|
||||
const std::string& key) -> std::map<std::string, std::string> {
|
||||
if (!parent.contains(key)) {
|
||||
return {};
|
||||
}
|
||||
std::map<std::string, std::string> out;
|
||||
try {
|
||||
// Keep the sub-value alive as a local so that the table reference
|
||||
// obtained from as_table() stays valid for the loop below,
|
||||
// regardless of whether toml::find returns by reference or by
|
||||
// value in the toml11 version that is linked.
|
||||
const toml::value& sub = toml::find(parent, key);
|
||||
for (const auto& [k, v] : sub.as_table()) {
|
||||
out.emplace(k, v.as_string());
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
throw std::runtime_error(
|
||||
"Manifest table \"" + key +
|
||||
"\" contains an invalid entry: " + e.what());
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
manifest.strs = readStringTable(root, "strs");
|
||||
manifest.icons = readStringTable(root, "icons");
|
||||
manifest.behaviors = readStringTable(root, "behaviors");
|
||||
|
||||
// Extension table. Each entry is itself a table with name/icon/behavior.
|
||||
if (root.contains("exts")) {
|
||||
try {
|
||||
const toml::value& exts_value = toml::find(root, "exts");
|
||||
for (const auto& [ext_key, ext_value] : exts_value.as_table()) {
|
||||
ManifestExt ext;
|
||||
ext.name = readString(ext_value, "name");
|
||||
ext.icon = readString(ext_value, "icon");
|
||||
ext.behavior = readString(ext_value, "behavior");
|
||||
manifest.exts.emplace(ext_key, std::move(ext));
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
throw std::runtime_error(
|
||||
"Manifest \"exts\" table contains an invalid entry: " +
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Schema Conversion
|
||||
|
||||
wfassocpp::Schema buildSchema(const Manifest& manifest) {
|
||||
wfassocpp::Schema schema;
|
||||
|
||||
// The wfassocpp wrappers translate any underlying failure into a
|
||||
// std::runtime_error via _Check, so we let those propagate untouched.
|
||||
schema.SetIdentifier(manifest.identifier.c_str());
|
||||
schema.SetPath(manifest.path.c_str());
|
||||
schema.SetClsid(manifest.clsid.c_str());
|
||||
|
||||
// Optional fields: passing nullptr tells wfassoc to clear the value.
|
||||
schema.SetName(manifest.name.has_value() ? manifest.name->c_str()
|
||||
: nullptr);
|
||||
schema.SetIcon(manifest.icon.has_value() ? manifest.icon->c_str()
|
||||
: nullptr);
|
||||
schema.SetBehavior(manifest.behavior.has_value() ? manifest.behavior->c_str()
|
||||
: nullptr);
|
||||
|
||||
for (const auto& [key, value] : manifest.strs) {
|
||||
schema.AddStr(key.c_str(), value.c_str());
|
||||
}
|
||||
for (const auto& [key, value] : manifest.icons) {
|
||||
schema.AddIcon(key.c_str(), value.c_str());
|
||||
}
|
||||
for (const auto& [key, value] : manifest.behaviors) {
|
||||
schema.AddBehavior(key.c_str(), value.c_str());
|
||||
}
|
||||
for (const auto& [key, value] : manifest.exts) {
|
||||
schema.AddExt(key.c_str(),
|
||||
value.name.c_str(),
|
||||
value.icon.c_str(),
|
||||
value.behavior.c_str());
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
} // namespace qwfassoc
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#ifndef QWFASSOC_STANDALONE_MANIFEST_PARSER_H_
|
||||
#define QWFASSOC_STANDALONE_MANIFEST_PARSER_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <wfassoc++.h>
|
||||
|
||||
#include "manifest.h"
|
||||
|
||||
namespace qwfassoc {
|
||||
|
||||
// Parse a manifest TOML file from disk into a Manifest value.
|
||||
// Throws std::runtime_error on any IO or TOML syntax error.
|
||||
Manifest parseManifestFile(const std::string& path);
|
||||
|
||||
// Build a wfassocpp::Schema from a manifest value.
|
||||
// Throws std::runtime_error (originating from wfassocpp::_Check) when the
|
||||
// wfassoc library rejects an operation, e.g. on duplicate keys or dangling
|
||||
// references.
|
||||
wfassocpp::Schema buildSchema(const Manifest& manifest);
|
||||
|
||||
} // namespace qwfassoc
|
||||
|
||||
#endif // QWFASSOC_STANDALONE_MANIFEST_PARSER_H_
|
||||
@@ -0,0 +1,58 @@
|
||||
# qwfassoc: shared library exporting reusable Qt widgets that wrap wfassoc.
|
||||
|
||||
# Qt 6 requires C++17 at minimum.
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
set(QWFASSOC_SOURCES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/application_widget.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/association_widget.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/icon_utils.cpp"
|
||||
)
|
||||
|
||||
set(QWFASSOC_HEADERS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/qwfassoc_global.h"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/scope.h"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/manifest.h"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/application_widget.h"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/association_widget.h"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/icon_utils.h"
|
||||
)
|
||||
|
||||
set(QWFASSOC_UI
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/application_widget.ui"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/association_widget.ui"
|
||||
)
|
||||
|
||||
add_library(qwfassoc SHARED
|
||||
${QWFASSOC_SOURCES}
|
||||
${QWFASSOC_HEADERS}
|
||||
${QWFASSOC_UI}
|
||||
)
|
||||
|
||||
# QWFASSOC_LIBRARY switches QWFASSOC_EXPORT from import to export mode.
|
||||
target_compile_definitions(qwfassoc PRIVATE QWFASSOC_LIBRARY)
|
||||
|
||||
# Consumers (and the library itself) need to find the public headers under
|
||||
# src/. PUBLIC propagates the include path to anyone linking against qwfassoc.
|
||||
target_include_directories(qwfassoc PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src"
|
||||
)
|
||||
|
||||
target_link_libraries(qwfassoc PUBLIC
|
||||
Qt6::Widgets
|
||||
wfassoc::wfassoc
|
||||
)
|
||||
|
||||
# Translation pipeline for the library. qt6_add_translations() runs lupdate
|
||||
# against the target's sources and embeds the lrelease output under the
|
||||
# ":/i18n" resource prefix, where installTranslators() in the executable
|
||||
# looks it up at runtime.
|
||||
set(QWFASSOC_TS_FILES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/i18n/qwfassoc_zh_CN.ts"
|
||||
)
|
||||
|
||||
qt6_add_translations(qwfassoc
|
||||
TS_FILES ${QWFASSOC_TS_FILES}
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="zh_CN">
|
||||
</TS>
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "application_widget.h"
|
||||
#include "ui_application_widget.h"
|
||||
#include "icon_utils.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace qwfassoc {
|
||||
|
||||
namespace {
|
||||
|
||||
// Convert the library's TargetScope enum to the wfassocpp::Scope value used by
|
||||
// the program APIs (register / unregister / link / unlink / is_registered).
|
||||
wfassocpp::Scope toWfassocScope(TargetScope scope) {
|
||||
return scope == TargetScope::User ? wfassocpp::Scope::User
|
||||
: wfassocpp::Scope::System;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ApplicationWidget::ApplicationWidget(QWidget* parent)
|
||||
: QWidget(parent), ui_(new Ui::ApplicationWidget) {
|
||||
ui_->setupUi(this);
|
||||
|
||||
connect(ui_->installButton, &QPushButton::clicked, this,
|
||||
&ApplicationWidget::onInstallClicked);
|
||||
connect(ui_->uninstallButton, &QPushButton::clicked, this,
|
||||
&ApplicationWidget::onUninstallClicked);
|
||||
|
||||
// Until setConfig() is called we have no program to operate on; keep the
|
||||
// whole widget disabled.
|
||||
setEnabled(false);
|
||||
}
|
||||
|
||||
ApplicationWidget::~ApplicationWidget() = default;
|
||||
|
||||
void ApplicationWidget::setConfig(const Config& config) {
|
||||
program_ = config.program;
|
||||
scope_ = config.scope;
|
||||
|
||||
if (program_ != nullptr) {
|
||||
// Resolve program metadata that several labels depend on. These calls
|
||||
// may throw std::runtime_error on failure; the caller is expected to
|
||||
// wrap setConfig() in a try/catch and present an error dialog.
|
||||
programName_ = QString::fromUtf8(program_->ResolveName());
|
||||
|
||||
auto iconRc = program_->ResolveIcon();
|
||||
auto handle = iconRc.GetIcon();
|
||||
programIcon_ = icon_utils::fromHicon(handle);
|
||||
|
||||
if (!programIcon_.isNull()) {
|
||||
ui_->appIconLabel->setPixmap(
|
||||
programIcon_.scaled(32, 32, Qt::KeepAspectRatio,
|
||||
Qt::SmoothTransformation));
|
||||
}
|
||||
ui_->appDescLabel->setText(
|
||||
tr("Install or uninstall %1 here.").arg(programName_));
|
||||
|
||||
setEnabled(true);
|
||||
refresh();
|
||||
} else {
|
||||
programName_.clear();
|
||||
programIcon_ = QPixmap();
|
||||
ui_->appIconLabel->setPixmap(QPixmap());
|
||||
ui_->appIconLabel->setText(QString());
|
||||
ui_->appDescLabel->setText(QString());
|
||||
setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void ApplicationWidget::refresh() {
|
||||
if (program_ == nullptr) {
|
||||
ui_->installButton->setEnabled(false);
|
||||
ui_->uninstallButton->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool registered = program_->IsRegistered(toWfassocScope(scope_));
|
||||
ui_->installButton->setEnabled(!registered);
|
||||
ui_->uninstallButton->setEnabled(registered);
|
||||
}
|
||||
|
||||
void ApplicationWidget::onInstallClicked() {
|
||||
if (program_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
program_->Register(toWfassocScope(scope_));
|
||||
} catch (const std::exception& e) {
|
||||
QMessageBox::critical(this, tr("Error"), QString::fromUtf8(e.what()));
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::information(this, tr("Information"),
|
||||
tr("Application installed successfully."));
|
||||
refresh();
|
||||
emit changed();
|
||||
}
|
||||
|
||||
void ApplicationWidget::onUninstallClicked() {
|
||||
if (program_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
program_->Unregister(toWfassocScope(scope_));
|
||||
} catch (const std::exception& e) {
|
||||
QMessageBox::critical(this, tr("Error"), QString::fromUtf8(e.what()));
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::information(this, tr("Information"),
|
||||
tr("Application uninstalled successfully."));
|
||||
refresh();
|
||||
emit changed();
|
||||
}
|
||||
|
||||
} // namespace qwfassoc
|
||||
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
#ifndef QWFASSOC_APPLICATION_WIDGET_H_
|
||||
#define QWFASSOC_APPLICATION_WIDGET_H_
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
#include <wfassoc++.h>
|
||||
|
||||
#include "qwfassoc_global.h"
|
||||
#include "scope.h"
|
||||
|
||||
namespace Ui {
|
||||
class ApplicationWidget;
|
||||
}
|
||||
|
||||
namespace qwfassoc {
|
||||
|
||||
// Widget exposing install / uninstall actions for a single wfassoc program.
|
||||
//
|
||||
// The widget follows the two-phase initialization pattern required by Qt
|
||||
// Designer promoted widgets: the constructor only takes a parent, and the
|
||||
// caller must invoke setConfig() with the target program and scope before the
|
||||
// widget becomes usable. Until setConfig() is called the widget is disabled.
|
||||
class QWFASSOC_EXPORT ApplicationWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// Configuration bundle passed to setConfig().
|
||||
struct Config {
|
||||
// Non-owning pointer to the wfassoc program. Must outlive the widget.
|
||||
wfassocpp::Program* program = nullptr;
|
||||
// Scope that install/unregister operations apply to.
|
||||
TargetScope scope = TargetScope::User;
|
||||
};
|
||||
|
||||
explicit ApplicationWidget(QWidget* parent = nullptr);
|
||||
~ApplicationWidget() override;
|
||||
|
||||
// Two-phase initialization. Calling this with a non-null program enables
|
||||
// the widget and triggers an initial refresh. Calling it with a null
|
||||
// program (or not calling it at all) leaves the widget disabled.
|
||||
void setConfig(const Config& config);
|
||||
|
||||
// Re-query the live wfassoc state and update the enabled state of the
|
||||
// install / uninstall buttons. Called automatically by setConfig() and
|
||||
// also intended to be called by the host whenever another component has
|
||||
// mutated wfassoc state.
|
||||
void refresh();
|
||||
|
||||
signals:
|
||||
// Emitted whenever the user performs an action that mutates wfassoc
|
||||
// state (i.e. install or uninstall). The host should refresh every
|
||||
// widget that depends on wfassoc state in response.
|
||||
void changed();
|
||||
|
||||
private slots:
|
||||
void onInstallClicked();
|
||||
void onUninstallClicked();
|
||||
|
||||
private:
|
||||
Ui::ApplicationWidget* ui_;
|
||||
wfassocpp::Program* program_ = nullptr;
|
||||
TargetScope scope_ = TargetScope::User;
|
||||
|
||||
// Cached metadata used to fill the widget labels.
|
||||
QString programName_;
|
||||
QPixmap programIcon_;
|
||||
};
|
||||
|
||||
} // namespace qwfassoc
|
||||
|
||||
#endif // QWFASSOC_APPLICATION_WIDGET_H_
|
||||
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ApplicationWidget</class>
|
||||
<widget class="QWidget" name="ApplicationWidget">
|
||||
<layout class="QVBoxLayout" name="mainLayout">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Install and Uninstall</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="groupLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="headerLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="appIconLabel">
|
||||
<property name="text">
|
||||
<string notr="true">[icon]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="appDescLabel">
|
||||
<property name="text">
|
||||
<string>Install or uninstall the application here.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="actionLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="installButton">
|
||||
<property name="text">
|
||||
<string>Install</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="uninstallButton">
|
||||
<property name="text">
|
||||
<string>Uninstall</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,438 @@
|
||||
#include "association_widget.h"
|
||||
#include "ui_association_widget.h"
|
||||
#include "icon_utils.h"
|
||||
|
||||
#include <QHeaderView>
|
||||
#include <QMessageBox>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QPushButton>
|
||||
#include <QTableWidgetItem>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace qwfassoc {
|
||||
|
||||
namespace {
|
||||
|
||||
// Convert the library's TargetScope enum to the wfassocpp::Scope value used by
|
||||
// the program APIs (register / unregister / link / unlink / is_registered).
|
||||
wfassocpp::Scope toWfassocScope(TargetScope scope) {
|
||||
return scope == TargetScope::User ? wfassocpp::Scope::User
|
||||
: wfassocpp::Scope::System;
|
||||
}
|
||||
|
||||
// Decide which CellState a queried ExtStatus corresponds to, given the
|
||||
// resolved "self" name for this extension. We treat the cell as Self when the
|
||||
// resolved display name matches our own; otherwise it is treated as Other.
|
||||
detail::CellState classifyCell(const QString& selfName,
|
||||
const QString& observedName) {
|
||||
return observedName == selfName ? detail::CellState::Self
|
||||
: detail::CellState::Other;
|
||||
}
|
||||
|
||||
// Effective icon to draw for a cell, based on its state.
|
||||
QPixmap effectiveCellIcon(const detail::ExtRow& row,
|
||||
const detail::CellData& cell) {
|
||||
using detail::CellState;
|
||||
switch (cell.state) {
|
||||
case CellState::Blank:
|
||||
return QPixmap();
|
||||
case CellState::Self:
|
||||
return row.selfIcon;
|
||||
case CellState::Other:
|
||||
return cell.icon;
|
||||
}
|
||||
return QPixmap();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AssociationWidget::AssociationWidget(QWidget* parent)
|
||||
: QWidget(parent), ui_(new Ui::AssociationWidget) {
|
||||
ui_->setupUi(this);
|
||||
|
||||
// Fetch the current user name for the second column header. The USERNAME
|
||||
// environment variable is good enough on Windows; fall back to a static
|
||||
// translatable placeholder if it is unset for any reason.
|
||||
userName_ = QProcessEnvironment::systemEnvironment().value(
|
||||
QStringLiteral("USERNAME"), tr("User"));
|
||||
|
||||
// Give the table as much vertical room as possible inside its layout.
|
||||
ui_->mainLayout->setStretch(2, 1);
|
||||
|
||||
// Reasonable default column widths so dotted extensions and ProgId names
|
||||
// stay readable in the 480px dialog the widget typically lives in.
|
||||
ui_->assocTable->setColumnWidth(0, 90);
|
||||
ui_->assocTable->setColumnWidth(1, 175);
|
||||
ui_->assocTable->setColumnWidth(2, 175);
|
||||
ui_->assocTable->verticalHeader()->setVisible(false);
|
||||
ui_->assocTable->setShowGrid(true);
|
||||
|
||||
connect(ui_->selectUserButton, &QPushButton::clicked, this,
|
||||
&AssociationWidget::onSelectUserClicked);
|
||||
connect(ui_->selectSystemButton, &QPushButton::clicked, this,
|
||||
&AssociationWidget::onSelectSystemClicked);
|
||||
connect(ui_->assocTable, &QTableWidget::cellClicked, this,
|
||||
&AssociationWidget::onCellClicked);
|
||||
connect(ui_->okButton, &QPushButton::clicked, this,
|
||||
&AssociationWidget::onOkClicked);
|
||||
connect(ui_->cancelButton, &QPushButton::clicked, this,
|
||||
&AssociationWidget::onCancelClicked);
|
||||
connect(ui_->applyButton, &QPushButton::clicked, this,
|
||||
&AssociationWidget::onApplyClicked);
|
||||
|
||||
// Until setConfig() is called we have no program to operate on; keep the
|
||||
// whole widget disabled.
|
||||
setEnabled(false);
|
||||
}
|
||||
|
||||
AssociationWidget::~AssociationWidget() = default;
|
||||
|
||||
void AssociationWidget::setConfig(const Config& config) {
|
||||
program_ = config.program;
|
||||
scope_ = config.scope;
|
||||
showOkCancelButtons_ = config.showOkCancelButtons;
|
||||
|
||||
ui_->okButton->setVisible(showOkCancelButtons_);
|
||||
ui_->cancelButton->setVisible(showOkCancelButtons_);
|
||||
|
||||
if (program_ != nullptr) {
|
||||
programName_ = QString::fromUtf8(program_->ResolveName());
|
||||
ui_->assocHeaderLabel->setText(
|
||||
tr("File types associated with %1:").arg(programName_));
|
||||
|
||||
QStringList headers;
|
||||
headers << tr("Type") << userName_ << tr("All Users");
|
||||
ui_->assocTable->setHorizontalHeaderLabels(headers);
|
||||
|
||||
setEnabled(true);
|
||||
refresh();
|
||||
} else {
|
||||
programName_.clear();
|
||||
ui_->assocHeaderLabel->setText(QString());
|
||||
ui_->assocTable->setRowCount(0);
|
||||
rows_.clear();
|
||||
setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void AssociationWidget::refresh() {
|
||||
if (program_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
updateEnabledState();
|
||||
rebuildTable();
|
||||
}
|
||||
|
||||
void AssociationWidget::updateEnabledState() {
|
||||
if (program_ == nullptr) {
|
||||
ui_->selectUserButton->setEnabled(false);
|
||||
ui_->selectSystemButton->setEnabled(false);
|
||||
ui_->assocTable->setEnabled(false);
|
||||
ui_->applyButton->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool registered = program_->IsRegistered(toWfassocScope(scope_));
|
||||
const bool userColumnActive = registered;
|
||||
const bool systemColumnActive = registered && isSystemColumnEnabled();
|
||||
|
||||
ui_->selectUserButton->setEnabled(userColumnActive);
|
||||
ui_->selectSystemButton->setEnabled(systemColumnActive);
|
||||
ui_->assocTable->setEnabled(registered);
|
||||
// The Apply button enable state is driven by pending changes too; only
|
||||
// touch it here to make sure it's disabled when nothing is registered.
|
||||
if (!registered) {
|
||||
ui_->applyButton->setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void AssociationWidget::rebuildTable() {
|
||||
if (program_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshing_ = true;
|
||||
|
||||
rows_.clear();
|
||||
const size_t count = program_->ExtsLen();
|
||||
rows_.reserve(count);
|
||||
|
||||
ui_->assocTable->setRowCount(static_cast<int>(count));
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
detail::ExtRow row;
|
||||
row.index = i;
|
||||
|
||||
// Self extension info: dotted body, display name and cached icon.
|
||||
auto selfExt = program_->ResolveExt(i);
|
||||
row.extBody = QString::fromUtf8(selfExt.GetExt());
|
||||
row.dottedExt = QString::fromUtf8(selfExt.GetDottedExt());
|
||||
row.selfName = QString::fromUtf8(selfExt.GetName());
|
||||
row.selfIcon = icon_utils::fromHicon(selfExt.GetIcon());
|
||||
|
||||
// Query the user-view and system-view states. None means blank;
|
||||
// a match against our self name means Self; anything else is Other
|
||||
// and we keep the original name/icon around for display.
|
||||
auto userStatus = program_->QueryExt(wfassocpp::View::User, i);
|
||||
if (userStatus) {
|
||||
const QString observedName =
|
||||
QString::fromUtf8(userStatus->GetName());
|
||||
row.initialUser.state = classifyCell(row.selfName, observedName);
|
||||
row.initialUser.name = observedName;
|
||||
row.initialUser.icon =
|
||||
icon_utils::fromHicon(userStatus->GetIcon());
|
||||
}
|
||||
|
||||
auto systemStatus = program_->QueryExt(wfassocpp::View::System, i);
|
||||
if (systemStatus) {
|
||||
const QString observedName =
|
||||
QString::fromUtf8(systemStatus->GetName());
|
||||
row.initialSystem.state =
|
||||
classifyCell(row.selfName, observedName);
|
||||
row.initialSystem.name = observedName;
|
||||
row.initialSystem.icon =
|
||||
icon_utils::fromHicon(systemStatus->GetIcon());
|
||||
}
|
||||
|
||||
row.pendingUser = row.initialUser;
|
||||
row.pendingSystem = row.initialSystem;
|
||||
|
||||
rows_.push_back(std::move(row));
|
||||
|
||||
// Create the QTableWidgetItem cells once; subsequent refreshes only
|
||||
// update their text/icon and flags.
|
||||
const int rowIdx = static_cast<int>(i);
|
||||
|
||||
auto* typeItem = new QTableWidgetItem(rows_.back().dottedExt);
|
||||
ui_->assocTable->setItem(rowIdx, 0, typeItem);
|
||||
|
||||
auto* userItem = new QTableWidgetItem;
|
||||
userItem->setTextAlignment(Qt::AlignCenter);
|
||||
ui_->assocTable->setItem(rowIdx, 1, userItem);
|
||||
|
||||
auto* systemItem = new QTableWidgetItem;
|
||||
systemItem->setTextAlignment(Qt::AlignCenter);
|
||||
ui_->assocTable->setItem(rowIdx, 2, systemItem);
|
||||
|
||||
refreshRowDisplay(rowIdx);
|
||||
}
|
||||
|
||||
refreshing_ = false;
|
||||
|
||||
updateApplyButtonEnabled();
|
||||
}
|
||||
|
||||
void AssociationWidget::refreshRowDisplay(int row) {
|
||||
using detail::CellState;
|
||||
|
||||
if (row < 0 || row >= static_cast<int>(rows_.size())) {
|
||||
return;
|
||||
}
|
||||
const detail::ExtRow& r = rows_[row];
|
||||
|
||||
// Column 0: hybrid icon (user-preferred) + dotted extension.
|
||||
QPixmap hybridIcon;
|
||||
if (r.pendingUser.state != CellState::Blank) {
|
||||
hybridIcon = effectiveCellIcon(r, r.pendingUser);
|
||||
} else if (r.pendingSystem.state != CellState::Blank) {
|
||||
hybridIcon = effectiveCellIcon(r, r.pendingSystem);
|
||||
}
|
||||
|
||||
QTableWidgetItem* typeItem = ui_->assocTable->item(row, 0);
|
||||
if (typeItem) {
|
||||
typeItem->setIcon(QIcon(hybridIcon));
|
||||
typeItem->setText(r.dottedExt);
|
||||
}
|
||||
|
||||
// Column 1: user scope display name.
|
||||
QTableWidgetItem* userItem = ui_->assocTable->item(row, 1);
|
||||
if (userItem) {
|
||||
userItem->setText(r.pendingUser.state == CellState::Blank
|
||||
? QString()
|
||||
: r.pendingUser.name);
|
||||
}
|
||||
|
||||
// Column 2: system scope display name. When the system column is
|
||||
// inactive (user-only run), the cells are flagged as disabled so that
|
||||
// clicks are ignored and the rendering is greyed out.
|
||||
QTableWidgetItem* systemItem = ui_->assocTable->item(row, 2);
|
||||
if (systemItem) {
|
||||
systemItem->setText(r.pendingSystem.state == CellState::Blank
|
||||
? QString()
|
||||
: r.pendingSystem.name);
|
||||
|
||||
const Qt::ItemFlags enabledFlags =
|
||||
Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
// Without Qt::ItemIsEnabled the cell renders disabled (greyed out)
|
||||
// and cellClicked is not emitted, so clicks are silently ignored.
|
||||
const Qt::ItemFlags disabledFlags = Qt::ItemIsSelectable;
|
||||
if (isSystemColumnEnabled()) {
|
||||
systemItem->setFlags(enabledFlags);
|
||||
} else {
|
||||
systemItem->setFlags(disabledFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AssociationWidget::updateApplyButtonEnabled() {
|
||||
bool dirty = false;
|
||||
for (const detail::ExtRow& r : rows_) {
|
||||
if (r.pendingUser.state != r.initialUser.state ||
|
||||
r.pendingSystem.state != r.initialSystem.state) {
|
||||
dirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ui_->applyButton->setEnabled(dirty);
|
||||
}
|
||||
|
||||
void AssociationWidget::toggleCell(int row, int column) {
|
||||
using detail::CellState;
|
||||
|
||||
if (refreshing_) {
|
||||
return;
|
||||
}
|
||||
if (row < 0 || row >= static_cast<int>(rows_.size())) {
|
||||
return;
|
||||
}
|
||||
|
||||
detail::CellData* cell = nullptr;
|
||||
const detail::ExtRow* rowPtr = &rows_[row];
|
||||
if (column == 1) {
|
||||
cell = &rows_[row].pendingUser;
|
||||
} else if (column == 2) {
|
||||
if (!isSystemColumnEnabled()) {
|
||||
return;
|
||||
}
|
||||
cell = &rows_[row].pendingSystem;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle: Self -> Blank, anything else -> Self.
|
||||
if (cell->state == CellState::Self) {
|
||||
cell->state = CellState::Blank;
|
||||
cell->name.clear();
|
||||
cell->icon = QPixmap();
|
||||
} else {
|
||||
cell->state = CellState::Self;
|
||||
cell->name = rowPtr->selfName;
|
||||
cell->icon = rowPtr->selfIcon;
|
||||
}
|
||||
|
||||
refreshRowDisplay(row);
|
||||
updateApplyButtonEnabled();
|
||||
}
|
||||
|
||||
void AssociationWidget::selectAllInScope(bool isUser) {
|
||||
using detail::CellState;
|
||||
|
||||
if (!isUser && !isSystemColumnEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Progressively select more. If there is at least one blank cell, the
|
||||
// first click only fills blanks; otherwise the click overrides cells
|
||||
// pointing at other handlers as well.
|
||||
bool hasBlank = false;
|
||||
for (detail::ExtRow& r : rows_) {
|
||||
const detail::CellData& cell = isUser ? r.pendingUser : r.pendingSystem;
|
||||
if (cell.state == CellState::Blank) {
|
||||
hasBlank = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (detail::ExtRow& r : rows_) {
|
||||
detail::CellData& cell = isUser ? r.pendingUser : r.pendingSystem;
|
||||
if (hasBlank) {
|
||||
if (cell.state == CellState::Blank) {
|
||||
cell.state = CellState::Self;
|
||||
cell.name = r.selfName;
|
||||
cell.icon = r.selfIcon;
|
||||
}
|
||||
} else if (cell.state != CellState::Self) {
|
||||
cell.state = CellState::Self;
|
||||
cell.name = r.selfName;
|
||||
cell.icon = r.selfIcon;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < rows_.size(); ++i) {
|
||||
refreshRowDisplay(static_cast<int>(i));
|
||||
}
|
||||
updateApplyButtonEnabled();
|
||||
}
|
||||
|
||||
void AssociationWidget::applyAllChanges() {
|
||||
// Walk through every row and commit any cell whose pending state differs
|
||||
// from the initial snapshot. wfassoc's link/unlink take an index rather
|
||||
// than a scope/view, so we map columns back to (scope, index) pairs.
|
||||
for (const detail::ExtRow& r : rows_) {
|
||||
if (r.pendingUser.state != r.initialUser.state) {
|
||||
if (r.pendingUser.state == detail::CellState::Self) {
|
||||
program_->LinkExt(wfassocpp::Scope::User, r.index);
|
||||
} else {
|
||||
program_->UnlinkExt(wfassocpp::Scope::User, r.index);
|
||||
}
|
||||
}
|
||||
if (r.pendingSystem.state != r.initialSystem.state) {
|
||||
if (r.pendingSystem.state == detail::CellState::Self) {
|
||||
program_->LinkExt(wfassocpp::Scope::System, r.index);
|
||||
} else {
|
||||
program_->UnlinkExt(wfassocpp::Scope::System, r.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-query and rebuild the table so the UI reflects the live registry.
|
||||
rebuildTable();
|
||||
}
|
||||
|
||||
bool AssociationWidget::isSystemColumnEnabled() const {
|
||||
return scope_ == TargetScope::System;
|
||||
}
|
||||
|
||||
void AssociationWidget::onSelectUserClicked() {
|
||||
selectAllInScope(/*isUser=*/true);
|
||||
}
|
||||
|
||||
void AssociationWidget::onSelectSystemClicked() {
|
||||
selectAllInScope(/*isUser=*/false);
|
||||
}
|
||||
|
||||
void AssociationWidget::onCellClicked(int row, int column) {
|
||||
toggleCell(row, column);
|
||||
}
|
||||
|
||||
void AssociationWidget::onOkClicked() {
|
||||
try {
|
||||
applyAllChanges();
|
||||
} catch (const std::exception& e) {
|
||||
QMessageBox::critical(this, tr("Error"), QString::fromUtf8(e.what()));
|
||||
// Sync the table with the live registry, since some changes may have
|
||||
// been committed before the failure.
|
||||
refresh();
|
||||
return;
|
||||
}
|
||||
emit changed();
|
||||
emit finished(/*accepted=*/true);
|
||||
}
|
||||
|
||||
void AssociationWidget::onCancelClicked() {
|
||||
emit finished(/*accepted=*/false);
|
||||
}
|
||||
|
||||
void AssociationWidget::onApplyClicked() {
|
||||
try {
|
||||
applyAllChanges();
|
||||
} catch (const std::exception& e) {
|
||||
QMessageBox::critical(this, tr("Error"), QString::fromUtf8(e.what()));
|
||||
refresh();
|
||||
return;
|
||||
}
|
||||
emit changed();
|
||||
}
|
||||
|
||||
} // namespace qwfassoc
|
||||
@@ -0,0 +1,160 @@
|
||||
#pragma once
|
||||
#ifndef QWFASSOC_ASSOCIATION_WIDGET_H_
|
||||
#define QWFASSOC_ASSOCIATION_WIDGET_H_
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <wfassoc++.h>
|
||||
|
||||
#include "qwfassoc_global.h"
|
||||
#include "scope.h"
|
||||
|
||||
namespace Ui {
|
||||
class AssociationWidget;
|
||||
}
|
||||
|
||||
namespace qwfassoc {
|
||||
|
||||
// Internal helper types used by AssociationWidget. They live in a `detail`
|
||||
// namespace to signal that they are not part of the public API even though
|
||||
// they need to be visible in the header.
|
||||
namespace detail {
|
||||
|
||||
enum class CellState {
|
||||
// The extension has no associated handler in this scope.
|
||||
Blank,
|
||||
// The extension is associated with the application described by the
|
||||
// current manifest.
|
||||
Self,
|
||||
// The extension is associated with some other application.
|
||||
Other,
|
||||
};
|
||||
|
||||
// Per-cell cached data.
|
||||
struct CellData {
|
||||
CellState state = CellState::Blank;
|
||||
// Display name. Only meaningful when state == Other, since the Self name
|
||||
// is shared per row (it does not change between cells).
|
||||
QString name;
|
||||
// Display icon. Used for both Self and Other states.
|
||||
QPixmap icon;
|
||||
};
|
||||
|
||||
// All data attached to a single row in the file association table.
|
||||
struct ExtRow {
|
||||
// wfassoc extension index, used when calling LinkExt/UnlinkExt/QueryExt.
|
||||
size_t index = 0;
|
||||
// Dotted extension body, e.g. ".jpg". Displayed in column 0.
|
||||
QString dottedExt;
|
||||
// Body without leading dot, e.g. "jpg".
|
||||
QString extBody;
|
||||
// The display name when this program is the handler.
|
||||
QString selfName;
|
||||
// The icon when this program is the handler.
|
||||
QPixmap selfIcon;
|
||||
|
||||
// Snapshot taken from the live registry at load/refresh time.
|
||||
CellData initialUser;
|
||||
CellData initialSystem;
|
||||
// Working copy edited by the user; compared against initial* on Apply.
|
||||
CellData pendingUser;
|
||||
CellData pendingSystem;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// Widget showing the per-extension file-association status of a wfassoc
|
||||
// program, and letting the user stage link / unlink operations.
|
||||
//
|
||||
// The widget follows the two-phase initialization pattern: the constructor
|
||||
// only takes a parent, and the caller invokes setConfig() with the target
|
||||
// program and scope before the widget becomes usable.
|
||||
//
|
||||
// By default the OK and Cancel buttons are hidden because they imply a
|
||||
// dialog-level operation (close). Hosts that embed this widget in a dialog
|
||||
// can enable them through Config::showOkCancelButtons and react to the
|
||||
// finished() signal.
|
||||
class QWFASSOC_EXPORT AssociationWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct Config {
|
||||
// Non-owning pointer to the wfassoc program. Must outlive the widget.
|
||||
wfassocpp::Program* program = nullptr;
|
||||
// Scope that link / unlink operations apply to. Also controls whether
|
||||
// the system column is interactive (only System scope unlocks it).
|
||||
TargetScope scope = TargetScope::User;
|
||||
// Whether the OK and Cancel buttons are visible. They are hidden by
|
||||
// default since closing the host window is a host-level decision.
|
||||
bool showOkCancelButtons = false;
|
||||
};
|
||||
|
||||
explicit AssociationWidget(QWidget* parent = nullptr);
|
||||
~AssociationWidget() override;
|
||||
|
||||
// Two-phase initialization.
|
||||
void setConfig(const Config& config);
|
||||
|
||||
// Re-query the live wfassoc state and rebuild the table.
|
||||
void refresh();
|
||||
|
||||
signals:
|
||||
// Emitted after the user applies pending changes (OK or Apply). The host
|
||||
// should refresh every widget that depends on wfassoc state in response.
|
||||
void changed();
|
||||
// Emitted when the widget wants its host window to close. `accepted` is
|
||||
// true when the OK button was used (after the changes were applied and
|
||||
// changed() was emitted) and false when the Cancel button was used.
|
||||
void finished(bool accepted);
|
||||
|
||||
private slots:
|
||||
void onSelectUserClicked();
|
||||
void onSelectSystemClicked();
|
||||
void onCellClicked(int row, int column);
|
||||
void onOkClicked();
|
||||
void onCancelClicked();
|
||||
void onApplyClicked();
|
||||
|
||||
private:
|
||||
// Refresh install / apply button enable state based on the live registry
|
||||
// and the pending edits.
|
||||
void updateEnabledState();
|
||||
// Drop and rebuild the table contents from the live registry.
|
||||
void rebuildTable();
|
||||
// Refresh a single row's displayed cells from its pending state.
|
||||
void refreshRowDisplay(int row);
|
||||
// Update the "Apply" button enable state based on whether any pending
|
||||
// cell differs from its initial state.
|
||||
void updateApplyButtonEnabled();
|
||||
// Toggle the pending state of a (row, column) cell. Column 1 maps to the
|
||||
// user scope, column 2 to the system scope.
|
||||
void toggleCell(int row, int column);
|
||||
// Apply the "+" semantics to a given scope: blank cells become Self, and
|
||||
// if there are none, all non-Self cells become Self.
|
||||
void selectAllInScope(bool isUser);
|
||||
// Commit every pending change to the registry via wfassoc.
|
||||
void applyAllChanges();
|
||||
// True when the system column should be interactive.
|
||||
bool isSystemColumnEnabled() const;
|
||||
|
||||
Ui::AssociationWidget* ui_;
|
||||
wfassocpp::Program* program_ = nullptr;
|
||||
TargetScope scope_ = TargetScope::User;
|
||||
bool showOkCancelButtons_ = false;
|
||||
|
||||
QString programName_;
|
||||
QString userName_;
|
||||
|
||||
std::vector<detail::ExtRow> rows_;
|
||||
// Re-entrancy guard used while rebuilding the table to avoid feeding
|
||||
// model-change signals back into toggleCell().
|
||||
bool refreshing_ = false;
|
||||
};
|
||||
|
||||
} // namespace qwfassoc
|
||||
|
||||
#endif // QWFASSOC_ASSOCIATION_WIDGET_H_
|
||||
@@ -0,0 +1,162 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AssociationWidget</class>
|
||||
<widget class="QWidget" name="AssociationWidget">
|
||||
<layout class="QVBoxLayout" name="mainLayout">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="assocHeaderLabel">
|
||||
<property name="text">
|
||||
<string>File types associated with this application:</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="selectButtonsLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="selectUserButton">
|
||||
<property name="toolTip">
|
||||
<string>Select all for current user</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">+</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="selectSystemButton">
|
||||
<property name="toolTip">
|
||||
<string>Select all for all users</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">+</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="selectButtonsSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="assocTable">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectItems</enum>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Type</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>User</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>All Users</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="bottomButtonsLayout">
|
||||
<item>
|
||||
<spacer name="bottomButtonsSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="okButton">
|
||||
<property name="text">
|
||||
<string>OK</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="applyButton">
|
||||
<property name="text">
|
||||
<string>Apply</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "icon_utils.h"
|
||||
|
||||
#include <QImage>
|
||||
|
||||
// QImage::fromHICON is only available on Windows and requires the real Win32
|
||||
// HICON typedef to be visible. The qt_windows.h wrapper (shipped with Qt 6 on
|
||||
// Windows) takes care of including <windows.h> in the order Qt expects, so it
|
||||
// is included after all Qt headers.
|
||||
#include <qt_windows.h>
|
||||
|
||||
namespace qwfassoc {
|
||||
namespace icon_utils {
|
||||
|
||||
QPixmap fromHicon(wfassocpp::HICON handle) {
|
||||
if (handle == wfassocpp::INVALID_HICON) {
|
||||
return QPixmap();
|
||||
}
|
||||
QImage image = QImage::fromHICON(static_cast<HICON>(handle));
|
||||
return QPixmap::fromImage(std::move(image));
|
||||
}
|
||||
|
||||
} // namespace icon_utils
|
||||
} // namespace qwfassoc
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#ifndef QWFASSOC_ICON_UTILS_H_
|
||||
#define QWFASSOC_ICON_UTILS_H_
|
||||
|
||||
#include <QPixmap>
|
||||
|
||||
#include <wfassoc++.h>
|
||||
|
||||
#include "qwfassoc_global.h"
|
||||
|
||||
namespace qwfassoc {
|
||||
namespace icon_utils {
|
||||
|
||||
// Convert a wfassocpp::HICON handle (the C++ wrapper around wfassoc's opaque
|
||||
// icon handle) into a QPixmap suitable for use in Qt widgets.
|
||||
//
|
||||
// The conversion goes through QImage::fromHICON() (available since Qt 6.0 on
|
||||
// Windows) and then QPixmap::fromImage() using its rvalue-reference overload
|
||||
// so that no extra pixel buffer copy is performed. The returned pixmap is null
|
||||
// if the input handle is null.
|
||||
QWFASSOC_EXPORT QPixmap fromHicon(wfassocpp::HICON handle);
|
||||
|
||||
} // namespace icon_utils
|
||||
} // namespace qwfassoc
|
||||
|
||||
#endif // QWFASSOC_ICON_UTILS_H_
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
#ifndef QWFASSOC_MANIFEST_H_
|
||||
#define QWFASSOC_MANIFEST_H_
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace qwfassoc {
|
||||
|
||||
// Description of a single extension declared in the manifest.
|
||||
//
|
||||
// For a given extension (e.g. "jpg"), the manifest references one entry from
|
||||
// each of the `strs`, `icons` and `behaviors` tables by its token name.
|
||||
struct ManifestExt {
|
||||
std::string name;
|
||||
std::string icon;
|
||||
std::string behavior;
|
||||
};
|
||||
|
||||
// In-memory representation of a wfassoc manifest TOML file.
|
||||
//
|
||||
// This struct mirrors the Rust `Manifest` type defined in
|
||||
// `wfassoc-exec/src/manifest.rs`. It is a plain data struct: parsing from
|
||||
// TOML and conversion into a wfassocpp::Schema are intentionally kept out of
|
||||
// the library so that the library itself does not depend on a TOML parser.
|
||||
// Consumers (such as qwfassoc-standalone) are responsible for filling the
|
||||
// fields in.
|
||||
struct Manifest {
|
||||
std::string identifier;
|
||||
std::string path;
|
||||
std::string clsid;
|
||||
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> icon;
|
||||
std::optional<std::string> behavior;
|
||||
|
||||
std::map<std::string, std::string> strs;
|
||||
std::map<std::string, std::string> icons;
|
||||
std::map<std::string, std::string> behaviors;
|
||||
std::map<std::string, ManifestExt> exts;
|
||||
};
|
||||
|
||||
} // namespace qwfassoc
|
||||
|
||||
#endif // QWFASSOC_MANIFEST_H_
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#ifndef QWFASSOC_GLOBAL_H_
|
||||
#define QWFASSOC_GLOBAL_H_
|
||||
|
||||
#include <qglobal.h>
|
||||
|
||||
// Standard Qt shared-library export/import macros.
|
||||
//
|
||||
// When the qwfassoc library itself is being built, QWFASSOC_LIBRARY is
|
||||
// defined (see the library's CMakeLists.txt) and QWFASSOC_EXPORT expands to
|
||||
// Q_DECL_EXPORT so that symbols are exported from the .dll. Consumers of the
|
||||
// library leave QWFASSOC_LIBRARY undefined, so QWFASSOC_EXPORT expands to
|
||||
// Q_DECL_IMPORT and the same symbols are imported.
|
||||
#if defined(QWFASSOC_LIBRARY)
|
||||
# define QWFASSOC_EXPORT Q_DECL_EXPORT
|
||||
#else
|
||||
# define QWFASSOC_EXPORT Q_DECL_IMPORT
|
||||
#endif
|
||||
|
||||
#endif // QWFASSOC_GLOBAL_H_
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#ifndef QWFASSOC_SCOPE_H_
|
||||
#define QWFASSOC_SCOPE_H_
|
||||
|
||||
namespace qwfassoc {
|
||||
|
||||
// The target scope an application is being managed for.
|
||||
//
|
||||
// This value typically comes from a `--for user` / `--for system` command
|
||||
// line argument and decides which columns of the file-association table are
|
||||
// interactive, as well as which scope install / unregister operations apply
|
||||
// to.
|
||||
enum class TargetScope {
|
||||
User,
|
||||
System,
|
||||
};
|
||||
|
||||
} // namespace qwfassoc
|
||||
|
||||
#endif // QWFASSOC_SCOPE_H_
|
||||
@@ -1,405 +0,0 @@
|
||||
# Custom ignore
|
||||
# ignore build directory
|
||||
builds/
|
||||
Debug_MB/
|
||||
Debug_UNICODE/
|
||||
Release_UNICODE/
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
@@ -1,10 +0,0 @@
|
||||
# wfassoc
|
||||
|
||||
**W**indows **F**ile **Assoc**iation Library
|
||||
|
||||
**Work In Progress**
|
||||
|
||||
## Introduction
|
||||
|
||||
* wfassoc: Main library
|
||||
* wfassoc_example: A full example about how to use this library
|
||||
@@ -1,51 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.31702.278
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "wfassoc", "wfassoc\wfassoc.vcxproj", "{4AAC8F0C-3E1C-4584-B682-05BBF96A813F}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "wfassoc_example", "wfassoc_example\wfassoc_example.vcxproj", "{AD275AD7-CBD5-41CA-AB24-BB707B3F7534}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug_MB|x64 = Debug_MB|x64
|
||||
Debug_MB|x86 = Debug_MB|x86
|
||||
Debug_UNICODE|x64 = Debug_UNICODE|x64
|
||||
Debug_UNICODE|x86 = Debug_UNICODE|x86
|
||||
Release_UNICODE|x64 = Release_UNICODE|x64
|
||||
Release_UNICODE|x86 = Release_UNICODE|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{4AAC8F0C-3E1C-4584-B682-05BBF96A813F}.Debug_MB|x64.ActiveCfg = Debug|x64
|
||||
{4AAC8F0C-3E1C-4584-B682-05BBF96A813F}.Debug_MB|x64.Build.0 = Debug|x64
|
||||
{4AAC8F0C-3E1C-4584-B682-05BBF96A813F}.Debug_MB|x86.ActiveCfg = Debug|Win32
|
||||
{4AAC8F0C-3E1C-4584-B682-05BBF96A813F}.Debug_MB|x86.Build.0 = Debug|Win32
|
||||
{4AAC8F0C-3E1C-4584-B682-05BBF96A813F}.Debug_UNICODE|x64.ActiveCfg = Debug|x64
|
||||
{4AAC8F0C-3E1C-4584-B682-05BBF96A813F}.Debug_UNICODE|x64.Build.0 = Debug|x64
|
||||
{4AAC8F0C-3E1C-4584-B682-05BBF96A813F}.Debug_UNICODE|x86.ActiveCfg = Debug|Win32
|
||||
{4AAC8F0C-3E1C-4584-B682-05BBF96A813F}.Debug_UNICODE|x86.Build.0 = Debug|Win32
|
||||
{4AAC8F0C-3E1C-4584-B682-05BBF96A813F}.Release_UNICODE|x64.ActiveCfg = Release|x64
|
||||
{4AAC8F0C-3E1C-4584-B682-05BBF96A813F}.Release_UNICODE|x64.Build.0 = Release|x64
|
||||
{4AAC8F0C-3E1C-4584-B682-05BBF96A813F}.Release_UNICODE|x86.ActiveCfg = Release|Win32
|
||||
{4AAC8F0C-3E1C-4584-B682-05BBF96A813F}.Release_UNICODE|x86.Build.0 = Release|Win32
|
||||
{AD275AD7-CBD5-41CA-AB24-BB707B3F7534}.Debug_MB|x64.ActiveCfg = Debug_UNICODE|x64
|
||||
{AD275AD7-CBD5-41CA-AB24-BB707B3F7534}.Debug_MB|x64.Build.0 = Debug_UNICODE|x64
|
||||
{AD275AD7-CBD5-41CA-AB24-BB707B3F7534}.Debug_MB|x86.ActiveCfg = Debug_MB|Win32
|
||||
{AD275AD7-CBD5-41CA-AB24-BB707B3F7534}.Debug_MB|x86.Build.0 = Debug_MB|Win32
|
||||
{AD275AD7-CBD5-41CA-AB24-BB707B3F7534}.Debug_UNICODE|x64.ActiveCfg = Debug_UNICODE|x64
|
||||
{AD275AD7-CBD5-41CA-AB24-BB707B3F7534}.Debug_UNICODE|x64.Build.0 = Debug_UNICODE|x64
|
||||
{AD275AD7-CBD5-41CA-AB24-BB707B3F7534}.Debug_UNICODE|x86.ActiveCfg = Debug_UNICODE|Win32
|
||||
{AD275AD7-CBD5-41CA-AB24-BB707B3F7534}.Debug_UNICODE|x86.Build.0 = Debug_UNICODE|Win32
|
||||
{AD275AD7-CBD5-41CA-AB24-BB707B3F7534}.Release_UNICODE|x64.ActiveCfg = Release_UNICODE|x64
|
||||
{AD275AD7-CBD5-41CA-AB24-BB707B3F7534}.Release_UNICODE|x64.Build.0 = Release_UNICODE|x64
|
||||
{AD275AD7-CBD5-41CA-AB24-BB707B3F7534}.Release_UNICODE|x86.ActiveCfg = Release_UNICODE|Win32
|
||||
{AD275AD7-CBD5-41CA-AB24-BB707B3F7534}.Release_UNICODE|x86.Build.0 = Release_UNICODE|Win32
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {1AD32362-840E-4399-A7D5-26A0B67A614D}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,9 +0,0 @@
|
||||
LIBRARY wfassoc
|
||||
EXPORTS
|
||||
|
||||
WFInstallApplicationW
|
||||
WFInstallApplicationA
|
||||
WFUninstallApplicationW
|
||||
WFUninstallApplicationA
|
||||
WFGenerateProgIDW
|
||||
WFGenerateProgIDA
|
||||
@@ -1,165 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{4aac8f0c-3e1c-4584-b682-05bbf96a813f}</ProjectGuid>
|
||||
<RootNamespace>wfassoc</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<CharacterSet>NotSet</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>NotSet</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
<OutDir>$(SolutionDir)builds\Debug\</OutDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
<OutDir>$(SolutionDir)builds\Release\</OutDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32;_CRT_SECURE_NO_WARNINGS;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<CompileAs>CompileAsC</CompileAs>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<ModuleDefinitionFile>wfassoc.def</ModuleDefinitionFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32;_CRT_SECURE_NO_WARNINGS;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<CompileAs>CompileAsC</CompileAs>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<ModuleDefinitionFile>wfassoc.def</ModuleDefinitionFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<ModuleDefinitionFile>wfassoc.def</ModuleDefinitionFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<ModuleDefinitionFile>wfassoc.def</ModuleDefinitionFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="wfassoc_utils.h" />
|
||||
<ClInclude Include="wfassoc_core.h" />
|
||||
<ClInclude Include="wfassoc_private.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="wfassoc_private.c" />
|
||||
<ClCompile Include="wfassoc_utils.c" />
|
||||
<ClCompile Include="wfassoc_core.c" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="wfassoc.def" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -1,44 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="源文件">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="头文件">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="资源文件">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="wfassoc_core.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="wfassoc_utils.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="wfassoc_private.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="wfassoc_core.c">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="wfassoc_utils.c">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="wfassoc_private.c">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="wfassoc.def">
|
||||
<Filter>源文件</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,407 +0,0 @@
|
||||
#include "wfassoc_core.h"
|
||||
#include <strsafe.h>
|
||||
#include <ShlObj.h>
|
||||
|
||||
// private function and variable declearions.
|
||||
// the function with _AL tail mean that the value returned by function is allocated from heap and should be free manually.
|
||||
// otherwise, the tail of _NAL mean this function will not allocate any new memeory.
|
||||
|
||||
/// <summary>
|
||||
/// Convert multi byte string to wide char string
|
||||
/// Notice: this function will allocate memory for returns and it should be released safely.
|
||||
/// </summary>
|
||||
/// <param name="source">The string will be converted</param>
|
||||
/// <param name="error">The error happend during converting</param>
|
||||
/// <returns>The string has been converted. If return NULL, it mean that the converting failed.</returns>
|
||||
//WFERROR ConvMultiByteToWideChar(CHAR* source);
|
||||
WFERROR WFSplitAppPath(WFString* app_path, WFString* app_name, WFString* base_path);
|
||||
//WFERROR Strcat(WCHAR* str1, WCHAR* str2);
|
||||
//WFERROR WFGetBasePathFromAppPath(WFString* app_path, WFString* base_path);
|
||||
WFERROR WFSplitSupportedTypesString(wchar_t* typesString, WFLinkedList* list);
|
||||
|
||||
void WFPrintflnInDebug();
|
||||
|
||||
// some effective reg function
|
||||
LSTATUS WFRegOpenKeyWithCreation(HKEY hkey, LPCWSTR lpSubKey, PHKEY phkResult);
|
||||
LSTATUS WFRegSetStringValue(HKEY hkey, LPCWSTR lpValueName, const WCHAR* data);
|
||||
|
||||
#define LEGACY_PROGID_FORMAT L"%s.%s.%d"
|
||||
#define WFALLOC(type,count) (type*)malloc(sizeof(type)*count);
|
||||
#define SAFE_EXEC_WIN32(wfError, recvError, skipLabel, function) recvError=function; if(recvError!=ERROR_SUCCESS) {wfError = WFERROR_WIN32;goto skipLabel;}
|
||||
#define SAFE_EXEC_WF_LABEL(wfError, skipLabel, function) if((wfError=function)!=WFERROR_OK) {goto skipLabel;}
|
||||
#define SAFE_EXEC_WF_RETURN(wfError, function) if((wfError=function)!=WFERROR_OK) {return wfError;}
|
||||
#define SAFE_EXEC_STRALLOC(wfError, skipLabel, vStr, function) vStr=function; if(vStr==NULL) {wfError = WFERROR_ALLOC;goto skipLabel;}
|
||||
#define SAFE_FREE_PTR(obj) if(obj!=NULL){free(obj);obj=NULL;}
|
||||
#define SAFE_FREE_HKEY(hkey) if(hkey!=NULL&&hkey!=INVALID_HANDLE_VALUE){RegCloseKey(hkey);hkey=NULL;}
|
||||
|
||||
// public function implements.
|
||||
|
||||
WFERROR WFInstallApplication(WFAPP_PROFILE* profile) {
|
||||
if (profile->WFVersion != WFVERSION) return WFERROR_INVALID_VERSION;
|
||||
|
||||
WFERROR wf_error = WFERROR_OK;
|
||||
LSTATUS win32_error = ERROR_SUCCESS;
|
||||
//WCHAR* itemSupportedTypes = NULL;
|
||||
|
||||
// ==================================
|
||||
// generate necessary data
|
||||
|
||||
// init string
|
||||
WFString* strProgID = NULL,
|
||||
* strAppPath = NULL,
|
||||
* strAppBasePath = NULL,
|
||||
* strAppFileName = NULL;
|
||||
|
||||
SAFE_EXEC_WF_LABEL(wf_error, final_process,
|
||||
WFString_Alloc(&strProgID)
|
||||
);
|
||||
SAFE_EXEC_WF_LABEL(wf_error, final_process,
|
||||
WFString_Alloc_Wchar(&strAppPath, profile->AppPath)
|
||||
);
|
||||
SAFE_EXEC_WF_LABEL(wf_error, final_process,
|
||||
WFString_Alloc(&strAppBasePath)
|
||||
);
|
||||
SAFE_EXEC_WF_LABEL(wf_error, final_process,
|
||||
WFString_Alloc(&strAppFileName)
|
||||
);
|
||||
|
||||
// write string
|
||||
SAFE_EXEC_WF_LABEL(wf_error, final_process,
|
||||
WFString_Printf(strProgID, LEGACY_PROGID_FORMAT, profile->ProgID_Vendor, profile->ProgID_Component, profile->ProgID_Version)
|
||||
);
|
||||
SAFE_EXEC_WF_LABEL(wf_error, final_process,
|
||||
WFSplitAppPath(strAppPath, strAppFileName, strAppBasePath)
|
||||
);
|
||||
|
||||
WFString* regpathAppPaths = NULL,
|
||||
* regpathApplicationsRealName = NULL,
|
||||
* regpathApplicationsProgId = NULL;
|
||||
|
||||
SAFE_EXEC_WF_LABEL(wf_error, final_process,
|
||||
WFString_Alloc_Wchar(®pathAppPaths, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\")
|
||||
);
|
||||
SAFE_EXEC_WF_LABEL(wf_error, final_process,
|
||||
WFString_Concat_String(regpathAppPaths, strAppFileName)
|
||||
);
|
||||
|
||||
SAFE_EXEC_WF_LABEL(wf_error, final_process,
|
||||
WFString_Alloc_Wchar(®pathApplicationsRealName, L"SOFTWARE\\Classes\\Applications\\")
|
||||
);
|
||||
SAFE_EXEC_WF_LABEL(wf_error, final_process,
|
||||
WFString_Concat_String(regpathApplicationsRealName, strAppFileName)
|
||||
);
|
||||
|
||||
SAFE_EXEC_WF_LABEL(wf_error, final_process,
|
||||
WFString_Alloc_Wchar(®pathApplicationsProgId, L"SOFTWARE\\Classes\\Applications\\")
|
||||
);
|
||||
SAFE_EXEC_WF_LABEL(wf_error, final_process,
|
||||
WFString_Concat_String(regpathApplicationsProgId, strProgID)
|
||||
);
|
||||
|
||||
|
||||
//WCHAR* strAppPath = NULL,
|
||||
// * strPath = NULL,
|
||||
// * strApplications = NULL,
|
||||
// * strProgID = NULL,
|
||||
// * strOpenWithList = NULL;
|
||||
//SAFE_EXEC_STRALLOC(error, final_process,
|
||||
// strPath, GetPathFromAppPath_AL(profile->AppPath)
|
||||
//);
|
||||
//SAFE_EXEC_STRALLOC(error, final_process,
|
||||
// strAppPath, Strcat_AL(
|
||||
// L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\",
|
||||
// GetAppNameFromAppPath_NAL(profile->AppPath)
|
||||
//)
|
||||
//);
|
||||
//SAFE_EXEC_STRALLOC(error, final_process,
|
||||
// strProgID, Strcat_AL(
|
||||
// L"SOFTWARE\\Classes\\Applications\\",
|
||||
// profile->ProgID
|
||||
//)
|
||||
//);
|
||||
//SAFE_EXEC_STRALLOC(error, final_process,
|
||||
// strApplications, Strcat_AL(
|
||||
// L"SOFTWARE\\Classes\\Applications\\",
|
||||
// GetAppNameFromAppPath_NAL(profile->AppPath)
|
||||
//)
|
||||
//);
|
||||
|
||||
// generate necessary HKEY
|
||||
HKEY nodeAppPath = NULL,
|
||||
nodeApplications = NULL,
|
||||
nodeApplications_Verb = NULL,
|
||||
nodeApplications_SupportedTypes = NULL,
|
||||
nodeProgID = NULL,
|
||||
nodeProgID_Verb = NULL,
|
||||
nodeClasses = NULL,
|
||||
nodeExt = NULL,
|
||||
nodeExt_OpenWithProgIds = NULL,
|
||||
nodeExt_OpenWithList = NULL;
|
||||
|
||||
// register in `App Paths`
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegOpenKeyWithCreation(profile->RegisterForAllUsers ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, strAppPath, &nodeAppPath)
|
||||
);
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegOpenKeyWithCreation(profile->RegisterForAllUsers ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, strApplications, &nodeApplications)
|
||||
);
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegOpenKeyWithCreation(nodeApplications, L"shell\\open\\command", &nodeApplications_Verb)
|
||||
);
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegOpenKeyWithCreation(nodeApplications, L"SupportedTypes", &nodeApplications_SupportedTypes)
|
||||
);
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegOpenKeyWithCreation(profile->RegisterForAllUsers ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, strProgID, &nodeProgID)
|
||||
);
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegOpenKeyWithCreation(nodeProgID, L"shell\\open\\command", &nodeProgID_Verb)
|
||||
);
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegOpenKeyWithCreation(profile->RegisterForAllUsers ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, L"SOFTWARE\\Classes", &nodeClasses)
|
||||
);
|
||||
|
||||
// operate HKEY
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegSetStringValue(nodeAppPath, NULL, profile->AppPath) // visit Default key
|
||||
);
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegSetStringValue(nodeAppPath, L"Path", strPath)
|
||||
);
|
||||
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegSetStringValue(nodeApplications_Verb, NULL, profile->AppCommand) // visit Default key
|
||||
);
|
||||
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegSetStringValue(nodeProgID_Verb, NULL, profile->AppCommand) // visit Default key
|
||||
);
|
||||
|
||||
|
||||
while ((itemSupportedTypes = IterateSupportedTypesString_NAL(profile->SupportedTypes, itemSupportedTypes)) != NULL) {
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegSetStringValue(nodeApplications_SupportedTypes, itemSupportedTypes, NULL) // register supported type with blank item
|
||||
);
|
||||
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegOpenKeyWithCreation(nodeClasses, itemSupportedTypes, &nodeExt)
|
||||
);
|
||||
if (profile->SetAsDefault) {
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegSetStringValue(nodeExt, NULL, profile->ProgID) // visit Default key
|
||||
);
|
||||
}
|
||||
if (profile->ShowInOpenWithMenu) {
|
||||
if (profile->UseOpenWithList) {
|
||||
// use Windows XP node
|
||||
SAFE_EXEC_STRALLOC(error, final_process,
|
||||
strOpenWithList, Strcat_AL(
|
||||
L"OpenWithList\\",
|
||||
GetAppNameFromAppPath_NAL(profile->AppPath)
|
||||
)
|
||||
);
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegOpenKeyWithCreation(nodeExt, itemSupportedTypes, &nodeExt_OpenWithList);
|
||||
);
|
||||
|
||||
SAFE_FREE_PTR(strOpenWithList);
|
||||
SAFE_FREE_HKEY(nodeExt_OpenWithList);
|
||||
} else {
|
||||
// use Windows Vista node
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegOpenKeyWithCreation(nodeExt, itemSupportedTypes, &nodeExt_OpenWithProgIds);
|
||||
);
|
||||
SAFE_EXEC_WIN32(error, win32_error, final_process,
|
||||
WFRegSetStringValue(nodeExt_OpenWithProgIds, profile->ProgID, NULL)
|
||||
);
|
||||
|
||||
SAFE_FREE_HKEY(nodeExt_OpenWithProgIds);
|
||||
}
|
||||
}
|
||||
SAFE_FREE_HKEY(nodeExt);
|
||||
}
|
||||
|
||||
final_process:
|
||||
// free HKEY and strings
|
||||
SAFE_FREE_HKEY(nodeAppPath);
|
||||
SAFE_FREE_HKEY(nodeApplications);
|
||||
SAFE_FREE_HKEY(nodeApplications_Verb);
|
||||
SAFE_FREE_HKEY(nodeApplications_SupportedTypes);
|
||||
SAFE_FREE_HKEY(nodeProgID);
|
||||
SAFE_FREE_HKEY(nodeProgID_Verb);
|
||||
SAFE_FREE_HKEY(nodeClasses);
|
||||
SAFE_FREE_HKEY(nodeExt);
|
||||
SAFE_FREE_HKEY(nodeExt_OpenWithProgIds);
|
||||
SAFE_FREE_HKEY(nodeExt_OpenWithList);
|
||||
|
||||
SAFE_FREE_PTR(strAppPath);
|
||||
SAFE_FREE_PTR(strPath);
|
||||
SAFE_FREE_PTR(strApplications);
|
||||
SAFE_FREE_PTR(strProgID);
|
||||
|
||||
// order uninstall to wipe out all written data
|
||||
// if function failed
|
||||
if (error != WFERROR_OK)
|
||||
WFUninstallApplicationW(profile);
|
||||
|
||||
// active changes
|
||||
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL);
|
||||
|
||||
// return error
|
||||
return error;
|
||||
}
|
||||
|
||||
//WFERROR WFInstallApplicationA(WFAPP_PROFILEA* profile) {
|
||||
// return WFERROR_OK;
|
||||
//}
|
||||
//
|
||||
//WFERROR WFUninstallApplicationW(WFAPP_PROFILEW* profile) {
|
||||
// if (profile->WFVersion != WFVERSION) return WFERROR_INVALID_VERSION;
|
||||
//
|
||||
// return WFERROR_OK;
|
||||
//}
|
||||
//
|
||||
//WFERROR WFUninstallApplicationA(WFAPP_PROFILEA* profile) {
|
||||
// return WFERROR_OK;
|
||||
//}
|
||||
//
|
||||
//WFERROR WFGenerateProgIDW(WCHAR* vendor, WCHAR* component, INT version, WCHAR* result, INT* result_length) {
|
||||
// if (result_length == NULL) return WFERROR_NULLPTR;
|
||||
// if (result == NULL) {
|
||||
// *result_length = _snwprintf(NULL, 0, LEGACY_PROGID_FORMATW, vendor, component, version) + 1;
|
||||
// } else {
|
||||
// int write_result = _snwprintf(result, *result_length, LEGACY_PROGID_FORMATW, vendor, component, version);
|
||||
// if (write_result < 0 || write_result >= *result_length) return WFERROR_INSUFFICIENT_BUFFER;
|
||||
// }
|
||||
// return WFERROR_OK;
|
||||
//}
|
||||
//
|
||||
//WFERROR WFGenerateProgIDA(CHAR* vendor, CHAR* component, INT version, CHAR* result, INT* result_length) {
|
||||
// if (result_length == NULL) return WFERROR_NULLPTR;
|
||||
// if (result == NULL) {
|
||||
// *result_length = _snprintf(NULL, 0, LEGACY_PROGID_FORMATA, vendor, component, version) + 1;
|
||||
// } else {
|
||||
// int write_result = _snprintf(result, *result_length, LEGACY_PROGID_FORMATA, vendor, component, version);
|
||||
// if (write_result < 0 || write_result >= *result_length) return WFERROR_INSUFFICIENT_BUFFER;
|
||||
// }
|
||||
// return WFERROR_OK;
|
||||
//}
|
||||
|
||||
|
||||
// private function and variable implements.
|
||||
|
||||
//WCHAR* ConvMultiByteToWideChar_AL(CHAR* source) {
|
||||
// WCHAR* dest = NULL;
|
||||
// size_t sourceLength = strlen(source);
|
||||
//
|
||||
// int destLength = MultiByteToWideChar(CP_ACP, 0, source, sourceLength, NULL, 0);
|
||||
// if (destLength <= 0) return NULL;
|
||||
// destLength += 10;
|
||||
//
|
||||
// dest = WFALLOC(WCHAR, destLength);
|
||||
// if (dest == NULL) return NULL;
|
||||
//
|
||||
// memset(dest, 0, sizeof(WCHAR) * destLength);
|
||||
// int error = MultiByteToWideChar(CP_ACP, 0, source, sourceLength, dest, destLength);
|
||||
// if (error <= 0) {
|
||||
// free(dest);
|
||||
// return NULL;
|
||||
// }
|
||||
//
|
||||
// return dest;
|
||||
//}
|
||||
|
||||
WFERROR WFSplitAppPath(WFString* app_path, WFString* app_name, WFString* base_path) {
|
||||
WFERROR ec;
|
||||
wchar_t* lastSlash, *src, *ptr;
|
||||
SAFE_EXEC_WF_RETURN(ec, WFString_GetData(app_path, &ptr));
|
||||
src = lastSlash = ptr;
|
||||
|
||||
while (*ptr != L'\0') {
|
||||
if (*ptr == L'\\' || *ptr == L'/') {
|
||||
lastSlash = ptr;
|
||||
}
|
||||
ptr++;
|
||||
}
|
||||
|
||||
SAFE_EXEC_WF_RETURN(ec, WFString_SubString(app_path, base_path, 0, lastSlash - src));
|
||||
SAFE_EXEC_WF_RETURN(ec, WFString_SetData(app_name, ++lastSlash));
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
//WCHAR* Strcat_AL(WCHAR* str1, WCHAR* str2) {
|
||||
// size_t length = wcslen(str1) + wcslen(str2) + 10;
|
||||
//
|
||||
// WCHAR* dest = NULL;
|
||||
// dest = WFALLOC(WCHAR, length);
|
||||
// if (dest == NULL) return NULL;
|
||||
//
|
||||
// wcscpy(dest, str1);
|
||||
// wcscat(dest, str2);
|
||||
// return dest;
|
||||
//}
|
||||
|
||||
WFERROR WFSplitSupportedTypesString(wchar_t* typesString, WFLinkedList* list) {
|
||||
WFERROR ec;
|
||||
wchar_t* ptr;
|
||||
WFString* strl;
|
||||
uint32_t len_str;
|
||||
ptr = typesString;
|
||||
|
||||
while (*ptr != L'\0') {
|
||||
// add into list
|
||||
SAFE_EXEC_WF_RETURN(ec, WFString_Alloc(&strl, ptr));
|
||||
SAFE_EXEC_WF_RETURN(ec, WFLinkedList_Add(list, strl));
|
||||
|
||||
// shift to next string
|
||||
SAFE_EXEC_WF_RETURN(ec, WFString_GetLength(strl, &len_str));
|
||||
ptr += len_str + 1;
|
||||
}
|
||||
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
//WCHAR* IterateSupportedTypesString_NAL(WCHAR* typesString, WCHAR* prev) {
|
||||
// if (typesString == NULL || *typesString == L'\0') return NULL;
|
||||
// if (prev == NULL) return typesString;
|
||||
//
|
||||
// // skip prev string
|
||||
// while (*prev != L'\0') {
|
||||
// prev++;
|
||||
// }
|
||||
//
|
||||
// // if the next string is start with zero, it mean that the full string is over and return NULL to terminate analyse.
|
||||
// // otherwise return next string
|
||||
// prev++;
|
||||
// if (*prev == L'\0') return NULL;
|
||||
// else return prev;
|
||||
//}
|
||||
|
||||
void WFPrintflnInDebug() {
|
||||
|
||||
}
|
||||
|
||||
LSTATUS WFRegOpenKeyWithCreation(HKEY hkey, LPCWSTR lpSubKey, PHKEY phkResult) {
|
||||
return RegCreateKeyExW(
|
||||
hkey,
|
||||
lpSubKey,
|
||||
0,
|
||||
NULL,
|
||||
REG_OPTION_NON_VOLATILE,
|
||||
KEY_ALL_ACCESS,
|
||||
NULL,
|
||||
phkResult,
|
||||
NULL
|
||||
);
|
||||
}
|
||||
|
||||
LSTATUS WFRegSetStringValue(HKEY hkey, LPCWSTR lpValueName, const WCHAR* data) {
|
||||
return RegSetValueExW(
|
||||
hkey,
|
||||
lpValueName,
|
||||
0,
|
||||
REG_SZ,
|
||||
data,
|
||||
data == NULL ? 0 : ((wcslen(data) + 1) * sizeof(WCHAR))
|
||||
);
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
#if !defined(_YYCDLL_WFASSOC_H__IMPORTED_)
|
||||
#define _YYCDLL_WFASSOC_H__IMPORTED_
|
||||
|
||||
#include <Windows.h>
|
||||
#include <sal.h>
|
||||
#include "wfassoc_utils.h"
|
||||
|
||||
// quick marco for developer and should not be used in wfassoc self
|
||||
|
||||
//#if defined(_UNICODE)
|
||||
//#define WFAPP_PROFILE WFAPP_PROFILEW
|
||||
//#define WFInstallApplication WFInstallApplicationW
|
||||
//#define WFUninstallApplication WFUninstallApplicationW
|
||||
//#define WFGenerateProgID WFGenerateProgIDW
|
||||
//#elif defined(_MBCS)
|
||||
//#define WFAPP_PROFILE WFAPP_PROFILEA
|
||||
//#define WFInstallApplication WFInstallApplicationA
|
||||
//#define WFUninstallApplication WFUninstallApplicationA
|
||||
//#define WFGenerateProgID WFGenerateProgIDA
|
||||
//#endif
|
||||
|
||||
/// <summary>
|
||||
/// Install Application via Wide Character
|
||||
/// </summary>
|
||||
/// <param name="profile"></param>
|
||||
/// <returns></returns>
|
||||
WFERROR WFInstallApplication(WFAPP_PROFILE* profile);
|
||||
WFERROR WFUninstallApplication(WFAPP_PROFILE* profile);
|
||||
|
||||
WFERROR WFRegisterAppPath(WFAPP_INTERNAL_PROFILE* internal_profile);
|
||||
WFERROR WFRegisterApplication(WFAPP_INTERNAL_PROFILE* internal_profile);
|
||||
WFERROR WFRegisterExtensions(WFAPP_INTERNAL_PROFILE* internal_profile);
|
||||
|
||||
//WFERROR WFProfile_Alloc(WFAPP_PROFILE** profile);
|
||||
//WFERROR WFProfile_Free(WFAPP_PROFILE* profile);
|
||||
//WFERROR WFProfile_SetProgIDA(WFAPP_PROFILE** profile, char* vendor, char* component, uint32_t version, BOOL is_utf8);
|
||||
//WFERROR WFProfile_SetProgIDW(WFAPP_PROFILE** profile, wchar_t* vendor, wchar_t* component, uint32_t version);
|
||||
|
||||
/// <summary>
|
||||
/// Generate Legacy ProgID
|
||||
/// </summary>
|
||||
/// <param name="vendor">Vendor. Such as `Word`, `Excel`, `PowerPoint`.</param>
|
||||
/// <param name="component">Component. Such as `Document`, `Sheet`, `Diagram`.</param>
|
||||
/// <param name="version">Version. Such as `0`, `1`, `114514`.</param>
|
||||
/// <param name="result">Pointer to output ProgID. If this variable is NULL, function will calculate proper length of receiving buffer and return it via `result_length`.</param>
|
||||
/// <param name="result_length">Pointer to a int variable containing buffer's length. If `result` is not NULL, it should be the length of `result`.</param>
|
||||
/// <returns></returns>
|
||||
//WFERROR WFGenerateProgIDW(WCHAR* vendor, WCHAR* component, INT version, WCHAR* result, INT* result_length);
|
||||
/// <summary>
|
||||
/// Generate Legacy ProgID
|
||||
/// </summary>
|
||||
/// <param name="vendor">Vendor. Such as `Word`, `Excel`, `PowerPoint`.</param>
|
||||
/// <param name="component">Component. Such as `Document`, `Sheet`, `Diagram`.</param>
|
||||
/// <param name="version">Version. Such as `0`, `1`, `114514`.</param>
|
||||
/// <param name="result">Pointer to output ProgID. If this variable is NULL, function will calculate proper length of receiving buffer and return it via `result_length`.</param>
|
||||
/// <param name="result_length">Pointer to a int variable containing buffer's length. If `result` is not NULL, it should be the length of `result`.</param>
|
||||
/// <returns></returns>
|
||||
//WFERROR WFGenerateProgIDA(CHAR* vendor, CHAR* component, INT version, CHAR* result, INT* result_length);
|
||||
|
||||
#endif
|
||||
@@ -1 +0,0 @@
|
||||
#include "wfassoc_private.h"
|
||||
@@ -1,9 +0,0 @@
|
||||
#if !defined(_YYCDLL_WFASSOC_PRIVATE_H__IMPORTED_)
|
||||
#define _YYCDLL_WFASSOC_PRIVATE_H__IMPORTED_
|
||||
|
||||
typedef struct _WFAPP_RAWDATA {
|
||||
uint32_t mVersion;
|
||||
|
||||
}WFAPP_RAWDATA;
|
||||
|
||||
#endif
|
||||
@@ -1,256 +0,0 @@
|
||||
#include "wfassoc_utils.h"
|
||||
|
||||
#pragma region WFString
|
||||
|
||||
WFERROR WFString_Alloc_Wchar(WFString** strl, const wchar_t* raw_data) {
|
||||
if (raw_data == NULL) return WFERROR_NULLPTR;
|
||||
|
||||
WFERROR ec;
|
||||
if ((ec = WFString_Alloc(strl)) != WFERROR_OK) return ec;
|
||||
if ((ec = WFString_SetData(*strl, raw_data)) != WFERROR_OK) return ec;
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
WFERROR WFString_Alloc_Char(WFString** strl, const char* raw_data, BOOL is_utf8) {
|
||||
if (raw_data == NULL) return WFERROR_NULLPTR;
|
||||
|
||||
// init string
|
||||
WFERROR ec;
|
||||
if ((ec = WFString_Alloc(strl)) != WFERROR_OK) return ec;
|
||||
|
||||
// compute expected string length
|
||||
uint32_t sourceLength = strlen(raw_data);
|
||||
int destLength = MultiByteToWideChar(is_utf8 ? CP_UTF8 : CP_ACP, 0, raw_data, sourceLength, NULL, 0);
|
||||
if (destLength <= 0) return WFERROR_CRT;
|
||||
--destLength; // remove terminal char
|
||||
|
||||
// resize string
|
||||
if ((ec = WFString_Resize(*strl, destLength)) != WFERROR_OK) return ec;
|
||||
|
||||
// clear buffer and write data
|
||||
wchar_t* buffer;
|
||||
if ((ec = WFString_GetData(*strl, &buffer)) != WFERROR_OK) return ec;
|
||||
memset(buffer, 0, sizeof(WCHAR) * destLength);
|
||||
int crt_error = MultiByteToWideChar(is_utf8 ? CP_UTF8 : CP_ACP, 0, raw_data, sourceLength, buffer, destLength);
|
||||
if (crt_error <= 0) return WFERROR_CRT;
|
||||
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
WFERROR WFString_Alloc_Capacity(WFString** strl, uint32_t size) {
|
||||
*strl = WFNEW(WFString);
|
||||
if (*strl == NULL) return WFERROR_ALLOC;
|
||||
|
||||
// init struct data
|
||||
(*strl)->mCapacity = size;
|
||||
(*strl)->mRealCapacity = size + 1;
|
||||
(*strl)->mLength = 0;
|
||||
(*strl)->mRealLength = 1;
|
||||
|
||||
(*strl)->mRawData = malloc((*strl)->mRealCapacity * sizeof(wchar_t));
|
||||
if ((*strl)->mRawData == NULL) return WFERROR_ALLOC;
|
||||
|
||||
(*strl)->mRawData[(*strl)->mRealLength - 1] = 0;
|
||||
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
WFERROR WFString_Alloc(WFString** strl) {
|
||||
return WFString_Alloc_Capacity(strl, 256);
|
||||
}
|
||||
|
||||
WFERROR WFString_Free(WFString* strl) {
|
||||
if (strl == NULL) return WFERROR_OK;
|
||||
if (strl->mRawData == NULL) return WFERROR_NULLPTR;
|
||||
free(strl->mRawData);
|
||||
free(strl);
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
WFERROR WFString_Resize(WFString* strl, uint32_t new_size) {
|
||||
// if remain data is not enough, we need alloc new one
|
||||
if (new_size >= strl->mCapacity) {
|
||||
strl->mCapacity = new_size * 2;
|
||||
strl->mRealCapacity = strl->mCapacity + 1;
|
||||
|
||||
// alloc buffer
|
||||
if (strl->mRawData != NULL)
|
||||
strl->mRawData = realloc(strl->mRawData, strl->mRealCapacity * sizeof(wchar_t));
|
||||
else
|
||||
strl->mRawData = malloc(strl->mRealCapacity * sizeof(wchar_t));
|
||||
if (strl->mRawData == NULL) return WFERROR_ALLOC;
|
||||
}
|
||||
|
||||
// set length as new length
|
||||
strl->mLength = new_size;
|
||||
strl->mRealLength = strl->mLength + 1;
|
||||
|
||||
// set the last one is zero
|
||||
strl->mRawData[strl->mRealLength - 1] = 0;
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
WFERROR WFString_GetData(WFString* strl, wchar_t** pdata) {
|
||||
if (strl == NULL) return WFERROR_NULLPTR;
|
||||
*pdata = strl->mRawData;
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
WFERROR WFString_SetData(WFString* strl, const wchar_t* data) {
|
||||
if (strl == NULL || data == NULL) return WFERROR_NULLPTR;
|
||||
|
||||
// get length and resize buffer
|
||||
WFERROR ec;
|
||||
uint32_t size = wcslen(data);
|
||||
ec = WFString_Resize(strl, size);
|
||||
if (ec != WFERROR_OK) return ec;
|
||||
|
||||
// copy data
|
||||
if (size != 0)
|
||||
memcpy(strl->mRawData, data, sizeof(wchar_t) * size);
|
||||
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
WFERROR WFString_GetLength(WFString* strl, uint32_t* len) {
|
||||
if (strl == NULL) return WFERROR_NULLPTR;
|
||||
*len = strl->mLength;
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
WFERROR WFString_Printf(WFString* strl, const wchar_t* format, ...) {
|
||||
if (strl == NULL) return WFERROR_NULLPTR;
|
||||
|
||||
// get expected size
|
||||
va_list argptr;
|
||||
va_start(argptr, format);
|
||||
uint32_t count = _vsnwprintf(NULL, 0, format, argptr);
|
||||
//count++;
|
||||
va_end(argptr);
|
||||
|
||||
// resize string and get buffer ptr
|
||||
WFERROR ec;
|
||||
wchar_t* buffer;
|
||||
if ((ec = WFString_Resize(strl, count)) != WFERROR_OK) return ec;
|
||||
if ((ec = WFString_GetData(strl, &buffer)) != WFERROR_OK) return ec;
|
||||
|
||||
// write data to buffer
|
||||
buffer[count - 1] = L'\0';
|
||||
va_start(argptr, format);
|
||||
int write_result = _vsnwprintf(buffer, count, format, argptr);
|
||||
va_end(argptr);
|
||||
if (write_result < 0 || write_result >= count) return WFERROR_CRT;
|
||||
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
WFERROR WFString_Concat_Wchar(WFString* strl, const wchar_t* extra) {
|
||||
if (strl == NULL) return WFERROR_NULLPTR;
|
||||
if (extra == NULL) return WFERROR_OK;
|
||||
|
||||
uint32_t count = wcslen(extra);
|
||||
uint32_t oldlen = strl->mLength;
|
||||
|
||||
// if extra strl count is 0, do not copy any data.
|
||||
if (count == 0) return WFERROR_OK;
|
||||
|
||||
WFERROR ec;
|
||||
if ((ec = WFString_Resize(strl, oldlen + count)) != WFERROR_OK) return ec;
|
||||
memcpy(strl->mRawData + oldlen, extra, sizeof(wchar_t) * count);
|
||||
return WFERROR_OK;
|
||||
}
|
||||
WFERROR WFString_Concat_String(WFString* strl, WFString* extra) {
|
||||
if (strl == NULL) return WFERROR_NULLPTR;
|
||||
if (extra == NULL) return WFERROR_OK;
|
||||
|
||||
return WFString_Concat_Wchar(strl, extra->mRawData);
|
||||
}
|
||||
|
||||
WFERROR WFString_SubString(WFString* strl, WFString* substring, uint32_t start_index, uint32_t length) {
|
||||
if (strl == NULL) return WFERROR_NULLPTR;
|
||||
if (start_index >= strl->mLength || length > strl->mLength - start_index) return WFERROR_INVALID_ARGUMENTS;
|
||||
|
||||
WFERROR ec;
|
||||
if ((ec = WFString_Resize(substring, length)) != WFERROR_OK) return ec;
|
||||
if (length != 0) {
|
||||
memcpy(substring->mRawData, strl->mRawData + start_index, sizeof(wchar_t) * length);
|
||||
}
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
#pragma endregion
|
||||
|
||||
#pragma region WFLinkedList
|
||||
|
||||
WFERROR WFLinkedList_Alloc(WFLinkedList** list) {
|
||||
*list = WFNEW(WFLinkedList);
|
||||
if (*list == NULL) return WFERROR_ALLOC;
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
WFERROR WFLinkedList_Free(WFLinkedList* list) {
|
||||
return WFLinkedList_Free_Full(list, NULL);
|
||||
}
|
||||
|
||||
WFERROR WFLinkedList_Free_Full(WFLinkedList* list, WFLinkedListNode_FreeDataFunc free_func) {
|
||||
if (list == NULL) return WFERROR_OK;
|
||||
|
||||
// iterate full list and remove data and node
|
||||
WFLinkedListNode* node = list->mHead, * free_node = NULL;
|
||||
while (node != NULL) {
|
||||
// free raw data
|
||||
if (free_func != NULL) (*free_func)(node->mRawData);
|
||||
// move to next node and free current node
|
||||
free_node = node;
|
||||
node = node->mNext;
|
||||
free(free_node);
|
||||
}
|
||||
|
||||
// free list self
|
||||
free(list);
|
||||
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
WFERROR WFLinkedList_Add(WFLinkedList* list, void* data) {
|
||||
if (list == NULL) return WFERROR_NULLPTR;
|
||||
|
||||
WFLinkedListNode* new_item = WFNEW(WFLinkedListNode);
|
||||
if (new_item == NULL) return WFERROR_ALLOC;
|
||||
new_item->mNext = NULL;
|
||||
new_item->mRawData = data;
|
||||
|
||||
if (list->mLength == 0) {
|
||||
list->mHead = list->mTail = new_item;
|
||||
} else {
|
||||
list->mTail->mNext = new_item;
|
||||
list->mTail = new_item;
|
||||
}
|
||||
++list->mLength;
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
WFERROR WFLinkedList_NodeIterator(WFLinkedList* list, WFLinkedListNode** node_ptr) {
|
||||
if (list == NULL) return WFERROR_NULLPTR;
|
||||
|
||||
if (*node_ptr == NULL) {
|
||||
// if node_ptr is NULL, it mean that wo should iterate this list from head
|
||||
*node_ptr = list->mHead;
|
||||
} else {
|
||||
// otherwise, move to next node
|
||||
*node_ptr = (*node_ptr)->mNext;
|
||||
}
|
||||
|
||||
// if the header is null, return end of tail error to notice caller stop iterate
|
||||
if (*node_ptr == NULL) return WFERROR_END_OF_TAIL;
|
||||
else return WFERROR_OK;
|
||||
}
|
||||
|
||||
WFERROR WFLinkedListNode_GetData(WFLinkedListNode* node, void** pdata) {
|
||||
if (node == NULL) return WFERROR_NULLPTR;
|
||||
|
||||
*pdata = node->mRawData;
|
||||
return WFERROR_OK;
|
||||
}
|
||||
|
||||
#pragma endregion
|
||||
@@ -1,136 +0,0 @@
|
||||
#if !defined(_YYCDLL_WFASSOC_UTILS_H__IMPORTED_)
|
||||
#define _YYCDLL_WFASSOC_UTILS_H__IMPORTED_
|
||||
|
||||
#include <Windows.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
// useful macro
|
||||
|
||||
#define WFNEW(type) ((type*)malloc(sizeof(type)))
|
||||
#define WFNEW_ARRAY(type) ((type*)malloc(sizeof(type)*count);)
|
||||
#define WFVERSION 0
|
||||
#define WFSUCCESS(expr) (!expr)
|
||||
#define WFFAILED(expr) expr
|
||||
|
||||
/// <summary>
|
||||
/// wfassoc Error Enum
|
||||
/// </summary>
|
||||
typedef enum _WFERROR {
|
||||
/// <summary>
|
||||
/// All operation done successfully
|
||||
/// </summary>
|
||||
WFERROR_OK = 0,
|
||||
/// <summary>
|
||||
/// The filed `WFVersion` in Profile Struct is not matched. It usually mean that currently used DLL is not matched with the DLL when compiling this application.
|
||||
/// </summary>
|
||||
WFERROR_INVALID_VERSION = 1,
|
||||
/// <summary>
|
||||
/// The buffer in some operations is insufficient, please try expanding buffer.
|
||||
/// </summary>
|
||||
WFERROR_INSUFFICIENT_BUFFER = 2,
|
||||
/// <summary>
|
||||
/// Some essential pointer variable is NULL.
|
||||
/// </summary>
|
||||
WFERROR_NULLPTR = 3,
|
||||
WFERROR_WIN32 = 5,
|
||||
WFERROR_ALLOC = 6,
|
||||
WFERROR_END_OF_TAIL = 7,
|
||||
WFERROR_CRT = 8,
|
||||
WFERROR_INVALID_ARGUMENTS = 9
|
||||
}WFERROR;
|
||||
|
||||
/// <summary>
|
||||
/// wfassoc Profile Struct
|
||||
/// </summary>
|
||||
typedef struct _WFAPP_PROFILE {
|
||||
/// <summary>
|
||||
/// wfassoc version. Fill it with `WFVERSION` in your application. This field is used for version checking.
|
||||
/// </summary>
|
||||
uint32_t WFVersion;
|
||||
/// <summary>
|
||||
/// The path to locate your application.
|
||||
/// </summary>
|
||||
wchar_t* AppPath;
|
||||
/// <summary>
|
||||
/// The command will be executed when opening files.
|
||||
/// </summary>
|
||||
wchar_t* AppCommand;
|
||||
/// <summary>
|
||||
/// Your application's ProgID.
|
||||
/// For more detail about how to create your ProgID, please browse: https://docs.microsoft.com/en-us/windows/win32/shell/fa-progids
|
||||
/// We also provide a generator for creating legacy ProgID. Use `WFGenerateProgID` to create a legacy ProgID.
|
||||
/// </summary>
|
||||
wchar_t* ProgID_Vendor;
|
||||
wchar_t* ProgID_Component;
|
||||
uint32_t ProgID_Version;
|
||||
/// <summary>
|
||||
/// Your application supported file extensions.
|
||||
/// The structure of this field is connecting all supported extensions with dot(.) end to end. For example:
|
||||
/// `.jpg\0.png\0.gif\0`
|
||||
/// </summary>
|
||||
wchar_t* SupportedTypes;
|
||||
BOOL RegisterForAllUsers;
|
||||
BOOL SetAsDefault;
|
||||
BOOL ShowInOpenWithMenu;
|
||||
BOOL UseOpenWithList;
|
||||
}WFAPP_PROFILE;
|
||||
|
||||
typedef struct _WFAPP_INTERNAL_PROFILE {
|
||||
WFString* AppPath;
|
||||
WFString* AppBasePath;
|
||||
WFString* AppFileName;
|
||||
|
||||
WFString* ProgID;
|
||||
|
||||
WFString* AppCommand;
|
||||
|
||||
WFLinkedList* SupportedTypes;
|
||||
|
||||
BOOL RegisterForAllUsers;
|
||||
BOOL SetAsDefault;
|
||||
BOOL ShowInOpenWithMenu;
|
||||
BOOL UseOpenWithList;
|
||||
}WFAPP_INTERNAL_PROFILE;
|
||||
|
||||
typedef struct _WFString {
|
||||
wchar_t* mRawData;
|
||||
uint32_t mLength;
|
||||
uint32_t mCapacity;
|
||||
|
||||
uint32_t mRealLength;
|
||||
uint32_t mRealCapacity;
|
||||
}WFString;
|
||||
|
||||
WFERROR WFString_Alloc_Wchar(WFString** strl, const wchar_t* raw_data);
|
||||
WFERROR WFString_Alloc_Char(WFString** strl, const char* raw_data, BOOL is_utf8);
|
||||
WFERROR WFString_Alloc_Capacity(WFString** strl, uint32_t size);
|
||||
WFERROR WFString_Alloc(WFString** strl);
|
||||
WFERROR WFString_Free(WFString* strl);
|
||||
WFERROR WFString_Resize(WFString* strl, uint32_t new_size);
|
||||
WFERROR WFString_GetData(WFString* strl, wchar_t** pdata);
|
||||
WFERROR WFString_SetData(WFString* strl, const wchar_t* data);
|
||||
WFERROR WFString_GetLength(WFString* strl, uint32_t* len);
|
||||
WFERROR WFString_Printf(WFString* strl, const wchar_t* format, ...);
|
||||
WFERROR WFString_Concat_Wchar(WFString* strl, const wchar_t* extra);
|
||||
WFERROR WFString_Concat_String(WFString* strl, WFString* extra);
|
||||
WFERROR WFString_SubString(WFString* strl, WFString* substring, uint32_t start_index, uint32_t length);
|
||||
|
||||
typedef WFERROR(*WFLinkedListNode_FreeDataFunc)(void* data);
|
||||
typedef struct _WFLinkedListNode {
|
||||
void* mRawData;
|
||||
WFLinkedListNode* mNext;
|
||||
}WFLinkedListNode;
|
||||
typedef struct _WFLinkedList {
|
||||
WFLinkedListNode* mHead;
|
||||
WFLinkedListNode* mTail;
|
||||
uint32_t mLength;
|
||||
}WFLinkedList;
|
||||
|
||||
WFERROR WFLinkedList_Alloc(WFLinkedList** list);
|
||||
WFERROR WFLinkedList_Free(WFLinkedList* list);
|
||||
WFERROR WFLinkedList_Free_Full(WFLinkedList* list, WFLinkedListNode_FreeDataFunc free_func);
|
||||
WFERROR WFLinkedList_Add(WFLinkedList* list, void* data);
|
||||
WFERROR WFLinkedList_NodeIterator(WFLinkedList* list, WFLinkedListNode** node_ptr);
|
||||
WFERROR WFLinkedListNode_GetData(WFLinkedListNode* node, void** pdata);
|
||||
|
||||
#endif
|
||||
@@ -1,81 +0,0 @@
|
||||
#include "../wfassoc/wfassoc.h"
|
||||
#include <stdio.h>
|
||||
#include <tchar.h>
|
||||
|
||||
#define COMMAND_MAX_LENGTH 128
|
||||
|
||||
WFAPP_PROFILE* create_profile();
|
||||
TCHAR* create_progId();
|
||||
void free_profile(WFAPP_PROFILE* profile);
|
||||
|
||||
int main(int argc, char* args[]) {
|
||||
|
||||
// alloc application profile
|
||||
WFAPP_PROFILE* profile = create_profile();
|
||||
if (profile == NULL) {
|
||||
puts("Error: Fail to allocating profile structure.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// alloc input string and check it
|
||||
char* input_str = malloc(sizeof(char) * (COMMAND_MAX_LENGTH + 1));
|
||||
if (input_str == NULL) return;
|
||||
|
||||
// accept input
|
||||
while (TRUE) {
|
||||
gets_s(input_str, COMMAND_MAX_LENGTH);
|
||||
if (!strcmp(input_str, "install")) {
|
||||
_tprintf(TEXT("%s\n"), profile->ProgID);
|
||||
} else if (!strcmp(input_str, "uninstall")) {
|
||||
_tprintf(TEXT("%s\n"), profile->ProgID);
|
||||
} else if (!strcmp(input_str, "quit")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//free input string
|
||||
free(input_str);
|
||||
return 0;
|
||||
}
|
||||
|
||||
WFAPP_PROFILE* create_profile() {
|
||||
WFAPP_PROFILE* profile = (WFAPP_PROFILE*)malloc(sizeof(WFAPP_PROFILE));
|
||||
if (profile == NULL) return NULL;
|
||||
memset(profile, 0, sizeof(WFAPP_PROFILE));
|
||||
|
||||
profile->WFVersion = WFVERSION;
|
||||
profile->AppPath = TEXT("E:\\pineapple-picture\\ppic.exe");
|
||||
profile->AppCommand = TEXT("E:\\pineapple-picture\\ppic.exe \"%1\"");
|
||||
profile->SupportedTypes = TEXT(".png\0.jpg\0");
|
||||
profile->ProgID = create_progId();
|
||||
if (profile->ProgID == NULL) {
|
||||
free_profile(profile);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
TCHAR* create_progId() {
|
||||
TCHAR* progid = NULL;
|
||||
int progIdSize;
|
||||
if (WFFAILED(WFGenerateProgID(TEXT("PineapplePicture"), TEXT("Image"), 0, NULL, &progIdSize))) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
progid = (TCHAR*)malloc(sizeof(TCHAR) * progIdSize);
|
||||
if (progid == NULL) return NULL;
|
||||
memset(progid, 0, sizeof(TCHAR) * progIdSize);
|
||||
|
||||
if (WFFAILED(WFGenerateProgID(TEXT("PineapplePicture"), TEXT("Image"), 0, progid, &progIdSize))) {
|
||||
free(progid);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return progid;
|
||||
}
|
||||
|
||||
void free_profile(WFAPP_PROFILE* profile) {
|
||||
if (profile->ProgID != NULL) free(profile->ProgID);
|
||||
free(profile);
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug_MB|Win32">
|
||||
<Configuration>Debug_MB</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug_MB|x64">
|
||||
<Configuration>Debug_MB</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug_UNICODE|Win32">
|
||||
<Configuration>Debug_UNICODE</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release_UNICODE|Win32">
|
||||
<Configuration>Release_UNICODE</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug_UNICODE|x64">
|
||||
<Configuration>Debug_UNICODE</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release_UNICODE|x64">
|
||||
<Configuration>Release_UNICODE</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{ad275ad7-cbd5-41ca-ab24-bb707b3f7534}</ProjectGuid>
|
||||
<RootNamespace>wfassocexample</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_UNICODE|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_MB|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release_UNICODE|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_UNICODE|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_MB|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release_UNICODE|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug_UNICODE|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug_MB|Win32'" Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release_UNICODE|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug_UNICODE|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug_MB|x64'" Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release_UNICODE|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_UNICODE|Win32'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
<OutDir>$(SolutionDir)builds\Debug\</OutDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_MB|Win32'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
<OutDir>$(SolutionDir)builds\Debug\</OutDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release_UNICODE|Win32'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
<OutDir>$(SolutionDir)builds\Release\</OutDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_UNICODE|x64'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_MB|x64'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release_UNICODE|x64'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug_UNICODE|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<CompileAs>CompileAsC</CompileAs>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug_MB|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<CompileAs>CompileAsC</CompileAs>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release_UNICODE|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<CompileAs>CompileAsC</CompileAs>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug_UNICODE|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug_MB|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release_UNICODE|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\wfassoc\wfassoc.vcxproj">
|
||||
<Project>{4aac8f0c-3e1c-4584-b682-05bbf96a813f}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="main.c" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="源文件">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="头文件">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="资源文件">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="main.c">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -12,6 +12,5 @@ crate-type = ["cdylib"]
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
wfassoc = { path="../wfassoc" }
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "0.29.0"
|
||||
slotmap = "1.1.1"
|
||||
num_enum = "0.7.6"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Wfassoc C/C++ Library
|
||||
# WFAssoc C/C++ Binding
|
||||
|
||||
For how to utilize this library, please see our example located in `example/ppic`. It is a Qt project built with CMake and demonstrate basically all usage of exposed functions in this dynamic library.
|
||||
|
||||
All crucial spots are written in this example as the code comment so I don't write them in there again.
|
||||
|
||||
Due to the limitation of Rust compiler, you may need to enable `RUSTC_BOOTSTRAP=1` environment when creating binding (it is not necessary for just building this dynamic library). See [this GitHub issue](https://github.com/mozilla/cbindgen/issues/1015) for more info.
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# Findwfassoc.cmake
|
||||
# ----------------
|
||||
# Find wfassoc library and headers.
|
||||
#
|
||||
# This module requires the user to set wfassoc_ROOT to the installation
|
||||
# directory of wfassoc. The directory structure under wfassoc_ROOT must be:
|
||||
# bin/ - contains wfassoc_cdylib.dll
|
||||
# include/ - contains wfassoc.h and wfassoc++.h
|
||||
# lib/ - contains wfassoc_cdylib.dll.lib (import library)
|
||||
#
|
||||
# This module defines the following variables:
|
||||
# wfassoc_FOUND - True if wfassoc was found
|
||||
# wfassoc_INCLUDE_DIRS - Path to wfassoc include directory
|
||||
# wfassoc_LIBRARIES - Path to wfassoc import library
|
||||
# wfassoc_DLL - Path to wfassoc DLL
|
||||
# wfassoc_ROOT - The root directory (user-provided)
|
||||
#
|
||||
# This module also creates the following imported targets:
|
||||
# wfassoc::wfassoc - Main wfassoc library (includes both include and link)
|
||||
#
|
||||
|
||||
set(wfassoc_FOUND FALSE)
|
||||
|
||||
# Require user to set wfassoc_ROOT
|
||||
if(NOT wfassoc_ROOT)
|
||||
message(FATAL_ERROR "wfassoc_ROOT must be set to the installation directory of wfassoc")
|
||||
endif()
|
||||
|
||||
# Check existence of required subdirectories
|
||||
if(NOT EXISTS ${wfassoc_ROOT})
|
||||
message(FATAL_ERROR "wfassoc_ROOT directory does not exist: ${wfassoc_ROOT}")
|
||||
endif()
|
||||
|
||||
set(wfassoc_INCLUDE_DIR ${wfassoc_ROOT}/include)
|
||||
set(wfassoc_LIB_DIR ${wfassoc_ROOT}/lib)
|
||||
set(wfassoc_BIN_DIR ${wfassoc_ROOT}/bin)
|
||||
|
||||
# Find header files
|
||||
if(EXISTS ${wfassoc_INCLUDE_DIR}/wfassoc.h AND EXISTS ${wfassoc_INCLUDE_DIR}/wfassoc++.h)
|
||||
set(wfassoc_INCLUDE_DIRS ${wfassoc_INCLUDE_DIR})
|
||||
else()
|
||||
message(SEND_ERROR "Missing wfassoc header files in ${wfassoc_INCLUDE_DIR}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Find import library (.lib)
|
||||
find_file(wfassoc_LIBRARIES
|
||||
NAMES wfassoc_cdylib.dll.lib
|
||||
PATHS ${wfassoc_LIB_DIR}
|
||||
NO_DEFAULT_PATH
|
||||
DOC "wfassoc import library"
|
||||
)
|
||||
|
||||
if(NOT wfassoc_LIBRARIES)
|
||||
message(SEND_ERROR "Missing wfassoc import library (wfassoc_cdylib.dll.lib) in ${wfassoc_LIB_DIR}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Find DLL file
|
||||
find_file(wfassoc_DLL
|
||||
NAMES wfassoc_cdylib.dll
|
||||
PATHS ${wfassoc_BIN_DIR}
|
||||
NO_DEFAULT_PATH
|
||||
DOC "wfassoc dynamic library"
|
||||
)
|
||||
|
||||
if(NOT wfassoc_DLL)
|
||||
message(SEND_ERROR "Missing wfassoc DLL (wfassoc_cdylib.dll) in ${wfassoc_BIN_DIR}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Everything found
|
||||
set(wfassoc_FOUND TRUE)
|
||||
|
||||
# Mark variables as advanced for ccmake/cmake-gui
|
||||
mark_as_advanced(wfassoc_INCLUDE_DIRS wfassoc_LIBRARIES wfassoc_DLL)
|
||||
|
||||
# Create imported target for wfassoc
|
||||
if(wfassoc_FOUND AND NOT TARGET wfassoc::wfassoc)
|
||||
add_library(wfassoc::wfassoc SHARED IMPORTED)
|
||||
|
||||
# Set include directories
|
||||
set_target_properties(wfassoc::wfassoc PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES ${wfassoc_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
# Set import library location
|
||||
set_target_properties(wfassoc::wfassoc PROPERTIES
|
||||
IMPORTED_IMPLIB "${wfassoc_LIBRARIES}"
|
||||
IMPORTED_LOCATION "${wfassoc_DLL}"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Optional: Print status message
|
||||
if(wfassoc_FOUND)
|
||||
message(STATUS "Found wfassoc:")
|
||||
message(STATUS " Root : ${wfassoc_ROOT}")
|
||||
message(STATUS " Include : ${wfassoc_INCLUDE_DIRS}")
|
||||
message(STATUS " Library : ${wfassoc_LIBRARIES}")
|
||||
message(STATUS " DLL : ${wfassoc_DLL}")
|
||||
endif()
|
||||
@@ -0,0 +1,693 @@
|
||||
/**
|
||||
* @file wfassoc++.h
|
||||
* @brief Windows File Association C++ API header
|
||||
*
|
||||
* This header provides C++ API for managing Windows file associations,
|
||||
* based on its C-compatible API.
|
||||
* The API is designed to work with at least C++17.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#ifndef WFASSOCPP_H_
|
||||
#define WFASSOCPP_H_
|
||||
|
||||
#include "wfassoc.h"
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
* @brief Windows File Association C++ API namespace
|
||||
*
|
||||
* Provides C++ RAII wrappers around the C-compatible wfassoc API.
|
||||
*/
|
||||
namespace wfassocpp {
|
||||
|
||||
/** @brief Type representing a null-terminated UTF-8 C-style string */
|
||||
using CString = wfassoc::WFCString;
|
||||
/**
|
||||
* @brief Type representing a handle/token for managed objects
|
||||
*
|
||||
* This library uses an object pool to manage any objects created during calling.
|
||||
* This type is exposed as an opaque handle for visiting your created object.
|
||||
*/
|
||||
using Token = wfassoc::WFToken;
|
||||
/**
|
||||
* @brief Type representing an icon handle (opaque pointer)
|
||||
*
|
||||
* This type is equivalent to the Win32 HICON type.
|
||||
*/
|
||||
using HICON = wfassoc::WFHICON;
|
||||
/** @brief Invalid icon handle value */
|
||||
using INVALID_HICON = wfassoc::WF_INVALID_HICON;
|
||||
/** @brief Invalid index value used for error conditions */
|
||||
using INVALID_INDEX = wfassoc::WF_INVALID_INDEX;
|
||||
/**
|
||||
* @brief Registration scope for file associations
|
||||
*
|
||||
* Determines whether a program is registered for the current user or system-wide.
|
||||
*/
|
||||
using Scope = wfassoc::WFScope;
|
||||
/**
|
||||
* @brief View mode for querying file association status
|
||||
*
|
||||
* Determines how the association status is viewed/queried.
|
||||
*/
|
||||
using View = wfassoc::WFView;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @brief Check the result of a C API call and throw on failure
|
||||
*
|
||||
* @param[in] result Boolean result from a C API call
|
||||
* @throws std::runtime_error if result is false, with the error message from WFGetLastError()
|
||||
*/
|
||||
inline void _Check(bool result) {
|
||||
if (!result) {
|
||||
throw std::runtime_error(wfassoc::WFGetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @brief Get the invalid token value
|
||||
*
|
||||
* In theory, invalid token value should be a constant value.
|
||||
* However, due to the library used in Rust side, this value can only be fetched at runtime.
|
||||
* This function caches the value on first call for subsequent use.
|
||||
*
|
||||
* @return An invalid token value
|
||||
*/
|
||||
inline Token _INVALID_TOKEN() {
|
||||
static Token v = wfassoc::WFInvalidToken();
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Schema object for defining a program's file association configuration
|
||||
*
|
||||
* A Schema is a sketchpad of a complete program.
|
||||
* Create a Schema object first, then convert it to a Program object for following operations.
|
||||
*
|
||||
* This class is move-only (non-copyable).
|
||||
*/
|
||||
class Schema {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Schema object
|
||||
* @throws std::runtime_error if schema creation fails
|
||||
*/
|
||||
Schema() { _Check(wfassoc::WFSchemaCreate(&_token)); }
|
||||
|
||||
/**
|
||||
* @brief Destroy the Schema object
|
||||
*
|
||||
* Releases resources associated with the Schema object.
|
||||
*/
|
||||
~Schema() {
|
||||
if (_token != _INVALID_TOKEN()) {
|
||||
wfassoc::WFSchemaDestroy(_token);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Non-copyable */
|
||||
Schema(const Schema&) = delete;
|
||||
/** @brief Non-copyable */
|
||||
Schema& operator=(const Schema&) = delete;
|
||||
|
||||
/** @brief Move constructor */
|
||||
Schema(Schema&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
|
||||
/** @brief Move assignment operator */
|
||||
Schema& operator=(Schema&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (_token != _INVALID_TOKEN()) {
|
||||
wfassoc::WFSchemaDestroy(_token);
|
||||
}
|
||||
_token = other._token;
|
||||
other._token = _INVALID_TOKEN();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the program identifier for this Schema
|
||||
*
|
||||
* @param[in] value Null-terminated UTF-8 string containing the identifier.
|
||||
* This identifier should not be empty, must start with alphabet character,
|
||||
* and follow with alphabet characters, digits, underline, or hyphens.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void SetIdentifier(const char* value) { _Check(wfassoc::WFSchemaSetIdentifier(_token, value)); }
|
||||
|
||||
/**
|
||||
* @brief Set the program path for this Schema
|
||||
*
|
||||
* @param[in] value Null-terminated UTF-8 string containing the program path.
|
||||
* This path should be the fully qualified path to the application.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void SetPath(const char* value) { _Check(wfassoc::WFSchemaSetPath(_token, value)); }
|
||||
|
||||
/**
|
||||
* @brief Set the program CLSID for this Schema
|
||||
*
|
||||
* @param[in] value Null-terminated UTF-8 string containing the CLSID.
|
||||
* This CLSID string should be in the format of @c {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}.
|
||||
* Please note that curly braces are required.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void SetClsid(const char* value) { _Check(wfassoc::WFSchemaSetClsid(_token, value)); }
|
||||
|
||||
/**
|
||||
* @brief Set the program name for this Schema (optional)
|
||||
*
|
||||
* @param[in] value Null-terminated UTF-8 string containing the name, or nullptr to clear
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void SetName(const char* value) { _Check(wfassoc::WFSchemaSetName(_token, value)); }
|
||||
|
||||
/**
|
||||
* @brief Set the program icon for this Schema (optional)
|
||||
*
|
||||
* @param[in] value Null-terminated UTF-8 string containing the icon path, or nullptr to clear
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void SetIcon(const char* value) { _Check(wfassoc::WFSchemaSetIcon(_token, value)); }
|
||||
|
||||
/**
|
||||
* @brief Set the program behavior for this Schema (optional)
|
||||
*
|
||||
* @param[in] value Null-terminated UTF-8 string containing the behavior command, or nullptr to clear
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void SetBehavior(const char* value) { _Check(wfassoc::WFSchemaSetBehavior(_token, value)); }
|
||||
|
||||
/**
|
||||
* @brief Add a string resource entry to this Schema
|
||||
*
|
||||
* @param[in] name Null-terminated UTF-8 string containing the name of this entry
|
||||
* @param[in] value Null-terminated UTF-8 string containing the value of this entry.
|
||||
* It can be a plain string or a reference string to resource.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void AddStr(const char* name, const char* value) { _Check(wfassoc::WFSchemaAddStr(_token, name, value)); }
|
||||
|
||||
/**
|
||||
* @brief Add an icon registry entry to this Schema
|
||||
*
|
||||
* @param[in] name Null-terminated UTF-8 string containing the name of this entry
|
||||
* @param[in] value Null-terminated UTF-8 string containing the value of this entry.
|
||||
* It can be a path to icon or a reference string to resource.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void AddIcon(const char* name, const char* value) { _Check(wfassoc::WFSchemaAddIcon(_token, name, value)); }
|
||||
|
||||
/**
|
||||
* @brief Add a behavior registry entry to this Schema
|
||||
*
|
||||
* @param[in] name Null-terminated UTF-8 string containing the name of this entry
|
||||
* @param[in] value Null-terminated UTF-8 string containing the value of this entry.
|
||||
* It should be a valid command line string which uses @c %1, @c %2, etc. to represent parameters.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void AddBehavior(const char* name, const char* value) { _Check(wfassoc::WFSchemaAddBehavior(_token, name, value)); }
|
||||
|
||||
/**
|
||||
* @brief Add a file extension to this Schema
|
||||
*
|
||||
* @param[in] ext Null-terminated UTF-8 string containing the file extension name (without leading dot)
|
||||
* @param[in] ext_name Null-terminated UTF-8 string containing the name pointing to associated name for this extension.
|
||||
* This name should be registered by calling AddStr().
|
||||
* @param[in] ext_icon Null-terminated UTF-8 string containing the name pointing to associated icon for this extension.
|
||||
* This name should be registered by calling AddIcon().
|
||||
* @param[in] ext_behavior Null-terminated UTF-8 string containing the name pointing to associated behavior for this extension.
|
||||
* This name should be registered by calling AddBehavior().
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void AddExt(const char* ext, const char* ext_name, const char* ext_icon, const char* ext_behavior) {
|
||||
_Check(wfassoc::WFSchemaAddExt(_token, ext, ext_name, ext_icon, ext_behavior));
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Program;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @brief Release ownership of the token
|
||||
*
|
||||
* @return The released token
|
||||
*/
|
||||
Token Release() noexcept {
|
||||
Token t = _token;
|
||||
_token = _INVALID_TOKEN();
|
||||
return t;
|
||||
}
|
||||
|
||||
Token _token;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Icon resource object for managing icon handles
|
||||
*
|
||||
* This class wraps an icon resource token and provides access to the icon handle.
|
||||
* The icon handle will be freed when this object is destroyed.
|
||||
*
|
||||
* This class is move-only (non-copyable).
|
||||
*/
|
||||
class IconRc {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct an IconRc from an existing token
|
||||
*
|
||||
* @param[in] token The icon resource token to wrap
|
||||
*/
|
||||
explicit IconRc(Token token) : _token(token) {}
|
||||
|
||||
/**
|
||||
* @brief Destroy the IconRc object
|
||||
*
|
||||
* Releases resources associated with the icon resource.
|
||||
*/
|
||||
~IconRc() {
|
||||
if (_token != _INVALID_TOKEN()) {
|
||||
wfassoc::WFIconRcDestroy(_token);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Non-copyable */
|
||||
IconRc(const IconRc&) = delete;
|
||||
/** @brief Non-copyable */
|
||||
IconRc& operator=(const IconRc&) = delete;
|
||||
|
||||
/** @brief Move constructor */
|
||||
IconRc(IconRc&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
|
||||
/** @brief Move assignment operator */
|
||||
IconRc& operator=(IconRc&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (_token != _INVALID_TOKEN()) {
|
||||
wfassoc::WFIconRcDestroy(_token);
|
||||
}
|
||||
_token = other._token;
|
||||
other._token = _INVALID_TOKEN();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the icon handle from this icon resource
|
||||
*
|
||||
* @return The icon handle. This handle will be freed when this object is destroyed.
|
||||
* Please make a copy immediately if you need to use it longer.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
HICON GetIcon() {
|
||||
HICON icon = nullptr;
|
||||
_Check(wfassoc::WFIconRcGetIcon(_token, &icon));
|
||||
return icon;
|
||||
}
|
||||
|
||||
private:
|
||||
Token _token;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Extension status object for querying file extension registration status
|
||||
*
|
||||
* This class wraps an extension status token and provides access to extension details.
|
||||
* The extension status object will be freed when this object is destroyed.
|
||||
*
|
||||
* This class is move-only (non-copyable).
|
||||
*/
|
||||
class ExtStatus {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct an ExtStatus from an existing token
|
||||
*
|
||||
* @param[in] token The extension status token to wrap
|
||||
*/
|
||||
explicit ExtStatus(Token token) : _token(token) {}
|
||||
|
||||
/**
|
||||
* @brief Destroy the ExtStatus object
|
||||
*
|
||||
* Releases resources associated with the extension status.
|
||||
*/
|
||||
~ExtStatus() {
|
||||
if (_token != _INVALID_TOKEN()) {
|
||||
wfassoc::WFExtStatusDestroy(_token);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Non-copyable */
|
||||
ExtStatus(const ExtStatus&) = delete;
|
||||
/** @brief Non-copyable */
|
||||
ExtStatus& operator=(const ExtStatus&) = delete;
|
||||
|
||||
/** @brief Move constructor */
|
||||
ExtStatus(ExtStatus&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
|
||||
/** @brief Move assignment operator */
|
||||
ExtStatus& operator=(ExtStatus&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (_token != _INVALID_TOKEN()) {
|
||||
wfassoc::WFExtStatusDestroy(_token);
|
||||
}
|
||||
_token = other._token;
|
||||
other._token = _INVALID_TOKEN();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the display name from this extension status
|
||||
*
|
||||
* The display name will be user specified first, then fallback to its ProgId verbatim.
|
||||
* The library will try to use localized name first, then use raw ProgId name if localized name is not available.
|
||||
*
|
||||
* @return The display name string. There is no possibility that this value is empty.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
std::string GetName() {
|
||||
const char* name = nullptr;
|
||||
_Check(wfassoc::WFExtStatusGetName(_token, &name));
|
||||
return std::string(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the icon from this extension status
|
||||
*
|
||||
* The icon will be user specified first, then fallback to the system default file icon.
|
||||
*
|
||||
* @return The icon handle. This handle will be freed when this object is destroyed.
|
||||
* Please make a copy immediately if you need to use it longer.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
HICON GetIcon() {
|
||||
HICON icon = nullptr;
|
||||
_Check(wfassoc::WFExtStatusGetIcon(_token, &icon));
|
||||
return icon;
|
||||
}
|
||||
|
||||
private:
|
||||
Token _token;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Self extension status object for accessing program-provided extension details
|
||||
*
|
||||
* This class wraps a self extension status token and provides access to extension details
|
||||
* as defined by the program itself.
|
||||
*
|
||||
* This class is move-only (non-copyable).
|
||||
*/
|
||||
class SelfExtStatus {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a SelfExtStatus from an existing token
|
||||
*
|
||||
* @param[in] token The self extension status token to wrap
|
||||
*/
|
||||
explicit SelfExtStatus(Token token) : _token(token) {}
|
||||
|
||||
/**
|
||||
* @brief Destroy the SelfExtStatus object
|
||||
*
|
||||
* Releases resources associated with the self extension status.
|
||||
*/
|
||||
~SelfExtStatus() {
|
||||
if (_token != _INVALID_TOKEN()) {
|
||||
wfassoc::WFSelfExtStatusDestroy(_token);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Non-copyable */
|
||||
SelfExtStatus(const SelfExtStatus&) = delete;
|
||||
/** @brief Non-copyable */
|
||||
SelfExtStatus& operator=(const SelfExtStatus&) = delete;
|
||||
|
||||
/** @brief Move constructor */
|
||||
SelfExtStatus(SelfExtStatus&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
|
||||
/** @brief Move assignment operator */
|
||||
SelfExtStatus& operator=(SelfExtStatus&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (_token != _INVALID_TOKEN()) {
|
||||
wfassoc::WFSelfExtStatusDestroy(_token);
|
||||
}
|
||||
_token = other._token;
|
||||
other._token = _INVALID_TOKEN();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the display name from this self extension status
|
||||
*
|
||||
* The display name will be user specified first, then fallback to its ProgId verbatim.
|
||||
*
|
||||
* @return The display name string. There is no possibility that this value is empty.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
std::string GetName() {
|
||||
const char* name = nullptr;
|
||||
_Check(wfassoc::WFSelfExtStatusGetName(_token, &name));
|
||||
return std::string(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the icon from this self extension status
|
||||
*
|
||||
* The icon will be user specified first, then fallback to the system default file icon.
|
||||
*
|
||||
* @return The icon handle. This handle will be freed when this object is destroyed.
|
||||
* Please make a copy immediately if you need to use it longer.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
HICON GetIcon() {
|
||||
HICON icon = nullptr;
|
||||
_Check(wfassoc::WFSelfExtStatusGetIcon(_token, &icon));
|
||||
return icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the extension string (without leading dot) from this self extension status
|
||||
*
|
||||
* @return The file extension name (without leading dot). There is no possibility that this value is empty.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
std::string GetExt() {
|
||||
const char* inner = nullptr;
|
||||
_Check(wfassoc::WFSelfExtStatusGetExt(_token, &inner));
|
||||
return std::string(inner);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the dotted extension string (with leading dot) from this self extension status
|
||||
*
|
||||
* @return The file extension string (with leading dot). There is no possibility that this value is empty.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
std::string GetDottedExt() {
|
||||
const char* inner = nullptr;
|
||||
_Check(wfassoc::WFSelfExtStatusGetDottedExt(_token, &inner));
|
||||
return std::string(inner);
|
||||
}
|
||||
|
||||
private:
|
||||
Token _token;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Program object for managing file association registration
|
||||
*
|
||||
* A Program is created from a Schema and provides methods for registering,
|
||||
* unregistering, and querying file associations.
|
||||
*
|
||||
* This class is move-only (non-copyable).
|
||||
*/
|
||||
class Program {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a Program from a Schema
|
||||
*
|
||||
* This constructor consumes the Schema object.
|
||||
* The Schema object cannot be used after this call.
|
||||
*
|
||||
* @param[in] schema The Schema to create the Program from (will be consumed)
|
||||
* @throws std::runtime_error if program creation fails
|
||||
*/
|
||||
explicit Program(Schema&& schema) {
|
||||
_Check(wfassoc::WFProgramCreate(schema.Release(), &_token));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Destroy the Program object
|
||||
*
|
||||
* Releases resources associated with the Program.
|
||||
*/
|
||||
~Program() {
|
||||
if (_token != _INVALID_TOKEN()) {
|
||||
wfassoc::WFProgramDestroy(_token);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Non-copyable */
|
||||
Program(const Program&) = delete;
|
||||
/** @brief Non-copyable */
|
||||
Program& operator=(const Program&) = delete;
|
||||
|
||||
/** @brief Move constructor */
|
||||
Program(Program&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
|
||||
/** @brief Move assignment operator */
|
||||
Program& operator=(Program&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (_token != _INVALID_TOKEN()) {
|
||||
wfassoc::WFProgramDestroy(_token);
|
||||
}
|
||||
_token = other._token;
|
||||
other._token = _INVALID_TOKEN();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Resolve the provided program name of this Program
|
||||
*
|
||||
* The name will be user specified first,
|
||||
* then fallback to program manifest file specified name,
|
||||
* and finally fallback to the file name of executable.
|
||||
*
|
||||
* @return The resolved program name. There is no possibility that this value is empty.
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
std::string ResolveName() {
|
||||
const char* name = nullptr;
|
||||
_Check(wfassoc::WFProgramResolveName(_token, &name));
|
||||
return std::string(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Resolve the Program icon resource
|
||||
*
|
||||
* The icon will be user specified first,
|
||||
* then fallback to the first icon of program,
|
||||
* and finally fallback to the system default executable icon.
|
||||
*
|
||||
* @return An IconRc object containing the resolved icon
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
IconRc ResolveIcon() {
|
||||
Token token = _INVALID_TOKEN();
|
||||
_Check(wfassoc::WFProgramResolveIcon(_token, &token));
|
||||
return IconRc(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the number of file extensions in the Program
|
||||
*
|
||||
* @return The number of extensions
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
size_t ExtsLen() {
|
||||
size_t len = 0;
|
||||
_Check(wfassoc::WFProgramExtsLen(_token, &len));
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Find a file extension by its body (extension string)
|
||||
*
|
||||
* @param[in] body Null-terminated UTF-8 string containing the file extension name (without leading dot) to find
|
||||
* @return The index of the extension, or INVALID_INDEX if not found
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
size_t FindExt(const char* body) {
|
||||
size_t index = INVALID_INDEX;
|
||||
_Check(wfassoc::WFProgramFindExt(_token, body, &index));
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Resolve this program provided extension's details by index
|
||||
*
|
||||
* @param[in] index Index of the extension to resolve
|
||||
* @return A SelfExtStatus object containing the extension details
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
SelfExtStatus ResolveExt(size_t index) {
|
||||
Token token = _INVALID_TOKEN();
|
||||
_Check(wfassoc::WFProgramResolveExt(_token, index, &token));
|
||||
return SelfExtStatus(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Register the Program in the specified scope
|
||||
*
|
||||
* @param[in] scope Registration scope
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void Register(Scope scope) { _Check(wfassoc::WFProgramRegister(_token, scope)); }
|
||||
|
||||
/**
|
||||
* @brief Unregister the Program from the specified scope
|
||||
*
|
||||
* @param[in] scope Registration scope
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void Unregister(Scope scope) { _Check(wfassoc::WFProgramUnregister(_token, scope)); }
|
||||
|
||||
/**
|
||||
* @brief Check if the Program is registered in the specified scope
|
||||
*
|
||||
* @param[in] scope Registration scope for checking
|
||||
* @return true if the Program is registered in the specified scope, false otherwise
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
bool IsRegistered(Scope scope) {
|
||||
bool result = false;
|
||||
_Check(wfassoc::WFProgramIsRegistered(_token, scope, &result));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Link a file extension in the specified scope
|
||||
*
|
||||
* @param[in] scope Registration scope
|
||||
* @param[in] index Index of the extension to link
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void LinkExt(Scope scope, size_t index) { _Check(wfassoc::WFProgramLinkExt(_token, scope, index)); }
|
||||
|
||||
/**
|
||||
* @brief Unlink a file extension in the specified scope
|
||||
*
|
||||
* @param[in] scope Registration scope
|
||||
* @param[in] index Index of the extension to unlink
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
void UnlinkExt(Scope scope, size_t index) { _Check(wfassoc::WFProgramUnlinkExt(_token, scope, index)); }
|
||||
|
||||
/**
|
||||
* @brief Query the status of a file extension
|
||||
*
|
||||
* @param[in] view View viewpoint
|
||||
* @param[in] index Index of the extension to query
|
||||
* @return An optional ExtStatus object. Returns std::nullopt if the extension is not found
|
||||
* (usually means that this extension is not registered in the specified scope).
|
||||
* @throws std::runtime_error if the operation fails
|
||||
*/
|
||||
std::optional<ExtStatus> QueryExt(View view, size_t index) {
|
||||
Token token = _INVALID_TOKEN();
|
||||
_Check(wfassoc::WFProgramQueryExt(_token, view, index, &token));
|
||||
if (token == _INVALID_TOKEN()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return ExtStatus(token);
|
||||
}
|
||||
|
||||
private:
|
||||
Token _token;
|
||||
};
|
||||
|
||||
} // namespace wfassocpp
|
||||
|
||||
#endif // WFASSOCPP_H_
|
||||
@@ -0,0 +1,595 @@
|
||||
/**
|
||||
* @file wfassoc.h
|
||||
* @brief Windows File Association C API header
|
||||
*
|
||||
* This header provides a C-compatible API for managing Windows file associations,
|
||||
* including schema creation, program registration, and extension management.
|
||||
* The API is designed to at least work with both C99 and C++17 compilers.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#ifndef WFASSOC_H_
|
||||
#define WFASSOC_H_
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#else // __cplusplus
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#endif // __cplusplus
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
/**
|
||||
* @brief Windows File Association C/C++ API namespace
|
||||
*
|
||||
* This namespace only provide the raw binding to C-compatible wfassoc API in C++.
|
||||
* For the C++ RAII wrappers of this, please see @ref wfassocpp namespace.
|
||||
*/
|
||||
namespace wfassoc {
|
||||
#endif // __cplusplus
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
/** Type representing a null-terminated UTF-8 C-style string */
|
||||
using WFCString = const char*;
|
||||
/**
|
||||
* @brief Type representing a handle/token for managed objects
|
||||
*
|
||||
* This library use object pool to manage any objects created during calling.
|
||||
* And we expose this type as an opaque handle for visiting your created object.
|
||||
*/
|
||||
using WFToken = uint64_t;
|
||||
/**
|
||||
* @brief Type representing an icon handle (opaque pointer)
|
||||
*
|
||||
* This type is equivalent with Win32 HICON type.
|
||||
*/
|
||||
using WFHICON = void*;
|
||||
#else // __cplusplus
|
||||
typedef const char *WFCString;
|
||||
typedef uint64_t WFToken;
|
||||
typedef void *WFHICON;
|
||||
#endif // __cplusplus
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
/** Invalid icon handle value */
|
||||
constexpr WFHICON WF_INVALID_HICON = nullptr;
|
||||
/** Invalid index value used for error conditions */
|
||||
constexpr size_t WF_INVALID_INDEX = static_cast<size_t>(-1);
|
||||
#else // __cplusplus
|
||||
static const WFHICON WF_INVALID_HICON = NULL;
|
||||
static const size_t WF_INVALID_INDEX = ((size_t)-1);
|
||||
#endif // __cplusplus
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
/**
|
||||
* @brief Registration scope for file associations
|
||||
*
|
||||
* Determines whether a program is registered for the current user or system-wide.
|
||||
*/
|
||||
enum class WFScope : uint32_t {
|
||||
/** Current user scope */
|
||||
User = 0u,
|
||||
/** System-wide scope */
|
||||
System = 1u
|
||||
};
|
||||
/**
|
||||
* @brief View mode for querying file association status
|
||||
*
|
||||
* Determines how the association status is viewed/queried.
|
||||
*/
|
||||
enum class WFView : uint32_t {
|
||||
/** User-level view */
|
||||
User = 0u,
|
||||
/** System-level view */
|
||||
System = 1u,
|
||||
/** Combined hybrid view of both user and system */
|
||||
Hybrid = 2u
|
||||
};
|
||||
#else // __cplusplus
|
||||
typedef uint32_t WFScope;
|
||||
/** Current user scope */
|
||||
static const WFScope WF_SCOPE_USER = 0u;
|
||||
/** System-wide scope */
|
||||
static const WFScope WF_SCOPE_SYSTEM = 1u;
|
||||
typedef uint32_t WFView;
|
||||
/** User-level view */
|
||||
static const WFView WF_VIEW_USER = 0u;
|
||||
/** System-level view */
|
||||
static const WFView WF_VIEW_SYSTEM = 1u;
|
||||
/** Combined hybrid view of both user and system */
|
||||
static const WFView WF_VIEW_HYBRID = 2u;
|
||||
#endif // __cplusplus
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif // __cplusplus
|
||||
|
||||
/**
|
||||
* @brief Initialize the wfassoc library
|
||||
*
|
||||
* This function must be called before using the MOST of any other wfassoc functions.
|
||||
*
|
||||
* @return true on success, false on failure.
|
||||
*/
|
||||
bool WFStartup(void);
|
||||
|
||||
/**
|
||||
* @brief Shutdown the wfassoc library
|
||||
*
|
||||
* Cleans up all allocated resources and object pools.
|
||||
* Should be called when done using the library.
|
||||
*
|
||||
* @return true on success, false on failure.
|
||||
*/
|
||||
bool WFShutdown(void);
|
||||
|
||||
/**
|
||||
* @brief Get the last error message
|
||||
*
|
||||
* Returns a human-readable error message describing the last error that occurred.
|
||||
* The returned error message string is valid until the next API call.
|
||||
*
|
||||
* For most functions located in this library, except some special function indicated in their notes,
|
||||
* they return boolean value indicating whether function is successful or not.
|
||||
* Once they fail, you can call this function to get a human-readable error message.
|
||||
*
|
||||
* The execution of this function do not need to be wrapped by WFStartup() and WFShutdown().
|
||||
*
|
||||
* The string this function return use different buffer with function return string value.
|
||||
* So you don't worry about that calling this function may invalidate function function return string value.
|
||||
*
|
||||
* @return Null-terminated UTF-8 string containing the error message.
|
||||
* If no error has occurred, the string is empty.
|
||||
* There is no possibility of a NULL return value.
|
||||
*/
|
||||
WFCString WFGetLastError(void);
|
||||
|
||||
/**
|
||||
* @brief Check if the current process has administrative privileges
|
||||
*
|
||||
* This function will not throw any error.
|
||||
* There is no necessity to call WFGetLastError() after this function.
|
||||
* The return value only indicates whether the current process has administrative privileges or not.
|
||||
*
|
||||
* The execution of this function do not need to be wrapped by WFStartup() and WFShutdown().
|
||||
*
|
||||
* @return true if running with admin privileges, false otherwise
|
||||
*/
|
||||
bool WFHasPrivilege(void);
|
||||
|
||||
/**
|
||||
* @brief Get an invalid token value
|
||||
*
|
||||
* In theory, invalid token value should be a constant value.
|
||||
* However, due to the library I used in Rust side, this value only can be fetched at runtime.
|
||||
* So I expose this function to make programmer can fetch this constant value.
|
||||
* Theoretically, you just need to fetch this function only once at the beginning of your program.
|
||||
*
|
||||
* The execution of this function do not need to be wrapped by WFStartup() and WFShutdown().
|
||||
*
|
||||
* @return An invalid token value
|
||||
*/
|
||||
WFToken WFInvalidWFToken(void);
|
||||
|
||||
/**
|
||||
* @brief Create a new Schema object
|
||||
*
|
||||
* A Schema is a sketchpad of a complete program.
|
||||
* For the user of this library, they should create a Schema object first.
|
||||
* Then convert it to a Program object for following operations.
|
||||
*
|
||||
* @param[out] out_schema Pointer to receive the Schema token.
|
||||
* The receiver take the ownership of this Schema object.
|
||||
* And it should be freed by calling WFSchemaDestroy() when it is no longer needed,
|
||||
* or consumed by creating a Program object via WFProgramCreate().
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSchemaCreate(WFToken *out_schema);
|
||||
|
||||
/**
|
||||
* @brief Destroy a Schema object
|
||||
*
|
||||
* Releases resources associated with the Schema object.
|
||||
*
|
||||
* Usually you do not need to call this function,
|
||||
* because the convertion function from Schema to Program will consume given Schema object to produce Program object.
|
||||
*
|
||||
* @param[in] in_schema Schema token to destroy
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSchemaDestroy(WFToken in_schema);
|
||||
|
||||
/**
|
||||
* @brief Set the program identifier for a Schema
|
||||
*
|
||||
* @param[in] in_schema Schema token
|
||||
* @param[in] in_value Null-terminated UTF-8 string containing the identifier.
|
||||
* This identifier should not be empty, must start with alphabet character,
|
||||
* and follow with alphabet characters, digits, underline, or hyphens.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSchemaSetIdentifier(WFToken in_schema, WFCString in_value);
|
||||
|
||||
/**
|
||||
* @brief Set the program path for a Schema
|
||||
*
|
||||
* @param[in] in_schema Schema token
|
||||
* @param[in] in_value Null-terminated UTF-8 string containing the program path.
|
||||
* This path should be the fully qualified path to the application.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSchemaSetPath(WFToken in_schema, WFCString in_value);
|
||||
|
||||
/**
|
||||
* @brief Set the program CLSID for a Schema
|
||||
*
|
||||
* @param[in] in_schema Schema token
|
||||
* @param[in] in_value Null-terminated UTF-8 string containing the CLSID.
|
||||
* This CLSID string should be in the format of @c {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} .
|
||||
* Please note that curly braces are required.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSchemaSetClsid(WFToken in_schema, WFCString in_value);
|
||||
|
||||
/**
|
||||
* @brief Set the program name for a Schema (optional)
|
||||
*
|
||||
* @param[in] in_schema Schema token
|
||||
* @param[in] in_value Null-terminated UTF-8 string containing the name, or NULL to clear
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSchemaSetName(WFToken in_schema, WFCString in_value);
|
||||
|
||||
/**
|
||||
* @brief Set the program icon for a Schema (optional)
|
||||
*
|
||||
* @param[in] in_schema Schema token
|
||||
* @param[in] in_value Null-terminated UTF-8 string containing the icon path, or NULL to clear
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSchemaSetIcon(WFToken in_schema, WFCString in_value);
|
||||
|
||||
/**
|
||||
* @brief Set the program behavior for a Schema (optional)
|
||||
*
|
||||
* @param[in] in_schema Schema token
|
||||
* @param[in] in_value Null-terminated UTF-8 string containing the behavior command, or NULL to clear
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSchemaSetBehavior(WFToken in_schema, WFCString in_value);
|
||||
|
||||
/**
|
||||
* @brief Add a string resource entry to a Schema
|
||||
*
|
||||
* @param[in] in_schema Schema token
|
||||
* @param[in] in_name Null-terminated UTF-8 string containing the name of this entry
|
||||
* @param[in] in_value Null-terminated UTF-8 string containing the value of this entry.
|
||||
* It can be a plain string or a reference string to resource.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSchemaAddStr(WFToken in_schema, WFCString in_name, WFCString in_value);
|
||||
|
||||
/**
|
||||
* @brief Add an icon registry entry to a Schema
|
||||
*
|
||||
* @param[in] in_schema Schema token
|
||||
* @param[in] in_name Null-terminated UTF-8 string containing the name of this entry
|
||||
* @param[in] in_value Null-terminated UTF-8 string containing the value of this entry.
|
||||
* It can be a path to icon or a reference string to resource.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSchemaAddIcon(WFToken in_schema, WFCString in_name, WFCString in_value);
|
||||
|
||||
/**
|
||||
* @brief Add a behavior registry entry to a Schema
|
||||
*
|
||||
* @param[in] in_schema Schema token
|
||||
* @param[in] in_name Null-terminated UTF-8 string containing the name of this entry
|
||||
* @param[in] in_value Null-terminated UTF-8 string containing the value of this entry.
|
||||
* It should be a valid command line string which use \c %1, \c %2, etc. to represent parameters.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSchemaAddBehavior(WFToken in_schema, WFCString in_name, WFCString in_value);
|
||||
|
||||
/**
|
||||
* @brief Add a file extension to a Schema
|
||||
*
|
||||
* @param[in] in_schema Schema token
|
||||
* @param[in] in_ext Null-terminated UTF-8 string containing the file extension name (without leading dot).
|
||||
* @param[in] in_ext_name Null-terminated UTF-8 string containing the name pointing to associated name for this extension.
|
||||
* This name should be registered by calling WFSchemaAddStr().
|
||||
* @param[in] in_ext_icon Null-terminated UTF-8 string containing the name pointing to associated icon for this extension.
|
||||
* This name should be registered by calling WFSchemaAddIcon().
|
||||
* @param[in] in_ext_behavior Null-terminated UTF-8 string containing the name pointing to associated behavior for this extension.
|
||||
* This name should be registered by calling WFSchemaAddBehavior().
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSchemaAddExt(WFToken in_schema,
|
||||
WFCString in_ext,
|
||||
WFCString in_ext_name,
|
||||
WFCString in_ext_icon,
|
||||
WFCString in_ext_behavior);
|
||||
|
||||
/**
|
||||
* @brief Create a Program object from a Schema
|
||||
*
|
||||
* Please note that this function will consume the Schema object.
|
||||
* It means that the Schema object cannot be used after this call.
|
||||
* And you do not need to call WFSchemaDestroy() for this Schema object after this call.
|
||||
*
|
||||
* Please note that the given Schema object will always be consumed,
|
||||
* no matter this function return success or failure.
|
||||
*
|
||||
* @param[in] in_schema Schema token (will be consumed)
|
||||
* @param[out] out_program Pointer to receive the Program token.
|
||||
* The receiver take the ownership of this Program object.
|
||||
* And it should be freed by calling WFProgramDestroy() when it is no longer needed.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFProgramCreate(WFToken in_schema, WFToken *out_program);
|
||||
|
||||
/**
|
||||
* @brief Destroy a Program object
|
||||
*
|
||||
* Releases resources associated with the Program.
|
||||
*
|
||||
* @param[in] in_program Program token to destroy
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFProgramDestroy(WFToken in_program);
|
||||
|
||||
/**
|
||||
* @brief Resolve the provided program name of this Program
|
||||
*
|
||||
* The name will be user specified first,
|
||||
* then fallback to program manifest file specified name,
|
||||
* and finally fallback to the file name of executable.
|
||||
*
|
||||
* @param[in] in_program Program token
|
||||
* @param[out] out_name Pointer to receive the resolved name.
|
||||
* There is no possibility that this value is NULL.
|
||||
* This string will be freed at the next API call. Please make a copy immediately if you need to use it longer.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFProgramResolveName(WFToken in_program, WFCString *out_name);
|
||||
|
||||
/**
|
||||
* @brief Resolve the Program icon resource
|
||||
*
|
||||
* The icon will be user specified first,
|
||||
* the fallback to the first icon of program,
|
||||
* and finally fallback to the system default executable icon.
|
||||
*
|
||||
* @param[in] in_program Program token
|
||||
* @param[out] out_icon_rc Pointer to receive the icon resource token.
|
||||
* The caller take the ownership of created icon resource object.
|
||||
* And it should be freed by calling WFIconRcDestroy() when it is no longer needed.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFProgramResolveIcon(WFToken in_program, WFToken *out_icon_rc);
|
||||
|
||||
/**
|
||||
* @brief Get the number of file extensions in the Program
|
||||
*
|
||||
* @param[in] in_program Program token
|
||||
* @param[out] out_len Pointer to receive the number of extensions
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFProgramExtsLen(WFToken in_program, size_t *out_len);
|
||||
|
||||
/**
|
||||
* @brief Find a file extension by its body (extension string)
|
||||
*
|
||||
* @param[in] in_program Program token
|
||||
* @param[in] in_body Null-terminated UTF-8 string containing the file extension name (without leading dot) to find.
|
||||
* @param[out] out_index Pointer to receive the file extension index, or WF_INVALID_INDEX if not found.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFProgramFindExt(WFToken in_program, WFCString in_body, size_t *out_index);
|
||||
|
||||
/**
|
||||
* @brief Resolve this program provided extension's details by index
|
||||
*
|
||||
* @param[in] in_program Program token
|
||||
* @param[in] in_index Index of the extension to resolve
|
||||
* @param[out] out_self_ext_status Pointer to receive the self extension status token.
|
||||
* The caller take the ownership of created self extension status object.
|
||||
* And it should be freed by calling WFSelfExtStatusDestroy() when it is no longer needed.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFProgramResolveExt(WFToken in_program, size_t in_index, WFToken *out_self_ext_status);
|
||||
|
||||
/**
|
||||
* @brief Register the Program in the specified scope
|
||||
*
|
||||
* @param[in] in_program Program token
|
||||
* @param[in] in_scope Registration scope
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFProgramRegister(WFToken in_program, WFScope in_scope);
|
||||
|
||||
/**
|
||||
* @brief Unregister the Program from the specified scope
|
||||
*
|
||||
* @param[in] in_program Program token
|
||||
* @param[in] in_scope Registration scope
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFProgramUnregister(WFToken in_program, WFScope in_scope);
|
||||
|
||||
/**
|
||||
* @brief Check if the Program is registered in the specified scope
|
||||
*
|
||||
* @param[in] in_program Program token
|
||||
* @param[in] in_scope Registration scope for checking
|
||||
* @param[out] out_is_registered Pointer to receive the registration status.
|
||||
* True if the Program is registered in the specified scope, false otherwise.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFProgramIsRegistered(WFToken in_program, WFScope in_scope, bool *out_is_registered);
|
||||
|
||||
/**
|
||||
* @brief Link a file extension in the specified scope
|
||||
*
|
||||
* @param[in] in_program Program token
|
||||
* @param[in] in_scope Registration scope
|
||||
* @param[in] in_index Index of the extension to link
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFProgramLinkExt(WFToken in_program, WFScope in_scope, size_t in_index);
|
||||
|
||||
/**
|
||||
* @brief Unlink a file extension in the specified scope
|
||||
*
|
||||
* @param[in] in_program Program token
|
||||
* @param[in] in_scope Registration scope
|
||||
* @param[in] in_index Index of the extension to unlink
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFProgramUnlinkExt(WFToken in_program, WFScope in_scope, size_t in_index);
|
||||
|
||||
/**
|
||||
* @brief Query the status of a file extension
|
||||
*
|
||||
* @param[in] in_program Program token
|
||||
* @param[in] in_view View viewpoint.
|
||||
* @param[in] in_index Index of the extension to query
|
||||
* @param[out] out_ext_status Pointer to receive the extension status token, or invalid token if not found.
|
||||
* If the extension is not found, it usually means that this extension is not registered in the specified scope.
|
||||
* The caller take the ownership of created extension status object.
|
||||
* And it should be freed by calling WFExtStatusDestroy() when it is no longer needed.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFProgramQueryExt(WFToken in_program, WFView in_view, size_t in_index, WFToken *out_ext_status);
|
||||
|
||||
/**
|
||||
* @brief Destroy an extension status object
|
||||
*
|
||||
* @param[in] in_ext_status Extension status token to destroy
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFExtStatusDestroy(WFToken in_ext_status);
|
||||
|
||||
/**
|
||||
* @brief Get the display name from an extension status object
|
||||
*
|
||||
* The display will be user specified first,
|
||||
* the fallback to its ProgId verbatim.
|
||||
*
|
||||
* @param[in] in_ext_status Extension status token
|
||||
* @param[out] out_name Pointer to receive the name.
|
||||
* There is no possibility that this value is NULL.
|
||||
* We will try to use localized name first, then use raw ProgId name if localized name is not available.
|
||||
* This string will be freed at the next API call. Please make a copy immediately if you need to use it longer.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFExtStatusGetName(WFToken in_ext_status, WFCString *out_name);
|
||||
|
||||
/**
|
||||
* @brief Get the icon from an extension status object
|
||||
*
|
||||
* The icon will be user specified first,
|
||||
* the fallback to the system default file icon.
|
||||
*
|
||||
* @param[in] in_ext_status Extension status token
|
||||
* @param[out] out_icon Pointer to receive the icon handle.
|
||||
* This icon handle will be freed once this icon resource object is destroyed.
|
||||
* Please make a copy immediately if you need to use it longer.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFExtStatusGetIcon(WFToken in_ext_status, WFHICON *out_icon);
|
||||
|
||||
/**
|
||||
* @brief Destroy a self extension status object
|
||||
*
|
||||
* @param[in] in_self_ext_status Self extension status token to destroy
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSelfExtStatusDestroy(WFToken in_self_ext_status);
|
||||
|
||||
/**
|
||||
* @brief Get the display name from a self extension status object
|
||||
*
|
||||
* The display will be user specified first,
|
||||
* the fallback to its ProgId verbatim.
|
||||
*
|
||||
* @param[in] in_self_ext_status Self extension status token
|
||||
* @param[out] out_name Pointer to receive the name string.
|
||||
* There is no possibility that this value is NULL.
|
||||
* This string will be freed at the next API call. Please make a copy immediately if you need to use it longer.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSelfExtStatusGetName(WFToken in_self_ext_status, WFCString *out_name);
|
||||
|
||||
/**
|
||||
* @brief Get the icon from a self extension status object
|
||||
*
|
||||
* The icon will be user specified first,
|
||||
* the fallback to the system default file icon.
|
||||
*
|
||||
* @param[in] in_self_ext_status Self extension status token
|
||||
* @param[out] out_icon Pointer to receive the icon handle.
|
||||
* This icon handle will be freed once this self extension status object is destroyed.
|
||||
* Please make a copy immediately if you need to use it longer.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSelfExtStatusGetIcon(WFToken in_self_ext_status, WFHICON *out_icon);
|
||||
|
||||
/**
|
||||
* @brief Get the extension string (without leading dot) from a self extension status object
|
||||
*
|
||||
* @param[in] in_self_ext_status Self extension status token
|
||||
* @param[out] out_inner Pointer to receive the file extension name (without leading dot).
|
||||
* There is no possibility that this value is NULL.
|
||||
* This string will be freed at the next API call. Please make a copy immediately if you need to use it longer.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSelfExtStatusGetExt(WFToken in_self_ext_status, WFCString *out_inner);
|
||||
|
||||
/**
|
||||
* @brief Get the dotted extension string (with leading dot) from a self extension status object
|
||||
*
|
||||
* @param[in] in_self_ext_status Self extension status token
|
||||
* @param[out] out_inner Pointer to receive the file extension string (with leading dot).
|
||||
* There is no possibility that this value is NULL.
|
||||
* This string will be freed at the next API call. Please make a copy immediately if you need to use it longer.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFSelfExtStatusGetDottedExt(WFToken in_self_ext_status, WFCString *out_inner);
|
||||
|
||||
/**
|
||||
* @brief Destroy an icon resource object
|
||||
*
|
||||
* @param[in] in_icon_rc Icon resource token to destroy
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFIconRcDestroy(WFToken in_icon_rc);
|
||||
|
||||
/**
|
||||
* @brief Get the icon handle from an icon resource object
|
||||
*
|
||||
* @param[in] in_icon_rc Icon resource token
|
||||
* @param[out] out_icon Pointer to receive the icon handle.
|
||||
* There is no possibility that this value is WF_INVALID_HICON.
|
||||
* This icon handle will be freed once this icon resource object is destroyed.
|
||||
* Please make a copy immediately if you need to use it longer.
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
bool WFIconRcGetIcon(WFToken in_icon_rc, WFHICON *out_icon);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif // __cplusplus
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // namespace wfassoc
|
||||
#endif // __cplusplus
|
||||
|
||||
#endif // WFASSOC_H_
|
||||
@@ -0,0 +1,101 @@
|
||||
//! When calling this dynamic library with outside programs,
|
||||
//! outer programs may usually need to fetch string resource produced by Rust code.
|
||||
//! However it is impossible pass Rust string directly to outer program.
|
||||
//!
|
||||
//! This module provide **thread independent** string cache for resolving this issue.
|
||||
//! When we need pass string to outer programs, we push that string into this string as C-like format,
|
||||
//! then return its pointer to outer program.
|
||||
//! So that outside program can utilize it like calling C/C++ library.
|
||||
//! The only thing that outer programs should note is that this string is volatile,
|
||||
//! once they get it, they must dupliate it immediately before any futher calling to this dynamic library.
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{CStr, CString};
|
||||
use thiserror::Error as TeError;
|
||||
use crate::ffi_types::CStyleString;
|
||||
|
||||
// region: Error
|
||||
|
||||
/// Error occurs in this crate.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum Error {
|
||||
#[error("unexpected NUL when parsing into C/C++ string")]
|
||||
UnexpectedNul(#[from] std::ffi::NulError),
|
||||
|
||||
#[error("given string pointer is nullptr when parsing from C/C++ string")]
|
||||
NullPtr,
|
||||
#[error("invalid UTF8 sequence when parsing from C/C++ string")]
|
||||
InvalidEncoding(#[from] std::str::Utf8Error),
|
||||
}
|
||||
|
||||
/// Result type used in this crate.
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// endregion
|
||||
|
||||
// region: String Cache for Exposing
|
||||
|
||||
struct StringCache {
|
||||
msg: CString,
|
||||
}
|
||||
|
||||
impl StringCache {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
msg: CString::new("").expect("empty string must be valid for CString"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_msg(&mut self, msg: &str) -> Result<()> {
|
||||
self.msg = CString::new(msg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_msg(&self) -> CStyleString {
|
||||
self.msg.as_ptr()
|
||||
}
|
||||
|
||||
pub fn clear_msg(&mut self) {
|
||||
self.msg = CString::new("").expect("empty string must be valid for CString");
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Exposed Functions
|
||||
|
||||
thread_local! {
|
||||
static STRING_CACHE: RefCell<StringCache> = RefCell::new(StringCache::new());
|
||||
}
|
||||
|
||||
/// Set thread local string exposed for C code.
|
||||
pub fn set_ffi_string(msg: &str) -> Result<()> {
|
||||
STRING_CACHE.with(|e| {
|
||||
e.borrow_mut().set_msg(msg)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get const pointer to thread local string exposed for C code.
|
||||
pub fn get_ffi_string() -> CStyleString {
|
||||
STRING_CACHE.with(|e| e.borrow().get_msg())
|
||||
}
|
||||
|
||||
/// Clear thread local string exposed for C code.
|
||||
///
|
||||
/// This function usually should be called at the beginning of every exposed C functions.
|
||||
pub fn clear_ffi_string() {
|
||||
STRING_CACHE.with(|e| {
|
||||
e.borrow_mut().clear_msg();
|
||||
});
|
||||
}
|
||||
|
||||
/// Parse string given by C code into Rust string.
|
||||
pub fn parse_ffi_string<'a>(ptr: CStyleString) -> Result<&'a str> {
|
||||
if ptr.is_null() {
|
||||
Err(Error::NullPtr)
|
||||
} else {
|
||||
let c_str = unsafe { CStr::from_ptr(ptr) };
|
||||
Ok(c_str.to_str()?)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -0,0 +1,77 @@
|
||||
//! The module including all FFI types used by this crate, except string type.
|
||||
//! For string type, see also [crate::cstr_ffi].
|
||||
|
||||
use std::ffi::{c_void, c_char};
|
||||
use num_enum::TryFromPrimitive;
|
||||
|
||||
// region: Misc Types and Constants
|
||||
|
||||
/// The type representing the raw pointer to immutable C-style NUL-terminated string.
|
||||
pub type CStyleString = *const c_char;
|
||||
|
||||
/// The invalid value of index.
|
||||
pub const INVALID_INDEX: usize = usize::MAX;
|
||||
|
||||
// endregion
|
||||
|
||||
// region: HICON
|
||||
|
||||
/// The type representing Win32 HICON handle.
|
||||
///
|
||||
/// In theory, we can fetch HICON type from "windows_sys" crate.
|
||||
/// However, I don't want to add it as this crate's dependency,
|
||||
/// because I don't use anything within it except this type.
|
||||
/// So I check Microsoft document, re-define it in there for this crate.
|
||||
/// Reference: <https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types>
|
||||
pub type HICON = *mut c_void;
|
||||
|
||||
/// The invalid value of Win32 HICON handle.
|
||||
///
|
||||
/// The same reason like [WFHICON] to re-define it in there.
|
||||
pub const INVALID_HICON: HICON = std::ptr::null_mut();
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Scope
|
||||
|
||||
/// The FFI wrapper for [wfassoc::Scope].
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Copy, Clone, TryFromPrimitive)]
|
||||
pub enum Scope {
|
||||
User = 0,
|
||||
System = 1,
|
||||
}
|
||||
|
||||
impl From<Scope> for wfassoc::Scope {
|
||||
fn from(value: Scope) -> Self {
|
||||
match value {
|
||||
Scope::User => wfassoc::Scope::User,
|
||||
Scope::System => wfassoc::Scope::System,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: View
|
||||
|
||||
/// The FFI wrapper for [wfassoc::View].
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Copy, Clone, TryFromPrimitive)]
|
||||
pub enum View {
|
||||
User = 0,
|
||||
System = 1,
|
||||
Hybrid = 2,
|
||||
}
|
||||
|
||||
impl From<View> for wfassoc::View {
|
||||
fn from(value: View) -> Self {
|
||||
match value {
|
||||
View::User => wfassoc::View::User,
|
||||
View::System => wfassoc::View::System,
|
||||
View::Hybrid => wfassoc::View::Hybrid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
+757
-20
@@ -1,24 +1,41 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{CString, c_char};
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
mod cstr_ffi;
|
||||
mod ffi_types;
|
||||
mod last_error;
|
||||
mod string_cache;
|
||||
mod wrapper;
|
||||
mod object_pool;
|
||||
|
||||
use object_pool::ObjectPool;
|
||||
use std::sync::{LazyLock, RwLock};
|
||||
use thiserror::Error as TeError;
|
||||
use wfassoc::highlevel::{Program, Schema};
|
||||
|
||||
// region: Error
|
||||
|
||||
/// Error occurs in this crate.
|
||||
#[derive(Debug, TeError)]
|
||||
enum Error {
|
||||
/// Error occurs in wfassoc core.
|
||||
#[error("{0}")]
|
||||
Core(#[from] wfassoc::Error),
|
||||
/// Error when operating Schema.
|
||||
#[error("wfassoc error: {0}")]
|
||||
Schema(#[from] wfassoc::highlevel::SchemaError),
|
||||
/// Error when parsing Schema into Program.
|
||||
#[error("wfassoc error: {0}")]
|
||||
ParseProgram(#[from] wfassoc::highlevel::ParseProgramError),
|
||||
/// Error when operating Program.
|
||||
#[error("wfassoc error: {0}")]
|
||||
Program(#[from] wfassoc::highlevel::ProgramError),
|
||||
|
||||
#[error("error occurs when parsing into C/C++ string")]
|
||||
CastIntoCStr(#[from] std::ffi::NulError),
|
||||
#[error("error occurs when parsing from C/C++ string")]
|
||||
CastFromCStr(#[from] std::ffi::IntoStringError),
|
||||
/// Error when manipulating with C-style string.
|
||||
#[error("C-Style string FFI error: {0}")]
|
||||
CStrFfi(#[from] cstr_ffi::Error),
|
||||
/// Error when manipulating with object pool.
|
||||
#[error("object pool error: {0}")]
|
||||
ObjectPool(#[from] object_pool::Error),
|
||||
|
||||
/// Error occurs when checking enum value
|
||||
#[error("the enumeration value provided to FFI function is out of its range")]
|
||||
EnumOutOfRange,
|
||||
/// Error when manipulating with poison RwLock
|
||||
#[error("concurrency error: RwLock is poisonous")]
|
||||
PoisonRwLock,
|
||||
}
|
||||
|
||||
/// Result type used in this crate.
|
||||
@@ -28,31 +45,751 @@ type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// region: Macros
|
||||
|
||||
macro_rules! in_param_ty {
|
||||
($t:ty) => {
|
||||
$t
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! out_param_ty {
|
||||
($t:ty) => {
|
||||
*mut $t
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! set_out_param {
|
||||
($lhs:expr, $rhs:expr) => {
|
||||
unsafe { *$lhs = $rhs };
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! resolve_enum {
|
||||
($t:ty, $v:expr) => {
|
||||
<$t>::try_from($v).map_err(|_| Error::EnumOutOfRange)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! pull_reader {
|
||||
($pool:expr) => {
|
||||
$pool.read().map_err(|_| Error::PoisonRwLock)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! pull_writer {
|
||||
($pool:expr) => {
|
||||
$pool.write().map_err(|_| Error::PoisonRwLock)
|
||||
};
|
||||
}
|
||||
|
||||
/// Macro to wrap inner function execution with standard error handling pattern.
|
||||
///
|
||||
/// For functions with no output parameter and no input parameters:
|
||||
/// ```ignore
|
||||
/// cffi_wrapper!(|| {
|
||||
/// // inner function body returning Result<()>
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// For functions with no output parameter and with input parameters:
|
||||
/// ```ignore
|
||||
/// cffi_wrapper!(|param1: Type1, param2: Type2| {
|
||||
/// // inner function body using param1, param2 returning Result<()>
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// For functions with one output parameter and no input parameters:
|
||||
/// ```ignore
|
||||
/// cffi_wrapper!(|| -> (out_param, OutType) {
|
||||
/// // inner function body returning Result<T>
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// For functions with one output parameter and with input parameters:
|
||||
/// ```ignore
|
||||
/// cffi_wrapper!(|param1: Type1| -> (out_param, OutType) {
|
||||
/// // inner function body using param1 returning Result<T>
|
||||
/// });
|
||||
/// ```
|
||||
macro_rules! cffi_wrapper {
|
||||
// Case with output parameter and input parameters
|
||||
(|$($param:ident: $param_ty:ty),*| -> ($out_param:ident: $out_param_ty:ty) $inner_body:block) => {{
|
||||
fn inner($($param: $param_ty),*) -> Result<$out_param_ty> $inner_body
|
||||
|
||||
match inner($($param),*) {
|
||||
Ok(rv) => {
|
||||
set_out_param!($out_param, rv);
|
||||
last_error::clear_last_error();
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
last_error::set_last_error(e.to_string().as_str());
|
||||
false
|
||||
}
|
||||
}
|
||||
}};
|
||||
|
||||
// Case with output parameter and no input parameters
|
||||
(|| -> ($out_param:ident: $out_param_ty:ty) $inner_body:block) => {{
|
||||
fn inner() -> Result<$out_param_ty> $inner_body
|
||||
|
||||
match inner() {
|
||||
Ok(rv) => {
|
||||
set_out_param!($out_param, rv);
|
||||
last_error::clear_last_error();
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
last_error::set_last_error(e.to_string().as_str());
|
||||
false
|
||||
}
|
||||
}
|
||||
}};
|
||||
|
||||
// Case without output parameter but with input parameters
|
||||
(|$($param:ident: $param_ty:ty),*| $inner_body:block) => {{
|
||||
fn inner($($param: $param_ty),*) -> Result<()> $inner_body
|
||||
|
||||
match inner($($param),*) {
|
||||
Ok(_) => {
|
||||
last_error::clear_last_error();
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
last_error::set_last_error(e.to_string().as_str());
|
||||
false
|
||||
}
|
||||
}
|
||||
}};
|
||||
|
||||
// Case without output parameter and no input parameters
|
||||
(|| $inner_body:block) => {{
|
||||
fn inner() -> Result<()> $inner_body
|
||||
|
||||
match inner() {
|
||||
Ok(_) => {
|
||||
last_error::clear_last_error();
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
last_error::set_last_error(e.to_string().as_str());
|
||||
false
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Object Pools
|
||||
|
||||
static SCHEMA_POOL: LazyLock<RwLock<ObjectPool<Schema>>> =
|
||||
LazyLock::new(|| RwLock::new(ObjectPool::new()));
|
||||
|
||||
static PROGRAM_POOL: LazyLock<RwLock<ObjectPool<Program>>> =
|
||||
LazyLock::new(|| RwLock::new(ObjectPool::new()));
|
||||
|
||||
static EXT_STATUS_POOL: LazyLock<RwLock<ObjectPool<wfassoc::highlevel::ProgramExtStatus>>> =
|
||||
LazyLock::new(|| RwLock::new(ObjectPool::new()));
|
||||
|
||||
static SELF_EXT_STATUS_POOL: LazyLock<
|
||||
RwLock<ObjectPool<wfassoc::highlevel::ProgramSelfExtStatus>>,
|
||||
> = LazyLock::new(|| RwLock::new(ObjectPool::new()));
|
||||
|
||||
static ICON_RC_POOL: LazyLock<RwLock<ObjectPool<wfassoc::win32::concept::IconRc>>> =
|
||||
LazyLock::new(|| RwLock::new(ObjectPool::new()));
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Exposed Types
|
||||
|
||||
pub use ffi_types::{
|
||||
CStyleString as WFCString, HICON as WFHICON, Scope as WFScope, View as WFView,
|
||||
};
|
||||
pub use ffi_types::{INVALID_HICON as WF_INVALID_HICON, INVALID_INDEX as WF_INVALID_INDEX};
|
||||
pub use object_pool::Token as WFToken;
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Exposed Functions
|
||||
|
||||
// region: Facilities
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFStartup() -> bool {
|
||||
false
|
||||
// Initialize all pool by fetching writer from them
|
||||
cffi_wrapper!(|| {
|
||||
let _pool = pull_writer!(SCHEMA_POOL)?;
|
||||
let _pool = pull_writer!(PROGRAM_POOL)?;
|
||||
let _pool = pull_writer!(EXT_STATUS_POOL)?;
|
||||
let _pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
|
||||
let _pool = pull_writer!(ICON_RC_POOL)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFShutdown() -> bool {
|
||||
false
|
||||
// Free all pool stored objects
|
||||
cffi_wrapper!(|| {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
pool.clear();
|
||||
let mut pool = pull_writer!(PROGRAM_POOL)?;
|
||||
pool.clear();
|
||||
let mut pool = pull_writer!(EXT_STATUS_POOL)?;
|
||||
pool.clear();
|
||||
let mut pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
|
||||
pool.clear();
|
||||
let mut pool = pull_writer!(ICON_RC_POOL)?;
|
||||
pool.clear();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFGetLastError() -> *const c_char {
|
||||
pub extern "C" fn WFGetLastError() -> WFCString {
|
||||
last_error::get_last_error()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFHasPrivilege() -> bool {
|
||||
wfassoc::utilities::has_privilege()
|
||||
wfassoc::win32::utilities::has_privilege()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFAdd(left: u32, right: u32, rv: *mut u32) -> bool {
|
||||
unsafe { *rv = left + right; }
|
||||
return true;
|
||||
pub extern "C" fn WFInvalidToken() -> WFToken {
|
||||
object_pool::invalid_token()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Schema
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSchemaCreate(out_schema: out_param_ty!(WFToken)) -> bool {
|
||||
cffi_wrapper!(|| -> (out_schema: WFToken) {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
Ok(pool.allocate(Schema::new())?)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSchemaDestroy(in_schema: in_param_ty!(WFToken)) -> bool {
|
||||
cffi_wrapper!(|in_schema: WFToken| {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
Ok(pool.free(in_schema)?)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSchemaSetIdentifier(
|
||||
in_schema: in_param_ty!(WFToken),
|
||||
in_value: in_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
let schema = pool.get_mut(in_schema)?;
|
||||
schema.set_identifier(cstr_ffi::parse_ffi_string(in_value)?);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSchemaSetPath(
|
||||
in_schema: in_param_ty!(WFToken),
|
||||
in_value: in_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
let schema = pool.get_mut(in_schema)?;
|
||||
schema.set_path(cstr_ffi::parse_ffi_string(in_value)?);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSchemaSetClsid(
|
||||
in_schema: in_param_ty!(WFToken),
|
||||
in_value: in_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
let schema = pool.get_mut(in_schema)?;
|
||||
schema.set_clsid(cstr_ffi::parse_ffi_string(in_value)?);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSchemaSetName(
|
||||
in_schema: in_param_ty!(WFToken),
|
||||
in_value: in_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
let schema = pool.get_mut(in_schema)?;
|
||||
|
||||
let name = if in_value.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(cstr_ffi::parse_ffi_string(in_value)?)
|
||||
};
|
||||
schema.set_name(name);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSchemaSetIcon(
|
||||
in_schema: in_param_ty!(WFToken),
|
||||
in_value: in_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
let schema = pool.get_mut(in_schema)?;
|
||||
|
||||
let icon = if in_value.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(cstr_ffi::parse_ffi_string(in_value)?)
|
||||
};
|
||||
schema.set_icon(icon);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSchemaSetBehavior(
|
||||
in_schema: in_param_ty!(WFToken),
|
||||
in_value: in_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
let schema = pool.get_mut(in_schema)?;
|
||||
|
||||
let behavior = if in_value.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(cstr_ffi::parse_ffi_string(in_value)?)
|
||||
};
|
||||
schema.set_behavior(behavior);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSchemaAddStr(
|
||||
in_schema: in_param_ty!(WFToken),
|
||||
in_name: in_param_ty!(WFCString),
|
||||
in_value: in_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(
|
||||
|in_schema: WFToken, in_name: WFCString, in_value: WFCString| {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
let schema = pool.get_mut(in_schema)?;
|
||||
schema.add_str(
|
||||
cstr_ffi::parse_ffi_string(in_name)?,
|
||||
cstr_ffi::parse_ffi_string(in_value)?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSchemaAddIcon(
|
||||
in_schema: in_param_ty!(WFToken),
|
||||
in_name: in_param_ty!(WFCString),
|
||||
in_value: in_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(
|
||||
|in_schema: WFToken, in_name: WFCString, in_value: WFCString| {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
let schema = pool.get_mut(in_schema)?;
|
||||
schema.add_icon(
|
||||
cstr_ffi::parse_ffi_string(in_name)?,
|
||||
cstr_ffi::parse_ffi_string(in_value)?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSchemaAddBehavior(
|
||||
in_schema: in_param_ty!(WFToken),
|
||||
in_name: in_param_ty!(WFCString),
|
||||
in_value: in_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(
|
||||
|in_schema: WFToken, in_name: WFCString, in_value: WFCString| {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
let schema = pool.get_mut(in_schema)?;
|
||||
schema.add_behavior(
|
||||
cstr_ffi::parse_ffi_string(in_name)?,
|
||||
cstr_ffi::parse_ffi_string(in_value)?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSchemaAddExt(
|
||||
in_schema: in_param_ty!(WFToken),
|
||||
in_ext: in_param_ty!(WFCString),
|
||||
in_ext_name: in_param_ty!(WFCString),
|
||||
in_ext_icon: in_param_ty!(WFCString),
|
||||
in_ext_behavior: in_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_schema: WFToken,
|
||||
in_ext: WFCString,
|
||||
in_ext_name: WFCString,
|
||||
in_ext_icon: WFCString,
|
||||
in_ext_behavior: WFCString| {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
let schema = pool.get_mut(in_schema)?;
|
||||
schema.add_ext(
|
||||
cstr_ffi::parse_ffi_string(in_ext)?,
|
||||
cstr_ffi::parse_ffi_string(in_ext_name)?,
|
||||
cstr_ffi::parse_ffi_string(in_ext_icon)?,
|
||||
cstr_ffi::parse_ffi_string(in_ext_behavior)?,
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Program
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFProgramCreate(
|
||||
in_schema: in_param_ty!(WFToken),
|
||||
out_program: out_param_ty!(WFToken),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_schema: WFToken| -> (out_program: WFToken) {
|
||||
let mut pool = pull_writer!(SCHEMA_POOL)?;
|
||||
let schema = pool.pop(in_schema)?;
|
||||
|
||||
let mut pool = pull_writer!(PROGRAM_POOL)?;
|
||||
let program = Program::new(schema)?;
|
||||
Ok(pool.allocate(program)?)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFProgramDestroy(in_program: in_param_ty!(WFToken)) -> bool {
|
||||
cffi_wrapper!(|in_program: WFToken| {
|
||||
let mut pool = pull_writer!(PROGRAM_POOL)?;
|
||||
Ok(pool.free(in_program)?)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFProgramResolveName(
|
||||
in_program: in_param_ty!(WFToken),
|
||||
out_name: out_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_program: WFToken| -> (out_name: WFCString) {
|
||||
let mut pool = pull_writer!(PROGRAM_POOL)?;
|
||||
let program = pool.get_mut(in_program)?;
|
||||
|
||||
let name = program.resolve_name()?;
|
||||
cstr_ffi::set_ffi_string(&name)?;
|
||||
Ok(cstr_ffi::get_ffi_string())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFProgramResolveIcon(
|
||||
in_program: in_param_ty!(WFToken),
|
||||
out_icon_rc: out_param_ty!(WFToken),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_program: WFToken| -> (out_icon_rc: WFToken) {
|
||||
let mut pool = pull_writer!(PROGRAM_POOL)?;
|
||||
let program = pool.get_mut(in_program)?;
|
||||
|
||||
let icon = program.resolve_icon()?;
|
||||
let mut pool = pull_writer!(ICON_RC_POOL)?;
|
||||
Ok(pool.allocate(icon)?)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFProgramExtsLen(
|
||||
in_program: in_param_ty!(WFToken),
|
||||
out_len: out_param_ty!(usize),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_program: WFToken| -> (out_len: usize) {
|
||||
let mut pool = pull_writer!(PROGRAM_POOL)?;
|
||||
let program = pool.get_mut(in_program)?;
|
||||
Ok(program.exts_len())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFProgramFindExt(
|
||||
in_program: in_param_ty!(WFToken),
|
||||
in_body: in_param_ty!(WFCString),
|
||||
out_index: out_param_ty!(usize),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_program: WFToken, in_body: WFCString| -> (out_index: usize) {
|
||||
let mut pool = pull_writer!(PROGRAM_POOL)?;
|
||||
let program = pool.get_mut(in_program)?;
|
||||
|
||||
let body = cstr_ffi::parse_ffi_string(in_body)?;
|
||||
let index = match program.find_ext(body) {
|
||||
Some(index) => index,
|
||||
None => WF_INVALID_INDEX,
|
||||
};
|
||||
Ok(index)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFProgramResolveExt(
|
||||
in_program: in_param_ty!(WFToken),
|
||||
in_index: in_param_ty!(usize),
|
||||
out_self_ext_status: out_param_ty!(WFToken),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_program: WFToken, in_index: usize| -> (out_self_ext_status: WFToken) {
|
||||
let mut pool = pull_writer!(PROGRAM_POOL)?;
|
||||
let program = pool.get_mut(in_program)?;
|
||||
|
||||
let self_ext_status = program.resolve_ext(in_index)?;
|
||||
let mut pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
|
||||
let token = pool.allocate(self_ext_status)?;
|
||||
Ok(token)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFProgramRegister(
|
||||
in_program: in_param_ty!(WFToken),
|
||||
in_scope: in_param_ty!(u32),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_program: WFToken, in_scope: u32| {
|
||||
let mut pool = pull_writer!(PROGRAM_POOL)?;
|
||||
let program = pool.get_mut(in_program)?;
|
||||
let scope = resolve_enum!(WFScope, in_scope)?;
|
||||
program.register(scope.into())?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFProgramUnregister(
|
||||
in_program: in_param_ty!(WFToken),
|
||||
in_scope: in_param_ty!(u32),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_program: WFToken, in_scope: u32| {
|
||||
let mut pool = pull_writer!(PROGRAM_POOL)?;
|
||||
let program = pool.get_mut(in_program)?;
|
||||
let scope = resolve_enum!(WFScope, in_scope)?;
|
||||
program.unregister(scope.into())?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFProgramIsRegistered(
|
||||
in_program: in_param_ty!(WFToken),
|
||||
in_scope: in_param_ty!(u32),
|
||||
out_is_registered: out_param_ty!(bool),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_program: WFToken, in_scope: u32| -> (out_is_registered: bool) {
|
||||
let pool = pull_reader!(PROGRAM_POOL)?;
|
||||
let program = pool.get(in_program)?;
|
||||
let scope = resolve_enum!(WFScope, in_scope)?;
|
||||
Ok(program.is_registered(scope.into())?)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFProgramLinkExt(
|
||||
in_program: in_param_ty!(WFToken),
|
||||
in_scope: in_param_ty!(u32),
|
||||
in_index: in_param_ty!(usize),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_program: WFToken, in_scope: u32, in_index: usize| {
|
||||
let mut pool = pull_writer!(PROGRAM_POOL)?;
|
||||
let program = pool.get_mut(in_program)?;
|
||||
let scope = resolve_enum!(WFScope, in_scope)?;
|
||||
program.link_ext(scope.into(), in_index)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFProgramUnlinkExt(
|
||||
in_program: in_param_ty!(WFToken),
|
||||
in_scope: in_param_ty!(u32),
|
||||
in_index: in_param_ty!(usize),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_program: WFToken, in_scope: u32, in_index: usize| {
|
||||
let mut pool = pull_writer!(PROGRAM_POOL)?;
|
||||
let program = pool.get_mut(in_program)?;
|
||||
let scope = resolve_enum!(WFScope, in_scope)?;
|
||||
program.unlink_ext(scope.into(), in_index)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFProgramQueryExt(
|
||||
in_program: in_param_ty!(WFToken),
|
||||
in_view: in_param_ty!(u32),
|
||||
in_index: in_param_ty!(usize),
|
||||
out_ext_status: out_param_ty!(WFToken),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_program: WFToken, in_view: u32, in_index: usize| -> (out_ext_status: WFToken) {
|
||||
let pool = pull_reader!(PROGRAM_POOL)?;
|
||||
let program = pool.get(in_program)?;
|
||||
let view = resolve_enum!(WFView, in_view)?;
|
||||
|
||||
let ext_status = program.query_ext(view.into(), in_index)?;
|
||||
let token = match ext_status {
|
||||
Some(ext_status) => {
|
||||
let mut pool = pull_writer!(EXT_STATUS_POOL)?;
|
||||
pool.allocate(ext_status)?
|
||||
},
|
||||
None => object_pool::invalid_token(),
|
||||
};
|
||||
Ok(token)
|
||||
})
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Extension Status
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFExtStatusDestroy(in_ext_status: in_param_ty!(WFToken)) -> bool {
|
||||
cffi_wrapper!(|in_ext_status: WFToken| {
|
||||
let mut pool = pull_writer!(EXT_STATUS_POOL)?;
|
||||
Ok(pool.free(in_ext_status)?)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFExtStatusGetName(
|
||||
in_ext_status: in_param_ty!(WFToken),
|
||||
out_name: out_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_ext_status: WFToken| -> (out_name: WFCString) {
|
||||
let pool = pull_reader!(EXT_STATUS_POOL)?;
|
||||
let ext_status = pool.get(in_ext_status)?;
|
||||
|
||||
cstr_ffi::set_ffi_string(ext_status.get_name())?;
|
||||
Ok(cstr_ffi::get_ffi_string())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFExtStatusGetIcon(
|
||||
in_ext_status: in_param_ty!(WFToken),
|
||||
out_icon: out_param_ty!(WFHICON),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_ext_status: WFToken| -> (out_icon: WFHICON) {
|
||||
let pool = pull_reader!(EXT_STATUS_POOL)?;
|
||||
let ext_status = pool.get(in_ext_status)?;
|
||||
|
||||
let icon = ext_status.get_icon();
|
||||
Ok(icon.get_icon())
|
||||
})
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Self Extension Status
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSelfExtStatusDestroy(in_self_ext_status: in_param_ty!(WFToken)) -> bool {
|
||||
cffi_wrapper!(|in_self_ext_status: WFToken| {
|
||||
let mut pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
|
||||
Ok(pool.free(in_self_ext_status)?)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSelfExtStatusGetName(
|
||||
in_self_ext_status: in_param_ty!(WFToken),
|
||||
out_name: out_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_self_ext_status: WFToken| -> (out_name: WFCString) {
|
||||
let pool = pull_reader!(SELF_EXT_STATUS_POOL)?;
|
||||
let self_ext_status = pool.get(in_self_ext_status)?;
|
||||
|
||||
cstr_ffi::set_ffi_string(self_ext_status.get_name())?;
|
||||
Ok(cstr_ffi::get_ffi_string())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSelfExtStatusGetIcon(
|
||||
in_self_ext_status: in_param_ty!(WFToken),
|
||||
out_icon: out_param_ty!(WFHICON),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_self_ext_status: WFToken| -> (out_icon: WFHICON) {
|
||||
let pool = pull_reader!(SELF_EXT_STATUS_POOL)?;
|
||||
let self_ext_status = pool.get(in_self_ext_status)?;
|
||||
|
||||
let icon = self_ext_status.get_icon();
|
||||
Ok(icon.get_icon())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSelfExtStatusGetExt(
|
||||
in_self_ext_status: in_param_ty!(WFToken),
|
||||
out_inner: out_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_self_ext_status: WFToken| -> (out_inner: WFCString) {
|
||||
let pool = pull_reader!(SELF_EXT_STATUS_POOL)?;
|
||||
let self_ext_status = pool.get(in_self_ext_status)?;
|
||||
|
||||
cstr_ffi::set_ffi_string(self_ext_status.get_ext())?;
|
||||
Ok(cstr_ffi::get_ffi_string())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFSelfExtStatusGetDottedExt(
|
||||
in_self_ext_status: in_param_ty!(WFToken),
|
||||
out_inner: out_param_ty!(WFCString),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_self_ext_status: WFToken| -> (out_inner: WFCString) {
|
||||
let pool = pull_reader!(SELF_EXT_STATUS_POOL)?;
|
||||
let self_ext_status = pool.get(in_self_ext_status)?;
|
||||
|
||||
cstr_ffi::set_ffi_string(self_ext_status.get_dotted_ext().as_str())?;
|
||||
Ok(cstr_ffi::get_ffi_string())
|
||||
})
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Icon Resource
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFIconRcDestroy(in_icon_rc: in_param_ty!(WFToken)) -> bool {
|
||||
cffi_wrapper!(|in_icon_rc: WFToken| {
|
||||
let mut pool = pull_writer!(ICON_RC_POOL)?;
|
||||
Ok(pool.free(in_icon_rc)?)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn WFIconRcGetIcon(
|
||||
in_icon_rc: in_param_ty!(WFToken),
|
||||
out_icon: out_param_ty!(WFHICON),
|
||||
) -> bool {
|
||||
cffi_wrapper!(|in_icon_rc: WFToken| -> (out_icon: WFHICON) {
|
||||
let pool = pull_reader!(ICON_RC_POOL)?;
|
||||
let icon_rc = pool.get(in_icon_rc)?;
|
||||
|
||||
Ok(icon_rc.get_icon())
|
||||
})
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
//! When exporting resources for C interface, resource management and ownership are important things.
|
||||
//! In this dynamic library, we hold all resources' ownership in Rust world,
|
||||
//! and only expose a token for C code manipulation.
|
||||
//!
|
||||
//! We need to create a container for holding all resources and providing corresponding operations.
|
||||
//! So we introduce [ObjectPool] in this module for this purpose.
|
||||
use slotmap::{DefaultKey, Key, KeyData, SlotMap};
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
/// Error occurs when operating with [ObjectPool].
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum Error {
|
||||
#[error("given token is not presented in object pool")]
|
||||
NoSuchToken,
|
||||
}
|
||||
|
||||
/// The token for fetching object in [ObjectPool].
|
||||
pub type Token = u64;
|
||||
|
||||
/// Get the invalid token.
|
||||
///
|
||||
/// Invalid token is always invalid for fetching object in pool,
|
||||
/// And can be useful in FFI scenario.
|
||||
pub fn invalid_token() -> Token {
|
||||
DefaultKey::null().data().as_ffi()
|
||||
}
|
||||
|
||||
/// A pool for managing objects with unique tokens.
|
||||
///
|
||||
/// It is highly suggested to use this pool with [std::sync::RwLock] guard.
|
||||
pub struct ObjectPool<T> {
|
||||
objs: SlotMap<DefaultKey, T>,
|
||||
}
|
||||
|
||||
impl<T> ObjectPool<T> {
|
||||
/// Create a new [ObjectPool].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
objs: SlotMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert [slotmap] crate's [DefaultKey] to our [Token].
|
||||
fn key_to_token(key: &DefaultKey) -> Token {
|
||||
key.data().as_ffi()
|
||||
}
|
||||
|
||||
/// Convert our [Token] to [slotmap] crate's [DefaultKey].
|
||||
fn token_to_key(token: Token) -> DefaultKey {
|
||||
DefaultKey::from(KeyData::from_ffi(token))
|
||||
}
|
||||
|
||||
/// Put given object into the pool and return its token.
|
||||
///
|
||||
/// The ownership of given object is transferred to the pool.
|
||||
pub fn allocate(&mut self, value: T) -> Result<Token, Error> {
|
||||
let key = self.objs.insert(value);
|
||||
Ok(Self::key_to_token(&key))
|
||||
}
|
||||
|
||||
/// Free the object in the pool corresponding to the given token.
|
||||
pub fn free(&mut self, token: Token) -> Result<(), Error> {
|
||||
let _ = self.pop(token)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the object from the pool corresponding to the given token and return it.
|
||||
///
|
||||
/// The ownership of the object is transferred to the caller.
|
||||
pub fn pop(&mut self, token: Token) -> Result<T, Error> {
|
||||
match self.objs.remove(Self::token_to_key(token)) {
|
||||
Some(obj) => Ok(obj),
|
||||
None => Err(Error::NoSuchToken),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all objects in the pool.
|
||||
pub fn clear(&mut self) -> () {
|
||||
self.objs.clear();
|
||||
}
|
||||
|
||||
/// Get a reference to the object in the pool corresponding to the given token.
|
||||
pub fn get(&self, token: Token) -> Result<&T, Error> {
|
||||
self.objs
|
||||
.get(Self::token_to_key(token))
|
||||
.ok_or(Error::NoSuchToken)
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the object in the pool corresponding to the given token.
|
||||
pub fn get_mut(&mut self, token: Token) -> Result<&mut T, Error> {
|
||||
self.objs
|
||||
.get_mut(Self::token_to_key(token))
|
||||
.ok_or(Error::NoSuchToken)
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
//! When calling this dynamic library with outside programs,
|
||||
//! outer programs may usually need to fetch string resource produced by Rust code.
|
||||
//! However it is impossible pass Rust string directly to outer program.
|
||||
//!
|
||||
//! This module provide **thread independent** string cache for resolving this issue.
|
||||
//! When we need pass string to outer programs, we push that string into this string as C-like format,
|
||||
//! then return its pointer to outer program.
|
||||
//! So that outside program can utilize it like calling C/C++ library.
|
||||
//! The only thing that outer programs should note is that this string is volatile,
|
||||
//! once they get it, they must dupliate it immediately before any futher calling to this dynamic library.
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{CString, c_char};
|
||||
|
||||
struct StringCache {
|
||||
msg: CString,
|
||||
}
|
||||
|
||||
impl StringCache {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
msg: CString::new("").expect("empty string must be valid for CString"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_msg(&mut self, msg: &str) {
|
||||
self.msg = CString::new(msg).expect("unexpected blank in string push into string cache");
|
||||
}
|
||||
|
||||
pub fn get_msg(&self) -> *const c_char {
|
||||
self.msg.as_ptr()
|
||||
}
|
||||
|
||||
pub fn clear_msg(&mut self) {
|
||||
self.msg = CString::new("").expect("empty string must be valid for CString");
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static STRING_CACHE: RefCell<StringCache> = RefCell::new(StringCache::new());
|
||||
}
|
||||
|
||||
/// Set thread local string cache.
|
||||
pub fn set_string_cache(msg: &str) {
|
||||
STRING_CACHE.with(|e| {
|
||||
e.borrow_mut().set_msg(msg);
|
||||
});
|
||||
}
|
||||
|
||||
/// Get const pointer to thread local string cache for outside program visiting.
|
||||
pub fn get_string_cache() -> *const c_char {
|
||||
STRING_CACHE.with(|e| e.borrow().get_msg())
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
/// Error occurs in this module.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum Error {}
|
||||
|
||||
/// Result type used in this module.
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Schema is the sketchpad of complete Program.
|
||||
///
|
||||
/// We will create a Schema first, fill some properties, add file extensions,
|
||||
/// then convert it into immutable Program for following using.
|
||||
#[derive(Debug)]
|
||||
pub struct Schema {
|
||||
identifier: String,
|
||||
path: String,
|
||||
clsid: String,
|
||||
icons: HashMap<String, String>,
|
||||
behaviors: HashMap<String, String>,
|
||||
exts: HashMap<String, SchemaExt>,
|
||||
}
|
||||
|
||||
/// Internal used struct as the Schema file extensions hashmap value type.
|
||||
#[derive(Debug)]
|
||||
struct SchemaExt {
|
||||
name: String,
|
||||
icon: String,
|
||||
behavior: String,
|
||||
}
|
||||
|
||||
impl Schema {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
identifier: String::new(),
|
||||
path: String::new(),
|
||||
clsid: String::new(),
|
||||
icons: HashMap::new(),
|
||||
behaviors: HashMap::new(),
|
||||
exts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_identifier(&mut self, identifier: &str) -> Result<()> {}
|
||||
|
||||
pub fn set_path(&mut self, exe_path: &str) -> Result<()> {}
|
||||
|
||||
pub fn set_clsid(&mut self, clsid: &str) -> Result<()> {}
|
||||
|
||||
pub fn add_icon(&mut self, name: &str, value: &str) -> Result<()> {}
|
||||
|
||||
pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<()> {}
|
||||
|
||||
pub fn add_ext(
|
||||
&mut self,
|
||||
ext: &str,
|
||||
ext_name: &str,
|
||||
ext_icon: &str,
|
||||
ext_behavior: &str,
|
||||
) -> Result<()> {
|
||||
}
|
||||
|
||||
pub fn into_program(self) -> Result<Program> {
|
||||
Program::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Program is a complete and immutable program representer
|
||||
pub struct Program {}
|
||||
|
||||
impl TryFrom<Schema> for Program {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Schema) -> std::result::Result<Self, Self::Error> {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
pub fn new(schema: Schema) -> Result<Self> {}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# Wfassoc Executable
|
||||
# WFAssoc Executable
|
||||
|
||||
For the syntax of this executable's parameters, execute it with `--help` for more.
|
||||
|
||||
|
||||
@@ -83,8 +83,8 @@ pub enum CliCommands {
|
||||
#[command(about = "Fetch the status of registration with given manifest and scope.")]
|
||||
Status {
|
||||
/// The view where fetch info.
|
||||
#[arg(short = 't', long = "target", value_name = "TARGET", required = true, value_enum, default_value_t = RegView::User)]
|
||||
target: RegView,
|
||||
#[arg(short = 't', long = "target", value_name = "TARGET", required = true, value_enum, default_value_t = RegScope::User)]
|
||||
target: RegScope,
|
||||
},
|
||||
#[command(name = "ext")]
|
||||
#[command(about = "File extension related operations according to given program manifest.")]
|
||||
@@ -114,7 +114,7 @@ pub enum CliExtCommands {
|
||||
/// The scope where unlink file extension.
|
||||
#[arg(short = 't', long = "target", value_name = "TARGET", required = true, value_enum, default_value_t = RegScope::User)]
|
||||
target: RegScope,
|
||||
// The file extensions used for this operation. Specify * for all file extension.
|
||||
// The file extensions (without leading dot) used for this operation. Specify * for all file extension.
|
||||
#[arg(required = true, value_name = "EXTS", num_args = 1..)]
|
||||
exts: Vec<String>,
|
||||
},
|
||||
|
||||
@@ -63,7 +63,7 @@ impl Manifest {
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum ParseSchemaError {
|
||||
/// Error when operating with schema.
|
||||
#[error("{0}")]
|
||||
#[error("failed on parsing manifest file to Schema: {0}")]
|
||||
Schema(#[from] wfassoc::highlevel::SchemaError)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
pub(crate) mod cli;
|
||||
pub(crate) mod manifest;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Commands, Target};
|
||||
use comfy_table::Table;
|
||||
use manifest::Manifest;
|
||||
use std::collections::HashMap;
|
||||
use std::process;
|
||||
use thiserror::Error as TeError;
|
||||
use wfassoc::{Program, Scope, Token, View};
|
||||
|
||||
// region: Basic Types
|
||||
|
||||
/// All errors occurs in this executable.
|
||||
#[derive(TeError, Debug)]
|
||||
enum Error {
|
||||
/// Error from wfassoc core.
|
||||
#[error("{0}")]
|
||||
Core(#[from] wfassoc::Error),
|
||||
/// Error when parsing manifest TOML file.
|
||||
#[error("invalid manifest file: {0}")]
|
||||
Manifest(#[from] manifest::Error),
|
||||
|
||||
/// Error when specifying invalid manner name for extension.
|
||||
#[error("extension {ext} associated manner {manner} is invalid in manifest file")]
|
||||
InvalidMannerName { manner: String, ext: String },
|
||||
}
|
||||
|
||||
/// Result type used in this executable.
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Correponding Runner
|
||||
|
||||
struct Composition {
|
||||
program: Program,
|
||||
ext_tokens: Vec<Token>,
|
||||
}
|
||||
|
||||
impl Composition {
|
||||
pub fn new(cli: &Cli) -> Result<Composition> {
|
||||
// Open file and read manifest TOML file
|
||||
let mf = Manifest::from_file(&cli.config_file)?;
|
||||
// Create instance
|
||||
let mut program = Program::new(&mf.identifier, &cli.config_file)?;
|
||||
// Setup manner
|
||||
let mut manner_token_map: HashMap<&str, Token> = HashMap::new();
|
||||
for (k, v) in mf.manners.iter() {
|
||||
let token = program.add_manner(v.as_str())?;
|
||||
manner_token_map.insert(k.as_str(), token);
|
||||
}
|
||||
// Setup extension
|
||||
let mut ext_tokens = Vec::new();
|
||||
for (k, v) in mf.exts.iter() {
|
||||
let token = match manner_token_map.get(v.as_str()) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err(Error::InvalidMannerName {
|
||||
manner: v.to_string(),
|
||||
ext: k.clone(),
|
||||
});
|
||||
}
|
||||
};
|
||||
let token = program.add_ext(k.as_str(), *token)?;
|
||||
ext_tokens.push(token);
|
||||
}
|
||||
// Okey
|
||||
Ok(Self {
|
||||
program,
|
||||
ext_tokens,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_program(&self) -> &Program {
|
||||
&self.program
|
||||
}
|
||||
|
||||
pub fn iter_ext_tokens(&self) -> impl Iterator<Item = Token> {
|
||||
self.ext_tokens.iter().copied()
|
||||
}
|
||||
}
|
||||
|
||||
fn run_register(cli: &Cli, target: &Target) -> Result<()> {
|
||||
let composition = Composition::new(cli)?;
|
||||
let scope = target.clone().into();
|
||||
|
||||
// Register program
|
||||
let program = composition.get_program();
|
||||
program.register(scope)?;
|
||||
// Link all file extensions
|
||||
for token in composition.iter_ext_tokens() {
|
||||
program.link_ext(token, scope)?;
|
||||
}
|
||||
|
||||
println!("Register OK.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_unregister(cli: &Cli, target: &Target) -> Result<()> {
|
||||
let composition = Composition::new(cli)?;
|
||||
let scope = target.clone().into();
|
||||
|
||||
// Unlink all file extensions
|
||||
let program = composition.get_program();
|
||||
for token in composition.iter_ext_tokens() {
|
||||
program.unlink_ext(token, scope)?;
|
||||
}
|
||||
// Unregister prorgam
|
||||
program.unregister(scope)?;
|
||||
|
||||
println!("Unregister OK.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_query(cli: &Cli) -> Result<()> {
|
||||
let composition = Composition::new(cli)?;
|
||||
|
||||
// Show file association
|
||||
let mut table = Table::new();
|
||||
table.set_header(["Extension", "Hybrid", "User", "System"]);
|
||||
|
||||
let program = composition.get_program();
|
||||
for token in composition.iter_ext_tokens() {
|
||||
let cell_ext = program.get_ext_str(token).unwrap();
|
||||
let cell_hybrid = program
|
||||
.query_ext(token, View::Hybrid)?
|
||||
.map(|pi| pi.to_string())
|
||||
.unwrap_or_default();
|
||||
let cell_user = program
|
||||
.query_ext(token, View::User)?
|
||||
.map(|pi| pi.to_string())
|
||||
.unwrap_or_default();
|
||||
let cell_system = program
|
||||
.query_ext(token, View::System)?
|
||||
.map(|pi| pi.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
table.add_row([cell_ext, cell_hybrid, cell_user, cell_system]);
|
||||
}
|
||||
|
||||
println!("{table}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let rv = match &cli.command {
|
||||
Commands::Register { target } => run_register(&cli, target),
|
||||
Commands::Unregister { target } => run_unregister(&cli, target),
|
||||
Commands::Query => run_query(&cli),
|
||||
};
|
||||
rv.unwrap_or_else(|e| {
|
||||
eprintln!("Runtime error: {}.", e);
|
||||
process::exit(1)
|
||||
});
|
||||
}
|
||||
+107
-24
@@ -1,7 +1,10 @@
|
||||
use crate::cli;
|
||||
use crate::manifest;
|
||||
use comfy_table::Table;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
use thiserror::Error as TeError;
|
||||
use toml;
|
||||
|
||||
// region: Error Handling
|
||||
|
||||
@@ -9,17 +12,31 @@ use thiserror::Error as TeError;
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum Error {
|
||||
/// Error when parsing Manifest TOML file.
|
||||
#[error("{0}")]
|
||||
#[error("initialization error: {0}")]
|
||||
ParseManifest(#[from] manifest::ParseManifestError),
|
||||
/// Error when parsing Manifest into Schema.
|
||||
#[error("{0}")]
|
||||
#[error("initialization error: {0}")]
|
||||
ParseSchema(#[from] manifest::ParseSchemaError),
|
||||
/// Error when serializing TOML
|
||||
#[error("initialization error: {0}")]
|
||||
SerializeToml(#[from] toml::ser::Error),
|
||||
|
||||
/// Error when parsing Schema into Program.
|
||||
#[error("{0}")]
|
||||
#[error("wfassoc error: {0}")]
|
||||
ParseProgram(#[from] wfassoc::highlevel::ParseProgramError),
|
||||
/// Error when operating Program.
|
||||
#[error("{0}")]
|
||||
#[error("wfassoc error: {0}")]
|
||||
Program(#[from] wfassoc::highlevel::ProgramError),
|
||||
|
||||
/// Find duplicated name when converting extension name to index
|
||||
#[error("given extension name {0} has been specified more than one time")]
|
||||
DupExtName(String),
|
||||
/// Find invalid name when converting extension name to index
|
||||
#[error("given extension name {0} is not presented in application")]
|
||||
BadExtName(String),
|
||||
/// Find star (*) extension name with other extension names when converting extension name to index
|
||||
#[error(r#"wildcard extension name "*" is not allowed to be used with other extension names"#)]
|
||||
ExclusiveStarExtName,
|
||||
}
|
||||
|
||||
/// Result type used in this module.
|
||||
@@ -27,10 +44,38 @@ type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Utilities Functions
|
||||
// region: Utilities
|
||||
|
||||
fn stringified_exts_to_indices(program: &wfassoc::Program, exts: Vec<String>) -> Result<Vec<usize>> {
|
||||
todo!()
|
||||
fn stringified_exts_to_indices(
|
||||
program: &wfassoc::Program,
|
||||
exts: Vec<String>,
|
||||
) -> Result<Vec<usize>> {
|
||||
// Check for duplicate extension names
|
||||
let mut seen = HashSet::new();
|
||||
for ext in &exts {
|
||||
if !seen.insert(ext.as_str()) {
|
||||
return Err(Error::DupExtName(ext.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for star (*) with other extensions
|
||||
let has_star = exts.iter().any(|ext| ext == "*");
|
||||
if has_star && exts.len() > 1 {
|
||||
return Err(Error::ExclusiveStarExtName);
|
||||
}
|
||||
|
||||
// If star is present alone, return fixed list from zero to the maximum ext index.
|
||||
if has_star {
|
||||
return Ok((0..program.exts_len()).collect());
|
||||
}
|
||||
|
||||
// Convert each extension name to index using program.find_ext()
|
||||
let indices = exts
|
||||
.into_iter()
|
||||
.map(|ext| program.find_ext(&ext).ok_or(Error::BadExtName(ext)))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
Ok(indices)
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -38,33 +83,29 @@ fn stringified_exts_to_indices(program: &wfassoc::Program, exts: Vec<String>) ->
|
||||
// region: Respective Runners
|
||||
|
||||
fn run_register(mut program: wfassoc::Program, scope: wfassoc::Scope) -> Result<()> {
|
||||
Ok(program.register(scope)?)
|
||||
program.register(scope)?;
|
||||
println!("Application now is installed.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_unregister(mut program: wfassoc::Program, scope: wfassoc::Scope) -> Result<()> {
|
||||
Ok(program.unregister(scope)?)
|
||||
program.unregister(scope)?;
|
||||
println!("Application now is uninstalled.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_status(program: wfassoc::Program, view: wfassoc::View) -> Result<()> {
|
||||
if program.is_registered(view)? {
|
||||
fn run_status(program: wfassoc::Program, scope: wfassoc::Scope) -> Result<()> {
|
||||
if program.is_registered(scope)? {
|
||||
println!("Application is installed.");
|
||||
} else {
|
||||
println!("Application is not installed.");
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_ext_link(program: wfassoc::Program, scope: wfassoc::Scope, exts: Vec<String>) -> Result<()> {
|
||||
let exts = stringified_exts_to_indices(&program, exts)?;
|
||||
for index in exts {
|
||||
program.link_ext(scope, index)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_ext_unlink(
|
||||
program: wfassoc::Program,
|
||||
fn run_ext_link(
|
||||
mut program: wfassoc::Program,
|
||||
scope: wfassoc::Scope,
|
||||
exts: Vec<String>,
|
||||
) -> Result<()> {
|
||||
@@ -72,6 +113,22 @@ fn run_ext_unlink(
|
||||
for index in exts {
|
||||
program.link_ext(scope, index)?;
|
||||
}
|
||||
|
||||
println!("File extension now is linked.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_ext_unlink(
|
||||
mut program: wfassoc::Program,
|
||||
scope: wfassoc::Scope,
|
||||
exts: Vec<String>,
|
||||
) -> Result<()> {
|
||||
let exts = stringified_exts_to_indices(&program, exts)?;
|
||||
for index in exts {
|
||||
program.unlink_ext(scope, index)?;
|
||||
}
|
||||
|
||||
println!("File extension now is unlinked.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -80,7 +137,34 @@ fn run_ext_list(
|
||||
view: wfassoc::View,
|
||||
style: cli::ExtListStyle,
|
||||
) -> Result<()> {
|
||||
todo!()
|
||||
// Fetch info
|
||||
let mut ext_list: HashMap<String, Option<String>> = HashMap::new();
|
||||
for index in 0..program.exts_len() {
|
||||
let self_ext_status = program.resolve_ext(index)?;
|
||||
let status = program.query_ext(view, index)?;
|
||||
ext_list.insert(
|
||||
self_ext_status.get_dotted_ext(),
|
||||
status.map(|s| s.get_name().to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
// Output by styles
|
||||
use cli::ExtListStyle;
|
||||
match style {
|
||||
ExtListStyle::Human => {
|
||||
let mut table = Table::new();
|
||||
table.set_header(["Extension", "Association"]);
|
||||
for (k, v) in ext_list {
|
||||
table.add_row([k, v.unwrap_or("".to_string())]);
|
||||
}
|
||||
println!("{table}");
|
||||
}
|
||||
ExtListStyle::Machine => {
|
||||
let stoml = toml::to_string(&ext_list)?;
|
||||
println!("{stoml}")
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -88,7 +172,6 @@ fn run_ext_list(
|
||||
pub fn run(c: cli::Cli) -> Result<()> {
|
||||
// Read manifest file first
|
||||
let mf = manifest::Manifest::from_file(Path::new(&c.manifest_file))?;
|
||||
println!("{:?}", mf);
|
||||
// Parse it into schema
|
||||
let schema = mf.into_schema()?;
|
||||
// Parse it into program
|
||||
|
||||
@@ -22,3 +22,6 @@ widestring = "1.2.1"
|
||||
indexmap = "2.11.4"
|
||||
regex = "1.11.3"
|
||||
uuid = { version = "1.18.1", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
strfmt = "0.2.5"
|
||||
|
||||
+21
-1
@@ -1,4 +1,6 @@
|
||||
# WFassoc Core
|
||||
# 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.
|
||||
@@ -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 highly 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.
|
||||
|
||||
@@ -1,391 +0,0 @@
|
||||
//! The module including all struct representing Windows file association concept,
|
||||
//! like file extension, ProgId, CLSID and etc.
|
||||
|
||||
use crate::extra::windows::{Ext, ProgId};
|
||||
use crate::extra::winreg as winreg_extra;
|
||||
use crate::utilities;
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error as TeError;
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::{
|
||||
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
|
||||
};
|
||||
|
||||
// region: Error Types
|
||||
|
||||
/// All possible error occurs in this crate.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum Error {
|
||||
#[error("error occurs when manipulating with Registry: {0}")]
|
||||
BadRegOper(#[from] std::io::Error),
|
||||
#[error("{0}")]
|
||||
ParseExt(#[from] crate::extra::windows::ParseExtError),
|
||||
#[error("{0}")]
|
||||
ParseProgId(#[from] crate::extra::windows::ParseProgIdError),
|
||||
#[error("{0}")]
|
||||
ParseProgIdKind(#[from] ParseProgIdKindError),
|
||||
#[error("{0}")]
|
||||
BlankPath(#[from] crate::extra::winreg::BlankPathError),
|
||||
}
|
||||
|
||||
/// The result type used in this crate.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Data Types
|
||||
|
||||
/// The token for access registered items in Program.
|
||||
/// This is usually returned when you registering them.
|
||||
pub type Token = usize;
|
||||
|
||||
// region: Scope
|
||||
|
||||
/// The scope where wfassoc will register and unregister application.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Scope {
|
||||
/// Scope for current user.
|
||||
User,
|
||||
/// Scope for all users under this computer.
|
||||
System,
|
||||
}
|
||||
|
||||
/// The error occurs when cast View into Scope.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("hybrid View can not be cast into Scope")]
|
||||
pub struct TryFromViewError {}
|
||||
|
||||
impl TryFromViewError {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<View> for Scope {
|
||||
type Error = TryFromViewError;
|
||||
|
||||
fn try_from(value: View) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
View::User => Ok(Self::User),
|
||||
View::System => Ok(Self::System),
|
||||
View::Hybrid => Err(TryFromViewError::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
/// Check whether we have enough privilege when operating in current scope.
|
||||
/// If we have, return true, otherwise false.
|
||||
pub fn has_privilege(&self) -> bool {
|
||||
// If we operate on System, and we do not has privilege,
|
||||
// we think we do not have privilege, otherwise,
|
||||
// there is no privilege required.
|
||||
!matches!(self, Self::System if !utilities::has_privilege())
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: View
|
||||
|
||||
/// The view when wfassoc querying file extension association.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum View {
|
||||
/// The view of current user.
|
||||
User,
|
||||
/// The view of system.
|
||||
System,
|
||||
/// Hybrid view of User and System.
|
||||
/// It can be seen as that we use System first and then use User to override any existing items.
|
||||
Hybrid,
|
||||
}
|
||||
|
||||
impl From<Scope> for View {
|
||||
fn from(value: Scope) -> Self {
|
||||
match value {
|
||||
Scope::User => Self::User,
|
||||
Scope::System => Self::System,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: ProgId Kind
|
||||
|
||||
/// The error occurs when parsing ProgId kind.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum ParseProgIdKindError {
|
||||
#[error("{0}")]
|
||||
FromStd(#[from] crate::extra::windows::ParseProgIdError),
|
||||
#[error("given ProgId is blank")]
|
||||
BlankProgId,
|
||||
}
|
||||
|
||||
/// The variant of ProgId for the compatibility
|
||||
/// with those software which do not follow Microsoft suggestions.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ProgIdKind {
|
||||
/// Other ProgId which not follow Microsoft standards.
|
||||
Other(String),
|
||||
/// Standard ProgId.
|
||||
Std(ProgId),
|
||||
}
|
||||
|
||||
impl Display for ProgIdKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ProgIdKind::Other(v) => write!(f, "{}", v),
|
||||
ProgIdKind::Std(prog_id) => write!(f, "{}", prog_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ProgIdKind {
|
||||
type Err = ParseProgIdKindError;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
if s.is_empty() {
|
||||
Err(ParseProgIdKindError::BlankProgId)
|
||||
} else {
|
||||
Ok(match s.parse::<ProgId>() {
|
||||
Ok(v) => Self::Std(v),
|
||||
Err(_) => Self::Other(s.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Registry Visitor
|
||||
|
||||
// region: Application Visitor
|
||||
|
||||
/// The static struct for visiting "Application" in registry
|
||||
pub struct ApplicationVisitor {}
|
||||
|
||||
impl ApplicationVisitor {
|
||||
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
|
||||
const APPLICATIONS: &str = "Software\\Classes\\Applications";
|
||||
|
||||
/// Open a readonly registry key to "App Paths" with given scope.
|
||||
pub fn open_app_paths(scope: Scope) -> Result<RegKey> {
|
||||
let hk = RegKey::predef(match scope {
|
||||
Scope::User => HKEY_CURRENT_USER,
|
||||
Scope::System => HKEY_LOCAL_MACHINE,
|
||||
});
|
||||
let app_paths = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
|
||||
|
||||
Ok(app_paths)
|
||||
}
|
||||
|
||||
/// Open a readonly registry key to "Applications" with given scope.
|
||||
pub fn open_applications(scope: Scope) -> Result<RegKey> {
|
||||
let hk = RegKey::predef(match scope {
|
||||
Scope::User => HKEY_CURRENT_USER,
|
||||
Scope::System => HKEY_LOCAL_MACHINE,
|
||||
});
|
||||
let applications = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
|
||||
|
||||
Ok(applications)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Classes Visitor
|
||||
|
||||
/// The static struct for visiting "Classes" in registry.
|
||||
pub struct ClassesVisitor {}
|
||||
|
||||
impl ClassesVisitor {
|
||||
const CLASSES: &str = "Software\\Classes";
|
||||
|
||||
/// Open a readonly registry key to "Classes" with given view.
|
||||
pub fn open_with_view(view: View) -> Result<RegKey> {
|
||||
// Fetch root key and navigate to Classes
|
||||
let hk = match view {
|
||||
View::User => RegKey::predef(HKEY_CURRENT_USER),
|
||||
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
||||
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
|
||||
};
|
||||
let classes = match view {
|
||||
View::User | View::System => hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?,
|
||||
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?,
|
||||
};
|
||||
|
||||
Ok(classes)
|
||||
}
|
||||
|
||||
/// Open a readonly registry key to "Classes" with given scope.
|
||||
pub fn open_with_scope(scope: Scope) -> Result<RegKey> {
|
||||
// Fetch root key and navigate to Classes
|
||||
let hk = RegKey::predef(match scope {
|
||||
Scope::User => HKEY_CURRENT_USER,
|
||||
Scope::System => HKEY_LOCAL_MACHINE,
|
||||
});
|
||||
let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
|
||||
|
||||
Ok(classes)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// endregion
|
||||
|
||||
// region: File Extension Registry Key
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ExtKey {
|
||||
ext: Ext,
|
||||
}
|
||||
|
||||
impl ExtKey {
|
||||
/// Create new file extension registry key representer.
|
||||
pub fn new(s: &str) -> Result<Self> {
|
||||
Ok(Self {
|
||||
ext: Ext::from_str(s)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch the reference to inner extension representer.
|
||||
pub fn as_inner(&self) -> &Ext {
|
||||
&self.ext
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtKey {
|
||||
/// Set the default "Open With" of this file extension to given ProgId.
|
||||
pub fn link(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
|
||||
use winreg_extra::blank_path_guard;
|
||||
|
||||
// Open Classes key
|
||||
let classes = ClassesVisitor::open_with_scope(scope)?;
|
||||
|
||||
// Open or create this extension key
|
||||
let (subkey, _) =
|
||||
classes.create_subkey_with_flags(blank_path_guard(self.ext.to_string())?, KEY_WRITE)?;
|
||||
// Set the default way to open this file extension
|
||||
subkey.set_value("", &prog_id.as_inner().to_string())?;
|
||||
|
||||
// Okey
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reset the default "Open With" of this file extension to blank.
|
||||
///
|
||||
/// If the default "Open With" of this file extension is not given ProgId,
|
||||
/// or there is no such file extension, this function do nothing.
|
||||
pub fn unlink(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
|
||||
use winreg_extra::{blank_path_guard, try_get_value, try_open_subkey_with_flags};
|
||||
|
||||
// Open Classes key
|
||||
let classes = ClassesVisitor::open_with_scope(scope)?;
|
||||
|
||||
// Open key for this extension.
|
||||
// If there is no such key, return directly.
|
||||
if let Some(subkey) = try_open_subkey_with_flags(
|
||||
&classes,
|
||||
blank_path_guard(self.ext.to_string())?,
|
||||
KEY_WRITE,
|
||||
)? {
|
||||
// Only delete the default key if it is equal to our ProgId
|
||||
if let Some(value) = try_get_value::<String, _>(&subkey, "")? {
|
||||
if value == prog_id.as_inner().to_string() {
|
||||
// Delete the default key.
|
||||
subkey.delete_value("")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Okey
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Query the default "Open With" of this file extension associated ProgId.
|
||||
///
|
||||
/// This function will return its associated ProgId if "Open With" was set.
|
||||
pub fn query(&self, view: View) -> Result<Option<ProgIdKey>> {
|
||||
use winreg_extra::{blank_path_guard, try_get_value, try_open_subkey_with_flags};
|
||||
|
||||
// Open Classes key
|
||||
let classes = ClassesVisitor::open_with_view(view)?;
|
||||
|
||||
// Open key for this extension if possible
|
||||
let rv = match try_open_subkey_with_flags(
|
||||
&classes,
|
||||
blank_path_guard(self.ext.to_string())?,
|
||||
KEY_READ,
|
||||
)? {
|
||||
Some(subkey) => {
|
||||
// Try get associated ProgId if possible
|
||||
match try_get_value::<String, _>(&subkey, "")? {
|
||||
Some(value) => Some(ProgIdKey::new(value.as_str())?),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
// Okey
|
||||
Ok(rv)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: ProgId Registry Key
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ProgIdKey {
|
||||
prog_id: ProgIdKind,
|
||||
}
|
||||
|
||||
impl ProgIdKey {
|
||||
/// Create new ProgId registry representer.
|
||||
pub fn new(s: &str) -> Result<Self> {
|
||||
Ok(Self {
|
||||
prog_id: ProgIdKind::from_str(s)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch the reference to inner ProgId.
|
||||
pub fn as_inner(&self) -> &ProgIdKind {
|
||||
&self.prog_id
|
||||
}
|
||||
}
|
||||
|
||||
impl ProgIdKey {
|
||||
/// Create ProgId into Registry in given scope with given parameters
|
||||
pub fn create(&self, scope: Scope, command: &str) -> Result<()> {
|
||||
use winreg_extra::blank_path_guard;
|
||||
|
||||
let classes = ClassesVisitor::open_with_scope(scope)?;
|
||||
let (subkey, _) = classes
|
||||
.create_subkey_with_flags(blank_path_guard(self.prog_id.to_string())?, KEY_WRITE)?;
|
||||
|
||||
// Create verb
|
||||
let (subkey_verb, _) = subkey.create_subkey_with_flags("open", KEY_READ)?;
|
||||
let (subkey_command, _) = subkey_verb.create_subkey_with_flags("command", KEY_WRITE)?;
|
||||
subkey_command.set_value("", &command.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete this ProgId from registry in given scope.
|
||||
pub fn delete(&self, scope: Scope) -> Result<()> {
|
||||
use winreg_extra::blank_path_guard;
|
||||
|
||||
let classes = ClassesVisitor::open_with_scope(scope)?;
|
||||
classes.delete_subkey_all(blank_path_guard(self.prog_id.to_string())?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
+31
-310
@@ -1,314 +1,35 @@
|
||||
use crate::{lowlevel, utilities, win32};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::rc::Rc;
|
||||
use std::path::Path;
|
||||
use thiserror::Error as TeError;
|
||||
//! The module gives a convenient highlevel wrapper for lowlevel file association operations.
|
||||
|
||||
use crate::lowlevel;
|
||||
|
||||
// region: Utilities
|
||||
|
||||
/// The println macro only works on Debug mode
|
||||
/// for tracing the execution of some important functions.
|
||||
macro_rules! debug_println {
|
||||
// For no argument.
|
||||
() => {
|
||||
if cfg!(debug_assertions) {
|
||||
eprintln!();
|
||||
}
|
||||
};
|
||||
// For one or more arguments like println!.
|
||||
($($arg:tt)*) => {
|
||||
if cfg!(debug_assertions) {
|
||||
eprintln!($($arg)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Exposed Stuff
|
||||
|
||||
mod schema;
|
||||
mod program;
|
||||
|
||||
pub use schema::{Schema, SchemaError};
|
||||
pub use program::{Program, ParseProgramError, ProgramError, ProgramSelfExtStatus, ProgramExtStatus};
|
||||
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}")]
|
||||
BadFileName(#[from] win32::concept::BadFileNameError),
|
||||
#[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 occurs when operating with `Program`.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum ProgramError {}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Schema
|
||||
|
||||
// region: Schema Body
|
||||
|
||||
/// Schema is the sketchpad of complete Program.
|
||||
///
|
||||
/// We will create a Schema first, fill some properties, add file extensions,
|
||||
/// then convert it into immutable Program for following 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(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_identifier(&mut self, identifier: &str) -> () {
|
||||
self.identifier = identifier.to_string();
|
||||
}
|
||||
|
||||
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(()),
|
||||
}
|
||||
}
|
||||
|
||||
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(()),
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
file_name: String,
|
||||
dir_name: String,
|
||||
name: Rc<ProgramStr>,
|
||||
icon: Rc<ProgramIcon>,
|
||||
behavior: Rc<ProgramBehavior>,
|
||||
|
||||
strs: Vec<Rc<ProgramStr>>,
|
||||
icons: Vec<Rc<ProgramIcon>>,
|
||||
behaviors: Vec<Rc<ProgramBehavior>>,
|
||||
|
||||
progid_keys: Vec<Rc<ProgramProgIdKey>>,
|
||||
ext_keys: Vec<ProgramExtKey>,
|
||||
}
|
||||
|
||||
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_name(full_path: &Path) -> Result<&OsStr, ParseProgramError> {
|
||||
full_path
|
||||
.parent()
|
||||
.map(|p| p.as_os_str())
|
||||
.ok_or(ParseProgramError::NoDirNamePart)
|
||||
}
|
||||
|
||||
/// Try creating Program from Schema.
|
||||
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 file_name = Self::extract_file_name(schema_path)?;
|
||||
let file_name = String::from(utilities::osstr_to_str(file_name)?);
|
||||
let dir_name = Self::extract_dir_name(schema_path)?;
|
||||
let dir_name = String::from(utilities::osstr_to_str(dir_name)?);
|
||||
// Build app paths key and applications key respectively
|
||||
let key = win32::concept::FileName::new(&file_name)?;
|
||||
let app_paths_key = lowlevel::AppPathsKey::new(key.clone());
|
||||
let applications_key = lowlevel::ApplicationsKey::new(key.clone());
|
||||
|
||||
todo!();
|
||||
// Ok(Self {
|
||||
// app_paths_key,
|
||||
// applications_key,
|
||||
// file_name,
|
||||
// dir_name,
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
/// Register this application.
|
||||
///
|
||||
/// If there is registration of this application,
|
||||
/// this function will return error.
|
||||
pub fn register(&mut self, scope: Scope) -> Result<(), ProgramError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Unregister this application.
|
||||
///
|
||||
/// If there is no registration of this application,
|
||||
/// this function will return error.
|
||||
pub fn unregister(&mut self, scope: Scope) -> Result<(), ProgramError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// 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, view: View) -> Result<bool, ProgramError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn link_ext(&self, scope: Scope, index: usize) -> Result<(), ProgramError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn unlink_ext(&self, scope: Scope, index: usize) -> Result<(), ProgramError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn ext_status(&self, view: View, index: usize) -> Result<(), ProgramError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Program Internals
|
||||
|
||||
/// Internal used enum presenting a Program string resource.
|
||||
type ProgramStr = lowlevel::LosseStrRefStr;
|
||||
|
||||
/// Internal used enum presenting a Program icon resource.
|
||||
type ProgramIcon = lowlevel::LosseIconRefStr;
|
||||
|
||||
/// Internal used enum presenting a Program behavior (command line setups).
|
||||
#[derive(Debug)]
|
||||
struct ProgramBehavior {
|
||||
inner: String,
|
||||
}
|
||||
|
||||
/// Internal used struct presenting a Program ProgId.
|
||||
#[derive(Debug)]
|
||||
struct ProgramProgIdKey {
|
||||
key: lowlevel::ProgIdKey,
|
||||
name: Rc<ProgramStr>,
|
||||
icon: Rc<ProgramIcon>,
|
||||
behavior: Rc<ProgramBehavior>,
|
||||
}
|
||||
|
||||
/// Internal used struct presenting a Program file extension.
|
||||
#[derive(Debug)]
|
||||
struct ProgramExtKey {
|
||||
key: lowlevel::ExtKey,
|
||||
assoc: Rc<ProgramProgIdKey>
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -0,0 +1,757 @@
|
||||
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("parsing into Program error: {0}")]
|
||||
BadExtBody(#[from] concept::BadExtBodyError),
|
||||
#[error("parsing into Program error: {0}")]
|
||||
BadProgIdPart(#[from] concept::BadProgIdPartError),
|
||||
#[error("parsing into Program error: {0}")]
|
||||
BadFileName(#[from] concept::BadFileNameError),
|
||||
#[error("parsing into Program error: {0}")]
|
||||
ParseClsid(#[from] concept::ParseClsidError),
|
||||
#[error("parsing into Program error: {0}")]
|
||||
ParseCmdLine(#[from] concept::ParseCmdLineError),
|
||||
#[error("parsing into Program error: {0}")]
|
||||
CastOsStr(#[from] utilities::CastOsStrError),
|
||||
|
||||
#[error("given path doesn't has legal file name part when parsing into Program")]
|
||||
NoFileNamePart,
|
||||
#[error("given path doesn't has legal directory part when parsing into Program")]
|
||||
NoDirNamePart,
|
||||
#[error("identifier {0} is unexpected not presented in HashMap when parsing into Program")]
|
||||
NoSuchIdentifier(String),
|
||||
#[error("bad program identifier found when parsing into Program")]
|
||||
BadIdentifier,
|
||||
}
|
||||
|
||||
/// Error occurs when operating with [Program].
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum ProgramError {
|
||||
#[error("Program operation error: {0}")]
|
||||
Lowlevel(#[from] lowlevel::Error),
|
||||
#[error("Program operation error: {0}")]
|
||||
LoadIconRc(#[from] concept::LoadIconRcError),
|
||||
#[error("given file extension index {0} is invalid when visiting Program")]
|
||||
BadIndex(usize),
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Program
|
||||
|
||||
/// Program is a complete and immutable program representer
|
||||
#[derive(Debug)]
|
||||
pub struct Program {
|
||||
app_paths_key: lowlevel::AppPathsKey,
|
||||
applications_key: lowlevel::ApplicationsKey,
|
||||
app_path: String,
|
||||
app_file_name: String,
|
||||
app_dir_path: String,
|
||||
|
||||
// TODO: Remove this dead_code attribute once we start to use CLSID.
|
||||
#[allow(dead_code)]
|
||||
clsid: concept::Clsid,
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// Flatten a `HashMap<String, V>` into a `Vec<U>` with a reverse index map,
|
||||
/// by applying the given transformation function `f` to each value.
|
||||
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))
|
||||
}
|
||||
|
||||
/// Resolve a key string to an `Arc<T>` by looking it up in the index map,
|
||||
/// then indexing into the vector.
|
||||
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(key.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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());
|
||||
|
||||
// Parse CLSID
|
||||
let clsid = schema.get_clsid().parse::<concept::Clsid>()?;
|
||||
|
||||
// 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_file_name,
|
||||
app_dir_path,
|
||||
clsid,
|
||||
name,
|
||||
icon,
|
||||
behavior,
|
||||
strs,
|
||||
icons,
|
||||
behaviors,
|
||||
ext_keys,
|
||||
ext_keys_map,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
/// Resolve the display name of this application.
|
||||
///
|
||||
/// Returns the user-specified display name first.
|
||||
/// Falls back to the executable file name if no display name is configured.
|
||||
pub fn resolve_name(&self) -> Result<String, ProgramError> {
|
||||
// Fecch from user specified name first
|
||||
let name = self
|
||||
.name
|
||||
.as_ref()
|
||||
.map(|name| name.inner.extract().ok())
|
||||
.flatten();
|
||||
if let Some(name) = name {
|
||||
return Ok(name);
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// Fetch it from executable manifest file.
|
||||
|
||||
// Finally fallback to use executable name.
|
||||
Ok(self.app_file_name.clone())
|
||||
}
|
||||
|
||||
/// Resolve the icon of this application.
|
||||
///
|
||||
/// Returns the user-specified icon first, then falls back to the first icon from the executable,
|
||||
/// and finally to the system default executable icon.
|
||||
pub fn resolve_icon(&self) -> Result<concept::IconRc, ProgramError> {
|
||||
// Fetch from user specified icon first
|
||||
let icon = self
|
||||
.icon
|
||||
.as_ref()
|
||||
.map(|icon| icon.inner.extract(concept::IconSizeKind::Small).ok())
|
||||
.flatten();
|
||||
if let Some(icon) = icon {
|
||||
return Ok(icon);
|
||||
}
|
||||
// Fetch from the first icon of executable instead.
|
||||
let icon = concept::IconRc::new(&self.app_path, 0, concept::IconSizeKind::Small).ok();
|
||||
if let Some(icon) = icon {
|
||||
return Ok(icon);
|
||||
}
|
||||
// Finally fallback to use system default executable icon.
|
||||
Ok(concept::IconRc::GENERIC_APPLICATION(
|
||||
concept::IconSizeKind::Small,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Return the number of file extensions associated with this program.
|
||||
pub fn exts_len(&self) -> usize {
|
||||
self.ext_keys.len()
|
||||
}
|
||||
|
||||
/// Find the index of a file extension by its body (without leading dot).
|
||||
///
|
||||
/// Returns `None` if the given extension is not associated with this program.
|
||||
pub fn find_ext(&self, body: &str) -> Option<usize> {
|
||||
self.ext_keys_map.get(body).copied()
|
||||
}
|
||||
|
||||
/// Resolve the extension status for the extension at the given index.
|
||||
/// This status including the ProgId associated with this file extension,
|
||||
/// and the display name and icon for this file extension.
|
||||
///
|
||||
/// Returns the fetched status, or the error occurs when fetching.
|
||||
pub fn resolve_ext(&self, index: usize) -> Result<ProgramSelfExtStatus, ProgramError> {
|
||||
// Fetch data
|
||||
let progid_ext_key = self
|
||||
.ext_keys
|
||||
.get(index)
|
||||
.ok_or(ProgramError::BadIndex(index))?;
|
||||
|
||||
// Try resolving name with string resource first,
|
||||
// and fallback to ProgId verbatim.
|
||||
let name = progid_ext_key
|
||||
.name
|
||||
.inner
|
||||
.extract()
|
||||
.unwrap_or(progid_ext_key.progid_key.inner().to_string());
|
||||
// Try to fetch icon, and fallback to system default file icon.
|
||||
let icon = progid_ext_key
|
||||
.icon
|
||||
.inner
|
||||
.extract(concept::IconSizeKind::Small)
|
||||
.unwrap_or(concept::IconRc::GENERIC_DOCUMENT(
|
||||
concept::IconSizeKind::Small,
|
||||
)?);
|
||||
|
||||
// Okey, return it
|
||||
Ok(ProgramSelfExtStatus::new(
|
||||
progid_ext_key.ext_key.inner().clone(),
|
||||
name,
|
||||
icon,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
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!(
|
||||
r#"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.ensure(scope)?;
|
||||
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!(
|
||||
r#"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,
|
||||
// if this file extension is existing
|
||||
let ext_key = &mut program_key.ext_key;
|
||||
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)?;
|
||||
}
|
||||
|
||||
// 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-requirements of [Self::register] and [Self::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!(
|
||||
r#"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)
|
||||
}
|
||||
|
||||
/// Set this program as the default handler for the file extension at the given index.
|
||||
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!(
|
||||
r#"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(index)),
|
||||
};
|
||||
|
||||
// Everything is okey.
|
||||
// Notify changes and return
|
||||
win32::utilities::notify_assoc_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove this program as the default handler for the file extension at the given index.
|
||||
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!(
|
||||
r#"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(index)),
|
||||
}
|
||||
|
||||
// Everything is okey.
|
||||
// Notify changes and return
|
||||
win32::utilities::notify_assoc_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Query the current default association for the file extension at the given index.
|
||||
///
|
||||
/// Returns `Ok(None)` if the extension is not associated with any program,
|
||||
/// or if the associated program's registration data is missing.
|
||||
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!(
|
||||
r#"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 and fallback to system default file icon.
|
||||
let icon = progid_key
|
||||
.get_default_icon(view)?
|
||||
.map(|ico| ico.extract(concept::IconSizeKind::Small).ok())
|
||||
.flatten()
|
||||
.unwrap_or(concept::IconRc::GENERIC_DOCUMENT(
|
||||
concept::IconSizeKind::Small,
|
||||
)?);
|
||||
|
||||
// Okey, return it.
|
||||
Ok(Some(ProgramExtStatus::new(name, icon)))
|
||||
}
|
||||
None => Err(ProgramError::BadIndex(index)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 this program provided method for opening specific file extension.
|
||||
///
|
||||
/// The data including the extension name, diaplay name and icon.
|
||||
#[derive(Debug)]
|
||||
pub struct ProgramSelfExtStatus {
|
||||
ext: concept::Ext,
|
||||
name: String,
|
||||
icon: concept::IconRc,
|
||||
}
|
||||
|
||||
impl ProgramSelfExtStatus {
|
||||
/// Create a new `ProgramSelfExtStatus`.
|
||||
fn new(ext: concept::Ext, name: String, icon: concept::IconRc) -> Self {
|
||||
Self { ext, name, icon }
|
||||
}
|
||||
|
||||
/// Get the extension name without leading dot like `jpg`.
|
||||
pub fn get_ext(&self) -> &str {
|
||||
self.ext.inner()
|
||||
}
|
||||
|
||||
/// Get the extension name with leading dot like `.jpg`.
|
||||
pub fn get_dotted_ext(&self) -> String {
|
||||
self.ext.dotted_inner()
|
||||
}
|
||||
|
||||
/// 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) -> &concept::IconRc {
|
||||
&self.icon
|
||||
}
|
||||
}
|
||||
|
||||
/// Exposed struct representing the default associated program of specific file extension.
|
||||
///
|
||||
/// The data including the diaplay name and icon.
|
||||
#[derive(Debug)]
|
||||
pub struct ProgramExtStatus {
|
||||
name: String,
|
||||
icon: concept::IconRc,
|
||||
}
|
||||
|
||||
impl ProgramExtStatus {
|
||||
/// Create a new `ProgramExtStatus`.
|
||||
fn new(name: String, icon: 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) -> &concept::IconRc {
|
||||
&self.icon
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -0,0 +1,259 @@
|
||||
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 in Schema operation: {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 {
|
||||
/// Create an empty `Schema` with all fields set to their defaults.
|
||||
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();
|
||||
}
|
||||
|
||||
/// Set the CLSID (Class Identifier) of the application.
|
||||
///
|
||||
/// # Todo
|
||||
///
|
||||
/// Currently this field is not used by this crate,
|
||||
/// but you still need to fill it with a legal CLSID for future expanding.
|
||||
pub fn set_clsid(&mut self, clsid: &str) -> () {
|
||||
self.clsid = clsid.to_string();
|
||||
}
|
||||
|
||||
/// Set the key referencing a named string resource for the application display name.
|
||||
///
|
||||
/// This field is optional and can pass `None` to clear it.
|
||||
pub fn set_name(&mut self, name: Option<&str>) -> () {
|
||||
self.name = name.map(|n| n.to_string());
|
||||
}
|
||||
|
||||
/// Set the key referencing a named icon resource for the application default icon.
|
||||
///
|
||||
/// This field is optional and can pass `None` to clear it.
|
||||
pub fn set_icon(&mut self, icon: Option<&str>) -> () {
|
||||
self.icon = icon.map(|i| i.to_string());
|
||||
}
|
||||
|
||||
/// Set the key referencing a named behavior resource for the application default execution behavior.
|
||||
///
|
||||
/// This field is optional and can pass `None` to clear it.
|
||||
pub fn set_behavior(&mut self, behavior: Option<&str>) -> () {
|
||||
self.behavior = behavior.map(|b| b.to_string());
|
||||
}
|
||||
|
||||
/// Add a named string resource entry.
|
||||
///
|
||||
/// Returns error if the name already exists.
|
||||
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(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a named icon resource entry.
|
||||
///
|
||||
/// Returns error if the name already exists.
|
||||
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(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a named behavior (command line) entry.
|
||||
///
|
||||
/// Returns error if the name already exists.
|
||||
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 `.`.
|
||||
/// And the parameter `ext_name`, `ext_icon` and `ext_behavior` are the key
|
||||
/// referencing a named string resource, icon resource and behavior resource respectively.
|
||||
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 {
|
||||
/// Get the identifier of the schema.
|
||||
pub(super) fn get_identifier(&self) -> &str {
|
||||
&self.identifier
|
||||
}
|
||||
|
||||
/// Get the absolute path to the executable file.
|
||||
pub(super) fn get_path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Get the CLSID (Class Identifier) of the application.
|
||||
pub(super) fn get_clsid(&self) -> &str {
|
||||
&self.clsid
|
||||
}
|
||||
|
||||
/// Get the key referencing the named string resource for the display name.
|
||||
/// Return `None` if user doesn't specify this.
|
||||
pub(super) fn get_name(&self) -> Option<&str> {
|
||||
self.name.as_ref().map(|v| v.as_str())
|
||||
}
|
||||
|
||||
/// Get the key referencing the named icon resource for the default icon.
|
||||
/// Return `None` if user doesn't specify this.
|
||||
pub(super) fn get_icon(&self) -> Option<&str> {
|
||||
self.icon.as_ref().map(|v| v.as_str())
|
||||
}
|
||||
|
||||
/// Get the key referencing the named behavior resource for the default verb.
|
||||
/// Return `None` if user doesn't specify this.
|
||||
pub(super) fn get_behavior(&self) -> Option<&str> {
|
||||
self.behavior.as_ref().map(|v| v.as_str())
|
||||
}
|
||||
|
||||
/// Get a reference to the string resources map.
|
||||
pub(super) fn get_strs(&self) -> &HashMap<String, String> {
|
||||
&self.strs
|
||||
}
|
||||
|
||||
/// Get a reference to the icon resources map.
|
||||
pub(super) fn get_icons(&self) -> &HashMap<String, String> {
|
||||
&self.icons
|
||||
}
|
||||
|
||||
/// Get a reference to the behavior resources map.
|
||||
pub(super) fn get_behaviors(&self) -> &HashMap<String, String> {
|
||||
&self.behaviors
|
||||
}
|
||||
|
||||
/// Get a reference to the file extensions map.
|
||||
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 {
|
||||
/// Create a new `SchemaExt` with the given name, icon key and behavior key.
|
||||
fn new(name: &str, icon: &str, behavior: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
icon: icon.to_string(),
|
||||
behavior: behavior.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SchemaExt {
|
||||
/// Get the key referencing the named string resource for display name.
|
||||
pub(super) fn get_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Get the key referencing the named icon resource for file icon.
|
||||
pub(super) fn get_icon(&self) -> &str {
|
||||
&self.icon
|
||||
}
|
||||
|
||||
/// Get the key referencing the named behavior resource.
|
||||
pub(super) fn get_behavior(&self) -> &str {
|
||||
&self.behavior
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
//! This crate provide utilities fetching and manilupating Windows file association.
|
||||
//! All code under crate are following Microsoft document: https://learn.microsoft.com/en-us/windows/win32/shell/customizing-file-types-bumper
|
||||
//! All code under crate are following Microsoft document: <https://learn.microsoft.com/en-us/windows/win32/shell/customizing-file-types-bumper>
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
compile_error!("Crate wfassoc is only supported on Windows.");
|
||||
|
||||
@@ -1,501 +0,0 @@
|
||||
//! This crate provide utilities fetching and manilupating Windows file association.
|
||||
//! All code under crate are following Microsoft document: https://learn.microsoft.com/en-us/windows/win32/shell/customizing-file-types-bumper
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
compile_error!("Crate wfassoc is only supported on Windows.");
|
||||
|
||||
pub mod extra;
|
||||
pub mod utilities;
|
||||
pub mod assoc;
|
||||
|
||||
use assoc::{Ext, ProgId};
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use regex::Regex;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
use thiserror::Error as TeError;
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::{
|
||||
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
|
||||
};
|
||||
|
||||
// region: Error Types
|
||||
|
||||
/// All possible error occurs in this crate.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum Error {
|
||||
#[error("error occurs when manipulating with Registry: {0}")]
|
||||
BadRegOper(#[from] std::io::Error),
|
||||
#[error("{0}")]
|
||||
CastOsStr(#[from] utilities::CastOsStrError),
|
||||
#[error("{0}")]
|
||||
ParseExt(#[from] assoc::ParseExtError),
|
||||
|
||||
#[error("no administrative privilege")]
|
||||
NoPrivilege,
|
||||
#[error("given identifier \"{0}\" of application is invalid")]
|
||||
BadIdentifier(String),
|
||||
#[error("given full path to application is invalid")]
|
||||
BadFullAppPath,
|
||||
#[error("manner \"{0}\" is already registered")]
|
||||
DupManner(String),
|
||||
#[error("file extension \"{0}\" is already registered")]
|
||||
DupExt(String),
|
||||
#[error("the token of manner is invalid")]
|
||||
InvalidMannerToken,
|
||||
#[error("the token of file extension is invalid")]
|
||||
InvalidExtToken,
|
||||
}
|
||||
|
||||
/// The result type used in this crate.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// endregion
|
||||
|
||||
// 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.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Scope {
|
||||
/// Scope for current user.
|
||||
User,
|
||||
/// Scope for all users under this computer.
|
||||
System,
|
||||
}
|
||||
|
||||
/// The error occurs when cast View into Scope.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("hybrid View can not be cast into Scope")]
|
||||
pub struct TryFromViewError {}
|
||||
|
||||
impl TryFromViewError {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<View> for Scope {
|
||||
type Error = TryFromViewError;
|
||||
|
||||
fn try_from(value: View) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
View::User => Ok(Self::User),
|
||||
View::System => Ok(Self::System),
|
||||
View::Hybrid => Err(TryFromViewError::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
/// Check whether we have enough privilege when operating in current scope.
|
||||
/// If we have, return true, otherwise false.
|
||||
pub fn has_privilege(&self) -> bool {
|
||||
// If we operate on System, and we do not has privilege,
|
||||
// we think we do not have privilege, otherwise,
|
||||
// there is no privilege required.
|
||||
!matches!(self, Self::System if !utilities::has_privilege())
|
||||
}
|
||||
}
|
||||
|
||||
/// The view when wfassoc querying file extension association.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum View {
|
||||
/// The view of current user.
|
||||
User,
|
||||
/// The view of system.
|
||||
System,
|
||||
/// Hybrid view of User and System.
|
||||
/// It can be seen as that we use System first and then use User to override any existing items.
|
||||
Hybrid,
|
||||
}
|
||||
|
||||
impl From<Scope> for View {
|
||||
fn from(value: Scope) -> Self {
|
||||
match value {
|
||||
Scope::User => Self::User,
|
||||
Scope::System => Self::System,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Program
|
||||
|
||||
/// The struct representing a complete program for registration and unregistration.
|
||||
#[derive(Debug)]
|
||||
pub struct Program {
|
||||
/// The identifier of this program.
|
||||
identifier: String,
|
||||
/// The fully qualified path to the application.
|
||||
full_path: PathBuf,
|
||||
/// The collection holding all manners of this program.
|
||||
manners: IndexSet<String>,
|
||||
/// 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<Ext, Token>,
|
||||
}
|
||||
|
||||
impl Program {
|
||||
/// Create a new registrar for following operations.
|
||||
///
|
||||
/// `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程序` (means "My Qt App" in English)
|
||||
///
|
||||
/// 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: &str) -> Result<Self> {
|
||||
// Check identifier
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9]*$").unwrap());
|
||||
if !RE.is_match(identifier) {
|
||||
return Err(Error::BadIdentifier(identifier.to_string()));
|
||||
}
|
||||
|
||||
// Everything is okey, build self.
|
||||
Ok(Self {
|
||||
identifier: identifier.to_string(),
|
||||
// The error type of PathBuf FromStr trait is Infallible,
|
||||
// so it must be okey and we can use unwrap safely.
|
||||
full_path: full_path.parse().unwrap(),
|
||||
manners: IndexSet::new(),
|
||||
exts: IndexMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Add manner provided by this program.
|
||||
pub fn add_manner(&mut self, manner: &str) -> Result<Token> {
|
||||
// TODO: Use wincmd::CmdArgs instead of String.
|
||||
// Create manner from string
|
||||
let manner = manner.to_string();
|
||||
// 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::DupManner(manner_str))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the string display of manner represented by given token
|
||||
pub fn get_manner_str(&self, token: Token) -> Option<String> {
|
||||
self.manners.get_index(token).map(|s| s.clone())
|
||||
}
|
||||
|
||||
/// Add file extension supported by this program and its associated manner.
|
||||
pub fn add_ext(&mut self, ext: &str, token: Token) -> Result<Token> {
|
||||
// Check manner token
|
||||
if let None = self.manners.get_index(token) {
|
||||
return Err(Error::InvalidMannerToken);
|
||||
}
|
||||
|
||||
// Create extension from string
|
||||
let ext = Ext::new(ext)?;
|
||||
// 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 string display of file extension represented by given token
|
||||
pub fn get_ext_str(&self, token: Token) -> Option<String> {
|
||||
self.exts.get_index(token).map(|p| p.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
|
||||
const APPLICATIONS: &str = "Software\\Classes\\Applications";
|
||||
|
||||
/// Register this application.
|
||||
pub fn register(&self, scope: Scope) -> Result<()> {
|
||||
// Check privilege
|
||||
if !scope.has_privilege() {
|
||||
return Err(Error::NoPrivilege);
|
||||
}
|
||||
|
||||
// Fetch root key.
|
||||
let hk = RegKey::predef(match scope {
|
||||
Scope::User => HKEY_CURRENT_USER,
|
||||
Scope::System => HKEY_LOCAL_MACHINE,
|
||||
});
|
||||
// Fetch file name and start in path.
|
||||
let file_name = self.extract_file_name()?;
|
||||
let start_in = self.extract_start_in()?;
|
||||
|
||||
// Create App Paths subkey
|
||||
debug_println!("Adding App Paths subkey...");
|
||||
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)?;
|
||||
// Write App Paths values
|
||||
subkey.set_value("", &utilities::path_to_str(&self.full_path)?)?;
|
||||
subkey.set_value("Path", &utilities::osstr_to_str(&start_in)?)?;
|
||||
|
||||
// Create Applications subkey
|
||||
debug_println!("Adding Applications subkey...");
|
||||
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
|
||||
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
|
||||
// Write Applications values
|
||||
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(), &"")?;
|
||||
}
|
||||
}
|
||||
|
||||
// Create ProgId subkeys
|
||||
debug_println!("Adding ProgId subkey...");
|
||||
let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
|
||||
for (ext, manner_token) in self.exts.iter() {
|
||||
let manner = self.manners.get_index(*manner_token).ok_or(Error::InvalidMannerToken)?;
|
||||
let prog_id = self.build_prog_id(ext);
|
||||
|
||||
debug_println!("Adding ProgId \"{0}\" subkey...", prog_id.to_string());
|
||||
let (subkey, _) = subkey_parent.create_subkey_with_flags(prog_id.to_string(), KEY_READ)?;
|
||||
let (subkey_verb, _) = subkey.create_subkey_with_flags("open", KEY_READ)?;
|
||||
let (subkey_command, _) = subkey_verb.create_subkey_with_flags("command", KEY_WRITE)?;
|
||||
subkey_command.set_value("", manner)?;
|
||||
}
|
||||
|
||||
// Okey
|
||||
utilities::notify_assoc_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unregister this application.
|
||||
pub fn unregister(&self, scope: Scope) -> Result<()> {
|
||||
// Check privilege
|
||||
if !scope.has_privilege() {
|
||||
return Err(Error::NoPrivilege);
|
||||
}
|
||||
|
||||
// Fetch root key and file name.
|
||||
let hk = RegKey::predef(match scope {
|
||||
Scope::User => HKEY_CURRENT_USER,
|
||||
Scope::System => HKEY_LOCAL_MACHINE,
|
||||
});
|
||||
let file_name = self.extract_file_name()?;
|
||||
|
||||
// Remove App Paths subkey
|
||||
debug_println!("Removing App Paths subkey...");
|
||||
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_WRITE)?;
|
||||
subkey_parent.delete_subkey_all(file_name)?;
|
||||
|
||||
// Remove Applications subkey
|
||||
debug_println!("Removing Applications subkey...");
|
||||
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
|
||||
subkey_parent.delete_subkey_all(file_name)?;
|
||||
|
||||
// Remove ProgId subkeys
|
||||
debug_println!("Removing ProgId subkey...");
|
||||
let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
|
||||
for ext in self.exts.keys() {
|
||||
let prog_id = self.build_prog_id(ext);
|
||||
|
||||
debug_println!("Removing ProgId \"{0}\" subkey...", prog_id.to_string());
|
||||
subkey_parent.delete_subkey_all(prog_id.to_string())?;
|
||||
}
|
||||
|
||||
// Okey
|
||||
utilities::notify_assoc_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check whether this application has been registered.
|
||||
///
|
||||
/// Please note that this is a rough check and do not validate any data.
|
||||
pub fn is_registered(&self, scope: Scope) -> Result<bool> {
|
||||
// Fetch root key and file name.
|
||||
let hk = RegKey::predef(match scope {
|
||||
Scope::User => HKEY_CURRENT_USER,
|
||||
Scope::System => HKEY_LOCAL_MACHINE,
|
||||
});
|
||||
let file_name = self.extract_file_name()?;
|
||||
|
||||
// Check App Paths subkey.
|
||||
debug_println!("Checking App Paths subkey...");
|
||||
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
|
||||
if let Err(_) = subkey_parent.open_subkey_with_flags(file_name, KEY_READ) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check Application subkey.
|
||||
debug_println!("Checking Applications subkey...");
|
||||
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
|
||||
if let Err(_) = subkey_parent.open_subkey_with_flags(file_name, KEY_READ) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Both subkeys are roughly existing.
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
const CLASSES: &str = "Software\\Classes";
|
||||
|
||||
/// Set the default "open with" of given token associated extension to this program.
|
||||
pub fn link_ext(&self, ext: Token, scope: Scope) -> Result<()> {
|
||||
// Check privilege
|
||||
if !scope.has_privilege() {
|
||||
return Err(Error::NoPrivilege);
|
||||
}
|
||||
|
||||
// Fetch file extension and build ProgId from it
|
||||
let (ext, _) = match self.exts.get_index(ext) {
|
||||
Some(v) => v,
|
||||
None => return Err(Error::InvalidExtToken),
|
||||
};
|
||||
let prog_id = self.build_prog_id(ext);
|
||||
|
||||
// Fetch root key and navigate to Classes
|
||||
let hk = RegKey::predef(match scope {
|
||||
Scope::User => HKEY_CURRENT_USER,
|
||||
Scope::System => HKEY_LOCAL_MACHINE,
|
||||
});
|
||||
let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
|
||||
|
||||
// Open or create this extension key
|
||||
let (subkey, _) = classes.create_subkey_with_flags(ext.to_string(), KEY_WRITE)?;
|
||||
// Set the default way to open this file extension
|
||||
subkey.set_value("", &prog_id.to_string())?;
|
||||
|
||||
// Okey
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove this program from the default "open with" of given token associated extension.
|
||||
///
|
||||
/// If the default "open with" of given extension is not our program,
|
||||
/// or there is no such file extension, this function do nothing.
|
||||
pub fn unlink_ext(&self, ext: Token, scope: Scope) -> Result<()> {
|
||||
// Check privilege
|
||||
if !scope.has_privilege() {
|
||||
return Err(Error::NoPrivilege);
|
||||
}
|
||||
|
||||
// Fetch file extension and build ProgId from it
|
||||
let (ext, _) = match self.exts.get_index(ext) {
|
||||
Some(v) => v,
|
||||
None => return Err(Error::InvalidExtToken),
|
||||
};
|
||||
let prog_id = self.build_prog_id(ext);
|
||||
|
||||
// Fetch root key and navigate to Classes
|
||||
let hk = RegKey::predef(match scope {
|
||||
Scope::User => HKEY_CURRENT_USER,
|
||||
Scope::System => HKEY_LOCAL_MACHINE,
|
||||
});
|
||||
let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
|
||||
|
||||
// Open key for this extension.
|
||||
// If there is no such key, return directly.
|
||||
if let Some(subkey) =
|
||||
extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_WRITE)?
|
||||
{
|
||||
// Only delete the default key if it is equal to our ProgId
|
||||
if let Some(value) = extra::winreg::try_get_value::<String, _>(&subkey, "")? {
|
||||
if value == prog_id.to_string() {
|
||||
// Delete the default key.
|
||||
subkey.delete_value("")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Okey
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Query the default "open with" of given token associated extension.
|
||||
///
|
||||
/// This function will return its associated ProgId if it is existing.
|
||||
pub fn query_ext(&self, ext: Token, view: View) -> Result<Option<ProgId>> {
|
||||
// Fetch file extension
|
||||
let (ext, _) = match self.exts.get_index(ext) {
|
||||
Some(v) => v,
|
||||
None => return Err(Error::InvalidExtToken),
|
||||
};
|
||||
|
||||
// Fetch root key and navigate to Classes
|
||||
let hk = match view {
|
||||
View::User => RegKey::predef(HKEY_CURRENT_USER),
|
||||
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
||||
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
|
||||
};
|
||||
let classes = match view {
|
||||
View::User | View::System => hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?,
|
||||
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?,
|
||||
};
|
||||
|
||||
// Open key for this extension if possible
|
||||
let rv =
|
||||
match extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_READ)? {
|
||||
Some(subkey) => {
|
||||
// Try get associated ProgId if possible
|
||||
match extra::winreg::try_get_value::<String, _>(&subkey, "")? {
|
||||
Some(value) => Some(ProgId::from(value.as_str())),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
// Okey
|
||||
Ok(rv)
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
/// Extract the file name part from full path to application,
|
||||
/// which was used in Registry path component.
|
||||
fn extract_file_name(&self) -> Result<&OsStr> {
|
||||
// Get the file name part and make sure it is not empty.
|
||||
// Empty checker is CRUCIAL!
|
||||
self.full_path
|
||||
.file_name()
|
||||
.and_then(|p| if p.is_empty() { None } else { Some(p) })
|
||||
.ok_or(Error::BadFullAppPath)
|
||||
}
|
||||
|
||||
/// Extract the start in path from full path to application,
|
||||
/// which basically is the stem of full path.
|
||||
fn extract_start_in(&self) -> Result<&OsStr> {
|
||||
// Get parent part and make sure it is not empty
|
||||
// Empty checker is CRUCIAL!
|
||||
self.full_path
|
||||
.parent()
|
||||
.map(|p| p.as_os_str())
|
||||
.and_then(|p| if p.is_empty() { None } else { Some(p) })
|
||||
.ok_or(Error::BadFullAppPath)
|
||||
}
|
||||
|
||||
/// Build ProgId from identifier and given file extension.
|
||||
fn build_prog_id(&self, ext: &Ext) -> ProgId {
|
||||
ProgId::Std(assoc::StdProgId::new(
|
||||
&self.identifier,
|
||||
&utilities::capitalize_first_ascii(ext.inner()),
|
||||
None,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -1,582 +0,0 @@
|
||||
|
||||
/// The expand of winreg crate according to our module requirements.
|
||||
mod winregex;
|
||||
|
||||
use regex::Regex;
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
use thiserror::Error as TeError;
|
||||
use winreg::RegKey;
|
||||
|
||||
// region: Error Types
|
||||
|
||||
/// All possible error occurs in this crate.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum Error {
|
||||
#[error(
|
||||
"can not register because lack essential privilege. please consider running with Administrator role"
|
||||
)]
|
||||
NoPrivilege,
|
||||
#[error("{0}")]
|
||||
Register(#[from] std::io::Error),
|
||||
#[error("{0}")]
|
||||
BadFileExt(#[from] ParseFileExtError),
|
||||
#[error("{0}")]
|
||||
BadProgId(#[from] ParseProgIdError),
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Privilege, Scope and View
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// The scope where wfassoc will register and unregister.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Scope {
|
||||
/// Scope for current user.
|
||||
User,
|
||||
/// Scope for all users under this computer.
|
||||
System,
|
||||
}
|
||||
|
||||
/// The view when wfassoc querying infomations.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum View {
|
||||
/// The view of current user.
|
||||
User,
|
||||
/// The view of system.
|
||||
System,
|
||||
/// Hybrid view of User and System.
|
||||
/// It can be seen as that we use System first and then use User to override any existing items.
|
||||
Hybrid,
|
||||
}
|
||||
|
||||
/// The error occurs when cast View into Scope.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("hybrid view can not be cast into any scope")]
|
||||
pub struct TryFromViewError {}
|
||||
|
||||
impl TryFromViewError {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Scope> for View {
|
||||
fn from(value: Scope) -> Self {
|
||||
match value {
|
||||
Scope::User => Self::User,
|
||||
Scope::System => Self::System,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<View> for Scope {
|
||||
type Error = TryFromViewError;
|
||||
|
||||
fn try_from(value: View) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
View::User => Ok(Self::User),
|
||||
View::System => Ok(Self::System),
|
||||
View::Hybrid => Err(TryFromViewError::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
/// Check whether we have enough privilege when operating in current scope.
|
||||
/// If we have, simply return, otherwise return error.
|
||||
fn check_privilege(&self) -> Result<(), Error> {
|
||||
if matches!(self, Self::System if !has_privilege()) {
|
||||
Err(Error::NoPrivilege)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// 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)]
|
||||
pub struct FileExt {
|
||||
/// The body of file extension (excluding dot).
|
||||
inner: String,
|
||||
}
|
||||
|
||||
impl FileExt {
|
||||
pub fn new(file_ext: &str) -> Result<Self, ParseFileExtError> {
|
||||
Self::from_str(file_ext)
|
||||
}
|
||||
}
|
||||
|
||||
/// The error occurs when try parsing string into FileExt.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("given file extension is invalid")]
|
||||
pub struct ParseFileExtError {}
|
||||
|
||||
impl ParseFileExtError {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FileExt {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, ".{}", self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FileExt {
|
||||
type Err = ParseFileExtError;
|
||||
|
||||
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 {
|
||||
inner: v[1].to_string(),
|
||||
}),
|
||||
None => Err(ParseFileExtError::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileExt {
|
||||
fn open_scope(&self, scope: Scope) -> Result<RegKey, Error> {
|
||||
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE};
|
||||
|
||||
// check privilege
|
||||
scope.check_privilege()?;
|
||||
// get the root key
|
||||
let hk = match scope {
|
||||
Scope::User => RegKey::predef(HKEY_CURRENT_USER),
|
||||
Scope::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
||||
};
|
||||
// navigate to classes
|
||||
let classes = hk.open_subkey_with_flags("Software\\Classes", KEY_READ | KEY_WRITE)?;
|
||||
// okey
|
||||
Ok(classes)
|
||||
}
|
||||
|
||||
fn open_view(&self, view: View) -> Result<Option<RegKey>, Error> {
|
||||
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ};
|
||||
|
||||
// navigate to extension container
|
||||
let hk = match view {
|
||||
View::User => RegKey::predef(HKEY_CURRENT_USER),
|
||||
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
||||
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
|
||||
};
|
||||
let classes = match view {
|
||||
View::User | View::System => {
|
||||
hk.open_subkey_with_flags("Software\\Classes", KEY_READ)?
|
||||
}
|
||||
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?,
|
||||
};
|
||||
// check whether there is this ext
|
||||
classes.
|
||||
// open extension key if possible
|
||||
let thisext = classes.open_subkey_with_flags(file_ext.to_string(), KEY_READ)?;
|
||||
// okey
|
||||
Ok(classes)
|
||||
}
|
||||
|
||||
pub fn get_current(&self, view: View) -> Option<ProgId> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn set_current(&mut self, scope: Scope, prog_id: Option<&ProgId>) -> Result<(), Error> {
|
||||
scope.check_privilege()?;
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn iter_open_with(&self, view: View) -> Result<impl Iterator<Item = ProgId>, Error> {
|
||||
let viewer = match self.open_view(view)? {
|
||||
Some(viewer) => viewer,
|
||||
None => return Ok(std::iter::empty::<ProgId>()),
|
||||
};
|
||||
let it = winregex::iter_sz_keys(&viewer);
|
||||
let it = winregex::exclude_default_key(it);
|
||||
|
||||
Ok(it.map(|s| ProgId::from(s.as_str())))
|
||||
}
|
||||
|
||||
pub fn insert_open_with(&mut self, scope: Scope, prog_id: &ProgId) -> Result<(), Error> {
|
||||
scope.check_privilege()?;
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn flash_open_with(
|
||||
&mut self,
|
||||
scope: Scope,
|
||||
prog_ids: impl Iterator<Item = ProgId>,
|
||||
) -> Result<(), Error> {
|
||||
scope.check_privilege()?;
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// The association infomations of specific file extension.
|
||||
#[derive(Debug)]
|
||||
pub struct FileExtAssoc {
|
||||
default: String,
|
||||
open_with_progids: Vec<String>,
|
||||
}
|
||||
|
||||
impl FileExtAssoc {
|
||||
fn new(file_ext: &FileExt, view: View) -> Option<Self> {
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ};
|
||||
|
||||
// navigate to extension container
|
||||
let hk = match view {
|
||||
View::User => RegKey::predef(HKEY_CURRENT_USER),
|
||||
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
||||
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
|
||||
};
|
||||
let classes = match view {
|
||||
View::User | View::System => hk
|
||||
.open_subkey_with_flags("Software\\Classes", KEY_READ)
|
||||
.unwrap(),
|
||||
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ).unwrap(),
|
||||
};
|
||||
|
||||
// open extension key if possible
|
||||
let thisext = match classes.open_subkey_with_flags(file_ext.to_string(), KEY_READ) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
// fetch extension infos.
|
||||
let default = thisext.get_value("").unwrap_or(String::new());
|
||||
let open_with_progids =
|
||||
if let Ok(progids) = thisext.open_subkey_with_flags("OpenWithProdIds", KEY_READ) {
|
||||
progids
|
||||
.enum_keys()
|
||||
.map(|x| x.unwrap())
|
||||
.filter(|k| !k.is_empty())
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
default,
|
||||
open_with_progids,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_default(&self) -> &str {
|
||||
&self.default
|
||||
}
|
||||
|
||||
pub fn len_open_with_progid(&self) -> usize {
|
||||
self.open_with_progids.len()
|
||||
}
|
||||
|
||||
pub fn iter_open_with_progids(&self) -> impl Iterator<Item = &str> {
|
||||
self.open_with_progids.iter().map(|s| s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Executable Resource
|
||||
|
||||
// /// The struct representing an Windows executable resources path like
|
||||
// /// `path_to_file.exe,1`.
|
||||
// pub struct ExecRc {
|
||||
// /// The path to binary for finding resources.
|
||||
// binary: PathBuf,
|
||||
// /// The inner index of resources.
|
||||
// index: u32,
|
||||
// }
|
||||
|
||||
// impl ExecRc {
|
||||
// pub fn new(res_str: &str) -> Result<Self, ParseExecRcError> {
|
||||
// static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([^,]+),([0-9]+)$").unwrap());
|
||||
// let caps = RE.captures(res_str);
|
||||
// if let Some(caps) = caps {
|
||||
// let binary = PathBuf::from_str(&caps[1])?;
|
||||
// let index = caps[2].parse::<u32>()?;
|
||||
// Ok(Self { binary, index })
|
||||
// } else {
|
||||
// Err(ParseExecRcError::NoCapture)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// The error occurs when try parsing string into ExecRc.
|
||||
// #[derive(Debug, TeError)]
|
||||
// #[error("given string is not a valid executable resource string")]
|
||||
// pub enum ParseExecRcError {
|
||||
// /// Given string is not matched with format.
|
||||
// NoCapture,
|
||||
// /// Fail to convert executable part into path.
|
||||
// BadBinaryPath(#[from] std::convert::Infallible),
|
||||
// /// Fail to convert index part into valid number.
|
||||
// BadIndex(#[from] std::num::ParseIntError),
|
||||
// }
|
||||
|
||||
// impl FromStr for ExecRc {
|
||||
// type Err = ParseExecRcError;
|
||||
|
||||
// fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// ExecRc::new(s)
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Display for ExecRc {
|
||||
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// write!(f, "{},{}", self.binary.to_str().unwrap(), self.index)
|
||||
// }
|
||||
// }
|
||||
|
||||
// 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
|
||||
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 string is invalid")]
|
||||
pub struct ParseProgIdError {}
|
||||
|
||||
impl ParseProgIdError {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())?;
|
||||
Ok(Self::new(vendor, component, version))
|
||||
} else {
|
||||
Err(ParseProgIdError::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: Program
|
||||
|
||||
// /// The struct representing a complete Win32 program.
|
||||
// pub struct Program {
|
||||
// file_exts: Vec<FileExt>,
|
||||
// }
|
||||
|
||||
// impl Program {
|
||||
// /// Create a program descriptor.
|
||||
// pub fn new() -> Self {
|
||||
// Self {
|
||||
// file_exts: Vec::new(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Program {
|
||||
// /// Register program in this computer
|
||||
// pub fn register(&self, kind: RegisterKind) -> Result<(), Error> {
|
||||
// todo!("pretend to register >_<...")
|
||||
// }
|
||||
|
||||
// /// Unregister program from this computer.
|
||||
// pub fn unregister(&self) -> Result<(), Error> {
|
||||
// todo!("pretend to unregister >_<...")
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Program {
|
||||
// /// Query file extension infos which this program want to associate with.
|
||||
// pub fn query(&self) -> Result<(), Error> {
|
||||
// todo!("pretend to query >_<...")
|
||||
// }
|
||||
// }
|
||||
|
||||
// endregion
|
||||
+252
-462
@@ -1,34 +1,49 @@
|
||||
//! The module containing lowlevel operation involving file association.
|
||||
|
||||
use crate::win32::{concept, regext, utilities};
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error as TeError;
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::{
|
||||
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
|
||||
};
|
||||
use winreg::enums::{KEY_READ, KEY_WRITE};
|
||||
|
||||
// region: Error Type
|
||||
// region: Error and Result Types
|
||||
|
||||
/// Error occurs in this module.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum Error {
|
||||
#[error("can not perform this operation because lack essential privilege")]
|
||||
#[error("some lowlevel operations require escalating privilege")]
|
||||
NoPrivilege,
|
||||
#[error("given registry key is inexistant")]
|
||||
#[error("given lowlevel registry key is not presented")]
|
||||
InexistantKey,
|
||||
#[error("registry operation error: {0}")]
|
||||
|
||||
#[error("lowlevel registry operation error: {0}")]
|
||||
BadRegOp(#[from] std::io::Error),
|
||||
#[error("{0}")]
|
||||
#[error("unexpected blank registry key in lowlevel registry operation")]
|
||||
UnexpectedBlankKey(#[from] regext::BlankPathError),
|
||||
#[error("{0}")]
|
||||
|
||||
#[error("lowlevel operation error: {0}")]
|
||||
ExpandEnvVar(#[from] concept::ExpandEnvVarError),
|
||||
#[error("lowlevel operation error: {0}")]
|
||||
LoadIconRc(#[from] concept::LoadIconRcError),
|
||||
#[error("{0}")]
|
||||
#[error("lowlevel operation error: {0}")]
|
||||
LoadStrRc(#[from] concept::LoadStrRcError),
|
||||
#[error("lowlevel operation error: {0}")]
|
||||
ParseVerb(#[from] concept::ParseVerbError),
|
||||
#[error("lowlevel operation error: {0}")]
|
||||
ParseCmdLine(#[from] concept::ParseCmdLineError),
|
||||
#[error("lowlevel operation error: {0}")]
|
||||
ParseExt(#[from] concept::ParseExtError),
|
||||
}
|
||||
|
||||
/// The result type used in this module and all sub-modules.
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Scope and View
|
||||
// region: Exposed Stuff
|
||||
|
||||
// region: Scope
|
||||
|
||||
/// The scope where wfassoc will register and unregister.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
@@ -39,6 +54,34 @@ pub enum Scope {
|
||||
System,
|
||||
}
|
||||
|
||||
/// The error occurs when cast View into Scope.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("hybrid view can not be cast into any scope")]
|
||||
pub struct TryFromViewError {}
|
||||
|
||||
impl TryFromViewError {
|
||||
/// Creates a new `TryFromViewError`.
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<View> for Scope {
|
||||
type Error = TryFromViewError;
|
||||
|
||||
fn try_from(value: View) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
View::User => Ok(Self::User),
|
||||
View::System => Ok(Self::System),
|
||||
View::Hybrid => Err(TryFromViewError::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: View
|
||||
|
||||
/// The view when wfassoc querying infomations.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum View {
|
||||
@@ -51,17 +94,6 @@ pub enum View {
|
||||
Hybrid,
|
||||
}
|
||||
|
||||
/// The error occurs when cast View into Scope.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("hybrid view can not be cast into any scope")]
|
||||
pub struct TryFromViewError {}
|
||||
|
||||
impl TryFromViewError {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Scope> for View {
|
||||
fn from(value: Scope) -> Self {
|
||||
match value {
|
||||
@@ -71,29 +103,19 @@ impl From<Scope> for View {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<View> for Scope {
|
||||
type Error = TryFromViewError;
|
||||
|
||||
fn try_from(value: View) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
View::User => Ok(Self::User),
|
||||
View::System => Ok(Self::System),
|
||||
View::Hybrid => Err(TryFromViewError::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Losse Structs
|
||||
|
||||
// region: Losse ProgId
|
||||
|
||||
/// The enum representing a losse Programmatic Identifiers (ProgId).
|
||||
///
|
||||
/// In real world, not all software developers are willing to following Microsoft suggestions to use ProgId.
|
||||
/// They use string which do not have any regulation as ProgId.
|
||||
/// In real world, not all software developers are willing
|
||||
/// to following Microsoft suggestions to use ProgId.
|
||||
/// They frequently use string which do not have any regulation as ProgId.
|
||||
/// This enum is designed for handling this scenario.
|
||||
///
|
||||
/// You can utilize [FromStr] trait to build this struct.
|
||||
/// Standard ProgId and any casual string are both acceptable.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum LosseProgId {
|
||||
Plain(String),
|
||||
@@ -129,43 +151,64 @@ impl From<concept::ProgId> for LosseProgId {
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Losse Icon Reference String
|
||||
// region: Icon Resource Variant
|
||||
|
||||
/// The enum representing a losse Icon Reference String.
|
||||
///
|
||||
///
|
||||
/// In real usage, programmer can use Icon Reference String,
|
||||
/// or a plain string pointing to a icon file as the icon setting value.
|
||||
/// This enum is designed for handling this scenario.
|
||||
///
|
||||
/// You can utilize [FromStr] trait to build this struct.
|
||||
/// You can pass a standard Icon Reference String or a path to specific icon file.
|
||||
/// However, if you choose the last one, you must make sure
|
||||
/// that your path is must be unquoted, but expand string is allowed.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum LosseIconRefStr {
|
||||
pub enum IconResVariant {
|
||||
Plain(String),
|
||||
Strict(concept::IconRefStr),
|
||||
RefStr(concept::IconRefStr),
|
||||
}
|
||||
|
||||
impl LosseIconRefStr {
|
||||
pub fn extract(&self, kind: concept::IconSizeKind) -> Result<concept::IconRc, Error> {
|
||||
impl IconResVariant {
|
||||
/// Extracts the icon resource with the given size kind.
|
||||
pub fn extract(&self, kind: concept::IconSizeKind) -> Result<concept::IconRc> {
|
||||
// TODO:
|
||||
// Once we implement quote mechanism inside IconRefStr,
|
||||
// there is no need to preserve this unquote code in there.
|
||||
// However the expand string mechanism should be kept.
|
||||
let rc = match self {
|
||||
LosseIconRefStr::Plain(v) => concept::IconRc::with_ico_file(v.as_str(), kind)?,
|
||||
LosseIconRefStr::Strict(v) => concept::IconRc::new(v.get_path(), v.get_index(), kind)?,
|
||||
IconResVariant::Plain(v) => concept::IconRc::with_ico_file(v.as_str(), kind)?,
|
||||
IconResVariant::RefStr(v) => {
|
||||
// Try expand path part if possible
|
||||
let path_part = strip_quote(v.get_path());
|
||||
let path_part = match concept::ExpandString::new(path_part) {
|
||||
Ok(expand_string) => expand_string.expand()?,
|
||||
Err(_) => path_part.to_string(),
|
||||
};
|
||||
// Get index part.
|
||||
let index_part = v.get_index();
|
||||
// Resolve icon resource
|
||||
concept::IconRc::new(&path_part, index_part, kind)?
|
||||
}
|
||||
};
|
||||
Ok(rc)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LosseIconRefStr {
|
||||
impl Display for IconResVariant {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
LosseIconRefStr::Plain(v) => v.fmt(f),
|
||||
LosseIconRefStr::Strict(v) => v.fmt(f),
|
||||
IconResVariant::Plain(v) => v.fmt(f),
|
||||
IconResVariant::RefStr(v) => v.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for LosseIconRefStr {
|
||||
impl From<&str> for IconResVariant {
|
||||
fn from(s: &str) -> Self {
|
||||
// match it for standard IconRefStr first
|
||||
if let Ok(v) = concept::IconRefStr::from_str(s) {
|
||||
Self::Strict(v)
|
||||
Self::RefStr(v)
|
||||
} else {
|
||||
// fallback with other
|
||||
Self::Plain(s.to_string())
|
||||
@@ -173,56 +216,73 @@ impl From<&str> for LosseIconRefStr {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<concept::IconRefStr> for LosseIconRefStr {
|
||||
impl From<concept::IconRefStr> for IconResVariant {
|
||||
fn from(value: concept::IconRefStr) -> Self {
|
||||
Self::Strict(value)
|
||||
Self::RefStr(value)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Losse String Reference String
|
||||
// region: String Resource Variant
|
||||
|
||||
/// The enum representing a losse String Reference String.
|
||||
///
|
||||
///
|
||||
/// In real usage, programmer can use String Reference String,
|
||||
/// or a plain string as the string setting value.
|
||||
/// This enum is designed for handling this scenario.
|
||||
///
|
||||
/// You can utilize [FromStr] trait to build this struct.
|
||||
/// You can pass a standard String Reference String or a plain string.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum LosseStrRefStr {
|
||||
pub enum StrResVariant {
|
||||
Plain(String),
|
||||
Strict(concept::StrRefStr),
|
||||
RefStr(concept::StrRefStr),
|
||||
}
|
||||
|
||||
impl LosseStrRefStr {
|
||||
pub fn extract(&self) -> Result<String, Error> {
|
||||
impl StrResVariant {
|
||||
/// Resolves this string resource variant into a plain string.
|
||||
pub fn extract(&self) -> Result<String> {
|
||||
// TODO:
|
||||
// Once we implement quote mechanism inside StrRefStr,
|
||||
// there is no need to preserve this unquote code in there.
|
||||
// However the expand string mechanism should be kept.
|
||||
let rv = match self {
|
||||
// For plain string, we just simply clone it.
|
||||
LosseStrRefStr::Plain(v) => v.clone(),
|
||||
StrResVariant::Plain(v) => v.clone(),
|
||||
// For string reference string, we try to resolve it.
|
||||
LosseStrRefStr::Strict(v) => {
|
||||
let rc = concept::StrRc::new(v.get_path(), v.get_index())?;
|
||||
StrResVariant::RefStr(v) => {
|
||||
// Try expand path part if possible
|
||||
let path_part = strip_quote(v.get_path());
|
||||
let path_part = match concept::ExpandString::new(path_part) {
|
||||
Ok(expand_string) => expand_string.expand()?,
|
||||
Err(_) => path_part.to_string(),
|
||||
};
|
||||
// Get index part
|
||||
let index_part = v.get_index();
|
||||
// Resolve string resource
|
||||
let rc = concept::StrRc::new(&path_part, index_part)?;
|
||||
rc.into_string()
|
||||
},
|
||||
}
|
||||
};
|
||||
Ok(rv)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LosseStrRefStr {
|
||||
impl Display for StrResVariant {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
LosseStrRefStr::Plain(v) => v.fmt(f),
|
||||
LosseStrRefStr::Strict(v) => v.fmt(f),
|
||||
StrResVariant::Plain(v) => v.fmt(f),
|
||||
StrResVariant::RefStr(v) => v.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for LosseStrRefStr {
|
||||
impl From<&str> for StrResVariant {
|
||||
fn from(s: &str) -> Self {
|
||||
// match it for standard StrRefStr first
|
||||
if let Ok(v) = concept::StrRefStr::from_str(s) {
|
||||
Self::Strict(v)
|
||||
Self::RefStr(v)
|
||||
} else {
|
||||
// fallback with other
|
||||
Self::Plain(s.to_string())
|
||||
@@ -230,9 +290,43 @@ impl From<&str> for LosseStrRefStr {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<concept::StrRefStr> for LosseStrRefStr {
|
||||
impl From<concept::StrRefStr> for StrResVariant {
|
||||
fn from(value: concept::StrRefStr) -> Self {
|
||||
Self::Strict(value)
|
||||
Self::RefStr(value)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Shell Verb
|
||||
|
||||
/// The struct representing a shell verb pair.
|
||||
///
|
||||
/// Shell verb is frequently used in Windows to describe how to use an application.
|
||||
/// It can be used to define the default behavior of application,
|
||||
/// or the way to use this application for opening a specific file.
|
||||
///
|
||||
/// You can use [ShellVerb::new] with given verb and command line infos to build this struct.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ShellVerb {
|
||||
verb: concept::Verb,
|
||||
command: concept::CmdLine,
|
||||
}
|
||||
|
||||
impl ShellVerb {
|
||||
/// Creates a new `ShellVerb` from a verb and a command.
|
||||
pub fn new(verb: concept::Verb, command: concept::CmdLine) -> Self {
|
||||
Self { verb, command }
|
||||
}
|
||||
|
||||
/// Returns the verb of this shell verb pair.
|
||||
pub fn get_verb(&self) -> &concept::Verb {
|
||||
&self.verb
|
||||
}
|
||||
|
||||
/// Returns the command of this shell verb pair.
|
||||
pub fn get_command(&self) -> &concept::CmdLine {
|
||||
&self.command
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,12 +334,37 @@ impl From<concept::StrRefStr> for LosseStrRefStr {
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Utilities
|
||||
// region: Internal Stuff
|
||||
|
||||
/// The territory of opening key.
|
||||
// region: Opened Key
|
||||
|
||||
/// Internal used struct representing the result about opening registry key in given scope or view.
|
||||
#[derive(Debug)]
|
||||
struct OpenedKey {
|
||||
/// The parent key of opened key which must be presented.
|
||||
parent_key: RegKey,
|
||||
/// The opened key which may not be presented in registry.
|
||||
this_key: Option<RegKey>,
|
||||
}
|
||||
|
||||
impl OpenedKey {
|
||||
/// Creates a new `OpenedKey` with the given parent key and optional opened key.
|
||||
fn new(parent_key: RegKey, this_key: Option<RegKey>) -> Self {
|
||||
Self {
|
||||
parent_key,
|
||||
this_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Open Key Territory
|
||||
|
||||
/// Internal used enum representing the territory of opening registry key.
|
||||
///
|
||||
/// Scope and View will finally be converted into this type,
|
||||
/// and delivered to an uniform function to open key.
|
||||
/// [Scope] and [View] will finally be converted into this type,
|
||||
/// and delivered to an uniform function to open registry key.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum OpenKeyTerritory {
|
||||
System,
|
||||
@@ -272,7 +391,11 @@ impl From<View> for OpenKeyTerritory {
|
||||
}
|
||||
}
|
||||
|
||||
/// The purpose of opening this key.
|
||||
// endregion
|
||||
|
||||
// region: Open Key Purpose
|
||||
|
||||
/// The purpose of opening given registry key.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum OpenKeyPurpose {
|
||||
/// Only read something.
|
||||
@@ -281,9 +404,30 @@ enum OpenKeyPurpose {
|
||||
ReadWrite,
|
||||
}
|
||||
|
||||
impl OpenKeyPurpose {
|
||||
/// Converts this purpose to the corresponding registry permission flags.
|
||||
fn to_permission(&self) -> u32 {
|
||||
match self {
|
||||
OpenKeyPurpose::Read => PERM_R,
|
||||
OpenKeyPurpose::ReadWrite => PERM_RW,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Permission Constants
|
||||
|
||||
/// The read-only permission when opening Windows Registry.
|
||||
const PERM_R: u32 = KEY_READ;
|
||||
/// The read-write permission when opening Windows Registry.
|
||||
const PERM_RW: u32 = KEY_READ | KEY_WRITE;
|
||||
|
||||
// endregion
|
||||
|
||||
/// Check whether we have enough privilege when operating in given territory and purpose.
|
||||
/// If we have, simply return, otherwise return error.
|
||||
fn check_privilege(territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<(), Error> {
|
||||
fn check_privilege(territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<()> {
|
||||
match purpose {
|
||||
// Read is okey for every territory.
|
||||
OpenKeyPurpose::Read => Ok(()),
|
||||
@@ -303,400 +447,46 @@ fn check_privilege(territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Resu
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal used struct representing the result of opening scope or view.
|
||||
#[derive(Debug)]
|
||||
struct OpenedKey {
|
||||
/// The parent key of opened key which must be presented.
|
||||
parent_key: RegKey,
|
||||
/// The opened key which may not be presented in registry.
|
||||
this_key: Option<RegKey>,
|
||||
}
|
||||
// TODO:
|
||||
// This function should be eliminated or moved to another place,
|
||||
// once we finish the quote mechanism in StrRefStr and IconRefStr.
|
||||
|
||||
impl OpenedKey {
|
||||
fn new(parent_key: RegKey, this_key: Option<RegKey>) -> Self {
|
||||
Self {
|
||||
parent_key,
|
||||
this_key,
|
||||
}
|
||||
/// Internal function removing quote pair if possible.
|
||||
///
|
||||
/// # Todo
|
||||
///
|
||||
/// This function actually is the compromise about the current design of this crate.
|
||||
/// This function will be optimized in future.
|
||||
///
|
||||
/// In some cases, the path part of [concept::IconRefStr] or [concept::StrRefStr] is quoted by quote.
|
||||
/// This can no be recognized by Win32 functions.
|
||||
/// So in this case, we should remove this quote pair.
|
||||
fn strip_quote<'a>(s: &'a str) -> &'a str {
|
||||
let bytes = s.as_bytes();
|
||||
if bytes.len() >= 2 && bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"' {
|
||||
// Because quote is 1 byte in UTF8, we can safely slice it,
|
||||
// without worry about panic.
|
||||
&s[1..s.len() - 1]
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: App Paths Key
|
||||
// region: Registry Keys
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct AppPathsKey {
|
||||
key_name: concept::FileName,
|
||||
}
|
||||
// YYC MARK:
|
||||
// Load submodules and re-export them.
|
||||
|
||||
impl AppPathsKey {
|
||||
pub fn new(inner: concept::FileName) -> Self {
|
||||
Self { key_name: inner }
|
||||
}
|
||||
mod app_path_key;
|
||||
mod applications_key;
|
||||
mod ext_key;
|
||||
mod progid_key;
|
||||
|
||||
pub fn inner(&self) -> &concept::FileName {
|
||||
&self.key_name
|
||||
}
|
||||
}
|
||||
|
||||
impl AppPathsKey {
|
||||
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
|
||||
|
||||
fn open_key(
|
||||
&self,
|
||||
territory: OpenKeyTerritory,
|
||||
purpose: OpenKeyPurpose,
|
||||
) -> Result<OpenedKey, Error> {
|
||||
// check privilege
|
||||
check_privilege(territory, purpose)?;
|
||||
// Fetch the permission
|
||||
let perms = match purpose {
|
||||
OpenKeyPurpose::Read => KEY_READ,
|
||||
OpenKeyPurpose::ReadWrite => KEY_READ | KEY_WRITE,
|
||||
};
|
||||
|
||||
// get the root key
|
||||
let hk = match territory {
|
||||
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
|
||||
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
||||
// There is no way that territory is hybrid.
|
||||
OpenKeyTerritory::Hybrid => panic!("unexpected hybrid key territory"),
|
||||
};
|
||||
// navigate to App Paths
|
||||
let app_paths = hk.open_subkey_with_flags(Self::APP_PATHS, perms)?;
|
||||
// open file name key if possible
|
||||
let this_app = regext::try_open_subkey_with_flags(
|
||||
&app_paths,
|
||||
regext::blank_path_guard(self.key_name.inner())?,
|
||||
perms,
|
||||
)?;
|
||||
|
||||
// okey
|
||||
Ok(OpenedKey::new(app_paths, this_app))
|
||||
}
|
||||
|
||||
fn open_scope_for_read(&self, scope: Scope) -> Result<OpenedKey, Error> {
|
||||
self.open_key(scope.into(), OpenKeyPurpose::Read)
|
||||
}
|
||||
|
||||
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey, Error> {
|
||||
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
|
||||
}
|
||||
|
||||
pub fn is_exist(&self, scope: Scope) -> Result<bool, Error> {
|
||||
let key = self.open_scope_for_read(scope)?.this_key;
|
||||
Ok(key.is_some())
|
||||
}
|
||||
|
||||
/// Ensure this application key is presented in App Paths.
|
||||
///
|
||||
/// 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, Error> {
|
||||
let key = self.open_scope_for_write(scope)?;
|
||||
if let None = key.this_key {
|
||||
let _ = key.parent_key.create_subkey_with_flags(
|
||||
regext::blank_path_guard(self.key_name.inner())?,
|
||||
KEY_READ | KEY_WRITE,
|
||||
)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete this application key from App Paths.
|
||||
///
|
||||
/// If there is no such key in App Paths,
|
||||
/// this function does nothing.
|
||||
pub fn delete(&mut self, scope: Scope) -> Result<(), Error> {
|
||||
let key = self.open_scope_for_write(scope)?;
|
||||
key.parent_key
|
||||
.delete_subkey_all(regext::blank_path_guard(self.key_name.inner())?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_scope_for_getter(&self, scope: Scope) -> Result<RegKey, Error> {
|
||||
self.open_scope_for_read(scope)?
|
||||
.this_key
|
||||
.ok_or(Error::InexistantKey)
|
||||
}
|
||||
|
||||
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey, Error> {
|
||||
self.open_scope_for_write(scope)?
|
||||
.this_key
|
||||
.ok_or(Error::InexistantKey)
|
||||
}
|
||||
|
||||
const NAMEOF_PATH_TO_APPLICATION: &str = "";
|
||||
|
||||
pub fn get_path_to_application(&self, scope: Scope) -> Result<String, Error> {
|
||||
let key = self.open_scope_for_getter(scope)?;
|
||||
Ok(key.get_value(Self::NAMEOF_PATH_TO_APPLICATION)?)
|
||||
}
|
||||
|
||||
pub fn set_path_to_application(&mut self, scope: Scope, value: &str) -> Result<(), Error> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
key.set_value(Self::NAMEOF_PATH_TO_APPLICATION, &value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const NAMEOF_APPLICATION_DIRECTORY: &str = "Path";
|
||||
|
||||
pub fn get_application_directory(&self, scope: Scope) -> Result<String, Error> {
|
||||
let key = self.open_scope_for_getter(scope)?;
|
||||
Ok(key.get_value(Self::NAMEOF_APPLICATION_DIRECTORY)?)
|
||||
}
|
||||
|
||||
pub fn set_application_directory(&mut self, scope: Scope, value: &str) -> Result<(), Error> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
key.set_value(Self::NAMEOF_APPLICATION_DIRECTORY, &value)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Applications Key
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ApplicationsKey {
|
||||
key_name: concept::FileName,
|
||||
}
|
||||
|
||||
impl ApplicationsKey {
|
||||
pub fn new(inner: concept::FileName) -> Self {
|
||||
Self { key_name: inner }
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &concept::FileName {
|
||||
&self.key_name
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationsKey {
|
||||
const FULL_APPLICATIONS: &str = "Software\\Classes\\Applications";
|
||||
const PARTIAL_APPLICATIONS: &str = "Applications";
|
||||
|
||||
fn open_key(
|
||||
&self,
|
||||
territory: OpenKeyTerritory,
|
||||
purpose: OpenKeyPurpose,
|
||||
) -> Result<OpenedKey, Error> {
|
||||
// check privilege
|
||||
check_privilege(territory, purpose)?;
|
||||
// Fetch the permission
|
||||
let perms = match purpose {
|
||||
OpenKeyPurpose::Read => KEY_READ,
|
||||
OpenKeyPurpose::ReadWrite => KEY_READ | KEY_WRITE,
|
||||
};
|
||||
|
||||
// get the root key
|
||||
let hk = match territory {
|
||||
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
|
||||
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
||||
OpenKeyTerritory::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
|
||||
};
|
||||
let applications = match territory {
|
||||
OpenKeyTerritory::User | OpenKeyTerritory::System => {
|
||||
hk.open_subkey_with_flags(Self::FULL_APPLICATIONS, perms)?
|
||||
}
|
||||
OpenKeyTerritory::Hybrid => {
|
||||
hk.open_subkey_with_flags(Self::PARTIAL_APPLICATIONS, perms)?
|
||||
}
|
||||
};
|
||||
// open app key if possible
|
||||
let this_app = regext::try_open_subkey_with_flags(
|
||||
&applications,
|
||||
regext::blank_path_guard(self.key_name.inner())?,
|
||||
perms,
|
||||
)?;
|
||||
// okey
|
||||
Ok(OpenedKey::new(applications, this_app))
|
||||
}
|
||||
|
||||
fn open_view_for_read(&self, view: View) -> Result<OpenedKey, Error> {
|
||||
self.open_key(view.into(), OpenKeyPurpose::Read)
|
||||
}
|
||||
|
||||
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey, Error> {
|
||||
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
|
||||
}
|
||||
|
||||
pub fn is_exist(&self, view: View) -> Result<bool, Error> {
|
||||
let key = self.open_view_for_read(view)?.this_key;
|
||||
Ok(key.is_some())
|
||||
}
|
||||
|
||||
pub fn ensure(&mut self, scope: Scope) -> Result<bool, Error> {
|
||||
let key = self.open_scope_for_write(scope)?;
|
||||
if let None = key.this_key {
|
||||
let _ = key.parent_key.create_subkey_with_flags(
|
||||
regext::blank_path_guard(self.key_name.inner())?,
|
||||
KEY_READ | KEY_WRITE,
|
||||
)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, scope: Scope) -> Result<(), Error> {
|
||||
let key = self.open_scope_for_write(scope)?;
|
||||
key.parent_key
|
||||
.delete_subkey_all(regext::blank_path_guard(self.key_name.inner())?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_view_for_getter(&self, view: View) -> Result<RegKey, Error> {
|
||||
self.open_view_for_read(view)?
|
||||
.this_key
|
||||
.ok_or(Error::InexistantKey)
|
||||
}
|
||||
|
||||
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey, Error> {
|
||||
self.open_scope_for_write(scope)?
|
||||
.this_key
|
||||
.ok_or(Error::InexistantKey)
|
||||
}
|
||||
|
||||
const NAMEOF_SHELL_VERB_PART1: &str = "shell";
|
||||
const NAMEOF_SHELL_VERB_PART3: &str = "command";
|
||||
|
||||
// TODO:
|
||||
// We temporarily use String and &str as the command line argument parameter.
|
||||
// We may introduce a new complete Rust struct for replacing this arbitrary string.
|
||||
|
||||
pub fn get_shell_verb(&self, view: View) -> Result<(concept::Verb, String), Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn set_shell_verb(
|
||||
&mut self,
|
||||
scope: Scope,
|
||||
beh: (concept::Verb, &str),
|
||||
) -> Result<(), Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
const NAMEOF_DEFAULT_ICON: &str = "DefaultIcon";
|
||||
|
||||
pub fn get_default_icon(&self, view: View) -> Result<Option<LosseIconRefStr>, Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn set_default_icon(&self, scope: Scope, icon: Option<&LosseIconRefStr>) -> Result<(), Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
const NAMEOF_FRIENDLY_APP_NAME: &str = "FriendlyAppName";
|
||||
|
||||
pub fn get_friendly_app_name(&self, view: View) -> Result<Option<LosseStrRefStr>, Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn set_friendly_app_name(&self, scope: Scope, name: Option<&LosseStrRefStr>) -> Result<(), Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
const NAMEOF_SUPPORTED_TYPES: &str = "SupportedTypes";
|
||||
|
||||
pub fn get_supported_types(&self, view: View) -> Result<Option<Vec<concept::Ext>>, Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn set_supported_types(&self, scope: Scope, tys: Option<&[concept::Ext]>) -> Result<(), Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
const NAMEOF_NO_OPEN_WITH: &str = "NoOpenWith";
|
||||
|
||||
pub fn get_no_open_with(&self, view: View) -> Result<bool, Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn set_no_open_with(&self, scope: Scope, flag: bool) -> Result<(), Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: File Extension Key
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ExtKey {
|
||||
ext: concept::Ext,
|
||||
}
|
||||
|
||||
impl ExtKey {
|
||||
pub fn new(inner: concept::Ext) -> Self {
|
||||
Self { ext: inner }
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &concept::Ext {
|
||||
&self.ext
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtKey {
|
||||
const FULL_CLASSES: &str = "Software\\Classes";
|
||||
const PARTIAL_CLASSES: &str = "";
|
||||
|
||||
fn open_key(
|
||||
&self,
|
||||
territory: OpenKeyTerritory,
|
||||
purpose: OpenKeyPurpose,
|
||||
) -> Result<OpenedKey, Error> {
|
||||
// check privilege
|
||||
check_privilege(territory, purpose)?;
|
||||
// Fetch the permission
|
||||
let perms = match purpose {
|
||||
OpenKeyPurpose::Read => KEY_READ,
|
||||
OpenKeyPurpose::ReadWrite => KEY_READ | KEY_WRITE,
|
||||
};
|
||||
|
||||
// navigate to extension container
|
||||
let hk = match territory {
|
||||
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
|
||||
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
||||
OpenKeyTerritory::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
|
||||
};
|
||||
let classes = match territory {
|
||||
OpenKeyTerritory::User | OpenKeyTerritory::System => {
|
||||
hk.open_subkey_with_flags(Self::FULL_CLASSES, perms)?
|
||||
}
|
||||
OpenKeyTerritory::Hybrid => hk.open_subkey_with_flags(Self::PARTIAL_CLASSES, perms)?,
|
||||
};
|
||||
// open extension key if possible
|
||||
let this_ext = regext::try_open_subkey_with_flags(
|
||||
&classes,
|
||||
regext::blank_path_guard(self.ext.dotted_inner())?,
|
||||
perms,
|
||||
)?;
|
||||
// okey
|
||||
Ok(OpenedKey::new(classes, this_ext))
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: ProgId Key
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ProgIdKey {
|
||||
progid: LosseProgId,
|
||||
}
|
||||
|
||||
impl ProgIdKey {
|
||||
pub fn new(inner: LosseProgId) -> Self {
|
||||
Self { progid: inner }
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &LosseProgId {
|
||||
&self.progid
|
||||
}
|
||||
}
|
||||
pub use app_path_key::AppPathsKey;
|
||||
pub use applications_key::ApplicationsKey;
|
||||
pub use ext_key::ExtKey;
|
||||
pub use progid_key::ProgIdKey;
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
use super::{
|
||||
Error, OpenKeyPurpose, OpenKeyTerritory, OpenedKey, PERM_RW, Result, Scope, check_privilege,
|
||||
};
|
||||
use crate::win32::{concept, regext};
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
|
||||
|
||||
/// A registry key wrapper for `Software\Microsoft\Windows\CurrentVersion\App Paths\<APP>`.
|
||||
///
|
||||
/// This key can only be opened under a Scope (not a View), since
|
||||
/// "App Paths" only exists under HKCU and HKLM, not under a merged HKCR view.
|
||||
///
|
||||
/// This struct provides [Self::is_exist], [Self::ensure] and [Self::delete] to
|
||||
/// check whether this key is in Registry, make sure this key is presneted in Registry,
|
||||
/// and delete self from Registry respectively.
|
||||
///
|
||||
/// And there are some getter and setter in this struct,
|
||||
/// before calling them, you must use [Self::ensure] make sure that this key is presented in Registry,
|
||||
/// otherwise these functions will return errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct AppPathsKey {
|
||||
key_name: concept::FileName,
|
||||
}
|
||||
|
||||
impl AppPathsKey {
|
||||
/// Creates a new `AppPathsKey` with the given file name.
|
||||
pub fn new(inner: concept::FileName) -> Self {
|
||||
Self { key_name: inner }
|
||||
}
|
||||
|
||||
/// Returns the inner file name of this application path key.
|
||||
pub fn inner(&self) -> &concept::FileName {
|
||||
&self.key_name
|
||||
}
|
||||
}
|
||||
|
||||
impl AppPathsKey {
|
||||
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
|
||||
|
||||
/// Attempts to open the registry key for this application path with the given territory and purpose.
|
||||
fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
|
||||
// check privilege
|
||||
check_privilege(territory, purpose)?;
|
||||
// Fetch the permission
|
||||
let perms = purpose.to_permission();
|
||||
|
||||
// get the root key
|
||||
let hk = match territory {
|
||||
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
|
||||
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
||||
// There is no way that territory is hybrid.
|
||||
OpenKeyTerritory::Hybrid => panic!("unexpected hybrid key territory"),
|
||||
};
|
||||
// navigate to App Paths
|
||||
let app_paths = hk.open_subkey_with_flags(Self::APP_PATHS, perms)?;
|
||||
// open file name key if possible
|
||||
let this_app = regext::try_open_subkey_with_flags(
|
||||
&app_paths,
|
||||
regext::blank_path_guard(self.key_name.inner())?,
|
||||
perms,
|
||||
)?;
|
||||
|
||||
// okey
|
||||
Ok(OpenedKey::new(app_paths, this_app))
|
||||
}
|
||||
|
||||
/// Opens the registry key for read-only access under the given scope.
|
||||
fn open_scope_for_read(&self, scope: Scope) -> Result<OpenedKey> {
|
||||
self.open_key(scope.into(), OpenKeyPurpose::Read)
|
||||
}
|
||||
|
||||
/// Opens the registry key for read-write access under the given scope.
|
||||
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
|
||||
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
|
||||
}
|
||||
|
||||
/// Checks whether this application path key exists in the registry under the given scope.
|
||||
/// Return true if it is presented in registry, otherwise false.
|
||||
pub fn is_exist(&self, scope: Scope) -> Result<bool> {
|
||||
let key = self.open_scope_for_read(scope)?.this_key;
|
||||
Ok(key.is_some())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
|
||||
let key = self.open_scope_for_write(scope)?;
|
||||
if let None = key.this_key {
|
||||
let _ = key.parent_key.create_subkey_with_flags(
|
||||
regext::blank_path_guard(self.key_name.inner())?,
|
||||
PERM_RW,
|
||||
)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete this application key from App Paths 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)?;
|
||||
Ok(regext::arbitrarily_delete_subkey_all(
|
||||
&key.parent_key,
|
||||
regext::blank_path_guard(self.key_name.inner())?,
|
||||
)?)
|
||||
}
|
||||
|
||||
// YYC MARK:
|
||||
// Reference: <https://learn.microsoft.com/en-us/windows/win32/shell/app-registration#using-the-app-paths-subkey>
|
||||
|
||||
// TODO:
|
||||
// We only support these keys in there because current interface are enough to use.
|
||||
// We may expand these in future.
|
||||
|
||||
/// Opens the registry key for getter reading, returning an error if the key does not exist.
|
||||
fn open_scope_for_getter(&self, scope: Scope) -> Result<RegKey> {
|
||||
self.open_scope_for_read(scope)?
|
||||
.this_key
|
||||
.ok_or(Error::InexistantKey)
|
||||
}
|
||||
|
||||
/// Opens the registry key for setter writing, returning an error if the key does not exist.
|
||||
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
|
||||
self.open_scope_for_write(scope)?
|
||||
.this_key
|
||||
.ok_or(Error::InexistantKey)
|
||||
}
|
||||
|
||||
const NAMEOF_DEFAULT: &str = "";
|
||||
|
||||
/// Gets the default value of this application path key.
|
||||
///
|
||||
/// The content of this key should point to the fully qualified path of the application.
|
||||
pub fn get_default(&self, scope: Scope) -> Result<String> {
|
||||
let key = self.open_scope_for_getter(scope)?;
|
||||
Ok(key.get_value(Self::NAMEOF_DEFAULT)?)
|
||||
}
|
||||
|
||||
/// Sets the default value of this application path key.
|
||||
pub fn set_default(&mut self, scope: Scope, value: &str) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
key.set_value(Self::NAMEOF_DEFAULT, &value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// This key may be REG_SZ or REG_EXPAND_SZ.
|
||||
// Currently we see them as one type.
|
||||
// This should be improved in future.
|
||||
|
||||
const NAMEOF_PATH: &str = "Path";
|
||||
|
||||
/// Gets the `Path` value of this application path key.
|
||||
///
|
||||
/// The content of this key will be added to PATH environment variable,
|
||||
/// to enable that application can find its dependencies conveniently.
|
||||
/// If it contains multiple pathes, semicolon-separated form is required.
|
||||
/// Usually this is the path to application directory.
|
||||
pub fn get_path(&self, scope: Scope) -> Result<String> {
|
||||
let key = self.open_scope_for_getter(scope)?;
|
||||
Ok(key.get_value(Self::NAMEOF_PATH)?)
|
||||
}
|
||||
|
||||
/// Sets the `Path` value of this application path key.
|
||||
pub fn set_path(&mut self, scope: Scope, value: &str) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
key.set_value(Self::NAMEOF_PATH, &value)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
use super::{
|
||||
Error, IconResVariant, OpenKeyPurpose, OpenKeyTerritory, OpenedKey, PERM_R, PERM_RW, Result,
|
||||
Scope, ShellVerb, StrResVariant, View, check_privilege,
|
||||
};
|
||||
use crate::win32::{concept, regext};
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
|
||||
|
||||
/// A registry key wrapper for `Software\Classes\Applications\<APP>`.
|
||||
///
|
||||
/// This struct provides [Self::is_exist], [Self::ensure] and [Self::delete] to
|
||||
/// check whether this key is in Registry, make sure this key is presneted in Registry,
|
||||
/// and delete self from Registry respectively.
|
||||
///
|
||||
/// And there are some getter and setter in this struct,
|
||||
/// before calling them, you must use [Self::ensure] make sure that this key is presented in Registry,
|
||||
/// otherwise these functions will return errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ApplicationsKey {
|
||||
key_name: concept::FileName,
|
||||
}
|
||||
|
||||
impl ApplicationsKey {
|
||||
/// Creates a new `ApplicationsKey` with the given file name.
|
||||
pub fn new(inner: concept::FileName) -> Self {
|
||||
Self { key_name: inner }
|
||||
}
|
||||
|
||||
/// Returns the inner file name of this applications key.
|
||||
pub fn inner(&self) -> &concept::FileName {
|
||||
&self.key_name
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationsKey {
|
||||
const FULL_APPLICATIONS: &str = "Software\\Classes\\Applications";
|
||||
const PARTIAL_APPLICATIONS: &str = "Applications";
|
||||
|
||||
/// Attempts to open the registry key for this application with the given territory and purpose.
|
||||
fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
|
||||
// check privilege
|
||||
check_privilege(territory, purpose)?;
|
||||
// Fetch the permission
|
||||
let perms = purpose.to_permission();
|
||||
|
||||
// get the root key
|
||||
let hk = match territory {
|
||||
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
|
||||
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
||||
OpenKeyTerritory::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
|
||||
};
|
||||
let applications = match territory {
|
||||
OpenKeyTerritory::User | OpenKeyTerritory::System => {
|
||||
hk.open_subkey_with_flags(Self::FULL_APPLICATIONS, perms)?
|
||||
}
|
||||
OpenKeyTerritory::Hybrid => {
|
||||
hk.open_subkey_with_flags(Self::PARTIAL_APPLICATIONS, perms)?
|
||||
}
|
||||
};
|
||||
// open app key if possible
|
||||
let this_app = regext::try_open_subkey_with_flags(
|
||||
&applications,
|
||||
regext::blank_path_guard(self.key_name.inner())?,
|
||||
perms,
|
||||
)?;
|
||||
// okey
|
||||
Ok(OpenedKey::new(applications, this_app))
|
||||
}
|
||||
|
||||
/// Opens the registry key for read-only access under the given view.
|
||||
fn open_view_for_read(&self, view: View) -> Result<OpenedKey> {
|
||||
self.open_key(view.into(), OpenKeyPurpose::Read)
|
||||
}
|
||||
|
||||
/// Opens the registry key for read-write access under the given scope.
|
||||
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
|
||||
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
|
||||
}
|
||||
|
||||
/// Checks whether this applications key exists in the registry under the given view.
|
||||
/// Return true if it is presented in registry, otherwise false.
|
||||
pub fn is_exist(&self, view: View) -> Result<bool> {
|
||||
let key = self.open_view_for_read(view)?.this_key;
|
||||
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 {
|
||||
let _ = key.parent_key.create_subkey_with_flags(
|
||||
regext::blank_path_guard(self.key_name.inner())?,
|
||||
PERM_RW,
|
||||
)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
Ok(regext::arbitrarily_delete_subkey_all(
|
||||
&key.parent_key,
|
||||
regext::blank_path_guard(self.key_name.inner())?,
|
||||
)?)
|
||||
}
|
||||
|
||||
// YYC MARK:
|
||||
// Reference: <https://learn.microsoft.com/en-us/windows/win32/shell/app-registration#using-the-applications-subkey>
|
||||
|
||||
// TODO:
|
||||
// We only support these keys in there because current interface are enough to use.
|
||||
// We may expand these in future.
|
||||
|
||||
/// Opens the registry key for getter reading, returning an error if the key does not exist.
|
||||
fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
|
||||
self.open_view_for_read(view)?
|
||||
.this_key
|
||||
.ok_or(Error::InexistantKey)
|
||||
}
|
||||
|
||||
/// Opens the registry key for setter writing, returning an error if the key does not exist.
|
||||
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
|
||||
self.open_scope_for_write(scope)?
|
||||
.this_key
|
||||
.ok_or(Error::InexistantKey)
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// Support mutiple ShellVerb in future.
|
||||
|
||||
const NAMEOF_SHELL_VERB_PART1: &str = "shell";
|
||||
const NAMEOF_SHELL_VERB_PART3: &str = "command";
|
||||
const NAMEOF_SHELL_VERB_PART4: &str = "";
|
||||
|
||||
/// Gets the shell verb registered for this application under the given view.
|
||||
/// Return `None` if there is no any shell verb.
|
||||
///
|
||||
/// # Todo
|
||||
///
|
||||
/// Currently we only support single shell verb.
|
||||
/// So if there is multiple shell verb, this function still return `None`.
|
||||
/// This issue will be resolved in future.
|
||||
pub fn get_shell_verb(&self, view: View) -> Result<Option<ShellVerb>> {
|
||||
let key = self.open_view_for_getter(view)?;
|
||||
|
||||
// Get shell subkey
|
||||
let shell_key = match regext::try_open_subkey_with_flags(
|
||||
&key,
|
||||
Self::NAMEOF_SHELL_VERB_PART1,
|
||||
PERM_R,
|
||||
)? {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
// Get verb subkey name, then get subkey itself.
|
||||
let verb_key_name = match regext::get_sole_subkey_name(&shell_key)? {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let verb_key = match regext::try_open_subkey_with_flags(
|
||||
&shell_key,
|
||||
regext::blank_path_guard(&verb_key_name)?,
|
||||
PERM_R,
|
||||
)? {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
// Get command subkey.
|
||||
let command_key = match regext::try_open_subkey_with_flags(
|
||||
&verb_key,
|
||||
Self::NAMEOF_SHELL_VERB_PART3,
|
||||
PERM_R,
|
||||
)? {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
// Get the default value of command subkey
|
||||
let command_default_value = match regext::try_get_value::<String, _>(
|
||||
&command_key,
|
||||
Self::NAMEOF_SHELL_VERB_PART4,
|
||||
)? {
|
||||
Some(value) => value,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Okey, return value.
|
||||
Ok(Some(ShellVerb::new(
|
||||
verb_key_name.parse()?,
|
||||
command_default_value.parse()?,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Sets the shell verb for this application under the given scope.
|
||||
/// Pass `None` as shell verb to remove the shell verb.
|
||||
///
|
||||
/// # Todo
|
||||
///
|
||||
/// Currently we only support single shell verb.
|
||||
/// So if there is multiple shell verb, all of them will be deleted at first,
|
||||
/// and then write your given shell verb inside it.
|
||||
/// This issue will be resolved in future.
|
||||
pub fn set_shell_verb(&mut self, scope: Scope, sv: Option<&ShellVerb>) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
|
||||
match sv {
|
||||
Some(sv) => {
|
||||
// Create shell subkey
|
||||
let (shell_key, _) =
|
||||
key.create_subkey_with_flags(Self::NAMEOF_SHELL_VERB_PART1, PERM_RW)?;
|
||||
// Create verb key
|
||||
let (verb_key, _) =
|
||||
shell_key.create_subkey_with_flags(sv.get_verb().inner(), PERM_RW)?;
|
||||
// Create command key
|
||||
let (command_key, _) =
|
||||
verb_key.create_subkey_with_flags(Self::NAMEOF_SHELL_VERB_PART3, PERM_RW)?;
|
||||
// Set command key default value
|
||||
command_key.set_value(Self::NAMEOF_SHELL_VERB_PART4, &sv.get_command().full())?;
|
||||
}
|
||||
None => {
|
||||
// Delete shell and its all subkey.
|
||||
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SHELL_VERB_PART1)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const NAMEOF_DEFAULT_ICON_PART1: &str = "DefaultIcon";
|
||||
const NAMEOF_DEFAULT_ICON_PART2: &str = "";
|
||||
|
||||
/// Gets the default icon registered for this application under the given view.
|
||||
/// Return `None` if there is no such set default icon.
|
||||
///
|
||||
/// This icon will be used to represent the application instead of the first icon stored in the .exe file.
|
||||
pub fn get_default_icon(&self, view: View) -> Result<Option<IconResVariant>> {
|
||||
let key = self.open_view_for_getter(view)?;
|
||||
// Get default icon subkey
|
||||
let default_icon_key = match regext::try_open_subkey_with_flags(
|
||||
&key,
|
||||
Self::NAMEOF_DEFAULT_ICON_PART1,
|
||||
PERM_R,
|
||||
)? {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
// Get the default value of default icon subkey
|
||||
let default_icon_default_value =
|
||||
regext::try_get_value::<String, _>(&default_icon_key, Self::NAMEOF_DEFAULT_ICON_PART2)?;
|
||||
// Transform it as result
|
||||
Ok(default_icon_default_value.map(|v| IconResVariant::from(v.as_str())))
|
||||
}
|
||||
|
||||
/// Sets the default icon for this application under the given scope.
|
||||
/// Pass `None` as icon to remove the default icon.
|
||||
pub fn set_default_icon(&mut self, scope: Scope, icon: Option<&IconResVariant>) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
|
||||
match icon {
|
||||
Some(icon) => {
|
||||
// Create default icon subkey
|
||||
let (default_icon_key, _) =
|
||||
key.create_subkey_with_flags(Self::NAMEOF_DEFAULT_ICON_PART1, PERM_RW)?;
|
||||
// Set default value of default icon subkey.
|
||||
default_icon_key.set_value(Self::NAMEOF_DEFAULT_ICON_PART2, &icon.to_string())?;
|
||||
}
|
||||
None => {
|
||||
// Delete shell and its all subkey.
|
||||
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_DEFAULT_ICON_PART1)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const NAMEOF_FRIENDLY_APP_NAME: &str = "FriendlyAppName";
|
||||
|
||||
/// Gets the friendly app name registered for this application under the given view.
|
||||
/// Return `None` if there is no such set friendly app name.
|
||||
///
|
||||
/// This name will be used to display for an application instead of just the version information appearing.
|
||||
pub fn get_friendly_app_name(&self, view: View) -> Result<Option<StrResVariant>> {
|
||||
let key = self.open_view_for_getter(view)?;
|
||||
// Get value of it
|
||||
let value = regext::try_get_value::<String, _>(&key, Self::NAMEOF_FRIENDLY_APP_NAME)?;
|
||||
// Transform it as result
|
||||
Ok(value.map(|v| StrResVariant::from(v.as_str())))
|
||||
}
|
||||
|
||||
/// Sets the friendly app name for this application under the given scope.
|
||||
/// Pass `None` to remove the friendly app name value.
|
||||
pub fn set_friendly_app_name(
|
||||
&mut self,
|
||||
scope: Scope,
|
||||
name: Option<&StrResVariant>,
|
||||
) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
|
||||
match name {
|
||||
Some(name) => {
|
||||
// Set value for this key
|
||||
key.set_value(Self::NAMEOF_FRIENDLY_APP_NAME, &name.to_string())?;
|
||||
}
|
||||
None => {
|
||||
// Delete this key
|
||||
regext::arbitrarily_delete_value(&key, Self::NAMEOF_FRIENDLY_APP_NAME)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const NAMEOF_SUPPORTED_TYPES: &str = "SupportedTypes";
|
||||
|
||||
/// Gets the supported file types registered for this application under the given view.
|
||||
/// Return `None` if application doesn't set this (because this is suggested but not forced).
|
||||
///
|
||||
/// This is a list defining the file types that the application supports.
|
||||
pub fn get_supported_types(&self, view: View) -> Result<Option<Vec<concept::Ext>>> {
|
||||
let key = self.open_view_for_getter(view)?;
|
||||
// Get supported types subkey
|
||||
let supported_types_key =
|
||||
match regext::try_open_subkey_with_flags(&key, Self::NAMEOF_SUPPORTED_TYPES, PERM_R)? {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
// Fetch all sub-values
|
||||
let key_names = regext::get_all_string_subkey_names(&supported_types_key)?;
|
||||
// Map the result
|
||||
let exts = key_names
|
||||
.into_iter()
|
||||
.map(|name| name.parse::<concept::Ext>())
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
// Return value
|
||||
Ok(Some(exts))
|
||||
}
|
||||
|
||||
/// Sets the supported file types for this application under the given scope.
|
||||
/// Pass `None` to remove this list.
|
||||
pub fn set_supported_types(
|
||||
&mut self,
|
||||
scope: Scope,
|
||||
tys: Option<&[&concept::Ext]>,
|
||||
) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
|
||||
match tys {
|
||||
Some(tys) => {
|
||||
// Create supported types key
|
||||
let (supported_types_key, _) =
|
||||
key.create_subkey_with_flags(Self::NAMEOF_SUPPORTED_TYPES, PERM_RW)?;
|
||||
// Clean all contents of this key
|
||||
regext::clean_all_contents(&supported_types_key)?;
|
||||
// Add file types one by one
|
||||
for ty in tys {
|
||||
supported_types_key
|
||||
.set_value(regext::blank_path_guard(ty.dotted_inner())?, &"")?;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Delete this subkey.
|
||||
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SUPPORTED_TYPES)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const NAMEOF_NO_OPEN_WITH: &str = "NoOpenWith";
|
||||
|
||||
/// Checks whether the "NoOpenWith" flag is set for this application under the given view.
|
||||
pub fn get_no_open_with(&self, view: View) -> Result<bool> {
|
||||
let key = self.open_view_for_getter(view)?;
|
||||
match regext::try_get_value::<String, _>(&key, Self::NAMEOF_NO_OPEN_WITH)? {
|
||||
Some(_) => Ok(true),
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets or clears the "NoOpenWith" flag for this application under the given scope.
|
||||
///
|
||||
/// Microsoft document said that this key indicates that no application is specified for opening this file type.
|
||||
/// But I totally doesn't understand this. However, most of application set this.
|
||||
/// So let we set it together.
|
||||
pub fn set_no_open_with(&mut self, scope: Scope, flag: bool) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
if flag {
|
||||
key.set_value(Self::NAMEOF_NO_OPEN_WITH, &"")?;
|
||||
} else {
|
||||
regext::arbitrarily_delete_value(&key, Self::NAMEOF_NO_OPEN_WITH)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
use super::{
|
||||
Error, LosseProgId, OpenKeyPurpose, OpenKeyTerritory, OpenedKey, PERM_R, PERM_RW, Result,
|
||||
Scope, View, check_privilege,
|
||||
};
|
||||
use crate::win32::{concept, regext};
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
|
||||
|
||||
/// A registry key wrapper for `Software\Classes\<EXTENSION>`.
|
||||
///
|
||||
/// This struct provides [Self::is_exist], [Self::ensure] and [Self::delete] to
|
||||
/// check whether this key is in Registry, make sure this key is presneted in Registry,
|
||||
/// and delete self from Registry respectively.
|
||||
///
|
||||
/// And there are some getter and setter in this struct,
|
||||
/// before calling them, you must use [Self::ensure] make sure that this key is presented in Registry,
|
||||
/// otherwise these functions will return errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ExtKey {
|
||||
ext: concept::Ext,
|
||||
}
|
||||
|
||||
impl ExtKey {
|
||||
/// Creates a new `ExtKey` with the given file extension.
|
||||
pub fn new(inner: concept::Ext) -> Self {
|
||||
Self { ext: inner }
|
||||
}
|
||||
|
||||
/// Returns the inner file extension of this extension key.
|
||||
pub fn inner(&self) -> &concept::Ext {
|
||||
&self.ext
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtKey {
|
||||
const FULL_CLASSES: &str = "Software\\Classes";
|
||||
const PARTIAL_CLASSES: &str = "";
|
||||
|
||||
/// Attempts to open the registry key for this file extension with the given territory and purpose.
|
||||
fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
|
||||
// check privilege
|
||||
check_privilege(territory, purpose)?;
|
||||
// Fetch the permission
|
||||
let perms = purpose.to_permission();
|
||||
|
||||
// navigate to extension container
|
||||
let hk = match territory {
|
||||
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
|
||||
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
||||
OpenKeyTerritory::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
|
||||
};
|
||||
let classes = match territory {
|
||||
OpenKeyTerritory::User | OpenKeyTerritory::System => {
|
||||
hk.open_subkey_with_flags(Self::FULL_CLASSES, perms)?
|
||||
}
|
||||
OpenKeyTerritory::Hybrid => hk.open_subkey_with_flags(Self::PARTIAL_CLASSES, perms)?,
|
||||
};
|
||||
// open extension key if possible
|
||||
let this_ext = regext::try_open_subkey_with_flags(
|
||||
&classes,
|
||||
regext::blank_path_guard(self.ext.dotted_inner())?,
|
||||
perms,
|
||||
)?;
|
||||
// okey
|
||||
Ok(OpenedKey::new(classes, this_ext))
|
||||
}
|
||||
|
||||
/// Opens the registry key for read-only access under the given view.
|
||||
fn open_view_for_read(&self, view: View) -> Result<OpenedKey> {
|
||||
self.open_key(view.into(), OpenKeyPurpose::Read)
|
||||
}
|
||||
|
||||
/// Opens the registry key for read-write access under the given scope.
|
||||
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
|
||||
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
|
||||
}
|
||||
|
||||
/// Checks whether this file extension key exists in the registry under the given view.
|
||||
/// Return true if it is presented in registry, otherwise false.
|
||||
pub fn is_exist(&self, view: View) -> Result<bool> {
|
||||
let key = self.open_view_for_read(view)?.this_key;
|
||||
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 {
|
||||
let _ = key.parent_key.create_subkey_with_flags(
|
||||
regext::blank_path_guard(self.ext.dotted_inner())?,
|
||||
PERM_RW,
|
||||
)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
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>
|
||||
// And we explicitly do not support "OpenWithList", because it is obsolete in modern Windows.
|
||||
|
||||
// TODO:
|
||||
// We only support these keys in there because current interface are enough to use.
|
||||
// We may expand these in future.
|
||||
|
||||
/// Opens the registry key for getter reading, returning an error if the key does not exist.
|
||||
fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
|
||||
self.open_view_for_read(view)?
|
||||
.this_key
|
||||
.ok_or(Error::InexistantKey)
|
||||
}
|
||||
|
||||
/// Opens the registry key for setter writing, returning an error if the key does not exist.
|
||||
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
|
||||
self.open_scope_for_write(scope)?
|
||||
.this_key
|
||||
.ok_or(Error::InexistantKey)
|
||||
}
|
||||
|
||||
const NAMEOF_DEFAULT: &str = "";
|
||||
|
||||
/// Gets the default value of this file extension under the given view.
|
||||
/// Return `None` if there is no association for this file extension.
|
||||
///
|
||||
/// The content of this key is this file extension associated ProgId.
|
||||
pub fn get_default(&self, view: View) -> Result<Option<LosseProgId>> {
|
||||
let key = self.open_view_for_getter(view)?;
|
||||
// Get value of it
|
||||
let value = regext::try_get_value::<String, _>(&key, Self::NAMEOF_DEFAULT)?;
|
||||
// Transform it as result
|
||||
Ok(value.map(|v| LosseProgId::from(v.as_str())))
|
||||
}
|
||||
|
||||
/// Sets the default value (ProgId association) for this file extension under the given scope.
|
||||
/// Pass `None` to remove the association.
|
||||
pub fn set_default(&mut self, scope: Scope, pid: Option<&LosseProgId>) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
|
||||
match pid {
|
||||
Some(pid) => {
|
||||
// Set value for this key
|
||||
key.set_value(Self::NAMEOF_DEFAULT, &pid.to_string())?;
|
||||
}
|
||||
None => {
|
||||
// Delete this key
|
||||
regext::arbitrarily_delete_value(&key, Self::NAMEOF_DEFAULT)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const NAMEOF_OPEN_WITH_PROGIDS: &str = "OpenWithProgIds";
|
||||
|
||||
/// Gets the list of ProgIds registered in the "OpenWithProgIds" subkey under the given view.
|
||||
/// Return `None` is there is no this subkey.
|
||||
///
|
||||
/// This subkey contains a list of alternate ProgId for this file type.
|
||||
/// In Windows Explorer, these ProgId should appear in the Open with menu in right click menu of this file extension.
|
||||
pub fn get_open_with_progids(&self, view: View) -> Result<Option<Vec<LosseProgId>>> {
|
||||
let key = self.open_view_for_getter(view)?;
|
||||
// Get OpenWithProgIds subkey
|
||||
let open_with_progids_key =
|
||||
match regext::try_open_subkey_with_flags(&key, Self::NAMEOF_OPEN_WITH_PROGIDS, PERM_R)?
|
||||
{
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
// Fetch all sub-values
|
||||
let key_names = regext::get_all_string_subkey_names(&open_with_progids_key)?;
|
||||
// Map the result
|
||||
let progids = key_names
|
||||
.into_iter()
|
||||
.map(|name| LosseProgId::from(name.as_str()))
|
||||
.collect();
|
||||
// Return value
|
||||
Ok(Some(progids))
|
||||
}
|
||||
|
||||
/// Checks whether the given ProgId is in the "OpenWithProgIds" subkey under the given view.
|
||||
///
|
||||
/// If there is no "OpenWithProgIds" subkey, this function return false.
|
||||
pub fn is_in_open_with_progids(&self, view: View, pid: &LosseProgId) -> Result<bool> {
|
||||
let key = self.open_view_for_getter(view)?;
|
||||
// Get OpenWithProgIds subkey
|
||||
let open_with_progids_key =
|
||||
match regext::try_open_subkey_with_flags(&key, Self::NAMEOF_OPEN_WITH_PROGIDS, PERM_R)?
|
||||
{
|
||||
Some(key) => key,
|
||||
None => return Ok(false),
|
||||
};
|
||||
// Check whether there is given ProgId
|
||||
Ok(regext::try_get_value::<String, _>(
|
||||
&open_with_progids_key,
|
||||
regext::blank_path_guard(pid.to_string())?,
|
||||
)?
|
||||
.is_some())
|
||||
}
|
||||
|
||||
/// Adds the given ProgId into the "OpenWithProgIds" subkey under the given scope.
|
||||
///
|
||||
/// If there is no "OpenWithProgIds" subkey, this function will create it first,
|
||||
/// then add your given ProgId into it.
|
||||
pub fn add_into_open_with_progids(&mut self, scope: Scope, pid: &LosseProgId) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
// Get subkey
|
||||
let (open_with_progids_key, _) =
|
||||
key.create_subkey_with_flags(Self::NAMEOF_OPEN_WITH_PROGIDS, PERM_RW)?;
|
||||
// Add value
|
||||
open_with_progids_key.set_value(regext::blank_path_guard(pid.to_string())?, &"")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the given ProgId from the "OpenWithProgIds" subkey under the given scope.
|
||||
///
|
||||
/// If there is no "OpenWithProgIds" subkey, this function does nothing.
|
||||
pub fn remove_from_open_with_progids(&mut self, scope: Scope, pid: &LosseProgId) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
// Try get subkey
|
||||
let open_with_progids_key = match regext::try_open_subkey_with_flags(
|
||||
&key,
|
||||
Self::NAMEOF_OPEN_WITH_PROGIDS,
|
||||
PERM_RW,
|
||||
)? {
|
||||
Some(key) => key,
|
||||
None => return Ok(()),
|
||||
};
|
||||
// Remove given key
|
||||
regext::arbitrarily_delete_value(
|
||||
&open_with_progids_key,
|
||||
regext::blank_path_guard(pid.to_string())?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
use super::{
|
||||
Error, IconResVariant, LosseProgId, OpenKeyPurpose, OpenKeyTerritory, OpenedKey, PERM_R,
|
||||
PERM_RW, Result, Scope, ShellVerb, StrResVariant, View, check_privilege,
|
||||
};
|
||||
use crate::win32::regext;
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
|
||||
|
||||
/// A registry key wrapper for `Software\Classes\<PROGID>`.
|
||||
///
|
||||
/// This struct provides [Self::is_exist], [Self::ensure] and [Self::delete] to
|
||||
/// check whether this key is in Registry, make sure this key is presneted in Registry,
|
||||
/// and delete self from Registry respectively.
|
||||
///
|
||||
/// And there are some getter and setter in this struct,
|
||||
/// before calling them, you must use [Self::ensure] make sure that this key is presented in Registry,
|
||||
/// otherwise these functions will return errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ProgIdKey {
|
||||
progid: LosseProgId,
|
||||
}
|
||||
|
||||
impl ProgIdKey {
|
||||
/// Creates a new `ProgIdKey` with the given ProgId.
|
||||
pub fn new(inner: LosseProgId) -> Self {
|
||||
Self { progid: inner }
|
||||
}
|
||||
|
||||
/// Returns the inner ProgId of this ProgId key.
|
||||
pub fn inner(&self) -> &LosseProgId {
|
||||
&self.progid
|
||||
}
|
||||
}
|
||||
|
||||
impl ProgIdKey {
|
||||
const FULL_CLASSES: &str = "Software\\Classes";
|
||||
const PARTIAL_CLASSES: &str = "";
|
||||
|
||||
/// Attempts to open the registry key for this ProgId with the given territory and purpose.
|
||||
fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
|
||||
// check privilege
|
||||
check_privilege(territory, purpose)?;
|
||||
// Fetch the permission
|
||||
let perms = purpose.to_permission();
|
||||
|
||||
// navigate to ProgId container
|
||||
let hk = match territory {
|
||||
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
|
||||
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
|
||||
OpenKeyTerritory::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
|
||||
};
|
||||
let classes = match territory {
|
||||
OpenKeyTerritory::User | OpenKeyTerritory::System => {
|
||||
hk.open_subkey_with_flags(Self::FULL_CLASSES, perms)?
|
||||
}
|
||||
OpenKeyTerritory::Hybrid => hk.open_subkey_with_flags(Self::PARTIAL_CLASSES, perms)?,
|
||||
};
|
||||
// open ProgId key if possible
|
||||
let this_progid = regext::try_open_subkey_with_flags(
|
||||
&classes,
|
||||
regext::blank_path_guard(self.progid.to_string())?,
|
||||
perms,
|
||||
)?;
|
||||
// okey
|
||||
Ok(OpenedKey::new(classes, this_progid))
|
||||
}
|
||||
|
||||
/// Opens the registry key for read-only access under the given view.
|
||||
fn open_view_for_read(&self, view: View) -> Result<OpenedKey> {
|
||||
self.open_key(view.into(), OpenKeyPurpose::Read)
|
||||
}
|
||||
|
||||
/// Opens the registry key for read-write access under the given scope.
|
||||
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
|
||||
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
|
||||
}
|
||||
|
||||
/// Checks whether this ProgId key exists in the registry under the given view.
|
||||
/// Return true if it is presented in registry, otherwise false.
|
||||
pub fn is_exist(&self, view: View) -> Result<bool> {
|
||||
let key = self.open_view_for_read(view)?.this_key;
|
||||
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 {
|
||||
let _ = key.parent_key.create_subkey_with_flags(
|
||||
regext::blank_path_guard(self.progid.to_string())?,
|
||||
PERM_RW,
|
||||
)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
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:
|
||||
// We only support these keys in there because current interface are enough to use.
|
||||
// We may expand these in future.
|
||||
|
||||
/// Opens the registry key for getter reading, returning an error if the key does not exist.
|
||||
fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
|
||||
self.open_view_for_read(view)?
|
||||
.this_key
|
||||
.ok_or(Error::InexistantKey)
|
||||
}
|
||||
|
||||
/// Opens the registry key for setter writing, returning an error if the key does not exist.
|
||||
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
|
||||
self.open_scope_for_write(scope)?
|
||||
.this_key
|
||||
.ok_or(Error::InexistantKey)
|
||||
}
|
||||
|
||||
const NAMEOF_DEFAULT: &str = "";
|
||||
|
||||
/// Gets the default value of this ProgId under the given view.
|
||||
///
|
||||
/// The content of this key is the legacy way to introduce the friendly name of application.
|
||||
/// If you are fetching friendly name, please choose "FriendlyTypeName" at first, then fallback to this.
|
||||
/// If you are setting friendly name, please set this together.
|
||||
pub fn get_default(&self, view: View) -> Result<Option<StrResVariant>> {
|
||||
let key = self.open_view_for_getter(view)?;
|
||||
// Get value of it
|
||||
let value = regext::try_get_value::<String, _>(&key, Self::NAMEOF_DEFAULT)?;
|
||||
// Transform it as result
|
||||
Ok(value.map(|v| StrResVariant::from(v.as_str())))
|
||||
}
|
||||
|
||||
/// Sets the default value (friendly name) for this ProgId under the given scope.
|
||||
pub fn set_default(&mut self, scope: Scope, name: Option<&StrResVariant>) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
|
||||
match name {
|
||||
Some(name) => {
|
||||
// Set value for this key
|
||||
key.set_value(Self::NAMEOF_DEFAULT, &name.to_string())?;
|
||||
}
|
||||
None => {
|
||||
// Delete this key
|
||||
regext::arbitrarily_delete_value(&key, Self::NAMEOF_DEFAULT)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// Support mutiple ShellVerb in future.
|
||||
|
||||
const NAMEOF_SHELL_VERB_PART1: &str = "shell";
|
||||
const NAMEOF_SHELL_VERB_PART3: &str = "command";
|
||||
const NAMEOF_SHELL_VERB_PART4: &str = "";
|
||||
|
||||
/// Gets the shell verb registered for this ProgId under the given view.
|
||||
/// Return `None` if there is no any shell verb.
|
||||
///
|
||||
/// # Todo
|
||||
///
|
||||
/// Currently we only support single shell verb.
|
||||
/// So if there is multiple shell verb, this function still return `None`.
|
||||
/// This issue will be resolved in future.
|
||||
pub fn get_shell_verb(&self, view: View) -> Result<Option<ShellVerb>> {
|
||||
let key = self.open_view_for_getter(view)?;
|
||||
|
||||
// Get shell subkey
|
||||
let shell_key = match regext::try_open_subkey_with_flags(
|
||||
&key,
|
||||
Self::NAMEOF_SHELL_VERB_PART1,
|
||||
PERM_R,
|
||||
)? {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
// Get verb subkey name, then get subkey itself.
|
||||
let verb_key_name = match regext::get_sole_subkey_name(&shell_key)? {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let verb_key = match regext::try_open_subkey_with_flags(
|
||||
&shell_key,
|
||||
regext::blank_path_guard(&verb_key_name)?,
|
||||
PERM_R,
|
||||
)? {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
// Get command subkey.
|
||||
let command_key = match regext::try_open_subkey_with_flags(
|
||||
&verb_key,
|
||||
Self::NAMEOF_SHELL_VERB_PART3,
|
||||
PERM_R,
|
||||
)? {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
// Get the default value of command subkey
|
||||
let command_default_value = match regext::try_get_value::<String, _>(
|
||||
&command_key,
|
||||
Self::NAMEOF_SHELL_VERB_PART4,
|
||||
)? {
|
||||
Some(value) => value,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Okey, return value.
|
||||
Ok(Some(ShellVerb::new(
|
||||
verb_key_name.parse()?,
|
||||
command_default_value.parse()?,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Sets the shell verb for this ProgId under the given scope.
|
||||
/// Pass `None` as shell verb to remove the shell verb.
|
||||
///
|
||||
/// # Todo
|
||||
///
|
||||
/// Currently we only support single shell verb.
|
||||
/// So if there is multiple shell verb, all of them will be deleted at first,
|
||||
/// and then write your given shell verb inside it.
|
||||
/// This issue will be resolved in future.
|
||||
pub fn set_shell_verb(&mut self, scope: Scope, sv: Option<&ShellVerb>) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
|
||||
match sv {
|
||||
Some(sv) => {
|
||||
// Create shell subkey
|
||||
let (shell_key, _) =
|
||||
key.create_subkey_with_flags(Self::NAMEOF_SHELL_VERB_PART1, PERM_RW)?;
|
||||
// Create verb key
|
||||
let (verb_key, _) =
|
||||
shell_key.create_subkey_with_flags(sv.get_verb().inner(), PERM_RW)?;
|
||||
// Create command key
|
||||
let (command_key, _) =
|
||||
verb_key.create_subkey_with_flags(Self::NAMEOF_SHELL_VERB_PART3, PERM_RW)?;
|
||||
// Set command key default value
|
||||
command_key.set_value(Self::NAMEOF_SHELL_VERB_PART4, &sv.get_command().full())?;
|
||||
}
|
||||
None => {
|
||||
// Delete shell and its all subkey.
|
||||
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SHELL_VERB_PART1)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// This key may be REG_SZ or REG_EXPAND_SZ.
|
||||
// Currently we see them as one type.
|
||||
// This should be improved in future.
|
||||
|
||||
const NAMEOF_FRIENDLY_TYPE_NAME: &str = "FriendlyTypeName";
|
||||
|
||||
/// Gets the friendly type name registered for this ProgId under the given view.
|
||||
/// Return `None` if there is no such key.
|
||||
///
|
||||
/// The content of this key is the file type name which is suitable to display to the user.
|
||||
pub fn get_friendly_type_name(&self, view: View) -> Result<Option<StrResVariant>> {
|
||||
let key = self.open_view_for_getter(view)?;
|
||||
// Get value of it
|
||||
let value = regext::try_get_value::<String, _>(&key, Self::NAMEOF_FRIENDLY_TYPE_NAME)?;
|
||||
// Transform it as result
|
||||
Ok(value.map(|v| StrResVariant::from(v.as_str())))
|
||||
}
|
||||
|
||||
/// Sets the friendly type name for this ProgId under the given scope.
|
||||
/// Pass `None` to remove friendly type name.
|
||||
pub fn set_friendly_type_name(
|
||||
&mut self,
|
||||
scope: Scope,
|
||||
name: Option<&StrResVariant>,
|
||||
) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
|
||||
match name {
|
||||
Some(name) => {
|
||||
// Set value for this key
|
||||
key.set_value(Self::NAMEOF_FRIENDLY_TYPE_NAME, &name.to_string())?;
|
||||
}
|
||||
None => {
|
||||
// Delete this key
|
||||
regext::arbitrarily_delete_value(&key, Self::NAMEOF_FRIENDLY_TYPE_NAME)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// This key may be REG_SZ or REG_EXPAND_SZ.
|
||||
// Currently we see them as one type.
|
||||
// This should be improved in future.
|
||||
|
||||
const NAMEOF_DEFAULT_ICON_PART1: &str = "DefaultIcon";
|
||||
const NAMEOF_DEFAULT_ICON_PART2: &str = "";
|
||||
|
||||
/// Gets the default icon registered for this ProgId under the given view.
|
||||
/// Return `None` if there is no default icon.
|
||||
///
|
||||
/// The content of this key will be displayed for file types associated with this ProgId.
|
||||
pub fn get_default_icon(&self, view: View) -> Result<Option<IconResVariant>> {
|
||||
let key = self.open_view_for_getter(view)?;
|
||||
// Get default icon subkey
|
||||
let default_icon_key = match regext::try_open_subkey_with_flags(
|
||||
&key,
|
||||
Self::NAMEOF_DEFAULT_ICON_PART1,
|
||||
PERM_R,
|
||||
)? {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
// Get the default value of default icon subkey
|
||||
let default_icon_default_value =
|
||||
regext::try_get_value::<String, _>(&default_icon_key, Self::NAMEOF_DEFAULT_ICON_PART2)?;
|
||||
// Transform it as result
|
||||
Ok(default_icon_default_value.map(|v| IconResVariant::from(v.as_str())))
|
||||
}
|
||||
|
||||
/// Sets the default icon for this ProgId under the given scope.
|
||||
/// Pass `None` to remove the default icon subkey.
|
||||
///
|
||||
/// Although this function give the ability that do not set this key,
|
||||
/// however, I strongly suggest that set this to let file associated with this ProgId have an icon.
|
||||
pub fn set_default_icon(&mut self, scope: Scope, icon: Option<&IconResVariant>) -> Result<()> {
|
||||
let key = self.open_scope_for_setter(scope)?;
|
||||
|
||||
match icon {
|
||||
Some(icon) => {
|
||||
// Create default icon subkey
|
||||
let (default_icon_key, _) =
|
||||
key.create_subkey_with_flags(Self::NAMEOF_DEFAULT_ICON_PART1, PERM_RW)?;
|
||||
// Set default value of default icon subkey.
|
||||
default_icon_key.set_value(Self::NAMEOF_DEFAULT_ICON_PART2, &icon.to_string())?;
|
||||
}
|
||||
None => {
|
||||
// Delete shell and its all subkey.
|
||||
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_DEFAULT_ICON_PART1)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -5,29 +5,11 @@ use std::iter::FusedIterator;
|
||||
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) {
|
||||
eprintln!();
|
||||
}
|
||||
};
|
||||
// For one or more arguments like println!.
|
||||
($($arg:tt)*) => {
|
||||
if cfg!(debug_assertions) {
|
||||
eprintln!($($arg)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
// region: OS String Casting
|
||||
|
||||
// region: OS String Related
|
||||
|
||||
/// The error occurs when casting `OsStr` into `str`.
|
||||
/// The error occurs when casting [OsStr] into [str].
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("fail to cast OS string into string")]
|
||||
#[error("fail to cast OS string into Rust string")]
|
||||
pub struct CastOsStrError {}
|
||||
|
||||
impl CastOsStrError {
|
||||
@@ -50,6 +32,8 @@ pub fn osstr_to_str(osstr: &OsStr) -> Result<&str, CastOsStrError> {
|
||||
|
||||
// region: Capitalize First ASCII Letter
|
||||
|
||||
/// The struct transforming accept an iterator and perform as an iterator
|
||||
/// whose transform the first character of accepted iterator into capital form (ASCII char only).
|
||||
struct CapitalizeFirstAscii<T>
|
||||
where
|
||||
T: Iterator<Item = char>,
|
||||
@@ -90,6 +74,7 @@ where
|
||||
|
||||
impl<T> FusedIterator for CapitalizeFirstAscii<T> where T: Iterator<Item = char> + FusedIterator {}
|
||||
|
||||
/// Accept an string slice, transform its first char (ASCII only) as capital form and output.
|
||||
pub fn capitalize_first_ascii(s: &str) -> String {
|
||||
CapitalizeFirstAscii::new(s.chars()).collect()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! The module containing Win32 stuff related to file association operation,
|
||||
//! including Win32 concept, Registry operation helper and etc.
|
||||
|
||||
pub mod concept;
|
||||
pub mod utilities;
|
||||
pub mod regext;
|
||||
+257
-47
@@ -15,7 +15,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::HICON;
|
||||
|
||||
/// The error occurs when constructing Ext with bad body.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("given file extension body \"{inner}\" is invalid")]
|
||||
#[error(r#"given file extension body "{inner}" is invalid"#)]
|
||||
pub struct BadExtBodyError {
|
||||
/// The clone of string which is not a valid file extension body.
|
||||
inner: String,
|
||||
@@ -32,6 +32,12 @@ impl BadExtBodyError {
|
||||
|
||||
/// The struct representing an file extension which must start with dot (`.`)
|
||||
/// and followed by at least one arbitrary characters.
|
||||
///
|
||||
/// If you having a file extension expressed in string form with leading dot,
|
||||
/// please utilize [FromStr] trait to build this struct.
|
||||
/// Otherwise, use [Ext::new] with file extension without leading dot.
|
||||
///
|
||||
/// The [Display] trait this struct implemented always outputs file extension with leading dot.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Ext {
|
||||
/// The body of file extension (excluding dot).
|
||||
@@ -42,6 +48,8 @@ impl Ext {
|
||||
/// Create an new file extension.
|
||||
///
|
||||
/// `body` is the body of file extension (excluding dot, such as `jpg`).
|
||||
/// Empty body is not allowed.
|
||||
///
|
||||
/// If you want to create this struct with ordinary extension string like `.jpg`,
|
||||
/// please use `Ext::from_str()` or `parse::<Ext>()` instead.
|
||||
pub fn new(body: &str) -> Result<Self, BadExtBodyError> {
|
||||
@@ -56,11 +64,17 @@ impl Ext {
|
||||
}
|
||||
|
||||
/// Get the body part of file extension (excluding dot)
|
||||
///
|
||||
/// This struct promise that there is no possibility that return value is blank
|
||||
/// (i.e. the body of file extension is empty).
|
||||
pub fn inner(&self) -> &str {
|
||||
&self.body
|
||||
}
|
||||
|
||||
/// Get the body part of file extension (with leading dot)
|
||||
///
|
||||
/// This struct promise that there is no possibility that return value is dot only
|
||||
/// (i.e. the body of file extension is empty).
|
||||
pub fn dotted_inner(&self) -> String {
|
||||
// Reuse Display trait result
|
||||
self.to_string()
|
||||
@@ -69,7 +83,7 @@ impl Ext {
|
||||
|
||||
/// The error occurs when try parsing string into FileExt.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("given file extension name \"{inner}\" is invalid")]
|
||||
#[error(r#"given file extension name "{inner}" is invalid"#)]
|
||||
pub struct ParseExtError {
|
||||
/// The clone of string which is not a valid file extension.
|
||||
inner: String,
|
||||
@@ -110,7 +124,7 @@ impl FromStr for Ext {
|
||||
|
||||
/// The error occurs when constructing ProgId.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("given ProgId part \"{inner}\" is invalid")]
|
||||
#[error(r#"given ProgId part "{inner}" is invalid"#)]
|
||||
pub struct BadProgIdPartError {
|
||||
/// The clone of string which is not a valid ProgId part.
|
||||
inner: String,
|
||||
@@ -127,16 +141,21 @@ impl BadProgIdPartError {
|
||||
|
||||
/// The ProgId exactly follows Microsoft suggested
|
||||
/// `[Vendor or Application].[Component].[Version]` format.
|
||||
/// And additionally, `[Version]` part is optional.
|
||||
///
|
||||
/// Additionally, `[Version]` part is optional.
|
||||
///
|
||||
/// However, most of applications do no follow this standard,
|
||||
/// this scenario is not convered by this struct in there.
|
||||
/// In reality world, most of applications do no follow this standard.
|
||||
/// However, this scenario is not convered by this struct in there.
|
||||
/// It should be done by other structs in other places.
|
||||
///
|
||||
/// If you have a ProgId expressed in string form, please utilize [FromStr] trait to parse it.
|
||||
/// Or use [ProgId::new] with each parts to construct this struct.
|
||||
///
|
||||
/// The [Display] trait this struct implemented always
|
||||
/// outputs legal ProgId in string form.
|
||||
///
|
||||
/// Reference:
|
||||
/// - https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids
|
||||
/// - https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
|
||||
/// - <https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids>
|
||||
/// - <https://learn.microsoft.com/en-us/windows/win32/com/-progid--key>
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ProgId {
|
||||
/// The vendor part of ProgId.
|
||||
@@ -149,6 +168,9 @@ pub struct ProgId {
|
||||
|
||||
impl ProgId {
|
||||
/// Create a new ProgId with given parts.
|
||||
///
|
||||
/// Empty string or string with embedded dot is not allowed
|
||||
/// for vendor and component parts.
|
||||
pub fn new(
|
||||
vendor: &str,
|
||||
component: &str,
|
||||
@@ -186,7 +208,7 @@ impl ProgId {
|
||||
|
||||
/// The error occurs when parsing ProgId.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("given ProgId \"{inner}\" is invalid")]
|
||||
#[error(r#"given ProgId "{inner}" is invalid"#)]
|
||||
pub struct ParseProgIdError {
|
||||
/// The clone of string which is not a valid ProgId.
|
||||
inner: String,
|
||||
@@ -245,6 +267,14 @@ impl FromStr for ProgId {
|
||||
/// `{26EE0668-A00A-44D7-9371-BEB064C98683}` (case insensitive).
|
||||
///
|
||||
/// Please note that the curly brace is the essential part of CLSID.
|
||||
///
|
||||
/// If you have a CLSID expressed in string form,
|
||||
/// please utilize [FromStr] trait to parse it.
|
||||
/// You also can utilize [Clsid::new] with [uuid] crate to create a custom CLSID,
|
||||
/// or directly use [Clsid::with_random] to create a random new one.
|
||||
///
|
||||
/// The [Display] trait this struct implemented always
|
||||
/// output CLSID in lower-case with curly brace.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Clsid {
|
||||
inner: Uuid,
|
||||
@@ -252,7 +282,7 @@ pub struct Clsid {
|
||||
|
||||
impl Clsid {
|
||||
/// Create new CLSID from underlying UUID.
|
||||
fn new(uuid: &Uuid) -> Self {
|
||||
pub fn new(uuid: &Uuid) -> Self {
|
||||
Self { inner: *uuid }
|
||||
}
|
||||
|
||||
@@ -264,7 +294,7 @@ impl Clsid {
|
||||
|
||||
/// The error occurs when parsing CLSID.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("given string \"{inner}\" is invalid for uuid")]
|
||||
#[error(r#"given string "{inner}" is invalid for CLSID"#)]
|
||||
pub struct ParseClsidError {
|
||||
/// The clone of string which is not a valid CLSID.
|
||||
inner: String,
|
||||
@@ -301,7 +331,7 @@ impl Display for Clsid {
|
||||
|
||||
/// Error occurs when given string is not a valid Icon Reference String.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("given string \"{inner}\" is not a valid Icon Reference String")]
|
||||
#[error(r#"given string "{inner}" is not a valid Icon Reference String"#)]
|
||||
pub struct ParseIconRefStrError {
|
||||
/// The clone of string which is not a valid Icon Reference String
|
||||
inner: String,
|
||||
@@ -316,11 +346,22 @@ impl ParseIconRefStrError {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// IconRefStr should process the quote issue for path part in itself,
|
||||
// rather than fetching its path part and resolve quote issue in another place.
|
||||
|
||||
/// The struct representing an Icon Reference String
|
||||
/// looks like `%SystemRoot%\System32\imageres.dll,-72`.
|
||||
///
|
||||
/// As far as I know, the minus token `-` does nothing in this string.
|
||||
/// The following number is just the index.
|
||||
///
|
||||
/// If you have a Icon Reference String,
|
||||
/// please utilize [FromStr] trait to parse it into this struct.
|
||||
/// You also can use [IconRefStr::new] with path and index parts to build this struct.
|
||||
///
|
||||
/// The [Display] trait this struct implemented always
|
||||
/// outputs Icon Reference String in string form.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct IconRefStr {
|
||||
/// The path part of this reference string.
|
||||
@@ -352,6 +393,12 @@ impl IconRefStr {
|
||||
/// Get the path part of this reference string.
|
||||
///
|
||||
/// This path can be absolute path, relative path or expandable path.
|
||||
///
|
||||
/// # Todo
|
||||
///
|
||||
/// Currently this return value may be quoted path.
|
||||
/// You may need manually unquote it.
|
||||
/// This issue will be resolved in furture.
|
||||
pub fn get_path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
@@ -373,7 +420,7 @@ impl FromStr for IconRefStr {
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^([^,@].*),-([0-9]+)$").expect("unexpected bad regex pattern string")
|
||||
Regex::new(r"^([^,@][^,]*),-?([0-9]+)$").expect("unexpected bad regex pattern string")
|
||||
});
|
||||
let caps = RE.captures(s);
|
||||
if let Some(caps) = caps {
|
||||
@@ -395,7 +442,7 @@ impl FromStr for IconRefStr {
|
||||
|
||||
/// Error occurs when given string is not a valid String Reference String.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("given string \"{inner}\" is not a valid String Reference String")]
|
||||
#[error(r#"given string "{inner}" is not a valid String Reference String"#)]
|
||||
pub struct ParseStrRefStrError {
|
||||
/// The clone of string which is not a valid String Reference String
|
||||
inner: String,
|
||||
@@ -410,11 +457,22 @@ impl ParseStrRefStrError {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// StrRefStr should process the quote issue for path part in itself,
|
||||
// rather than fetching its path part and resolve quote issue in another place.
|
||||
|
||||
/// The struct representing an String Reference String
|
||||
/// looks like `@%SystemRoot%\System32\shell32.dll,-30596`.
|
||||
///
|
||||
/// As far as I know, the minus token `-` does nothing in this string.
|
||||
/// The following number is just the index.
|
||||
///
|
||||
/// If you have a String Reference String,
|
||||
/// please utilize [FromStr] trait to parse it into this struct.
|
||||
/// You also can use [StrRefStr::new] with path and index parts to build this struct.
|
||||
///
|
||||
/// The [Display] trait this struct implemented always
|
||||
/// outputs String Reference String in string form.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct StrRefStr {
|
||||
/// The path part of this reference string.
|
||||
@@ -440,6 +498,12 @@ impl StrRefStr {
|
||||
/// Get the path part of this reference string.
|
||||
///
|
||||
/// This path can be absolute path, relative path or expandable path.
|
||||
///
|
||||
/// # Todo
|
||||
///
|
||||
/// Currently this return value may be quoted path.
|
||||
/// You may need manually unquote it.
|
||||
/// This issue will be resolved in furture.
|
||||
pub fn get_path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
@@ -461,7 +525,7 @@ impl FromStr for StrRefStr {
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^@(.+),-([0-9]+)$").expect("unexpected bad regex pattern string")
|
||||
Regex::new(r"^@([^,]+),-?([0-9]+)$").expect("unexpected bad regex pattern string")
|
||||
});
|
||||
let caps = RE.captures(s);
|
||||
if let Some(caps) = caps {
|
||||
@@ -504,14 +568,40 @@ pub enum IconSizeKind {
|
||||
Large,
|
||||
}
|
||||
|
||||
/// The struct representing a loaded icon resource.
|
||||
/// The struct representing a loaded Win32 icon resource.
|
||||
///
|
||||
/// You can use [IconRc::new] or [IconRc::with_ico_file] to create this resource.
|
||||
///
|
||||
/// The ownership of loaded Win32 icon resource is managed by this struct,
|
||||
/// and will be free once this struct was destroyed.
|
||||
///
|
||||
/// According to Microsoft manual,
|
||||
/// Win32 icon resource is program scope resource,
|
||||
/// so it can be safely used between different threads.
|
||||
#[derive(Debug)]
|
||||
pub struct IconRc {
|
||||
icon: HICON,
|
||||
}
|
||||
|
||||
impl IconRc {
|
||||
/// Get the icon preset representing generic document.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn GENERIC_DOCUMENT(kind: IconSizeKind) -> Result<Self, LoadIconRcError> {
|
||||
Self::new("shell32.dll", 0, kind)
|
||||
}
|
||||
|
||||
/// Get the icon preset representing generic executable.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn GENERIC_APPLICATION(kind: IconSizeKind) -> Result<Self, LoadIconRcError> {
|
||||
Self::new("imageres.dll", 0, kind)
|
||||
}
|
||||
}
|
||||
|
||||
impl IconRc {
|
||||
/// Load icon from executable or `.ico` file.
|
||||
///
|
||||
/// Please note that the content of `file` parameter must NOT be
|
||||
/// expandable string or quoted string.
|
||||
///
|
||||
/// If you want to extract icon from `.ico` file, please pass `0` to `index` parameter.
|
||||
/// Otherwise `index` is the icon resource index located in executable.
|
||||
@@ -548,16 +638,28 @@ impl IconRc {
|
||||
Self::new(file, 0, kind)
|
||||
}
|
||||
|
||||
/// Construct this struct in raw Win32 icon handle.
|
||||
///
|
||||
/// Please note that after calling this function,
|
||||
/// the ownership of given Win32 icon handle are managed by this struct.
|
||||
pub unsafe fn from_raw(hicon: HICON) -> Self {
|
||||
Self { icon: hicon }
|
||||
}
|
||||
|
||||
pub fn into_raw(self) -> HICON {
|
||||
self.icon
|
||||
/// Consume and leak this struct held Win32 icon handle.
|
||||
///
|
||||
/// The ownership of this Win32 icon handle will be transfered to the caller of this function.
|
||||
pub fn into_raw(mut self) -> HICON {
|
||||
let rv = self.icon;
|
||||
self.icon = std::ptr::null_mut();
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
impl IconRc {
|
||||
/// Get underlying Win32 icon handle.
|
||||
///
|
||||
/// Please note that the ownership of this handle are still in this struct.
|
||||
pub fn get_icon(&self) -> HICON {
|
||||
self.icon
|
||||
}
|
||||
@@ -575,6 +677,18 @@ impl Drop for IconRc {
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY:
|
||||
// Win32 HICON can be safely moved between different threads naturally.
|
||||
unsafe impl Send for IconRc {}
|
||||
|
||||
// SAFETY:
|
||||
// When multiple threads calling this struct, there is no conflict between them,
|
||||
// because this struct only provide HICON itself.
|
||||
//
|
||||
// However, the caller should ensure that all calls using this HICON
|
||||
// follow Win32 thread model.
|
||||
unsafe impl Sync for IconRc {}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: String Resource
|
||||
@@ -596,12 +710,18 @@ pub enum LoadStrRcError {
|
||||
}
|
||||
|
||||
/// The struct representing a loaded string resource.
|
||||
///
|
||||
/// You can use [StrRc::new] to create this resource.
|
||||
#[derive(Debug)]
|
||||
pub struct StrRc {
|
||||
inner: String,
|
||||
}
|
||||
|
||||
impl StrRc {
|
||||
/// Load icon from executable file.
|
||||
///
|
||||
/// Please note that the content of `file` parameter must NOT be
|
||||
/// expandable string or quoted string.
|
||||
pub fn new(file: &str, index: u32) -> Result<Self, LoadStrRcError> {
|
||||
use windows_sys::Win32::Foundation::FreeLibrary;
|
||||
use windows_sys::Win32::System::LibraryLoader::{
|
||||
@@ -647,10 +767,12 @@ impl StrRc {
|
||||
}
|
||||
|
||||
impl StrRc {
|
||||
/// Get fetched string resource.
|
||||
pub fn get_string(&self) -> &str {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Consume and return fetched string resource.
|
||||
pub fn into_string(self) -> String {
|
||||
self.inner
|
||||
}
|
||||
@@ -691,21 +813,24 @@ pub enum ExpandEnvVarError {
|
||||
NoEnvVar,
|
||||
}
|
||||
|
||||
/// The struct representing an Expand String,
|
||||
/// The struct representing an expandable string,
|
||||
/// which contain environment variable in string,
|
||||
/// like `%LOCALAPPDATA%\SomeApp.exe`.
|
||||
///
|
||||
/// You can use [ExpandString::new] or [FromStr] trait to create an expandable string,
|
||||
/// and use [ExpandString::expand] to try expanding it.
|
||||
///
|
||||
/// The [Display] trait this struct implemented always
|
||||
/// outputs original expandable string.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ExpandString {
|
||||
inner: String,
|
||||
}
|
||||
|
||||
impl ExpandString {
|
||||
/// Internal shared compiled regex pattern matching Variable,
|
||||
/// the `%` braced string like `%SystemRoot%`.
|
||||
const VAR_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"%[a-zA-Z0-9_]+%").expect("unexpected bad regex pattern string")
|
||||
});
|
||||
}
|
||||
/// Internal shared compiled regex pattern matching Variable,
|
||||
/// the `%` braced string like `%SystemRoot%`.
|
||||
static WINVAR_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"%[a-zA-Z0-9_]+%").expect("unexpected bad regex pattern string"));
|
||||
|
||||
impl ExpandString {
|
||||
/// Create a new expand string
|
||||
@@ -714,17 +839,17 @@ impl ExpandString {
|
||||
}
|
||||
|
||||
/// Expand the variables located in this string
|
||||
/// and produce the final usable string.
|
||||
pub fn expand_string(&self) -> Result<String, ExpandEnvVarError> {
|
||||
/// and produce the final string without having any embedded variable syntax.
|
||||
pub fn expand(&self) -> Result<String, ExpandEnvVarError> {
|
||||
use windows_sys::Win32::System::Environment::ExpandEnvironmentStringsW;
|
||||
|
||||
// Fetch the size of expand result
|
||||
let source = WideCString::from_str(self.inner.as_str())?;
|
||||
// The return size is including null terminal.
|
||||
let size = unsafe { ExpandEnvironmentStringsW(source.as_ptr(), std::ptr::null_mut(), 0) };
|
||||
if size == 0 {
|
||||
return Err(ExpandEnvVarError::ExpandFunction);
|
||||
}
|
||||
let size_no_nul = size.checked_sub(1).ok_or(ExpandEnvVarError::Underflow)?;
|
||||
|
||||
// Allocate buffer for it.
|
||||
let len: usize = size.try_into()?;
|
||||
@@ -732,7 +857,7 @@ impl ExpandString {
|
||||
let mut buffer = vec![0; len];
|
||||
// Receive result
|
||||
let size =
|
||||
unsafe { ExpandEnvironmentStringsW(source.as_ptr(), buffer.as_mut_ptr(), size_no_nul) };
|
||||
unsafe { ExpandEnvironmentStringsW(source.as_ptr(), buffer.as_mut_ptr(), size) };
|
||||
if size == 0 {
|
||||
return Err(ExpandEnvVarError::ExpandFunction);
|
||||
}
|
||||
@@ -743,7 +868,7 @@ impl ExpandString {
|
||||
|
||||
// If the final string still has environment variable,
|
||||
// we think we fail to expand it.
|
||||
if Self::VAR_RE.is_match(rv.as_str()) {
|
||||
if WINVAR_RE.is_match(rv.as_str()) {
|
||||
Err(ExpandEnvVarError::NoEnvVar)
|
||||
} else {
|
||||
Ok(rv)
|
||||
@@ -761,7 +886,7 @@ impl FromStr for ExpandString {
|
||||
type Err = ParseExpandStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if Self::VAR_RE.is_match(s) {
|
||||
if WINVAR_RE.is_match(s) {
|
||||
Ok(Self {
|
||||
inner: s.to_string(),
|
||||
})
|
||||
@@ -775,9 +900,15 @@ impl FromStr for ExpandString {
|
||||
|
||||
// region: File Name
|
||||
|
||||
/// The error occurs when constructing FileName with bad file name.
|
||||
pub type BadFileNameError = ParseFileNameError;
|
||||
|
||||
/// The struct representing a legal Windows file name.
|
||||
///
|
||||
/// You can utilize [FileName::new] or [FromStr] trait to create this struct.
|
||||
///
|
||||
/// The [Display] trait this struct implemented always
|
||||
/// outputs the file name in string form.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FileName {
|
||||
/// The validated legal Windows file name.
|
||||
@@ -801,7 +932,7 @@ impl FileName {
|
||||
}
|
||||
}
|
||||
|
||||
/// The error occurs when constructing FileName with bad file name.
|
||||
/// The error occurs when parsing FileName with bad file name.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("given file name is illegal in Windows")]
|
||||
pub enum ParseFileNameError {
|
||||
@@ -848,9 +979,15 @@ impl FromStr for FileName {
|
||||
|
||||
// region: Verb
|
||||
|
||||
/// The error occurs when constructing Verb with bad verb name.
|
||||
pub type BadVerbError = ParseVerbError;
|
||||
|
||||
/// The struct representing a verb when manipulating file
|
||||
///
|
||||
/// You can utilize [Verb::new] or [FromStr] trait to create this struct.
|
||||
///
|
||||
/// The [Display] trait this struct implemented always
|
||||
/// outputs the verb in string form.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Verb {
|
||||
/// The validated verb name.
|
||||
@@ -858,9 +995,23 @@ pub struct Verb {
|
||||
}
|
||||
|
||||
impl Verb {
|
||||
pub const OPEN: LazyLock<Verb> = LazyLock::new(|| Verb::new("open").expect("unexpected bad verb"));
|
||||
pub const EDIT: LazyLock<Verb> = LazyLock::new(|| Verb::new("edit").expect("unexpected bad verb"));
|
||||
pub const PLAY: LazyLock<Verb> = LazyLock::new(|| Verb::new("play").expect("unexpected bad verb"));
|
||||
/// Get the verb representing Open.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn OPEN() -> Self {
|
||||
Verb::new("open").expect("unexpected bad verb")
|
||||
}
|
||||
|
||||
/// Get the verb representing Edit.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn EDIT() -> Self {
|
||||
Verb::new("edit").expect("unexpected bad verb")
|
||||
}
|
||||
|
||||
/// Get the verb representing Play.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn PLAY() -> Self {
|
||||
Verb::new("play").expect("unexpected bad verb")
|
||||
}
|
||||
}
|
||||
|
||||
impl Verb {
|
||||
@@ -878,9 +1029,9 @@ impl Verb {
|
||||
}
|
||||
}
|
||||
|
||||
/// The error occurs when constructing Verb with bad verb name.
|
||||
/// The error occurs when parsing Verb with bad verb name.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("given verb \"{inner}\" is illegal")]
|
||||
#[error(r#"given verb "{inner}" is illegal"#)]
|
||||
pub struct ParseVerbError {
|
||||
inner: String,
|
||||
}
|
||||
@@ -919,50 +1070,106 @@ impl FromStr for Verb {
|
||||
|
||||
// region: Command Line
|
||||
|
||||
// TODO:
|
||||
// Introduce CmdArg in future to describe single Windows command line argument.
|
||||
// And use CmdArg to finish the full functionality of CmdLine.
|
||||
|
||||
/// The error occurs when constructing CmdLine with bad arguments.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("given file extension body \"{inner}\" is invalid")]
|
||||
#[error(r#"given command line argument "{inner}" is invalid"#)]
|
||||
pub struct BadCmdLineError {
|
||||
inner: String,
|
||||
}
|
||||
|
||||
impl BadCmdLineError {
|
||||
// TODO: Remove this dead_code attribute once we need use this error type.
|
||||
#[allow(dead_code)]
|
||||
fn new(inner: &str) -> Self {
|
||||
Self {
|
||||
inner: inner.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The struct representing a Windows command line.
|
||||
///
|
||||
/// # Note
|
||||
/// If you have a complete Windows command line expressed in string form,
|
||||
/// you can utilize [FromStr] trait to create this struct.
|
||||
/// Or, you can utilize [CmdLine::new] to create this struct with command arguments one by one.
|
||||
///
|
||||
/// The [Display] trait this struct implemented always
|
||||
/// outputs a legal Windows command line in string form,
|
||||
/// which combine all arguments together.
|
||||
///
|
||||
/// # Todo
|
||||
///
|
||||
/// This struct currently does nothing for validation for given command line.
|
||||
/// Because there is no standard for validating this.
|
||||
/// So I just write this struct as a placeholder for future extension.
|
||||
///
|
||||
/// Currently if you are using [FromStr] trait to parse command line,
|
||||
/// it will simply split it with space as separator.
|
||||
/// This may cause unexpected behavior such as empty argument or unexpected break in argument.
|
||||
/// This issue will be resolved in future.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct CmdLine {
|
||||
inner: Vec<String>,
|
||||
}
|
||||
|
||||
impl CmdLine {
|
||||
/// Create a new command line.
|
||||
/// Create a new command line with arguments one by one.
|
||||
///
|
||||
/// Please note that passed command line argument must be unquoted.
|
||||
///
|
||||
/// # Todo
|
||||
///
|
||||
/// Currently, there is no unquote restrictions for those passed arguments.
|
||||
/// These passed argument will be honestly used in full command line.
|
||||
/// So if your argument require quote, please quote them at first.
|
||||
/// This issue will be resolved in future.
|
||||
pub fn new<S: AsRef<str>>(args: &[S]) -> Result<Self, BadCmdLineError> {
|
||||
Ok(Self { inner: args.iter().map(|s| s.as_ref().to_string()).collect() })
|
||||
Ok(Self {
|
||||
inner: args.iter().map(|s| s.as_ref().to_string()).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the full command line.
|
||||
/// Get the full command line in string form with proper quote.
|
||||
///
|
||||
/// # Todo
|
||||
///
|
||||
/// Currently this function only simply returns
|
||||
/// the joined command line arguments with space as separator.
|
||||
/// There is no guarantee about safe quote.
|
||||
/// This issue will be resolved in future.
|
||||
pub fn full(&self) -> String {
|
||||
self.inner.join(" ")
|
||||
}
|
||||
|
||||
/// Get the iterator of command line arguments.
|
||||
///
|
||||
/// Each returned argument are unquoted.
|
||||
///
|
||||
/// # Todo
|
||||
///
|
||||
/// Currently there is no guarantee about the unquote behavior
|
||||
/// for returned command line arguments.
|
||||
/// It simply returns the content of arguments list passed to constructor.
|
||||
/// This issue will be resolved in future.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &str> {
|
||||
self.inner.iter().map(|s| s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// The error occurs when constructing CmdLine with bad syntax.
|
||||
/// The error occurs when parsing CmdLine with bad syntax.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("given command line \"{inner}\" is invalid")]
|
||||
#[error(r#"given command line "{inner}" is invalid"#)]
|
||||
pub struct ParseCmdLineError {
|
||||
inner: String,
|
||||
}
|
||||
|
||||
impl ParseCmdLineError {
|
||||
// TODO: Remove this dead_code attribute once we need use this error type.
|
||||
#[allow(dead_code)]
|
||||
fn new(inner: &str) -> Self {
|
||||
Self {
|
||||
inner: inner.to_string(),
|
||||
@@ -981,7 +1188,10 @@ impl FromStr for CmdLine {
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// We simply split it by space for rough result.
|
||||
Ok(Self { inner: s.split(' ').map(|s| s.to_string()).collect() })
|
||||
Ok(Self {
|
||||
inner: s.split(' ').map(|s| s.to_string()).collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
+127
-11
@@ -1,23 +1,22 @@
|
||||
//! This module extend `winreg` crate to make it more suit for the usage of this crate.
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
use thiserror::Error as TeError;
|
||||
use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND;
|
||||
use windows_sys::Win32::System::Registry::REG_SAM_FLAGS;
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::RegType;
|
||||
use winreg::types::FromRegValue;
|
||||
|
||||
// region: Extra Operations
|
||||
|
||||
/// Get the subkey with given name.
|
||||
/// Try getting the subkey with given name.
|
||||
///
|
||||
/// If error occurs when fetching given subkey, it return `Err(...)`,
|
||||
/// otherwise, it will return `Ok(Some(...))` if subkey is existing,
|
||||
/// or `Ok(None)` if there is no suchsub key.
|
||||
///
|
||||
/// Comparing with the function provided by winreg,
|
||||
/// Comparing with the function provided by [winreg],
|
||||
/// it differ "no such subkey" error and other access error.
|
||||
pub fn try_open_subkey_with_flags<P: AsRef<OsStr>>(
|
||||
regkey: &RegKey,
|
||||
@@ -36,13 +35,13 @@ pub fn try_open_subkey_with_flags<P: AsRef<OsStr>>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the value by given key.
|
||||
/// Try getting the value by given key.
|
||||
///
|
||||
/// If error occurs when fetching given key, it return `Err(...)`,
|
||||
/// otherwise, it will return `Ok(Some(...))` if key is existing,
|
||||
/// or `Ok(None)` if there is no such key.
|
||||
///
|
||||
/// Comparing with the function provided by winreg,
|
||||
/// Comparing with the function provided by [winreg],
|
||||
/// it differ "no such key" error and other access error.
|
||||
pub fn try_get_value<T: FromRegValue, N: AsRef<OsStr>>(
|
||||
regkey: &RegKey,
|
||||
@@ -60,6 +59,113 @@ 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// Once we support multiple ShellVerb,
|
||||
// it seems that there is no need to preserve this function.
|
||||
|
||||
/// 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 used as fetching only one ShellVerb.
|
||||
pub fn get_sole_subkey_name(regkey: &RegKey) -> std::io::Result<Option<String>> {
|
||||
let mut subkey_enumerator = regkey.enum_keys();
|
||||
|
||||
// Get first one.
|
||||
let rv = match subkey_enumerator.next() {
|
||||
Some(key) => key?,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Check whether there is second one.
|
||||
match subkey_enumerator.next() {
|
||||
Some(_) => Ok(None),
|
||||
None => Ok(Some(rv)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the name list of all "string" subkeys in given key.
|
||||
///
|
||||
/// This is used in "OpenWithProgIds" subkey.
|
||||
pub fn get_all_string_subkey_names(regkey: &RegKey) -> std::io::Result<Vec<String>> {
|
||||
regkey
|
||||
.enum_values()
|
||||
.filter_map(|item| match item {
|
||||
Ok((key, value)) => {
|
||||
// Check subkey type.
|
||||
if value.vtype == RegType::REG_SZ {
|
||||
Some(Ok(key))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(err) => Some(Err(err)),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
/// 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("")
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Blank Path Guard
|
||||
@@ -76,12 +182,15 @@ impl BlankPathError {
|
||||
}
|
||||
|
||||
/// Check whether given registry path is empty.
|
||||
/// If it is, return error, otherwise the given path.
|
||||
/// If it is, return error, otherwise the given path itself.
|
||||
///
|
||||
/// Passing empty path for some registry functions is dangerous.
|
||||
/// 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.
|
||||
/// Passing empty path for some [winreg] functions is dangerous, expecially for those registry delete functions.
|
||||
/// Passing empty path to registry creation function will return themselves,
|
||||
/// and passing them to registry delete functions will delete all contents of them!
|
||||
/// So I create this function to prevent any harmful blank path was passed into [winreg] function.
|
||||
///
|
||||
/// This function MUST be used for wrapping the value, whose content can not be confirmed at compile time,
|
||||
/// if that value 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> {
|
||||
if path.as_ref().is_empty() {
|
||||
Err(BlankPathError::new())
|
||||
@@ -94,6 +203,11 @@ pub fn blank_path_guard<P: AsRef<OsStr>>(path: P) -> std::result::Result<P, Blan
|
||||
|
||||
// region: Expand String
|
||||
|
||||
// TODO:
|
||||
// Re-enable following code when we need to distinguish between REG_SZ and REG_EXPAND_SZ.
|
||||
|
||||
/*
|
||||
|
||||
/// The struct basically is the alias of String, but make a slight difference with it,
|
||||
/// to make they are different when use it with String as generic argument.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
@@ -161,4 +275,6 @@ impl From<&str> for ExpandString {
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
/// 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
|
||||
/// Reference: <https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership>
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if any Windows functions failed unexpectedly.
|
||||
/// This is a very common check and in theory there is no any possibility that this check will fail.
|
||||
pub fn has_privilege() -> bool {
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Security::{
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/// Check whether we are in sandbox.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panic if we are not in sandbox (we can not perform dangerous test).
|
||||
/// Return if we are in sandbox environment.
|
||||
pub fn check_sandbox() {
|
||||
assert!(
|
||||
std::env::var("SANDBOXIE").is_ok(),
|
||||
concat!(
|
||||
"Non-sandbox environment detected. ",
|
||||
"Executing these tests in non-sandbox environment is VERY dangerous. ",
|
||||
r#"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."
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::str::FromStr;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use wfassoc::win32::concept::*;
|
||||
|
||||
// region: File Extension
|
||||
@@ -134,6 +134,13 @@ fn test_icon_ref_str() {
|
||||
r#"%SystemRoot%\System32\imageres.dll,-72"#,
|
||||
(r#"%SystemRoot%\System32\imageres.dll"#, 72),
|
||||
);
|
||||
ok_tester(
|
||||
r#""D:\Software\Krita\Krita (x64)\shellex\kritafile.ico",0"#,
|
||||
(
|
||||
r#""D:\Software\Krita\Krita (x64)\shellex\kritafile.ico""#,
|
||||
0,
|
||||
),
|
||||
);
|
||||
err_tester(r#"C:\Windows\Cursors\aero_arrow.cur"#);
|
||||
err_tester(r#"This is my application, OK?"#);
|
||||
err_tester(r#"@%SystemRoot%\System32\shell32.dll,-30596"#);
|
||||
@@ -161,6 +168,10 @@ fn test_str_ref_str() {
|
||||
r#"@%SystemRoot%\System32\shell32.dll,-30596"#,
|
||||
(r#"%SystemRoot%\System32\shell32.dll"#, 30596),
|
||||
);
|
||||
ok_tester(
|
||||
r#"@%SystemRoot%\System32\shell32.dll,30596"#,
|
||||
(r#"%SystemRoot%\System32\shell32.dll"#, 30596),
|
||||
);
|
||||
err_tester(r#"This is my application, OK?"#);
|
||||
err_tester(r#"%SystemRoot%\System32\imageres.dll,-72"#);
|
||||
}
|
||||
@@ -175,6 +186,9 @@ fn test_str_ref_str() {
|
||||
|
||||
#[test]
|
||||
fn test_icon_rc() {
|
||||
fn const_tester(icon: Result<IconRc, LoadIconRcError>) {
|
||||
assert!(icon.is_ok())
|
||||
}
|
||||
fn ok_tester(file: &str, index: u32) {
|
||||
let icon = IconRc::new(file, index, IconSizeKind::Small);
|
||||
assert!(icon.is_ok())
|
||||
@@ -184,6 +198,9 @@ fn test_icon_rc() {
|
||||
assert!(icon.is_err())
|
||||
}
|
||||
|
||||
// Test 2 const value
|
||||
const_tester(IconRc::GENERIC_APPLICATION(IconSizeKind::Small));
|
||||
const_tester(IconRc::GENERIC_APPLICATION(IconSizeKind::Small));
|
||||
// We pick it from "jpegfile" ProgId
|
||||
ok_tester("imageres.dll", 72);
|
||||
ok_tester("notepad.exe", 0);
|
||||
@@ -218,16 +235,36 @@ fn test_str_rc() {
|
||||
|
||||
#[test]
|
||||
fn test_expand_string() {
|
||||
fn tester(s: &str) {
|
||||
let rv = ExpandString::new(s);
|
||||
assert!(rv.is_ok());
|
||||
let rv = rv.unwrap();
|
||||
fn tester(fmt: &str, var_name: &str) {
|
||||
// We first insert variable name into format string to get the final string
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("0".to_string(), var_name.to_string());
|
||||
let final_string = strfmt::strfmt(fmt, &vars).unwrap();
|
||||
|
||||
let rv = rv.expand_string();
|
||||
assert!(rv.is_ok());
|
||||
// The we try expanding final string first
|
||||
let expand_final_string = ExpandString::new(&final_string);
|
||||
assert!(expand_final_string.is_ok());
|
||||
let expand_final_string = expand_final_string.unwrap();
|
||||
let expanded_final_string = expand_final_string.expand();
|
||||
assert!(expanded_final_string.is_ok());
|
||||
let expanded_final_string = expanded_final_string.unwrap();
|
||||
|
||||
// Then we expand variable name individually
|
||||
let expand_var_name = ExpandString::new(var_name);
|
||||
assert!(expand_var_name.is_ok());
|
||||
let expand_var_name = expand_var_name.unwrap();
|
||||
let expanded_var_name = expand_var_name.expand();
|
||||
assert!(expanded_var_name.is_ok());
|
||||
let expanded_var_name = expanded_var_name.unwrap();
|
||||
|
||||
// Finally, we directly insert expanded variable name into format string
|
||||
// to get the string which can be compared with final string.
|
||||
vars.insert("0".to_string(), expanded_var_name.clone());
|
||||
let built_final_string = strfmt::strfmt(fmt, &vars).unwrap();
|
||||
assert_eq!(expanded_final_string.to_string(), built_final_string);
|
||||
}
|
||||
|
||||
tester(r#"%SystemRoot%\System32\shell32.dll"#);
|
||||
tester(r#"{0}\System32\shell32.dll"#, "%SystemRoot%");
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -281,12 +318,17 @@ fn test_verb() {
|
||||
|
||||
#[test]
|
||||
fn test_cmd_line() {
|
||||
// TODO:
|
||||
// Remove error tester dead_code attribute,
|
||||
// improve ok tester and add more error tests
|
||||
// once we finish a complete CmdLine.
|
||||
fn ok_tester(s: &str) {
|
||||
let rv = CmdLine::from_str(s);
|
||||
assert!(rv.is_ok());
|
||||
// let rv = rv.unwrap();
|
||||
// assert_eq!(s, rv.full());
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
fn err_tester(s: &str) {
|
||||
let rv = CmdLine::from_str(s);
|
||||
assert!(rv.is_err());
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
use wfassoc::highlevel::*;
|
||||
mod common;
|
||||
|
||||
static IDENTIFIER: &str = "Passoc";
|
||||
static APP_PATH: &str = r"C:\Passoc\passoc.exe";
|
||||
static CLSID: &str = "{59031a47-3f72-44a7-89c5-5595fe6b30ee}";
|
||||
static EXT_BODY: &str = "pacfg";
|
||||
|
||||
fn make_valid_schema() -> Schema {
|
||||
let mut schema = Schema::new();
|
||||
schema.set_identifier(IDENTIFIER);
|
||||
schema.set_path(APP_PATH);
|
||||
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", "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"));
|
||||
schema.set_icon(Some("main_icon"));
|
||||
schema.set_behavior(Some("main_behavior"));
|
||||
schema.add_ext(EXT_BODY, "ext_name", "ext_icon", "ext_behavior").unwrap();
|
||||
schema
|
||||
}
|
||||
|
||||
// region: 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 essential parts (schema, path and etc)
|
||||
let schema = Schema::new();
|
||||
let rv = schema.into_program();
|
||||
assert!(rv.is_err());
|
||||
|
||||
// invalid path
|
||||
let mut schema = make_valid_schema();
|
||||
schema.set_path(r"C:\");
|
||||
let rv = schema.into_program();
|
||||
assert!(rv.is_err());
|
||||
|
||||
// prepare a schema for following test
|
||||
let mut schema = Schema::new();
|
||||
schema.set_identifier(IDENTIFIER);
|
||||
schema.set_path(APP_PATH);
|
||||
|
||||
// duplicate detection on add_str
|
||||
let rv = schema.add_str("k", "v1");
|
||||
assert!(rv.is_ok());
|
||||
let rv = schema.add_str("k", "v2");
|
||||
assert!(rv.is_err());
|
||||
|
||||
// duplicate detection on add_icon
|
||||
let rv = schema.add_icon("k", "v1");
|
||||
assert!(rv.is_ok());
|
||||
let rv = schema.add_icon("k", "v2");
|
||||
assert!(rv.is_err());
|
||||
|
||||
// duplicate detection on add_behavior
|
||||
let rv = schema.add_behavior("k", "v1");
|
||||
assert!(rv.is_ok());
|
||||
let rv = schema.add_behavior("k", "v2");
|
||||
assert!(rv.is_err());
|
||||
|
||||
// duplicate detection on add_ext
|
||||
let rv = schema.add_ext(EXT_BODY, "k", "k", "k");
|
||||
assert!(rv.is_ok());
|
||||
let rv = schema.add_ext(EXT_BODY, "k", "k", "k");
|
||||
assert!(rv.is_err());
|
||||
|
||||
// ext referencing non-existent map entry
|
||||
schema.add_str("k2", "v").unwrap();
|
||||
schema.add_icon("k2", "v").unwrap();
|
||||
let rv = schema.add_ext("pacfg2", "nonexistent", "k2", "k2");
|
||||
assert!(rv.is_ok());
|
||||
let rv = schema.into_program();
|
||||
assert!(rv.is_err());
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Program
|
||||
|
||||
#[test]
|
||||
fn test_program() {
|
||||
common::check_sandbox();
|
||||
common::check_privilege();
|
||||
|
||||
fn tester(scope: Scope, view: View) {
|
||||
// build program
|
||||
let schema = make_valid_schema();
|
||||
let rv = schema.into_program();
|
||||
assert!(rv.is_ok());
|
||||
let mut program = rv.unwrap();
|
||||
|
||||
// cleanup before test
|
||||
let rv = program.unregister(scope);
|
||||
assert!(rv.is_ok());
|
||||
|
||||
// initially not registered
|
||||
let rv = program.is_registered(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), false);
|
||||
|
||||
// register
|
||||
let rv = program.register(scope);
|
||||
assert!(rv.is_ok());
|
||||
|
||||
// should be registered now
|
||||
let rv = program.is_registered(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), true);
|
||||
|
||||
// link_ext first so ext key exists before register
|
||||
let rv = program.link_ext(scope, 0);
|
||||
assert!(rv.is_ok());
|
||||
|
||||
// query_ext after link + register
|
||||
let rv = program.query_ext(view, 0);
|
||||
assert!(rv.is_ok());
|
||||
assert!(rv.unwrap().is_some());
|
||||
|
||||
// unlink_ext
|
||||
let rv = program.unlink_ext(scope, 0);
|
||||
assert!(rv.is_ok());
|
||||
|
||||
// query_ext should return None
|
||||
let rv = program.query_ext(view, 0);
|
||||
assert!(rv.is_ok());
|
||||
assert!(rv.unwrap().is_none());
|
||||
|
||||
// resolve_name, resolve_icon
|
||||
let rv = program.resolve_name();
|
||||
assert!(rv.is_ok());
|
||||
let rv = program.resolve_icon();
|
||||
assert!(rv.is_ok());
|
||||
|
||||
// exts_len, find_ext, resolve_ext
|
||||
assert_eq!(program.exts_len(), 1);
|
||||
assert_eq!(program.find_ext(EXT_BODY), Some(0));
|
||||
let rv = program.resolve_ext(0);
|
||||
assert!(rv.is_ok());
|
||||
|
||||
// bad index
|
||||
let rv = program.resolve_ext(1);
|
||||
assert!(rv.is_err());
|
||||
let rv = program.link_ext(scope, 1);
|
||||
assert!(rv.is_err());
|
||||
let rv = program.query_ext(view, 1);
|
||||
assert!(rv.is_err());
|
||||
|
||||
// cleanup
|
||||
let rv = program.unregister(scope);
|
||||
assert!(rv.is_ok());
|
||||
}
|
||||
|
||||
tester(Scope::User, View::User);
|
||||
tester(Scope::System, View::System);
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -0,0 +1,334 @@
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
use wfassoc::lowlevel::*;
|
||||
use wfassoc::win32::concept;
|
||||
mod common;
|
||||
|
||||
static EXT: LazyLock<concept::Ext> = LazyLock::new(|| concept::Ext::from_str(".pacfg").unwrap());
|
||||
static APP_FILE: LazyLock<concept::FileName> =
|
||||
LazyLock::new(|| concept::FileName::from_str("passoc.exe").unwrap());
|
||||
static PROG_ID: LazyLock<LosseProgId> = LazyLock::new(|| "Passoc.Pacfg".into());
|
||||
static ICON: LazyLock<IconResVariant> =
|
||||
LazyLock::new(|| r"%SystemRoot%\System32\imageres.dll,-72".into());
|
||||
static VERB: LazyLock<ShellVerb> = LazyLock::new(|| {
|
||||
let verb = concept::Verb::from_str("open").unwrap();
|
||||
let cmdline = concept::CmdLine::from_str("notepad.exe %1").unwrap();
|
||||
ShellVerb::new(verb, cmdline)
|
||||
});
|
||||
|
||||
// region: AppPathsKey
|
||||
|
||||
#[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";
|
||||
|
||||
fn tester(scope: Scope) {
|
||||
let mut key = AppPathsKey::new(APP_FILE.clone());
|
||||
|
||||
// delete and ensure
|
||||
let rv = key.delete(scope);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.is_exist(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), false);
|
||||
let rv = key.ensure(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), true);
|
||||
let rv = key.is_exist(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), true);
|
||||
let rv = key.ensure(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), false);
|
||||
|
||||
// get/set default
|
||||
let rv = key.set_default(scope, APP_PATH);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_default(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), APP_PATH);
|
||||
|
||||
// get/set path
|
||||
let rv = key.set_path(scope, APP_DIR);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_path(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), APP_DIR);
|
||||
|
||||
// clean up
|
||||
let rv = key.delete(scope);
|
||||
assert!(rv.is_ok());
|
||||
}
|
||||
|
||||
tester(Scope::User);
|
||||
tester(Scope::System);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: ApplicationsKey
|
||||
|
||||
#[test]
|
||||
fn test_applications_key() {
|
||||
common::check_sandbox();
|
||||
common::check_privilege();
|
||||
|
||||
static FRIENDLY_APP_NAME: LazyLock<StrResVariant> =
|
||||
LazyLock::new(|| "Passoc Application".into());
|
||||
|
||||
fn tester(scope: Scope, view: View) {
|
||||
let mut key = ApplicationsKey::new(APP_FILE.clone());
|
||||
|
||||
// delete and ensure
|
||||
let rv = key.delete(scope);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.is_exist(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), false);
|
||||
let rv = key.ensure(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), true);
|
||||
let rv = key.is_exist(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), true);
|
||||
let rv = key.ensure(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), false);
|
||||
|
||||
// get/set shell verb
|
||||
let rv = key.set_shell_verb(scope, Some(&VERB));
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_shell_verb(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), Some(VERB.clone()));
|
||||
let rv = key.set_shell_verb(scope, None);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_shell_verb(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), None);
|
||||
|
||||
// get/set default icon
|
||||
let rv = key.set_default_icon(scope, Some(&ICON));
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_default_icon(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), Some(ICON.clone()));
|
||||
let rv = key.set_default_icon(scope, None);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_default_icon(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), None);
|
||||
|
||||
// get/set friendly app name
|
||||
let rv = key.set_friendly_app_name(scope, Some(&FRIENDLY_APP_NAME));
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_friendly_app_name(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), Some(FRIENDLY_APP_NAME.clone()));
|
||||
let rv = key.set_friendly_app_name(scope, None);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_friendly_app_name(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), None);
|
||||
|
||||
// get/set supported types
|
||||
let rv = key.set_supported_types(scope, Some(&vec![EXT.deref()]));
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_supported_types(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), Some(vec![EXT.clone()]));
|
||||
let rv = key.set_supported_types(scope, None);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_supported_types(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), None);
|
||||
|
||||
// get/set no open with
|
||||
let rv = key.get_no_open_with(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), false);
|
||||
let rv = key.set_no_open_with(scope, true);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_no_open_with(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), true);
|
||||
let rv = key.set_no_open_with(scope, false);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_no_open_with(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), false);
|
||||
|
||||
// clean up
|
||||
let rv = key.delete(scope);
|
||||
assert!(rv.is_ok());
|
||||
}
|
||||
|
||||
tester(Scope::User, View::User);
|
||||
tester(Scope::System, View::System);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: ExtKey
|
||||
|
||||
#[test]
|
||||
fn test_ext_key() {
|
||||
common::check_sandbox();
|
||||
common::check_privilege();
|
||||
|
||||
fn tester(scope: Scope, view: View) {
|
||||
let mut key = ExtKey::new(EXT.clone());
|
||||
|
||||
// delete and ensure
|
||||
let rv = key.delete(scope);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.is_exist(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), false);
|
||||
let rv = key.ensure(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), true);
|
||||
let rv = key.is_exist(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), true);
|
||||
let rv = key.ensure(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), false);
|
||||
|
||||
// get/set default
|
||||
let rv = key.set_default(scope, Some(&PROG_ID));
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_default(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), Some(PROG_ID.clone()));
|
||||
let rv = key.set_default(scope, None);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_default(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), None);
|
||||
|
||||
// test open with progids
|
||||
let rv = key.is_in_open_with_progids(view, &PROG_ID);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), false);
|
||||
|
||||
let rv = key.add_into_open_with_progids(scope, &PROG_ID);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.is_in_open_with_progids(view, &PROG_ID);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), true);
|
||||
|
||||
let rv = key.get_open_with_progids(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), Some(vec![PROG_ID.clone()]));
|
||||
|
||||
let rv = key.remove_from_open_with_progids(scope, &PROG_ID);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.is_in_open_with_progids(view, &PROG_ID);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), false);
|
||||
|
||||
// clean up
|
||||
let rv = key.delete(scope);
|
||||
assert!(rv.is_ok());
|
||||
}
|
||||
|
||||
tester(Scope::User, View::User);
|
||||
tester(Scope::System, View::System);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: ProgIdKey
|
||||
|
||||
#[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> =
|
||||
LazyLock::new(|| "Passoc Pacfg File Type".into());
|
||||
|
||||
fn tester(scope: Scope, view: View) {
|
||||
let mut key = ProgIdKey::new(PROG_ID.clone());
|
||||
|
||||
// delete and ensure
|
||||
let rv = key.delete(scope);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.is_exist(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), false);
|
||||
let rv = key.ensure(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), true);
|
||||
let rv = key.is_exist(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), true);
|
||||
let rv = key.ensure(scope);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), false);
|
||||
|
||||
// get/set default (friendly name, legacy)
|
||||
let rv = key.set_default(scope, Some(&LEGACY_NAME));
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_default(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), Some(LEGACY_NAME.clone()));
|
||||
let rv = key.set_default(scope, None);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_default(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), None);
|
||||
|
||||
// get/set friendly type name
|
||||
let rv = key.set_friendly_type_name(scope, Some(&FRIENDLY_TYPE_NAME));
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_friendly_type_name(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), Some(FRIENDLY_TYPE_NAME.clone()));
|
||||
let rv = key.set_friendly_type_name(scope, None);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_friendly_type_name(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), None);
|
||||
|
||||
// get/set shell verb
|
||||
let rv = key.set_shell_verb(scope, Some(&VERB));
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_shell_verb(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), Some(VERB.clone()));
|
||||
let rv = key.set_shell_verb(scope, None);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_shell_verb(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), None);
|
||||
|
||||
// get/set default icon
|
||||
let rv = key.set_default_icon(scope, Some(&ICON));
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_default_icon(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), Some(ICON.clone()));
|
||||
let rv = key.set_default_icon(scope, None);
|
||||
assert!(rv.is_ok());
|
||||
let rv = key.get_default_icon(view);
|
||||
assert!(rv.is_ok());
|
||||
assert_eq!(rv.unwrap(), None);
|
||||
|
||||
// clean up
|
||||
let rv = key.delete(scope);
|
||||
assert!(rv.is_ok());
|
||||
}
|
||||
|
||||
tester(Scope::User, View::User);
|
||||
tester(Scope::System, View::System);
|
||||
}
|
||||
|
||||
// endregion
|
||||
Reference in New Issue
Block a user