Compare commits
123
Commits
c4b825f7f6
..
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 | ||
|
|
6096265017 | ||
|
|
5f9c893d95 | ||
|
|
2fff56b74d | ||
|
|
94db3fdfd4 | ||
|
|
10b27c0fc2 | ||
|
|
e8ff9a3e4f | ||
|
|
6ff42928f1 | ||
|
|
fed02ad9bc | ||
|
|
fc2097560c | ||
|
|
3e588f0ac9 | ||
|
|
c650290df6 | ||
|
|
a7a9a71e80 | ||
|
|
e9ca5dd5ec | ||
|
|
d2bd4425df | ||
|
|
1509723ada | ||
|
|
69c7592f41 | ||
|
|
6b7019e640 | ||
|
|
d7359d41d4 | ||
|
|
7d8757c0ec | ||
|
|
796b2efb1c | ||
|
|
27ab6721de | ||
|
|
7a03d38f68 | ||
|
|
0b98610e1f | ||
|
|
11c48b4aaf | ||
|
|
2741969164 | ||
|
|
3694a6a82a | ||
|
|
a3456e9fdd | ||
|
|
3f1a070b65 | ||
|
|
4d679588f8 | ||
|
|
1342799303 | ||
|
|
6be27def80 | ||
|
|
81fd224236 | ||
|
|
cc79951ee6 | ||
|
|
23d0a79c0b | ||
|
|
e908e775ed | ||
|
|
217d3d5fd3 | ||
|
|
563ab483fc | ||
|
|
ffd58ff677 | ||
|
|
84a29c862b | ||
|
|
26d867d42f | ||
|
|
d493285900 | ||
|
|
f42c50ce10 | ||
|
|
d5fed1e580 | ||
|
|
f37b4b6652 | ||
|
|
4f0f9670cb | ||
|
|
6e9f91d0e6 | ||
|
|
bcd0d503d4 | ||
|
|
bdb6b4ceee | ||
|
|
dab91f1581 | ||
|
|
4b1f85c2f3 | ||
|
|
eee91d8498 |
@@ -1 +1,2 @@
|
|||||||
|
/.vscode
|
||||||
/target
|
/target
|
||||||
|
|||||||
Generated
+222
-2
@@ -168,6 +168,12 @@ dependencies = [
|
|||||||
"litrs",
|
"litrs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -178,12 +184,40 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.1"
|
version = "1.70.1"
|
||||||
@@ -239,6 +273,28 @@ version = "2.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
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]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@@ -274,6 +330,15 @@ dependencies = [
|
|||||||
"windows-link",
|
"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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.101"
|
version = "1.0.101"
|
||||||
@@ -292,6 +357,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@@ -355,12 +426,66 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
|
||||||
|
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]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strfmt"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29fdc163db75f7b5ffa3daf0c5a7136fb0d4b2f35523cd1769da05e034159feb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -398,6 +523,57 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.9.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.23.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.19"
|
version = "1.0.19"
|
||||||
@@ -428,10 +604,26 @@ version = "1.18.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
|
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"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"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.104"
|
version = "0.2.104"
|
||||||
@@ -495,31 +687,44 @@ dependencies = [
|
|||||||
name = "wfassoc"
|
name = "wfassoc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
"regex",
|
"regex",
|
||||||
|
"strfmt",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"widestring",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wfassoc_dylib"
|
name = "wfassoc-cdylib"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"num_enum",
|
||||||
|
"slotmap",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"wfassoc",
|
"wfassoc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wfassoc_exec"
|
name = "wfassoc-exec"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"comfy-table",
|
"comfy-table",
|
||||||
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"toml",
|
||||||
"wfassoc",
|
"wfassoc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "widestring"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -695,6 +900,15 @@ version = "0.53.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winreg"
|
name = "winreg"
|
||||||
version = "0.55.0"
|
version = "0.55.0"
|
||||||
@@ -704,3 +918,9 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.46.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["wfassoc", "wfassoc_dylib", "wfassoc_exec"]
|
members = ["wfassoc", "wfassoc-cdylib", "wfassoc-exec"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,11 +1,44 @@
|
|||||||
# wfassoc
|
# WFAssoc
|
||||||
|
|
||||||
**W**indows **F**ile **Assoc**iation Library
|
**W**indows **F**ile **Assoc**iation Library
|
||||||
|
|
||||||
**Work In Progress**
|
**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
|
## Introduction
|
||||||
|
|
||||||
* wfassoc: Core Rust library. Rust programmer can directly utilize it.
|
* `wfassoc`: Core Rust library. Rust programmer can directly utilize it.
|
||||||
* wfassoc_dylib: A dynamic library exposed for C/C++ and other languages users.
|
* `wfassoc-cdylib`: A dynamic library exposed for C/C++ and other languages users.
|
||||||
* wfassoc_exec: A executable configuring file assocation according to user given profile and request.
|
* `wfassoc-exec`: A executable configuring file assocation according to user given profile and request.
|
||||||
|
|
||||||
|
If you are a programmer who just want to silently set your file associations for user,
|
||||||
|
`wfassoc-exec` would be your best choice.
|
||||||
|
The only thing you need to do is that write a manifest TOML file,
|
||||||
|
and provide it and `wfassoc-exec` with your executable.
|
||||||
|
By executing `wfassoc-exec` with this TOML file in your executable,
|
||||||
|
you can simply achieve this goal, by analyse its return value to check whether it success.
|
||||||
|
|
||||||
|
However, if you are prefering that let user decide which file associations should be created,
|
||||||
|
even have an UI displaying all current file associations related with this program (like 7-Zip File Manager does),
|
||||||
|
you can choose `wfassoc-cdylib` as your solution.
|
||||||
|
`wfassoc-cdylib` exposes all essential functions for this task.
|
||||||
|
And you can build it with your own program.
|
||||||
|
|
||||||
|
At last, if all scenarios above can not cover your requirements,
|
||||||
|
you can utilize `wfassoc` directly in Rust, change all essential code in your favor,
|
||||||
|
and finally produce the artifact which only works for your requirements.
|
||||||
|
|
||||||
|
For the usage of these projects, please browse their README respectively.
|
||||||
|
|||||||
@@ -0,0 +1,361 @@
|
|||||||
|
# ======== Generic Settings ========
|
||||||
|
|
||||||
|
# Identifier is used as an unique token for your program.
|
||||||
|
# Please choose it carefully to avoid any name conflict.
|
||||||
|
#
|
||||||
|
# The value of identifier should only contain alphabet chars and digits,
|
||||||
|
# and digits should not be the first char (like C variable naming convention).
|
||||||
|
#
|
||||||
|
# For technical details, this identifier is used as the "vendor" part of ProgId.
|
||||||
|
identifier = "PineapplePicture"
|
||||||
|
|
||||||
|
# The fully qualified path to the application.
|
||||||
|
#
|
||||||
|
# A fully qualified path or absolute path always defines an exact path
|
||||||
|
# from a particular drive or device to a target file or directory,
|
||||||
|
# and does not depend on the current drive or current directory.
|
||||||
|
#
|
||||||
|
# Considering Windows use back-slash as path splittor,
|
||||||
|
# which is frequently used as escape char in string syntax,
|
||||||
|
# you can utilize "literal strings" syntax in this TOML file
|
||||||
|
# to quickly write this path as this example following does.
|
||||||
|
path = 'C:\path\to\ppic.exe'
|
||||||
|
|
||||||
|
# CLSID is an unique UUID or GUID of your program,
|
||||||
|
# which makes your program is different with others.
|
||||||
|
#
|
||||||
|
# Although this key is not used now,
|
||||||
|
# I still suggest that generate an unique one for it.
|
||||||
|
clsid = "{B5291320-FE7C-4069-BF87-A0AC327FCD20}"
|
||||||
|
|
||||||
|
# The friendly name of this program.
|
||||||
|
#
|
||||||
|
# The value of this key should be available in "strs" list as the key.
|
||||||
|
#
|
||||||
|
# This key is optional. If you don't fill this key,
|
||||||
|
# Windows will use your executable embedded Win32 resources for friendly name.
|
||||||
|
name = "self"
|
||||||
|
|
||||||
|
# The icon of this program.
|
||||||
|
#
|
||||||
|
# The value of this key should be available in "icons" list as the key.
|
||||||
|
#
|
||||||
|
# This key is optional. If you don't fill this key,
|
||||||
|
# Windows will use the first icon embedded in your executable as icon.
|
||||||
|
icon = "self"
|
||||||
|
|
||||||
|
# The default behavior of your program.
|
||||||
|
#
|
||||||
|
# The value of this key should be available in "behaviors" list as the key.
|
||||||
|
#
|
||||||
|
# This key is optional. If you don't fill this key,
|
||||||
|
# Windows will directly call your executable and passes the file name on the command line.
|
||||||
|
# If your executable need extra options, like `program.exe --file file.rs`,
|
||||||
|
# this key is essential for you.
|
||||||
|
behavior = "generic"
|
||||||
|
|
||||||
|
# ======== String Resources ========
|
||||||
|
|
||||||
|
# "strs" list include all possible used strings in this manifest file.
|
||||||
|
[strs]
|
||||||
|
|
||||||
|
# For each entry located in this list,
|
||||||
|
# its key is the token for referring them, and its value is the true resource.
|
||||||
|
#
|
||||||
|
# The value of this key is a valid "Program String Resource String" like following example does.
|
||||||
|
# When we talk about "Program String Resource String",
|
||||||
|
# it is a string pointing to a string resources located inside program Win32 resources.
|
||||||
|
# It has a specific pattern which show in following example and is familiar with Win32 programmer.
|
||||||
|
#
|
||||||
|
# Due to the same use of back-slash in path part,
|
||||||
|
# you can utilize TOML "literal strings" syntax to write this value easier.
|
||||||
|
# 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.
|
||||||
|
self = "Pineapple Pictures"
|
||||||
|
|
||||||
|
# And more string resources...
|
||||||
|
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 ========
|
||||||
|
|
||||||
|
# "icons" list include all possible used icons in this manifest file.
|
||||||
|
#
|
||||||
|
# Another small hint is that if you just want a single icon for all file associations,
|
||||||
|
# you can just create one entry and refer it in every entries located in extension list.
|
||||||
|
[icons]
|
||||||
|
|
||||||
|
# For each entry located in this list,
|
||||||
|
# its key is the token for referring them, and its value is the true resource.
|
||||||
|
#
|
||||||
|
# The value of this key is a valid "Program Icon Resource String" like following example does.
|
||||||
|
# When we talk about "Program Icon Resource String",
|
||||||
|
# it is a string pointing to a icon resources located inside program Win32 resources.
|
||||||
|
# It has a specific pattern which show in following example and is familiar with Win32 programmer.
|
||||||
|
#
|
||||||
|
# Due to the same use of back-slash in path part,
|
||||||
|
# you can utilize TOML "literal strings" syntax to write this value easier.
|
||||||
|
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.
|
||||||
|
generic = 'C:\path\to\ppic\icons\generic.ico'
|
||||||
|
|
||||||
|
# Add more icon resources...
|
||||||
|
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 ========
|
||||||
|
|
||||||
|
# "behaviors" list include all ways to open with your software in this manifest file.
|
||||||
|
#
|
||||||
|
# For the way to open with your program, let we have an example.
|
||||||
|
# If your program order you use `program.exe --solution solution.toml` to open solution file,
|
||||||
|
# and use `program.exe --file file.rs` to open code file respectively,
|
||||||
|
# you must define two seperated "behaviors" describe this difference ways to open file.
|
||||||
|
# That's the work this list done.
|
||||||
|
#
|
||||||
|
# In addition, if your program only have one way to open file,
|
||||||
|
# please leave one entry and use it everywhere without any doubt.
|
||||||
|
[behaviors]
|
||||||
|
|
||||||
|
# For each entry located in this list,
|
||||||
|
# its key is the token for referring them,
|
||||||
|
# and its value is the command line for running your program.
|
||||||
|
#
|
||||||
|
# Please note that currently wfassoc will not validate this property properly,
|
||||||
|
# because there is no criteria for it. Please write them carefully.
|
||||||
|
# For the maning of place holder like "%1", please check out Microsoft document.
|
||||||
|
#
|
||||||
|
# Due to the same use of back-slash in value,
|
||||||
|
# you can utilize TOML "literal strings" syntax to write this value easier.
|
||||||
|
generic = '"C:\path\to\ppic.exe" "%1"'
|
||||||
|
|
||||||
|
# ======== File Extensions ========
|
||||||
|
|
||||||
|
# "exts" list includeall potential file extensions related with this program,
|
||||||
|
# and the corresponding name, behavior and icon of them.
|
||||||
|
[exts]
|
||||||
|
|
||||||
|
# In this case, we start to define ".jpg" file extension.
|
||||||
|
[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 = "jpeg"
|
||||||
|
# Then, speficy its associated icon.
|
||||||
|
# The value of this field is the token declared above in "icons" list.
|
||||||
|
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.jpeg]
|
||||||
|
name = "jpeg"
|
||||||
|
icon = "jpeg"
|
||||||
|
behavior = "generic"
|
||||||
|
[exts.jfif]
|
||||||
|
name = "jpeg"
|
||||||
|
icon = "jpeg"
|
||||||
|
behavior = "generic"
|
||||||
|
|
||||||
|
[exts.gif]
|
||||||
|
name = "gif"
|
||||||
|
icon = "gif"
|
||||||
|
behavior = "generic"
|
||||||
|
|
||||||
|
[exts.png]
|
||||||
|
name = "png"
|
||||||
|
icon = "png"
|
||||||
|
behavior = "generic"
|
||||||
|
|
||||||
|
[exts.svg]
|
||||||
|
name = "svg"
|
||||||
|
icon = "svg"
|
||||||
|
behavior = "generic"
|
||||||
|
|
||||||
|
[exts.webp]
|
||||||
|
name = "webp"
|
||||||
|
icon = "generic"
|
||||||
|
behavior = "generic"
|
||||||
|
|
||||||
|
[exts.avif]
|
||||||
|
name = "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 = "generic"
|
||||||
|
behavior = "generic"
|
||||||
|
|
||||||
|
[exts.ras]
|
||||||
|
name = "ras"
|
||||||
|
icon = "generic"
|
||||||
|
behavior = "generic"
|
||||||
|
[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"
|
||||||
@@ -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>
|
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wfassoc_dylib"
|
name = "wfassoc-cdylib"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["yyc12345"]
|
authors = ["yyc12345"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "The dynamic wfassoc library exposed for C/C++ and other languages users."
|
description = "The dynamic wfassoc library exposed for C/C++ and other languages users."
|
||||||
license = "SPDX:MIT"
|
license = "SPDX:MIT"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
wfassoc = { path="../wfassoc" }
|
wfassoc = { path="../wfassoc" }
|
||||||
|
slotmap = "1.1.1"
|
||||||
|
num_enum = "0.7.6"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
//! When calling function with dynamic library,
|
||||||
|
//! function return value indicates whether function has been successfully executed.
|
||||||
|
//! When function return `false`, programmer may want to know which error occurs.
|
||||||
|
//!
|
||||||
|
//! This module provide **thread independent** error message string storage,
|
||||||
|
//! which is more like Win32 `GetLastError()` but return error message instead of error code.
|
||||||
|
//! These module provided functions will be called when executing main module functions.
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::ffi::{CString, c_char};
|
||||||
|
|
||||||
|
struct LastError {
|
||||||
|
msg: CString,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LastError {
|
||||||
|
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 error message output");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 LAST_ERROR: RefCell<LastError> = RefCell::new(LastError::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set thread local error message.
|
||||||
|
pub fn set_last_error(msg: &str) {
|
||||||
|
LAST_ERROR.with(|e| {
|
||||||
|
e.borrow_mut().set_msg(msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get const pointer to thread local error message string.
|
||||||
|
/// If there is no error, return pointer will point to empty string.
|
||||||
|
pub fn get_last_error() -> *const c_char {
|
||||||
|
LAST_ERROR.with(|e| e.borrow().get_msg())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear thread local error message (reset to empty string).
|
||||||
|
pub fn clear_last_error() {
|
||||||
|
LAST_ERROR.with(|e| {
|
||||||
|
e.borrow_mut().clear_msg();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,795 @@
|
|||||||
|
mod cstr_ffi;
|
||||||
|
mod ffi_types;
|
||||||
|
mod last_error;
|
||||||
|
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 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 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.
|
||||||
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// 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 {
|
||||||
|
// 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() -> WFCString {
|
||||||
|
last_error::get_last_error()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn WFHasPrivilege() -> bool {
|
||||||
|
wfassoc::win32::utilities::has_privilege()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
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,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wfassoc_exec"
|
name = "wfassoc-exec"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["yyc12345"]
|
authors = ["yyc12345"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
@@ -10,4 +10,6 @@ license = "SPDX:MIT"
|
|||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
wfassoc = { path="../wfassoc" }
|
wfassoc = { path="../wfassoc" }
|
||||||
clap = { version="4.5.48", features=["derive"]}
|
clap = { version="4.5.48", features=["derive"]}
|
||||||
|
serde = { version = "1.0.228", features=["derive"]}
|
||||||
|
toml = "0.9.8"
|
||||||
comfy-table = "7.2.1"
|
comfy-table = "7.2.1"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# WFAssoc Executable
|
||||||
|
|
||||||
|
For the syntax of this executable's parameters, execute it with `--help` for more.
|
||||||
|
|
||||||
|
For how to write manifest TOML file passed to `config` parameter,
|
||||||
|
please see our example located in `example/manifest/ppic.toml`.
|
||||||
|
|
||||||
|
Please note that the value passed to `for` parameter should be consistent when calling `query`, `register` and `unregister`.
|
||||||
|
For example you should not call `register` with `--for user`, and call `unregister` with `--for system` later.
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
|
|
||||||
|
// region: Clap Declaration
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ValueEnum)]
|
||||||
|
pub enum RegScope {
|
||||||
|
#[value(name = "user")]
|
||||||
|
User,
|
||||||
|
#[value(name = "system")]
|
||||||
|
System,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RegScope> for wfassoc::Scope {
|
||||||
|
fn from(value: RegScope) -> Self {
|
||||||
|
match value {
|
||||||
|
RegScope::User => wfassoc::Scope::User,
|
||||||
|
RegScope::System => wfassoc::Scope::System,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ValueEnum)]
|
||||||
|
pub enum RegView {
|
||||||
|
#[value(name = "user")]
|
||||||
|
User,
|
||||||
|
#[value(name = "system")]
|
||||||
|
System,
|
||||||
|
#[value(name = "hybrid")]
|
||||||
|
Hybrid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RegView> for wfassoc::View {
|
||||||
|
fn from(value: RegView) -> Self {
|
||||||
|
match value {
|
||||||
|
RegView::User => wfassoc::View::User,
|
||||||
|
RegView::System => wfassoc::View::System,
|
||||||
|
RegView::Hybrid => wfassoc::View::Hybrid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ValueEnum)]
|
||||||
|
pub enum ExtListStyle {
|
||||||
|
#[value(name = "human")]
|
||||||
|
Human,
|
||||||
|
#[value(name = "machine")]
|
||||||
|
Machine,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple program to manage Windows file associations
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "Windows File Association Operator", version, about)]
|
||||||
|
pub struct Cli {
|
||||||
|
/// The TOML manifest file representing the complete program
|
||||||
|
#[arg(
|
||||||
|
short = 'm',
|
||||||
|
long = "manifest",
|
||||||
|
value_name = "MANIFEST_TOML",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
|
pub manifest_file: String,
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: CliCommands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum CliCommands {
|
||||||
|
#[command(name = "register")]
|
||||||
|
#[command(about = "Register application with given manifest and scope.")]
|
||||||
|
Register {
|
||||||
|
/// The scope where wfassoc operate.
|
||||||
|
#[arg(short = 't', long = "target", value_name = "TARGET", required = true, value_enum, default_value_t = RegScope::User)]
|
||||||
|
target: RegScope,
|
||||||
|
},
|
||||||
|
#[command(name = "unregister")]
|
||||||
|
#[command(about = "Unregister application with given manifest and scope.")]
|
||||||
|
Unregister {
|
||||||
|
/// The scope where wfassoc operate.
|
||||||
|
#[arg(short = 't', long = "target", value_name = "TARGET", required = true, value_enum, default_value_t = RegScope::User)]
|
||||||
|
target: RegScope,
|
||||||
|
},
|
||||||
|
#[command(name = "status")]
|
||||||
|
#[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 = RegScope::User)]
|
||||||
|
target: RegScope,
|
||||||
|
},
|
||||||
|
#[command(name = "ext")]
|
||||||
|
#[command(about = "File extension related operations according to given program manifest.")]
|
||||||
|
Ext {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: CliExtCommands,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum CliExtCommands {
|
||||||
|
#[command(name = "link")]
|
||||||
|
#[command(about = "Link user given file extension to the program declared in manifest file.")]
|
||||||
|
Link {
|
||||||
|
/// The scope where link 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.
|
||||||
|
#[arg(required = true, value_name = "EXTS", num_args = 1..)]
|
||||||
|
exts: Vec<String>,
|
||||||
|
},
|
||||||
|
#[command(name = "unlink")]
|
||||||
|
#[command(
|
||||||
|
about = "Unlink user given file extension from the program declared in manifest file."
|
||||||
|
)]
|
||||||
|
Unlink {
|
||||||
|
/// 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 (without leading dot) used for this operation. Specify * for all file extension.
|
||||||
|
#[arg(required = true, value_name = "EXTS", num_args = 1..)]
|
||||||
|
exts: Vec<String>,
|
||||||
|
},
|
||||||
|
#[command(name = "list")]
|
||||||
|
#[command(
|
||||||
|
about = "List the association status for all extensions declared in given program manifest."
|
||||||
|
)]
|
||||||
|
List {
|
||||||
|
/// The view where list file extension.
|
||||||
|
#[arg(short = 't', long = "target", value_name = "TARGET", required = true, value_enum, default_value_t = RegView::User)]
|
||||||
|
target: RegView,
|
||||||
|
/// The style when listing file extensions.
|
||||||
|
#[arg(short = 's', long = "style", value_name = "STYLE", required = true, value_enum, default_value_t = ExtListStyle::Human)]
|
||||||
|
style: ExtListStyle,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
/// Parse current commandline argument.
|
||||||
|
///
|
||||||
|
/// Please note that if there is "help" or "version" command matched,
|
||||||
|
/// or any command line parse error occurs,
|
||||||
|
/// this function will order program exit immediately.
|
||||||
|
/// This is the mechanism of `clap` crate.
|
||||||
|
pub fn parse() -> Cli {
|
||||||
|
Cli::parse()
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
pub(crate) mod cli;
|
||||||
|
pub(crate) mod manifest;
|
||||||
|
pub(crate) mod runner;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = cli::parse();
|
||||||
|
runner::run(cli).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Runtime error: {}.", e);
|
||||||
|
std::process::exit(1)
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
use thiserror::Error as TeError;
|
||||||
|
use toml;
|
||||||
|
use wfassoc;
|
||||||
|
|
||||||
|
// region: Manifest
|
||||||
|
|
||||||
|
/// Raw user input manifest.
|
||||||
|
///
|
||||||
|
/// This manifest is the raw input of user.
|
||||||
|
/// Some fields may not be checked due to user invalid input.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Manifest {
|
||||||
|
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, ManifestExt>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sub-type in raw user input manifest.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ManifestExt {
|
||||||
|
name: String,
|
||||||
|
icon: String,
|
||||||
|
behavior: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Manifest Operation
|
||||||
|
|
||||||
|
/// Error occurs when parsing manifest TOML file.
|
||||||
|
#[derive(Debug, TeError)]
|
||||||
|
pub enum ParseManifestError {
|
||||||
|
/// Io error
|
||||||
|
#[error("IO error when reading manifest file")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
/// Toml deserialization error
|
||||||
|
#[error("given TOML manifest file has bad format")]
|
||||||
|
Toml(#[from] toml::de::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manifest {
|
||||||
|
/// Read user manifest.
|
||||||
|
pub fn from_file(path: &Path) -> Result<Manifest, ParseManifestError> {
|
||||||
|
let contents = std::fs::read_to_string(path)?;
|
||||||
|
let config: Manifest = toml::from_str(&contents)?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error occurs when parsing manifest into schema.
|
||||||
|
#[derive(Debug, TeError)]
|
||||||
|
pub enum ParseSchemaError {
|
||||||
|
/// Error when operating with schema.
|
||||||
|
#[error("failed on parsing manifest file to Schema: {0}")]
|
||||||
|
Schema(#[from] wfassoc::highlevel::SchemaError)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Manifest> for wfassoc::Schema {
|
||||||
|
type Error = ParseSchemaError;
|
||||||
|
|
||||||
|
fn try_from(value: Manifest) -> Result<Self, Self::Error> {
|
||||||
|
let mut schema = wfassoc::Schema::new();
|
||||||
|
|
||||||
|
schema.set_identifier(&value.identifier);
|
||||||
|
schema.set_path(&value.path);
|
||||||
|
schema.set_clsid(&value.clsid);
|
||||||
|
schema.set_name(value.name.as_ref().map(|x| x.as_str()));
|
||||||
|
schema.set_icon(value.icon.as_ref().map(|x| x.as_str()));
|
||||||
|
schema.set_behavior(value.behavior.as_ref().map(|x| x.as_str()));
|
||||||
|
for (key, value) in value.strs {
|
||||||
|
schema.add_str(&key, &value)?;
|
||||||
|
}
|
||||||
|
for (key, value) in value.icons {
|
||||||
|
schema.add_icon(&key, &value)?;
|
||||||
|
}
|
||||||
|
for (key, value) in value.behaviors {
|
||||||
|
schema.add_behavior(&key, &value)?;
|
||||||
|
}
|
||||||
|
for (key, value) in value.exts {
|
||||||
|
schema.add_ext(&key, &value.name, &value.icon, &value.behavior)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manifest {
|
||||||
|
pub fn into_schema(self) -> Result<wfassoc::Schema, ParseSchemaError> {
|
||||||
|
wfassoc::Schema::try_from(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
/// Error occurs in this module.
|
||||||
|
#[derive(Debug, TeError)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Error when parsing Manifest TOML file.
|
||||||
|
#[error("initialization error: {0}")]
|
||||||
|
ParseManifest(#[from] manifest::ParseManifestError),
|
||||||
|
/// Error when parsing Manifest into Schema.
|
||||||
|
#[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("wfassoc error: {0}")]
|
||||||
|
ParseProgram(#[from] wfassoc::highlevel::ParseProgramError),
|
||||||
|
/// Error when operating Program.
|
||||||
|
#[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.
|
||||||
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Utilities
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// region: Respective Runners
|
||||||
|
|
||||||
|
fn run_register(mut program: wfassoc::Program, scope: wfassoc::Scope) -> Result<()> {
|
||||||
|
program.register(scope)?;
|
||||||
|
println!("Application now is installed.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_unregister(mut program: wfassoc::Program, scope: wfassoc::Scope) -> Result<()> {
|
||||||
|
program.unregister(scope)?;
|
||||||
|
println!("Application now is uninstalled.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
mut 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)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_ext_list(
|
||||||
|
program: wfassoc::Program,
|
||||||
|
view: wfassoc::View,
|
||||||
|
style: cli::ExtListStyle,
|
||||||
|
) -> Result<()> {
|
||||||
|
// 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
|
||||||
|
|
||||||
|
pub fn run(c: cli::Cli) -> Result<()> {
|
||||||
|
// Read manifest file first
|
||||||
|
let mf = manifest::Manifest::from_file(Path::new(&c.manifest_file))?;
|
||||||
|
// Parse it into schema
|
||||||
|
let schema = mf.into_schema()?;
|
||||||
|
// Parse it into program
|
||||||
|
let program = schema.into_program()?;
|
||||||
|
|
||||||
|
match c.command {
|
||||||
|
cli::CliCommands::Register { target } => run_register(program, target.into()),
|
||||||
|
cli::CliCommands::Unregister { target } => run_unregister(program, target.into()),
|
||||||
|
cli::CliCommands::Status { target } => run_status(program, target.into()),
|
||||||
|
cli::CliCommands::Ext { command } => match command {
|
||||||
|
cli::CliExtCommands::Link { target, exts } => {
|
||||||
|
run_ext_link(program, target.into(), exts)
|
||||||
|
}
|
||||||
|
cli::CliExtCommands::Unlink { target, exts } => {
|
||||||
|
run_ext_unlink(program, target.into(), exts)
|
||||||
|
}
|
||||||
|
cli::CliExtCommands::List { target, style } => {
|
||||||
|
run_ext_list(program, target.into(), style)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-2
@@ -8,7 +8,20 @@ license = "SPDX:MIT"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
windows-sys = { version = "0.60.2", features = ["Win32_Security", "Win32_System_SystemServices"] }
|
windows-sys = { version = "0.60.2", features = [
|
||||||
|
"Win32_UI_Shell",
|
||||||
|
"Win32_UI_WindowsAndMessaging",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_System_Environment",
|
||||||
|
"Win32_System_LibraryLoader",
|
||||||
|
"Win32_System_Registry",
|
||||||
|
"Win32_System_SystemServices",
|
||||||
|
] }
|
||||||
winreg = { version = "0.55.0", features = ["transactions"] }
|
winreg = { version = "0.55.0", features = ["transactions"] }
|
||||||
|
widestring = "1.2.1"
|
||||||
|
indexmap = "2.11.4"
|
||||||
regex = "1.11.3"
|
regex = "1.11.3"
|
||||||
uuid = "1.18.1"
|
uuid = { version = "1.18.1", features = ["v4"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
strfmt = "0.2.5"
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# WFAssoc Core
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This crate provides low level API and high level API for manipulating Windows file associations at the same time.
|
||||||
|
For the convenient use of this project, the root module of this crate re-expose high level API.
|
||||||
|
So programmers can directly use them.
|
||||||
|
|
||||||
|
High level API is type-safe and very easy to use for common application.
|
||||||
|
For visiting high level API, please use crate root module directly or `highlevel` module.
|
||||||
|
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.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//! 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};
|
||||||
|
|
||||||
|
// 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
|
||||||
+6
-308
@@ -1,314 +1,12 @@
|
|||||||
//! This crate provide utilities fetching and manilupating Windows file association.
|
//! 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"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
compile_error!("Crate wfassoc is only supported on Windows.");
|
compile_error!("Crate wfassoc is only supported on Windows.");
|
||||||
|
|
||||||
use std::ffi::OsStr;
|
pub mod utilities;
|
||||||
use std::path::{Path, PathBuf};
|
pub mod win32;
|
||||||
use thiserror::Error as TeError;
|
pub mod lowlevel;
|
||||||
use winreg::RegKey;
|
pub mod highlevel;
|
||||||
use winreg::enums::{
|
|
||||||
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
|
|
||||||
};
|
|
||||||
use winreg::transaction::Transaction;
|
|
||||||
|
|
||||||
// region: Error Types
|
pub use highlevel::{Scope, View, Schema, Program};
|
||||||
|
|
||||||
/// All possible error occurs in this crate.
|
|
||||||
#[derive(Debug, TeError)]
|
|
||||||
pub enum WfError {
|
|
||||||
#[error("no administrative privilege")]
|
|
||||||
NoPrivilege,
|
|
||||||
#[error("error occurs when manipulating with Registry: {0}")]
|
|
||||||
BadRegOper(#[from] std::io::Error),
|
|
||||||
#[error("given full path to application is invalid")]
|
|
||||||
BadFullAppPath,
|
|
||||||
#[error("failed when casting path or OS string into string")]
|
|
||||||
BadOsStrCast,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The result type used in this crate.
|
|
||||||
pub type WfResult<T> = Result<T, WfError>;
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Scope and View
|
|
||||||
|
|
||||||
/// 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 any scope")]
|
|
||||||
pub struct TryFromViewError {}
|
|
||||||
|
|
||||||
impl TryFromViewError {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, return true, otherwise false.
|
|
||||||
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 !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: 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) {
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// For one or more arguments like println!.
|
|
||||||
($($arg:tt)*) => {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
println!($($arg)*);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
fn has_privilege() -> bool {
|
|
||||||
use windows_sys::Win32::Foundation::HANDLE;
|
|
||||||
use windows_sys::Win32::Security::{
|
|
||||||
AllocateAndInitializeSid, CheckTokenMembership, FreeSid, PSID, SECURITY_NT_AUTHORITY,
|
|
||||||
};
|
|
||||||
use windows_sys::Win32::System::SystemServices::{
|
|
||||||
DOMAIN_ALIAS_RID_ADMINS, SECURITY_BUILTIN_DOMAIN_RID,
|
|
||||||
};
|
|
||||||
use windows_sys::core::BOOL;
|
|
||||||
|
|
||||||
let nt_authority = SECURITY_NT_AUTHORITY.clone();
|
|
||||||
let mut administrators_group: PSID = PSID::default();
|
|
||||||
let success: BOOL = unsafe {
|
|
||||||
AllocateAndInitializeSid(
|
|
||||||
&nt_authority,
|
|
||||||
2,
|
|
||||||
SECURITY_BUILTIN_DOMAIN_RID as u32,
|
|
||||||
DOMAIN_ALIAS_RID_ADMINS as u32,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
&mut administrators_group,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if success == 0 {
|
|
||||||
panic!("Win32 AllocateAndInitializeSid() failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut is_member: BOOL = BOOL::default();
|
|
||||||
let success: BOOL =
|
|
||||||
unsafe { CheckTokenMembership(HANDLE::default(), administrators_group, &mut is_member) };
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
FreeSid(administrators_group);
|
|
||||||
}
|
|
||||||
|
|
||||||
if success == 0 {
|
|
||||||
panic!("Win32 CheckTokenMembership() failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
is_member != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try casting given &Path into &str.
|
|
||||||
fn path_to_str(path: &Path) -> WfResult<&str> {
|
|
||||||
path.to_str().ok_or(WfError::BadOsStrCast)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try casting given &OsStr into &str.
|
|
||||||
fn osstr_to_str(osstr: &OsStr) -> WfResult<&str> {
|
|
||||||
osstr.to_str().ok_or(WfError::BadOsStrCast)
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Registrar
|
|
||||||
|
|
||||||
/// The core registrar for register and unregister application.
|
|
||||||
pub struct Registrar {
|
|
||||||
/// The fully qualified path to the application.
|
|
||||||
full_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Registrar {
|
|
||||||
/// Create a new registrar for following operations.
|
|
||||||
pub fn new(full_path: &Path) -> Self {
|
|
||||||
Self {
|
|
||||||
full_path: full_path.to_path_buf(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Registrar {
|
|
||||||
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) -> WfResult<()> {
|
|
||||||
// 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("", &path_to_str(&self.full_path)?)?;
|
|
||||||
subkey.set_value("Path", &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
|
|
||||||
subkey.set_value("FriendlyAppName", &"WoW!")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unregister this application.
|
|
||||||
pub fn unregister(&self, scope: Scope) -> WfResult<()> {
|
|
||||||
// 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)?;
|
|
||||||
|
|
||||||
// Okey
|
|
||||||
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) -> WfResult<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 Registrar {
|
|
||||||
/// Extract the file name part from full path to application,
|
|
||||||
/// which was used in Registry path component.
|
|
||||||
fn extract_file_name(&self) -> WfResult<&OsStr> {
|
|
||||||
// Get the file name part and make sure it is not empty
|
|
||||||
self.full_path
|
|
||||||
.file_name()
|
|
||||||
.and_then(|p| if p.is_empty() { None } else { Some(p) })
|
|
||||||
.ok_or(WfError::BadFullAppPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract the start in path from full path to application,
|
|
||||||
/// which basically is the stem of full path.
|
|
||||||
fn extract_start_in(&self) -> WfResult<&OsStr> {
|
|
||||||
// Get parent part and make sure it is not empty
|
|
||||||
self.full_path
|
|
||||||
.parent()
|
|
||||||
.map(|p| p.as_os_str())
|
|
||||||
.and_then(|p| if p.is_empty() { None } else { Some(p) })
|
|
||||||
.ok_or(WfError::BadFullAppPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|||||||
@@ -0,0 +1,492 @@
|
|||||||
|
//! 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::{KEY_READ, KEY_WRITE};
|
||||||
|
|
||||||
|
// region: Error and Result Types
|
||||||
|
|
||||||
|
/// Error occurs in this module.
|
||||||
|
#[derive(Debug, TeError)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("some lowlevel operations require escalating privilege")]
|
||||||
|
NoPrivilege,
|
||||||
|
#[error("given lowlevel registry key is not presented")]
|
||||||
|
InexistantKey,
|
||||||
|
|
||||||
|
#[error("lowlevel registry operation error: {0}")]
|
||||||
|
BadRegOp(#[from] std::io::Error),
|
||||||
|
#[error("unexpected blank registry key in lowlevel registry operation")]
|
||||||
|
UnexpectedBlankKey(#[from] regext::BlankPathError),
|
||||||
|
|
||||||
|
#[error("lowlevel operation error: {0}")]
|
||||||
|
ExpandEnvVar(#[from] concept::ExpandEnvVarError),
|
||||||
|
#[error("lowlevel operation error: {0}")]
|
||||||
|
LoadIconRc(#[from] concept::LoadIconRcError),
|
||||||
|
#[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: Exposed Stuff
|
||||||
|
|
||||||
|
// region: Scope
|
||||||
|
|
||||||
|
/// 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 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 {
|
||||||
|
/// 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: 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 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),
|
||||||
|
Strict(concept::ProgId),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for LosseProgId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
LosseProgId::Plain(v) => v.fmt(f),
|
||||||
|
LosseProgId::Strict(v) => v.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for LosseProgId {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
// match it for standard ProgId first
|
||||||
|
if let Ok(v) = concept::ProgId::from_str(s) {
|
||||||
|
Self::Strict(v)
|
||||||
|
} else {
|
||||||
|
// fallback with other
|
||||||
|
Self::Plain(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<concept::ProgId> for LosseProgId {
|
||||||
|
fn from(value: concept::ProgId) -> Self {
|
||||||
|
Self::Strict(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// 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 IconResVariant {
|
||||||
|
Plain(String),
|
||||||
|
RefStr(concept::IconRefStr),
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 IconResVariant {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
IconResVariant::Plain(v) => v.fmt(f),
|
||||||
|
IconResVariant::RefStr(v) => v.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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::RefStr(v)
|
||||||
|
} else {
|
||||||
|
// fallback with other
|
||||||
|
Self::Plain(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<concept::IconRefStr> for IconResVariant {
|
||||||
|
fn from(value: concept::IconRefStr) -> Self {
|
||||||
|
Self::RefStr(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// 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 StrResVariant {
|
||||||
|
Plain(String),
|
||||||
|
RefStr(concept::StrRefStr),
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
StrResVariant::Plain(v) => v.clone(),
|
||||||
|
// For string reference string, we try to resolve it.
|
||||||
|
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 StrResVariant {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
StrResVariant::Plain(v) => v.fmt(f),
|
||||||
|
StrResVariant::RefStr(v) => v.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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::RefStr(v)
|
||||||
|
} else {
|
||||||
|
// fallback with other
|
||||||
|
Self::Plain(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<concept::StrRefStr> for StrResVariant {
|
||||||
|
fn from(value: concept::StrRefStr) -> Self {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Internal Stuff
|
||||||
|
|
||||||
|
// 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 registry key.
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
enum OpenKeyTerritory {
|
||||||
|
System,
|
||||||
|
User,
|
||||||
|
Hybrid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Scope> for OpenKeyTerritory {
|
||||||
|
fn from(value: Scope) -> Self {
|
||||||
|
match value {
|
||||||
|
Scope::User => Self::User,
|
||||||
|
Scope::System => Self::System,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<View> for OpenKeyTerritory {
|
||||||
|
fn from(value: View) -> Self {
|
||||||
|
match value {
|
||||||
|
View::User => Self::User,
|
||||||
|
View::System => Self::System,
|
||||||
|
View::Hybrid => Self::Hybrid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Open Key Purpose
|
||||||
|
|
||||||
|
/// The purpose of opening given registry key.
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
enum OpenKeyPurpose {
|
||||||
|
/// Only read something.
|
||||||
|
Read,
|
||||||
|
/// Need to write something.
|
||||||
|
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<()> {
|
||||||
|
match purpose {
|
||||||
|
// Read is okey for every territory.
|
||||||
|
OpenKeyPurpose::Read => Ok(()),
|
||||||
|
// Read and write do not works in Hybrid,
|
||||||
|
// and should check privilege in System.
|
||||||
|
OpenKeyPurpose::ReadWrite => match territory {
|
||||||
|
OpenKeyTerritory::System => {
|
||||||
|
if utilities::has_privilege() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::NoPrivilege)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenKeyTerritory::User => Ok(()),
|
||||||
|
OpenKeyTerritory::Hybrid => Err(Error::NoPrivilege),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// This function should be eliminated or moved to another place,
|
||||||
|
// once we finish the quote mechanism in StrRefStr and IconRefStr.
|
||||||
|
|
||||||
|
/// 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: Registry Keys
|
||||||
|
|
||||||
|
// YYC MARK:
|
||||||
|
// Load submodules and re-export them.
|
||||||
|
|
||||||
|
mod app_path_key;
|
||||||
|
mod applications_key;
|
||||||
|
mod ext_key;
|
||||||
|
mod progid_key;
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
//! The module containing useful stuff used in this crate.
|
||||||
|
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::iter::FusedIterator;
|
||||||
|
use std::path::Path;
|
||||||
|
use thiserror::Error as TeError;
|
||||||
|
|
||||||
|
// region: OS String Casting
|
||||||
|
|
||||||
|
/// The error occurs when casting [OsStr] into [str].
|
||||||
|
#[derive(Debug, TeError)]
|
||||||
|
#[error("fail to cast OS string into Rust string")]
|
||||||
|
pub struct CastOsStrError {}
|
||||||
|
|
||||||
|
impl CastOsStrError {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try casting given &Path into &str.
|
||||||
|
pub fn path_to_str(path: &Path) -> Result<&str, CastOsStrError> {
|
||||||
|
path.to_str().ok_or(CastOsStrError::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try casting given &OsStr into &str.
|
||||||
|
pub fn osstr_to_str(osstr: &OsStr) -> Result<&str, CastOsStrError> {
|
||||||
|
osstr.to_str().ok_or(CastOsStrError::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// 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>,
|
||||||
|
{
|
||||||
|
is_first: bool,
|
||||||
|
iter: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> CapitalizeFirstAscii<T>
|
||||||
|
where
|
||||||
|
T: Iterator<Item = char>,
|
||||||
|
{
|
||||||
|
fn new(iter: T) -> Self {
|
||||||
|
Self {
|
||||||
|
is_first: false,
|
||||||
|
iter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Iterator for CapitalizeFirstAscii<T>
|
||||||
|
where
|
||||||
|
T: Iterator<Item = char>,
|
||||||
|
{
|
||||||
|
type Item = char;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.iter.next().map(|c| {
|
||||||
|
if self.is_first {
|
||||||
|
self.is_first = false;
|
||||||
|
c.to_ascii_uppercase()
|
||||||
|
} else {
|
||||||
|
c.to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> FusedIterator for CapitalizeFirstAscii<T> where T: Iterator<Item = char> + FusedIterator {}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
@@ -0,0 +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;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,280 @@
|
|||||||
|
//! This module extend `winreg` crate to make it more suit for the usage of this crate.
|
||||||
|
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
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
|
||||||
|
|
||||||
|
/// 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],
|
||||||
|
/// it differ "no such subkey" error and other access error.
|
||||||
|
pub fn try_open_subkey_with_flags<P: AsRef<OsStr>>(
|
||||||
|
regkey: &RegKey,
|
||||||
|
path: P,
|
||||||
|
perms: REG_SAM_FLAGS,
|
||||||
|
) -> std::io::Result<Option<RegKey>> {
|
||||||
|
match regkey.open_subkey_with_flags(path, perms) {
|
||||||
|
Ok(v) => Ok(Some(v)),
|
||||||
|
Err(e) => match e.raw_os_error() {
|
||||||
|
Some(errno) => match errno as u32 {
|
||||||
|
ERROR_FILE_NOT_FOUND => Ok(None),
|
||||||
|
_ => Err(e),
|
||||||
|
},
|
||||||
|
_ => Err(e),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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],
|
||||||
|
/// it differ "no such key" error and other access error.
|
||||||
|
pub fn try_get_value<T: FromRegValue, N: AsRef<OsStr>>(
|
||||||
|
regkey: &RegKey,
|
||||||
|
name: N,
|
||||||
|
) -> std::io::Result<Option<T>> {
|
||||||
|
match regkey.get_value::<T, N>(name) {
|
||||||
|
Ok(v) => Ok(Some(v)),
|
||||||
|
Err(e) => match e.raw_os_error() {
|
||||||
|
Some(errno) => match errno as u32 {
|
||||||
|
ERROR_FILE_NOT_FOUND => Ok(None),
|
||||||
|
_ => Err(e),
|
||||||
|
},
|
||||||
|
_ => Err(e),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
|
||||||
|
/// The error occurs when given registry path is empty.
|
||||||
|
#[derive(Debug, TeError)]
|
||||||
|
#[error("unexpected empty registry path")]
|
||||||
|
pub struct BlankPathError {}
|
||||||
|
|
||||||
|
impl BlankPathError {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether given registry path is empty.
|
||||||
|
/// If it is, return error, otherwise the given path itself.
|
||||||
|
///
|
||||||
|
/// 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())
|
||||||
|
} else {
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// 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)]
|
||||||
|
pub struct ExpandString(String);
|
||||||
|
|
||||||
|
impl ExpandString {
|
||||||
|
/// Construct new ExpandString.
|
||||||
|
pub fn new(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from &str
|
||||||
|
pub fn from_str(s: &str) -> Self {
|
||||||
|
Self(s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get reference to internal String.
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get mutable reference to internal String.
|
||||||
|
pub fn as_mut_str(&mut self) -> &mut String {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Comsule self, return internal String.
|
||||||
|
pub fn into_inner(self) -> String {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Deref trait to make it can be used like &str
|
||||||
|
impl Deref for ExpandString {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement DerefMut trait
|
||||||
|
impl DerefMut for ExpandString {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement From/Into trait for explicit convertion
|
||||||
|
impl From<String> for ExpandString {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self::new(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExpandString> for String {
|
||||||
|
fn from(expand: ExpandString) -> Self {
|
||||||
|
expand.into_inner()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for ExpandString {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
Self::from_str(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// endregion
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//! The module contains some Windows-specific functions for file associations.
|
||||||
|
//! These functions can not be grouped as Windows concept.
|
||||||
|
//! So they are placed in there as an independent module.
|
||||||
|
|
||||||
|
/// 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>
|
||||||
|
///
|
||||||
|
/// # 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::{
|
||||||
|
AllocateAndInitializeSid, CheckTokenMembership, FreeSid, PSID, SECURITY_NT_AUTHORITY,
|
||||||
|
};
|
||||||
|
use windows_sys::Win32::System::SystemServices::{
|
||||||
|
DOMAIN_ALIAS_RID_ADMINS, SECURITY_BUILTIN_DOMAIN_RID,
|
||||||
|
};
|
||||||
|
use windows_sys::core::BOOL;
|
||||||
|
|
||||||
|
let nt_authority = SECURITY_NT_AUTHORITY.clone();
|
||||||
|
let mut administrators_group: PSID = PSID::default();
|
||||||
|
let success: BOOL = unsafe {
|
||||||
|
AllocateAndInitializeSid(
|
||||||
|
&nt_authority,
|
||||||
|
2,
|
||||||
|
SECURITY_BUILTIN_DOMAIN_RID as u32,
|
||||||
|
DOMAIN_ALIAS_RID_ADMINS as u32,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
&mut administrators_group,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if success == 0 {
|
||||||
|
panic!("Win32 AllocateAndInitializeSid() failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut is_member: BOOL = BOOL::default();
|
||||||
|
let success: BOOL =
|
||||||
|
unsafe { CheckTokenMembership(HANDLE::default(), administrators_group, &mut is_member) };
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
FreeSid(administrators_group);
|
||||||
|
}
|
||||||
|
|
||||||
|
if success == 0 {
|
||||||
|
panic!("Win32 CheckTokenMembership() failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
is_member != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Notify Windows that some file associations are changed, and should refresh them.
|
||||||
|
/// This function must be called once you change any file associations.
|
||||||
|
pub fn notify_assoc_changed() -> () {
|
||||||
|
use windows_sys::Win32::UI::Shell::{SHCNE_ASSOCCHANGED, SHCNF_IDLIST, SHChangeNotify};
|
||||||
|
unsafe {
|
||||||
|
SHChangeNotify(
|
||||||
|
SHCNE_ASSOCCHANGED as i32,
|
||||||
|
SHCNF_IDLIST,
|
||||||
|
std::ptr::null(),
|
||||||
|
std::ptr::null(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
use wfassoc::win32::concept::*;
|
||||||
|
|
||||||
|
// region: File Extension
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ext_new() {
|
||||||
|
fn ok_tester(s: &str, probe: &str) {
|
||||||
|
let rv = Ext::new(s);
|
||||||
|
assert!(rv.is_ok());
|
||||||
|
let rv = rv.unwrap();
|
||||||
|
assert_eq!(rv.to_string(), probe);
|
||||||
|
}
|
||||||
|
fn err_tester(s: &str) {
|
||||||
|
let rv = Ext::new(s);
|
||||||
|
assert!(rv.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
ok_tester("jpg", ".jpg");
|
||||||
|
err_tester(".jpg");
|
||||||
|
err_tester("");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ext_parse() {
|
||||||
|
fn ok_tester(s: &str, probe: &str) {
|
||||||
|
let rv = Ext::from_str(s);
|
||||||
|
assert!(rv.is_ok());
|
||||||
|
let rv = rv.unwrap();
|
||||||
|
assert_eq!(rv.inner(), probe);
|
||||||
|
}
|
||||||
|
fn err_tester(s: &str) {
|
||||||
|
let rv = Ext::from_str(s);
|
||||||
|
assert!(rv.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
ok_tester(".jpg", "jpg");
|
||||||
|
err_tester(".");
|
||||||
|
err_tester(".jar.disabled");
|
||||||
|
err_tester("jar");
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Programmatic Identifiers
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prog_id_new() {
|
||||||
|
fn ok_tester(vendor: &str, component: &str, version: Option<u32>, probe: &str) {
|
||||||
|
let rv = ProgId::new(vendor, component, version);
|
||||||
|
assert!(rv.is_ok());
|
||||||
|
let rv = rv.unwrap();
|
||||||
|
assert_eq!(rv.to_string(), probe);
|
||||||
|
}
|
||||||
|
fn err_tester(vendor: &str, component: &str, version: Option<u32>) {
|
||||||
|
let rv = ProgId::new(vendor, component, version);
|
||||||
|
assert!(rv.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
ok_tester("VSCode", "c++", None, "VSCode.c++");
|
||||||
|
ok_tester("PowerPoint", "Template", Some(12), "PowerPoint.Template.12");
|
||||||
|
err_tester("", "MyApp", None);
|
||||||
|
err_tester("Me", "", None);
|
||||||
|
err_tester("M.e", "MyApp", None);
|
||||||
|
err_tester("Me", "My.App", None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prog_id_parse() {
|
||||||
|
fn ok_tester(s: &str, probe_vendor: &str, probe_component: &str, probe_version: Option<u32>) {
|
||||||
|
let rv = ProgId::from_str(s);
|
||||||
|
assert!(rv.is_ok());
|
||||||
|
let rv = rv.unwrap();
|
||||||
|
assert_eq!(rv.get_vendor(), probe_vendor);
|
||||||
|
assert_eq!(rv.get_component(), probe_component);
|
||||||
|
assert_eq!(rv.get_version(), probe_version);
|
||||||
|
}
|
||||||
|
fn err_tester(s: &str) {
|
||||||
|
let rv = ProgId::from_str(s);
|
||||||
|
assert!(rv.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
ok_tester("VSCode.c++", "VSCode", "c++", None);
|
||||||
|
ok_tester("PowerPoint.Template.12", "PowerPoint", "Template", Some(12));
|
||||||
|
err_tester("Me.MyApp.");
|
||||||
|
err_tester("WMP11.AssocFile.3G2");
|
||||||
|
err_tester("What the f*ck?");
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: CLSID
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clsid() {
|
||||||
|
fn ok_tester(s: &str) {
|
||||||
|
let rv = Clsid::from_str(s);
|
||||||
|
assert!(rv.is_ok());
|
||||||
|
let rv = rv.unwrap();
|
||||||
|
assert_eq!(s.to_lowercase(), rv.to_string().to_lowercase());
|
||||||
|
}
|
||||||
|
fn err_tester(s: &str) {
|
||||||
|
let rv = Clsid::from_str(s);
|
||||||
|
assert!(rv.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
ok_tester("{59031a47-3f72-44a7-89c5-5595fe6b30ee}");
|
||||||
|
ok_tester("{26EE0668-A00A-44D7-9371-BEB064C98683}");
|
||||||
|
err_tester("26EE0668-A00A-44D7-9371-BEB064C98683");
|
||||||
|
err_tester("{26EE0668A00A-44D7-9371-BEB064C98683}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Windows Resource Reference String
|
||||||
|
|
||||||
|
// region: Icon Reference String
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_icon_ref_str() {
|
||||||
|
fn ok_tester(s: &str, probe: (&str, u32)) {
|
||||||
|
let rv = s.parse::<IconRefStr>();
|
||||||
|
assert!(rv.is_ok());
|
||||||
|
let rv = rv.unwrap();
|
||||||
|
assert_eq!(rv.get_path(), probe.0);
|
||||||
|
assert_eq!(rv.get_index(), probe.1);
|
||||||
|
}
|
||||||
|
fn err_tester(s: &str) {
|
||||||
|
let rv = s.parse::<IconRefStr>();
|
||||||
|
assert!(rv.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
ok_tester(
|
||||||
|
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"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: String Reference String
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_str_ref_str() {
|
||||||
|
fn ok_tester(s: &str, probe: (&str, u32)) {
|
||||||
|
let rv = s.parse::<StrRefStr>();
|
||||||
|
assert!(rv.is_ok());
|
||||||
|
let rv = rv.unwrap();
|
||||||
|
assert_eq!(rv.get_path(), probe.0);
|
||||||
|
assert_eq!(rv.get_index(), probe.1);
|
||||||
|
}
|
||||||
|
fn err_tester(s: &str) {
|
||||||
|
let rv = s.parse::<StrRefStr>();
|
||||||
|
assert!(rv.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
ok_tester(
|
||||||
|
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"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Windows Resource
|
||||||
|
|
||||||
|
// region: Icon Resource
|
||||||
|
|
||||||
|
#[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())
|
||||||
|
}
|
||||||
|
fn err_tester(file: &str, index: u32) {
|
||||||
|
let icon = IconRc::new(file, index, IconSizeKind::Small);
|
||||||
|
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);
|
||||||
|
err_tester("this-executable-must-inexisting.exe", 114514);
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: String Resource
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_str_rc() {
|
||||||
|
fn ok_tester(file: &str, index: u32) {
|
||||||
|
let rv = StrRc::new(file, index);
|
||||||
|
assert!(rv.is_ok());
|
||||||
|
}
|
||||||
|
fn err_tester(file: &str, index: u32) {
|
||||||
|
let rv = StrRc::new(file, index);
|
||||||
|
assert!(rv.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
// We pick it from "jpegfile" ProgId
|
||||||
|
ok_tester("shell32.dll", 30596);
|
||||||
|
err_tester("this-executable-must-inexisting.exe", 114514);
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Expand String
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_expand_string() {
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 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#"{0}\System32\shell32.dll"#, "%SystemRoot%");
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: File Name
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_file_name() {
|
||||||
|
fn ok_tester(s: &str) {
|
||||||
|
let rv = FileName::from_str(s);
|
||||||
|
assert!(rv.is_ok());
|
||||||
|
let rv = rv.unwrap();
|
||||||
|
assert_eq!(s, rv.inner());
|
||||||
|
}
|
||||||
|
fn err_tester(s: &str) {
|
||||||
|
let rv = FileName::from_str(s);
|
||||||
|
assert!(rv.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
ok_tester("GoodExecutable.exe");
|
||||||
|
err_tester("");
|
||||||
|
err_tester("*.?xaml");
|
||||||
|
err_tester(r#"\\\lol///"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Verb
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verb() {
|
||||||
|
fn ok_tester(s: &str) {
|
||||||
|
let rv = Verb::from_str(s);
|
||||||
|
assert!(rv.is_ok());
|
||||||
|
let rv = rv.unwrap();
|
||||||
|
assert_eq!(s, rv.inner());
|
||||||
|
}
|
||||||
|
fn err_tester(s: &str) {
|
||||||
|
let rv = Verb::from_str(s);
|
||||||
|
assert!(rv.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
ok_tester("Open");
|
||||||
|
ok_tester("open");
|
||||||
|
err_tester("Space Name");
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Command Line
|
||||||
|
|
||||||
|
#[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());
|
||||||
|
}
|
||||||
|
|
||||||
|
ok_tester("notepad.exe");
|
||||||
|
ok_tester("calc.exe /c");
|
||||||
|
ok_tester("unix-like.exe -i some.file --option");
|
||||||
|
// err_tester("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
@@ -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
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
pub fn add(left: u64, right: u64) -> u64 {
|
|
||||||
left + right
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
|
||||||
let result = add(2, 2);
|
|
||||||
assert_eq!(result, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
use clap::{Parser, Subcommand};
|
|
||||||
use comfy_table::Table;
|
|
||||||
use std::process;
|
|
||||||
use thiserror::Error as TeError;
|
|
||||||
use wfassoc::{Error as WfError, FileExt, Scope, View};
|
|
||||||
|
|
||||||
// region: Basic Types
|
|
||||||
|
|
||||||
/// All errors occurs in this executable.
|
|
||||||
#[derive(TeError, Debug)]
|
|
||||||
enum Error {
|
|
||||||
/// Error from wfassoc core.
|
|
||||||
#[error("{0}")]
|
|
||||||
Core(#[from] WfError),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Result type used in this executable.
|
|
||||||
type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Command Line Parser
|
|
||||||
|
|
||||||
/// Simple program to manage Windows file associations
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(name = "Windows File Association Operator", version, about)]
|
|
||||||
struct Cli {
|
|
||||||
/// The toml file introducing the complete program
|
|
||||||
#[arg(
|
|
||||||
short = 'c',
|
|
||||||
long = "config",
|
|
||||||
value_name = "PROG_CONFIG",
|
|
||||||
required = true
|
|
||||||
)]
|
|
||||||
config_file: String,
|
|
||||||
|
|
||||||
/// The scope where wfassoc operate
|
|
||||||
#[arg(short = 'f', long = "for", value_name = "TARGET", value_enum, default_value_t = ForTarget::User)]
|
|
||||||
for_which: ForTarget,
|
|
||||||
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: Commands,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(clap::ValueEnum, Clone)]
|
|
||||||
enum ForTarget {
|
|
||||||
#[value(name = "user")]
|
|
||||||
User,
|
|
||||||
#[value(name = "system")]
|
|
||||||
System,
|
|
||||||
}
|
|
||||||
|
|
||||||
// impl From<ForTarget> for RegisterKind {
|
|
||||||
// fn from(target: ForTarget) -> Self {
|
|
||||||
// match target {
|
|
||||||
// ForTarget::User => RegisterKind::User,
|
|
||||||
// ForTarget::System => RegisterKind::System,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
enum Commands {
|
|
||||||
/// Register the program
|
|
||||||
#[command(name = "register")]
|
|
||||||
Register,
|
|
||||||
/// Unregister the program
|
|
||||||
#[command(name = "unregister")]
|
|
||||||
Unregister,
|
|
||||||
/// Query file associations
|
|
||||||
#[command(name = "query")]
|
|
||||||
Query,
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Correponding Runner
|
|
||||||
|
|
||||||
fn run_register(cli: Cli) -> Result<()> {
|
|
||||||
// let program = Program::new();
|
|
||||||
// let kind: RegisterKind = cli.for_which.into();
|
|
||||||
// program.register(kind)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_unregister(cli: Cli) -> Result<()> {
|
|
||||||
// let program = Program::new();
|
|
||||||
// program.unregister()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_query(cli: Cli) -> Result<()> {
|
|
||||||
let exts = [
|
|
||||||
".jpg", ".jfif", ".gif", ".bmp", ".png", ".ico", ".jpeg", ".tif", ".tiff", ".webp", ".svg",
|
|
||||||
".kra", ".xcf", ".avif", ".qoi", ".apng", ".exr",
|
|
||||||
];
|
|
||||||
|
|
||||||
for ext in exts.iter().map(|e| FileExt::new(e).unwrap()) {
|
|
||||||
if let Some(ext_assoc) = ext.query(View::Hybrid) {
|
|
||||||
println!("{:?}", ext_assoc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// let mut table = Table::new();
|
|
||||||
// table.set_header(["Extension", "Default Open", "Open With"]);
|
|
||||||
// for ext in exts.iter().map(|e| FileExt::new(e).unwrap()) {
|
|
||||||
// if let Some(ext_assoc) = ext.query(View::Hybrid) {
|
|
||||||
// if ext_assoc.len_open_with_progid() == 0 {
|
|
||||||
// table.add_row([ext.to_string().as_str(), ext_assoc.get_default(), ""]);
|
|
||||||
// } else {
|
|
||||||
// for (i, open_with_entry) in ext_assoc.iter_open_with_progids().enumerate() {
|
|
||||||
// if i == 0 {
|
|
||||||
// table.add_row([
|
|
||||||
// ext.to_string().as_str(),
|
|
||||||
// ext_assoc.get_default(),
|
|
||||||
// open_with_entry,
|
|
||||||
// ]);
|
|
||||||
// } else {
|
|
||||||
// table.add_row(["", "", open_with_entry]);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// table.add_row([ext.to_string().as_str(), "", ""]);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// println!("{table}");
|
|
||||||
|
|
||||||
// let program = Program::new();
|
|
||||||
// program.query()?;
|
|
||||||
//println!("Has privilege: {}", wfassoc::has_privilege());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let cli = Cli::parse();
|
|
||||||
|
|
||||||
let rv = match &cli.command {
|
|
||||||
Commands::Register => run_register(cli),
|
|
||||||
Commands::Unregister => run_unregister(cli),
|
|
||||||
Commands::Query => run_query(cli),
|
|
||||||
};
|
|
||||||
rv.unwrap_or_else(|e| {
|
|
||||||
eprintln!("Runtime error: {}.", e);
|
|
||||||
process::exit(1)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user